/* * 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.gwt.dev; 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.core.ext.linker.ArtifactSet; import com.google.gwt.core.ext.linker.impl.StandardLinkerContext; import com.google.gwt.dev.cfg.ModuleDef; import com.google.gwt.dev.resource.impl.ResourceOracleImpl; import com.google.gwt.dev.shell.BrowserListener; import com.google.gwt.dev.shell.CodeServerListener; import com.google.gwt.dev.shell.OophmSessionHandler; import com.google.gwt.dev.shell.SuperDevListener; import com.google.gwt.dev.shell.jetty.JettyLauncher; import com.google.gwt.dev.ui.RestartServerCallback; import com.google.gwt.dev.ui.RestartServerEvent; import com.google.gwt.dev.util.InstalledHelpInfo; import com.google.gwt.dev.util.Util; import com.google.gwt.dev.util.arg.ArgHandlerDeployDir; import com.google.gwt.dev.util.arg.ArgHandlerDisableUpdateCheck; import com.google.gwt.dev.util.arg.ArgHandlerExtraDir; import com.google.gwt.dev.util.arg.ArgHandlerFilterJsInteropExports; import com.google.gwt.dev.util.arg.ArgHandlerGenerateJsInteropExports; import com.google.gwt.dev.util.arg.ArgHandlerIncrementalCompile; import com.google.gwt.dev.util.arg.ArgHandlerMethodNameDisplayMode; import com.google.gwt.dev.util.arg.ArgHandlerModuleName; import com.google.gwt.dev.util.arg.ArgHandlerModulePathPrefix; import com.google.gwt.dev.util.arg.ArgHandlerScriptStyle; import com.google.gwt.dev.util.arg.ArgHandlerSetProperties; import com.google.gwt.dev.util.arg.ArgHandlerSourceLevel; import com.google.gwt.dev.util.arg.ArgHandlerStrict; import com.google.gwt.dev.util.arg.ArgHandlerWarDir; import com.google.gwt.dev.util.arg.ArgHandlerWorkDirOptional; import com.google.gwt.dev.util.arg.OptionModulePathPrefix; import com.google.gwt.dev.util.log.speedtracer.DevModeEventType; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event; import com.google.gwt.util.tools.ArgHandlerFlag; import com.google.gwt.util.tools.ArgHandlerString; import com.google.gwt.util.tools.Utility; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.net.BindException; import java.net.URL; import java.util.LinkedHashMap; import java.util.Map; import java.util.regex.Pattern; /** * The main executable class for the hosted mode shell. NOTE: the public API for * this class is to be determined. Consider this class as having <b>no</b> * public API other than {@link #main(String[])}. */ public class DevMode extends DevModeBase implements RestartServerCallback { /** * Handles the -superDevMode command line flag. */ public interface HostedModeOptions extends HostedModeBaseOptions, CompilerOptions, OptionSuperDevMode, OptionModulePathPrefix { ServletContainerLauncher getServletContainerLauncher(); String getServletContainerLauncherArgs(); void setServletContainerLauncher(ServletContainerLauncher scl); void setServletContainerLauncherArgs(String args); } /** * Runs the superdev-mode code server instead of classic one. */ protected static class ArgHandlerSuperDevMode extends ArgHandlerFlag { private final HostedModeOptions options; public ArgHandlerSuperDevMode(HostedModeOptions options) { this.options = options; addTagValue("-superDevMode", true); } @Override public boolean getDefaultValue() { return true; } @Override public String getLabel() { return "superDevMode"; } @Override public String getPurposeSnippet() { return "Runs Super Dev Mode instead of classic Development Mode."; } @Override public boolean setFlag(boolean value) { options.setSuperDevMode(value); // Superdev uses incremental by default if (options.isSuperDevMode()) { options.setIncrementalCompileEnabled(true); } return true; } } /** * Handles the -server command line flag. */ protected static class ArgHandlerServer extends ArgHandlerString { private static final String DEFAULT_SCL = JettyLauncher.class.getName(); private HostedModeOptions options; public ArgHandlerServer(HostedModeOptions options) { this.options = options; } @Override public String[] getDefaultArgs() { if (options.isNoServer()) { return null; } else { return new String[] {getTag(), DEFAULT_SCL}; } } @Override public String getPurpose() { return "Specify a different embedded web server to run (must implement ServletContainerLauncher)"; } @Override public String getTag() { return "-server"; } @Override public String[] getTagArgs() { return new String[] {"servletContainerLauncher[:args]"}; } @Override public boolean setString(String arg) { // Supercedes -noserver. options.setNoServer(false); String sclClassName; String sclArgs; int idx = arg.indexOf(':'); if (idx >= 0) { sclArgs = arg.substring(idx + 1); sclClassName = arg.substring(0, idx); } else { sclArgs = null; sclClassName = arg; } if (sclClassName.length() == 0) { sclClassName = DEFAULT_SCL; } Throwable t; try { Class<?> clazz = Class.forName(sclClassName, true, Thread.currentThread().getContextClassLoader()); Class<? extends ServletContainerLauncher> sclClass = clazz.asSubclass(ServletContainerLauncher.class); options.setServletContainerLauncher(sclClass.newInstance()); options.setServletContainerLauncherArgs(sclArgs); return true; } catch (ClassCastException e) { t = e; } catch (ClassNotFoundException e) { t = e; } catch (InstantiationException e) { t = e; } catch (IllegalAccessException e) { t = e; } System.err.println("Unable to load server class '" + sclClassName + "'"); t.printStackTrace(); return false; } } /** * Handles a startup url that can be passed on the command line. */ protected static class ArgHandlerStartupURLs extends ArgHandlerString { private final OptionStartupURLs options; public ArgHandlerStartupURLs(OptionStartupURLs options) { this.options = options; } @Override public String getPurpose() { return "Automatically launches the specified URL"; } @Override public String getTag() { return "-startupUrl"; } @Override public String[] getTagArgs() { return new String[] {"url"}; } @Override public boolean setString(String arg) { options.addStartupURL(arg); return true; } } /** * The argument processor. */ protected static class ArgProcessor extends DevModeBase.ArgProcessor { public ArgProcessor(HostedModeOptions options) { super(options, false); registerHandler(new ArgHandlerSuperDevMode(options)); registerHandler(new ArgHandlerServer(options)); registerHandler(new ArgHandlerStartupURLs(options)); registerHandler(new ArgHandlerWarDir(options)); registerHandler(new ArgHandlerDeployDir(options)); registerHandler(new ArgHandlerExtraDir(options)); registerHandler(new ArgHandlerModulePathPrefix(options)); registerHandler(new ArgHandlerWorkDirOptional(options)); registerHandler(new ArgHandlerDisableUpdateCheck(options)); registerHandler(new ArgHandlerMethodNameDisplayMode(options)); registerHandler(new ArgHandlerSourceLevel(options)); registerHandler(new ArgHandlerGenerateJsInteropExports(options)); registerHandler(new ArgHandlerFilterJsInteropExports(options)); registerHandler(new ArgHandlerIncrementalCompile(options)); registerHandler(new ArgHandlerScriptStyle(options)); registerHandler(new ArgHandlerStrict(options)); registerHandler(new ArgHandlerModuleName(options) { @Override public String getPurpose() { return super.getPurpose() + " to host"; } }); registerHandler(new ArgHandlerSetProperties(options)); } @Override protected String getName() { return DevMode.class.getName(); } } /** * Concrete class to implement all hosted mode options. */ @SuppressWarnings("serial") protected static class HostedModeOptionsImpl extends HostedModeBaseOptionsImpl implements HostedModeOptions { private File deployDir; private File extraDir; private int localWorkers; private ServletContainerLauncher scl; private String sclArgs; private boolean sdm = true; private File moduleBaseDir; private String modulePathPrefix = ""; private File warDir; private boolean closureCompilerFormatEnabled; @Override public File getDeployDir() { return (deployDir == null) ? new File(warDir, "WEB-INF/deploy") : deployDir; } @Override public File getExtraDir() { return extraDir; } @Override public int getLocalWorkers() { return localWorkers; } @Override public File getSaveSourceOutput() { return null; } @Override public ServletContainerLauncher getServletContainerLauncher() { return scl; } @Override public File getModuleBaseDir() { return moduleBaseDir; } @Override public String getServletContainerLauncherArgs() { return sclArgs; } @Override public File getWarDir() { return warDir; } @Override public boolean isSuperDevMode() { return sdm; } @Override public void setDeployDir(File deployDir) { this.deployDir = deployDir; } @Override public void setExtraDir(File extraDir) { this.extraDir = extraDir; } @Override public void setLocalWorkers(int localWorkers) { this.localWorkers = localWorkers; } @Override public void setModulePathPrefix(String prefix) { if (!prefix.equals(modulePathPrefix)) { modulePathPrefix = prefix; updateModuleBaseDir(); } } @Deprecated public void setOutDir(File outDir) { setWarDir(outDir); } @Override public void setSaveSourceOutput(File debugDir) { throw new UnsupportedOperationException(); } @Override public void setSuperDevMode(boolean sdm) { this.sdm = sdm; } @Override public void setServletContainerLauncher(ServletContainerLauncher scl) { this.scl = scl; } @Override public void setServletContainerLauncherArgs(String args) { sclArgs = args; } @Override public void setWarDir(File warDir) { this.warDir = warDir; updateModuleBaseDir(); } private void updateModuleBaseDir() { this.moduleBaseDir = new File(warDir, modulePathPrefix); } @Override public boolean isClosureCompilerFormatEnabled() { return closureCompilerFormatEnabled; } @Override public void setClosureCompilerFormatEnabled(boolean enabled) { this.closureCompilerFormatEnabled = enabled; } } /** * Determines whether to start the code server or not. */ protected interface OptionSuperDevMode { boolean isSuperDevMode(); void setSuperDevMode(boolean sdm); } /** * The pattern for files usable as startup URLs. */ private static final Pattern STARTUP_FILE_PATTERN = Pattern.compile(".*\\.(html|jsp)", Pattern.CASE_INSENSITIVE); /** * Startup development mode. * * @param args command line arguments */ public static void main(String[] args) { /* * NOTE: main always exits with a call to System.exit to terminate any * non-daemon threads that were started in Generators. Typically, this is to * shutdown AWT related threads, since the contract for their termination is * still implementation-dependent. */ DevMode hostedMode = new DevMode(); if (new ArgProcessor(hostedMode.options).processArgs(args)) { hostedMode.run(); // Exit w/ success code. System.exit(0); } // Exit w/ non-success code. System.exit(-1); } protected CodeServerListener listener; /** * Hiding super field because it's actually the same object, just with a stronger type. */ @SuppressWarnings("hiding") protected final HostedModeOptions options = (HostedModeOptionsImpl) super.options; /** * The server that was started. */ private ServletContainer server; private final Map<String, ModuleDef> startupModules = new LinkedHashMap<String, ModuleDef>(); /** * Tracks whether we created a temp workdir that we need to destroy. */ private boolean tempWorkDir = false; /** * Default constructor for testing; no public API yet. */ protected DevMode() { } /** * Called by the UI on a restart server event. */ @Override public void onRestartServer(TreeLogger logger) { try { server.refresh(); } catch (UnableToCompleteException e) { // ignore, problem already logged } } @Override protected HostedModeOptions createOptions() { HostedModeOptionsImpl hostedModeOptions = new HostedModeOptionsImpl(); hostedModeOptions.setIncrementalCompileEnabled(true); compilerContext = compilerContextBuilder.options(hostedModeOptions).build(); return hostedModeOptions; } @Override protected void doShutDownServer() { if (server != null) { try { server.stop(); } catch (UnableToCompleteException e) { // Already logged. } server = null; } if (tempWorkDir) { Util.recursiveDelete(options.getWorkDir(), false); } } @Override protected boolean doSlowStartup() { tempWorkDir = options.getWorkDir() == null; if (tempWorkDir) { try { options.setWorkDir(Utility.makeTemporaryDirectory(null, "gwtc")); } catch (IOException e) { System.err.println("Unable to create hosted mode work directory"); e.printStackTrace(); return false; } } TreeLogger branch = getTopLogger().branch(TreeLogger.TRACE, "Linking modules"); Event slowStartupEvent = SpeedTracerLogger.start(DevModeEventType.SLOW_STARTUP); try { for (ModuleDef module : startupModules.values()) { TreeLogger loadLogger = branch.branch(TreeLogger.DEBUG, "Bootstrap link for command-line module '" + module.getCanonicalName() + "'"); link(loadLogger, module); } } catch (UnableToCompleteException e) { // Already logged. return false; } finally { slowStartupEvent.end(); } return true; } @Override protected boolean doStartup() { // Background scan the classpath to warm the cache. Thread scanThread = new Thread(new Runnable() { @Override public void run() { ResourceOracleImpl.preload(TreeLogger.NULL); } }); scanThread.setDaemon(true); scanThread.setPriority((Thread.MIN_PRIORITY + Thread.NORM_PRIORITY) / 2); scanThread.start(); File persistentCacheDir = null; if (options.getWarDir() != null && !options.getWarDir().getName().endsWith(".jar")) { persistentCacheDir = new File(options.getWarDir(), "../"); } if (!super.doStartup(persistentCacheDir)) { return false; } ServletValidator servletValidator = null; ServletWriter servletWriter = null; File webXml = new File(options.getWarDir(), "WEB-INF/web.xml"); if (!options.isNoServer()) { if (webXml.exists()) { servletValidator = ServletValidator.create(getTopLogger(), webXml); } else { servletWriter = new ServletWriter(); } } TreeLogger branch = getTopLogger().branch(TreeLogger.TRACE, "Loading modules"); try { for (String moduleName : options.getModuleNames()) { TreeLogger moduleBranch = branch.branch(TreeLogger.TRACE, moduleName); ModuleDef module = loadModule(moduleBranch, moduleName, false); // Create a hard reference to the module to avoid gc-ing it until we // actually load the module from the browser. startupModules.put(module.getName(), module); if (!options.isNoServer()) { validateServletTags(moduleBranch, servletValidator, servletWriter, module); } } if (servletWriter != null) { servletWriter.realize(webXml); } } catch (IOException e) { getTopLogger().log(TreeLogger.WARN, "Unable to generate '" + webXml.getAbsolutePath() + "'"); } catch (UnableToCompleteException e) { // Already logged. return false; } return true; } @Override protected int doStartUpServer() { // Create the war directory if it doesn't exist File warDir = options.getWarDir(); if (!warDir.exists() && !warDir.mkdirs()) { getTopLogger().log(TreeLogger.ERROR, "Unable to create war directory " + warDir); return -1; } Event jettyStartupEvent = SpeedTracerLogger.start(DevModeEventType.JETTY_STARTUP); boolean clearCallback = true; try { ui.setCallback(RestartServerEvent.getType(), this); ServletContainerLauncher scl = options.getServletContainerLauncher(); TreeLogger serverLogger = ui.getWebServerLogger(getWebServerName(), scl.getIconBytes()); String sclArgs = options.getServletContainerLauncherArgs(); if (sclArgs != null) { if (!scl.processArguments(serverLogger, sclArgs)) { return -1; } } isHttps = scl.isSecure(); // Tell the UI if the web server is secure if (isHttps) { ui.setWebServerSecure(serverLogger); } /* * TODO: This is a hack to pass the base log level to the SCL. We'll have * to figure out a better way to do this for SCLs in general. */ if (scl instanceof JettyLauncher) { JettyLauncher jetty = (JettyLauncher) scl; jetty.setBaseRequestLogLevel(getBaseLogLevelForUI()); } scl.setBindAddress(options.getBindAddress()); if (serverLogger.isLoggable(TreeLogger.TRACE)) { serverLogger.log(TreeLogger.TRACE, "Starting HTTP on port " + getPort(), null); } server = scl.start(serverLogger, getPort(), options.getWarDir()); assert (server != null); clearCallback = false; return server.getPort(); } catch (BindException e) { System.err.println("Port " + options.getBindAddress() + ':' + getPort() + " is already in use; you probably still have another session active"); } catch (Exception e) { System.err.println("Unable to start embedded HTTP server"); e.printStackTrace(); } finally { jettyStartupEvent.end(); if (clearCallback) { // Clear the callback if we failed to start the server ui.setCallback(RestartServerEvent.getType(), null); } } return -1; } @Override protected void ensureCodeServerListener() { if (listener == null) { if (options.isSuperDevMode()) { listener = new SuperDevListener(getTopLogger(), options); } else { listener = new BrowserListener(getTopLogger(), options, new OophmSessionHandler(getTopLogger(), browserHost)); } listener.start(); } } protected String getWebServerName() { return options.getServletContainerLauncher().getName(); } @Override protected void inferStartupUrls() { // Look for launchable files directly under war File warDir = options.getWarDir(); if (!warDir.exists()) { // if the war directory doesn't exist, there are no startup files there return; } for (File htmlFile : warDir.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return STARTUP_FILE_PATTERN.matcher(name).matches(); } })) { options.addStartupURL(htmlFile.getName()); } } @Override protected ModuleDef loadModule(TreeLogger logger, String moduleName, boolean refresh) throws UnableToCompleteException { if (startupModules.containsKey(moduleName)) { // First load of a startup module; remove from list, no need to refresh. return startupModules.remove(moduleName); } return super.loadModule(logger, moduleName, refresh); } @Override protected URL makeStartupUrl(String url) throws UnableToCompleteException { return listener.makeStartupUrl(url); } @Override protected synchronized void produceOutput(TreeLogger logger, StandardLinkerContext linkerStack, ArtifactSet artifacts, ModuleDef module, boolean isRelink) throws UnableToCompleteException { listener.writeCompilerOutput(linkerStack, artifacts, module, isRelink); } @Override protected void warnAboutNoStartupUrls() { getTopLogger().log(TreeLogger.WARN, "No startup URLs supplied and no plausible ones found -- use " + "-startupUrl"); } private void validateServletTags(TreeLogger logger, ServletValidator servletValidator, ServletWriter servletWriter, ModuleDef module) { String[] servletPaths = module.getServletPaths(); if (servletPaths.length == 0) { return; } TreeLogger servletLogger = logger.branch(TreeLogger.DEBUG, "Validating <servlet> tags for module '" + module.getName() + "'", null, new InstalledHelpInfo("servletMappings.html")); for (String servletPath : servletPaths) { String servletClass = module.findServletForPath(servletPath); assert (servletClass != null); // Prefix module name to convert module mapping to global mapping. servletPath = "/" + module.getName() + servletPath; if (servletValidator == null) { servletWriter.addMapping(servletClass, servletPath); } else { servletValidator.validate(servletLogger, servletClass, servletPath); } } } }