diff --git a/.gitignore b/.gitignore index c148916..474eca9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .gemcache/ vendor/ .jarcache +build/ diff --git a/build.gradle b/build.gradle index 3408456..b1c2fda 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'maven' apply plugin: 'java' +apply plugin: 'war' repositories { // Pull in Maven Central for all our non-ruby dependencies @@ -14,8 +15,10 @@ repositories { dependencies { runtime group: 'rubygems', name: 'sinatra', version: '1.4.5' runtime group: 'rubygems', name: 'rake', version: '10.3.+' - runtime group: 'org.apache.kafka', name: 'kafka_2.9.2', version: '0.8.+' + runtime group: 'rubygems', name: 'jruby-rack', version: '1.1.+' + runtime group: 'org.jruby', name: 'jruby-complete', version: '1.7.+' + runtime group: 'org.apache.kafka', name: 'kafka_2.9.2', version: '0.8.+' // Needed otherwise we get `"Artifact 'com.sun.jdmk:jmxtools:1.2.1:jmxtools.jar' not found."` runtime group: 'log4j', name: 'log4j', version: '1.2.+', transitive: true } @@ -59,4 +62,18 @@ task prepare { dependsOn cachejars, preparegems } +war { + dependsOn prepare + baseName 'rubygradle' + from "$buildDir/classes/main" + manifest { attributes 'Main-Class': 'WarMain' } + + webInf { + from 'vendor/gems', 'vendor/specifications', 'vendor/bin' + into 'gems' + } + + metaInf { from "init.rb" } +} + // vim: et ts=2 sw=2 autoindent ft=groovy diff --git a/init.rb b/init.rb new file mode 100644 index 0000000..60209c1 --- /dev/null +++ b/init.rb @@ -0,0 +1,19 @@ +WARBLER_CONFIG = {"public.root"=>"/", "rack.env"=>"production", "jruby.compat.version"=>"1.9"} + +if $servlet_context.nil? + ENV['GEM_HOME'] ||= File.expand_path('../../WEB-INF', __FILE__) + + ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../WEB-INF/Gemfile', __FILE__) + +else + ENV['GEM_HOME'] ||= $servlet_context.getRealPath('/WEB-INF/gems') + + ENV['BUNDLE_GEMFILE'] ||= $servlet_context.getRealPath('/WEB-INF/Gemfile') + +end +ENV['BUNDLE_WITHOUT'] = 'development:test:assets' + +ENV['RACK_ENV'] = 'production' + +$LOAD_PATH.unshift $servlet_context.getRealPath('/WEB-INF') if $servlet_context + diff --git a/run.sh b/run.sh index 0f09deb..ba3bd94 100755 --- a/run.sh +++ b/run.sh @@ -4,4 +4,4 @@ export GEM_HOME='' export GEM_PATH='' -exec ./gradlew prepare pkg +exec ./gradlew prepare war diff --git a/src/main/java/org/jruby/JarMain.java b/src/main/java/org/jruby/JarMain.java new file mode 100644 index 0000000..f1424c2 --- /dev/null +++ b/src/main/java/org/jruby/JarMain.java @@ -0,0 +1,272 @@ +/** + * Copyright (c) 2010-2012 Engine Yard, Inc. + * Copyright (c) 2007-2009 Sun Microsystems, Inc. + * This source code is available under the MIT license. + * See the file LICENSE.txt for details. + */ + +import java.io.File; +import java.io.IOException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public class JarMain implements Runnable { + + static final String MAIN = "/" + JarMain.class.getName().replace('.', '/') + ".class"; + + final boolean debug = isDebug(); + + protected final String[] args; + protected final String archive; + private final String path; + + protected File extractRoot; + + protected URLClassLoader classLoader; + + JarMain(String[] args) { + this.args = args; + URL mainClass = getClass().getResource(MAIN); + try { + this.path = mainClass.toURI().getSchemeSpecificPart(); + } + catch (URISyntaxException e) { + throw new RuntimeException(e); + } + archive = this.path.replace("!" + MAIN, "").replace("file:", ""); + + Runtime.getRuntime().addShutdownHook(new Thread(this)); + } + + protected URL[] extractArchive() throws Exception { + final JarFile jarFile = new JarFile(archive); + try { + Map jarNames = new HashMap(); + for (Enumeration e = jarFile.entries(); e.hasMoreElements(); ) { + JarEntry entry = e.nextElement(); + String extractPath = getExtractEntryPath(entry); + if ( extractPath != null ) jarNames.put(extractPath, entry); + } + + extractRoot = File.createTempFile("jruby", "extract"); + extractRoot.delete(); extractRoot.mkdirs(); + + final List urls = new ArrayList(); + for (Map.Entry e : jarNames.entrySet()) { + URL entryURL = extractEntry(e.getValue(), e.getKey()); + if (entryURL != null) urls.add( entryURL ); + } + return (URL[]) urls.toArray(new URL[urls.size()]); + } + finally { + jarFile.close(); + } + } + + protected String getExtractEntryPath(final JarEntry entry) { + final String name = entry.getName(); + if ( name.startsWith("META-INF/lib") && name.endsWith(".jar") ) { + return name.substring(name.lastIndexOf("/") + 1); + } + return null; // do not extract entry + } + + protected URL extractEntry(final JarEntry entry, final String path) throws Exception { + final File file = new File(extractRoot, path); + if ( entry.isDirectory() ) { + file.mkdirs(); + return null; + } + final String entryPath = entryPath(entry.getName()); + final InputStream entryStream; + try { + entryStream = new URI("jar", entryPath, null).toURL().openStream(); + } + catch (IllegalArgumentException e) { + // TODO gems '%' file name "encoding" ?! + debug("failed to open jar:" + entryPath + " skipping entry: " + entry.getName(), e); + return null; + } + final File parent = file.getParentFile(); + if ( parent != null ) parent.mkdirs(); + FileOutputStream outStream = new FileOutputStream(file); + final byte[] buf = new byte[65536]; + try { + int bytesRead = 0; + while ((bytesRead = entryStream.read(buf)) != -1) { + outStream.write(buf, 0, bytesRead); + } + } + finally { + entryStream.close(); + outStream.close(); + file.deleteOnExit(); + } + if (false) debug(entry.getName() + " extracted to " + file.getPath()); + return file.toURI().toURL(); + } + + protected String entryPath(String name) { + if ( ! name.startsWith("/") ) name = "/" + name; + return path.replace(MAIN, name); + } + + protected Object newScriptingContainer(final URL[] jars) throws Exception { + System.setProperty("org.jruby.embed.class.path", ""); + classLoader = new URLClassLoader(jars); + Class scriptingContainerClass = Class.forName("org.jruby.embed.ScriptingContainer", true, classLoader); + Object scriptingContainer = scriptingContainerClass.newInstance(); + debug("scripting container class loader urls: " + Arrays.toString(jars)); + invokeMethod(scriptingContainer, "setArgv", (Object) args); + invokeMethod(scriptingContainer, "setClassLoader", new Class[] { ClassLoader.class }, classLoader); + return scriptingContainer; + } + + protected int launchJRuby(final URL[] jars) throws Exception { + final Object scriptingContainer = newScriptingContainer(jars); + debug("invoking " + archive + " with: " + Arrays.deepToString(args)); + Object outcome = invokeMethod(scriptingContainer, "runScriptlet", launchScript()); + return ( outcome instanceof Number ) ? ( (Number) outcome ).intValue() : 0; + } + + protected String launchScript() { + return + "begin\n" + + " require 'META-INF/init.rb'\n" + + " require 'META-INF/main.rb'\n" + + " 0\n" + + "rescue SystemExit => e\n" + + " e.status\n" + + "end"; + } + + protected int start() throws Exception { + final URL[] jars = extractArchive(); + return launchJRuby(jars); + } + + protected void debug(String msg) { + debug(msg, null); + } + + protected void debug(String msg, Throwable t) { + if (debug) System.out.println(msg); + if (debug && t != null) t.printStackTrace(System.out); + } + + protected void warn(String msg) { + System.out.println("WARNING: " + msg); + } + + protected void delete(File f) { + try { + if (f.isDirectory() && !isSymlink(f)) { + File[] children = f.listFiles(); + for (int i = 0; i < children.length; i++) { + delete(children[i]); + } + } + f.delete(); + } catch (IOException e) { + System.err.println("error: " + e.toString()); + } + } + + protected boolean isSymlink(File file) throws IOException { + if (file == null) + throw new NullPointerException("File must not be null"); + File canon; + if (file.getParent() == null) { + canon = file; + } else { + File canonDir = file.getParentFile().getCanonicalFile(); + canon = new File(canonDir, file.getName()); + } + return !canon.getCanonicalFile().equals(canon.getAbsoluteFile()); + } + + public void run() { + // If the URLClassLoader isn't closed, on Windows, temp JARs won't be cleaned up + try { + invokeMethod(classLoader, "close"); + } + catch(NoSuchMethodException e) { } // We're not being run on Java >= 7 + catch(Exception e) { + System.err.println("error: " + e.toString()); + } + + if ( extractRoot != null ) delete(extractRoot); + } + + public static void main(String[] args) { + doStart(new JarMain(args)); + } + + protected static void doStart(final JarMain main) { + try { + int exit = main.start(); + if(isSystemExitEnabled()) System.exit(exit); + } catch (Exception e) { + System.err.println("error: " + e.toString()); + Throwable t = e; + while (t.getCause() != null && t.getCause() != t) { + t = t.getCause(); + } + if (isDebug()) { + t.printStackTrace(); + } + System.exit(1); + } + } + + protected static Object invokeMethod(final Object self, final String name, final Object... args) + throws NoSuchMethodException, IllegalAccessException, Exception { + + final Class[] signature = new Class[args.length]; + for ( int i = 0; i < args.length; i++ ) signature[i] = args[i].getClass(); + return invokeMethod(self, name, signature, args); + } + + protected static Object invokeMethod(final Object self, final String name, final Class[] signature, final Object... args) + throws NoSuchMethodException, IllegalAccessException, Exception { + Method method = self.getClass().getDeclaredMethod(name, signature); + try { + return method.invoke(self, args); + } + catch (InvocationTargetException e) { + Throwable target = e.getTargetException(); + if (target instanceof Exception) { + throw (Exception) target; + } + throw e; + } + } + + static boolean isDebug() { + return Boolean.getBoolean("warbler.debug"); + } + + /** + * if warbler.skip_system_exit system property is defined, we will not + * call System.exit in the normal flow. System.exit can cause problems + * for wrappers like procrun + */ + private static boolean isSystemExitEnabled(){ + return System.getProperty("warbler.skip_system_exit") == null; //omission enables System.exit use + } + +} diff --git a/src/main/java/org/jruby/WarMain.java b/src/main/java/org/jruby/WarMain.java new file mode 100644 index 0000000..42aac33 --- /dev/null +++ b/src/main/java/org/jruby/WarMain.java @@ -0,0 +1,372 @@ +/** + * Copyright (c) 2010-2012 Engine Yard, Inc. + * Copyright (c) 2007-2009 Sun Microsystems, Inc. + * This source code is available under the MIT license. + * See the file LICENSE.txt for details. + */ + +import java.lang.reflect.Method; +import java.io.InputStream; +import java.io.ByteArrayInputStream; +import java.io.SequenceInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.net.URI; +import java.net.URLClassLoader; +import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.Set; + +/** + * Used as a Main-Class in the manifest for a .war file, so that you can run + * a .war file with java -jar. + * + * WarMain can be used with different web server libraries. WarMain expects + * to have two files present in the .war file, + * WEB-INF/webserver.properties and WEB-INF/webserver.jar. + * + * When WarMain starts up, it extracts the webserver jar to a temporary + * directory, and creates a temporary work directory for the webapp. Both + * are deleted on exit. + * + * It then reads webserver.properties into a java.util.Properties object, + * creates a URL classloader holding the jar, and loads and invokes the + * main method of the main class mentioned in the properties. + * + * An example webserver.properties follows for Winstone. The args + * property indicates the names and ordering of other properties to be used + * as command-line arguments. The special tokens {{warfile}} and + * {{webroot}} are substituted with the location of the .war file + * being run and the temporary work directory, respectively. + *
+ * mainclass = winstone.Launcher
+ * args = args0,args1,args2
+ * args0 = --warfile={{warfile}}
+ * args1 = --webroot={{webroot}}
+ * args2 = --directoryListings=false
+ * 
+ * + * System properties can also be set via webserver.properties. For example, + * the following entries set jetty.home before launching the server. + *
+ * props = jetty.home
+ * jetty.home = {{webroot}}
+ * 
+ */ +public class WarMain extends JarMain { + + static final String MAIN = "/" + WarMain.class.getName().replace('.', '/') + ".class"; + static final String WEBSERVER_PROPERTIES = "/WEB-INF/webserver.properties"; + static final String WEBSERVER_JAR = "/WEB-INF/webserver.jar"; + + /** + * jruby arguments, consider the following command : + * `java -jar rails.war --1.9 -S rake db:migrate` + * arguments == [ "--1.9" ] + * executable == "rake" + * executableArgv == [ "db:migrate" ] + */ + private final String[] arguments; + + /** + * null to launch webserver or != null to run a executable e.g. rake + */ + private final String executable; + private final String[] executableArgv; + + private File webroot; + + WarMain(final String[] args) { + super(args); + final List argsList = Arrays.asList(args); + final int sIndex = argsList.indexOf("-S"); + if ( sIndex == -1 ) { + executable = null; executableArgv = null; arguments = null; + } + else { + if ( args.length == sIndex + 1 || args[sIndex + 1].isEmpty() ) { + throw new IllegalArgumentException("missing executable after -S"); + } + arguments = argsList.subList(0, sIndex).toArray(new String[0]); + String execArg = argsList.get(sIndex + 1); + executableArgv = argsList.subList(sIndex + 2, argsList.size()).toArray(new String[0]); + + if (execArg.equals("bundle") && executableArgv.length > 0 && executableArgv[0].equals("exec")) { + warn("`bundle exec' may drop out of the Warbler environment and into the system environment"); + } else if (execArg.equals("rails")) { + // The rails executable doesn't play well with ScriptingContainer, so we've packaged the + // same script that would have been generated by `rake rails:update:bin` + execArg = "./META-INF/rails.rb"; + } + + executable = execArg; + } + } + + private URL extractWebserver() throws Exception { + this.webroot = File.createTempFile("warbler", "webroot"); + this.webroot.delete(); + this.webroot.mkdirs(); + this.webroot = new File(this.webroot, new File(archive).getName()); + debug("webroot directory is " + this.webroot.getPath()); + InputStream jarStream = new URI("jar", entryPath(WEBSERVER_JAR), null).toURL().openStream(); + File jarFile = File.createTempFile("webserver", ".jar"); + jarFile.deleteOnExit(); + FileOutputStream outStream = new FileOutputStream(jarFile); + try { + byte[] buf = new byte[4096]; + int bytesRead = 0; + while ((bytesRead = jarStream.read(buf)) != -1) { + outStream.write(buf, 0, bytesRead); + } + } finally { + jarStream.close(); + outStream.close(); + } + debug("webserver.jar extracted to " + jarFile.getPath()); + return jarFile.toURI().toURL(); + } + + private Properties getWebserverProperties() throws Exception { + Properties props = new Properties(); + try { + InputStream is = getClass().getResourceAsStream(WEBSERVER_PROPERTIES); + if ( is != null ) props.load(is); + } catch (Exception e) { } + + for (Map.Entry entry : props.entrySet()) { + String val = (String) entry.getValue(); + val = val.replace("{{warfile}}", archive).replace("{{webroot}}", webroot.getAbsolutePath()); + entry.setValue(val); + } + + if (props.getProperty("props") != null) { + String[] propsToSet = props.getProperty("props").split(","); + for (String key : propsToSet) { + System.setProperty(key, props.getProperty(key)); + } + } + + return props; + } + + private void launchWebServer(URL jar) throws Exception { + URLClassLoader loader = new URLClassLoader(new URL[] {jar}); + Thread.currentThread().setContextClassLoader(loader); + Properties props = getWebserverProperties(); + String mainClass = props.getProperty("mainclass"); + if (mainClass == null) { + throw new IllegalArgumentException("unknown webserver main class (" + + WEBSERVER_PROPERTIES + + " is missing 'mainclass' property)"); + } + Class klass = Class.forName(mainClass, true, loader); + Method main = klass.getDeclaredMethod("main", new Class[] { String[].class }); + String[] newArgs = launchWebServerArguments(props); + debug("invoking webserver with: " + Arrays.deepToString(newArgs)); + main.invoke(null, new Object[] { newArgs }); + + // the following code is specific to winstone. but a whole winstone module like the jetty module seemed + // excessive. if running under jetty (or anything other than wintstone) this will effectively do nothing. + Set threads = Thread.getAllStackTraces().keySet(); + for (Thread thread : threads) { + String name = thread.getName(); + if (name.startsWith("LauncherControlThread")) { + debug("joining thread: " + name); + thread.join(); + } + } + } + + private String[] launchWebServerArguments(Properties props) { + String[] newArgs = args; + + if (props.getProperty("args") != null) { + String[] insertArgs = props.getProperty("args").split(","); + newArgs = new String[args.length + insertArgs.length]; + for (int i = 0; i < insertArgs.length; i++) { + newArgs[i] = props.getProperty(insertArgs[i], ""); + } + System.arraycopy(args, 0, newArgs, insertArgs.length, args.length); + } + + return newArgs; + } + + // JarMain overrides to make WarMain "launchable" + // e.g. java -jar rails.war -S rake db:migrate + + @Override + protected String getExtractEntryPath(final JarEntry entry) { + final String name = entry.getName(); + final String start = "WEB-INF"; + if ( name.startsWith(start) ) { + // WEB-INF/app/controllers/application_controller.rb -> + // app/controllers/application_controller.rb + return name.substring(start.length()); + } + if ( name.indexOf('/') == -1 ) { + // 404.html -> public/404.html + return "/public/" + name; + } + return "/" + name; + } + + @Override + protected URL extractEntry(final JarEntry entry, final String path) throws Exception { + // always extract but only return class-path entry URLs : + final URL entryURL = super.extractEntry(entry, path); + return path.endsWith(".jar") ? entryURL : null; + } + + @Override + protected int launchJRuby(final URL[] jars) throws Exception { + final Object scriptingContainer = newScriptingContainer(jars); + + invokeMethod(scriptingContainer, "setArgv", (Object) executableArgv); + invokeMethod(scriptingContainer, "setCurrentDirectory", extractRoot.getAbsolutePath()); + initJRubyScriptingEnv(scriptingContainer, jars); + + final Object provider = invokeMethod(scriptingContainer, "getProvider"); + final Object rubyInstanceConfig = invokeMethod(provider, "getRubyInstanceConfig"); + + invokeMethod(rubyInstanceConfig, "setUpdateNativeENVEnabled", new Class[] { Boolean.TYPE }, false); + + final String executablePath = locateExecutable(scriptingContainer); + if ( executablePath == null ) { + throw new IllegalStateException("failed to locate gem executable: '" + executable + "'"); + } + invokeMethod(scriptingContainer, "setScriptFilename", executablePath); + + invokeMethod(rubyInstanceConfig, "processArguments", (Object) arguments); + + Object runtime = invokeMethod(scriptingContainer, "getRuntime"); + Object executableInput = + new SequenceInputStream(new ByteArrayInputStream(executableScriptEnvPrefix().getBytes()), + (InputStream) invokeMethod(rubyInstanceConfig, "getScriptSource")); + + debug("invoking " + executablePath + " with: " + Arrays.toString(executableArgv)); + + Object outcome = invokeMethod(runtime, "runFromMain", + new Class[] { InputStream.class, String.class }, + executableInput, executablePath + ); + return ( outcome instanceof Number ) ? ( (Number) outcome ).intValue() : 0; + } + + protected String locateExecutable(final Object scriptingContainer) throws Exception { + if ( executable == null ) { + throw new IllegalStateException("no executable"); + } + final File exec = new File(extractRoot, executable); + if ( exec.exists() ) { + return exec.getAbsolutePath(); + } + else { + final String script = locateExecutableScript(executable); + return (String) invokeMethod(scriptingContainer, "runScriptlet", script); + } + } + protected String executableScriptEnvPrefix() { + final String gemsDir = new File(extractRoot, "gems").getAbsolutePath(); + final String gemfile = new File(extractRoot, "Gemfile").getAbsolutePath(); + debug("setting GEM_HOME to " + gemsDir); + debug("... and BUNDLE_GEMFILE to " + gemfile); + + // ideally this would look up the config.override_gem_home setting + return "ENV['GEM_HOME'] = ENV['GEM_PATH'] = '"+ gemsDir +"' \n" + + "ENV['BUNDLE_GEMFILE'] ||= '"+ gemfile +"' \n" + + "require 'META-INF/init.rb' \n"; + } + + protected String locateExecutableScript(final String executable) { + return executableScriptEnvPrefix() + + "begin\n" + + // locate the executable within gemspecs : + " require 'rubygems' \n" + + " begin\n" + + // add bundler gems to load path: + " require 'bundler' \n" + + // TODO: environment from web.xml. Any others? + " Bundler.setup(:default, *ENV.values_at('RACK_ENV', 'RAILS_ENV').compact)\n" + + " rescue LoadError\n" + + // bundler not used + " end\n" + + " exec = '"+ executable +"' \n" + + " spec = Gem::Specification.find { |s| s.executables.include?(exec) } \n" + + " spec ? spec.bin_file(exec) : nil \n" + + // returns the full path to the executable + "rescue SystemExit => e\n" + + " e.status\n" + + "end"; + } + + protected void initJRubyScriptingEnv(Object scriptingContainer, final URL[] jars) throws Exception { + String jrubyStdlibJar = ""; + String bcpkixJar = ""; + String bcprovJar = ""; + for (URL url : jars) { + if (url.toString().matches("file:/.*jruby-stdlib-.*jar")) { + jrubyStdlibJar = url.toString(); + debug("using jruby-stdlib: " + jrubyStdlibJar); + } else if (url.toString().matches("file:/.*bcpkix-jdk15on-.*jar")) { + bcpkixJar = url.toString(); + debug("using bcpkix: " + bcpkixJar); + } else if (url.toString().matches("file:/.*bcprov-jdk15on-.*jar")) { + bcprovJar = url.toString(); + debug("using bcprov: " + bcprovJar); + } + } + + invokeMethod(scriptingContainer, "runScriptlet", "" + + "ruby = RUBY_VERSION.match(/^\\d\\.\\d/)[0] \n" + + "jruby_major_version = JRUBY_VERSION.match(/^\\d\\.\\d/)[0].to_f \n" + + "jruby_minor_version = JRUBY_VERSION.split('.')[2].to_i\n" + + "$: << \"" + jrubyStdlibJar + "!/META-INF/jruby.home/lib/ruby/#{ruby}/site_ruby\"\n" + + "$: << \"" + jrubyStdlibJar + "!/META-INF/jruby.home/lib/ruby/shared\"\n" + + "$: << \"" + jrubyStdlibJar + "!/META-INF/jruby.home/lib/ruby/#{ruby}\"\n" + + "if jruby_major_version >= 1.7 and jruby_minor_version < 13\n" + + " require \"" + bcpkixJar + "\".gsub('file:', '') unless \"" + bcpkixJar + "\".empty?\n" + + " require \"" + bcprovJar + "\".gsub('file:', '') unless \"" + bcprovJar + "\".empty?\n" + + "end"); + + invokeMethod(scriptingContainer, "setHomeDirectory", "classpath:/META-INF/jruby.home"); + } + + @Override + protected int start() throws Exception { + if ( executable == null ) { + try { + URL server = extractWebserver(); + launchWebServer(server); + } + catch (FileNotFoundException e) { + if ( e.getMessage().indexOf("WEB-INF/webserver.jar") > -1 ) { + System.out.println("specify the -S argument followed by the bin file to run e.g. `java -jar rails.war -S rake -T` ..."); + System.out.println("(or if you'd like your .war file to start a web server package it using `warbler executable war`)"); + } + throw e; + } + return 0; + } + else { + return super.start(); + } + } + + @Override + public void run() { + super.run(); + if ( webroot != null ) delete(webroot.getParentFile()); + } + + public static void main(String[] args) { + doStart(new WarMain(args)); + } + +}