Rearrange how module logging is handled.

Patch by: jat
Review by: rdayal


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7027 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/DevModeBase.java b/dev/core/src/com/google/gwt/dev/DevModeBase.java
index 25cb7b3..503df51 100644
--- a/dev/core/src/com/google/gwt/dev/DevModeBase.java
+++ b/dev/core/src/com/google/gwt/dev/DevModeBase.java
@@ -17,7 +17,6 @@
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
-import com.google.gwt.core.ext.TreeLogger.HelpInfo;
 import com.google.gwt.core.ext.linker.ArtifactSet;
 import com.google.gwt.core.ext.linker.impl.StandardLinkerContext;
 import com.google.gwt.dev.Precompile.PrecompileOptionsImpl;
@@ -38,7 +37,6 @@
 import com.google.gwt.dev.ui.DevModeUI;
 import com.google.gwt.dev.ui.DoneCallback;
 import com.google.gwt.dev.ui.DoneEvent;
-import com.google.gwt.dev.ui.DevModeUI.ModuleHandle;
 import com.google.gwt.dev.util.BrowserInfo;
 import com.google.gwt.dev.util.Util;
 import com.google.gwt.dev.util.arg.ArgHandlerGenDir;
@@ -57,7 +55,6 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
@@ -77,22 +74,25 @@
    */
   public class UiBrowserWidgetHostImpl implements BrowserWidgetHost {
 
-    public ModuleSpaceHost createModuleSpaceHost(TreeLogger mainLogger,
-        String moduleName, String userAgent, String url, String tabKey,
-        String sessionKey, BrowserChannelServer serverChannel,
-        byte[] userAgentIcon) throws UnableToCompleteException {
+    public ModuleHandle createModuleLogger(String moduleName, String userAgent,
+        String url, String tabKey, String sessionKey,
+        BrowserChannelServer serverChannel, byte[] userAgentIcon) {
       if (sessionKey == null) {
         // if we don't have a unique session key, make one up
         sessionKey = randomString();
       }
-      TreeLogger logger = mainLogger;
       TreeLogger.Type maxLevel = options.getLogLevel();
       String agentTag = BrowserInfo.getShortName(userAgent);
       String remoteSocket = serverChannel.getRemoteEndpoint();
-      ModuleHandle module = ui.loadModule(userAgent, remoteSocket, url, tabKey,
-          moduleName, sessionKey, agentTag, userAgentIcon, maxLevel);
+      ModuleHandle module = ui.getModuleLogger(userAgent, remoteSocket, url,
+          tabKey, moduleName, sessionKey, agentTag, userAgentIcon, maxLevel);
+      return module;
+    }
+
+    public ModuleSpaceHost createModuleSpaceHost(ModuleHandle module,
+        String moduleName) throws UnableToCompleteException {
       // TODO(jat): add support for closing an active module
-      logger = module.getLogger();
+      TreeLogger logger = module.getLogger();
       try {
         // Try to find an existing loaded version of the module def.
         ModuleDef moduleDef = loadModule(logger, moduleName, true);
@@ -102,26 +102,13 @@
 
         ShellModuleSpaceHost host = doCreateShellModuleSpaceHost(logger,
             moduleDef.getCompilationState(logger), moduleDef);
-
-        loadedModules.put(host, module);
         return host;
       } catch (RuntimeException e) {
         logger.log(TreeLogger.ERROR, "Exception initializing module", e);
-        ui.unloadModule(module);
+        module.unload();
         throw e;
       }
     }
-
-    public TreeLogger getLogger() {
-      return getTopLogger();
-    }
-
-    public void unloadModule(ModuleSpaceHost moduleSpaceHost) {
-      ModuleHandle module = loadedModules.remove(moduleSpaceHost);
-      if (module != null) {
-        ui.unloadModule(module);
-      }
-    }
   }
 
   /**
@@ -589,7 +576,8 @@
 
   private static final AtomicLong uniqueId = new AtomicLong();
 
-  public static String normalizeURL(String unknownUrlText, int port, String host) {
+  public static String normalizeURL(String unknownUrlText, int port,
+      String host) {
     if (unknownUrlText.indexOf(":") != -1) {
       // Assume it's a full url.
       return unknownUrlText;
@@ -676,8 +664,6 @@
 
   private boolean headlessMode = false;
 
-  private final Map<ModuleSpaceHost, ModuleHandle> loadedModules = new IdentityHashMap<ModuleSpaceHost, ModuleHandle>();
-
   private boolean started;
 
   private TreeLogger topLogger;
@@ -709,16 +695,19 @@
   public void launchStartupUrls(final TreeLogger logger) {
     ensureCodeServerListener();
     String startupURL = "";
-    try {
-      for (String prenormalized : options.getStartupURLs()) {
-        startupURL = normalizeURL(prenormalized, getPort(), getHost());
-        logger.log(TreeLogger.TRACE, "Starting URL: " + startupURL, null);
-        launchURL(startupURL);
+    Map<String, URL> startupUrls = new HashMap<String, URL>();
+    for (String prenormalized : options.getStartupURLs()) {
+      startupURL = normalizeURL(prenormalized, getPort(), getHost());
+      logger.log(TreeLogger.TRACE, "Starting URL: " + startupURL, null);
+      try {
+        URL url = processUrl(startupURL);
+        startupUrls.put(prenormalized, url);
+      } catch (UnableToCompleteException e) {
+        logger.log(TreeLogger.ERROR,
+            "Unable to process startup URL " + startupURL, null);
       }
-    } catch (UnableToCompleteException e) {
-      logger.log(TreeLogger.ERROR,
-          "Unable to open new window for startup URL: " + startupURL, null);
     }
+    ui.setStartupUrls(startupUrls);
   }
 
   /**
@@ -737,7 +726,7 @@
       // Eager AWT init for OS X to ensure safe coexistence with SWT.
       BootStrapPlatform.initGui();
 
-      if (startUp() && !options.useRemoteUI()) {
+      if (startUp()) {
         // The web server is running now, so launch browsers for startup urls.
         launchStartupUrls(getTopLogger());
       }
@@ -838,7 +827,7 @@
     if (listener == null) {
       codeServerPort = options.getCodeServerPort();
       listener = new BrowserListener(getTopLogger(), codeServerPort,
-          new OophmSessionHandler(browserHost));
+          new OophmSessionHandler(getTopLogger(), browserHost));
       listener.start();
       try {
         // save the port we actually used if it was auto
@@ -862,55 +851,6 @@
     return headlessMode;
   }
 
-  protected void launchURL(String url) throws UnableToCompleteException {
-    /*
-     * TODO(jat): properly support launching arbitrary browsers -- need some UI
-     * API tweaks to support that.
-     */
-    URL parsedUrl = null;
-    try {
-      parsedUrl = new URL(url);
-      String path = parsedUrl.getPath();
-      String query = parsedUrl.getQuery();
-      String hash = parsedUrl.getRef();
-      String hostedParam = BrowserListener.getDevModeURLParams(listener.getEndpointIdentifier());
-      if (query == null) {
-        query = hostedParam;
-      } else {
-        query += '&' + hostedParam;
-      }
-      path += '?' + query;
-      if (hash != null) {
-        path += '#' + hash;
-      }
-      parsedUrl = new URL(parsedUrl.getProtocol(), parsedUrl.getHost(),
-          parsedUrl.getPort(), path);
-      url = parsedUrl.toExternalForm();
-    } catch (MalformedURLException e) {
-      getTopLogger().log(TreeLogger.ERROR, "Invalid URL " + url, e);
-      throw new UnableToCompleteException();
-    }
-    
-    final URL helpInfoUrl = parsedUrl;
-    getTopLogger().log(TreeLogger.INFO,
-        "Waiting for browser connection to " + url, null, new HelpInfo() {
-          @Override
-          public String getAnchorText() {
-            return "Launch default browser";
-          }
-
-          @Override
-          public String getPrefix() {
-            return "";
-          }
-
-          @Override
-          public URL getURL() {
-            return helpInfoUrl;
-          }
-        });
-  }
-
   /**
    * Perform an initial hosted mode link, without overwriting newer or
    * unmodified files in the output folder.
@@ -951,6 +891,37 @@
     return moduleDef;
   }
 
+  protected URL processUrl(String url) throws UnableToCompleteException {
+    /*
+     * TODO(jat): properly support launching arbitrary browsers -- need some UI
+     * API tweaks to support that.
+     */
+    URL parsedUrl = null;
+    try {
+      parsedUrl = new URL(url);
+      String path = parsedUrl.getPath();
+      String query = parsedUrl.getQuery();
+      String hash = parsedUrl.getRef();
+      String hostedParam = BrowserListener.getDevModeURLParams(listener.getEndpointIdentifier());
+      if (query == null) {
+        query = hostedParam;
+      } else {
+        query += '&' + hostedParam;
+      }
+      path += '?' + query;
+      if (hash != null) {
+        path += '#' + hash;
+      }
+      parsedUrl = new URL(parsedUrl.getProtocol(), parsedUrl.getHost(),
+          parsedUrl.getPort(), path);
+      url = parsedUrl.toExternalForm();
+    } catch (MalformedURLException e) {
+      getTopLogger().log(TreeLogger.ERROR, "Invalid URL " + url, e);
+      throw new UnableToCompleteException();
+    }
+    return parsedUrl;
+  }
+
   protected abstract void produceOutput(TreeLogger logger,
       StandardLinkerContext linkerStack, ArtifactSet artifacts, ModuleDef module)
       throws UnableToCompleteException;
@@ -990,8 +961,8 @@
         return false;
       }
       options.setPort(resultPort);
-      getTopLogger().log(TreeLogger.TRACE,
-          "Started web server on port " + resultPort);
+      getTopLogger().log(TreeLogger.TRACE, "Started web server on port "
+          + resultPort);
     }
 
     return true;
diff --git a/dev/core/src/com/google/gwt/dev/HeadlessUI.java b/dev/core/src/com/google/gwt/dev/HeadlessUI.java
index 6a6ce61..c70e10f 100644
--- a/dev/core/src/com/google/gwt/dev/HeadlessUI.java
+++ b/dev/core/src/com/google/gwt/dev/HeadlessUI.java
@@ -34,17 +34,16 @@
   }
 
   @Override
-  public ModuleHandle loadModule(String userAgent, String remoteSocket,
+  public ModuleHandle getModuleLogger(String userAgent, String remoteSocket,
       String url, String tabKey, String moduleName, String sessionKey,
       String agentTag, byte[] agentIcon, Type logLevel) {
     return new ModuleHandle() {
       public TreeLogger getLogger() {
         return getConsoleLogger();
       }
-    };
-  }
 
-  @Override
-  public void unloadModule(ModuleHandle module) {
+      public void unload() {
+      }
+    };
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/ModuleHandle.java b/dev/core/src/com/google/gwt/dev/ModuleHandle.java
new file mode 100644
index 0000000..4da2f92
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/ModuleHandle.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2009 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.TreeLogger;
+
+/**
+ * Opaque handle to a module instance - external code can only get a logger or
+ * notify the module handle it is no longer needed.
+ */
+public interface ModuleHandle {
+
+  /**
+   * @return the logger for this module.
+   */
+  TreeLogger getLogger();
+  
+  /**
+   * Mark the module instance associated with this handle as unloaded.
+   */
+  void unload();
+}
diff --git a/dev/core/src/com/google/gwt/dev/SwingUI.java b/dev/core/src/com/google/gwt/dev/SwingUI.java
index 1a1fbfd..eb2cd1b 100644
--- a/dev/core/src/com/google/gwt/dev/SwingUI.java
+++ b/dev/core/src/com/google/gwt/dev/SwingUI.java
@@ -16,6 +16,7 @@
 package com.google.gwt.dev;
 
 import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.TreeLogger.HelpInfo;
 import com.google.gwt.core.ext.TreeLogger.Type;
 import com.google.gwt.dev.DevModeBase.HostedModeBaseOptions;
 import com.google.gwt.dev.WebServerPanel.RestartAction;
@@ -35,6 +36,7 @@
 import java.lang.reflect.Method;
 import java.net.URL;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
@@ -78,6 +80,12 @@
     public ModulePanel getTab() {
       return tab;
     }
+
+    public void unload() {
+      if (tab != null) {
+        tab.disconnect();
+      }
+    }
   }
 
   /**
@@ -164,6 +172,36 @@
   }
 
   @Override
+  public ModuleHandle getModuleLogger(String userAgent, String remoteSocket,
+      String url, String tabKey, String moduleName, String sessionKey,
+      String agentTag, byte[] agentIcon, TreeLogger.Type logLevel) {
+    // TODO(jat): add support for closing an active module
+    ModuleTabPanel tabPanel = null;
+    ModulePanel tab = null;
+    tabPanel = findModuleTab(userAgent, remoteSocket, url, tabKey,
+        moduleName, agentIcon);
+    tab = tabPanel.addModuleSession(logLevel, moduleName, sessionKey,
+        options.getLogFile(String.format("%s-%s-%d.log", moduleName, agentTag,
+            getNextSessionCounter(options.getLogDir()))));
+    TreeLogger logger = tab.getLogger();
+    TreeLogger branch = logger.branch(TreeLogger.INFO, "Loading module "
+        + moduleName);
+    if (url != null) {
+      branch.log(TreeLogger.INFO, "Top URL: " + url);
+    }
+    branch.log(TreeLogger.INFO, "User agent: " + userAgent);
+    branch.log(TreeLogger.TRACE, "Remote socket: " + remoteSocket);
+    if (tabKey != null) {
+      branch.log(TreeLogger.DEBUG, "Tab key: " + tabKey);
+    }
+    if (sessionKey != null) {
+      branch.log(TreeLogger.DEBUG, "Session key: " + sessionKey);
+    }
+    // TODO: Switch to a wait cursor?
+    return new SwingModuleHandle(logger, tab);
+  }
+
+  @Override
   public TreeLogger getTopLogger() {
     return topLogger;
   }
@@ -229,41 +267,30 @@
   }
 
   @Override
-  public ModuleHandle loadModule(String userAgent, String remoteSocket,
-      String url, String tabKey, String moduleName, String sessionKey,
-      String agentTag, byte[] agentIcon, TreeLogger.Type logLevel) {
-    // TODO(jat): add support for closing an active module
-    ModuleTabPanel tabPanel = null;
-    ModulePanel tab = null;
-    tabPanel = findModuleTab(userAgent, remoteSocket, url, tabKey,
-        moduleName, agentIcon);
-    tab = tabPanel.addModuleSession(logLevel, moduleName, sessionKey,
-        options.getLogFile(String.format("%s-%s-%d.log", moduleName, agentTag,
-            getNextSessionCounter(options.getLogDir()))));
-    TreeLogger logger = tab.getLogger();
-    TreeLogger branch = logger.branch(TreeLogger.INFO, "Loading module "
-        + moduleName);
-    if (url != null) {
-      branch.log(TreeLogger.INFO, "Top URL: " + url);
-    }
-    branch.log(TreeLogger.INFO, "User agent: " + userAgent);
-    branch.log(TreeLogger.TRACE, "Remote socket: " + remoteSocket);
-    if (tabKey != null) {
-      branch.log(TreeLogger.DEBUG, "Tab key: " + tabKey);
-    }
-    if (sessionKey != null) {
-      branch.log(TreeLogger.DEBUG, "Session key: " + sessionKey);
-    }
-    // TODO: Switch to a wait cursor?
-    return new SwingModuleHandle(logger, tab);
-  }
+  public void setStartupUrls(Map<String, URL> urls) {
+    // TODO(jat): provide UI for selecting URLs and launching
+    ArrayList<String> keys = new ArrayList<String>(urls.keySet());
+    Collections.sort(keys);
+    for (String url : keys) {
+      final URL helpInfoUrl = urls.get(url);
+      getTopLogger().log(TreeLogger.INFO, "Waiting for browser connection to "
+          + helpInfoUrl.toExternalForm(), null,
+          new HelpInfo() {
+            @Override
+            public String getAnchorText() {
+              return "Launch default browser";
+            }
 
-  @Override
-  public void unloadModule(ModuleHandle module) {
-    SwingModuleHandle handle = (SwingModuleHandle) module;
-    Disconnectable tab = handle.getTab();
-    if (tab != null) {
-      tab.disconnect();
+            @Override
+            public String getPrefix() {
+              return "";
+            }
+
+            @Override
+            public URL getURL() {
+              return helpInfoUrl;
+            }
+          });
     }
   }
 
diff --git a/dev/core/src/com/google/gwt/dev/shell/BrowserChannel.java b/dev/core/src/com/google/gwt/dev/shell/BrowserChannel.java
index 96dd267..5e536fd 100644
--- a/dev/core/src/com/google/gwt/dev/shell/BrowserChannel.java
+++ b/dev/core/src/com/google/gwt/dev/shell/BrowserChannel.java
@@ -254,8 +254,7 @@
 
     /**
      * Load a new instance of a module.
-     * 
-     * @param logger
+     *
      * @param channel
      * @param moduleName
      * @param userAgent
@@ -267,9 +266,9 @@
      *     24x24) representing the user agent or null if unavailable
      * @return a TreeLogger to use for the module's logs
      */
-    public abstract TreeLogger loadModule(TreeLogger logger,
-        BrowserChannel channel, String moduleName, String userAgent, String url,
-        String tabKey, String sessionKey, byte[] userAgentIcon);
+    public abstract TreeLogger loadModule(BrowserChannel channel,
+        String moduleName, String userAgent, String url, String tabKey,
+        String sessionKey, byte[] userAgentIcon);
 
     public abstract ExceptionOrReturnValue setProperty(BrowserChannel channel,
         int refId, int dispId, Value newValue);
diff --git a/dev/core/src/com/google/gwt/dev/shell/BrowserChannelServer.java b/dev/core/src/com/google/gwt/dev/shell/BrowserChannelServer.java
index 52801b5..b9c062a 100644
--- a/dev/core/src/com/google/gwt/dev/shell/BrowserChannelServer.java
+++ b/dev/core/src/com/google/gwt/dev/shell/BrowserChannelServer.java
@@ -373,7 +373,7 @@
     Thread.currentThread().setName(
         "Code server for " + moduleName + " from " + userAgent + " on " + url
         + " @ " + sessionKey);
-    logger = handler.loadModule(logger, this, moduleName, userAgent, url,
+    logger = handler.loadModule(this, moduleName, userAgent, url,
         tabKey, sessionKey, iconBytes);
     try {
       // send LoadModule response
diff --git a/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHost.java b/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHost.java
index 1002d4e..0e7bd0e 100644
--- a/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHost.java
+++ b/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHost.java
@@ -15,8 +15,8 @@
  */
 package com.google.gwt.dev.shell;
 
-import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.ModuleHandle;
 
 /**
  * Interface that unifies access to the <code>BrowserWidget</code>,
@@ -25,9 +25,8 @@
 public interface BrowserWidgetHost {
 
   /**
-   * For OOPHM.
+   * Create a logger for a module instance.
    * 
-   * @param logger
    * @param moduleName
    * @param userAgent
    * @param url URL of top-level window, may be null for old browser plugins
@@ -37,16 +36,22 @@
    * @param serverChannel connection from the client
    * @param userAgentIcon byte array containing an icon (which fits in 24x24)
    *     for this user agent or null if unavailable
+   * @return ModuleHandle instance -- caller is responsible for calling
+   *     {@link ModuleHandle#unload()} on it when done
    */
-  ModuleSpaceHost createModuleSpaceHost(TreeLogger logger, String moduleName,
-      String userAgent, String url, String tabKey, String sessionKey,
-      BrowserChannelServer serverChannel, byte[] userAgentIcon)
-      throws UnableToCompleteException;
-
-  TreeLogger getLogger();
+  ModuleHandle createModuleLogger(String moduleName, String userAgent,
+      String url, String tabKey, String sessionKey,
+      BrowserChannelServer serverChannel, byte[] userAgentIcon);
 
   /**
-   * For OOPHM.
+   * Create a ModuleSpaceHost for the specified module.
+   * 
+   * @param module ModuleHandle returned from a previous createModuleLogger
+   *     call.
+   * @param moduleName name of module to create
+   * @return ModuleSpaceHost instance
+   * @throws UnableToCompleteException 
    */
-  void unloadModule(ModuleSpaceHost moduleSpaceHost);
+  ModuleSpaceHost createModuleSpaceHost(ModuleHandle module, String moduleName)
+      throws UnableToCompleteException;
 }
diff --git a/dev/core/src/com/google/gwt/dev/shell/HtmlUnitSessionHandler.java b/dev/core/src/com/google/gwt/dev/shell/HtmlUnitSessionHandler.java
index 9c87d9f..a2a415c 100644
--- a/dev/core/src/com/google/gwt/dev/shell/HtmlUnitSessionHandler.java
+++ b/dev/core/src/com/google/gwt/dev/shell/HtmlUnitSessionHandler.java
@@ -250,9 +250,9 @@
   }
 
   @Override
-  public TreeLogger loadModule(TreeLogger logger, BrowserChannel channel,
-      String moduleName, String userAgent, String url, String tabKey,
-      String sessionKey, byte[] userAgentIcon) {
+  public TreeLogger loadModule(BrowserChannel channel, String moduleName,
+      String userAgent, String url, String tabKey, String sessionKey,
+      byte[] userAgentIcon) {
     throw new UnsupportedOperationException("loadModule must not be called");
   }
 
diff --git a/dev/core/src/com/google/gwt/dev/shell/OophmSessionHandler.java b/dev/core/src/com/google/gwt/dev/shell/OophmSessionHandler.java
index f65ec0f..91482e9 100644
--- a/dev/core/src/com/google/gwt/dev/shell/OophmSessionHandler.java
+++ b/dev/core/src/com/google/gwt/dev/shell/OophmSessionHandler.java
@@ -16,6 +16,7 @@
 package com.google.gwt.dev.shell;
 
 import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.dev.ModuleHandle;
 import com.google.gwt.dev.shell.BrowserChannel.SessionHandler;
 import com.google.gwt.dev.shell.BrowserChannel.Value;
 import com.google.gwt.dev.shell.JsValue.DispatchMethod;
@@ -34,16 +35,20 @@
 
   private BrowserWidgetHost host;
 
-  private TreeLogger logger;
-
   private Map<BrowserChannelServer, ModuleSpace> moduleMap = Collections.synchronizedMap(new HashMap<BrowserChannelServer, ModuleSpace>());
 
+  private Map<BrowserChannelServer, ModuleHandle> moduleHandleMap = Collections.synchronizedMap(new HashMap<BrowserChannelServer, ModuleHandle>());
+
+  private final TreeLogger topLogger;
+
   /**
    * Listens for new connections from browsers.
+   * @param topLogger logger to use for non-module-related messages
+   * @param host BrowserWidgetHost instance
    */
-  public OophmSessionHandler(BrowserWidgetHost host) {
+  public OophmSessionHandler(TreeLogger topLogger, BrowserWidgetHost host) {
     this.host = host;
-    logger = host.getLogger();
+    this.topLogger = topLogger;
   }
 
   @Override
@@ -60,7 +65,9 @@
       int dispId) {
     BrowserChannelServer serverChannel = (BrowserChannelServer) channel;
     ModuleSpace moduleSpace = moduleMap.get(serverChannel);
-    assert moduleSpace != null;
+    ModuleHandle moduleHandle = moduleHandleMap.get(serverChannel);
+    assert moduleSpace != null && moduleHandle != null;
+    TreeLogger logger = moduleHandle.getLogger();
     ServerObjectsTable localObjects = serverChannel.getJavaObjectsExposedInBrowser();
     try {
       JsValueOOPHM obj = new JsValueOOPHM();
@@ -94,7 +101,9 @@
     BrowserChannelServer serverChannel = (BrowserChannelServer) channel;
     ServerObjectsTable localObjects = serverChannel.getJavaObjectsExposedInBrowser();
     ModuleSpace moduleSpace = moduleMap.get(serverChannel);
-    assert moduleSpace != null;
+    ModuleHandle moduleHandle = moduleHandleMap.get(serverChannel);
+    assert moduleSpace != null && moduleHandle != null;
+    TreeLogger logger = moduleHandle.getLogger();
     CompilingClassLoader cl = moduleSpace.getIsolatedClassLoader();
 
     // Treat dispatch id 0 as toString()
@@ -155,21 +164,21 @@
   }
 
   @Override
-  public synchronized TreeLogger loadModule(TreeLogger loadModuleLogger,
-      BrowserChannel channel, String moduleName, String userAgent, String url,
-      String tabKey, String sessionKey, byte[] userAgentIcon) {
-    logger = loadModuleLogger;
+  public synchronized TreeLogger loadModule(BrowserChannel channel,
+      String moduleName, String userAgent, String url, String tabKey,
+      String sessionKey, byte[] userAgentIcon) {
+    PerfLogger.start("OophmSessionHandler.loadModule " + moduleName);
+    BrowserChannelServer serverChannel = (BrowserChannelServer) channel;
+    ModuleHandle moduleHandle = host.createModuleLogger(moduleName, userAgent,
+        url, tabKey, sessionKey, serverChannel, userAgentIcon);
+    TreeLogger logger = moduleHandle.getLogger();
+    moduleHandleMap.put(serverChannel, moduleHandle);
+    ModuleSpace moduleSpace = null;
     try {
-      PerfLogger.start("OophmSessionHandler.loadModule " + moduleName);
       // Attach a new ModuleSpace to make it programmable.
-      //
-      BrowserChannelServer serverChannel = (BrowserChannelServer) channel;
-      ModuleSpaceHost msh = host.createModuleSpaceHost(loadModuleLogger,
-          moduleName, userAgent, url, tabKey, sessionKey, serverChannel,
-          userAgentIcon);
-      logger = msh.getLogger();
-      ModuleSpace moduleSpace = new ModuleSpaceOOPHM(msh, moduleName,
-          serverChannel);
+      ModuleSpaceHost msh = host.createModuleSpaceHost(moduleHandle,
+          moduleName);
+      moduleSpace = new ModuleSpaceOOPHM(msh, moduleName, serverChannel);
       moduleMap.put(serverChannel, moduleSpace);
       PerfLogger.start("ModuleSpace.onLoad");
       moduleSpace.onLoad(logger);
@@ -178,14 +187,20 @@
       // that can go wrong trying to load a module, including Error-derived
       // things like NoClassDefFoundError.
       // 
-      this.logger.log(TreeLogger.ERROR, "Failed to load module '" + moduleName
-          + "' from user agent '" + userAgent + "' at "
+      moduleHandle.getLogger().log(TreeLogger.ERROR, "Failed to load module '"
+          + moduleName + "' from user agent '" + userAgent + "' at "
           + channel.getRemoteEndpoint(), e);
+      if (moduleSpace != null) {
+        moduleSpace.dispose();        
+      }
+      moduleHandle.unload();
+      moduleMap.remove(serverChannel);
+      moduleHandleMap.remove(serverChannel);
     } finally {
       PerfLogger.end();
       PerfLogger.end();
     }
-    return this.logger;
+    return moduleHandle.getLogger();
   }
 
   @Override
@@ -193,7 +208,9 @@
       int dispId, Value newValue) {
     BrowserChannelServer serverChannel = (BrowserChannelServer) channel;
     ModuleSpace moduleSpace = moduleMap.get(serverChannel);
-    assert moduleSpace != null;
+    ModuleHandle moduleHandle = moduleHandleMap.get(serverChannel);
+    assert moduleSpace != null && moduleHandle != null;
+    TreeLogger logger = moduleHandle.getLogger();
     ServerObjectsTable localObjects = serverChannel.getJavaObjectsExposedInBrowser();
     try {
       JsValueOOPHM obj = new JsValueOOPHM();
@@ -220,16 +237,18 @@
   @Override
   public void unloadModule(BrowserChannel channel, String moduleName) {
     BrowserChannelServer serverChannel = (BrowserChannelServer) channel;
+    ModuleHandle moduleHandle = moduleHandleMap.get(serverChannel);
     ModuleSpace moduleSpace = moduleMap.get(serverChannel);
-    if (moduleSpace == null) {
-      logger.log(TreeLogger.ERROR, "Unload request without a module loaded",
+    if (moduleSpace == null || moduleHandle == null) {
+      topLogger.log(TreeLogger.ERROR, "Unload request without a module loaded",
           null);
       return;
     }
-    logger.log(TreeLogger.INFO, "Unloading module "
+    moduleHandle.getLogger().log(TreeLogger.INFO, "Unloading module "
         + moduleSpace.getModuleName() + " (" + moduleName + ")", null);
-    host.unloadModule(moduleSpace.host);
     moduleSpace.dispose();
+    moduleHandle.unload();
     moduleMap.remove(serverChannel);
+    moduleHandleMap.remove(serverChannel);
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/shell/remoteui/RemoteUI.java b/dev/core/src/com/google/gwt/dev/shell/remoteui/RemoteUI.java
index dcb7968..1d81ada 100644
--- a/dev/core/src/com/google/gwt/dev/shell/remoteui/RemoteUI.java
+++ b/dev/core/src/com/google/gwt/dev/shell/remoteui/RemoteUI.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.TreeLogger.Type;
+import com.google.gwt.dev.ModuleHandle;
 import com.google.gwt.dev.shell.BrowserListener;
 import com.google.gwt.dev.ui.DevModeUI;
 import com.google.gwt.dev.ui.DoneCallback;
@@ -85,7 +86,7 @@
   }
 
   @Override
-  public ModuleHandle loadModule(String userAgent, String remoteSocket,
+  public ModuleHandle getModuleLogger(String userAgent, String remoteSocket,
       String url, String tabKey, String moduleName, String sessionKey,
       String agentTag, byte[] agentIcon, Type logLevel) {
 
@@ -100,6 +101,24 @@
       public TreeLogger getLogger() {
         return moduleLogger;
       }
+
+      public void unload() {
+        synchronized (modulesLock) {
+          if (!modules.contains(this)) {
+            return;
+          }
+        }
+
+        ViewerServiceTreeLogger moduleLogger = (ViewerServiceTreeLogger) (getLogger());
+
+        try {
+          viewerServiceClient.disconnectLog(moduleLogger.getLogHandle());
+        } finally {
+          synchronized (modulesLock) {
+            modules.remove(this);
+          }
+        }
+      }
     };
     synchronized (modulesLock) {
       modules.add(handle);
@@ -154,23 +173,4 @@
   public boolean supportsRestartWebServer() {
     return hasCallback(RestartServerEvent.getType());
   }
-
-  @Override
-  public void unloadModule(ModuleHandle module) {
-    synchronized (modulesLock) {
-      if (!modules.contains(module)) {
-        return;
-      }
-    }
-
-    ViewerServiceTreeLogger moduleLogger = (ViewerServiceTreeLogger) (module.getLogger());
-
-    try {
-      viewerServiceClient.disconnectLog(moduleLogger.getLogHandle());
-    } finally {
-      synchronized (modulesLock) {
-        modules.remove(module);
-      }
-    }
-  }
 }
diff --git a/dev/core/src/com/google/gwt/dev/ui/CloseModuleCallback.java b/dev/core/src/com/google/gwt/dev/ui/CloseModuleCallback.java
index 5ef7b74..52bdd7e 100644
--- a/dev/core/src/com/google/gwt/dev/ui/CloseModuleCallback.java
+++ b/dev/core/src/com/google/gwt/dev/ui/CloseModuleCallback.java
@@ -15,7 +15,7 @@
  */
 package com.google.gwt.dev.ui;
 
-import com.google.gwt.dev.ui.DevModeUI.ModuleHandle;
+import com.google.gwt.dev.ModuleHandle;
 
 /**
  * Callback for a request to close an active module from the UI.
@@ -28,7 +28,7 @@
    * the UI), there will be separate calls for each one.
    * 
    * @param moduleHandle module handle returned from
-   *     {@link DevModeUI#loadModule}. 
+   *     {@link DevModeUI#getModuleLogger}. 
    */
   void onCloseModule(ModuleHandle moduleHandle);
-}
\ No newline at end of file
+}
diff --git a/dev/core/src/com/google/gwt/dev/ui/DevModeUI.java b/dev/core/src/com/google/gwt/dev/ui/DevModeUI.java
index f77f545..572baf6 100644
--- a/dev/core/src/com/google/gwt/dev/ui/DevModeUI.java
+++ b/dev/core/src/com/google/gwt/dev/ui/DevModeUI.java
@@ -17,8 +17,10 @@
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.TreeLogger.Type;
+import com.google.gwt.dev.ModuleHandle;
 import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
 
+import java.net.URL;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -29,19 +31,6 @@
 public abstract class DevModeUI {
 
   /**
-   * Opaque handle to a module - it is returned from loadModule and the client
-   * can only pass it to unloadModule or get a logger for messages about that
-   * module.
-   */
-  public interface ModuleHandle {
-
-    /**
-     * @return the logger for this module.
-     */
-    TreeLogger getLogger();
-  }
-
-  /**
    * Map of callbacks.
    */
   private final Map<UiEvent.Type<? extends UiCallback>, UiCallback> callbacks
@@ -58,45 +47,6 @@
   private Type logLevel;
 
   /**
-   * Create a top-level logger for messages which are not associated with the
-   * web server or any module.  Defaults to logging to stdout.
-   * 
-   * @return TreeLogger instance to use
-   */
-  public TreeLogger getTopLogger() {
-    return getConsoleLogger();
-  }
-
-  /**
-   * Create the web server portion of the UI if not already created, and
-   * return its TreeLogger instance.
-   * 
-   * <p>Note that the {@link RestartServerEvent} should already have a callback
-   * registered when this is called -- the UI is not required to change the
-   * UI if it is registered later.
-   * 
-   * @param serverName short name of the web server or null if only the icon
-   *     should be used
-   * @param serverIcon byte array containing an icon (fitting into 24x24) to
-   *     use for the server, or null if only the name should be used
-   * @return TreeLogger instance
-   */
-  public abstract TreeLogger getWebServerLogger(String serverName,
-      byte[] serverIcon);
-  
-  /**
-   * Initialize the UI - must be called exactly once and before any other method
-   * on this class.
-   * 
-   * <p>Subclasses should call super.initialize(logLevel).
-   * 
-   * @param logLevel log level for all logging
-   */
-  public void initialize(Type logLevel) {
-    this.logLevel = logLevel;
-  }
-
-  /**
    * Show that a module is loaded in the UI.
    * 
    * <p>Note that the {@link CloseModuleEvent} should already have a callback
@@ -117,9 +67,48 @@
    * @param logLevel logging detail requested
    * @return a handle to the module
    */
-  public abstract ModuleHandle loadModule(String userAgent,
+  public abstract ModuleHandle getModuleLogger(String userAgent,
       String remoteSocket, String url, String tabKey, String moduleName,
       String sessionKey, String agentTag, byte[] agentIcon, Type logLevel);
+
+  /**
+   * Create a top-level logger for messages which are not associated with the
+   * web server or any module.  Defaults to logging to stdout.
+   * 
+   * @return TreeLogger instance to use
+   */
+  public TreeLogger getTopLogger() {
+    return getConsoleLogger();
+  }
+  
+  /**
+   * Create the web server portion of the UI if not already created, and
+   * return its TreeLogger instance.
+   * 
+   * <p>Note that the {@link RestartServerEvent} should already have a callback
+   * registered when this is called -- the UI is not required to change the
+   * UI if it is registered later.
+   * 
+   * @param serverName short name of the web server or null if only the icon
+   *     should be used
+   * @param serverIcon byte array containing an icon (fitting into 24x24) to
+   *     use for the server, or null if only the name should be used
+   * @return TreeLogger instance
+   */
+  public abstract TreeLogger getWebServerLogger(String serverName,
+      byte[] serverIcon);
+
+  /**
+   * Initialize the UI - must be called exactly once and before any other method
+   * on this class.
+   * 
+   * <p>Subclasses should call super.initialize(logLevel).
+   * 
+   * @param logLevel log level for all logging
+   */
+  public void initialize(Type logLevel) {
+    this.logLevel = logLevel;
+  }
   
   /**
    * Sets the callback for a given event type..
@@ -135,13 +124,15 @@
   }
 
   /**
-   * Show that a previously loaded module has been unloaded.
+   * Set the URLs that should be available to start.
    * 
-   * @param module ModuleHandle instance returned from loadModule on this UI
-   *     instance
+   * @param urls map of URLs -- the key is the name supplied with -startupUrls,
+   *     and the value is the mapped URL with all parameters included
    */
-  public abstract void unloadModule(ModuleHandle module);
-
+  public void setStartupUrls(Map<String, URL> urls) {
+    // do nothing by default
+  }
+  
   /**
    * Call callback for a given event.
    * 
diff --git a/dev/core/test/com/google/gwt/dev/shell/BrowserChannelServerTest.java b/dev/core/test/com/google/gwt/dev/shell/BrowserChannelServerTest.java
index 6c47e9f..d981c37 100644
--- a/dev/core/test/com/google/gwt/dev/shell/BrowserChannelServerTest.java
+++ b/dev/core/test/com/google/gwt/dev/shell/BrowserChannelServerTest.java
@@ -156,9 +156,9 @@
     }
 
     @Override
-    public TreeLogger loadModule(TreeLogger logger, BrowserChannel channel,
-        String moduleName, String userAgent, String url, String tabKey,
-        String sessionKey, byte[] userAgentIcon) {
+    public TreeLogger loadModule(BrowserChannel channel, String moduleName,
+        String userAgent, String url, String tabKey, String sessionKey,
+        byte[] userAgentIcon) {
       loadedModule = moduleName;
       this.moduleName = moduleName;
       this.userAgent = userAgent;
@@ -166,7 +166,7 @@
       this.tabKey = tabKey;
       this.sessionKey = sessionKey;
       this.userAgentIcon = userAgentIcon;
-      return logger;
+      return new FailErrorLogger();
     }
 
     @Override