warbler 1.3.6 → 1.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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