// ========================================================================
// Copyright (c) 2003-2009 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
package org.eclipse.jetty.start;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
/*-------------------------------------------*/
/**
* <p>
* Main start class. This class is intended to be the main class listed in the MANIFEST.MF of the start.jar archive. It allows an application to be started with
* the command "java -jar start.jar".
* </p>
*
* <p>
* The behaviour of Main is controlled by the parsing of the {@link Config} "org/eclipse/start/start.config" file obtained as a resource or file.
* </p>
*/
public class Main
{
private static final int EXIT_USAGE = 1;
private static final int ERR_LOGGING = -1;
private static final int ERR_INVOKE_MAIN = -2;
private static final int ERR_NOT_STOPPED = -4;
private static final int ERR_UNKNOWN = -5;
private boolean _showUsage = false;
private boolean _dumpVersions = false;
private boolean _listConfig = false;
private boolean _listOptions = false;
private boolean _dryRun = false;
private boolean _exec = false;
private final Config _config = new Config();
private final Set<String> _sysProps = new HashSet<String>();
private final List<String> _jvmArgs = new ArrayList<String>();
private String _startConfig = null;
private String _jettyHome;
public static void main(String[] args)
{
try
{
Main main = new Main();
List<String> arguments = main.expandCommandLine(args);
List<String> xmls = main.processCommandLine(arguments);
if (xmls!=null)
main.start(xmls);
}
catch (Throwable e)
{
usageExit(e,ERR_UNKNOWN);
}
}
Main() throws IOException
{
_jettyHome = System.getProperty("jetty.home",".");
_jettyHome = new File(_jettyHome).getCanonicalPath();
}
public List<String> expandCommandLine(String[] args) throws Exception
{
List<String> arguments = new ArrayList<String>();
// add the command line args and look for start.ini args
boolean ini = false;
for (String arg : args)
{
if (arg.startsWith("--ini=") || arg.equals("--ini"))
{
ini = true;
if (arg.length() > 6)
{
arguments.addAll(loadStartIni(new File(arg.substring(6))));
continue;
}
}
else if (arg.startsWith("--config="))
{
_startConfig = arg.substring(9);
}
else
{
arguments.add(arg);
}
}
// if no non-option inis, add the start.ini and start.d
if (!ini)
{
arguments.addAll(0,parseStartIniFiles());
}
return arguments;
}
List<String> parseStartIniFiles()
{
List<String> ini_args=new ArrayList<String>();
File start_ini = new File(_jettyHome,"start.ini");
if (start_ini.exists())
ini_args.addAll(loadStartIni(start_ini));
File start_d = new File(_jettyHome,"start.d");
if (start_d.isDirectory())
{
File[] inis = start_d.listFiles(new FilenameFilter()
{
public boolean accept(File dir, String name)
{
return name.toLowerCase().endsWith(".ini");
}
});
Arrays.sort(inis);
for (File i : inis)
ini_args.addAll(loadStartIni(i));
}
return ini_args;
}
public List<String> processCommandLine(List<String> arguments) throws Exception
{
// The XML Configuration Files to initialize with
List<String> xmls = new ArrayList<String>();
// Process the arguments
int startup = 0;
for (String arg : arguments)
{
if ("--help".equals(arg) || "-?".equals(arg))
{
_showUsage = true;
continue;
}
if ("--stop".equals(arg))
{
int port = Integer.parseInt(Config.getProperty("STOP.PORT","-1"));
String key = Config.getProperty("STOP.KEY",null);
stop(port,key);
return null;
}
if ("--version".equals(arg) || "-v".equals(arg) || "--info".equals(arg))
{
_dumpVersions = true;
continue;
}
if ("--list-modes".equals(arg) || "--list-options".equals(arg))
{
_listOptions = true;
continue;
}
if ("--list-config".equals(arg))
{
_listConfig = true;
continue;
}
if ("--exec-print".equals(arg) || "--dry-run".equals(arg))
{
_dryRun = true;
continue;
}
if ("--exec".equals(arg))
{
_exec = true;
continue;
}
// Special internal indicator that jetty was started by the jetty.sh Daemon
if ("--daemon".equals(arg))
{
File startDir = new File(System.getProperty("jetty.logs","logs"));
if (!startDir.exists() || !startDir.canWrite())
startDir = new File(".");
File startLog = new File(startDir,"start.log");
if (!startLog.exists() && !startLog.createNewFile())
{
// Output about error is lost in majority of cases.
System.err.println("Unable to create: " + startLog.getAbsolutePath());
// Toss a unique exit code indicating this failure.
usageExit(ERR_LOGGING);
}
if (!startLog.canWrite())
{
// Output about error is lost in majority of cases.
System.err.println("Unable to write to: " + startLog.getAbsolutePath());
// Toss a unique exit code indicating this failure.
usageExit(ERR_LOGGING);
}
PrintStream logger = new PrintStream(new FileOutputStream(startLog,false));
System.setOut(logger);
System.setErr(logger);
System.out.println("Establishing start.log on " + new Date());
continue;
}
if (arg.startsWith("--pre="))
{
xmls.add(startup++,arg.substring(6));
continue;
}
if (arg.startsWith("-D"))
{
String[] assign = arg.substring(2).split("=",2);
_sysProps.add(assign[0]);
switch (assign.length)
{
case 2:
System.setProperty(assign[0],assign[1]);
break;
case 1:
System.setProperty(assign[0],"");
break;
default:
break;
}
continue;
}
if (arg.startsWith("-"))
{
_jvmArgs.add(arg);
continue;
}
// Is this a Property?
if (arg.indexOf('=') >= 0)
{
String[] assign = arg.split("=",2);
switch (assign.length)
{
case 2:
if ("OPTIONS".equals(assign[0]))
{
String opts[] = assign[1].split(",");
for (String opt : opts)
_config.addActiveOption(opt);
}
else
{
this._config.setProperty(assign[0],assign[1]);
}
break;
case 1:
this._config.setProperty(assign[0],null);
break;
default:
break;
}
continue;
}
// Anything else is considered an XML file.
if (xmls.contains(arg))
{
System.out.println("WARN: Argument '" + arg + "' specified multiple times. Check start.ini?");
System.out.println("Use \"java -jar start.jar --help\" for more information.");
}
xmls.add(arg);
}
return xmls;
}
private void usage()
{
String usageResource = "org/eclipse/jetty/start/usage.txt";
InputStream usageStream = getClass().getClassLoader().getResourceAsStream(usageResource);
if (usageStream == null)
{
System.err.println("ERROR: detailed usage resource unavailable");
usageExit(EXIT_USAGE);
}
BufferedReader buf = null;
try
{
buf = new BufferedReader(new InputStreamReader(usageStream));
String line;
while ((line = buf.readLine()) != null)
{
if (line.endsWith("@") && line.indexOf('@') != line.lastIndexOf('@'))
{
String indent = line.substring(0,line.indexOf("@"));
String info = line.substring(line.indexOf('@'),line.lastIndexOf('@'));
if (info.equals("@OPTIONS"))
{
List<String> sortedOptions = new ArrayList<String>();
sortedOptions.addAll(_config.getSectionIds());
Collections.sort(sortedOptions);
for (String option : sortedOptions)
{
if ("*".equals(option) || option.trim().length() == 0)
continue;
System.out.print(indent);
System.out.println(option);
}
}
else if (info.equals("@CONFIGS"))
{
File etc = new File(System.getProperty("jetty.home","."),"etc");
if (!etc.exists() || !etc.isDirectory())
{
System.out.print(indent);
System.out.println("Unable to find/list " + etc);
continue;
}
File configs[] = etc.listFiles(new FileFilter()
{
public boolean accept(File path)
{
if (!path.isFile())
{
return false;
}
String name = path.getName().toLowerCase();
return (name.startsWith("jetty") && name.endsWith(".xml"));
}
});
List<File> configFiles = new ArrayList<File>();
configFiles.addAll(Arrays.asList(configs));
Collections.sort(configFiles);
for (File configFile : configFiles)
{
System.out.print(indent);
System.out.print("etc/");
System.out.println(configFile.getName());
}
}
else if (info.equals("@STARTINI"))
{
List<String> ini = loadStartIni(null);
if (ini != null && ini.size() > 0)
{
for (String a : ini)
{
System.out.print(indent);
System.out.println(a);
}
}
else
{
System.out.print(indent);
System.out.println("none");
}
}
}
else
{
System.out.println(line);
}
}
}
catch (IOException e)
{
usageExit(e,EXIT_USAGE);
}
finally
{
close(buf);
}
System.exit(EXIT_USAGE);
}
public void invokeMain(ClassLoader classloader, String classname, List<String> args) throws IllegalAccessException, InvocationTargetException,
NoSuchMethodException, ClassNotFoundException
{
Class<?> invoked_class = null;
try
{
invoked_class = classloader.loadClass(classname);
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}
if (Config.isDebug() || invoked_class == null)
{
if (invoked_class == null)
{
System.err.println("ClassNotFound: " + classname);
}
else
{
System.err.println(classname + " " + invoked_class.getPackage().getImplementationVersion());
}
if (invoked_class == null)
{
usageExit(ERR_INVOKE_MAIN);
return;
}
}
String argArray[] = args.toArray(new String[0]);
Class<?>[] method_param_types = new Class[]
{ argArray.getClass() };
Method main = invoked_class.getDeclaredMethod("main",method_param_types);
Object[] method_params = new Object[]
{ argArray };
main.invoke(null,method_params);
}
/* ------------------------------------------------------------ */
public static void close(Closeable c)
{
if (c == null)
{
return;
}
try
{
c.close();
}
catch (IOException e)
{
e.printStackTrace(System.err);
}
}
/* ------------------------------------------------------------ */
public void start(List<String> xmls) throws FileNotFoundException, IOException, InterruptedException
{
// Setup Start / Stop Monitoring
int port = Integer.parseInt(Config.getProperty("STOP.PORT","-1"));
String key = Config.getProperty("STOP.KEY",null);
Monitor monitor = new Monitor(port,key);
// Load potential Config (start.config)
List<String> configuredXmls = loadConfig(xmls);
// No XML defined in start.config or command line. Can't execute.
if (configuredXmls.isEmpty())
{
throw new FileNotFoundException("No XML configuration files specified in start.config or command line.");
}
// Normalize the XML config options passed on the command line.
configuredXmls = resolveXmlConfigs(configuredXmls);
// Get Desired Classpath based on user provided Active Options.
Classpath classpath = _config.getActiveClasspath();
System.setProperty("java.class.path",classpath.toString());
ClassLoader cl = classpath.getClassLoader();
if (Config.isDebug())
{
System.err.println("java.class.path=" + System.getProperty("java.class.path"));
System.err.println("jetty.home=" + System.getProperty("jetty.home"));
System.err.println("java.home=" + System.getProperty("java.home"));
System.err.println("java.io.tmpdir=" + System.getProperty("java.io.tmpdir"));
System.err.println("java.class.path=" + classpath);
System.err.println("classloader=" + cl);
System.err.println("classloader.parent=" + cl.getParent());
System.err.println("properties=" + Config.getProperties());
}
// Show the usage information and return
if (_showUsage)
{
usage();
return;
}
// Show the version information and return
if (_dumpVersions)
{
showClasspathWithVersions(classpath);
return;
}
// Show all options with version information
if (_listOptions)
{
showAllOptionsWithVersions(classpath);
return;
}
if (_listConfig)
{
listConfig();
return;
}
// Show Command Line to execute Jetty
if (_dryRun)
{
CommandLineBuilder cmd = buildCommandLine(classpath,configuredXmls);
System.out.println(cmd.toString());
return;
}
// execute Jetty in another JVM
if (_exec)
{
CommandLineBuilder cmd = buildCommandLine(classpath,configuredXmls);
ProcessBuilder pbuilder = new ProcessBuilder(cmd.getArgs());
final Process process = pbuilder.start();
try
{
copyInThread(process.getErrorStream(),System.err);
copyInThread(process.getInputStream(),System.out);
copyInThread(System.in,process.getOutputStream());
monitor.setProcess(process);
process.waitFor();
}
finally
{
Config.debug("Destroying " + process);
process.destroy();
}
return;
}
if (_jvmArgs.size() > 0 || _sysProps.size() > 0)
{
System.err.println("WARNING: System properties and/or JVM args set. Consider using --dry-run or --exec");
}
// Set current context class loader to what is selected.
Thread.currentThread().setContextClassLoader(cl);
// Invoke the Main Class
try
{
// Get main class as defined in start.config
String classname = _config.getMainClassname();
// Check for override of start class (via "jetty.server" property)
String mainClass = System.getProperty("jetty.server");
if (mainClass != null)
{
classname = mainClass;
}
// Check for override of start class (via "main.class" property)
mainClass = System.getProperty("main.class");
if (mainClass != null)
{
classname = mainClass;
}
Config.debug("main.class=" + classname);
invokeMain(cl,classname,configuredXmls);
}
catch (Exception e)
{
usageExit(e,ERR_INVOKE_MAIN);
}
}
private void copyInThread(final InputStream in, final OutputStream out)
{
new Thread(new Runnable()
{
public void run()
{
try
{
byte[] buf = new byte[1024];
int len = in.read(buf);
while (len > 0)
{
out.write(buf,0,len);
len = in.read(buf);
}
}
catch (IOException e)
{
// e.printStackTrace();
}
}
}).start();
}
private String resolveXmlConfig(String xmlFilename) throws FileNotFoundException
{
if (!xmlFilename.toLowerCase().endsWith(".xml"))
{
// Nothing to resolve.
return xmlFilename;
}
File xml = new File(xmlFilename);
if (xml.exists() && xml.isFile())
{
return xml.getAbsolutePath();
}
xml = new File(_jettyHome,fixPath(xmlFilename));
if (xml.exists() && xml.isFile())
{
return xml.getAbsolutePath();
}
xml = new File(_jettyHome,fixPath("etc/" + xmlFilename));
if (xml.exists() && xml.isFile())
{
return xml.getAbsolutePath();
}
throw new FileNotFoundException("Unable to find XML Config: " + xmlFilename);
}
CommandLineBuilder buildCommandLine(Classpath classpath, List<String> xmls) throws IOException
{
CommandLineBuilder cmd = new CommandLineBuilder(findJavaBin());
for (String x : _jvmArgs)
{
cmd.addArg(x);
}
cmd.addRawArg("-Djetty.home=" + _jettyHome);
for (String p : _sysProps)
{
String v = System.getProperty(p);
cmd.addEqualsArg("-D" + p,v);
}
cmd.addArg("-cp");
cmd.addRawArg(classpath.toString());
cmd.addRawArg(_config.getMainClassname());
// Check if we need to pass properties as a file
Properties properties = Config.getProperties();
if (properties.size() > 0)
{
File prop_file = File.createTempFile("start",".properties");
if (!_dryRun)
prop_file.deleteOnExit();
properties.store(new FileOutputStream(prop_file),"start.jar properties");
cmd.addArg(prop_file.getAbsolutePath());
}
for (String xml : xmls)
{
cmd.addRawArg(xml);
}
return cmd;
}
private String findJavaBin()
{
File javaHome = new File(System.getProperty("java.home"));
if (!javaHome.exists())
{
return null;
}
File javabin = findExecutable(javaHome,"bin/java");
if (javabin != null)
{
return javabin.getAbsolutePath();
}
javabin = findExecutable(javaHome,"bin/java.exe");
if (javabin != null)
{
return javabin.getAbsolutePath();
}
return "java";
}
private File findExecutable(File root, String path)
{
String npath = path.replace('/',File.separatorChar);
File exe = new File(root,npath);
if (!exe.exists())
{
return null;
}
return exe;
}
private void showAllOptionsWithVersions(Classpath classpath)
{
Set<String> sectionIds = _config.getSectionIds();
StringBuffer msg = new StringBuffer();
msg.append("There ");
if (sectionIds.size() > 1)
{
msg.append("are ");
}
else
{
msg.append("is ");
}
msg.append(String.valueOf(sectionIds.size()));
msg.append(" OPTION");
if (sectionIds.size() > 1)
{
msg.append("s");
}
msg.append(" available to use.");
System.out.println(msg);
System.out.println("Each option is listed along with associated available classpath entries, in the order that they would appear from that mode.");
System.out.println("Note: If using multiple options (eg: 'Server,servlet,webapp,jms,jmx') "
+ "then overlapping entries will not be repeated in the eventual classpath.");
System.out.println();
System.out.printf("${jetty.home} = %s%n",_jettyHome);
System.out.println();
for (String sectionId : sectionIds)
{
if (Config.DEFAULT_SECTION.equals(sectionId))
{
System.out.println("GLOBAL option (Prepended Entries)");
}
else if ("*".equals(sectionId))
{
System.out.println("GLOBAL option (Appended Entries) (*)");
}
else
{
System.out.printf("Option [%s]",sectionId);
if (Character.isUpperCase(sectionId.charAt(0)))
{
System.out.print(" (Aggregate)");
}
System.out.println();
}
System.out.println("-------------------------------------------------------------");
Classpath sectionCP = _config.getSectionClasspath(sectionId);
if (sectionCP.isEmpty())
{
System.out.println("Empty option, no classpath entries active.");
System.out.println();
continue;
}
int i = 0;
for (File element : sectionCP.getElements())
{
String elementPath = element.getAbsolutePath();
if (elementPath.startsWith(_jettyHome))
{
elementPath = "${jetty.home}" + elementPath.substring(_jettyHome.length());
}
System.out.printf("%2d: %20s | %s\n",i++,getVersion(element),elementPath);
}
System.out.println();
}
}
private void showClasspathWithVersions(Classpath classpath)
{
// Iterate through active classpath, and fetch Implementation Version from each entry (if present)
// to dump to end user.
System.out.println("Active Options: " + _config.getActiveOptions());
if (classpath.count() == 0)
{
System.out.println("No version information available show.");
return;
}
System.out.println("Version Information on " + classpath.count() + " entr" + ((classpath.count() > 1)?"ies":"y") + " in the classpath.");
System.out.println("Note: order presented here is how they would appear on the classpath.");
System.out.println(" changes to the OPTIONS=[option,option,...] command line option will be reflected here.");
int i = 0;
for (File element : classpath.getElements())
{
String elementPath = element.getAbsolutePath();
if (elementPath.startsWith(_jettyHome))
{
elementPath = "${jetty.home}" + elementPath.substring(_jettyHome.length());
}
System.out.printf("%2d: %20s | %s\n",i++,getVersion(element),elementPath);
}
}
private String fixPath(String path)
{
return path.replace('/',File.separatorChar);
}
private String getVersion(File element)
{
if (element.isDirectory())
{
return "(dir)";
}
if (element.isFile())
{
String name = element.getName().toLowerCase();
if (name.endsWith(".jar"))
{
return JarVersion.getVersion(element);
}
if (name.endsWith(".zip"))
{
return getZipVersion(element);
}
}
return "";
}
private String getZipVersion(File element)
{
// TODO - find version in zip file. Look for META-INF/MANIFEST.MF ?
return "";
}
private List<String> resolveXmlConfigs(List<String> xmls) throws FileNotFoundException
{
List<String> ret = new ArrayList<String>();
for (String xml : xmls)
{
ret.add(resolveXmlConfig(xml));
}
return ret;
}
private void listConfig()
{
InputStream cfgstream = null;
try
{
cfgstream = getConfigStream();
byte[] buf = new byte[4096];
int len = 0;
while (len >= 0)
{
len = cfgstream.read(buf);
if (len > 0)
System.out.write(buf,0,len);
}
}
catch (Exception e)
{
usageExit(e,ERR_UNKNOWN);
}
finally
{
close(cfgstream);
}
}
/**
* Load Configuration.
*
* No specific configuration is real until a {@link Config#getCombinedClasspath(java.util.Collection)} is used to execute the {@link Class} specified by
* {@link Config#getMainClassname()} is executed.
*
* @param xmls
* the command line specified xml configuration options.
* @return the list of xml configurations arriving via command line and start.config choices.
*/
private List<String> loadConfig(List<String> xmls)
{
InputStream cfgstream = null;
try
{
// Pass in xmls.size into Config so that conditions based on "nargs" work.
_config.setArgCount(xmls.size());
cfgstream = getConfigStream();
// parse the config
_config.parse(cfgstream);
_jettyHome = Config.getProperty("jetty.home",_jettyHome);
if (_jettyHome != null)
{
_jettyHome = new File(_jettyHome).getCanonicalPath();
System.setProperty("jetty.home",_jettyHome);
}
// Collect the configured xml configurations.
List<String> ret = new ArrayList<String>();
ret.addAll(xmls); // add command line provided xmls first.
for (String xmlconfig : _config.getXmlConfigs())
{
// add xmlconfigs arriving via start.config
if (!ret.contains(xmlconfig))
{
ret.add(xmlconfig);
}
}
return ret;
}
catch (Exception e)
{
usageExit(e,ERR_UNKNOWN);
return null; // never executed (just here to satisfy javac compiler)
}
finally
{
close(cfgstream);
}
}
private InputStream getConfigStream() throws FileNotFoundException
{
String config = _startConfig;
if (config == null || config.length() == 0)
{
config = System.getProperty("START","org/eclipse/jetty/start/start.config");
}
Config.debug("config=" + config);
// Look up config as resource first.
InputStream cfgstream = getClass().getClassLoader().getResourceAsStream(config);
// resource not found, try filesystem next
if (cfgstream == null)
{
cfgstream = new FileInputStream(config);
}
return cfgstream;
}
/**
* Stop a running jetty instance.
*/
public void stop(int port, String key)
{
int _port = port;
String _key = key;
try
{
if (_port <= 0)
{
System.err.println("STOP.PORT system property must be specified");
}
if (_key == null)
{
_key = "";
System.err.println("STOP.KEY system property must be specified");
System.err.println("Using empty key");
}
Socket s = new Socket(InetAddress.getByName("127.0.0.1"),_port);
try
{
OutputStream out = s.getOutputStream();
out.write((_key + "\r\nstop\r\n").getBytes());
out.flush();
}
finally
{
s.close();
}
}
catch (ConnectException e)
{
usageExit(e,ERR_NOT_STOPPED);
}
catch (Exception e)
{
usageExit(e,ERR_UNKNOWN);
}
}
static void usageExit(Throwable t, int exit)
{
t.printStackTrace(System.err);
System.err.println();
System.err.println("Usage: java -jar start.jar [options] [properties] [configs]");
System.err.println(" java -jar start.jar --help # for more information");
System.exit(exit);
}
static void usageExit(int exit)
{
System.err.println();
System.err.println("Usage: java -jar start.jar [options] [properties] [configs]");
System.err.println(" java -jar start.jar --help # for more information");
System.exit(exit);
}
/**
* Convert a start.ini format file into an argument list.
*/
static List<String> loadStartIni(File ini)
{
File startIniFile = ini;
if (!startIniFile.exists())
{
if (ini != null)
{
System.err.println("Warning - can't find ini file: " + ini);
}
// No start.ini found, skip load.
return Collections.emptyList();
}
List<String> args = new ArrayList<String>();
FileReader reader = null;
BufferedReader buf = null;
try
{
reader = new FileReader(ini);
buf = new BufferedReader(reader);
String arg;
while ((arg = buf.readLine()) != null)
{
arg = arg.trim();
if (arg.length() == 0 || arg.startsWith("#"))
{
continue;
}
args.add(arg);
}
}
catch (IOException e)
{
usageExit(e,ERR_UNKNOWN);
}
finally
{
Main.close(buf);
Main.close(reader);
}
return args;
}
void addJvmArgs(List<String> jvmArgs)
{
_jvmArgs.addAll(jvmArgs);
}
}