KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > com > google > gwt > junit > remote > BrowserManagerServer


1 /*
2  * Copyright 2006 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */

16 package com.google.gwt.junit.remote;
17
18 import java.io.IOException JavaDoc;
19 import java.rmi.Naming JavaDoc;
20 import java.rmi.RemoteException JavaDoc;
21 import java.rmi.registry.LocateRegistry JavaDoc;
22 import java.rmi.registry.Registry JavaDoc;
23 import java.rmi.server.UnicastRemoteObject JavaDoc;
24 import java.util.HashMap JavaDoc;
25 import java.util.Map JavaDoc;
26 import java.util.Timer JavaDoc;
27 import java.util.TimerTask JavaDoc;
28
29 /**
30  * Manages instances of a web browser as child processes. This class is
31  * experimental and unsupported. An instance of this class can create browser
32  * windows using one specific shell-level command. It performs process
33  * managagement (babysitting) on behalf of a remote client. This can be useful
34  * for running a GWTTestCase on a browser that cannot be run on the native
35  * platform. For example, a GWTTestCase test running on Linux could use a remote
36  * call to a Windows machine to test with Internet Explorer.
37  *
38  * <p>
39  * Calling {@link #main(String[])} can instantiate and register multiple
40  * instances of this class at given RMI namespace locations.
41  * </p>
42  *
43  * <p>
44  * This system has been tested on Internet Explorer 6. Firefox does not work in
45  * the general case; if an existing Firefox process is already running, new
46  * processes simply delegate to the existing process and terminate, which breaks
47  * the model. Safari on MacOS requires very special treatment given Safari's
48  * poor command line support, but that is beyond the scope of this
49  * documentation.
50  * </p>
51  *
52  * <p>
53  * TODO(scottb): We technically need a watchdog thread to slurp up stdout and
54  * stderr from the child processes, or they might block. However, most browsers
55  * never write to stdout and stderr, so this is low priority.
56  * </p>
57  *
58  * see http://bugs.sun.com/bugdatabase/view_bug.do;:YfiG?bug_id=4062587
59  */

60 public class BrowserManagerServer extends UnicastRemoteObject JavaDoc implements
61     BrowserManager {
62
63   /**
64    * Implementation notes: <code>processByToken</code> must be locked before
65    * performing any state-changing operations.
66    */

67
68   /**
69    * Manages one web browser child process. This class contains a TimerTask
70    * which tries to kill the managed process.
71    *
72    * Invariants:
73    * <ul>
74    * <li> If process is alive, this manager is in <code>processByToken</code>.
75    * </li>
76    * <li> If process is dead, this manager <i>might</i> be in
77    * <code>processByToken</code>. It will be observed to be dead next time
78    * {@link #keepAlive(long)} or {@link #doKill()} are called. </li>
79    * <li> Calling {@link #keepAlive(long)} and {@link #doKill()} require the
80    * lock on <code>processByToken</code> to be held, so they cannot be called
81    * at the same time. </li>
82    * </ul>
83    */

84   private final class ProcessManager {
85
86     /**
87      * Kills the child process when fired, unless it is no longer the active
88      * {@link ProcessManager#killTask} in its outer ProcessManager.
89      */

90     private final class KillTask extends TimerTask JavaDoc {
91       /*
92        * @see java.lang.Runnable#run()
93        */

94       public void run() {
95         synchronized (processByToken) {
96           /*
97            * CORNER CASE: Verify we're still the active KillTask, because it's
98            * possible we were bumped out by a keepAlive call after our execution
99            * started but before we could grab the lock on processByToken.
100            */

101           if (killTask == this) {
102             doKill();
103           }
104         }
105       }
106     }
107
108     /**
109      * The key associated with <code>process</code> in
110      * <code>processByToken</code>.
111      */

112     private Object JavaDoc key;
113
114     /**
115      * If non-null, the active TimerTask which will kill <code>process</code>
116      * when it fires.
117      */

118     private KillTask killTask;
119
120     /**
121      * The managed child process.
122      */

123     private final Process JavaDoc process;
124
125     /**
126      * Constructs a new ProcessManager for the specified process, and adds
127      * itself to <code>processByToken</code> using the supplied key. You must
128      * hold the lock on <code>processByToken</code> to call this method.
129      *
130      * @param key the key to be used when adding the new object to
131      * <code>processByToken</code>
132      * @param process the process being managed
133      * @param initKeepAliveMs the initial time to wait before killing
134      * <code>process</code>
135      */

136     ProcessManager(Object JavaDoc key, Process JavaDoc process, long initKeepAliveMs) {
137       this.process = process;
138       this.key = key;
139       schedule(initKeepAliveMs);
140       processByToken.put(key, this);
141     }
142
143     /**
144      * Kills the managed process. You must hold the lock on
145      * <code>processByToken</code> to call this method.
146      */

147     public void doKill() {
148       Object JavaDoc removed = processByToken.remove(key);
149       assert (removed == this);
150       process.destroy();
151       schedule(0);
152     }
153
154     /**
155      * Keeps the underlying process alive for <code>keepAliveMs</code>
156      * starting now. If the managed process is already dead, cleanup is
157      * performed and the method return false. You must hold the lock on
158      * <code>processByToken</code> to call this method.
159      *
160      * @param keepAliveMs the time to wait before killing the underlying process
161      * @return <code>true</code> if the process was successfully kept alive,
162      * <code>false</code> if the process is already dead.
163      */

164     public boolean keepAlive(long keepAliveMs) {
165       try {
166         /*
167          * See if the managed process is still alive. WEIRD: The only way to
168          * check the process's liveness appears to be asking for its exit status
169          * and seeing whether it throws an IllegalThreadStateException.
170          */

171         process.exitValue();
172       } catch (IllegalThreadStateException JavaDoc e) {
173         // The process is still alive.
174
schedule(keepAliveMs);
175         return true;
176       }
177
178       // The process is dead already; perform cleanup.
179
doKill();
180       return false;
181     }
182
183     /**
184      * Cancels any existing kill task and optionally schedules a new one to run
185      * <code>keepAliveMs</code> from now. You must hold the lock on
186      * <code>processByToken</code> to call this method.
187      *
188      * @param keepAliveMs if > 0, schedules a new kill task to run in
189      * keepAliveMs milliseconds; if <= 0, a new kill task is not
190      * scheduled.
191      */

192     private void schedule(long keepAliveMs) {
193       if (killTask != null) {
194         killTask.cancel();
195         killTask = null;
196       }
197       if (keepAliveMs > 0) {
198         killTask = new KillTask();
199         timer.schedule(killTask, keepAliveMs);
200       }
201     }
202   }
203
204   /**
205    * Starts up and registers one or more browser servers. Command-line entry
206    * point.
207    */

208   public static void main(String JavaDoc[] args) throws Exception JavaDoc {
209     if (args.length == 0) {
210       System.err.println(""
211           + "Manages local browser windows for a remote client using RMI.\n"
212           + "\n"
213           + "Pass in an even number of args, at least 2. The first argument\n"
214           + "is a short registration name, and the second argument is the\n"
215           + "executable to run when that name is used; for example,\n" + "\n"
216           + "\tie6 \"C:\\Program Files\\Internet Explorer\\IEXPLORE.EXE\"\n"
217           + "\n"
218           + "would register Internet Explorer to \"rmi://localhost/ie6\".\n"
219           + "The third and fourth arguments make another pair, and so on.\n");
220       System.exit(1);
221     }
222
223     if (args.length < 2) {
224       throw new IllegalArgumentException JavaDoc("Need at least 2 arguments");
225     }
226
227     if (args.length % 2 != 0) {
228       throw new IllegalArgumentException JavaDoc("Need an even number of arguments");
229     }
230
231     // Create an RMI registry so we don't need an external process.
232
// Uses the default RMI port.
233
// TODO(scottb): allow user to override the port via command line option.
234
LocateRegistry.createRegistry(Registry.REGISTRY_PORT);
235     System.out.println("RMI registry ready.");
236
237     for (int i = 0; i < args.length; i += 2) {
238       BrowserManagerServer bms = new BrowserManagerServer(args[i + 1]);
239       Naming.rebind(args[i], bms);
240       System.out.println(args[i] + " started and awaiting connections");
241     }
242   }
243
244   /**
245    * The shell command to launch when a new browser is requested.
246    */

247   private final String JavaDoc launchCmd;
248
249   /**
250    * The next token that will be returned from
251    * {@link #launchNewBrowser(String, long)}.
252    */

253   private int nextToken = 1;
254
255   /**
256    * Master map of tokens onto ProcessManagers managing live processes. Also
257    * serves as a lock that must be held before any state-changing operations on
258    * this class may be performed.
259    */

260   private final Map JavaDoc processByToken = new HashMap JavaDoc();
261
262   /**
263    * A single shared Timer used by all instances of
264    * {@link ProcessManager.KillTask}.
265    */

266   private final Timer JavaDoc timer = new Timer JavaDoc();
267
268   /**
269    * Constructs a manager for a particular shell command.
270    *
271    * @param launchCmd the path to a browser's executable, suitable for passing
272    * to {@link Runtime#exec(java.lang.String)}. The invoked process
273    * must accept a URL as a command line argument.
274    */

275   public BrowserManagerServer(String JavaDoc launchCmd) throws RemoteException JavaDoc {
276     this.launchCmd = launchCmd;
277   }
278
279   /*
280    * @see BrowserManager#keepAlive(int, long)
281    */

282   public void keepAlive(int token, long keepAliveMs) {
283
284     if (keepAliveMs <= 0) {
285       throw new IllegalArgumentException JavaDoc();
286     }
287
288     synchronized (processByToken) {
289       // Is the token one we've issued?
290
if (token < 0 || token >= nextToken) {
291         throw new IllegalArgumentException JavaDoc();
292       }
293       Integer JavaDoc intTok = new Integer JavaDoc(token);
294       ProcessManager process = (ProcessManager) processByToken.get(intTok);
295       if (process != null) {
296         if (process.keepAlive(keepAliveMs)) {
297           // The process was successfully kept alive.
298
return;
299         } else {
300           // The process is already dead. Fall through to failure.
301
}
302       }
303     }
304
305     throw new IllegalStateException JavaDoc("Process " + token + " already dead");
306   }
307
308   /*
309    * @see BrowserManager#killBrowser(int)
310    */

311   public void killBrowser(int token) {
312     synchronized (processByToken) {
313       // Is the token one we've issued?
314
if (token < 0 || token >= nextToken) {
315         throw new IllegalArgumentException JavaDoc();
316       }
317       Integer JavaDoc intTok = new Integer JavaDoc(token);
318       ProcessManager process = (ProcessManager) processByToken.get(intTok);
319       if (process != null) {
320         process.doKill();
321       }
322     }
323   }
324
325   /*
326    * @see BrowserManager#launchNewBrowser(java.lang.String, long)
327    */

328   public int launchNewBrowser(String JavaDoc url, long keepAliveMs) {
329
330     if (url == null || keepAliveMs <= 0) {
331       throw new IllegalArgumentException JavaDoc();
332     }
333
334     try {
335       Process JavaDoc child = Runtime.getRuntime().exec(new String JavaDoc[] {launchCmd, url});
336       synchronized (processByToken) {
337         int myToken = nextToken++;
338         Integer JavaDoc intTok = new Integer JavaDoc(myToken);
339         // Adds self to processByToken.
340
new ProcessManager(intTok, child, keepAliveMs);
341         return myToken;
342       }
343     } catch (IOException JavaDoc e) {
344       throw new RuntimeException JavaDoc("Error launching browser '" + launchCmd
345           + "' for '" + url + "'", e);
346     }
347   }
348 }
349
Popular Tags