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
This commit is contained in:
Dave Borowitz 2015-11-03 13:12:41 -05:00
parent 74317d4a07
commit c916b9e6a9
9 changed files with 475 additions and 277 deletions

View File

@ -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',

View File

@ -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<Path, Resource> 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;
}
}

View File

@ -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<String> 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("<html><title>BUILD FAILED</title><body>");
w.format("<h1>%s FAILED</h1>", html.escape(rule));
w.write("<pre>");
w.write(html.escape(RawParseUtils.decode(why)));
w.write("</pre>");
w.write("</body></html>");
}
}
@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<? extends ZipEntry> 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();
}
}
}

View File

@ -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<Resource> 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);
}
}

View File

@ -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<Path, Resource> 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;
}
}
}

View File

@ -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<Path, Resource> 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;
}
}

View File

@ -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:',
],

View File

@ -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.<String, String> emptyMap());
myArchive = result;
}
}
return myArchive;
return result;
}
public static FileSystem getDistributionArchiveFileSystem()
throws FileNotFoundException, IOException {
getDistributionArchive();
return myArchiveFs;
}
private static File locateMyArchive() throws FileNotFoundException {

View File

@ -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<String> 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<? extends ZipEntry> 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<String> 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("<html><title>BUILD FAILED</title><body>");
w.format("<h1>%s FAILED</h1>", html.escape(rule));
w.write("<pre>");
w.write(html.escape(RawParseUtils.decode(why)));
w.write("</pre>");
w.write("</body></html>");
}
}
@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;
}
}
}