warbler 1.3.6 → 1.3.7

Sign up to get free protection for your applications and to get access to all the features.
data/ext/WarMain.java CHANGED
@@ -5,16 +5,22 @@
5
5
  * See the file LICENSE.txt for details.
6
6
  */
7
7
 
8
- import java.net.URLClassLoader;
9
- import java.net.URL;
10
8
  import java.lang.reflect.Method;
11
- import java.io.IOException;
12
9
  import java.io.InputStream;
10
+ import java.io.ByteArrayInputStream;
11
+ import java.io.SequenceInputStream;
13
12
  import java.io.File;
13
+ import java.io.FileNotFoundException;
14
14
  import java.io.FileOutputStream;
15
+ import java.net.URI;
16
+ import java.net.URLClassLoader;
17
+ import java.net.URL;
15
18
  import java.util.Arrays;
19
+ import java.util.List;
16
20
  import java.util.Properties;
17
21
  import java.util.Map;
22
+ import java.util.jar.JarEntry;
23
+ import java.util.Set;
18
24
 
19
25
  /**
20
26
  * Used as a Main-Class in the manifest for a .war file, so that you can run
@@ -52,32 +58,48 @@ import java.util.Map;
52
58
  * jetty.home = {{webroot}}
53
59
  * </pre>
54
60
  */
55
- public class WarMain implements Runnable {
56
- public static final String MAIN = "/" + WarMain.class.getName().replace('.', '/') + ".class";
57
- public static final String WEBSERVER_PROPERTIES = "/WEB-INF/webserver.properties";
58
- public static final String WEBSERVER_JAR = "/WEB-INF/webserver.jar";
59
-
60
- private String[] args;
61
- private String path, warfile;
62
- private boolean debug;
61
+ public class WarMain extends JarMain {
62
+
63
+ static final String MAIN = "/" + WarMain.class.getName().replace('.', '/') + ".class";
64
+ static final String WEBSERVER_PROPERTIES = "/WEB-INF/webserver.properties";
65
+ static final String WEBSERVER_JAR = "/WEB-INF/webserver.jar";
66
+
67
+ // jruby arguments, consider the following command :
68
+ // `java -jar rails.was --1.9 -S rake db:migrate`
69
+ // arguments == [ "--1.9" ]
70
+ // executable == "rake"
71
+ // executableArgv == [ "db:migrate" ]
72
+ private final String[] arguments;
73
+ // null to launch webserver or != null to run a executable e.g. rake
74
+ private final String executable;
75
+ private final String[] executableArgv;
76
+
63
77
  private File webroot;
64
78
 
65
- public WarMain(String[] args) throws Exception {
66
- this.args = args;
67
- URL mainClass = getClass().getResource(MAIN);
68
- this.path = mainClass.toURI().getSchemeSpecificPart();
69
- this.warfile = this.path.replace("!" + MAIN, "").replace("file:", "");
70
- this.debug = isDebug();
79
+ WarMain(final String[] args) {
80
+ super(args);
81
+ final List<String> argsList = Arrays.asList(args);
82
+ final int sIndex = argsList.indexOf("-S");
83
+ if ( sIndex == -1 ) {
84
+ executable = null; executableArgv = null; arguments = null;
85
+ }
86
+ else {
87
+ if ( args.length == sIndex + 1 || args[sIndex + 1].isEmpty() ) {
88
+ throw new IllegalArgumentException("missing executable after -S");
89
+ }
90
+ arguments = argsList.subList(0, sIndex).toArray(new String[0]);
91
+ executable = argsList.get(sIndex + 1);
92
+ executableArgv = argsList.subList(sIndex + 2, argsList.size()).toArray(new String[0]);
93
+ }
94
+ }
95
+
96
+ private URL extractWebserver() throws Exception {
71
97
  this.webroot = File.createTempFile("warbler", "webroot");
72
98
  this.webroot.delete();
73
99
  this.webroot.mkdirs();
74
- this.webroot = new File(this.webroot, new File(warfile).getName());
100
+ this.webroot = new File(this.webroot, new File(archive).getName());
75
101
  debug("webroot directory is " + this.webroot.getPath());
76
- Runtime.getRuntime().addShutdownHook(new Thread(this));
77
- }
78
-
79
- private URL extractWebserver() throws Exception {
80
- InputStream jarStream = new URL("jar:" + path.replace(MAIN, WEBSERVER_JAR)).openStream();
102
+ InputStream jarStream = new URI("jar", entryPath(WEBSERVER_JAR), null).toURL().openStream();
81
103
  File jarFile = File.createTempFile("webserver", ".jar");
82
104
  jarFile.deleteOnExit();
83
105
  FileOutputStream outStream = new FileOutputStream(jarFile);
@@ -99,13 +121,12 @@ public class WarMain implements Runnable {
99
121
  Properties props = new Properties();
100
122
  try {
101
123
  InputStream is = getClass().getResourceAsStream(WEBSERVER_PROPERTIES);
102
- props.load(is);
103
- } catch (Exception e) {
104
- }
124
+ if ( is != null ) props.load(is);
125
+ } catch (Exception e) { }
105
126
 
106
127
  for (Map.Entry entry : props.entrySet()) {
107
128
  String val = (String) entry.getValue();
108
- val = val.replace("{{warfile}}", warfile).replace("{{webroot}}", webroot.getAbsolutePath());
129
+ val = val.replace("{{warfile}}", archive).replace("{{webroot}}", webroot.getAbsolutePath());
109
130
  entry.setValue(val);
110
131
  }
111
132
 
@@ -119,7 +140,7 @@ public class WarMain implements Runnable {
119
140
  return props;
120
141
  }
121
142
 
122
- private void launchWebserver(URL jar) throws Exception {
143
+ private void launchWebServer(URL jar) throws Exception {
123
144
  URLClassLoader loader = new URLClassLoader(new URL[] {jar});
124
145
  Thread.currentThread().setContextClassLoader(loader);
125
146
  Properties props = getWebserverProperties();
@@ -130,66 +151,174 @@ public class WarMain implements Runnable {
130
151
  + " is missing 'mainclass' property)");
131
152
  }
132
153
  Class klass = Class.forName(mainClass, true, loader);
133
- Method main = klass.getDeclaredMethod("main", new Class[] {String[].class});
134
- String[] newargs = launchArguments(props);
135
- debug("invoking webserver with: " + Arrays.deepToString(newargs));
136
- main.invoke(null, new Object[] {newargs});
154
+ Method main = klass.getDeclaredMethod("main", new Class[] { String[].class });
155
+ String[] newArgs = launchWebServerArguments(props);
156
+ debug("invoking webserver with: " + Arrays.deepToString(newArgs));
157
+ main.invoke(null, new Object[] { newArgs });
158
+
159
+ // the following code is specific to winstone. but a whole winstone module like the jetty module seemed
160
+ // excessive. if running under jetty (or anything other than wintstone) this will effectively do nothing.
161
+ Set<Thread> threads = Thread.getAllStackTraces().keySet();
162
+ for (Thread thread : threads) {
163
+ String name = thread.getName();
164
+ if (name.startsWith("LauncherControlThread")) {
165
+ debug("joining thread: " + name);
166
+ thread.join();
167
+ }
168
+ }
137
169
  }
138
170
 
139
- private String[] launchArguments(Properties props) {
140
- String[] newargs = args;
171
+ private String[] launchWebServerArguments(Properties props) {
172
+ String[] newArgs = args;
141
173
 
142
174
  if (props.getProperty("args") != null) {
143
175
  String[] insertArgs = props.getProperty("args").split(",");
144
- newargs = new String[args.length + insertArgs.length];
176
+ newArgs = new String[args.length + insertArgs.length];
145
177
  for (int i = 0; i < insertArgs.length; i++) {
146
- newargs[i] = props.getProperty(insertArgs[i], "");
178
+ newArgs[i] = props.getProperty(insertArgs[i], "");
147
179
  }
148
- System.arraycopy(args, 0, newargs, insertArgs.length, args.length);
180
+ System.arraycopy(args, 0, newArgs, insertArgs.length, args.length);
149
181
  }
150
182
 
151
- return newargs;
183
+ return newArgs;
152
184
  }
153
185
 
154
- private void start() throws Exception {
155
- URL u = extractWebserver();
156
- launchWebserver(u);
186
+ // JarMain overrides to make WarMain "launchable"
187
+ // e.g. java -jar rails.war -S rake db:migrate
188
+
189
+ @Override
190
+ protected String getExtractEntryPath(final JarEntry entry) {
191
+ final String name = entry.getName();
192
+ final String start = "WEB-INF";
193
+ if ( name.startsWith(start) ) {
194
+ // WEB-INF/app/controllers/application_controller.rb ->
195
+ // app/controllers/application_controller.rb
196
+ return name.substring(start.length());
197
+ }
198
+ if ( name.indexOf('/') == -1 ) {
199
+ // 404.html -> public/404.html
200
+ return "/public/" + name;
201
+ }
202
+ return "/" + name;
157
203
  }
158
-
159
- private void debug(String msg) {
160
- if (debug) {
161
- System.out.println(msg);
204
+
205
+ @Override
206
+ protected URL extractEntry(final JarEntry entry, final String path) throws Exception {
207
+ // always extract but only return class-path entry URLs :
208
+ final URL entryURL = super.extractEntry(entry, path);
209
+ return path.endsWith(".jar") ? entryURL : null;
210
+ }
211
+
212
+ @Override
213
+ protected int launchJRuby(final URL[] jars) throws Exception {
214
+ final Object scriptingContainer = newScriptingContainer(jars);
215
+
216
+ invokeMethod(scriptingContainer, "setArgv", (Object) executableArgv);
217
+ //invokeMethod(scriptingContainer, "setHomeDirectory", "classpath:/META-INF/jruby.home");
218
+ invokeMethod(scriptingContainer, "setCurrentDirectory", extractRoot.getAbsolutePath());
219
+ //invokeMethod(scriptingContainer, "runScriptlet", "ENV.clear");
220
+ //invokeMethod(scriptingContainer, "runScriptlet", "ENV['PATH']=''"); // bundler 1.1.x
221
+
222
+ final Object provider = invokeMethod(scriptingContainer, "getProvider");
223
+ final Object rubyInstanceConfig = invokeMethod(provider, "getRubyInstanceConfig");
224
+
225
+ invokeMethod(rubyInstanceConfig, "setUpdateNativeENVEnabled", new Class[] { Boolean.TYPE }, false);
226
+
227
+ final String executablePath = locateExecutable(scriptingContainer);
228
+ if ( executablePath == null ) {
229
+ throw new IllegalStateException("failed to locate gem executable: '" + executable + "'");
230
+ }
231
+ invokeMethod(scriptingContainer, "setScriptFilename", executablePath);
232
+
233
+ invokeMethod(rubyInstanceConfig, "processArguments", (Object) arguments);
234
+
235
+ Object runtime = invokeMethod(scriptingContainer, "getRuntime");
236
+ Object executableInput =
237
+ new SequenceInputStream(new ByteArrayInputStream(executableScriptEnvPrefix().getBytes()),
238
+ (InputStream) invokeMethod(rubyInstanceConfig, "getScriptSource"));
239
+
240
+ debug("invoking " + executablePath + " with: " + Arrays.toString(executableArgv));
241
+ Object outcome = invokeMethod(runtime, "runFromMain",
242
+ new Class[] { InputStream.class, String.class },
243
+ executableInput, executablePath
244
+ );
245
+ return ( outcome instanceof Number ) ? ( (Number) outcome ).intValue() : 0;
246
+ }
247
+
248
+ protected String locateExecutable(final Object scriptingContainer) throws Exception {
249
+ if ( executable == null ) {
250
+ throw new IllegalStateException("no executable");
251
+ }
252
+ final File exec = new File(extractRoot, executable);
253
+ if ( exec.exists() ) {
254
+ return exec.getAbsolutePath();
162
255
  }
256
+ else {
257
+ final String script = locateExecutableScript(executable);
258
+ return (String) invokeMethod(scriptingContainer, "runScriptlet", script);
259
+ }
260
+ }
261
+ protected String executableScriptEnvPrefix() {
262
+ final String gemsDir = new File(extractRoot, "gems").getAbsolutePath();
263
+ final String gemfile = new File(extractRoot, "Gemfile").getAbsolutePath();
264
+ debug("setting GEM_HOME to " + gemsDir);
265
+ debug("... and BUNDLE_GEMFILE to " + gemfile);
266
+ return "ENV['GEM_HOME'] ||= ENV['GEM_PATH'] = '"+ gemsDir +"' \n" +
267
+ "ENV['BUNDLE_GEMFILE'] ||= '"+ gemfile +"' \n" +
268
+ "require 'META-INF/init.rb' \n";
163
269
  }
164
270
 
165
- private void delete(File f) {
166
- if (f.isDirectory()) {
167
- File[] children = f.listFiles();
168
- for (int i = 0; i < children.length; i++) {
169
- delete(children[i]);
271
+ protected String locateExecutableScript(final String executable) {
272
+ return executableScriptEnvPrefix() +
273
+ "begin\n" +
274
+ // locate the executable within gemspecs :
275
+ " require 'rubygems' \n" +
276
+ " begin\n" +
277
+ // add bundler gems to load path:
278
+ " require 'bundler' \n" +
279
+ // TODO: environment from web.xml. Any others?
280
+ " Bundler.setup(:default, *ENV.values_at('RACK_ENV', 'RAILS_ENV').compact)\n" +
281
+ " rescue LoadError\n" +
282
+ // bundler not used
283
+ " end\n" +
284
+ " exec = '"+ executable +"' \n" +
285
+ " spec = Gem::Specification.find { |s| s.executables.include?(exec) } \n" +
286
+ " spec ? spec.bin_file(exec) : nil \n" +
287
+ // returns the full path to the executable
288
+ "rescue SystemExit => e\n" +
289
+ " e.status\n" +
290
+ "end";
291
+ }
292
+
293
+ @Override
294
+ protected int start() throws Exception {
295
+ if ( executable == null ) {
296
+ try {
297
+ URL server = extractWebserver();
298
+ launchWebServer(server);
299
+ }
300
+ catch (FileNotFoundException e) {
301
+ if ( e.getMessage().indexOf("WEB-INF/webserver.jar") > -1 ) {
302
+ System.out.println("specify the -S argument followed by the bin file to run e.g. `java -jar rails.war -S rake -T` ...");
303
+ System.out.println("(or if you'd like your .war file to start a web server package it using `warbler executable war`)");
304
+ }
305
+ throw e;
170
306
  }
307
+ return 0;
308
+ }
309
+ else {
310
+ return super.start();
171
311
  }
172
- f.delete();
173
312
  }
174
313
 
314
+ @Override
175
315
  public void run() {
176
- delete(webroot.getParentFile());
316
+ super.run();
317
+ if ( webroot != null ) delete(webroot.getParentFile());
177
318
  }
178
319
 
179
320
  public static void main(String[] args) {
180
- try {
181
- new WarMain(args).start();
182
- } catch (Exception e) {
183
- System.err.println("error: " + e.toString());
184
- if (isDebug()) {
185
- e.printStackTrace();
186
- }
187
- System.exit(1);
188
- }
321
+ doStart(new WarMain(args));
189
322
  }
190
323
 
191
- private static boolean isDebug() {
192
- return System.getProperty("warbler.debug") != null;
193
- }
194
324
  }
195
-
data/ext/WarblerJar.java CHANGED
@@ -168,11 +168,21 @@ public class WarblerJar {
168
168
  return entryInJar(stream, entry);
169
169
  }
170
170
 
171
+ private static String trimTrailingSlashes(String path) {
172
+ if (path.endsWith("/")) {
173
+ return path.substring(0, path.length() - 1);
174
+ } else {
175
+ return path;
176
+ }
177
+ }
178
+
171
179
  private static InputStream entryInJar(InputStream jar, String entry) throws IOException {
180
+ entry = trimTrailingSlashes(entry);
181
+
172
182
  ZipInputStream jstream = new ZipInputStream(jar);
173
183
  ZipEntry zentry = null;
174
184
  while ((zentry = jstream.getNextEntry()) != null) {
175
- if (zentry.getName().equals(entry)) {
185
+ if (trimTrailingSlashes(zentry.getName()).equals(entry)) {
176
186
  return jstream;
177
187
  }
178
188
  jstream.closeEntry();
data/lib/warbler.rb CHANGED
@@ -9,7 +9,8 @@
9
9
  # your Ruby applications into .jar or .war files.
10
10
  module Warbler
11
11
  WARBLER_HOME = File.expand_path(File.dirname(__FILE__) + '/..') unless defined?(WARBLER_HOME)
12
-
12
+ WARBLER_JAR = "#{WARBLER_HOME}/lib/warbler_jar.jar" unless defined?(WARBLER_JAR)
13
+
13
14
  class << self
14
15
  # An instance of Warbler::Application used by the +warble+ command.
15
16
  attr_accessor :application
@@ -37,9 +37,12 @@ class Warbler::Application < Rake::Application
37
37
  desc "Feature: package gem repository inside a jar"
38
38
  task :gemjar => "#{wt.name}:gemjar"
39
39
 
40
- desc "Feature: make an executable archive"
40
+ desc "Feature: make an executable archive (runnable + an embedded web server)"
41
41
  task :executable => "#{wt.name}:executable"
42
42
 
43
+ desc "Feature: make a runnable archive (e.g. java -jar rails.war -S rake db:migrate)"
44
+ task :runnable => "#{wt.name}:runnable"
45
+
43
46
  desc "Feature: precompile all Ruby files"
44
47
  task :compiled => "#{wt.name}:compiled"
45
48
 
@@ -14,7 +14,7 @@ module Warbler
14
14
  class Config
15
15
  include RakeHelper
16
16
 
17
- TOP_DIRS = %w(app config lib log vendor)
17
+ TOP_DIRS = %w(app db config lib log script vendor)
18
18
  FILE = "config/warble.rb"
19
19
  BUILD_GEMS = %w(warbler rake rcov)
20
20
 
@@ -96,7 +96,7 @@ module Warbler
96
96
  attr_accessor :bundler
97
97
 
98
98
  # An array of Bundler groups to avoid including in the war file.
99
- # Defaults to ["development", "test"].
99
+ # Defaults to ["development", "test", "assets"].
100
100
  attr_accessor :bundle_without
101
101
 
102
102
  # Path to the pre-bundled gem directory inside the war file. Default is '/WEB-INF/gems'.
@@ -115,6 +115,12 @@ module Warbler
115
115
  # If the filename ends in .erb the file will be expanded the same way web.xml.erb is; see below.
116
116
  attr_accessor :init_contents
117
117
 
118
+ # Override GEM_HOME environment variable at runtime. When false, gems in
119
+ # GEM_HOME will be loaded in preference to those packaged within the jar
120
+ # file. When true, only gems packaged in the jar file will be loaded.
121
+ # Defaults to false
122
+ attr_accessor :override_gem_home
123
+
118
124
  # Extra configuration for web.xml. Controls how the dynamically-generated web.xml
119
125
  # file is generated.
120
126
  #
@@ -158,7 +164,7 @@ module Warbler
158
164
  @warbler_templates = "#{WARBLER_HOME}/lib/warbler/templates"
159
165
  @features = Set.new
160
166
  @dirs = TOP_DIRS.select {|d| File.directory?(d)}
161
- @includes = FileList[]
167
+ @includes = FileList['*file'] # [r/R]akefile gets included
162
168
  @excludes = FileList[]
163
169
  @java_libs = FileList[]
164
170
  @java_classes = FileList[]
@@ -172,6 +178,7 @@ module Warbler
172
178
  @webinf_files = FileList[]
173
179
  @init_filename = 'META-INF/init.rb'
174
180
  @init_contents = ["#{@warbler_templates}/config.erb"]
181
+ @override_gem_home = false
175
182
 
176
183
  before_configure
177
184
  yield self if block_given?
@@ -180,6 +187,7 @@ module Warbler
180
187
  @compiled_ruby_files ||= FileList[*@dirs.map {|d| "#{d}/**/*.rb"}]
181
188
 
182
189
  @excludes += ["tmp/war", "tmp/war/**/*"] if File.directory?("tmp/war")
190
+ @excludes += ["tmp/cache/**/*"] if File.directory?("tmp/cache")
183
191
  @excludes += warbler_vendor_excludes(warbler_home)
184
192
  @excludes += FileList["**/*.log"] if @exclude_logs
185
193
  end
@@ -199,6 +207,9 @@ module Warbler
199
207
  task "executable" do
200
208
  self.features << "executable"
201
209
  end
210
+ task "runnable" do
211
+ self.features << "runnable"
212
+ end
202
213
  end
203
214
 
204
215
  # Deprecated
data/lib/warbler/jar.rb CHANGED
@@ -44,8 +44,13 @@ module Warbler
44
44
  end
45
45
 
46
46
  def run_javac(config, compiled_ruby_files)
47
+ if config.webxml && config.webxml.context_params.has_key?('jruby.compat.version')
48
+ compat_version = "--#{config.webxml.jruby.compat.version}"
49
+ else
50
+ compat_version = ''
51
+ end
47
52
  # Need to use the version of JRuby in the application to compile it
48
- %x{java -classpath #{config.java_libs.join(File::PATH_SEPARATOR)} org.jruby.Main -S jrubyc \"#{compiled_ruby_files.join('" "')}\"}
53
+ %x{java -classpath #{config.java_libs.join(File::PATH_SEPARATOR)} org.jruby.Main #{compat_version} -S jrubyc \"#{compiled_ruby_files.join('" "')}\"}
49
54
  end
50
55
 
51
56
  def replace_compiled_ruby_files(config, compiled_ruby_files)
@@ -54,7 +59,7 @@ module Warbler
54
59
  config.excludes += compiled_ruby_files
55
60
 
56
61
  compiled_ruby_files.each do |ruby_source|
57
- files[apply_pathmaps(config, ruby_source, :application)] = StringIO.new("require __FILE__.sub(/\.rb$/, '.class')")
62
+ files[apply_pathmaps(config, ruby_source, :application)] = StringIO.new("load __FILE__.sub(/\.rb$/, '.class')")
58
63
  end
59
64
  end
60
65
 
@@ -81,6 +86,9 @@ module Warbler
81
86
  rm_f path
82
87
  ensure_directory_entries
83
88
  puts "Creating #{path}"
89
+ if Warbler::Config === config_or_path
90
+ @files.delete("#{config_or_path.jar_name}/#{path}")
91
+ end
84
92
  create_jar path, @files
85
93
  end
86
94