From c916b9e6a91864024185b820ccede36546805794 Mon Sep 17 00:00:00 2001 From: Dave Borowitz Date: Tue, 3 Nov 2015 13:12:41 -0500 Subject: [PATCH] Serve GWT UI from ResourceServlet We already have a somewhat-featureful static content servlet for serving data from /static; use it for the GWT UI as well. Java's zip filesystem support makes the war case easy; we don't have to do the extract-to-a-directory hack that makes it work with Jetty. The developer case is also pretty easy, though we have to move the filter to recompile the GWT UI into the httpd package. One other wrinkle is that the GWT build process puts bogus timestamps on the GWT compiler output, so we need to pretend the timestamps on all the files are the startup time of the server. This means clients will have to re-download large identical JS assets after a server restart even if the assets didn't change. Gerrit has mostly pretty good uptime so this is not a huge deal. Change-Id: I0a7ade3cadf3a4a4e1726b56b87b0cbe4c6e0c93 --- gerrit-httpd/BUCK | 2 + .../httpd/raw/DeveloperGwtUiServlet.java | 51 ++++ .../httpd/raw/RecompileGwtUiFilter.java | 231 +++++++++++++++ .../gerrit/httpd/raw/ResourceServlet.java | 21 +- .../google/gerrit/httpd/raw/StaticModule.java | 101 +++++++ .../gerrit/httpd/raw/WarGwtUiServlet.java | 47 +++ gerrit-launcher/BUCK | 1 + .../gerrit/launcher/GerritLauncher.java | 31 +- .../gerrit/pgm/http/jetty/JettyServer.java | 267 +----------------- 9 files changed, 475 insertions(+), 277 deletions(-) create mode 100644 gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/DeveloperGwtUiServlet.java create mode 100644 gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java create mode 100644 gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarGwtUiServlet.java 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; - } - } }