Implements hot server reloading in GWT via a new Shell button. Clicking the button restarts embedded Jetty with a new classloader, picking up any class file changes on disk for the restarted server. This might be useful when changing the type declarations of serializable types during a hosted mode session, or testing how an app handles an incompatible remote service. Also changes the window title for HostedMode from "Development Shell" to "Hosted Mode". Review by: jat git-svn-id: https://google-web-toolkit.googlecode.com/svn/releases/1.6@4597 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/GWTShell.java b/dev/core/src/com/google/gwt/dev/GWTShell.java index d623526..e0a510b 100644 --- a/dev/core/src/com/google/gwt/dev/GWTShell.java +++ b/dev/core/src/com/google/gwt/dev/GWTShell.java
@@ -156,6 +156,14 @@ return new GWTCompilerOptionsImpl(options); } + public WebServerRestart hasWebServer() { + return WebServerRestart.NONE; + } + + public void restartServer(TreeLogger logger) throws UnableToCompleteException { + // Unimplemented. + } + public void setCompilerOptions(CompilerOptions options) { this.options.copyFrom(options); } @@ -228,6 +236,11 @@ } @Override + protected String getTitleText() { + return "Google Web Toolkit Development Shell"; + } + + @Override protected boolean initModule(String moduleName) { /* * Not used in legacy mode due to GWTShellServlet playing this role.
diff --git a/dev/core/src/com/google/gwt/dev/HostedMode.java b/dev/core/src/com/google/gwt/dev/HostedMode.java index 1d47fc3..7ddbc49 100644 --- a/dev/core/src/com/google/gwt/dev/HostedMode.java +++ b/dev/core/src/com/google/gwt/dev/HostedMode.java
@@ -270,6 +270,15 @@ HostedMode() { } + public WebServerRestart hasWebServer() { + return options.isNoServer() ? WebServerRestart.DISABLED + : WebServerRestart.ENABLED; + } + + public void restartServer(TreeLogger logger) throws UnableToCompleteException { + server.refresh(); + } + @Override protected void compile(TreeLogger logger) throws UnableToCompleteException { CompilerOptions newOptions = new CompilerOptionsImpl(options); @@ -380,6 +389,11 @@ } @Override + protected String getTitleText() { + return "Google Web Toolkit Hosted Mode"; + } + + @Override protected boolean initModule(String moduleName) { ModuleDef module = modulesByName.get(moduleName); if (module == null) { @@ -421,8 +435,8 @@ * * @param logger the logger to use * @param module the module to link - * @param includePublicFiles if <code>true</code>, include public files in the - * link, otherwise do not include them + * @param includePublicFiles if <code>true</code>, include public files in + * the link, otherwise do not include them * @throws UnableToCompleteException */ private void link(TreeLogger logger, ModuleDef module)
diff --git a/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java b/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java index c12aebb..0fac666 100644 --- a/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java +++ b/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java
@@ -178,6 +178,8 @@ return browserHost; } + protected abstract String getTitleText(); + @Override protected void initializeLogger() { final AbstractTreeLogger logger = mainWnd.getLogger(); @@ -221,8 +223,8 @@ shell.setImages(ShellMainWindow.getIcons()); - mainWnd = new ShellMainWindow(this, shell, options.isNoServer() ? 0 - : getPort()); + mainWnd = new ShellMainWindow(this, shell, getTitleText(), + options.isNoServer() ? 0 : getPort()); shell.setSize(700, 600); if (!isHeadless()) {
diff --git a/dev/core/src/com/google/gwt/dev/shell/BrowserWindowController.java b/dev/core/src/com/google/gwt/dev/shell/BrowserWindowController.java index e2bb884..ec178b1 100644 --- a/dev/core/src/com/google/gwt/dev/shell/BrowserWindowController.java +++ b/dev/core/src/com/google/gwt/dev/shell/BrowserWindowController.java
@@ -15,17 +15,29 @@ */ package com.google.gwt.dev.shell; +import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.UnableToCompleteException; /** * Interface to the browser window controller. */ public interface BrowserWindowController { + /** + * Whether to display server control(s). + */ + enum WebServerRestart { + DISABLED, ENABLED, NONE + } + void closeAllBrowserWindows(); boolean hasBrowserWindowsOpen(); + WebServerRestart hasWebServer(); + String normalizeURL(String string); BrowserWidget openNewBrowserWindow() throws UnableToCompleteException; -} \ No newline at end of file + + void restartServer(TreeLogger logger) throws UnableToCompleteException; +}
diff --git a/dev/core/src/com/google/gwt/dev/shell/ShellMainWindow.java b/dev/core/src/com/google/gwt/dev/shell/ShellMainWindow.java index 0e58953..e9a1e5e 100644 --- a/dev/core/src/com/google/gwt/dev/shell/ShellMainWindow.java +++ b/dev/core/src/com/google/gwt/dev/shell/ShellMainWindow.java
@@ -17,6 +17,7 @@ import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.dev.shell.BrowserWindowController.WebServerRestart; import com.google.gwt.dev.util.Util; import com.google.gwt.dev.util.log.AbstractTreeLogger; import com.google.gwt.dev.util.log.TreeLoggerWidget; @@ -50,6 +51,7 @@ private ToolItem collapseAll; private ToolItem expandAll; private ToolItem newWindow; + private ToolItem restartServer; public Toolbar(Composite parent) { super(parent); @@ -69,9 +71,27 @@ } } }); - newSeparator(); + if (browserWindowController.hasWebServer() != WebServerRestart.NONE) { + restartServer = newItem("reload-server.gif", "&Restart Server", + "Restart the embedded web server to pick up code changes"); + restartServer.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + try { + browserWindowController.restartServer(getLogger()); + } catch (UnableToCompleteException e) { + getLogger().log(TreeLogger.ERROR, "Unable to restart server", e); + } + } + }); + newSeparator(); + if (browserWindowController.hasWebServer() == WebServerRestart.DISABLED) { + restartServer.setEnabled(false); + } + } + collapseAll = newItem("collapse.gif", "&Collapse All", "Collapses all log entries"); collapseAll.addSelectionListener(new SelectionAdapter() { @@ -181,9 +201,8 @@ private Toolbar toolbar; public ShellMainWindow(BrowserWindowController browserWindowController, - final Shell parent, int serverPort) { + Shell parent, String titleText, int serverPort) { super(parent, SWT.NONE); - this.browserWindowController = browserWindowController; colorWhite = new Color(null, 255, 255, 255); @@ -193,10 +212,9 @@ setLayout(new FillLayout()); if (serverPort > 0) { - parent.setText("Google Web Toolkit Development Shell / Port " - + serverPort); + parent.setText(titleText + " / Port " + serverPort); } else { - parent.setText("Google Web Toolkit Development Shell"); + parent.setText(titleText); } GridLayout gridLayout = new GridLayout(1, true); @@ -207,7 +225,6 @@ setLayout(gridLayout); // Create the toolbar. - // { toolbar = new Toolbar(this); GridData data = new GridData(); @@ -217,7 +234,6 @@ } // Create the log pane. - // { logPane = new TreeLoggerWidget(this); GridData data = new GridData();
diff --git a/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java b/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java index 1b6023a..d47b05b 100644 --- a/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java +++ b/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
@@ -133,33 +133,6 @@ } } - /** - * Ensures that only Jetty and other server classes can be loaded into the web - * app classloader. This forces the user to put any necessary dependencies - * into WEB-INF/lib. - */ - private static final class FilteringParentClassLoader extends ClassLoader { - private WebAppClassLoader child = null; - private final ClassLoader delegateTo = Thread.currentThread().getContextClassLoader(); - - private FilteringParentClassLoader() { - super(null); - } - - public void setChild(WebAppClassLoader child) { - this.child = child; - } - - @Override - protected Class<?> findClass(String name) throws ClassNotFoundException { - if (child != null - && (child.isServerPath(name) || child.isSystemPath(name))) { - return delegateTo.loadClass(name); - } - throw new ClassNotFoundException(); - } - } - private static class JettyServletContainer extends ServletContainer { private final int actualPort; @@ -216,7 +189,61 @@ } } - @SuppressWarnings("unchecked") + /** + * 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. + */ + private final class WebAppContextWithReload extends WebAppContext { + /** + * Ensures that only Jetty and other server classes can be loaded into the + * {@link WebAppClassLoader}. This forces the user to put any necessary + * dependencies into WEB-INF/lib. + */ + private final ClassLoader parentClassLoader = new ClassLoader(null) { + private final ClassLoader delegateTo = Thread.currentThread().getContextClassLoader(); + + @Override + protected Class<?> findClass(String name) throws ClassNotFoundException { + if (webAppClassLoader != null + && (webAppClassLoader.isServerPath(name) || webAppClassLoader.isSystemPath(name))) { + return delegateTo.loadClass(name); + } + throw new ClassNotFoundException(); + } + + }; + + private WebAppClassLoader webAppClassLoader; + + @SuppressWarnings("unchecked") + private WebAppContextWithReload(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"); + } + + @Override + protected void doStart() throws Exception { + webAppClassLoader = new WebAppClassLoader(parentClassLoader, this); + setClassLoader(webAppClassLoader); + super.doStart(); + } + + @Override + protected void doStop() throws Exception { + super.doStop(); + webAppClassLoader = null; + setClassLoader(null); + } + } + public ServletContainer start(TreeLogger logger, int port, File appRootDir) throws Exception { checkStartParams(logger, port, appRootDir); @@ -249,15 +276,8 @@ server.addConnector(connector); // Create a new web app in the war directory. - WebAppContext wac = new WebAppContext(appRootDir.getAbsolutePath(), "/"); - FilteringParentClassLoader parentClassLoader = new FilteringParentClassLoader(); - WebAppClassLoader wacl = new WebAppClassLoader(parentClassLoader, wac); - parentClassLoader.setChild(wacl); - wac.setClassLoader(wacl); - - // Prevent file locking on Windows; pick up file changes. - wac.getInitParams().put( - "org.mortbay.jetty.servlet.Default.useFileMappedBuffer", "false"); + WebAppContext wac = new WebAppContextWithReload( + appRootDir.getAbsolutePath(), "/"); server.setHandler(wac); server.start();
diff --git a/dev/core/src/com/google/gwt/dev/shell/reload-server.gif b/dev/core/src/com/google/gwt/dev/shell/reload-server.gif new file mode 100644 index 0000000..f98857d --- /dev/null +++ b/dev/core/src/com/google/gwt/dev/shell/reload-server.gif Binary files differ