439 lines
14 KiB
Java
439 lines
14 KiB
Java
/*
|
|
* Copyright 2008 Google Inc.
|
|
*
|
|
* 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.gwtdebug;
|
|
|
|
import com.google.gwt.core.ext.ServletContainer;
|
|
import com.google.gwt.core.ext.ServletContainerLauncher;
|
|
import com.google.gwt.core.ext.TreeLogger;
|
|
import com.google.gwt.core.ext.UnableToCompleteException;
|
|
import com.google.gwt.dev.shell.jetty.JettyNullLogger;
|
|
|
|
import org.mortbay.component.AbstractLifeCycle;
|
|
import org.mortbay.jetty.AbstractConnector;
|
|
import org.mortbay.jetty.HttpFields.Field;
|
|
import org.mortbay.jetty.Request;
|
|
import org.mortbay.jetty.RequestLog;
|
|
import org.mortbay.jetty.Response;
|
|
import org.mortbay.jetty.Server;
|
|
import org.mortbay.jetty.handler.RequestLogHandler;
|
|
import org.mortbay.jetty.nio.SelectChannelConnector;
|
|
import org.mortbay.jetty.webapp.WebAppClassLoader;
|
|
import org.mortbay.jetty.webapp.WebAppContext;
|
|
import org.mortbay.log.Log;
|
|
import org.mortbay.log.Logger;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.net.URL;
|
|
import java.net.URLClassLoader;
|
|
import java.util.Iterator;
|
|
|
|
public class GerritDebugLauncher extends ServletContainerLauncher {
|
|
/**
|
|
* Log jetty requests/responses to TreeLogger.
|
|
*/
|
|
public static class JettyRequestLogger extends AbstractLifeCycle implements
|
|
RequestLog {
|
|
|
|
private final TreeLogger logger;
|
|
|
|
public JettyRequestLogger(TreeLogger logger) {
|
|
this.logger = logger;
|
|
}
|
|
|
|
/**
|
|
* Log an HTTP request/response to TreeLogger.
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
public void log(Request request, Response response) {
|
|
int status = response.getStatus();
|
|
if (status < 0) {
|
|
// Copied from NCSARequestLog
|
|
status = 404;
|
|
}
|
|
TreeLogger.Type logStatus, logHeaders;
|
|
if (status >= 500) {
|
|
logStatus = TreeLogger.ERROR;
|
|
logHeaders = TreeLogger.INFO;
|
|
} else if (status >= 400) {
|
|
logStatus = TreeLogger.WARN;
|
|
logHeaders = TreeLogger.INFO;
|
|
} else {
|
|
logStatus = TreeLogger.INFO;
|
|
logHeaders = TreeLogger.DEBUG;
|
|
}
|
|
String userString = request.getRemoteUser();
|
|
if (userString == null) {
|
|
userString = "";
|
|
} else {
|
|
userString += "@";
|
|
}
|
|
String bytesString = "";
|
|
if (response.getContentCount() > 0) {
|
|
bytesString = " " + response.getContentCount() + " bytes";
|
|
}
|
|
if (logger.isLoggable(logStatus)) {
|
|
TreeLogger branch =
|
|
logger.branch(logStatus, String.valueOf(status) + " - "
|
|
+ request.getMethod() + ' ' + request.getUri() + " ("
|
|
+ userString + request.getRemoteHost() + ')' + bytesString);
|
|
if (branch.isLoggable(logHeaders)) {
|
|
// Request headers
|
|
TreeLogger headers = branch.branch(logHeaders, "Request headers");
|
|
Iterator<Field> headerFields =
|
|
request.getConnection().getRequestFields().getFields();
|
|
while (headerFields.hasNext()) {
|
|
Field headerField = headerFields.next();
|
|
headers.log(logHeaders, headerField.getName() + ": "
|
|
+ headerField.getValue());
|
|
}
|
|
// Response headers
|
|
headers = branch.branch(logHeaders, "Response headers");
|
|
headerFields = response.getHttpFields().getFields();
|
|
while (headerFields.hasNext()) {
|
|
Field headerField = headerFields.next();
|
|
headers.log(logHeaders, headerField.getName() + ": "
|
|
+ headerField.getValue());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An adapter for the Jetty logging system to GWT's TreeLogger. This
|
|
* implementation class is only public to allow {@link Log} to instantiate it.
|
|
*
|
|
* The weird static data / default construction setup is a game we play with
|
|
* {@link Log}'s static initializer to prevent the initial log message from
|
|
* going to stderr.
|
|
*/
|
|
public static class JettyTreeLogger implements Logger {
|
|
private final TreeLogger logger;
|
|
|
|
public JettyTreeLogger(TreeLogger logger) {
|
|
if (logger == null) {
|
|
throw new NullPointerException();
|
|
}
|
|
this.logger = logger;
|
|
}
|
|
|
|
public void debug(String msg, Object arg0, Object arg1) {
|
|
logger.log(TreeLogger.SPAM, format(msg, arg0, arg1));
|
|
}
|
|
|
|
public void debug(String msg, Throwable th) {
|
|
logger.log(TreeLogger.SPAM, msg, th);
|
|
}
|
|
|
|
public Logger getLogger(String name) {
|
|
return this;
|
|
}
|
|
|
|
public void info(String msg, Object arg0, Object arg1) {
|
|
logger.log(TreeLogger.INFO, format(msg, arg0, arg1));
|
|
}
|
|
|
|
public boolean isDebugEnabled() {
|
|
return logger.isLoggable(TreeLogger.SPAM);
|
|
}
|
|
|
|
public void setDebugEnabled(boolean enabled) {
|
|
// ignored
|
|
}
|
|
|
|
public void warn(String msg, Object arg0, Object arg1) {
|
|
logger.log(TreeLogger.WARN, format(msg, arg0, arg1));
|
|
}
|
|
|
|
public void warn(String msg, Throwable th) {
|
|
logger.log(TreeLogger.WARN, msg, th);
|
|
}
|
|
|
|
/**
|
|
* Copied from org.mortbay.log.StdErrLog.
|
|
*/
|
|
private String format(String msg, Object arg0, Object arg1) {
|
|
int i0 = msg.indexOf("{}");
|
|
int i1 = i0 < 0 ? -1 : msg.indexOf("{}", i0 + 2);
|
|
|
|
if (arg1 != null && i1 >= 0) {
|
|
msg = msg.substring(0, i1) + arg1 + msg.substring(i1 + 2);
|
|
}
|
|
if (arg0 != null && i0 >= 0) {
|
|
msg = msg.substring(0, i0) + arg0 + msg.substring(i0 + 2);
|
|
}
|
|
return msg;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The resulting {@link ServletContainer} this is launched.
|
|
*/
|
|
protected static class JettyServletContainer extends ServletContainer {
|
|
private final int actualPort;
|
|
private final File appRootDir;
|
|
private final TreeLogger logger;
|
|
private final Server server;
|
|
private final WebAppContext wac;
|
|
|
|
public JettyServletContainer(TreeLogger logger, Server server,
|
|
WebAppContext wac, int actualPort, File appRootDir) {
|
|
this.logger = logger;
|
|
this.server = server;
|
|
this.wac = wac;
|
|
this.actualPort = actualPort;
|
|
this.appRootDir = appRootDir;
|
|
}
|
|
|
|
@Override
|
|
public int getPort() {
|
|
return actualPort;
|
|
}
|
|
|
|
@Override
|
|
public void refresh() throws UnableToCompleteException {
|
|
String msg =
|
|
"Reloading web app to reflect changes in "
|
|
+ appRootDir.getAbsolutePath();
|
|
TreeLogger branch = logger.branch(TreeLogger.INFO, msg);
|
|
// Temporarily log Jetty on the branch.
|
|
Log.setLog(new JettyTreeLogger(branch));
|
|
try {
|
|
wac.stop();
|
|
wac.start();
|
|
branch.log(TreeLogger.INFO, "Reload completed successfully");
|
|
} catch (Exception e) {
|
|
branch.log(TreeLogger.ERROR, "Unable to restart embedded Jetty server",
|
|
e);
|
|
throw new UnableToCompleteException();
|
|
} finally {
|
|
// Reset the top-level logger.
|
|
Log.setLog(new JettyTreeLogger(logger));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void stop() throws UnableToCompleteException {
|
|
TreeLogger branch =
|
|
logger.branch(TreeLogger.INFO, "Stopping Jetty server");
|
|
// Temporarily log Jetty on the branch.
|
|
Log.setLog(new JettyTreeLogger(branch));
|
|
try {
|
|
server.stop();
|
|
server.setStopAtShutdown(false);
|
|
branch.log(TreeLogger.INFO, "Stopped successfully");
|
|
} catch (Exception e) {
|
|
branch.log(TreeLogger.ERROR, "Unable to stop embedded Jetty server", e);
|
|
throw new UnableToCompleteException();
|
|
} finally {
|
|
// Reset the top-level logger.
|
|
Log.setLog(new JettyTreeLogger(logger));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A {@link WebAppContext} tailored to GWT hosted mode. Features hot-reload
|
|
* with a new {@link WebAppClassLoader} to pick up disk changes. The default
|
|
* Jetty {@code WebAppContext} will create new instances of servlets, but it
|
|
* will not create a brand new {@link ClassLoader}. By creating a new {@code
|
|
* ClassLoader} each time, we re-read updated classes from disk.
|
|
*
|
|
* Also provides special class filtering to isolate the web app from the GWT
|
|
* hosting environment.
|
|
*/
|
|
protected final class MyWebAppContext extends WebAppContext {
|
|
/**
|
|
* Parent ClassLoader for the Jetty web app, which can only load JVM
|
|
* classes. We would just use <code>null</code> for the parent ClassLoader
|
|
* except this makes Jetty unhappy.
|
|
*/
|
|
private final ClassLoader bootStrapOnlyClassLoader =
|
|
new ClassLoader(null) {};
|
|
|
|
private final ClassLoader systemClassLoader =
|
|
Thread.currentThread().getContextClassLoader();
|
|
|
|
@SuppressWarnings("unchecked")
|
|
private MyWebAppContext(String webApp, String contextPath) {
|
|
super(webApp, contextPath);
|
|
|
|
// Prevent file locking on Windows; pick up file changes.
|
|
getInitParams().put(
|
|
"org.mortbay.jetty.servlet.Default.useFileMappedBuffer", "false");
|
|
|
|
// Since the parent class loader is bootstrap-only, prefer it first.
|
|
setParentLoaderPriority(true);
|
|
}
|
|
|
|
@Override
|
|
protected void doStart() throws Exception {
|
|
setClassLoader(new MyLoader());
|
|
super.doStart();
|
|
}
|
|
|
|
@Override
|
|
protected void doStop() throws Exception {
|
|
super.doStop();
|
|
setClassLoader(null);
|
|
}
|
|
|
|
private class MyLoader extends WebAppClassLoader {
|
|
MyLoader() throws IOException {
|
|
super(bootStrapOnlyClassLoader, MyWebAppContext.this);
|
|
|
|
final URLClassLoader scl = (URLClassLoader) systemClassLoader;
|
|
final URL[] urls = scl.getURLs();
|
|
for (URL u : urls) {
|
|
if ("file".equals(u.getProtocol())) {
|
|
addClassPath(u.getPath());
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean isSystemPath(String name) {
|
|
name = name.replace('/', '.');
|
|
return super.isSystemPath(name) //
|
|
|| name.startsWith("org.bouncycastle.");
|
|
}
|
|
|
|
@Override
|
|
protected Class<?> findClass(String name) throws ClassNotFoundException {
|
|
// For system path, always prefer the outside world.
|
|
if (isSystemPath(name)) {
|
|
try {
|
|
return systemClassLoader.loadClass(name);
|
|
} catch (ClassNotFoundException e) {
|
|
}
|
|
}
|
|
return super.findClass(name);
|
|
}
|
|
}
|
|
}
|
|
|
|
static {
|
|
// Suppress spammy Jetty log initialization.
|
|
System
|
|
.setProperty("org.mortbay.log.class", JettyNullLogger.class.getName());
|
|
Log.getLog();
|
|
|
|
/*
|
|
* Make JDT the default Ant compiler so that JSP compilation just works
|
|
* out-of-the-box. If we don't set this, it's very, very difficult to make
|
|
* JSP compilation work.
|
|
*/
|
|
String antJavaC =
|
|
System.getProperty("build.compiler",
|
|
"org.eclipse.jdt.core.JDTCompilerAdapter");
|
|
System.setProperty("build.compiler", antJavaC);
|
|
|
|
System.setProperty("Gerrit.GwtDevMode", "" + true);
|
|
}
|
|
|
|
private String bindAddress = null;
|
|
|
|
@Override
|
|
public void setBindAddress(String bindAddress) {
|
|
this.bindAddress = bindAddress;
|
|
}
|
|
|
|
@Override
|
|
public ServletContainer start(TreeLogger logger, int port, File warDir)
|
|
throws Exception {
|
|
TreeLogger branch =
|
|
logger.branch(TreeLogger.INFO, "Starting Jetty on port " + port, null);
|
|
|
|
checkStartParams(branch, port, warDir);
|
|
|
|
// Setup our branch logger during startup.
|
|
Log.setLog(new JettyTreeLogger(branch));
|
|
|
|
// Turn off XML validation.
|
|
System.setProperty("org.mortbay.xml.XmlParser.Validating", "false");
|
|
|
|
AbstractConnector connector = getConnector();
|
|
if (bindAddress != null) {
|
|
connector.setHost(bindAddress.toString());
|
|
}
|
|
connector.setPort(port);
|
|
|
|
// Don't share ports with an existing process.
|
|
connector.setReuseAddress(false);
|
|
|
|
// Linux keeps the port blocked after shutdown if we don't disable this.
|
|
connector.setSoLingerTime(0);
|
|
|
|
Server server = new Server();
|
|
server.addConnector(connector);
|
|
|
|
File top;
|
|
String root = System.getProperty("gerrit.source_root");
|
|
if (root != null) {
|
|
top = new File(root);
|
|
} else {
|
|
// Under Maven warDir is "$top/gerrit-gwtui/target/gwt-hosted-mode"
|
|
top = warDir.getParentFile().getParentFile().getParentFile();
|
|
}
|
|
|
|
File app = new File(top, "gerrit-war/src/main/webapp");
|
|
File webxml = new File(app, "WEB-INF/web.xml");
|
|
|
|
// Jetty won't start unless this directory exists.
|
|
if (!warDir.exists() && !warDir.mkdirs())
|
|
logger.branch(TreeLogger.ERROR, "Cannot create "+warDir, null);
|
|
|
|
// Create a new web app in the war directory.
|
|
//
|
|
WebAppContext wac =
|
|
new MyWebAppContext(warDir.getAbsolutePath(), "/");
|
|
wac.setDescriptor(webxml.getAbsolutePath());
|
|
|
|
RequestLogHandler logHandler = new RequestLogHandler();
|
|
logHandler.setRequestLog(new JettyRequestLogger(logger));
|
|
logHandler.setHandler(wac);
|
|
server.setHandler(logHandler);
|
|
server.start();
|
|
server.setStopAtShutdown(true);
|
|
|
|
// Now that we're started, log to the top level logger.
|
|
Log.setLog(new JettyTreeLogger(logger));
|
|
|
|
return new JettyServletContainer(logger, server, wac, connector
|
|
.getLocalPort(), warDir);
|
|
}
|
|
|
|
protected AbstractConnector getConnector() {
|
|
return new SelectChannelConnector();
|
|
}
|
|
|
|
private void checkStartParams(TreeLogger logger, int port, File appRootDir) {
|
|
if (logger == null) {
|
|
throw new NullPointerException("logger cannot be null");
|
|
}
|
|
|
|
if (port < 0 || port > 65535) {
|
|
throw new IllegalArgumentException(
|
|
"port must be either 0 (for auto) or less than 65536");
|
|
}
|
|
|
|
if (appRootDir == null) {
|
|
throw new NullPointerException("app root direcotry cannot be null");
|
|
}
|
|
}
|
|
}
|