/*
* 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 java.io.File;
import java.io.FileInputStream;
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.lang.reflect.Constructor;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* 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.HttpListener";
static final String HTTPS_LISTENER_CLASS = "winstone.ssl.HttpsListener";
static final String AJP_LISTENER_CLASS = "winstone.ajp13.Ajp13Listener";
static final String CLUSTER_CLASS = "winstone.cluster.SimpleCluster";
static final String DEFAULT_JNDI_MGR_CLASS = "winstone.jndi.ContainerJNDIManager";
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 int DEFAULT_CONTROL_PORT = -1;
private Thread controlThread;
public final static WinstoneResourceBundle RESOURCES = new WinstoneResourceBundle("winstone.LocalStrings");
private int controlPort;
private HostGroup hostGroup;
private ObjectPool objectPool;
private List listeners;
private Map args;
private Cluster cluster;
private JNDIManager globalJndiManager;
/**
* Constructor - initialises the web app, object pools, control port and the
* available protocol listeners.
*/
public Launcher(Map args) throws IOException {
boolean useJNDI = WebAppConfiguration.booleanArg(args, "useJNDI", false);
// Set jndi resource handler if not set (workaround for JamVM bug)
if (useJNDI) try {
Class ctxFactoryClass = Class.forName("winstone.jndi.java.javaURLContextFactory");
if (System.getProperty("java.naming.factory.initial") == null) {
System.setProperty("java.naming.factory.initial", ctxFactoryClass.getName());
}
if (System.getProperty("java.naming.factory.url.pkgs") == null) {
System.setProperty("java.naming.factory.url.pkgs", "winstone.jndi");
}
} catch (ClassNotFoundException err) {}
Logger.log(Logger.MAX, RESOURCES, "Launcher.StartupArgs", args + "");
this.args = args;
this.controlPort = (args.get("controlPort") == null ? DEFAULT_CONTROL_PORT
: Integer.parseInt((String) args.get("controlPort")));
// Check for java home
List jars = new ArrayList();
List commonLibCLPaths = new ArrayList();
String defaultJavaHome = System.getProperty("java.home");
String javaHome = WebAppConfiguration.stringArg(args, "javaHome", defaultJavaHome);
Logger.log(Logger.DEBUG, RESOURCES, "Launcher.UsingJavaHome", javaHome);
String toolsJarLocation = WebAppConfiguration.stringArg(args, "toolsJar", null);
File toolsJar = null;
if (toolsJarLocation == 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 = new File(javaHome).getParentFile();
File toolsJar2 = new File(javaHome2, "lib/tools.jar");
if (toolsJar2.exists()) {
javaHome = javaHome2.getCanonicalPath();
toolsJar = toolsJar2;
}
}
} else {
toolsJar = new File(toolsJarLocation);
}
// 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 (WebAppConfiguration.booleanArg(args, "useJasper", false))
Logger.log(Logger.WARNING, RESOURCES, "Launcher.ToolsJarNotFound");
// Set up common lib class loader
String commonLibCLFolder = WebAppConfiguration.stringArg(args,
"commonLibFolder", "lib");
File libFolder = new File(commonLibCLFolder);
if (libFolder.exists() && libFolder.isDirectory()) {
Logger.log(Logger.DEBUG, RESOURCES, "Launcher.UsingCommonLib",
libFolder.getCanonicalPath());
File children[] = libFolder.listFiles();
for (int n = 0; n < children.length; n++)
if (children[n].getName().endsWith(".jar")
|| children[n].getName().endsWith(".zip")) {
jars.add(children[n].toURL());
commonLibCLPaths.add(children[n]);
Logger.log(Logger.DEBUG, RESOURCES, "Launcher.AddedCommonLibJar",
children[n].getName());
}
} else {
Logger.log(Logger.DEBUG, RESOURCES, "Launcher.NoCommonLib");
}
ClassLoader commonLibCL = new URLClassLoader((URL[]) 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.objectPool = new ObjectPool(args);
// Optionally set up clustering if enabled and libraries are available
String useCluster = (String) args.get("useCluster");
boolean switchOnCluster = (useCluster != null)
&& (useCluster.equalsIgnoreCase("true") || useCluster
.equalsIgnoreCase("yes"));
if (switchOnCluster) {
if (this.controlPort < 0) {
Logger.log(Logger.INFO, RESOURCES,
"Launcher.ClusterOffNoControlPort");
} else {
String clusterClassName = WebAppConfiguration.stringArg(args, "clusterClassName",
CLUSTER_CLASS).trim();
try {
Class clusterClass = Class.forName(clusterClassName);
Constructor clusterConstructor = clusterClass
.getConstructor(new Class[] { Map.class, Integer.class });
this.cluster = (Cluster) clusterConstructor
.newInstance(new Object[] { args, new Integer(this.controlPort) });
} catch (ClassNotFoundException err) {
Logger.log(Logger.DEBUG, RESOURCES, "Launcher.ClusterNotFound");
} catch (Throwable err) {
Logger.log(Logger.WARNING, RESOURCES, "Launcher.ClusterStartupError", err);
}
}
}
// If jndi is enabled, run the container wide jndi populator
if (useJNDI) {
String jndiMgrClassName = WebAppConfiguration.stringArg(args, "containerJndiClassName",
DEFAULT_JNDI_MGR_CLASS).trim();
try {
// Build the realm
Class jndiMgrClass = Class.forName(jndiMgrClassName, true, commonLibCL);
Constructor jndiMgrConstr = jndiMgrClass.getConstructor(new Class[] {
Map.class, List.class, ClassLoader.class });
this.globalJndiManager = (JNDIManager) jndiMgrConstr.newInstance(new Object[] {
args, null, commonLibCL });
this.globalJndiManager.setup();
} catch (ClassNotFoundException err) {
Logger.log(Logger.DEBUG, RESOURCES,
"Launcher.JNDIDisabled");
} catch (Throwable err) {
Logger.log(Logger.ERROR, RESOURCES,
"Launcher.JNDIError", jndiMgrClassName, err);
}
}
// Open the web apps
this.hostGroup = new HostGroup(this.cluster, this.objectPool, commonLibCL,
(File []) commonLibCLPaths.toArray(new File[0]), args);
// Create connectors (http, https and ajp)
this.listeners = new ArrayList();
spawnListener(HTTP_LISTENER_CLASS);
spawnListener(AJP_LISTENER_CLASS);
try {
Class.forName("javax.net.ServerSocketFactory");
spawnListener(HTTPS_LISTENER_CLASS);
} catch (ClassNotFoundException err) {
Logger.log(Logger.DEBUG, RESOURCES,
"Launcher.NeedsJDK14", HTTPS_LISTENER_CLASS);
}
this.controlThread = new Thread(this, RESOURCES.getString(
"Launcher.ThreadName", "" + this.controlPort));
this.controlThread.setDaemon(false);
this.controlThread.start();
Runtime.getRuntime().addShutdownHook(new ShutdownHook(this));
}
/**
* 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) {
try {
Class listenerClass = Class.forName(listenerClassName);
Constructor listenerConstructor = listenerClass
.getConstructor(new Class[] { Map.class,
ObjectPool.class, HostGroup.class});
Listener listener = (Listener) listenerConstructor
.newInstance(new Object[] { args, this.objectPool,
this.hostGroup });
if (listener.start()) {
this.listeners.add(listener);
}
} catch (ClassNotFoundException err) {
Logger.log(Logger.INFO, RESOURCES,
"Launcher.ListenerNotFound", listenerClassName);
} catch (Throwable err) {
Logger.log(Logger.ERROR, RESOURCES,
"Launcher.ListenerStartupError", listenerClassName, 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",
new String[] {RESOURCES.getString("ServerVersion"),
(this.controlPort > 0 ? "" + this.controlPort
: RESOURCES.getString("Launcher.ControlDisabled"))});
// Enter the main loop
while (!interrupted) {
// this.objectPool.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);
} else if (this.cluster != null) {
outSocket = csAccepted.getOutputStream();
this.cluster.clusterRequest((byte) reqType,
inSocket, outSocket, csAccepted,
this.hostGroup);
}
} 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() {
// Release all listeners/pools/webapps
for (Iterator i = this.listeners.iterator(); i.hasNext();)
((Listener) i.next()).destroy();
this.objectPool.destroy();
if (this.cluster != null)
this.cluster.destroy();
this.hostGroup.destroy();
if (this.globalJndiManager != null) {
this.globalJndiManager.tearDown();
}
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 {
Map args = getArgsFromCommandLine(argv);
if (args.containsKey("usage") || args.containsKey("help")) {
printUsage();
return;
}
// Check for embedded war
deployEmbeddedWarfile(args);
// Check for embedded warfile
if (!args.containsKey("webroot") && !args.containsKey("warfile")
&& !args.containsKey("webappsDir")&& !args.containsKey("hostsDir")) {
printUsage();
return;
}
// Launch
try {
new Launcher(args);
} catch (Throwable err) {
Logger.log(Logger.ERROR, RESOURCES, "Launcher.ContainerStartupError", err);
}
}
public static Map getArgsFromCommandLine(String argv[]) throws IOException {
Map args = loadArgsFromCommandLineAndConfig(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("webroot", firstNonSwitchArgument);
} else if (webapp.isFile()) {
args.put("warfile", firstNonSwitchArgument);
}
}
}
return args;
}
public static Map loadArgsFromCommandLineAndConfig(String argv[], String nonSwitchArgName)
throws IOException {
Map args = new HashMap();
// Load embedded properties file
String embeddedPropertiesFilename = RESOURCES.getString(
"Launcher.EmbeddedPropertiesFile");
InputStream embeddedPropsStream = Launcher.class.getResourceAsStream(
embeddedPropertiesFilename);
if (embeddedPropsStream != null) {
loadPropsFromStream(embeddedPropsStream, args);
embeddedPropsStream.close();
}
// Get command line args
String configFilename = RESOURCES.getString("Launcher.DefaultPropertyFile");
for (int n = 0; n < argv.length; n++) {
String option = argv[n];
if (option.startsWith("--")) {
int equalPos = option.indexOf('=');
String paramName = option.substring(2,
equalPos == -1 ? option.length() : equalPos);
if (equalPos != -1) {
args.put(paramName, option.substring(equalPos + 1));
} else {
args.put(paramName, "true");
}
if (paramName.equals("config")) {
configFilename = (String) args.get(paramName);
}
} else {
args.put(nonSwitchArgName, option);
}
}
// Load default props if available
File configFile = new File(configFilename);
if (configFile.exists() && configFile.isFile()) {
InputStream inConfig = new FileInputStream(configFile);
loadPropsFromStream(inConfig, args);
inConfig.close();
initLogger(args);
Logger.log(Logger.DEBUG, RESOURCES, "Launcher.UsingPropertyFile",
configFilename);
} else {
initLogger(args);
}
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 = 0;
byte buffer[] = new byte[2048];
while ((read = embeddedWarfile.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
out.close();
embeddedWarfile.close();
args.put("warfile", tempWarfile.getAbsolutePath());
args.put("webroot", tempWebroot.getAbsolutePath());
args.remove("webappsDir");
args.remove("hostsDir");
}
}
protected static void loadPropsFromStream(InputStream inConfig, Map args) throws IOException {
Properties props = new Properties();
props.load(inConfig);
for (Iterator i = props.keySet().iterator(); i.hasNext(); ) {
String key = (String) i.next();
if (!args.containsKey(key.trim())) {
args.put(key.trim(), props.getProperty(key).trim());
}
}
props.clear();
}
public static void initLogger(Map args) throws IOException {
// Reset the log level
int logLevel = WebAppConfiguration.intArg(args, "debug", Logger.INFO);
// boolean showThrowingLineNo = WebAppConfiguration.booleanArg(args, "logThrowingLineNo", false);
boolean showThrowingThread = WebAppConfiguration.booleanArg(args, "logThrowingThread", false);
OutputStream logStream = null;
if (args.get("logfile") != null) {
logStream = new FileOutputStream((String) args.get("logfile"));
} else if (WebAppConfiguration.booleanArg(args, "logToStdErr", false)) {
logStream = System.err;
} else {
logStream = System.out;
}
// Logger.init(logLevel, logStream, showThrowingLineNo, showThrowingThread);
Logger.init(logLevel, logStream, showThrowingThread);
}
protected static void printUsage() {
System.out.println(RESOURCES.getString("Launcher.UsageInstructions",
RESOURCES.getString("ServerVersion")));
}
}