001: /*
002: * Copyright 2003-2006 Rick Knowles <winstone-devel at lists sourceforge net>
003: * Distributed under the terms of either:
004: * - the common development and distribution license (CDDL), v1.0; or
005: * - the GNU Lesser General Public License, v2.1 or later
006: */
007: package winstone;
008:
009: import java.io.File;
010: import java.io.FileInputStream;
011: import java.io.FileOutputStream;
012: import java.io.IOException;
013: import java.io.InputStream;
014: import java.io.InterruptedIOException;
015: import java.io.ObjectInputStream;
016: import java.io.OutputStream;
017: import java.lang.reflect.Constructor;
018: import java.net.ServerSocket;
019: import java.net.Socket;
020: import java.net.URL;
021: import java.net.URLClassLoader;
022: import java.util.ArrayList;
023: import java.util.HashMap;
024: import java.util.Iterator;
025: import java.util.List;
026: import java.util.Map;
027: import java.util.Properties;
028:
029: /**
030: * Implements the main launcher daemon thread. This is the class that gets
031: * launched by the command line, and owns the server socket, etc.
032: *
033: * @author <a href="mailto:rick_knowles@hotmail.com">Rick Knowles</a>
034: * @version $Id: Launcher.java,v 1.29 2007/04/23 02:55:35 rickknowles Exp $
035: */
036: public class Launcher implements Runnable {
037:
038: static final String HTTP_LISTENER_CLASS = "winstone.HttpListener";
039: static final String HTTPS_LISTENER_CLASS = "winstone.ssl.HttpsListener";
040: static final String AJP_LISTENER_CLASS = "winstone.ajp13.Ajp13Listener";
041: static final String CLUSTER_CLASS = "winstone.cluster.SimpleCluster";
042: static final String DEFAULT_JNDI_MGR_CLASS = "winstone.jndi.ContainerJNDIManager";
043:
044: public static final byte SHUTDOWN_TYPE = (byte) '0';
045: public static final byte RELOAD_TYPE = (byte) '4';
046:
047: private int CONTROL_TIMEOUT = 2000; // wait 2s for control connection
048: private int DEFAULT_CONTROL_PORT = -1;
049:
050: private Thread controlThread;
051: public final static WinstoneResourceBundle RESOURCES = new WinstoneResourceBundle(
052: "winstone.LocalStrings");
053: private int controlPort;
054: private HostGroup hostGroup;
055: private ObjectPool objectPool;
056: private List listeners;
057: private Map args;
058: private Cluster cluster;
059: private JNDIManager globalJndiManager;
060:
061: /**
062: * Constructor - initialises the web app, object pools, control port and the
063: * available protocol listeners.
064: */
065: public Launcher(Map args) throws IOException {
066:
067: boolean useJNDI = WebAppConfiguration.booleanArg(args,
068: "useJNDI", false);
069:
070: // Set jndi resource handler if not set (workaround for JamVM bug)
071: if (useJNDI)
072: try {
073: Class ctxFactoryClass = Class
074: .forName("winstone.jndi.java.javaURLContextFactory");
075: if (System.getProperty("java.naming.factory.initial") == null) {
076: System.setProperty("java.naming.factory.initial",
077: ctxFactoryClass.getName());
078: }
079: if (System.getProperty("java.naming.factory.url.pkgs") == null) {
080: System.setProperty("java.naming.factory.url.pkgs",
081: "winstone.jndi");
082: }
083: } catch (ClassNotFoundException err) {
084: }
085:
086: Logger.log(Logger.MAX, RESOURCES, "Launcher.StartupArgs", args
087: + "");
088:
089: this .args = args;
090: this .controlPort = (args.get("controlPort") == null ? DEFAULT_CONTROL_PORT
091: : Integer.parseInt((String) args.get("controlPort")));
092:
093: // Check for java home
094: List jars = new ArrayList();
095: List commonLibCLPaths = new ArrayList();
096: String defaultJavaHome = System.getProperty("java.home");
097: String javaHome = WebAppConfiguration.stringArg(args,
098: "javaHome", defaultJavaHome);
099: Logger.log(Logger.DEBUG, RESOURCES, "Launcher.UsingJavaHome",
100: javaHome);
101: String toolsJarLocation = WebAppConfiguration.stringArg(args,
102: "toolsJar", null);
103: File toolsJar = null;
104: if (toolsJarLocation == null) {
105: toolsJar = new File(javaHome, "lib/tools.jar");
106:
107: // first try - if it doesn't exist, try up one dir since we might have
108: // the JRE home by mistake
109: if (!toolsJar.exists()) {
110: File javaHome2 = new File(javaHome).getParentFile();
111: File toolsJar2 = new File(javaHome2, "lib/tools.jar");
112: if (toolsJar2.exists()) {
113: javaHome = javaHome2.getCanonicalPath();
114: toolsJar = toolsJar2;
115: }
116: }
117: } else {
118: toolsJar = new File(toolsJarLocation);
119: }
120:
121: // Add tools jar to classloader path
122: if (toolsJar.exists()) {
123: jars.add(toolsJar.toURL());
124: commonLibCLPaths.add(toolsJar);
125: Logger.log(Logger.DEBUG, RESOURCES,
126: "Launcher.AddedCommonLibJar", toolsJar.getName());
127: } else if (WebAppConfiguration.booleanArg(args, "useJasper",
128: false))
129: Logger.log(Logger.WARNING, RESOURCES,
130: "Launcher.ToolsJarNotFound");
131:
132: // Set up common lib class loader
133: String commonLibCLFolder = WebAppConfiguration.stringArg(args,
134: "commonLibFolder", "lib");
135: File libFolder = new File(commonLibCLFolder);
136: if (libFolder.exists() && libFolder.isDirectory()) {
137: Logger.log(Logger.DEBUG, RESOURCES,
138: "Launcher.UsingCommonLib", libFolder
139: .getCanonicalPath());
140: File children[] = libFolder.listFiles();
141: for (int n = 0; n < children.length; n++)
142: if (children[n].getName().endsWith(".jar")
143: || children[n].getName().endsWith(".zip")) {
144: jars.add(children[n].toURL());
145: commonLibCLPaths.add(children[n]);
146: Logger.log(Logger.DEBUG, RESOURCES,
147: "Launcher.AddedCommonLibJar", children[n]
148: .getName());
149: }
150: } else {
151: Logger.log(Logger.DEBUG, RESOURCES, "Launcher.NoCommonLib");
152: }
153: ClassLoader commonLibCL = new URLClassLoader((URL[]) jars
154: .toArray(new URL[jars.size()]), getClass()
155: .getClassLoader());
156:
157: Logger.log(Logger.MAX, RESOURCES, "Launcher.CLClassLoader",
158: commonLibCL.toString());
159: Logger.log(Logger.MAX, RESOURCES, "Launcher.CLClassLoader",
160: commonLibCLPaths.toString());
161:
162: this .objectPool = new ObjectPool(args);
163:
164: // Optionally set up clustering if enabled and libraries are available
165: String useCluster = (String) args.get("useCluster");
166: boolean switchOnCluster = (useCluster != null)
167: && (useCluster.equalsIgnoreCase("true") || useCluster
168: .equalsIgnoreCase("yes"));
169: if (switchOnCluster) {
170: if (this .controlPort < 0) {
171: Logger.log(Logger.INFO, RESOURCES,
172: "Launcher.ClusterOffNoControlPort");
173: } else {
174: String clusterClassName = WebAppConfiguration
175: .stringArg(args, "clusterClassName",
176: CLUSTER_CLASS).trim();
177: try {
178: Class clusterClass = Class
179: .forName(clusterClassName);
180: Constructor clusterConstructor = clusterClass
181: .getConstructor(new Class[] { Map.class,
182: Integer.class });
183: this .cluster = (Cluster) clusterConstructor
184: .newInstance(new Object[] { args,
185: new Integer(this .controlPort) });
186: } catch (ClassNotFoundException err) {
187: Logger.log(Logger.DEBUG, RESOURCES,
188: "Launcher.ClusterNotFound");
189: } catch (Throwable err) {
190: Logger.log(Logger.WARNING, RESOURCES,
191: "Launcher.ClusterStartupError", err);
192: }
193: }
194: }
195:
196: // If jndi is enabled, run the container wide jndi populator
197: if (useJNDI) {
198: String jndiMgrClassName = WebAppConfiguration.stringArg(
199: args, "containerJndiClassName",
200: DEFAULT_JNDI_MGR_CLASS).trim();
201: try {
202: // Build the realm
203: Class jndiMgrClass = Class.forName(jndiMgrClassName,
204: true, commonLibCL);
205: Constructor jndiMgrConstr = jndiMgrClass
206: .getConstructor(new Class[] { Map.class,
207: List.class, ClassLoader.class });
208: this .globalJndiManager = (JNDIManager) jndiMgrConstr
209: .newInstance(new Object[] { args, null,
210: commonLibCL });
211: this .globalJndiManager.setup();
212: } catch (ClassNotFoundException err) {
213: Logger.log(Logger.DEBUG, RESOURCES,
214: "Launcher.JNDIDisabled");
215: } catch (Throwable err) {
216: Logger.log(Logger.ERROR, RESOURCES,
217: "Launcher.JNDIError", jndiMgrClassName, err);
218: }
219: }
220:
221: // Open the web apps
222: this .hostGroup = new HostGroup(this .cluster, this .objectPool,
223: commonLibCL, (File[]) commonLibCLPaths
224: .toArray(new File[0]), args);
225:
226: // Create connectors (http, https and ajp)
227: this .listeners = new ArrayList();
228: spawnListener(HTTP_LISTENER_CLASS);
229: spawnListener(AJP_LISTENER_CLASS);
230: try {
231: Class.forName("javax.net.ServerSocketFactory");
232: spawnListener(HTTPS_LISTENER_CLASS);
233: } catch (ClassNotFoundException err) {
234: Logger.log(Logger.DEBUG, RESOURCES, "Launcher.NeedsJDK14",
235: HTTPS_LISTENER_CLASS);
236: }
237:
238: this .controlThread = new Thread(this , RESOURCES.getString(
239: "Launcher.ThreadName", "" + this .controlPort));
240: this .controlThread.setDaemon(false);
241: this .controlThread.start();
242:
243: Runtime.getRuntime().addShutdownHook(new ShutdownHook(this ));
244:
245: }
246:
247: /**
248: * Instantiates listeners. Note that an exception thrown in the
249: * constructor is interpreted as the listener being disabled, so
250: * don't do anything too adventurous in the constructor, or if you do,
251: * catch and log any errors locally before rethrowing.
252: */
253: protected void spawnListener(String listenerClassName) {
254: try {
255: Class listenerClass = Class.forName(listenerClassName);
256: Constructor listenerConstructor = listenerClass
257: .getConstructor(new Class[] { Map.class,
258: ObjectPool.class, HostGroup.class });
259: Listener listener = (Listener) listenerConstructor
260: .newInstance(new Object[] { args, this .objectPool,
261: this .hostGroup });
262: if (listener.start()) {
263: this .listeners.add(listener);
264: }
265: } catch (ClassNotFoundException err) {
266: Logger.log(Logger.INFO, RESOURCES,
267: "Launcher.ListenerNotFound", listenerClassName);
268: } catch (Throwable err) {
269: Logger.log(Logger.ERROR, RESOURCES,
270: "Launcher.ListenerStartupError", listenerClassName,
271: err);
272: }
273: }
274:
275: /**
276: * The main run method. This handles the normal thread processing.
277: */
278: public void run() {
279: boolean interrupted = false;
280: try {
281: ServerSocket controlSocket = null;
282:
283: if (this .controlPort > 0) {
284: controlSocket = new ServerSocket(this .controlPort);
285: controlSocket.setSoTimeout(CONTROL_TIMEOUT);
286: }
287:
288: Logger
289: .log(
290: Logger.INFO,
291: RESOURCES,
292: "Launcher.StartupOK",
293: new String[] {
294: RESOURCES
295: .getString("ServerVersion"),
296: (this .controlPort > 0 ? ""
297: + this .controlPort
298: : RESOURCES
299: .getString("Launcher.ControlDisabled")) });
300:
301: // Enter the main loop
302: while (!interrupted) {
303: // this.objectPool.removeUnusedRequestHandlers();
304: // this.hostGroup.invalidateExpiredSessions();
305:
306: // Check for control request
307: Socket accepted = null;
308: try {
309: if (controlSocket != null) {
310: accepted = controlSocket.accept();
311: if (accepted != null) {
312: handleControlRequest(accepted);
313: }
314: } else {
315: Thread.sleep(CONTROL_TIMEOUT);
316: }
317: } catch (InterruptedIOException err) {
318: } catch (InterruptedException err) {
319: interrupted = true;
320: } catch (Throwable err) {
321: Logger.log(Logger.ERROR, RESOURCES,
322: "Launcher.ShutdownError", err);
323: } finally {
324: if (accepted != null) {
325: try {
326: accepted.close();
327: } catch (IOException err) {
328: }
329: }
330: if (Thread.interrupted()) {
331: interrupted = true;
332: }
333: }
334: }
335:
336: // Close server socket
337: if (controlSocket != null) {
338: controlSocket.close();
339: }
340: } catch (Throwable err) {
341: Logger.log(Logger.ERROR, RESOURCES,
342: "Launcher.ShutdownError", err);
343: }
344: Logger.log(Logger.INFO, RESOURCES,
345: "Launcher.ControlThreadShutdownOK");
346: }
347:
348: protected void handleControlRequest(Socket csAccepted)
349: throws IOException {
350: InputStream inSocket = null;
351: OutputStream outSocket = null;
352: ObjectInputStream inControl = null;
353: try {
354: inSocket = csAccepted.getInputStream();
355: int reqType = inSocket.read();
356: if ((byte) reqType == SHUTDOWN_TYPE) {
357: Logger.log(Logger.INFO, RESOURCES,
358: "Launcher.ShutdownRequestReceived");
359: shutdown();
360: } else if ((byte) reqType == RELOAD_TYPE) {
361: inControl = new ObjectInputStream(inSocket);
362: String host = inControl.readUTF();
363: String prefix = inControl.readUTF();
364: Logger
365: .log(Logger.INFO, RESOURCES,
366: "Launcher.ReloadRequestReceived", host
367: + prefix);
368: HostConfiguration hostConfig = this .hostGroup
369: .getHostByName(host);
370: hostConfig.reloadWebApp(prefix);
371: } else if (this .cluster != null) {
372: outSocket = csAccepted.getOutputStream();
373: this .cluster.clusterRequest((byte) reqType, inSocket,
374: outSocket, csAccepted, this .hostGroup);
375: }
376: } finally {
377: if (inControl != null) {
378: try {
379: inControl.close();
380: } catch (IOException err) {
381: }
382: }
383: if (inSocket != null) {
384: try {
385: inSocket.close();
386: } catch (IOException err) {
387: }
388: }
389: if (outSocket != null) {
390: try {
391: outSocket.close();
392: } catch (IOException err) {
393: }
394: }
395: }
396: }
397:
398: public void shutdown() {
399: // Release all listeners/pools/webapps
400: for (Iterator i = this .listeners.iterator(); i.hasNext();)
401: ((Listener) i.next()).destroy();
402: this .objectPool.destroy();
403: if (this .cluster != null)
404: this .cluster.destroy();
405: this .hostGroup.destroy();
406: if (this .globalJndiManager != null) {
407: this .globalJndiManager.tearDown();
408: }
409:
410: if (this .controlThread != null) {
411: this .controlThread.interrupt();
412: }
413: Thread.yield();
414:
415: Logger.log(Logger.INFO, RESOURCES, "Launcher.ShutdownOK");
416: }
417:
418: public boolean isRunning() {
419: return (this .controlThread != null)
420: && this .controlThread.isAlive();
421: }
422:
423: /**
424: * Main method. This basically just accepts a few args, then initialises the
425: * listener thread. For now, just shut it down with a control-C.
426: */
427: public static void main(String argv[]) throws IOException {
428: Map args = getArgsFromCommandLine(argv);
429:
430: if (args.containsKey("usage") || args.containsKey("help")) {
431: printUsage();
432: return;
433: }
434:
435: // Check for embedded war
436: deployEmbeddedWarfile(args);
437:
438: // Check for embedded warfile
439: if (!args.containsKey("webroot")
440: && !args.containsKey("warfile")
441: && !args.containsKey("webappsDir")
442: && !args.containsKey("hostsDir")) {
443: printUsage();
444: return;
445: }
446: // Launch
447: try {
448: new Launcher(args);
449: } catch (Throwable err) {
450: Logger.log(Logger.ERROR, RESOURCES,
451: "Launcher.ContainerStartupError", err);
452: }
453: }
454:
455: public static Map getArgsFromCommandLine(String argv[])
456: throws IOException {
457: Map args = loadArgsFromCommandLineAndConfig(argv, "nonSwitch");
458:
459: // Small hack to allow re-use of the command line parsing inside the control tool
460: String firstNonSwitchArgument = (String) args.get("nonSwitch");
461: args.remove("nonSwitch");
462:
463: // Check if the non-switch arg is a file or folder, and overwrite the config
464: if (firstNonSwitchArgument != null) {
465: File webapp = new File(firstNonSwitchArgument);
466: if (webapp.exists()) {
467: if (webapp.isDirectory()) {
468: args.put("webroot", firstNonSwitchArgument);
469: } else if (webapp.isFile()) {
470: args.put("warfile", firstNonSwitchArgument);
471: }
472: }
473: }
474: return args;
475: }
476:
477: public static Map loadArgsFromCommandLineAndConfig(String argv[],
478: String nonSwitchArgName) throws IOException {
479: Map args = new HashMap();
480:
481: // Load embedded properties file
482: String embeddedPropertiesFilename = RESOURCES
483: .getString("Launcher.EmbeddedPropertiesFile");
484:
485: InputStream embeddedPropsStream = Launcher.class
486: .getResourceAsStream(embeddedPropertiesFilename);
487: if (embeddedPropsStream != null) {
488: loadPropsFromStream(embeddedPropsStream, args);
489: embeddedPropsStream.close();
490: }
491:
492: // Get command line args
493: String configFilename = RESOURCES
494: .getString("Launcher.DefaultPropertyFile");
495: for (int n = 0; n < argv.length; n++) {
496: String option = argv[n];
497: if (option.startsWith("--")) {
498: int equalPos = option.indexOf('=');
499: String paramName = option.substring(2,
500: equalPos == -1 ? option.length() : equalPos);
501: if (equalPos != -1) {
502: args.put(paramName, option.substring(equalPos + 1));
503: } else {
504: args.put(paramName, "true");
505: }
506: if (paramName.equals("config")) {
507: configFilename = (String) args.get(paramName);
508: }
509: } else {
510: args.put(nonSwitchArgName, option);
511: }
512: }
513:
514: // Load default props if available
515: File configFile = new File(configFilename);
516: if (configFile.exists() && configFile.isFile()) {
517: InputStream inConfig = new FileInputStream(configFile);
518: loadPropsFromStream(inConfig, args);
519: inConfig.close();
520: initLogger(args);
521: Logger.log(Logger.DEBUG, RESOURCES,
522: "Launcher.UsingPropertyFile", configFilename);
523: } else {
524: initLogger(args);
525: }
526: return args;
527: }
528:
529: protected static void deployEmbeddedWarfile(Map args)
530: throws IOException {
531: String embeddedWarfileName = RESOURCES
532: .getString("Launcher.EmbeddedWarFile");
533: InputStream embeddedWarfile = Launcher.class
534: .getResourceAsStream(embeddedWarfileName);
535: if (embeddedWarfile != null) {
536: File tempWarfile = File.createTempFile("embedded", ".war")
537: .getAbsoluteFile();
538: tempWarfile.getParentFile().mkdirs();
539: tempWarfile.deleteOnExit();
540:
541: String embeddedWebroot = RESOURCES
542: .getString("Launcher.EmbeddedWebroot");
543: File tempWebroot = new File(tempWarfile.getParentFile(),
544: embeddedWebroot);
545: tempWebroot.mkdirs();
546:
547: Logger.log(Logger.DEBUG, RESOURCES,
548: "Launcher.CopyingEmbeddedWarfile", tempWarfile
549: .getAbsolutePath());
550: OutputStream out = new FileOutputStream(tempWarfile, true);
551: int read = 0;
552: byte buffer[] = new byte[2048];
553: while ((read = embeddedWarfile.read(buffer)) != -1) {
554: out.write(buffer, 0, read);
555: }
556: out.close();
557: embeddedWarfile.close();
558:
559: args.put("warfile", tempWarfile.getAbsolutePath());
560: args.put("webroot", tempWebroot.getAbsolutePath());
561: args.remove("webappsDir");
562: args.remove("hostsDir");
563: }
564: }
565:
566: protected static void loadPropsFromStream(InputStream inConfig,
567: Map args) throws IOException {
568: Properties props = new Properties();
569: props.load(inConfig);
570: for (Iterator i = props.keySet().iterator(); i.hasNext();) {
571: String key = (String) i.next();
572: if (!args.containsKey(key.trim())) {
573: args.put(key.trim(), props.getProperty(key).trim());
574: }
575: }
576: props.clear();
577: }
578:
579: public static void initLogger(Map args) throws IOException {
580: // Reset the log level
581: int logLevel = WebAppConfiguration.intArg(args, "debug",
582: Logger.INFO);
583: // boolean showThrowingLineNo = WebAppConfiguration.booleanArg(args, "logThrowingLineNo", false);
584: boolean showThrowingThread = WebAppConfiguration.booleanArg(
585: args, "logThrowingThread", false);
586: OutputStream logStream = null;
587: if (args.get("logfile") != null) {
588: logStream = new FileOutputStream((String) args
589: .get("logfile"));
590: } else if (WebAppConfiguration.booleanArg(args, "logToStdErr",
591: false)) {
592: logStream = System.err;
593: } else {
594: logStream = System.out;
595: }
596: // Logger.init(logLevel, logStream, showThrowingLineNo, showThrowingThread);
597: Logger.init(logLevel, logStream, showThrowingThread);
598: }
599:
600: protected static void printUsage() {
601: System.out.println(RESOURCES.getString(
602: "Launcher.UsageInstructions", RESOURCES
603: .getString("ServerVersion")));
604: }
605: }
|