Simplifies BrowserManagerServer based on the premise that the cleanup thread is the only guy who *ever* does any cleanup.

Review by: zundel

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@1864 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/junit/remote/BrowserManagerProcess.java b/user/src/com/google/gwt/junit/remote/BrowserManagerProcess.java
index 781c9b3..3d7f21d 100644
--- a/user/src/com/google/gwt/junit/remote/BrowserManagerProcess.java
+++ b/user/src/com/google/gwt/junit/remote/BrowserManagerProcess.java
@@ -47,20 +47,21 @@
 
   /**
    * Kills the child process when fired, unless it is no longer the active
-   * {@link BrowserManagerProcess#killTask} in its outer ProcessManager.
+   * {@link BrowserManagerProcess#killTask}.
    */
   private final class KillTask extends TimerTask {
     @Override
     public void run() {
       synchronized (BrowserManagerProcess.this) {
         /*
-         * CORNER CASE: Verify we're still the active KillTask, because it's
-         * possible we were bumped out by a keepAlive call after our execution
-         * started but before we could grab the lock.
+         * Verify we're still the active KillTask! If we're not the active
+         * killTask, it means we've been rescheduled and a newer kill timer is
+         * active.
          */
-        if (killTask == this) {
-          logger.info("Timeout expired for task: " + token);
-          doKill();
+        if (killTask == this && !deadOrDying) {
+          logger.info("Timeout expired for: " + token);
+          process.destroy();
+          deadOrDying = true;
         }
       }
     }
@@ -69,24 +70,22 @@
   private static final Logger logger = Logger.getLogger(BrowserManagerProcess.class.getName());
 
   /**
-   * Exit callback when the process exits.
+   * Compute elapsed time.
+   * 
+   * @param startTime the time the process started
+   * @return returns a string representing the number of seconds elapsed since
+   *         the process started.
    */
-  private final ProcessExitCb cb;
+  private static String getElapsed(long intervalMs) {
+    NumberFormat nf = NumberFormat.getNumberInstance();
+    nf.setMaximumFractionDigits(3);
+    return nf.format(intervalMs / 1000.0);
+  }
 
   /**
-   * Set to 'true' when the process exits.
+   * Set to 'true' when the process exits or starts being killed.
    */
-  private boolean exited = false;
-
-  /**
-   * Set to the exitValue() of the process when it actually exits.
-   */
-  private int exitValue = -1;
-
-  /**
-   * The key associated with <code>process</code>.
-   */
-  private final int token;
+  private boolean deadOrDying = false;
 
   /**
    * If non-null, the active TimerTask which will kill <code>process</code>
@@ -100,16 +99,16 @@
   private final Process process;
 
   /**
-   * Time the exec'ed child actually started.
-   */
-  private final long startTime;
-
-  /**
    * Timer instance passed in from BrowserManagerServer.
    */
   private final Timer timer;
 
   /**
+   * The key associated with <code>process</code>.
+   */
+  private final int token;
+
+  /**
    * Constructs a new ProcessManager for the specified process.
    * 
    * @param timer timer passed in from BrowserManagerServer instance.
@@ -118,30 +117,26 @@
    * @param initKeepAliveMs the initial time to wait before killing
    *          <code>process</code>
    */
-  BrowserManagerProcess(ProcessExitCb cb, Timer timer, final int token,
-      final Process process, long initKeepAliveMs) {
-    this.cb = cb;
-    this.timer = timer;
+  public BrowserManagerProcess(final ProcessExitCb cb, Timer timer,
+      final int token, final Process process, long initKeepAliveMs) {
     this.process = process;
+    this.timer = timer;
     this.token = token;
-    schedule(initKeepAliveMs);
-    startTime = System.currentTimeMillis();
 
+    final long startTime = System.currentTimeMillis();
     Thread cleanupThread = new Thread() {
       @Override
       public void run() {
-        try {
-          exitValue = process.waitFor();
-          if (cleanupBrowser() != 0) {
-            logger.warning("Browser " + token + "exited with bad status: "
-                + exitValue);
-          } else {
-            logger.info("Browser " + token + " process exited normally. "
-                + getElapsed() + " milliseconds.");
+        while (true) {
+          try {
+            int exitValue = process.waitFor();
+            doCleanup(cb, exitValue, token, System.currentTimeMillis()
+                - startTime);
+            return;
+          } catch (InterruptedException e) {
+            logger.log(Level.WARNING,
+                "Interrupted waiting for process exit of: " + token, e);
           }
-        } catch (InterruptedException e) {
-          logger.log(Level.WARNING, "Couldn't wait for process exit. token: "
-              + token, e);
         }
       }
     };
@@ -149,45 +144,7 @@
     cleanupThread.setDaemon(true);
     cleanupThread.setName("Browser-" + token + "-Wait");
     cleanupThread.start();
-  }
-
-  /**
-   * Kills the managed process.
-   * 
-   * @return the exit value of the task.
-   */
-  public int doKill() {
-
-    boolean doCleanup = false;
-    synchronized (this) {
-      if (!exited) {
-        logger.info("Killing browser process for " + this.token);
-        process.destroy();
-
-        // Wait for the process to exit.
-        try {
-          exitValue = process.waitFor();
-          doCleanup = true;
-        } catch (InterruptedException ie) {
-          logger.severe("Interrupted waiting for browser " + token
-              + " exit during kill.");
-        }
-      }
-    }
-
-    // Run cleanupBrowser() outside the critical section.
-    if (doCleanup) {
-      if (cleanupBrowser() != 0) {
-        logger.warning("Kill Browser " + token + "exited with bad status: "
-            + exitValue);
-
-      } else {
-        logger.info("Kill Browser " + token + " process exited normally. "
-            + getElapsed() + " milliseconds.");
-      }
-    }
-
-    return exitValue;
+    keepAlive(initKeepAliveMs);
   }
 
   /**
@@ -199,88 +156,47 @@
    * @return <code>true</code> if the process was successfully kept alive,
    *         <code>false</code> if the process is already dead.
    */
-  public boolean keepAlive(long keepAliveMs) {
-    synchronized (this) {
-      try {
-        /*
-         * See if the managed process is still alive. WEIRD: The only way to
-         * check the process's liveness appears to be asking for its exit status
-         * and seeing whether it throws an IllegalThreadStateException.
-         */
-        process.exitValue();
-      } catch (IllegalThreadStateException e) {
-        // The process is still alive.
-        schedule(keepAliveMs);
-        return true;
-      }
+  public synchronized boolean keepAlive(long keepAliveMs) {
+    assert (keepAliveMs > 0);
+    if (!deadOrDying) {
+      killTask = new KillTask();
+      timer.schedule(killTask, keepAliveMs);
+      return true;
     }
-
-    // The process is dead already; perform cleanup.
-    cleanupBrowser();
     return false;
   }
 
   /**
-   * Routine that informs the BrowserManagerServer of the exit status once and
-   * only once.
-   * 
-   * This should be called WITHOUT the lock on BrowserManagerProcess.this being
-   * held.
-   * 
-   * @return The exit value returned by the process when it exited.
+   * Kills the underlying browser process.
    */
-  private int cleanupBrowser() {
-    boolean doCb = false;
+  public synchronized void killBrowser() {
+    if (!deadOrDying) {
+      process.destroy();
+      deadOrDying = true;
+    }
+  }
+
+  /**
+   * Cleans up when the underlying process terminates. The lock must not be held
+   * when calling this method or deadlock could result.
+   * 
+   * @param cb the callback to fire
+   * @param exitValue the exit value of the process
+   * @param token the id of this browser instance
+   * @param startTime the time the process started
+   */
+  private void doCleanup(ProcessExitCb cb, int exitValue, int token,
+      long intervalMs) {
     synchronized (this) {
-      if (!exited) {
-        exited = true;
-        exitValue = process.exitValue();
-        // Stop the timer for this thread.
-        schedule(0);
-        doCb = true;
-      }
+      deadOrDying = true;
     }
-
-    /*
-     * Callback must occur without holding my own lock. This is because the
-     * callee will try to acquire the lock on
-     * BrowserManagerServer.processByToken. If another thread already has that
-     * lock and is tries to lock me at the same time, a deadlock would ensure.
-     */
-    if (doCb) {
-      cb.childExited(token, exitValue);
+    if (exitValue != 0) {
+      logger.warning("Browser: " + token + " exited with bad status: "
+          + exitValue);
+    } else {
+      logger.info("Browser: " + token + " process exited normally after "
+          + getElapsed(intervalMs) + "s");
     }
-
-    return exitValue;
-  }
-
-  /**
-   * Compute elapsed time.
-   * 
-   * @return returns a string representing the number of seconds elapsed since
-   *         the process started.
-   */
-  private synchronized String getElapsed() {
-    NumberFormat nf = NumberFormat.getNumberInstance();
-    nf.setMaximumFractionDigits(3);
-    return nf.format((System.currentTimeMillis() - startTime) / 1000.0);
-  }
-
-  /**
-   * Cancels any existing kill task and optionally schedules a new one to run
-   * <code>keepAliveMs</code> from now.
-   * 
-   * @param keepAliveMs if > 0, schedules a new kill task to run in keepAliveMs
-   *          milliseconds; if <= 0, a new kill task is not scheduled.
-   */
-  private void schedule(long keepAliveMs) {
-    if (killTask != null) {
-      killTask.cancel();
-      killTask = null;
-    }
-    if (keepAliveMs > 0) {
-      killTask = new KillTask();
-      timer.schedule(killTask, keepAliveMs);
-    }
+    cb.childExited(token, exitValue);
   }
 }
diff --git a/user/src/com/google/gwt/junit/remote/BrowserManagerServer.java b/user/src/com/google/gwt/junit/remote/BrowserManagerServer.java
index 9a39822..4459938 100644
--- a/user/src/com/google/gwt/junit/remote/BrowserManagerServer.java
+++ b/user/src/com/google/gwt/junit/remote/BrowserManagerServer.java
@@ -238,14 +238,13 @@
       }
       BrowserManagerProcess process = processByToken.get(token);
       if (process != null) {
-        logger.fine("Killing browser. token=" + token);
-        process.doKill();
+        logger.info("Client kill for active browser: " + token);
+        process.killBrowser();
       } else if (launchCommandQueue.contains(new LaunchCommand(token))) {
         launchCommandQueue.remove(new LaunchCommand(token));
-        logger.info(token + " removed from delayed launch queue.");
+        logger.info("Client kill for waiting browser: " + token);
       } else {
-        logger.fine("No action taken. Browser not active for token " + token
-            + ".");
+        logger.info("Client kill for inactive browser: " + token);
       }
     }
   }
@@ -276,7 +275,7 @@
         return myToken;
       }
     } catch (IOException e) {
-      logger.log(Level.SEVERE, "Error launching browser" + launchCmd
+      logger.log(Level.SEVERE, "Error launching browser '" + launchCmd
           + "' for '" + url + "'", e);
       throw new RuntimeException("Error launching browser '" + launchCmd
           + "' for '" + url + "'", e);
@@ -341,7 +340,6 @@
    * (Assumes that code is already synchronized by processBytoken)
    */
   private void launchDelayedCommand() {
-
     if (!serializeFlag || !processByToken.isEmpty()) {
       // No need to launch if serialization is off or
       // something is already running
@@ -352,22 +350,18 @@
     // successfully.
     while (!launchCommandQueue.isEmpty()) {
       LaunchCommand lc = launchCommandQueue.remove();
-
       try {
         execChild(lc.token, lc.url, lc.keepAliveMsecs);
         // No exception? Great!
-        logger.info("Started delayed browser " + lc.token);
+        logger.info("Started delayed browser: " + lc.token);
         return;
 
       } catch (IOException e) {
-
-        logger.log(Level.SEVERE, "Error launching browser" + launchCmd
+        logger.log(Level.SEVERE, "Error launching browser '" + launchCmd
             + "' for '" + lc.url + "'", e);
-
         throw new RuntimeException("Error launching browser '" + launchCmd
             + "' for '" + lc.url + "'", e);
       }
-
       // If an exception occurred, keep pulling cmds off the queue.
     }
   }