| 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. |