/* * 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.junit.remote; import java.text.NumberFormat; import java.util.Timer; import java.util.TimerTask; import java.util.logging.Level; import java.util.logging.Logger; /** * Manages one web browser child process. This class contains a TimerTask which * tries to kill the managed process. A thread is created for each task to wait * for the process to exit and give a callback. * * Invariants: * <ul> * <li> Most of this code executes in a separate thread per process. Thus, the * API entry points lock <code>this</code></li> * <li> The lock on this is removed before calling the <code>childExited</code> * callback. This prevents potential deadlock. * </ul> */ class BrowserManagerProcess { /** * Used to notify the caller of the constructor when a process exits. Note * that the childExited() method is called from a different thread than the * one that created the process. */ public interface ProcessExitCb { void childExited(int key, int exitValue); } /** * Kills the child process when fired, unless it is no longer the active * {@link BrowserManagerProcess#killTask}. */ private final class KillTask extends TimerTask { @Override public void run() { synchronized (BrowserManagerProcess.this) { /* * 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 && !deadOrDying) { logger.info("Timeout expired for: " + token); process.destroy(); deadOrDying = true; } } } } private static final Logger logger = Logger.getLogger(BrowserManagerProcess.class.getName()); /** * 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 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 or starts being killed. */ private boolean deadOrDying = false; /** * If non-null, the active TimerTask which will kill <code>process</code> * when it fires. */ private KillTask killTask; /** * The managed child process. */ private final Process process; /** * 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. * @param token the key to be used to identify this process. * @param process the process being managed * @param initKeepAliveMs the initial time to wait before killing * <code>process</code> */ public BrowserManagerProcess(final ProcessExitCb cb, Timer timer, final int token, final Process process, long initKeepAliveMs) { this.process = process; this.timer = timer; this.token = token; final long startTime = System.currentTimeMillis(); Thread cleanupThread = new Thread() { @Override public void run() { 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); } } } }; cleanupThread.setDaemon(true); cleanupThread.setName("Browser-" + token + "-Wait"); cleanupThread.start(); keepAlive(initKeepAliveMs); } /** * Keeps the underlying process alive for <code>keepAliveMs</code> starting * now. If the managed process is already dead, cleanup is performed and the * method return false. * * @param keepAliveMs the time to wait before killing the underlying process * @return <code>true</code> if the process was successfully kept alive, * <code>false</code> if the process is already dead. */ public synchronized boolean keepAlive(long keepAliveMs) { assert (keepAliveMs > 0); if (!deadOrDying) { killTask = new KillTask(); timer.schedule(killTask, keepAliveMs); return true; } return false; } /** * Kills the underlying browser process. */ 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) { deadOrDying = true; } if (exitValue != 0) { logger.warning("Browser: " + token + " exited with bad status: " + exitValue); } else { logger.info("Browser: " + token + " process exited normally after " + getElapsed(intervalMs) + "s"); } cb.childExited(token, exitValue); } }