Create a runnable war file using the WarMain from warbler

References #1
This commit is contained in:
R. Tyler Croy 2014-07-28 13:29:44 -07:00
parent e71f3db21f
commit f15e081468
6 changed files with 683 additions and 2 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@
.gemcache/
vendor/
.jarcache
build/

View File

@ -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

19
init.rb Normal file
View File

@ -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

2
run.sh
View File

@ -4,4 +4,4 @@
export GEM_HOME=''
export GEM_PATH=''
exec ./gradlew prepare pkg
exec ./gradlew prepare war

View File

@ -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<String, JarEntry> jarNames = new HashMap<String, JarEntry>();
for (Enumeration<JarEntry> 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<URL> urls = new ArrayList<URL>();
for (Map.Entry<String, JarEntry> 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
}
}

View File

@ -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 <tt>java -jar</tt>.
*
* WarMain can be used with different web server libraries. WarMain expects
* to have two files present in the .war file,
* <tt>WEB-INF/webserver.properties</tt> and <tt>WEB-INF/webserver.jar</tt>.
*
* 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
* <tt>main</tt> method of the main class mentioned in the properties.
*
* An example webserver.properties follows for Winstone. The <tt>args</tt>
* property indicates the names and ordering of other properties to be used
* as command-line arguments. The special tokens <tt>{{warfile}}</tt> and
* <tt>{{webroot}}</tt> are substituted with the location of the .war file
* being run and the temporary work directory, respectively.
* <pre>
* mainclass = winstone.Launcher
* args = args0,args1,args2
* args0 = --warfile={{warfile}}
* args1 = --webroot={{webroot}}
* args2 = --directoryListings=false
* </pre>
*
* System properties can also be set via webserver.properties. For example,
* the following entries set <tt>jetty.home</tt> before launching the server.
* <pre>
* props = jetty.home
* jetty.home = {{webroot}}
* </pre>
*/
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<String> 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<Thread> 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));
}
}