Merge changes from topic 'resource-servlet'

* changes:
  Serve GWT UI from ResourceServlet
  ResourceServlet: Stream large files, bypassing the cache
  ResourceServlet: Respect existing cache headers
  Rename StaticServlet to SiteStaticDirectoryServlet
  Refactor static content serving
This commit is contained in:
Dave Borowitz
2015-11-09 21:09:17 +00:00
committed by Gerrit Code Review
14 changed files with 892 additions and 522 deletions

View File

@@ -128,6 +128,12 @@ public class CacheHeaders {
cache(res, "private", age, unit, mustRevalidate);
}
public static boolean hasCacheHeader(HttpServletResponse res) {
return res.getHeader("Cache-Control") != null
|| res.getHeader("Expires") != null
|| "no-cache".equals(res.getHeader("Pragma"));
}
private static void cache(HttpServletResponse res,
String type, long age, TimeUnit unit, boolean revalidate) {
res.setHeader("Cache-Control", String.format(

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

@@ -23,7 +23,7 @@ import com.google.gerrit.httpd.raw.HostPageServlet;
import com.google.gerrit.httpd.raw.LegacyGerritServlet;
import com.google.gerrit.httpd.raw.RobotsServlet;
import com.google.gerrit.httpd.raw.SshInfoServlet;
import com.google.gerrit.httpd.raw.StaticServlet;
import com.google.gerrit.httpd.raw.StaticModule;
import com.google.gerrit.httpd.raw.ToolServlet;
import com.google.gerrit.httpd.rpc.access.AccessRestApiServlet;
import com.google.gerrit.httpd.rpc.account.AccountsRestApiServlet;
@@ -77,7 +77,6 @@ class UrlModule extends ServletModule {
serve("/signout").with(HttpLogoutServlet.class);
}
serve("/ssh_info").with(SshInfoServlet.class);
serve("/static/*").with(StaticServlet.class);
serve("/Main.class").with(notFound());
serve("/com/google/gerrit/launcher/*").with(notFound());
@@ -107,6 +106,8 @@ class UrlModule extends ServletModule {
filter("/Documentation/").through(QueryDocumentationFilter.class);
serve("/robots.txt").with(RobotsServlet.class);
install(new StaticModule());
}
private Key<HttpServlet> notFound() {

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

@@ -92,7 +92,7 @@ public class HostPageServlet extends HttpServlet {
private final Document template;
private final String noCacheName;
private final boolean refreshHeaderFooter;
private final StaticServlet staticServlet;
private final SiteStaticDirectoryServlet staticServlet;
private final boolean isNoteDbEnabled;
private final Integer pluginsLoadTimeout;
private final GetDiffPreferences getDiff;
@@ -108,7 +108,7 @@ public class HostPageServlet extends HttpServlet {
DynamicSet<WebUiPlugin> webUiPlugins,
DynamicSet<MessageOfTheDay> motd,
@GerritServerConfig Config cfg,
StaticServlet ss,
SiteStaticDirectoryServlet ss,
NotesMigration migration,
GetDiffPreferences diffPref)
throws IOException, ServletException {
@@ -302,7 +302,7 @@ public class HostPageServlet extends HttpServlet {
String src = e.getAttribute("src");
if (src != null && src.startsWith("static/")) {
String name = src.substring("static/".length());
StaticServlet.Resource r = staticServlet.getResource(name);
ResourceServlet.Resource r = staticServlet.getResource(name);
if (r != null) {
e.setAttribute("src", src + "?e=" + r.etag);
}

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

@@ -0,0 +1,314 @@
// 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 com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.net.HttpHeaders.CONTENT_ENCODING;
import static com.google.common.net.HttpHeaders.ETAG;
import static com.google.common.net.HttpHeaders.IF_MODIFIED_SINCE;
import static com.google.common.net.HttpHeaders.IF_NONE_MATCH;
import static com.google.common.net.HttpHeaders.LAST_MODIFIED;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
import com.google.common.base.CharMatcher;
import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableMap;
import com.google.common.hash.Hashing;
import com.google.gerrit.common.FileUtil;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.httpd.HtmlDomUtil;
import com.google.gwtexpui.server.CacheHeaders;
import com.google.gwtjsonrpc.server.RPCServletUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.zip.GZIPOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Base class for serving static resources.
* <p>
* Supports caching, ETags, basic content type detection, and limited gzip
* compression.
*/
public abstract class ResourceServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final Logger log =
LoggerFactory.getLogger(ResourceServlet.class);
private static final int CACHE_FILE_SIZE_LIMIT_BYTES = 100 << 10;
private static final String JS = "application/x-javascript";
private static final ImmutableMap<String, String> MIME_TYPES =
ImmutableMap.<String, String> builder()
.put("css", "text/css")
.put("gif", "image/gif")
.put("htm", "text/html")
.put("html", "text/html")
.put("jpeg", "image/jpeg")
.put("jpg", "image/jpeg")
.put("js", JS)
.put("pdf", "application/pdf")
.put("png", "image/png")
.put("rtf", "text/rtf")
.put("svg", "image/svg+xml")
.put("text", "text/plain")
.put("tif", "image/tiff")
.put("tiff", "image/tiff")
.put("txt", "text/plain")
.build();
protected static String contentType(String name) {
int dot = name.lastIndexOf('.');
String ext = 0 < dot ? name.substring(dot + 1) : "";
String type = MIME_TYPES.get(ext);
return type != null ? type : "application/octet-stream";
}
private final Cache<Path, Resource> cache;
private final boolean refresh;
protected ResourceServlet(Cache<Path, Resource> cache, boolean refresh) {
this.cache = checkNotNull(cache, "cache");
this.refresh = refresh;
}
/**
* Get the resource path on the filesystem that should be served for this
* request.
*
* @param pathInfo result of {@link HttpServletRequest#getPathInfo()}.
* @return path where static content can be found.
*/
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 {
String name = CharMatcher.is('/').trimFrom(req.getPathInfo());
if (isUnreasonableName(name)) {
notFound(rsp);
return;
}
Path p = getResourcePath(name);
if (p == null) {
notFound(rsp);
return;
}
Resource r = cache.getIfPresent(p);
if (r == null && maybeStream(p, req, rsp)) {
return;
}
if (r == null) {
Callable<Resource> loader = newLoader(p);
try {
r = cache.get(p, loader);
if (refresh && r.isStale(p, this)) {
cache.invalidate(p);
r = cache.get(p, loader);
}
} catch (ExecutionException | IOException e) {
log.warn("Cannot load static resource " + req.getPathInfo(), e);
CacheHeaders.setNotCacheable(rsp);
rsp.setStatus(SC_INTERNAL_SERVER_ERROR);
return;
}
}
if (r == Resource.NOT_FOUND) {
notFound(rsp);
return;
}
String e = req.getParameter("e");
if (e != null && !r.etag.equals(e)) {
CacheHeaders.setNotCacheable(rsp);
rsp.setStatus(SC_NOT_FOUND);
return;
} else if (r.etag.equals(req.getHeader(IF_NONE_MATCH))) {
rsp.setStatus(SC_NOT_MODIFIED);
return;
}
byte[] tosend = r.raw;
if (!r.contentType.equals(JS) && RPCServletUtils.acceptsGzipEncoding(req)) {
byte[] gz = HtmlDomUtil.compress(tosend);
if ((gz.length + 24) < tosend.length) {
rsp.setHeader(CONTENT_ENCODING, "gzip");
tosend = gz;
}
}
if (!CacheHeaders.hasCacheHeader(rsp)) {
if (e != null && r.etag.equals(e)) {
CacheHeaders.setCacheable(req, rsp, 360, DAYS, false);
} else {
CacheHeaders.setCacheable(req, rsp, 15, MINUTES, refresh);
}
}
rsp.setHeader(ETAG, r.etag);
rsp.setContentType(r.contentType);
rsp.setContentLength(tosend.length);
try (OutputStream out = rsp.getOutputStream()) {
out.write(tosend);
}
}
@Nullable
Resource getResource(String name) {
try {
Path p = getResourcePath(name);
return cache.get(p, newLoader(p));
} catch (ExecutionException e) {
log.warn(String.format("Cannot load static resource %s", name), e);
return null;
}
}
private static void notFound(HttpServletResponse rsp) {
rsp.setStatus(SC_NOT_FOUND);
CacheHeaders.setNotCacheable(rsp);
}
/**
* Maybe stream a path to the response, depending on the properties of the
* file and cache headers in the request.
*
* @param p path to stream
* @param req HTTP request.
* @param rsp HTTP response.
* @return true if the response was written (either the file contents or an
* error); false if the path is too small to stream and should be cached.
*/
private boolean maybeStream(Path p, HttpServletRequest req,
HttpServletResponse rsp) throws IOException {
try {
if (Files.size(p) < CACHE_FILE_SIZE_LIMIT_BYTES) {
return false;
}
} catch (NoSuchFileException e) {
cache.put(p, Resource.NOT_FOUND);
notFound(rsp);
return true;
}
long lastModified = FileUtil.lastModified(p);
if (req.getDateHeader(IF_MODIFIED_SINCE) >= lastModified) {
rsp.setStatus(SC_NOT_MODIFIED);
return true;
}
if (lastModified > 0) {
rsp.setDateHeader(LAST_MODIFIED, lastModified);
}
if (!CacheHeaders.hasCacheHeader(rsp)) {
CacheHeaders.setCacheable(req, rsp, 15, MINUTES, refresh);
}
rsp.setContentType(contentType(p.toString()));
OutputStream out = rsp.getOutputStream();
GZIPOutputStream gz = null;
if (RPCServletUtils.acceptsGzipEncoding(req)) {
rsp.setHeader(CONTENT_ENCODING, "gzip");
gz = new GZIPOutputStream(out);
out = gz;
}
Files.copy(p, out);
if (gz != null) {
gz.finish();
}
return true;
}
private static boolean isUnreasonableName(String name) {
return name.length() < 1
|| name.contains("\\") // no windows/dos style paths
|| name.startsWith("../") // no "../etc/passwd"
|| name.contains("/../") // no "foo/../etc/passwd"
|| name.contains("/./") // "foo/./foo" is insane to ask
|| name.contains("//"); // windows UNC path can be "//..."
}
private Callable<Resource> newLoader(final Path p) {
return new Callable<Resource>() {
@Override
public Resource call() throws IOException {
try {
return new Resource(
getLastModifiedTime(p),
contentType(p.toString()),
Files.readAllBytes(p));
} catch (NoSuchFileException e) {
return Resource.NOT_FOUND;
}
}
};
}
static class Resource {
static final Resource NOT_FOUND =
new Resource(FileTime.fromMillis(0), "", new byte[] {});
final FileTime lastModified;
final String contentType;
final String etag;
final byte[] raw;
Resource(FileTime lastModified, String contentType, byte[] raw) {
this.lastModified = checkNotNull(lastModified, "lastModified");
this.contentType = checkNotNull(contentType, "contentType");
this.raw = checkNotNull(raw, "raw");
this.etag = Hashing.md5().hashBytes(raw).toString();
}
boolean isStale(Path p, ResourceServlet rs) throws IOException {
FileTime t = rs.getLastModifiedTime(p);
return t.toMillis() == 0
|| lastModified.toMillis() == 0
|| !lastModified.equals(t);
}
}
static class Weigher
implements com.google.common.cache.Weigher<Path, Resource> {
@Override
public int weigh(Path p, Resource r) {
return 2 * p.toString().length() + r.raw.length;
}
}
}

View File

@@ -0,0 +1,65 @@
// Copyright (C) 2008 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.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import org.eclipse.jgit.lib.Config;
import java.io.IOException;
import java.nio.file.Path;
/** Sends static content from the site 's {@code static/} subdirectory. */
@Singleton
public class SiteStaticDirectoryServlet extends ResourceServlet {
private static final long serialVersionUID = 1L;
private final Path staticBase;
@Inject
SiteStaticDirectoryServlet(
SitePaths site,
@GerritServerConfig Config cfg,
@Named(StaticModule.CACHE) Cache<Path, Resource> cache) {
super(cache, cfg.getBoolean("site", "refreshHeaderFooter", true));
Path p;
try {
p = site.static_dir.toRealPath().normalize();
} catch (IOException e) {
p = site.static_dir.toAbsolutePath().normalize();
}
staticBase = p;
}
@Override
protected Path getResourcePath(String pathInfo) {
Path p = staticBase.resolve(pathInfo);
try {
p = p.toRealPath().normalize();
if (!p.startsWith(staticBase)) {
return null;
}
return p;
} catch (IOException e) {
return null;
}
}
}

View File

@@ -0,0 +1,139 @@
// 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.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() {
cache(CACHE, Path.class, Resource.class)
.maximumWeight(1 << 20)
.weigher(ResourceServlet.Weigher.class);
}
});
}
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

@@ -1,249 +0,0 @@
// Copyright (C) 2008 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 com.google.common.net.HttpHeaders.CONTENT_ENCODING;
import static com.google.common.net.HttpHeaders.ETAG;
import static com.google.common.net.HttpHeaders.IF_NONE_MATCH;
import static com.google.gerrit.common.FileUtil.lastModified;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
import com.google.common.base.CharMatcher;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.Weigher;
import com.google.common.collect.Maps;
import com.google.common.hash.Hashing;
import com.google.gerrit.common.FileUtil;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.httpd.HtmlDomUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.gwtexpui.server.CacheHeaders;
import com.google.gwtjsonrpc.server.RPCServletUtils;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** Sends static content from the site 's {@code static/} subdirectory. */
@SuppressWarnings("serial")
@Singleton
public class StaticServlet extends HttpServlet {
private static final Logger log = LoggerFactory.getLogger(StaticServlet.class);
private static final String JS = "application/x-javascript";
private static final Map<String, String> MIME_TYPES = Maps.newHashMap();
static {
MIME_TYPES.put("html", "text/html");
MIME_TYPES.put("htm", "text/html");
MIME_TYPES.put("js", JS);
MIME_TYPES.put("css", "text/css");
MIME_TYPES.put("rtf", "text/rtf");
MIME_TYPES.put("txt", "text/plain");
MIME_TYPES.put("text", "text/plain");
MIME_TYPES.put("pdf", "application/pdf");
MIME_TYPES.put("jpeg", "image/jpeg");
MIME_TYPES.put("jpg", "image/jpeg");
MIME_TYPES.put("gif", "image/gif");
MIME_TYPES.put("png", "image/png");
MIME_TYPES.put("tiff", "image/tiff");
MIME_TYPES.put("tif", "image/tiff");
MIME_TYPES.put("svg", "image/svg+xml");
}
private static String contentType(final String name) {
final int dot = name.lastIndexOf('.');
final String ext = 0 < dot ? name.substring(dot + 1) : "";
final String type = MIME_TYPES.get(ext);
return type != null ? type : "application/octet-stream";
}
private final Path staticBase;
private final boolean refresh;
private final LoadingCache<String, Resource> cache;
@Inject
StaticServlet(@GerritServerConfig Config cfg, SitePaths site) {
Path p;
try {
p = site.static_dir.toRealPath().normalize();
} catch (IOException e) {
p = site.static_dir.toAbsolutePath().normalize();
}
staticBase = p;
refresh = cfg.getBoolean("site", "refreshHeaderFooter", true);
cache = CacheBuilder.newBuilder()
.maximumWeight(1 << 20)
.weigher(new Weigher<String, Resource>() {
@Override
public int weigh(String name, Resource r) {
return 2 * name.length() + r.raw.length;
}
})
.build(new CacheLoader<String, Resource>() {
@Override
public Resource load(String name) throws Exception {
return loadResource(name);
}
});
}
@Nullable
Resource getResource(String name) {
try {
return cache.get(name);
} catch (ExecutionException e) {
log.warn(String.format("Cannot load static resource %s", name), e);
return null;
}
}
private Resource getResource(HttpServletRequest req)
throws ExecutionException {
String name = CharMatcher.is('/').trimFrom(req.getPathInfo());
if (isUnreasonableName(name)) {
return Resource.NOT_FOUND;
}
Resource r = cache.get(name);
if (r == Resource.NOT_FOUND) {
return Resource.NOT_FOUND;
}
if (refresh && r.isStale()) {
cache.invalidate(name);
r = cache.get(name);
}
return r;
}
private static boolean isUnreasonableName(String name) {
return name.length() < 1
|| name.contains("\\") // no windows/dos style paths
|| name.startsWith("../") // no "../etc/passwd"
|| name.contains("/../") // no "foo/../etc/passwd"
|| name.contains("/./") // "foo/./foo" is insane to ask
|| name.contains("//"); // windows UNC path can be "//..."
}
@Override
protected void doGet(final HttpServletRequest req,
final HttpServletResponse rsp) throws IOException {
Resource r;
try {
r = getResource(req);
} catch (ExecutionException e) {
log.warn(String.format(
"Cannot load static resource %s",
req.getPathInfo()), e);
CacheHeaders.setNotCacheable(rsp);
rsp.setStatus(SC_INTERNAL_SERVER_ERROR);
return;
}
String e = req.getParameter("e");
if (r == Resource.NOT_FOUND || (e != null && !r.etag.equals(e))) {
CacheHeaders.setNotCacheable(rsp);
rsp.setStatus(SC_NOT_FOUND);
return;
} else if (r.etag.equals(req.getHeader(IF_NONE_MATCH))) {
rsp.setStatus(SC_NOT_MODIFIED);
return;
}
byte[] tosend = r.raw;
if (!r.contentType.equals(JS) && RPCServletUtils.acceptsGzipEncoding(req)) {
byte[] gz = HtmlDomUtil.compress(tosend);
if ((gz.length + 24) < tosend.length) {
rsp.setHeader(CONTENT_ENCODING, "gzip");
tosend = gz;
}
}
if (e != null && r.etag.equals(e)) {
CacheHeaders.setCacheable(req, rsp, 360, DAYS, false);
} else {
CacheHeaders.setCacheable(req, rsp, 15, MINUTES, refresh);
}
rsp.setHeader(ETAG, r.etag);
rsp.setContentType(r.contentType);
rsp.setContentLength(tosend.length);
try (OutputStream out = rsp.getOutputStream()) {
out.write(tosend);
}
}
private Resource loadResource(String name) throws IOException {
Path p = staticBase.resolve(name);
try {
p = p.toRealPath().normalize();
} catch (IOException e) {
return Resource.NOT_FOUND;
}
if (!p.startsWith(staticBase)) {
return Resource.NOT_FOUND;
}
long ts = FileUtil.lastModified(p);
byte[] raw;
try {
raw = Files.readAllBytes(p);
} catch (NoSuchFileException e) {
return Resource.NOT_FOUND;
}
return new Resource(p, ts, contentType(name), raw);
}
static class Resource {
static final Resource NOT_FOUND = new Resource(null, -1, "", new byte[] {});
final Path src;
final long lastModified;
final String contentType;
final String etag;
final byte[] raw;
Resource(Path src, long lastModified, String contentType, byte[] raw) {
this.src = src;
this.lastModified = lastModified;
this.contentType = contentType;
this.etag = Hashing.md5().hashBytes(raw).toString();
this.raw = raw;
}
boolean isStale() {
return lastModified != lastModified(src);
}
}
}

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;
}
}
}