blob: 50dc7c2f13f4ba5886b10ed6e16752cf78279c16 [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2017 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 static org.eclipse.jetty.start.UsageException.*;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
import org.eclipse.jetty.start.config.CommandLineConfigSource;
/**
* Main start class.
* <p>
* This class is intended to be the main class listed in the MANIFEST.MF of the start.jar archive. It allows the Jetty Application server to be started with the
* command "java -jar start.jar".
* <p>
* Argument processing steps:
* <ol>
* <li>Directory Locations:
* <ul>
* <li>jetty.home=[directory] (the jetty.home location)</li>
* <li>jetty.base=[directory] (the jetty.base location)</li>
* </ul>
* </li>
* <li>Start Logging behavior:
* <ul>
* <li>--debug (debugging enabled)</li>
* <li>--start-log-file=logs/start.log (output start logs to logs/start.log location)</li>
* </ul>
* </li>
* <li>Module Resolution</li>
* <li>Properties Resolution</li>
* <li>Present Optional Informational Options</li>
* <li>Normal Startup</li>
* </li>
* </ol>
*/
public class Main
{
private static final String EXITING_LICENSE_NOT_ACKNOWLEDGED = "Exiting: license not acknowledged!";
private static final int EXIT_USAGE = 1;
public static String join(Collection<?> objs, String delim)
{
if (objs==null)
{
return "";
}
StringBuilder str = new StringBuilder();
boolean needDelim = false;
for (Object obj : objs)
{
if (needDelim)
{
str.append(delim);
}
str.append(obj);
needDelim = true;
}
return str.toString();
}
public static void main(String[] args)
{
try
{
Main main = new Main();
StartArgs startArgs = main.processCommandLine(args);
main.start(startArgs);
}
catch (UsageException e)
{
System.err.println(e.getMessage());
usageExit(e.getCause(),e.getExitCode());
}
catch (Throwable e)
{
usageExit(e,UsageException.ERR_UNKNOWN);
}
}
static void usageExit(int exit)
{
usageExit(null,exit);
}
static void usageExit(Throwable t, int exit)
{
if (t != null)
{
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);
}
private BaseHome baseHome;
private StartArgs startupArgs;
public Main() throws IOException
{
}
private void copyInThread(final InputStream in, final OutputStream out)
{
new Thread(new Runnable()
{
@Override
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 void initFile(StartArgs args, FileArg farg)
{
try
{
Path file = baseHome.getBasePath(farg.location);
StartLog.debug("[init-file] %s module specified file %s",file.toAbsolutePath(),(FS.exists(file)?"[Exists!]":""));
if (FS.exists(file))
{
// file already initialized / downloaded, skip it
return;
}
if (farg.uri!=null)
{
URL url = new URL(farg.uri);
StartLog.log("DOWNLOAD", "%s to %s", url, farg.location);
FS.ensureDirectoryExists(file.getParent());
if (args.isTestingModeEnabled())
{
StartLog.log("TESTING MODE", "Skipping download of " + url);
return;
}
byte[] buf = new byte[8192];
try (InputStream in = url.openStream();
OutputStream out = Files.newOutputStream(file,StandardOpenOption.CREATE_NEW,StandardOpenOption.WRITE))
{
while (true)
{
int len = in.read(buf);
if (len > 0)
{
out.write(buf,0,len);
}
if (len < 0)
{
break;
}
}
}
}
else if (farg.location.endsWith("/"))
{
StartLog.log("MKDIR",baseHome.toShortForm(file));
FS.ensureDirectoryExists(file);
}
else
{
String shortRef = baseHome.toShortForm(file);
if (args.isTestingModeEnabled())
{
StartLog.log("TESTING MODE","Skipping required file check on: %s",shortRef);
return;
}
StartLog.warn("MISSING: Required file %s",shortRef);
}
}
catch (Exception e)
{
StartLog.warn("ERROR: processing %s%n%s",farg,e);
StartLog.warn(e);
usageExit(EXIT_USAGE);
}
}
private void dumpClasspathWithVersions(Classpath classpath)
{
StartLog.endStartLog();
System.out.println();
System.out.println("Jetty Server Classpath:");
System.out.println("-----------------------");
if (classpath.count() == 0)
{
System.out.println("No classpath entries and/or 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 --module=name command line options will be reflected here.");
int i = 0;
for (File element : classpath.getElements())
{
System.out.printf("%2d: %24s | %s\n",i++,getVersion(element),baseHome.toShortForm(element));
}
}
public BaseHome getBaseHome()
{
return baseHome;
}
private String getVersion(File element)
{
if (element.isDirectory())
{
return "(dir)";
}
if (element.isFile())
{
String name = element.getName().toLowerCase(Locale.ENGLISH);
if (name.endsWith(".jar"))
{
return JarVersion.getVersion(element);
}
}
return "";
}
public void invokeMain(ClassLoader classloader, StartArgs args) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException, ClassNotFoundException, IOException
{
Class<?> invoked_class = null;
String mainclass = args.getMainClassname();
try
{
invoked_class = classloader.loadClass(mainclass);
}
catch (ClassNotFoundException e)
{
System.out.println("WARNING: Nothing to start, exiting ...");
StartLog.debug(e);
usageExit(ERR_INVOKE_MAIN);
return;
}
StartLog.debug("%s - %s",invoked_class,invoked_class.getPackage().getImplementationVersion());
CommandLineBuilder cmd = args.getMainArgs(baseHome,false);
String argArray[] = cmd.getArgs().toArray(new String[0]);
StartLog.debug("Command Line Args: %s",cmd.toString());
Class<?>[] method_param_types = new Class[]
{ argArray.getClass() };
Method main = invoked_class.getDeclaredMethod("main",method_param_types);
Object[] method_params = new Object[] { argArray };
StartLog.endStartLog();
main.invoke(null,method_params);
}
public void listConfig(StartArgs args)
{
StartLog.endStartLog();
// Dump Jetty Home / Base
args.dumpEnvironment(baseHome);
// Dump JVM Args
args.dumpJvmArgs();
// Dump System Properties
args.dumpSystemProperties();
// Dump Properties
args.dumpProperties();
// Dump Classpath
dumpClasspathWithVersions(args.getClasspath());
// Dump Resolved XMLs
args.dumpActiveXmls(baseHome);
}
private void listModules(StartArgs args)
{
StartLog.endStartLog();
System.out.println();
System.out.println("Jetty All Available Modules:");
System.out.println("----------------------------");
args.getAllModules().dump();
// Dump Enabled Modules
System.out.println();
System.out.println("Jetty Active Module Tree:");
System.out.println("-------------------------");
Modules modules = args.getAllModules();
modules.dumpEnabledTree();
}
/**
* Build out INI file.
* <p>
* This applies equally for either <code>${jetty.base}/start.ini</code> or
* <code>${jetty.base}/start.d/${name}.ini</code>
*
* @param args the arguments of what modules are enabled
* @param name the name of the module to based the build of the ini
* @param topLevel
* @param appendStartIni true to append to <code>${jetty.base}/start.ini</code>,
* false to create a <code>${jetty.base}/start.d/${name}.ini</code> entry instead.
* @throws IOException
*/
private void buildIni(StartArgs args, String name, boolean topLevel, boolean appendStartIni) throws IOException
{
// Find the start.d relative to the base directory only.
Path start_d = baseHome.getBasePath("start.d");
// Is this a module?
Modules modules = args.getAllModules();
Module module = modules.get(name);
if (module == null)
{
StartLog.warn("ERROR: No known module for %s",name);
return;
}
boolean transitive = module.isEnabled() && (module.getSources().size() == 0);
// Find any named ini file and check it follows the convention
Path start_ini = baseHome.getBasePath("start.ini");
String short_start_ini = baseHome.toShortForm(start_ini);
Path startd_ini = start_d.resolve(name + ".ini");
String short_startd_ini = baseHome.toShortForm(startd_ini);
StartIni module_ini = null;
if (FS.exists(startd_ini))
{
module_ini = new StartIni(startd_ini);
if (module_ini.getLineMatches(Pattern.compile("--module=(.*, *)*" + name)).size() == 0)
{
StartLog.warn("ERROR: %s is not enabled in %s!",name,short_startd_ini);
return;
}
}
if (!args.isApproveAllLicenses())
{
if (!module.hasFiles(baseHome) && !module.acknowledgeLicense())
{
StartLog.warn(EXITING_LICENSE_NOT_ACKNOWLEDGED);
System.exit(1);
}
}
boolean buildIni=false;
if (module.isEnabled())
{
// is it an explicit request to create an ini file?
if (topLevel && !FS.exists(startd_ini) && !appendStartIni)
{
buildIni=true;
}
// else is it transitive
else if (transitive)
{
if (module.hasDefaultConfig())
{
buildIni = true;
StartLog.info("%-15s initialised transitively",name);
}
}
// else must be initialized explicitly
else
{
for (String source : module.getSources())
{
StartLog.info("%-15s initialised in %s",name,baseHome.toShortForm(source));
}
}
}
else
{
buildIni=true;
}
String source = "<transitive>";
// If we need an ini
if (buildIni)
{
// File BufferedWriter
BufferedWriter writer = null;
PrintWriter out = null;
try
{
if (appendStartIni)
{
source = short_start_ini;
StartLog.info("%-15s initialised in %s (appended)",name,source);
writer = Files.newBufferedWriter(start_ini,StandardCharsets.UTF_8,StandardOpenOption.CREATE,StandardOpenOption.APPEND);
out = new PrintWriter(writer);
}
else
{
// Create the directory if needed
FS.ensureDirectoryExists(start_d);
FS.ensureDirectoryWritable(start_d);
source = short_startd_ini;
StartLog.info("%-15s initialised in %s (created)",name,source);
writer = Files.newBufferedWriter(startd_ini,StandardCharsets.UTF_8,StandardOpenOption.CREATE_NEW,StandardOpenOption.WRITE);
out = new PrintWriter(writer);
}
if (appendStartIni)
{
out.println();
}
out.println("# --------------------------------------- ");
out.println("# Module: " + name);
out.println("--module=" + name);
args.parse("--module=" + name,source);
args.parseModule(module);
for (String line : module.getDefaultConfig())
{
out.println(line);
}
}
finally
{
if (out != null)
{
out.close();
}
}
}
modules.enable(name,Collections.singletonList(source));
// Also list other places this module is enabled
for (String src : module.getSources())
{
StartLog.debug("also enabled in: %s",src);
if (!short_start_ini.equals(src))
{
StartLog.info("%-15s enabled in %s",name,baseHome.toShortForm(src));
}
}
// Do downloads now
for (String file : module.getFiles())
{
initFile(args, new FileArg(module,file));
}
// Process dependencies
module.expandProperties(args.getProperties());
modules.registerParentsIfMissing(module);
modules.buildGraph();
// process new ini modules
if (topLevel)
{
List<Module> depends = new ArrayList<>();
for (String depend : modules.resolveParentModulesOf(name))
{
if (!name.equals(depend))
{
Module m = modules.get(depend);
m.setEnabled(true);
depends.add(m);
}
}
Collections.sort(depends,Collections.reverseOrder(new Module.DepthComparator()));
Set<String> done = new HashSet<>(0);
while (true)
{
// initialize known dependencies
boolean complete=true;
for (Module m : depends)
{
if (!done.contains(m.getName()))
{
complete=false;
buildIni(args,m.getName(),false,appendStartIni);
done.add(m.getName());
}
}
if (complete)
{
break;
}
// look for any new ones resolved via expansion
depends.clear();
for (String depend : modules.resolveParentModulesOf(name))
{
if (!name.equals(depend))
{
Module m = modules.get(depend);
m.setEnabled(true);
depends.add(m);
}
}
Collections.sort(depends,Collections.reverseOrder(new Module.DepthComparator()));
}
}
}
/**
* Convenience for <code>processCommandLine(cmdLine.toArray(new String[cmdLine.size()]))</code>
*/
public StartArgs processCommandLine(List<String> cmdLine) throws Exception
{
return this.processCommandLine(cmdLine.toArray(new String[cmdLine.size()]));
}
public StartArgs processCommandLine(String[] cmdLine) throws Exception
{
// Processing Order is important!
// ------------------------------------------------------------
// 1) Configuration Locations
CommandLineConfigSource cmdLineSource = new CommandLineConfigSource(cmdLine);
baseHome = new BaseHome(cmdLineSource);
StartLog.debug("jetty.home=%s",baseHome.getHome());
StartLog.debug("jetty.base=%s",baseHome.getBase());
// ------------------------------------------------------------
// 2) Parse everything provided.
// This would be the directory information +
// the various start inis
// and then the raw command line arguments
StartLog.debug("Parsing collected arguments");
StartArgs args = new StartArgs();
args.parse(baseHome.getConfigSources());
// ------------------------------------------------------------
// 3) Module Registration
Modules modules = new Modules(baseHome,args);
StartLog.debug("Registering all modules");
modules.registerAll();
// ------------------------------------------------------------
// 4) Active Module Resolution
for (String enabledModule : args.getEnabledModules())
{
List<String> msources = args.getSources(enabledModule);
modules.enable(enabledModule,msources);
}
StartLog.debug("Building Module Graph");
modules.buildGraph();
args.setAllModules(modules);
List<Module> activeModules = modules.resolveEnabled();
// ------------------------------------------------------------
// 5) Lib & XML Expansion / Resolution
args.expandLibs(baseHome);
args.expandModules(baseHome,activeModules);
// ------------------------------------------------------------
// 6) Resolve Extra XMLs
args.resolveExtraXmls(baseHome);
// ------------------------------------------------------------
// 9) Resolve Property Files
args.resolvePropertyFiles(baseHome);
return args;
}
public void start(StartArgs args) throws IOException, InterruptedException
{
StartLog.debug("StartArgs: %s",args);
// Get Desired Classpath based on user provided Active Options.
Classpath classpath = args.getClasspath();
System.setProperty("java.class.path",classpath.toString());
// Show the usage information and return
if (args.isHelp())
{
usage(true);
}
// Show the version information and return
if (args.isListClasspath())
{
dumpClasspathWithVersions(classpath);
}
// Show configuration
if (args.isListConfig())
{
listConfig(args);
}
// Show modules
if (args.isListModules())
{
listModules(args);
}
// Generate Module Graph File
if (args.getModuleGraphFilename() != null)
{
Path outputFile = baseHome.getBasePath(args.getModuleGraphFilename());
System.out.printf("Generating GraphViz Graph of Jetty Modules at %s%n",baseHome.toShortForm(outputFile));
ModuleGraphWriter writer = new ModuleGraphWriter();
writer.config(args.getProperties());
writer.write(args.getAllModules(),outputFile);
}
// Show Command Line to execute Jetty
if (args.isDryRun())
{
CommandLineBuilder cmd = args.getMainArgs(baseHome,true);
System.out.println(cmd.toString(File.separatorChar=='/'?" \\\n":" "));
}
if (args.isStopCommand())
{
doStop(args);
}
boolean rebuildGraph = false;
// Initialize start.ini
for (String module : args.getAddToStartIni())
{
buildIni(args,module,true,true);
rebuildGraph = true;
}
// Initialize start.d
for (String module : args.getAddToStartdIni())
{
buildIni(args,module,true,false);
rebuildGraph = true;
}
if (rebuildGraph)
{
args.getAllModules().clearMissing();
args.getAllModules().buildGraph();
}
// If in --create-files, check licenses
if(args.isDownload())
{
if (!args.isApproveAllLicenses())
{
for (Module module : args.getAllModules().resolveEnabled())
{
if (!module.hasFiles(baseHome) && !module.acknowledgeLicense())
{
StartLog.warn(EXITING_LICENSE_NOT_ACKNOWLEDGED);
System.exit(1);
}
}
}
}
// Check ini files for download possibilities
for (FileArg arg : args.getFiles())
{
Path file = baseHome.getBasePath(arg.location);
if (!FS.exists(file) && args.isDownload())
{
initFile(args, arg);
}
if (!FS.exists(file))
{
boolean isDir = arg.location.endsWith("/");
if (isDir)
{
StartLog.log("MKDIR", baseHome.toShortForm(file));
FS.ensureDirectoryExists(file);
/* Startup should not fail to run on missing directories.
* See Bug #427204
*/
// args.setRun(false);
}
else
{
String shortRef = baseHome.toShortForm(file);
if (args.isTestingModeEnabled())
{
StartLog.log("TESTING MODE","Skipping required file check on: %s",shortRef);
return;
}
StartLog.warn("Missing Required File: %s",baseHome.toShortForm(file));
args.setRun(false);
if (arg.uri != null)
{
StartLog.warn(" Can be downloaded From: %s",arg.uri);
StartLog.warn(" Run start.jar --create-files to download");
}
}
}
}
// Informational command line, don't run jetty
if (!args.isRun())
{
return;
}
// execute Jetty in another JVM
if (args.isExec())
{
CommandLineBuilder cmd = args.getMainArgs(baseHome,true);
cmd.debug();
ProcessBuilder pbuilder = new ProcessBuilder(cmd.getArgs());
StartLog.endStartLog();
final Process process = pbuilder.start();
Runtime.getRuntime().addShutdownHook(new Thread()
{
@Override
public void run()
{
StartLog.debug("Destroying " + process);
process.destroy();
}
});
copyInThread(process.getErrorStream(),System.err);
copyInThread(process.getInputStream(),System.out);
copyInThread(System.in,process.getOutputStream());
process.waitFor();
System.exit(0); // exit JVM when child process ends.
return;
}
if (args.hasJvmArgs() || args.hasSystemProperties())
{
System.err.println("WARNING: System properties and/or JVM args set. Consider using --dry-run or --exec");
}
ClassLoader cl = classpath.getClassLoader();
Thread.currentThread().setContextClassLoader(cl);
// Invoke the Main Class
try
{
invokeMain(cl, args);
}
catch (Exception e)
{
usageExit(e,ERR_INVOKE_MAIN);
}
}
private void doStop(StartArgs args)
{
String stopHost = args.getProperties().getString("STOP.HOST");
int stopPort = Integer.parseInt(args.getProperties().getString("STOP.PORT"));
String stopKey = args.getProperties().getString("STOP.KEY");
if (args.getProperties().getString("STOP.WAIT") != null)
{
int stopWait = Integer.parseInt(args.getProperties().getString("STOP.WAIT"));
stop(stopHost,stopPort,stopKey,stopWait);
}
else
{
stop(stopHost,stopPort,stopKey);
}
}
/**
* Stop a running jetty instance.
*/
public void stop(String host, int port, String key)
{
stop(host,port,key,0);
}
public void stop(String host, int port, String key, int timeout)
{
if (host==null || host.length()==0)
host="127.0.0.1";
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");
}
try (Socket s = new Socket(InetAddress.getByName(host),port))
{
if (timeout > 0)
{
s.setSoTimeout(timeout * 1000);
}
try (OutputStream out = s.getOutputStream())
{
out.write((key + "\r\nstop\r\n").getBytes());
out.flush();
if (timeout > 0)
{
System.err.printf("Waiting %,d seconds for jetty to stop%n",timeout);
LineNumberReader lin = new LineNumberReader(new InputStreamReader(s.getInputStream()));
String response;
while ((response = lin.readLine()) != null)
{
StartLog.debug("Received \"%s\"",response);
if ("Stopped".equals(response))
{
StartLog.warn("Server reports itself as Stopped");
}
}
}
}
}
}
catch (SocketTimeoutException e)
{
System.err.println("Timed out waiting for stop confirmation");
System.exit(ERR_UNKNOWN);
}
catch (ConnectException e)
{
usageExit(e,ERR_NOT_STOPPED);
}
catch (Exception e)
{
usageExit(e,ERR_UNKNOWN);
}
}
public void usage(boolean exit)
{
StartLog.endStartLog();
if(!printTextResource("org/eclipse/jetty/start/usage.txt"))
{
System.err.println("ERROR: detailed usage resource unavailable");
}
if (exit)
{
System.exit(EXIT_USAGE);
}
}
public static boolean printTextResource(String resourceName)
{
boolean resourcePrinted = false;
try (InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourceName))
{
if (stream != null)
{
try (InputStreamReader reader = new InputStreamReader(stream); BufferedReader buf = new BufferedReader(reader))
{
resourcePrinted = true;
String line;
while ((line = buf.readLine()) != null)
{
System.out.println(line);
}
}
}
else
{
System.out.println("Unable to find resource: " + resourceName);
}
}
catch (IOException e)
{
StartLog.warn(e);
}
return resourcePrinted;
}
// ------------------------------------------------------------
// implement Apache commons daemon (jsvc) lifecycle methods (init, start, stop, destroy)
public void init(String[] args) throws Exception
{
try
{
startupArgs = processCommandLine(args);
}
catch (UsageException e)
{
System.err.println(e.getMessage());
usageExit(e.getCause(),e.getExitCode());
}
catch (Throwable e)
{
usageExit(e,UsageException.ERR_UNKNOWN);
}
}
public void start() throws Exception
{
start(startupArgs);
}
public void stop() throws Exception
{
doStop(startupArgs);
}
public void destroy()
{
}
}