/* * Copyright 2003-2006 Rick Knowles <winstone-devel at lists sourceforge net> * Distributed under the terms of either: * - the common development and distribution license (CDDL), v1.0; or * - the GNU Lesser General Public License, v2.1 or later */ package winstone; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.log.JavaUtilLog; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.thread.ExecutorThreadPool; import winstone.cmdline.CmdLineParser; import winstone.cmdline.Option; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.io.ObjectInputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.logging.Level; /** * Implements the main launcher daemon thread. This is the class that gets * launched by the command line, and owns the server socket, etc. * * @author <a href="mailto:rick_knowles@hotmail.com">Rick Knowles</a> * @version $Id: Launcher.java,v 1.29 2007/04/23 02:55:35 rickknowles Exp $ */ public class Launcher implements Runnable { static final String HTTP_LISTENER_CLASS = "winstone.HttpConnectorFactory"; static final String HTTPS_LISTENER_CLASS = "winstone.HttpsConnectorFactory"; static final String AJP_LISTENER_CLASS = "winstone.Ajp13ConnectorFactory"; public static final byte SHUTDOWN_TYPE = (byte) '0'; public static final byte RELOAD_TYPE = (byte) '4'; private int CONTROL_TIMEOUT = 2000; // wait 2s for control connection private Thread controlThread; public final static WinstoneResourceBundle RESOURCES = new WinstoneResourceBundle("winstone.LocalStrings"); private int controlPort; private HostGroup hostGroup; private ExecutorService threadPool; private Map args; public final Server server; /** * Constructor - initialises the web app, object pools, control port and the * available protocol listeners. */ public Launcher(Map args) throws IOException { boolean success=false; try { Logger.log(Logger.MAX, RESOURCES, "Launcher.StartupArgs", args + ""); this.args = args; this.controlPort =Option.CONTROL_PORT.get(args); // Check for java home List<URL> jars = new ArrayList<URL>(); List<File> commonLibCLPaths = new ArrayList<File>(); String defaultJavaHome = System.getProperty("java.home"); File javaHome = Option.JAVA_HOME.get(args,new File(defaultJavaHome)); Logger.log(Logger.DEBUG, RESOURCES, "Launcher.UsingJavaHome", javaHome.getPath()); File toolsJar = Option.TOOLS_JAR.get(args); if (toolsJar == null) { toolsJar = new File(javaHome, "lib/tools.jar"); // first try - if it doesn't exist, try up one dir since we might have // the JRE home by mistake if (!toolsJar.exists()) { File javaHome2 = javaHome.getParentFile(); File toolsJar2 = new File(javaHome2, "lib/tools.jar"); if (toolsJar2.exists()) { javaHome = javaHome2; toolsJar = toolsJar2; } } } // Add tools jar to classloader path if (toolsJar.exists()) { jars.add(toolsJar.toURL()); commonLibCLPaths.add(toolsJar); Logger.log(Logger.DEBUG, RESOURCES, "Launcher.AddedCommonLibJar", toolsJar.getName()); } else if (Option.USE_JASPER.get(args)) Logger.log(Logger.WARNING, RESOURCES, "Launcher.ToolsJarNotFound"); // Set up common lib class loader File libFolder = Option.COMMON_LIB_FOLDER.get(args,new File("lib")); if (libFolder.exists() && libFolder.isDirectory()) { Logger.log(Logger.DEBUG, RESOURCES, "Launcher.UsingCommonLib", libFolder.getCanonicalPath()); File children[] = libFolder.listFiles(); for (File aChildren : children) if (aChildren.getName().endsWith(".jar") || aChildren.getName().endsWith(".zip")) { jars.add(aChildren.toURL()); commonLibCLPaths.add(aChildren); Logger.log(Logger.DEBUG, RESOURCES, "Launcher.AddedCommonLibJar", aChildren.getName()); } } else { Logger.log(Logger.DEBUG, RESOURCES, "Launcher.NoCommonLib"); } ClassLoader commonLibCL = new URLClassLoader(jars.toArray(new URL[jars.size()]), getClass().getClassLoader()); Logger.log(Logger.MAX, RESOURCES, "Launcher.CLClassLoader", commonLibCL.toString()); Logger.log(Logger.MAX, RESOURCES, "Launcher.CLClassLoader", commonLibCLPaths.toString()); this.threadPool = createThreadPool(); this.server = new Server(new ExecutorThreadPool(threadPool)); int maxParameterCount = Option.MAX_PARAM_COUNT.get(args); if (maxParameterCount>0) { server.setAttribute("org.eclipse.jetty.server.Request.maxFormKeys",maxParameterCount); } server.setAttribute("org.eclipse.jetty.server.Request.maxFormContentSize", Option.REQUEST_FORM_CONTENT_SIZE.get(args)); // Open the web apps this.hostGroup = new HostGroup(server, commonLibCL, commonLibCLPaths.toArray(new File[0]), args); // Create connectors (http, https and ajp) spawnListener(HTTP_LISTENER_CLASS); spawnListener(AJP_LISTENER_CLASS); spawnListener(HTTPS_LISTENER_CLASS); try { server.start(); } catch (Exception e) { throw new IOException("Failed to start Jetty",e); } this.controlThread = new Thread(this, RESOURCES.getString( "Launcher.ThreadName", "" + this.controlPort)); this.controlThread.setDaemon(false); this.controlThread.start(); success = true; } finally { if (!success) shutdown(); } Runtime.getRuntime().addShutdownHook(new ShutdownHook(this)); } /** * Used to handle requests. */ private ExecutorService createThreadPool() { int maxConcurrentRequests = Option.HANDLER_COUNT_MAX.get(args); int maxIdleRequestHandlersInPool = Option.HANDLER_COUNT_MAX_IDLE.get(args); ExecutorService es = new ThreadPoolExecutor(maxIdleRequestHandlersInPool, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, // idle thread will only hang around for 60 secs new SynchronousQueue<Runnable>(), new ThreadFactory() { private int threadIndex; public synchronized Thread newThread(Runnable r) { String threadName = Launcher.RESOURCES.getString( "RequestHandlerThread.ThreadName", "" + (++threadIndex)); // allocate a thread to run on this object Thread thread = new Thread(r, threadName); thread.setDaemon(true); return thread; } }); return new BoundedExecutorService(es, maxConcurrentRequests); } /** * Instantiates listeners. Note that an exception thrown in the * constructor is interpreted as the listener being disabled, so * don't do anything too adventurous in the constructor, or if you do, * catch and log any errors locally before rethrowing. */ protected void spawnListener(String listenerClassName) throws IOException { try { ConnectorFactory connectorFactory = (ConnectorFactory) Class.forName(listenerClassName).newInstance(); connectorFactory. start(args, server); } catch (Throwable err) { throw (IOException)new IOException("Failed to start a listener: "+listenerClassName).initCause(err); } } /** * The main run method. This handles the normal thread processing. */ public void run() { boolean interrupted = false; try { ServerSocket controlSocket = null; if (this.controlPort > 0) { controlSocket = new ServerSocket(this.controlPort); controlSocket.setSoTimeout(CONTROL_TIMEOUT); } Logger.log(Logger.INFO, RESOURCES, "Launcher.StartupOK", RESOURCES.getString("ServerVersion"), (this.controlPort > 0 ? "" + this.controlPort : RESOURCES.getString("Launcher.ControlDisabled"))); // Enter the main loop while (!interrupted) { // this.threadPool.removeUnusedRequestHandlers(); // this.hostGroup.invalidateExpiredSessions(); // Check for control request Socket accepted = null; try { if (controlSocket != null) { accepted = controlSocket.accept(); if (accepted != null) { handleControlRequest(accepted); } } else { Thread.sleep(CONTROL_TIMEOUT); } } catch (InterruptedIOException err) { } catch (InterruptedException err) { interrupted = true; } catch (Throwable err) { Logger.log(Logger.ERROR, RESOURCES, "Launcher.ShutdownError", err); } finally { if (accepted != null) { try {accepted.close();} catch (IOException err) {} } if (Thread.interrupted()) { interrupted = true; } } } // Close server socket if (controlSocket != null) { controlSocket.close(); } } catch (Throwable err) { Logger.log(Logger.ERROR, RESOURCES, "Launcher.ShutdownError", err); } Logger.log(Logger.INFO, RESOURCES, "Launcher.ControlThreadShutdownOK"); } protected void handleControlRequest(Socket csAccepted) throws IOException { InputStream inSocket = null; OutputStream outSocket = null; ObjectInputStream inControl = null; try { inSocket = csAccepted.getInputStream(); int reqType = inSocket.read(); if ((byte) reqType == SHUTDOWN_TYPE) { Logger.log(Logger.INFO, RESOURCES, "Launcher.ShutdownRequestReceived"); shutdown(); } else if ((byte) reqType == RELOAD_TYPE) { inControl = new ObjectInputStream(inSocket); String host = inControl.readUTF(); String prefix = inControl.readUTF(); Logger.log(Logger.INFO, RESOURCES, "Launcher.ReloadRequestReceived", host + prefix); HostConfiguration hostConfig = this.hostGroup.getHostByName(host); hostConfig.reloadWebApp(prefix); } } finally { if (inControl != null) { try {inControl.close();} catch (IOException err) {} } if (inSocket != null) { try {inSocket.close();} catch (IOException err) {} } if (outSocket != null) { try {outSocket.close();} catch (IOException err) {} } } } public void shutdown() { try { server.stop(); } catch (Exception e) { Logger.log(Logger.INFO, RESOURCES, "Launcher.FailedShutdown", e); } // Release all listeners/pools/webapps this.threadPool.shutdown(); if (this.controlThread != null) { this.controlThread.interrupt(); } Thread.yield(); Logger.log(Logger.INFO, RESOURCES, "Launcher.ShutdownOK"); } public boolean isRunning() { return (this.controlThread != null) && this.controlThread.isAlive(); } /** * Main method. This basically just accepts a few args, then initialises the * listener thread. For now, just shut it down with a control-C. */ public static void main(String argv[]) throws IOException { Log.setLog(new JavaUtilLog()); // force java.util.logging for consistency & backward compatibility Map args = getArgsFromCommandLine(argv); if (Option.USAGE.isIn(args) || Option.HELP.isIn(args)) { printUsage(); return; } // Check for embedded war deployEmbeddedWarfile(args); // Check for embedded warfile if (!Option.WEBROOT.isIn(args) && !Option.WARFILE.isIn(args) && !Option.WEBAPPS_DIR.isIn(args)) { printUsage(); return; } // Launch try { new Launcher(args); } catch (Throwable err) { Logger.log(Logger.ERROR, RESOURCES, "Launcher.ContainerStartupError", err); System.exit(1); } } public static Map getArgsFromCommandLine(String argv[]) throws IOException { Map args = new CmdLineParser(Option.all(Option.class)).parse(argv,"nonSwitch"); // Small hack to allow re-use of the command line parsing inside the control tool String firstNonSwitchArgument = (String) args.get("nonSwitch"); args.remove("nonSwitch"); // Check if the non-switch arg is a file or folder, and overwrite the config if (firstNonSwitchArgument != null) { File webapp = new File(firstNonSwitchArgument); if (webapp.exists()) { if (webapp.isDirectory()) { args.put(Option.WEBROOT.name, firstNonSwitchArgument); } else if (webapp.isFile()) { args.put(Option.WARFILE.name, firstNonSwitchArgument); } } } return args; } protected static void deployEmbeddedWarfile(Map args) throws IOException { String embeddedWarfileName = RESOURCES.getString("Launcher.EmbeddedWarFile"); InputStream embeddedWarfile = Launcher.class.getResourceAsStream( embeddedWarfileName); if (embeddedWarfile != null) { File tempWarfile = File.createTempFile("embedded", ".war").getAbsoluteFile(); tempWarfile.getParentFile().mkdirs(); tempWarfile.deleteOnExit(); String embeddedWebroot = RESOURCES.getString("Launcher.EmbeddedWebroot"); File tempWebroot = new File(tempWarfile.getParentFile(), embeddedWebroot); tempWebroot.mkdirs(); Logger.log(Logger.DEBUG, RESOURCES, "Launcher.CopyingEmbeddedWarfile", tempWarfile.getAbsolutePath()); OutputStream out = new FileOutputStream(tempWarfile, true); int read; byte buffer[] = new byte[2048]; while ((read = embeddedWarfile.read(buffer)) != -1) { out.write(buffer, 0, read); } out.close(); embeddedWarfile.close(); Option.WARFILE.put(args, tempWarfile.getAbsolutePath()); Option.WARFILE.put(args, tempWebroot.getAbsolutePath()); Option.WEBAPPS_DIR.remove(args); } } public static void initLogger(Map args) throws IOException { // Reset the log level int logLevel = Option.intArg(args, Option.DEBUG.name, Logger.INFO.intValue()); boolean showThrowingLineNo = Option.LOG_THROWING_LINE_NO.get(args); boolean showThrowingThread = Option.LOG_THROWING_THREAD.get(args); OutputStream logStream; if (args.get("logfile") != null) { logStream = new FileOutputStream((String) args.get("logfile")); } else if (Option.booleanArg(args, "logToStdErr", false)) { logStream = System.err; } else { logStream = System.out; } // Logger.init(logLevel, logStream, showThrowingLineNo, showThrowingThread); Logger.init(Level.parse(String.valueOf(logLevel)), logStream, showThrowingThread); } protected static void printUsage() { // if the caller overrides the usage, use that instead. String usage = USAGE; String header = RESOURCES.getString("Launcher.UsageInstructions.Header", RESOURCES.getString("ServerVersion")); String options = RESOURCES.getString("Launcher.UsageInstructions.Options"); String footer = RESOURCES.getString("Launcher.UsageInstructions.Options"); if(usage==null) { usage = header+options+footer; } else { usage = usage.replace("{HEADER}",header).replace("{OPTIONS}",options).replace("{FOOTER}",footer); } System.out.println(usage); } /** * Overridable usage screen */ public static String USAGE; }