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