blob: a2a770ebe68e076c50bf2df67aa31042dd93bd22 [file] [log] [blame]
/*
* 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");
}
}
}