Line | Hits | Source |
---|---|---|
1 | /* | |
2 | ||
3 | Copyright 2004, Martian Software, Inc. | |
4 | ||
5 | Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | you may not use this file except in compliance with the License. | |
7 | You may obtain a copy of the License at | |
8 | ||
9 | http://www.apache.org/licenses/LICENSE-2.0 | |
10 | ||
11 | Unless required by applicable law or agreed to in writing, software | |
12 | distributed under the License is distributed on an "AS IS" BASIS, | |
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | See the License for the specific language governing permissions and | |
15 | limitations under the License. | |
16 | ||
17 | */ | |
18 | ||
19 | package com.martiansoftware.nailgun; | |
20 | import java.io.InputStream; | |
21 | import java.io.PrintStream; | |
22 | import java.lang.reflect.Method; | |
23 | import java.net.InetAddress; | |
24 | import java.net.ServerSocket; | |
25 | import java.net.Socket; | |
26 | import java.net.UnknownHostException; | |
27 | import java.util.Iterator; | |
28 | import java.util.Map; | |
29 | ||
30 | import com.martiansoftware.nailgun.builtins.DefaultNail; | |
31 | ||
32 | /** | |
33 | * <p>Listens for new connections from NailGun clients and launches | |
34 | * NGSession threads to process them.</p> | |
35 | * | |
36 | * <p>This class can be run as a standalone server or can be embedded | |
37 | * within larger applications as a means of providing command-line | |
38 | * interaction with the application.</p> | |
39 | * | |
40 | * @author <a href="http://www.martiansoftware.com/contact.html">Marty Lamb</a> | |
41 | */ | |
42 | public class NGServer implements Runnable { | |
43 | ||
44 | /** | |
45 | * The address on which to listen, or null to listen on all | |
46 | * local addresses | |
47 | */ | |
48 | 1 | private InetAddress addr = null; |
49 | ||
50 | /** | |
51 | * The port on which to listen, or zero to select a port automatically | |
52 | */ | |
53 | 1 | private int port = 0; |
54 | ||
55 | /** | |
56 | * The socket doing the listening | |
57 | */ | |
58 | private ServerSocket serversocket; | |
59 | ||
60 | /** | |
61 | * True if this NGServer has received instructions to shut down | |
62 | */ | |
63 | 1 | private boolean shutdown = false; |
64 | ||
65 | /** | |
66 | * True if this NGServer has been started and is accepting connections | |
67 | */ | |
68 | 1 | private boolean running = false; |
69 | ||
70 | /** | |
71 | * This NGServer's AliasManager, which maps aliases to classes | |
72 | */ | |
73 | private AliasManager aliasManager; | |
74 | ||
75 | /** | |
76 | * If true, fully-qualified classnames are valid commands | |
77 | */ | |
78 | 1 | private boolean allowNailsByClassName = true; |
79 | ||
80 | /** | |
81 | * The default class to use if an invalid alias or classname is | |
82 | * specified by the client. | |
83 | */ | |
84 | 1 | private Class defaultNailClass = null; |
85 | ||
86 | /** | |
87 | * A pool of NGSessions ready to handle client connections | |
88 | */ | |
89 | 1 | private NGSessionPool sessionPool = null; |
90 | ||
91 | /** | |
92 | * <code>System.out</code> at the time of the NGServer's creation | |
93 | */ | |
94 | 1 | public final PrintStream out = System.out; |
95 | ||
96 | /** | |
97 | * <code>System.err</code> at the time of the NGServer's creation | |
98 | */ | |
99 | 1 | public final PrintStream err = System.err; |
100 | ||
101 | /** | |
102 | * <code>System.in</code> at the time of the NGServer's creation | |
103 | */ | |
104 | 1 | public final InputStream in = System.in; |
105 | ||
106 | /** | |
107 | * a collection of all classes executed by this server so far | |
108 | */ | |
109 | 1 | private Map allNailStats = null; |
110 | ||
111 | /** | |
112 | * Remember the security manager we start with so we can restore it later | |
113 | */ | |
114 | 1 | private SecurityManager originalSecurityManager = null; |
115 | ||
116 | /** | |
117 | * Creates a new NGServer that will listen at the specified address and | |
118 | * on the specified port. | |
119 | * This does <b>not</b> cause the server to start listening. To do | |
120 | * so, create a new <code>Thread</code> wrapping this <code>NGServer</code> | |
121 | * and start it. | |
122 | * @param addr the address at which to listen, or <code>null</code> to bind | |
123 | * to all local addresses | |
124 | * @param port the port on which to listen. | |
125 | */ | |
126 | 0 | public NGServer(InetAddress addr, int port) { |
127 | 0 | init(addr, port); |
128 | 0 | } |
129 | ||
130 | /** | |
131 | * Creates a new NGServer that will listen on the default port | |
132 | * (defined in <code>NGConstants.DEFAULT_PORT</code>). | |
133 | * This does <b>not</b> cause the server to start listening. To do | |
134 | * so, create a new <code>Thread</code> wrapping this <code>NGServer</code> | |
135 | * and start it. | |
136 | */ | |
137 | 1 | public NGServer() { |
138 | 1 | init(null, NGConstants.DEFAULT_PORT); |
139 | 1 | } |
140 | ||
141 | /** | |
142 | * Sets up the NGServer internals | |
143 | * @param addr the InetAddress to bind to | |
144 | * @param port the port on which to listen | |
145 | */ | |
146 | private void init(InetAddress addr, int port) { | |
147 | 1 | this.addr = addr; |
148 | 1 | this.port = port; |
149 | ||
150 | 1 | this.aliasManager = new AliasManager(); |
151 | 1 | allNailStats = new java.util.HashMap(); |
152 | // allow a maximum of 10 idle threads. probably too high a number | |
153 | // and definitely should be configurable in the future | |
154 | 1 | sessionPool = new NGSessionPool(this, 10); |
155 | 1 | } |
156 | ||
157 | /** | |
158 | * Sets a flag that determines whether Nails can be executed by class name. | |
159 | * If this is false, Nails can only be run via aliases (and you should | |
160 | * probably remove ng-alias from the AliasManager). | |
161 | * | |
162 | * @param allowNailsByClassName true iff Nail lookups by classname are allowed | |
163 | */ | |
164 | public void setAllowNailsByClassName(boolean allowNailsByClassName) { | |
165 | 0 | this.allowNailsByClassName = allowNailsByClassName; |
166 | 0 | } |
167 | ||
168 | /** | |
169 | * Returns a flag that indicates whether Nail lookups by classname | |
170 | * are allowed. If this is false, Nails can only be run via aliases. | |
171 | * @return a flag that indicates whether Nail lookups by classname | |
172 | * are allowed. | |
173 | */ | |
174 | public boolean allowsNailsByClassName() { | |
175 | 0 | return (allowNailsByClassName); |
176 | } | |
177 | ||
178 | /** | |
179 | * Sets the default class to use for the Nail if no Nails can | |
180 | * be found via alias or classname. (may be <code>null</code>, | |
181 | * in which case NailGun will use its own default) | |
182 | * @param defaultNailClass the default class to use for the Nail | |
183 | * if no Nails can be found via alias or classname. | |
184 | * (may be <code>null</code>, in which case NailGun will use | |
185 | * its own default) | |
186 | */ | |
187 | public void setDefaultNailClass(Class defaultNailClass) { | |
188 | 0 | this.defaultNailClass = defaultNailClass; |
189 | 0 | } |
190 | ||
191 | /** | |
192 | * Returns the default class that will be used if no Nails | |
193 | * can be found via alias or classname. | |
194 | * @return the default class that will be used if no Nails | |
195 | * can be found via alias or classname. | |
196 | */ | |
197 | public Class getDefaultNailClass() { | |
198 | 0 | return ((defaultNailClass == null) ? DefaultNail.class : defaultNailClass) ; |
199 | } | |
200 | ||
201 | /** | |
202 | * Returns the current NailStats object for the specified class, creating | |
203 | * a new one if necessary | |
204 | * @param nailClass the class for which we're gathering stats | |
205 | * @return a NailStats object for the specified class | |
206 | */ | |
207 | private NailStats getOrCreateStatsFor(Class nailClass) { | |
208 | 0 | NailStats result = null; |
209 | 0 | synchronized(allNailStats) { |
210 | 0 | result = (NailStats) allNailStats.get(nailClass); |
211 | 0 | if (result == null) { |
212 | 0 | result = new NailStats(nailClass); |
213 | 0 | allNailStats.put(nailClass, result); |
214 | } | |
215 | 0 | } |
216 | 0 | return (result); |
217 | } | |
218 | ||
219 | /** | |
220 | * Provides a means for an NGSession to register the starting of | |
221 | * a nail execution with the server. | |
222 | * | |
223 | * @param nailClass the nail class that was launched | |
224 | */ | |
225 | void nailStarted(Class nailClass) { | |
226 | 0 | NailStats stats = getOrCreateStatsFor(nailClass); |
227 | 0 | stats.nailStarted(); |
228 | 0 | } |
229 | ||
230 | /** | |
231 | * Provides a means for an NGSession to register the completion of | |
232 | * a nails execution with the server. | |
233 | * | |
234 | * @param nailClass the nail class that finished | |
235 | */ | |
236 | void nailFinished(Class nailClass) { | |
237 | 0 | NailStats stats = (NailStats) allNailStats.get(nailClass); |
238 | 0 | stats.nailFinished(); |
239 | 0 | } |
240 | ||
241 | /** | |
242 | * Returns a snapshot of this NGServer's nail statistics. The result is a <code>java.util.Map</code>, | |
243 | * keyed by class name, with <a href="NailStats.html">NailStats</a> objects as values. | |
244 | * | |
245 | * @return a snapshot of this NGServer's nail statistics. | |
246 | */ | |
247 | public Map getNailStats() { | |
248 | 0 | Map result = new java.util.TreeMap(); |
249 | 0 | synchronized(allNailStats) { |
250 | 0 | for (Iterator i = allNailStats.keySet().iterator(); i.hasNext();) { |
251 | 0 | Class nailclass = (Class) i.next(); |
252 | 0 | result.put(nailclass.getName(), ((NailStats) allNailStats.get(nailclass)).clone()); |
253 | } | |
254 | 0 | } |
255 | 0 | return (result); |
256 | } | |
257 | ||
258 | /** | |
259 | * Returns the AliasManager in use by this NGServer. | |
260 | * @return the AliasManager in use by this NGServer. | |
261 | */ | |
262 | public AliasManager getAliasManager() { | |
263 | 0 | return (aliasManager); |
264 | } | |
265 | ||
266 | /** | |
267 | * <p>Shuts down the server. The server will stop listening | |
268 | * and its thread will finish. Any running nails will be allowed | |
269 | * to finish.</p> | |
270 | * | |
271 | * <p>Any nails that provide a | |
272 | * <pre><code>public static void nailShutdown(NGServer)</code></pre> | |
273 | * method will have this method called with this NGServer as its sole | |
274 | * parameter.</p> | |
275 | * | |
276 | * @param exitVM if true, this method will also exit the JVM after | |
277 | * calling nailShutdown() on any nails. This may prevent currently | |
278 | * running nails from exiting gracefully, but may be necessary in order | |
279 | * to perform some tasks, such as shutting down any AWT or Swing threads | |
280 | * implicitly launched by your nails. | |
281 | */ | |
282 | public void shutdown(boolean exitVM) { | |
283 | 0 | synchronized(this) { |
284 | 0 | if (shutdown) return; |
285 | 0 | shutdown = true; |
286 | 0 | } |
287 | ||
288 | try { | |
289 | 0 | serversocket.close(); |
290 | 0 | } catch (Throwable toDiscard) {} |
291 | ||
292 | 0 | sessionPool.shutdown(); |
293 | ||
294 | 0 | Class[] argTypes = new Class[1]; |
295 | 0 | argTypes[0] = NGServer.class; |
296 | 0 | Object[] argValues = new Object[1]; |
297 | 0 | argValues[0] = this; |
298 | ||
299 | // make sure that all aliased classes have associated nailstats | |
300 | // so they can be shut down. | |
301 | 0 | for (Iterator i = getAliasManager().getAliases().iterator(); i.hasNext();) { |
302 | 0 | Alias alias = (Alias) i.next(); |
303 | 0 | getOrCreateStatsFor(alias.getAliasedClass()); |
304 | } | |
305 | ||
306 | 0 | synchronized(allNailStats) { |
307 | 0 | for (Iterator i = allNailStats.values().iterator(); i.hasNext();) { |
308 | 0 | NailStats ns = (NailStats) i.next(); |
309 | 0 | Class nailClass = ns.getNailClass(); |
310 | ||
311 | // yes, I know this is lazy, relying upon the exception | |
312 | // to handle the case of no nailShutdown method. | |
313 | try { | |
314 | 0 | Method nailShutdown = nailClass.getMethod("nailShutdown", argTypes); |
315 | 0 | nailShutdown.invoke(null, argValues); |
316 | 0 | } catch (Throwable toDiscard) {} |
317 | } | |
318 | 0 | } |
319 | ||
320 | // restore system streams | |
321 | 0 | System.setIn(in); |
322 | 0 | System.setOut(out); |
323 | 0 | System.setErr(err); |
324 | ||
325 | 0 | System.setSecurityManager(originalSecurityManager); |
326 | ||
327 | 0 | if (exitVM) { |
328 | 0 | System.exit(0); |
329 | } | |
330 | 0 | } |
331 | ||
332 | /** | |
333 | * Returns true iff the server is currently running. | |
334 | * @return true iff the server is currently running. | |
335 | */ | |
336 | public boolean isRunning() { | |
337 | 0 | return (running); |
338 | } | |
339 | ||
340 | /** | |
341 | * Returns the port on which this server is (or will be) listening. | |
342 | * @return the port on which this server is (or will be) listening. | |
343 | */ | |
344 | public int getPort() { | |
345 | 0 | return ((serversocket == null) ? port : serversocket.getLocalPort()); |
346 | } | |
347 | ||
348 | /** | |
349 | * Listens for new connections and launches NGSession threads | |
350 | * to process them. | |
351 | */ | |
352 | public void run() { | |
353 | 0 | running = true; |
354 | 0 | NGSession sessionOnDeck = null; |
355 | ||
356 | 0 | originalSecurityManager = System.getSecurityManager(); |
357 | 0 | System.setSecurityManager( |
358 | new NGSecurityManager( | |
359 | originalSecurityManager)); | |
360 | ||
361 | ||
362 | 0 | synchronized(System.in) { |
363 | 0 | if (!(System.in instanceof ThreadLocalInputStream)) { |
364 | 0 | System.setIn(new ThreadLocalInputStream(in)); |
365 | 0 | System.setOut(new ThreadLocalPrintStream(out)); |
366 | 0 | System.setErr(new ThreadLocalPrintStream(err)); |
367 | } | |
368 | 0 | } |
369 | ||
370 | try { | |
371 | 0 | if (addr == null) { |
372 | 0 | serversocket = new ServerSocket(port); |
373 | } else { | |
374 | 0 | serversocket = new ServerSocket(port, 0, addr); |
375 | } | |
376 | ||
377 | 0 | while (!shutdown) { |
378 | 0 | sessionOnDeck = sessionPool.take(); |
379 | 0 | Socket socket = serversocket.accept(); |
380 | 0 | sessionOnDeck.run(socket); |
381 | } | |
382 | ||
383 | 0 | } catch (Throwable t) { |
384 | // if shutdown is called while the accept() method is blocking, | |
385 | // an exception will be thrown that we don't care about. filter | |
386 | // those out. | |
387 | 0 | if (!shutdown) { |
388 | 0 | t.printStackTrace(); |
389 | } | |
390 | 0 | } |
391 | 0 | if (sessionOnDeck != null) { |
392 | 0 | sessionOnDeck.shutdown(); |
393 | } | |
394 | 0 | running = false; |
395 | 0 | } |
396 | ||
397 | private static void usage() { | |
398 | 0 | System.err.println("Usage: java com.martiansoftware.nailgun.NGServer"); |
399 | 0 | System.err.println(" or: java com.martiansoftware.nailgun.NGServer port"); |
400 | 0 | System.err.println(" or: java com.martiansoftware.nailgun.NGServer IPAddress"); |
401 | 0 | System.err.println(" or: java com.martiansoftware.nailgun.NGServer IPAddress:port"); |
402 | 0 | } |
403 | ||
404 | /** | |
405 | * Creates and starts a new <code>NGServer</code>. A single optional | |
406 | * argument is valid, specifying the port on which this <code>NGServer</code> | |
407 | * should listen. If omitted, <code>NGServer.DEFAULT_PORT</code> will be used. | |
408 | * @param args a single optional argument specifying the port on which to listen. | |
409 | * @throws NumberFormatException if a non-numeric port is specified | |
410 | */ | |
411 | public static void main(String[] args) throws NumberFormatException, UnknownHostException { | |
412 | ||
413 | 0 | if (args.length > 1) { |
414 | 0 | usage(); |
415 | 0 | return; |
416 | } | |
417 | ||
418 | // null server address means bind to everything local | |
419 | 0 | InetAddress serverAddress = null; |
420 | 0 | int port = NGConstants.DEFAULT_PORT; |
421 | ||
422 | // parse the sole command line parameter, which | |
423 | // may be an inetaddress to bind to, a port number, | |
424 | // or an inetaddress followed by a port, separated | |
425 | // by a colon | |
426 | 0 | if (args.length != 0) { |
427 | 0 | String[] argParts = args[0].split(":"); |
428 | 0 | String addrPart = null; |
429 | 0 | String portPart = null; |
430 | 0 | if (argParts.length == 2) { |
431 | 0 | addrPart = argParts[0]; |
432 | 0 | portPart = argParts[1]; |
433 | 0 | } else if (argParts[0].indexOf('.') >= 0) { |
434 | 0 | addrPart = argParts[0]; |
435 | } else { | |
436 | 0 | portPart = argParts[0]; |
437 | } | |
438 | 0 | if (addrPart != null) { |
439 | 0 | serverAddress = InetAddress.getByName(addrPart); |
440 | } | |
441 | 0 | if (portPart != null) { |
442 | 0 | port = Integer.parseInt(portPart); |
443 | } | |
444 | } | |
445 | ||
446 | 0 | NGServer server = new NGServer(serverAddress, port); |
447 | 0 | Thread t = new Thread(server); |
448 | 0 | t.setName("NGServer(" + serverAddress + ", " + port + ")"); |
449 | 0 | t.start(); |
450 | ||
451 | 0 | Runtime.getRuntime().addShutdownHook(new NGServerShutdowner(server)); |
452 | ||
453 | // if the port is 0, it will be automatically determined. | |
454 | // add this little wait so the ServerSocket can fully | |
455 | // initialize and we can see what port it chose. | |
456 | 0 | int runningPort = server.getPort(); |
457 | 0 | while (runningPort == 0) { |
458 | 0 | try { Thread.sleep(50); } catch (Throwable toIgnore) {} |
459 | 0 | runningPort = server.getPort(); |
460 | } | |
461 | ||
462 | 0 | System.out.println("NGServer started on " |
463 | + ((serverAddress == null) | |
464 | ? "all interfaces" | |
465 | : serverAddress.getHostAddress()) | |
466 | + ", port " | |
467 | + runningPort | |
468 | + "."); | |
469 | 0 | } |
470 | ||
471 | /** | |
472 | * A shutdown hook that will cleanly bring down the NGServer if it | |
473 | * is interrupted. | |
474 | * | |
475 | * @author <a href="http://www.martiansoftware.com/contact.html">Marty Lamb</a> | |
476 | */ | |
477 | private static class NGServerShutdowner extends Thread { | |
478 | private NGServer server = null; | |
479 | ||
480 | NGServerShutdowner(NGServer server) { | |
481 | this.server = server; | |
482 | } | |
483 | ||
484 | ||
485 | public void run() { | |
486 | ||
487 | int count = 0; | |
488 | server.shutdown(false); | |
489 | ||
490 | // give the server up to five seconds to stop. is that enough? | |
491 | // remember that the shutdown will call nailShutdown in any | |
492 | // nails as well | |
493 | while (server.isRunning() && (count < 50)) { | |
494 | ||
495 | try {Thread.sleep(100);} catch(InterruptedException e) {} | |
496 | ++count; | |
497 | } | |
498 | ||
499 | if (server.isRunning()) { | |
500 | System.err.println("Unable to cleanly shutdown server. Exiting JVM Anyway."); | |
501 | } else { | |
502 | System.out.println("NGServer shut down."); | |
503 | } | |
504 | } | |
505 | } | |
506 | } |
this report was generated by version 1.0.5 of jcoverage. |
copyright © 2003, jcoverage ltd. all rights reserved. |