| /* |
| * 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.ArgHandlerIncrementalCompile; |
| import com.google.gwt.dev.util.arg.ArgHandlerJsInteropMode; |
| 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.ArgHandlerSourceLevel; |
| 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 ArgHandlerJsInteropMode(options)); |
| registerHandler(new ArgHandlerIncrementalCompile(options)); |
| registerHandler(new ArgHandlerModuleName(options) { |
| @Override |
| public String getPurpose() { |
| return super.getPurpose() + " to host"; |
| } |
| }); |
| } |
| |
| @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; |
| |
| @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); |
| } |
| } |
| |
| /** |
| * 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(); |
| } |
| |
| // When in GWTTestCase and not using Dev Mode, there is no listener. |
| if (listener == null) { |
| return true; |
| } |
| |
| // When this methods exits, the buttons will be enabled in the Swing UI. |
| // We shouldn't do that until the code server is ready, or the user might |
| // see a blank page. |
| try { |
| listener.waitUntilReady(getTopLogger()); |
| return true; |
| } catch (UnableToCompleteException e) { |
| return false; // already logged |
| } |
| } |
| |
| @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 is 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); |
| } |
| } |
| } |
| |
| } |