/* * 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.Request; import org.mortbay.jetty.RequestLog; import org.mortbay.jetty.Response; import org.mortbay.jetty.Server; import org.mortbay.jetty.HttpFields.Field; 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(); private final TreeLogger logger; @SuppressWarnings("unchecked") private MyWebAppContext(TreeLogger logger, String webApp, String contextPath) { super(webApp, contextPath); this.logger = logger; // 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); // warDir is "$top/gerrit-gwtui/target/gwt-hosted-mode" // File 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(logger, 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"); } } }