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