diff --git a/gerrit-httpd/BUCK b/gerrit-httpd/BUCK index b29bd2ac48..da224d5d01 100644 --- a/gerrit-httpd/BUCK +++ b/gerrit-httpd/BUCK @@ -12,7 +12,9 @@ java_library( '//gerrit-common:annotations', '//gerrit-common:server', '//gerrit-extension-api:api', + '//gerrit-gwtexpui:linker_server', '//gerrit-gwtexpui:server', + '//gerrit-launcher:launcher', '//gerrit-patch-jgit:server', '//gerrit-prettify:server', '//gerrit-reviewdb:server', diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/DeveloperGwtUiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/DeveloperGwtUiServlet.java new file mode 100644 index 0000000000..9d57b17b57 --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/DeveloperGwtUiServlet.java @@ -0,0 +1,51 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.httpd.raw; + +import com.google.common.cache.Cache; +import com.google.gerrit.common.TimeUtil; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; + +class DeveloperGwtUiServlet extends ResourceServlet { + private static final long serialVersionUID = 1L; + + private static final FileTime NOW = FileTime.fromMillis(TimeUtil.nowMs()); + + private final Path ui; + + DeveloperGwtUiServlet(Cache cache, Path unpackedWar) + throws IOException { + super(cache, false); + ui = unpackedWar.resolve("gerrit_ui"); + Files.createDirectory(ui); + ui.toFile().deleteOnExit(); + } + + @Override + protected Path getResourcePath(String pathInfo) { + return ui.resolve(pathInfo); + } + + @Override + protected FileTime getLastModifiedTime(Path p) { + // Return initialization time of this class, since the GWT outputs from the + // build process all have mtimes of 1980/1/1. + return NOW; + } +} diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java new file mode 100644 index 0000000000..a5bc6c661f --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java @@ -0,0 +1,231 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.httpd.raw; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.base.MoreObjects; +import com.google.common.escape.Escaper; +import com.google.common.html.HtmlEscapers; +import com.google.common.io.ByteStreams; +import com.google.gerrit.common.TimeUtil; +import com.google.gwtexpui.linker.server.UserAgentRule; +import com.google.gwtexpui.server.CacheHeaders; + +import org.eclipse.jgit.util.RawParseUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.PrintWriter; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +class RecompileGwtUiFilter implements Filter { + private static final Logger log = + LoggerFactory.getLogger(RecompileGwtUiFilter.class); + + private final boolean gwtuiRecompile = + System.getProperty("gerrit.disable-gwtui-recompile") == null; + private final UserAgentRule rule = new UserAgentRule(); + private final Set uaInitialized = new HashSet<>(); + private final Path unpackedWar; + private final Path gen; + private final Path root; + + private String lastTarget; + private long lastTime; + + RecompileGwtUiFilter(Path buckOut, Path unpackedWar) { + this.unpackedWar = unpackedWar; + gen = buckOut.resolve("gen"); + root = buckOut.getParent(); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse res, + FilterChain chain) throws IOException, ServletException { + String pkg = "gerrit-gwtui"; + String target = "ui_" + rule.select((HttpServletRequest) request); + if (gwtuiRecompile || !uaInitialized.contains(target)) { + String rule = "//" + pkg + ":" + target; + // TODO(davido): instead of assuming specific Buck's internal + // target directory for gwt_binary() artifacts, ask Buck for + // the location of user agent permutation GWT zip, e. g.: + // $ buck targets --show_output //gerrit-gwtui:ui_safari \ + // | awk '{print $2}' + String child = String.format("%s/__gwt_binary_%s__", pkg, target); + File zip = gen.resolve(child).resolve(target + ".zip").toFile(); + + synchronized (this) { + try { + build(root, gen, rule); + } catch (BuildFailureException e) { + displayFailure(rule, e.why, (HttpServletResponse) res); + return; + } + + if (!target.equals(lastTarget) || lastTime != zip.lastModified()) { + lastTarget = target; + lastTime = zip.lastModified(); + unpack(zip, unpackedWar.toFile()); + } + } + uaInitialized.add(target); + } + chain.doFilter(request, res); + } + + private void displayFailure(String rule, byte[] why, HttpServletResponse res) + throws IOException { + res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + res.setContentType("text/html"); + res.setCharacterEncoding(UTF_8.name()); + CacheHeaders.setNotCacheable(res); + + Escaper html = HtmlEscapers.htmlEscaper(); + try (PrintWriter w = res.getWriter()) { + w.write("BUILD FAILED"); + w.format("

%s FAILED

", html.escape(rule)); + w.write("
");
+      w.write(html.escape(RawParseUtils.decode(why)));
+      w.write("
"); + w.write(""); + } + } + + @Override + public void init(FilterConfig config) { + } + + @Override + public void destroy() { + } + + private static void unpack(File srcwar, File dstwar) throws IOException { + try (ZipFile zf = new ZipFile(srcwar)) { + final Enumeration e = zf.entries(); + while (e.hasMoreElements()) { + final ZipEntry ze = e.nextElement(); + final String name = ze.getName(); + + if (ze.isDirectory() + || name.startsWith("WEB-INF/") + || name.startsWith("META-INF/") + || name.startsWith("com/google/gerrit/launcher/") + || name.equals("Main.class")) { + continue; + } + + final File rawtmp = new File(dstwar, name); + mkdir(rawtmp.getParentFile()); + rawtmp.deleteOnExit(); + + try (FileOutputStream rawout = new FileOutputStream(rawtmp); + InputStream in = zf.getInputStream(ze)) { + final byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf, 0, buf.length)) > 0) { + rawout.write(buf, 0, n); + } + } + } + } + } + + private static void build(Path root, Path gen, String target) + throws IOException, BuildFailureException { + log.info("buck build " + target); + Properties properties = loadBuckProperties(gen); + String buck = MoreObjects.firstNonNull(properties.getProperty("buck"), "buck"); + ProcessBuilder proc = new ProcessBuilder(buck, "build", target) + .directory(root.toFile()) + .redirectErrorStream(true); + if (properties.containsKey("PATH")) { + proc.environment().put("PATH", properties.getProperty("PATH")); + } + long start = TimeUtil.nowMs(); + Process rebuild = proc.start(); + byte[] out; + try (InputStream in = rebuild.getInputStream()) { + out = ByteStreams.toByteArray(in); + } finally { + rebuild.getOutputStream().close(); + } + + int status; + try { + status = rebuild.waitFor(); + } catch (InterruptedException e) { + throw new InterruptedIOException("interrupted waiting for " + buck); + } + if (status != 0) { + throw new BuildFailureException(out); + } + + long time = TimeUtil.nowMs() - start; + log.info(String.format("UPDATED %s in %.3fs", target, time / 1000.0)); + } + + private static Properties loadBuckProperties(Path gen) + throws FileNotFoundException, IOException { + Properties properties = new Properties(); + try (InputStream in = new FileInputStream( + gen.resolve(Paths.get("tools/buck/buck.properties")).toFile())) { + properties.load(in); + } + return properties; + } + + @SuppressWarnings("serial") + private static class BuildFailureException extends Exception { + final byte[] why; + + BuildFailureException(byte[] why) { + this.why = why; + } + } + + private static void mkdir(File dir) throws IOException { + if (!dir.isDirectory()) { + mkdir(dir.getParentFile()); + if (!dir.mkdir()) { + throw new IOException("Cannot mkdir " + dir.getAbsolutePath()); + } + dir.deleteOnExit(); + } + } +} diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java index e6ad1b3795..a804e2a5d7 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java @@ -111,6 +111,10 @@ public abstract class ResourceServlet extends HttpServlet { */ protected abstract Path getResourcePath(String pathInfo); + protected FileTime getLastModifiedTime(Path p) throws IOException { + return Files.getLastModifiedTime(p); + } + @Override protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { @@ -134,11 +138,11 @@ public abstract class ResourceServlet extends HttpServlet { Callable loader = newLoader(p); try { r = cache.get(p, loader); - if (refresh && r.isStale(p)) { + if (refresh && r.isStale(p, this)) { cache.invalidate(p); r = cache.get(p, loader); } - } catch (ExecutionException e) { + } catch (ExecutionException | IOException e) { log.warn("Cannot load static resource " + req.getPathInfo(), e); CacheHeaders.setNotCacheable(rsp); rsp.setStatus(SC_INTERNAL_SERVER_ERROR); @@ -266,7 +270,7 @@ public abstract class ResourceServlet extends HttpServlet { public Resource call() throws IOException { try { return new Resource( - Files.getLastModifiedTime(p), + getLastModifiedTime(p), contentType(p.toString()), Files.readAllBytes(p)); } catch (NoSuchFileException e) { @@ -292,12 +296,11 @@ public abstract class ResourceServlet extends HttpServlet { this.etag = Hashing.md5().hashBytes(raw).toString(); } - boolean isStale(Path p) { - try { - return !lastModified.equals(Files.getLastModifiedTime(p)); - } catch (IOException e) { - return true; - } + boolean isStale(Path p, ResourceServlet rs) throws IOException { + FileTime t = rs.getLastModifiedTime(p); + return t.toMillis() == 0 + || lastModified.toMillis() == 0 + || !lastModified.equals(t); } } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java index 12fe177629..e18afa07de 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java @@ -14,18 +14,49 @@ package com.google.gerrit.httpd.raw; +import com.google.common.cache.Cache; import com.google.gerrit.httpd.raw.ResourceServlet.Resource; +import com.google.gerrit.launcher.GerritLauncher; import com.google.gerrit.server.cache.CacheModule; +import com.google.inject.Key; +import com.google.inject.Provides; +import com.google.inject.ProvisionException; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import com.google.inject.name.Names; import com.google.inject.servlet.ServletModule; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.FileSystem; import java.nio.file.Path; +import javax.servlet.http.HttpServlet; + public class StaticModule extends ServletModule { + private static final String GWT_UI_SERVLET = "GwtUiServlet"; static final String CACHE = "static_content"; + private final FileSystem warFs; + private final Path buckOut; + private final Path unpackedWar; + + public StaticModule() { + warFs = getDistributionArchive(); + if (warFs == null) { + buckOut = getDeveloperBuckOut(); + unpackedWar = makeWarTempDir(); + } else { + buckOut = null; + unpackedWar = null; + } + } + @Override protected void configureServlets() { serve("/static/*").with(SiteStaticDirectoryServlet.class); + serveGwtUi(); install(new CacheModule() { @Override protected void configure() { @@ -35,4 +66,74 @@ public class StaticModule extends ServletModule { } }); } + + private void serveGwtUi() { + serve("/gerrit_ui/*") + .with(Key.get(HttpServlet.class, Names.named(GWT_UI_SERVLET))); + if (warFs == null) { + filter("/").through(new RecompileGwtUiFilter(buckOut, unpackedWar)); + } + } + + @Provides + @Singleton + @Named(GWT_UI_SERVLET) + HttpServlet getGwtUiServlet(@Named(CACHE) Cache cache) + throws IOException { + if (warFs != null) { + return new WarGwtUiServlet(cache, warFs); + } else { + return new DeveloperGwtUiServlet(cache, unpackedWar); + } + } + + private static FileSystem getDistributionArchive() { + try { + return GerritLauncher.getDistributionArchiveFileSystem(); + } catch (IOException e) { + if ((e instanceof FileNotFoundException) + && GerritLauncher.NOT_ARCHIVED.equals(e.getMessage())) { + return null; + } else { + ProvisionException pe = + new ProvisionException("Error reading gerrit.war"); + pe.initCause(e); + throw pe; + } + } + } + + private static Path getDeveloperBuckOut() { + try { + return GerritLauncher.getDeveloperBuckOut(); + } catch (FileNotFoundException e) { + return null; + } + } + + private static Path makeWarTempDir() { + // Obtain our local temporary directory, but it comes back as a file + // so we have to switch it to be a directory post creation. + // + try { + File dstwar = GerritLauncher.createTempFile("gerrit_", "war"); + if (!dstwar.delete() || !dstwar.mkdir()) { + throw new IOException("Cannot mkdir " + dstwar.getAbsolutePath()); + } + + // Jetty normally refuses to serve out of a symlinked directory, as + // a security feature. Try to resolve out any symlinks in the path. + // + try { + return dstwar.getCanonicalFile().toPath(); + } catch (IOException e) { + return dstwar.getAbsoluteFile().toPath(); + } + } catch (IOException e) { + ProvisionException pe = + new ProvisionException("Cannot create war tempdir"); + pe.initCause(e); + throw pe; + } + } } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarGwtUiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarGwtUiServlet.java new file mode 100644 index 0000000000..45952cc41e --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarGwtUiServlet.java @@ -0,0 +1,47 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.httpd.raw; + +import com.google.common.cache.Cache; +import com.google.gerrit.common.TimeUtil; + +import java.nio.file.FileSystem; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; + +class WarGwtUiServlet extends ResourceServlet { + private static final long serialVersionUID = 1L; + + private static final FileTime NOW = FileTime.fromMillis(TimeUtil.nowMs()); + + private final FileSystem warFs; + + WarGwtUiServlet(Cache cache, FileSystem warFs) { + super(cache, false); + this.warFs = warFs; + } + + @Override + protected Path getResourcePath(String pathInfo) { + return warFs.getPath("/gerrit_ui/" + pathInfo); + } + + @Override + protected FileTime getLastModifiedTime(Path p) { + // Return initialization time of this class, since the GWT outputs from the + // build process all have mtimes of 1980/1/1. + return NOW; + } +} diff --git a/gerrit-launcher/BUCK b/gerrit-launcher/BUCK index 6281a1c0f1..687e02fd4e 100644 --- a/gerrit-launcher/BUCK +++ b/gerrit-launcher/BUCK @@ -5,6 +5,7 @@ java_library( srcs = ['src/main/java/com/google/gerrit/launcher/GerritLauncher.java'], visibility = [ '//gerrit-acceptance-tests/...', + '//gerrit-httpd:', '//gerrit-main:main_lib', '//gerrit-pgm:', ], diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java index 89c8ec6cd4..fb54bcffd2 100644 --- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java +++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java @@ -27,12 +27,16 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.JarURLConnection; import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import java.net.URLClassLoader; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Paths; import java.security.CodeSource; import java.util.ArrayList; +import java.util.Collections; import java.util.Enumeration; import java.util.List; import java.util.SortedMap; @@ -296,6 +300,7 @@ public final class GerritLauncher { } private static volatile File myArchive; + private static volatile FileSystem myArchiveFs; private static volatile File myHome; /** @@ -304,11 +309,29 @@ public final class GerritLauncher { * @return local path of the Gerrit WAR file. * @throws FileNotFoundException if the code cannot guess the location. */ - public static File getDistributionArchive() throws FileNotFoundException { - if (myArchive == null) { - myArchive = locateMyArchive(); + public static File getDistributionArchive() + throws FileNotFoundException, IOException { + File result = myArchive; + if (result == null) { + synchronized (GerritLauncher.class) { + result = myArchive; + if (result != null) { + return result; + } + result = locateMyArchive(); + myArchiveFs = FileSystems.newFileSystem( + URI.create("jar:" + result.toPath().toUri()), + Collections. emptyMap()); + myArchive = result; + } } - return myArchive; + return result; + } + + public static FileSystem getDistributionArchiveFileSystem() + throws FileNotFoundException, IOException { + getDistributionArchive(); + return myArchiveFs; } private static File locateMyArchive() throws FileNotFoundException { diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java index 25b351efa6..0684650c65 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java @@ -14,24 +14,15 @@ package com.google.gerrit.pgm.http.jetty; -import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; -import com.google.common.base.MoreObjects; import com.google.common.base.Strings; -import com.google.common.escape.Escaper; -import com.google.common.html.HtmlEscapers; -import com.google.common.io.ByteStreams; -import com.google.gerrit.common.TimeUtil; import com.google.gerrit.extensions.events.LifecycleListener; -import com.google.gerrit.launcher.GerritLauncher; import com.google.gerrit.pgm.http.jetty.HttpLog.HttpLogFactory; import com.google.gerrit.reviewdb.client.AuthType; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.SitePaths; -import com.google.gwtexpui.linker.server.UserAgentRule; -import com.google.gwtexpui.server.CacheHeaders; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Singleton; @@ -60,49 +51,26 @@ import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.BlockingArrayQueue; import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.ThreadPool; import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.util.RawParseUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InterruptedIOException; -import java.io.PrintWriter; import java.lang.management.ManagementFactory; -import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.EnumSet; -import java.util.Enumeration; import java.util.HashSet; import java.util.List; -import java.util.Properties; import java.util.Set; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; import javax.servlet.DispatcherType; import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; @Singleton public class JettyServer { @@ -158,13 +126,9 @@ public class JettyServer { private boolean reverseProxy; - /** Location on disk where our WAR file was unpacked to. */ - private Resource baseResource; - @Inject JettyServer(@GerritServerConfig final Config cfg, final SitePaths site, - final JettyEnv env, final HttpLogFactory httpLogFactory) - throws MalformedURLException, IOException { + final JettyEnv env, final HttpLogFactory httpLogFactory) { this.site = site; httpd = new Server(threadPool(cfg)); @@ -372,8 +336,7 @@ public class JettyServer { return pool; } - private Handler makeContext(final JettyEnv env, final Config cfg) - throws MalformedURLException, IOException { + private Handler makeContext(final JettyEnv env, final Config cfg) { final Set paths = new HashSet<>(); for (URI u : listenURLs(cfg)) { String p = u.getPath(); @@ -408,7 +371,7 @@ public class JettyServer { } private ContextHandler makeContext(final String contextPath, - final JettyEnv env, final Config cfg) throws MalformedURLException, IOException { + final JettyEnv env, final Config cfg) { final ServletContextHandler app = new ServletContextHandler(); // This enables the use of sessions in Jetty, feature available @@ -421,12 +384,6 @@ public class JettyServer { // app.setContextPath(contextPath); - // Serve static resources directly from our JAR. This way we don't - // need to unpack them into yet another temporary directory prior to - // serving to clients. - // - app.setBaseResource(getBaseResource(app)); - // HTTP front-end filter to be used as surrogate of Apache HTTP // reverse-proxy filtering. // It is meant to be used as simpler tiny deployment of custom-made @@ -478,222 +435,4 @@ public class JettyServer { app.setWelcomeFiles(new String[0]); return app; } - - private Resource getBaseResource(ServletContextHandler app) - throws IOException { - if (baseResource == null) { - try { - baseResource = unpackWar(GerritLauncher.getDistributionArchive()); - } catch (FileNotFoundException err) { - if (GerritLauncher.NOT_ARCHIVED.equals(err.getMessage())) { - baseResource = useDeveloperBuild(app); - } else { - throw err; - } - } - } - return baseResource; - } - - private static Resource unpackWar(File srcwar) throws IOException { - File dstwar = makeWarTempDir(); - unpack(srcwar, dstwar); - return Resource.newResource(dstwar.toURI()); - } - - private static File makeWarTempDir() throws IOException { - // Obtain our local temporary directory, but it comes back as a file - // so we have to switch it to be a directory post creation. - // - File dstwar = GerritLauncher.createTempFile("gerrit_", "war"); - if (!dstwar.delete() || !dstwar.mkdir()) { - throw new IOException("Cannot mkdir " + dstwar.getAbsolutePath()); - } - - // Jetty normally refuses to serve out of a symlinked directory, as - // a security feature. Try to resolve out any symlinks in the path. - // - try { - return dstwar.getCanonicalFile(); - } catch (IOException e) { - return dstwar.getAbsoluteFile(); - } - } - - private static void unpack(File srcwar, File dstwar) throws IOException { - try (ZipFile zf = new ZipFile(srcwar)) { - final Enumeration e = zf.entries(); - while (e.hasMoreElements()) { - final ZipEntry ze = e.nextElement(); - final String name = ze.getName(); - - if (ze.isDirectory() - || name.startsWith("WEB-INF/") - || name.startsWith("META-INF/") - || name.startsWith("com/google/gerrit/launcher/") - || name.equals("Main.class")) { - continue; - } - - final File rawtmp = new File(dstwar, name); - mkdir(rawtmp.getParentFile()); - rawtmp.deleteOnExit(); - - try (FileOutputStream rawout = new FileOutputStream(rawtmp); - InputStream in = zf.getInputStream(ze)) { - final byte[] buf = new byte[4096]; - int n; - while ((n = in.read(buf, 0, buf.length)) > 0) { - rawout.write(buf, 0, n); - } - } - } - } - } - - private static void mkdir(File dir) throws IOException { - if (!dir.isDirectory()) { - mkdir(dir.getParentFile()); - if (!dir.mkdir()) { - throw new IOException("Cannot mkdir " + dir.getAbsolutePath()); - } - dir.deleteOnExit(); - } - } - - private Resource useDeveloperBuild(ServletContextHandler app) - throws IOException { - final Path dir = GerritLauncher.getDeveloperBuckOut(); - final Path gen = dir.resolve("gen"); - final Path root = dir.getParent(); - final File dstwar = makeWarTempDir(); - File ui = new File(dstwar, "gerrit_ui"); - File p = new File(ui, "permutations"); - mkdir(ui); - p.createNewFile(); - p.deleteOnExit(); - - app.addFilter(new FilterHolder(new Filter() { - private final boolean gwtuiRecompile = - System.getProperty("gerrit.disable-gwtui-recompile") == null; - private final UserAgentRule rule = new UserAgentRule(); - private final Set uaInitialized = new HashSet<>(); - private String lastTarget; - private long lastTime; - - @Override - public void doFilter(ServletRequest request, ServletResponse res, - FilterChain chain) throws IOException, ServletException { - String pkg = "gerrit-gwtui"; - String target = "ui_" + rule.select((HttpServletRequest) request); - if (gwtuiRecompile || !uaInitialized.contains(target)) { - String rule = "//" + pkg + ":" + target; - // TODO(davido): instead of assuming specific Buck's internal - // target directory for gwt_binary() artifacts, ask Buck for - // the location of user agent permutation GWT zip, e. g.: - // $ buck targets --show_output //gerrit-gwtui:ui_safari \ - // | awk '{print $2}' - String child = String.format("%s/__gwt_binary_%s__", pkg, target); - File zip = gen.resolve(child).resolve(target + ".zip").toFile(); - - synchronized (this) { - try { - build(root, gen, rule); - } catch (BuildFailureException e) { - displayFailure(rule, e.why, (HttpServletResponse) res); - return; - } - - if (!target.equals(lastTarget) || lastTime != zip.lastModified()) { - lastTarget = target; - lastTime = zip.lastModified(); - unpack(zip, dstwar); - } - } - uaInitialized.add(target); - } - chain.doFilter(request, res); - } - - private void displayFailure(String rule, byte[] why, HttpServletResponse res) - throws IOException { - res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - res.setContentType("text/html"); - res.setCharacterEncoding(UTF_8.name()); - CacheHeaders.setNotCacheable(res); - - Escaper html = HtmlEscapers.htmlEscaper(); - try (PrintWriter w = res.getWriter()) { - w.write("BUILD FAILED"); - w.format("

%s FAILED

", html.escape(rule)); - w.write("
");
-          w.write(html.escape(RawParseUtils.decode(why)));
-          w.write("
"); - w.write(""); - } - } - - @Override - public void init(FilterConfig config) { - } - - @Override - public void destroy() { - } - }), "/", EnumSet.of(DispatcherType.REQUEST)); - return Resource.newResource(dstwar.toURI()); - } - - private static void build(Path root, Path gen, String target) - throws IOException, BuildFailureException { - log.info("buck build " + target); - Properties properties = loadBuckProperties(gen); - String buck = MoreObjects.firstNonNull(properties.getProperty("buck"), "buck"); - ProcessBuilder proc = new ProcessBuilder(buck, "build", target) - .directory(root.toFile()) - .redirectErrorStream(true); - if (properties.containsKey("PATH")) { - proc.environment().put("PATH", properties.getProperty("PATH")); - } - long start = TimeUtil.nowMs(); - Process rebuild = proc.start(); - byte[] out; - try (InputStream in = rebuild.getInputStream()) { - out = ByteStreams.toByteArray(in); - } finally { - rebuild.getOutputStream().close(); - } - - int status; - try { - status = rebuild.waitFor(); - } catch (InterruptedException e) { - throw new InterruptedIOException("interrupted waiting for " + buck); - } - if (status != 0) { - throw new BuildFailureException(out); - } - - long time = TimeUtil.nowMs() - start; - log.info(String.format("UPDATED %s in %.3fs", target, time / 1000.0)); - } - - private static Properties loadBuckProperties(Path gen) - throws FileNotFoundException, IOException { - Properties properties = new Properties(); - try (InputStream in = new FileInputStream( - gen.resolve(Paths.get("tools/buck/buck.properties")).toFile())) { - properties.load(in); - } - return properties; - } - - @SuppressWarnings("serial") - private static class BuildFailureException extends Exception { - final byte[] why; - - BuildFailureException(byte[] why) { - this.why = why; - } - } }