adding a -logdir flag to hosted mode; no more taking screenshots of interesting stuff.

Review by: jat

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@6105 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/CompileTaskRunner.java b/dev/core/src/com/google/gwt/dev/CompileTaskRunner.java
index 5b47004..02f8348 100644
--- a/dev/core/src/com/google/gwt/dev/CompileTaskRunner.java
+++ b/dev/core/src/com/google/gwt/dev/CompileTaskRunner.java
@@ -46,7 +46,8 @@
     if (options.isUseGuiLogger()) {
       // Initialize a tree logger window.
       DetachedTreeLoggerWindow loggerWindow = DetachedTreeLoggerWindow.getInstance(
-          "Build Output for " + options.getModuleNames(), 800, 600, true);
+          "Build Output for " + options.getModuleNames(), 800, 600, true,
+          options.getLogLevel());
 
       // Eager AWT initialization for OS X to ensure safe coexistence with SWT.
       BootStrapPlatform.initGui();
diff --git a/dev/core/src/com/google/gwt/dev/HostedModeBase.java b/dev/core/src/com/google/gwt/dev/HostedModeBase.java
index 1496a6d..44fd711 100644
--- a/dev/core/src/com/google/gwt/dev/HostedModeBase.java
+++ b/dev/core/src/com/google/gwt/dev/HostedModeBase.java
@@ -86,6 +86,37 @@
   }
 
   /**
+   * Handles the -logdir command line option.
+   */
+  protected static class ArgHandlerLogDir extends ArgHandlerString {
+    private final OptionLogDir options;
+
+    public ArgHandlerLogDir(OptionLogDir options) {
+      this.options = options;
+    }
+
+    @Override
+    public String getPurpose() {
+      return "Logs to a file in the given directory, as well as graphically";
+    }
+
+    @Override
+    public String getTag() {
+      return "-logdir";
+    }
+    @Override
+    public String[] getTagArgs() {
+      return new String[] {"directory"};
+    }
+
+    @Override
+    public boolean setString(String value) {
+      options.setLogFile(value);
+      return true;
+    }
+  }
+
+  /**
    * Handles the -noserver command line flag.
    */
   protected static class ArgHandlerNoServerFlag extends ArgHandlerFlag {
@@ -232,8 +263,9 @@
     }
   }
 
-  protected interface HostedModeBaseOptions extends JJSOptions, OptionLogLevel,
-      OptionGenDir, OptionNoServer, OptionPort, OptionStartupURLs {
+  protected interface HostedModeBaseOptions extends JJSOptions, OptionLogDir,
+      OptionLogLevel, OptionGenDir, OptionNoServer, OptionPort,
+      OptionStartupURLs {
 
     /**
      * The base shell work directory.
@@ -248,6 +280,7 @@
       PrecompileOptionsImpl implements HostedModeBaseOptions {
 
     private boolean isNoServer;
+    private File logDir;
     private int port;
     private final List<String> startupURLs = new ArrayList<String>();
 
@@ -255,6 +288,20 @@
       startupURLs.add(url);
     }
 
+    public boolean alsoLogToFile() {
+      return logDir != null;
+    }
+
+    public File getLogDir() {
+      return logDir;
+    }
+    public File getLogFile(String sublog) {
+      if (logDir == null) {
+        return null;
+      }
+      return new File(logDir, sublog);
+    }
+
     public int getPort() {
       return port;
     }
@@ -271,6 +318,10 @@
       return isNoServer;
     }
 
+    public void setLogFile(String filename) {
+      logDir = new File(filename);
+    }
+
     public void setNoServer(boolean isNoServer) {
       this.isNoServer = isNoServer;
     }
@@ -281,6 +332,20 @@
   }
 
   /**
+   * Controls whether and where to log data to file.
+   * 
+   */
+  protected interface OptionLogDir {
+    boolean alsoLogToFile();
+
+    File getLogDir();
+
+    File getLogFile(String subfile);
+
+    void setLogFile(String filename);
+  }
+
+  /**
    * Controls whether to run a server or not.
    * 
    */
@@ -317,6 +382,7 @@
       registerHandler(new ArgHandlerPort(options));
       registerHandler(new ArgHandlerWhitelist());
       registerHandler(new ArgHandlerBlacklist());
+      registerHandler(new ArgHandlerLogDir(options));
       registerHandler(new ArgHandlerLogLevel(options));
       registerHandler(new ArgHandlerGenDir(options));
       registerHandler(new ArgHandlerScriptStyle(options));
@@ -480,7 +546,7 @@
     // Initialize the logger.
     //
     initializeLogger();
-    
+
     // Check for updates
     final TreeLogger logger = getTopLogger();
     final CheckForUpdates updateChecker
diff --git a/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java b/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java
index 1633bfe..38ee586 100644
--- a/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java
+++ b/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java
@@ -285,7 +285,9 @@
     shell.setImages(ShellMainWindow.getIcons());
 
     mainWnd = new ShellMainWindow(this, shell, getTitleText(),
-        options.isNoServer() ? 0 : getPort());
+        options.isNoServer() ? 0 : getPort(), 
+        options.alsoLogToFile() ? options.getLogFile("hosted.log") : null,
+        options.getLogLevel());
 
     shell.setSize(700, 600);
     if (!isHeadless()) {
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 2af6125..8a3bebc 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.core.ext.TreeLogger.Type;
 import com.google.gwt.dev.shell.BrowserWindowController.WebServerRestart;
 import com.google.gwt.dev.shell.log.TreeLoggerWidget;
 import com.google.gwt.dev.util.Util;
@@ -38,6 +39,8 @@
 import org.eclipse.swt.widgets.Shell;
 import org.eclipse.swt.widgets.ToolItem;
 
+import java.io.File;
+
 /**
  * Implements the GWTShell's main window control.
  */
@@ -201,7 +204,8 @@
   private Toolbar toolbar;
 
   public ShellMainWindow(BrowserWindowController browserWindowController,
-      Shell parent, String titleText, int serverPort) {
+      Shell parent, String titleText, int serverPort, File logFile,
+      Type logLevel) {
     super(parent, SWT.NONE);
     this.browserWindowController = browserWindowController;
 
@@ -235,7 +239,7 @@
 
     // Create the log pane.
     {
-      logPane = new TreeLoggerWidget(this);
+      logPane = new TreeLoggerWidget(this, logFile, logLevel);
       GridData data = new GridData();
       data.grabExcessHorizontalSpace = true;
       data.grabExcessVerticalSpace = true;
diff --git a/dev/core/src/com/google/gwt/dev/shell/log/DetachedTreeLoggerWindow.java b/dev/core/src/com/google/gwt/dev/shell/log/DetachedTreeLoggerWindow.java
index 5a44b99..0aaf638 100644
--- a/dev/core/src/com/google/gwt/dev/shell/log/DetachedTreeLoggerWindow.java
+++ b/dev/core/src/com/google/gwt/dev/shell/log/DetachedTreeLoggerWindow.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.dev.shell.log;
 
+import com.google.gwt.core.ext.TreeLogger.Type;
 import com.google.gwt.dev.shell.LowLevel;
 import com.google.gwt.dev.util.log.AbstractTreeLogger;
 
@@ -42,10 +43,10 @@
    */
   public static synchronized DetachedTreeLoggerWindow getInstance(
       final String caption, final int width, final int height,
-      final boolean autoScroll) {
+      final boolean autoScroll, Type logLevel) {
     if (singleton == null) {
       singleton = new DetachedTreeLoggerWindow(caption, width, height,
-          autoScroll);
+          autoScroll, logLevel);
     }
     return singleton;
   }
@@ -55,7 +56,7 @@
   private boolean isRunning = false;
 
   private DetachedTreeLoggerWindow(final String caption, final int width,
-      final int height, final boolean autoScroll) {
+      final int height, final boolean autoScroll, Type logLevel) {
 
     shell = new Shell(Display.getCurrent());
     shell.setText(caption);
@@ -64,7 +65,8 @@
     fillLayout.marginHeight = 0;
     shell.setLayout(fillLayout);
 
-    final TreeLoggerWidget treeLoggerWidget = new TreeLoggerWidget(shell);
+    final TreeLoggerWidget treeLoggerWidget = new TreeLoggerWidget(shell, null,
+        logLevel);
     treeLoggerWidget.setAutoScroll(autoScroll);
     logger = treeLoggerWidget.getLogger();
 
diff --git a/dev/core/src/com/google/gwt/dev/shell/log/TreeLoggerWidget.java b/dev/core/src/com/google/gwt/dev/shell/log/TreeLoggerWidget.java
index 47f80d0..2bfc484 100644
--- a/dev/core/src/com/google/gwt/dev/shell/log/TreeLoggerWidget.java
+++ b/dev/core/src/com/google/gwt/dev/shell/log/TreeLoggerWidget.java
@@ -15,10 +15,14 @@
  */
 package com.google.gwt.dev.shell.log;
 
+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.shell.BrowserWidget;
 import com.google.gwt.dev.shell.log.TreeItemLogger.LogEvent;
 import com.google.gwt.dev.util.log.AbstractTreeLogger;
+import com.google.gwt.dev.util.log.CompositeTreeLogger;
+import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
 
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.SashForm;
@@ -43,6 +47,8 @@
 import org.eclipse.swt.widgets.Tree;
 import org.eclipse.swt.widgets.TreeItem;
 
+import java.io.File;
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.net.URL;
@@ -57,11 +63,19 @@
 
   private final Text details;
 
-  private final TreeItemLogger logger;
+  private final AbstractTreeLogger logger;
+  
+  private final TreeItemLogger uiLogger;
 
   private final Tree tree;
 
-  public TreeLoggerWidget(Composite parent) {
+  /**
+   * Creates a graphical widget, and optionally logging to file as well.
+   * 
+   * @param parent the graphical interface parent
+   * @param logFile the file to log to
+   */
+  public TreeLoggerWidget(Composite parent, File logFile, Type logLevel) {
     super(parent, SWT.NONE);
 
     setLayout(new FillLayout());
@@ -97,7 +111,21 @@
       }
     });
 
-    logger = new TreeItemLogger();
+    uiLogger = new TreeItemLogger();
+    AbstractTreeLogger bestLogger = uiLogger;
+    if (logFile != null) {
+      try {
+        PrintWriterTreeLogger fileLogger = new PrintWriterTreeLogger(logFile);
+        bestLogger = new CompositeTreeLogger(uiLogger, fileLogger);
+        fileLogger.setMaxDetail(logLevel);
+        uiLogger.setMaxDetail(logLevel);
+      } catch (IOException ex) {
+        uiLogger.log(TreeLogger.ERROR, "Can't log to " +
+            logFile.getAbsolutePath(), ex);
+      }
+    }
+    logger = bestLogger;
+    logger.setMaxDetail(logLevel);
 
     // The detail
     details = new Text(sash, SWT.MULTI | SWT.WRAP | SWT.READ_ONLY | SWT.BORDER
@@ -238,7 +266,7 @@
           return;
         }
 
-        if (logger.uiFlush(tree)) {
+        if (uiLogger.uiFlush(tree)) {
           // Sync to the end of the tree.
           //
           if (autoScroll) {
diff --git a/dev/core/src/com/google/gwt/dev/util/log/AbstractTreeLogger.java b/dev/core/src/com/google/gwt/dev/util/log/AbstractTreeLogger.java
index 2c7af4c..d128e8d 100644
--- a/dev/core/src/com/google/gwt/dev/util/log/AbstractTreeLogger.java
+++ b/dev/core/src/com/google/gwt/dev/util/log/AbstractTreeLogger.java
@@ -85,15 +85,15 @@
   }
 
   public int indexWithinMyParent;
+  
+  protected TreeLogger.Type logLevel = TreeLogger.ALL;
 
-  private TreeLogger.Type logLevel = TreeLogger.ALL;
+  protected AbstractTreeLogger parent;
 
   private int nextChildIndex;
 
   private final Object nextChildIndexLock = new Object();
 
-  private AbstractTreeLogger parent;
-
   private UncommittedBranchData uncommitted;
 
   /**
@@ -226,6 +226,38 @@
     return getLoggerId();
   }
 
+  protected int allocateNextChildIndex() {
+    synchronized (nextChildIndexLock) {
+      // postincrement because we want indices to start at 0
+      return nextChildIndex++;
+    }
+  }
+
+  /**
+   * Commits the branch after ensuring that the parent logger (if there is one)
+   * has been committed first.
+   */
+  protected synchronized void commitMyBranchEntryInMyParentLogger() {
+    // (Only the root logger doesn't have a parent.)
+    //
+    if (parent != null) {
+      if (uncommitted != null) {
+        // Commit the parent first.
+        //
+        parent.commitMyBranchEntryInMyParentLogger();
+  
+        // Let the subclass do its thing to commit this branch.
+        //
+        parent.doCommitBranch(this, uncommitted.type, uncommitted.message,
+            uncommitted.caught, uncommitted.helpInfo);
+  
+        // Release the uncommitted state.
+        //
+        uncommitted = null;
+      }
+    }
+  }
+
   /**
    * Derived classes should override this method to return a branched logger.
    */
@@ -267,13 +299,6 @@
   protected abstract void doLog(int indexOfLogEntryWithinParentLogger,
       TreeLogger.Type type, String msg, Throwable caught, HelpInfo helpInfo);
 
-  private int allocateNextChildIndex() {
-    synchronized (nextChildIndexLock) {
-      // postincrement because we want indices to start at 0
-      return nextChildIndex++;
-    }
-  }
-
   /**
    * Scans <code>t</code> and its causes for {@link OutOfMemoryError}.
    * 
@@ -293,31 +318,6 @@
     return false;
   }
 
-  /**
-   * Commits the branch after ensuring that the parent logger (if there is one)
-   * has been committed first.
-   */
-  private synchronized void commitMyBranchEntryInMyParentLogger() {
-    // (Only the root logger doesn't have a parent.)
-    //
-    if (parent != null) {
-      if (uncommitted != null) {
-        // Commit the parent first.
-        //
-        parent.commitMyBranchEntryInMyParentLogger();
-
-        // Let the subclass do its thing to commit this branch.
-        //
-        parent.doCommitBranch(this, uncommitted.type, uncommitted.message,
-            uncommitted.caught, uncommitted.helpInfo);
-
-        // Release the uncommitted state.
-        //
-        uncommitted = null;
-      }
-    }
-  }
-
   private String getLoggerId() {
     if (parent != null) {
       if (parent.parent == null) {
diff --git a/dev/core/src/com/google/gwt/dev/util/log/CompositeTreeLogger.java b/dev/core/src/com/google/gwt/dev/util/log/CompositeTreeLogger.java
new file mode 100644
index 0000000..c88f461
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/log/CompositeTreeLogger.java
@@ -0,0 +1,60 @@
+/**

+ * 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.util.log;

+

+/**

+ * Forks logging over two child loggers.  This provides the graphics + file

+ * logging of HostedModeBase's -logfile option.

+ */

+public class CompositeTreeLogger extends AbstractTreeLogger {

+

+  private AbstractTreeLogger[] loggers;

+  

+  public CompositeTreeLogger(AbstractTreeLogger... loggers) {

+    this.loggers = loggers;

+  }

+

+  @Override

+  protected AbstractTreeLogger doBranch() {

+    AbstractTreeLogger children[] = new AbstractTreeLogger[loggers.length];

+    for (int i = 0; i < loggers.length; i++) {

+      children[i] = loggers[i].doBranch();      

+      children[i].indexWithinMyParent = loggers[i].allocateNextChildIndex();

+      children[i].parent = loggers[i];

+      children[i].logLevel = loggers[i].logLevel;

+    }

+    return new CompositeTreeLogger(children);

+  }

+

+  @Override

+  protected void doCommitBranch(AbstractTreeLogger childBeingCommitted,

+      Type type, String msg, Throwable caught, HelpInfo helpInfo) {

+    CompositeTreeLogger child = (CompositeTreeLogger) childBeingCommitted;

+    assert loggers.length == child.loggers.length;

+    for (int i = 0; i < loggers.length; i++) {

+      loggers[i].doCommitBranch(child.loggers[i], type, msg, caught, helpInfo);      

+    }

+  }

+

+  @Override

+  protected void doLog(int indexOfLogEntryWithinParentLogger, Type type,

+      String msg, Throwable caught, HelpInfo helpInfo) {

+    for (AbstractTreeLogger logger : loggers) {

+      logger.doLog(indexOfLogEntryWithinParentLogger, type, msg, caught,

+          helpInfo);

+    }

+  }

+}

diff --git a/dev/core/src/com/google/gwt/dev/util/log/PrintWriterTreeLogger.java b/dev/core/src/com/google/gwt/dev/util/log/PrintWriterTreeLogger.java
index cdd7843..6b75ab9 100644
--- a/dev/core/src/com/google/gwt/dev/util/log/PrintWriterTreeLogger.java
+++ b/dev/core/src/com/google/gwt/dev/util/log/PrintWriterTreeLogger.java
@@ -15,6 +15,9 @@
  */
 package com.google.gwt.dev.util.log;
 
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.net.URL;
 
@@ -26,6 +29,8 @@
   private final String indent;
 
   private final PrintWriter out;
+  
+  private final Object mutex = new Object();
 
   public PrintWriterTreeLogger() {
     this(new PrintWriter(System.out, true));
@@ -34,12 +39,22 @@
   public PrintWriterTreeLogger(PrintWriter out) {
     this(out, "");
   }
+  
+  public PrintWriterTreeLogger(File logFile) throws IOException {
+    boolean existing = logFile.exists();
+    this.out = new PrintWriter(new FileWriter(logFile, true), true);
+    this.indent = "";
+    if (existing) {
+      out.println();  // blank line to mark relaunch
+    }
+  }
 
   protected PrintWriterTreeLogger(PrintWriter out, String indent) {
     this.out = out;
     this.indent = indent;
   }
 
+  @Override
   protected AbstractTreeLogger doBranch() {
     return new PrintWriterTreeLogger(out, indent + "   ");
   }
@@ -53,23 +68,25 @@
   @Override
   protected void doLog(int indexOfLogEntryWithinParentLogger, Type type,
       String msg, Throwable caught, HelpInfo helpInfo) {
-    out.print(indent);
-    if (type.needsAttention()) {
-      out.print("[");
-      out.print(type.getLabel());
-      out.print("] ");
-    }
-
-    out.println(msg);
-    if (helpInfo != null) {
-      URL url = helpInfo.getURL();
-      if (url != null) {
-        out.print(indent);
-        out.println("For additional info see: " + url.toString());
+    synchronized (mutex) { // ensure thread interleaving...
+      out.print(indent);
+      if (type.needsAttention()) {
+        out.print("[");
+        out.print(type.getLabel());
+        out.print("] ");
       }
-    }
-    if (caught != null) {
-      caught.printStackTrace(out);
+
+      out.println(msg);
+      if (helpInfo != null) {
+        URL url = helpInfo.getURL();
+        if (url != null) {
+          out.print(indent);
+          out.println("For additional info see: " + url.toString());
+        }
+      }
+      if (caught != null) {
+        caught.printStackTrace(out);
+      }
     }
   }
 }
diff --git a/dev/oophm/overlay/com/google/gwt/dev/shell/ShellMainWindow.java b/dev/oophm/overlay/com/google/gwt/dev/shell/ShellMainWindow.java
index 6a94906..a58d5cf 100644
--- a/dev/oophm/overlay/com/google/gwt/dev/shell/ShellMainWindow.java
+++ b/dev/oophm/overlay/com/google/gwt/dev/shell/ShellMainWindow.java
@@ -21,6 +21,7 @@
 
 import java.awt.BorderLayout;
 import java.awt.GridLayout;
+import java.io.File;
 
 import javax.swing.BorderFactory;
 import javax.swing.JLabel;
@@ -33,7 +34,7 @@
 
   private SwingLoggerPanel logWindow;
 
-  public ShellMainWindow(TreeLogger.Type maxLevel) {
+  public ShellMainWindow(TreeLogger.Type maxLevel, File logFile) {
     super(new BorderLayout());
     // TODO(jat): add back when we have real options
     if (false) {
@@ -49,7 +50,7 @@
       panel.add(launchPanel);
       add(panel, BorderLayout.NORTH);
     }
-    logWindow = new SwingLoggerPanel(maxLevel);
+    logWindow = new SwingLoggerPanel(maxLevel, logFile);
     add(logWindow);
   }
 
diff --git a/dev/oophm/src/com/google/gwt/dev/ModulePanel.java b/dev/oophm/src/com/google/gwt/dev/ModulePanel.java
index f20ec84..281dd76 100644
--- a/dev/oophm/src/com/google/gwt/dev/ModulePanel.java
+++ b/dev/oophm/src/com/google/gwt/dev/ModulePanel.java
@@ -24,6 +24,7 @@
 import java.awt.BorderLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.io.File;
 
 import javax.swing.JButton;
 import javax.swing.JOptionPane;
@@ -41,7 +42,7 @@
   private boolean disconnected;
 
   public ModulePanel(Type maxLevel, String moduleName,
-      Session session) {
+      Session session, File logFile) {
     super(new BorderLayout());
     this.session = session;
     if (false) {
@@ -57,7 +58,7 @@
       topPanel.add(compileButton);
       add(topPanel, BorderLayout.NORTH);
     }
-    loggerPanel = new SwingLoggerPanel(maxLevel);
+    loggerPanel = new SwingLoggerPanel(maxLevel, logFile);
     add(loggerPanel);
     session.addModule(moduleName, this);
   }
diff --git a/dev/oophm/src/com/google/gwt/dev/ModuleTabPanel.java b/dev/oophm/src/com/google/gwt/dev/ModuleTabPanel.java
index e6b3107..196fa1a 100644
--- a/dev/oophm/src/com/google/gwt/dev/ModuleTabPanel.java
+++ b/dev/oophm/src/com/google/gwt/dev/ModuleTabPanel.java
@@ -17,7 +17,7 @@
 
 import com.google.gwt.core.ext.TreeLogger.Type;
 import com.google.gwt.dev.OophmHostedModeBase.TabPanelCollection;
-import com.google.gwt.dev.shell.Icons;
+import com.google.gwt.dev.util.BrowserInfo;
 
 import java.awt.BorderLayout;
 import java.awt.CardLayout;
@@ -26,6 +26,7 @@
 import java.awt.Font;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.io.File;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.text.DateFormat;
@@ -226,40 +227,6 @@
   }
 
   /**
-   * Holds information about the browser used in the UI.
-   */
-  private static class BrowserInfo {
-
-    private final ImageIcon icon;
-    private final String shortName;
-
-    /**
-     * Create a BrowserInfo instance.
-     * 
-     * @param icon
-     * @param shortName
-     */
-    public BrowserInfo(ImageIcon icon, String shortName) {
-      this.icon = icon;
-      this.shortName = shortName;
-    }
-
-    /**
-     * @return the icon used to identify this browser, or null if none.
-     */
-    public ImageIcon getIcon() {
-      return icon;
-    }
-
-    /**
-     * @return the short name used to identify this browser, or null if none.
-     */
-    public String getShortName() {
-      return shortName;
-    }
-  }
-
-  /**
    * Renderer used to show entries in the module dropdown box.
    */
   private static class SessionModuleRenderer extends BasicComboBoxRenderer {
@@ -384,7 +351,7 @@
     cardLayout = new CardLayout();
     deckPanel.setLayout(cardLayout);
     add(deckPanel);
-    BrowserInfo browserInfo = getBrowserInfo(userAgent);
+    BrowserInfo browserInfo = BrowserInfo.getBrowserInfo(userAgent);
     
     // Construct the tab title and tooltip
     String tabTitle = url;
@@ -426,10 +393,11 @@
 
   public synchronized ModulePanel addModuleSession(Type maxLevel,
       String moduleName,
-      String sessionKey) {
+      String sessionKey,
+      File logFile) {
     Session session = findOrCreateSession(sessionKey);
     
-    ModulePanel panel = new ModulePanel(maxLevel, moduleName, session);
+    ModulePanel panel = new ModulePanel(maxLevel, moduleName, session, logFile);
     return panel;
   }
 
@@ -479,32 +447,6 @@
     return session;
   }
 
-  /**
-   * Choose an icon appropriate for this browser, or null if none.
-   * 
-   * @param userAgent User-Agent string from browser
-   * @return icon or null if none
-   */
-  private BrowserInfo getBrowserInfo(String userAgent) {
-    ImageIcon browserIcon = null;
-    String shortName = null;
-    String lcAgent = userAgent.toLowerCase();
-    if (lcAgent.contains("msie")) {
-      browserIcon = Icons.getIE24();
-      shortName = "IE";
-    } else if (lcAgent.contains("chrome")) {
-      browserIcon = Icons.getChrome24();
-      shortName = "Chrome";
-    } else if (lcAgent.contains("webkit") || lcAgent.contains("safari")) {
-      browserIcon = Icons.getSafari24();
-      shortName = "Safari";
-    } else if (lcAgent.contains("firefox")) {
-      browserIcon = Icons.getFirefox24();
-      shortName = "FF";
-    }
-    return new BrowserInfo(browserIcon, shortName);
-  }
-
   private String getTabTitle(URL parsedUrl) {
     String tabTitle = parsedUrl.getPath();
     if (tabTitle.length() > 0) {
diff --git a/dev/oophm/src/com/google/gwt/dev/OophmHostedModeBase.java b/dev/oophm/src/com/google/gwt/dev/OophmHostedModeBase.java
index bc3f169..b8930dd 100644
--- a/dev/oophm/src/com/google/gwt/dev/OophmHostedModeBase.java
+++ b/dev/oophm/src/com/google/gwt/dev/OophmHostedModeBase.java
@@ -27,6 +27,7 @@
 import com.google.gwt.dev.shell.OophmSessionHandler;
 import com.google.gwt.dev.shell.ShellMainWindow;
 import com.google.gwt.dev.shell.ShellModuleSpaceHost;
+import com.google.gwt.dev.util.BrowserInfo;
 import com.google.gwt.dev.util.collect.HashMap;
 import com.google.gwt.dev.util.log.AbstractTreeLogger;
 import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
@@ -35,6 +36,7 @@
 import java.awt.Cursor;
 import java.awt.event.WindowAdapter;
 import java.awt.event.WindowEvent;
+import java.io.File;
 import java.io.PrintWriter;
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -185,7 +187,10 @@
       if (!isHeadless()) {
         tabPanel = findModuleTab(userAgent, remoteSocket, url, tabKey,
             moduleName);
-        tab = tabPanel.addModuleSession(maxLevel, moduleName, sessionKey);
+        String agentTag = BrowserInfo.getShortName(userAgent).toLowerCase();
+        tab = tabPanel.addModuleSession(maxLevel, moduleName, sessionKey,
+            options.getLogFile(String.format("%s-%s-%d.log", moduleName,
+                agentTag, getNextSessionCounter(options.getLogDir()))));
         logger = tab.getLogger();
         TreeLogger branch = logger.branch(TreeLogger.INFO, "Loading module "
             + moduleName);
@@ -276,6 +281,8 @@
 
   private static final Random RNG = new Random();
   
+  private static int sessionCounter = 0;
+
   /**
    * Produce a random string that has low probability of collisions.
    * 
@@ -448,6 +455,23 @@
     return browserHost;
   }
 
+  protected int getNextSessionCounter(File logdir) {
+    if (sessionCounter == 0) {
+      // first time only, figure out the "last" session count already in use
+      for (String filename : logdir.list()) {
+        if (filename.matches("^[A-Za-z0-9_$]*-[a-z]*-[0-9]*.log$")) {
+          String substring = filename.substring(filename.lastIndexOf('-') + 1,
+              filename.length() - 4);
+          int number = Integer.parseInt(substring);
+          if (number > sessionCounter) {
+            sessionCounter = number;
+          }
+        }
+      }
+    }
+    return ++sessionCounter;
+  }
+
   /**
    * @return the icon to use for the web server tab
    */
@@ -498,10 +522,15 @@
     ImageIcon gwtIcon = loadImageIcon("icon24.png");
     frame = new JFrame("GWT Development Mode");
     tabs = new JTabbedPane();
-    mainWnd = new ShellMainWindow(options.getLogLevel());
+    if (options.alsoLogToFile()) {
+      options.getLogDir().mkdirs();
+    }
+    mainWnd = new ShellMainWindow(options.getLogLevel(),
+        options.getLogFile("main.log"));
     tabs.addTab("Development Mode", gwtIcon, mainWnd, "GWT Development mode");
     if (!options.isNoServer()) {
       webServerLog = new WebServerPanel(getPort(), options.getLogLevel(),
+          options.getLogFile("webserver.log"),
           new RestartAction() {
             public void restartServer(TreeLogger logger) {
               try {
diff --git a/dev/oophm/src/com/google/gwt/dev/WebServerPanel.java b/dev/oophm/src/com/google/gwt/dev/WebServerPanel.java
index 4c45873..4fb3986 100644
--- a/dev/oophm/src/com/google/gwt/dev/WebServerPanel.java
+++ b/dev/oophm/src/com/google/gwt/dev/WebServerPanel.java
@@ -21,6 +21,7 @@
 import java.awt.BorderLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.io.File;
 
 import javax.swing.JButton;
 import javax.swing.JPanel;
@@ -38,14 +39,15 @@
   
   private SwingLoggerPanel logWindow;
 
-  public WebServerPanel(int serverPort, TreeLogger.Type maxLevel) {
-    this(serverPort, maxLevel, null);
+  public WebServerPanel(int serverPort, TreeLogger.Type maxLevel,
+      File logFile) {
+    this(serverPort, maxLevel, logFile, null);
   }
 
   public WebServerPanel(int serverPort, TreeLogger.Type maxLevel,
-      final RestartAction restartServerAction) {
+      File logFile, final RestartAction restartServerAction) {
     super(new BorderLayout());
-    logWindow = new SwingLoggerPanel(maxLevel);
+    logWindow = new SwingLoggerPanel(maxLevel, logFile);
     if (restartServerAction != null) {
       JPanel panel = new JPanel();
       JButton restartButton = new JButton("Restart Server");
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/log/SwingLoggerPanel.java b/dev/oophm/src/com/google/gwt/dev/shell/log/SwingLoggerPanel.java
index c24a316..f530a15 100644
--- a/dev/oophm/src/com/google/gwt/dev/shell/log/SwingLoggerPanel.java
+++ b/dev/oophm/src/com/google/gwt/dev/shell/log/SwingLoggerPanel.java
@@ -21,6 +21,8 @@
 import com.google.gwt.dev.shell.CloseButton.Callback;
 import com.google.gwt.dev.shell.log.SwingTreeLogger.LogEvent;
 import com.google.gwt.dev.util.log.AbstractTreeLogger;
+import com.google.gwt.dev.util.log.CompositeTreeLogger;
+import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
@@ -35,6 +37,8 @@
 import java.awt.event.ActionListener;
 import java.awt.event.InputEvent;
 import java.awt.event.KeyEvent;
+import java.io.File;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Enumeration;
 
@@ -264,7 +268,7 @@
   
   private boolean disconnected = false;
 
-  public SwingLoggerPanel(TreeLogger.Type maxLevel) {
+  public SwingLoggerPanel(TreeLogger.Type maxLevel, File logFile) {
     super(new BorderLayout());
     regexFilter = "";
     levelFilter = maxLevel;
@@ -368,8 +372,22 @@
     treeView.setMinimumSize(minSize);
     splitter.setDividerLocation(0.80);
     add(splitter);
-    logger = new SwingTreeLogger(this);
-    logger.setMaxDetail(maxLevel);
+    
+    AbstractTreeLogger uiLogger = new SwingTreeLogger(this);
+    AbstractTreeLogger bestLogger = uiLogger;
+    if (logFile != null) {
+      try {
+        PrintWriterTreeLogger fileLogger = new PrintWriterTreeLogger(logFile);
+        bestLogger = new CompositeTreeLogger(bestLogger, fileLogger);
+        fileLogger.setMaxDetail(maxLevel);
+        uiLogger.setMaxDetail(maxLevel);
+      } catch (IOException ex) {
+        bestLogger.log(TreeLogger.ERROR, "Can't log to file "
+            + logFile.getAbsolutePath(), ex);
+      }
+    }
+    bestLogger.setMaxDetail(maxLevel);
+    logger = bestLogger;
     KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_F,
         InputEvent.CTRL_DOWN_MASK);
     getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(key, "find");
diff --git a/dev/oophm/src/com/google/gwt/dev/util/BrowserInfo.java b/dev/oophm/src/com/google/gwt/dev/util/BrowserInfo.java
new file mode 100644
index 0000000..8519f14
--- /dev/null
+++ b/dev/oophm/src/com/google/gwt/dev/util/BrowserInfo.java
@@ -0,0 +1,100 @@
+/**

+ * 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.util;

+

+import com.google.gwt.dev.shell.Icons;

+

+import javax.swing.ImageIcon;

+

+/**

+ * Holds information about the browser used in the UI.

+ */

+public class BrowserInfo {

+

+  private static final String UNKNOWN = "Unknown";

+  private static final String FIREFOX = "FF";

+  private static final String SAFARI = "Safari";

+  private static final String OPERA = "Opera";

+  private static final String CHROME = "Chrome";

+  private static final String IE = "IE";

+

+  /**

+   * Choose an icon and short name appropriate for this browser.  The icon

+   * may be null.

+   * 

+   * @param userAgent User-Agent string from browser

+   * @return icon or null if none

+   */

+  public static BrowserInfo getBrowserInfo(String userAgent) {

+    ImageIcon browserIcon = null;

+    String shortName = getShortName(userAgent);

+    if (shortName.equals(IE)) {

+      browserIcon = Icons.getIE24();

+    } else if (shortName.equals(CHROME)) {

+      browserIcon = Icons.getChrome24();

+    } else if (shortName.equals(OPERA)) {

+      browserIcon = Icons.getOpera24();

+    } else if (shortName.equals(SAFARI)) {

+      browserIcon = Icons.getSafari24();

+    } else if (shortName.equals(FIREFOX)) {

+      browserIcon = Icons.getFirefox24();

+    }

+    return new BrowserInfo(browserIcon, shortName);

+  }

+

+  public static String getShortName(String userAgent) {

+    String lcAgent = userAgent.toLowerCase();

+    if (lcAgent.contains("msie")) {

+      return IE;

+    } else if (lcAgent.contains("chrome")) {

+      return CHROME;

+    } else if (lcAgent.contains("opera")) {

+      return OPERA;

+    } else if (lcAgent.contains("webkit") || lcAgent.contains("safari")) {

+      return SAFARI;

+    } else if (lcAgent.contains("firefox")) {

+      return FIREFOX;

+    }

+    return UNKNOWN;

+  }

+  private final ImageIcon icon;

+  private final String shortName;

+

+  /**

+   * Create a BrowserInfo instance.

+   * 

+   * @param icon

+   * @param shortName

+   */

+  private BrowserInfo(ImageIcon icon, String shortName) {

+    this.icon = icon;

+    this.shortName = shortName;

+  }

+

+  /**

+   * @return the icon used to identify this browser, or null if none.

+   */

+  public ImageIcon getIcon() {

+    return icon;

+  }

+

+  /**

+   * @return the short name used to identify this browser, or null if none.

+   */

+  public String getShortName() {

+    return shortName;

+  }

+}
\ No newline at end of file