rack-unreloader 1.4.0 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: d9318a0edd80ea0c0bae830750c33a607ef0146a
4
- data.tar.gz: d85653f1770a69dcde0e4c15dd32db9b43ff8686
2
+ SHA256:
3
+ metadata.gz: f6fefe4d57aa2ea927929699e2a6aba2e8979cea284b0784fee379404b6f1726
4
+ data.tar.gz: 3aa21b88447194616e29d1ea2cf1181f102f5d58e6a29f4a11696a83d826a506
5
5
  SHA512:
6
- metadata.gz: ac585d0650959361e75a940f12b677062247ae88c9bc8811146e1808183abb907465ffc786dfffb98372ae9aaac4f5d578dd140d762c39b10ac9d76c51caf6f7
7
- data.tar.gz: 299ffb392c1d525c8037855f353209669bf06fca106af0b745fda6b84175d3f60d8e2cc3e0de0e6e8ed22a5e22bf36756678d47015a7969f62b0c214a97a88ab
6
+ metadata.gz: 6a5fbca1692b66ab4daf1f9b1677b15fb704d28bb972b26fdc15d78918fa37bed04657177438ff2351ba8c60a10105f7e138d869a4ab37fbd9e4143188c5685d
7
+ data.tar.gz: 12d9b2d24730e1f41da6089e5995c219af4b636755738405d1404e0be3f4044c8f935bd073ba53d92cada170761f79bfea82235c5b58d9ca6bc1dac1ee1b647f
data/CHANGELOG CHANGED
@@ -1,3 +1,25 @@
1
+ = 1.8.0 (2021-10-15)
2
+
3
+ * Avoid warnings in verbose warning mode on Ruby 3+ (jeremyevans)
4
+
5
+ * Check directory timestamps before looking for added or removed files (jeremyevans)
6
+
7
+ * Add support for a :delete_hook option to Unreloader#require, called when a file is deleted (jeremyevans)
8
+
9
+ * Handle cases where Module#name raises an exception (jeremyevans) (#9)
10
+
11
+ = 1.7.0 (2019-03-18)
12
+
13
+ * Add :handle_reload_errors option, for returning backtrace with error if there is an exception when reloading (jeremyevans)
14
+
15
+ = 1.6.0 (2017-02-24)
16
+
17
+ * Add Unreloader#strip_path_prefix, designed to support chrooting (jeremyevans)
18
+
19
+ = 1.5.0 (2016-03-10)
20
+
21
+ * Handle deletions of monitored files (jeremyevans)
22
+
1
23
  = 1.4.0 (2016-01-15)
2
24
 
3
25
  * Avoid use of Thread.exclusive, preventing warnings on ruby 2.3 (celsworth, jeremyevans) (#6)
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014-2015 Jeremy Evans
1
+ Copyright (c) 2014-2021 Jeremy Evans
2
2
  Copyright (c) 2011 Padrino
3
3
 
4
4
  Permission is hereby granted, free of charge, to any person obtaining
data/README.rdoc CHANGED
@@ -1,12 +1,13 @@
1
1
  = Rack::Unreloader
2
2
 
3
- Rack::Unreloader is a rack library that reloads application files when it
4
- detects changes, unloading constants defined in those files before reloading.
5
- Like other rack libraries for reloading, this can make application development
6
- much faster, as you don't need to restart the whole application when you change
7
- a single file. Unlike most other rack libraries for reloading, this unloads
8
- constants before requiring files, avoiding issues when loading a file is not
9
- idempotent.
3
+ Rack::Unreloader is a code reloader for {Rack}[https://github.com/rack/rack].
4
+ It speeds up application development by automatically reloading stale code
5
+ so that you don't have to restart your dev server every time you change a file.
6
+
7
+ Unlike most other code loading libraries for Rack,
8
+ this one ensures that reloads are clean and idempotent
9
+ by _unloading_ relevant constants first, and it does so incrementally, only
10
+ reloading the files that are modified.
10
11
 
11
12
  == Installation
12
13
 
@@ -18,49 +19,61 @@ Source code is available on GitHub at https://github.com/jeremyevans/rack-unrelo
18
19
 
19
20
  == Basic Usage
20
21
 
21
- Assuming a basic web application stored in +app.rb+:
22
-
23
- require 'roda'
22
+ Before:
24
23
 
25
- class App < Roda
26
- route do |r|
27
- "Hello world!"
28
- end
29
- end
24
+ # config.ru
30
25
 
31
- With a basic +config.ru+ like this:
26
+ require './app'
32
27
 
33
- require './app.rb'
34
28
  run App
35
29
 
36
- Change +config.ru+ to:
30
+ After:
31
+
32
+ # config.ru
37
33
 
38
34
  require 'rack/unreloader'
39
35
  Unreloader = Rack::Unreloader.new{App}
40
- require 'roda'
41
36
  Unreloader.require './app.rb'
37
+
42
38
  run Unreloader
43
39
 
44
- The block you pass to Rack::Unreloader.new should return the rack application
45
- to use. If you make any changes to +app.rb+, <tt>Rack::Unreloader</tt> will remove any
46
- constants defined by requiring +app.rb+, and rerequire the file.
40
+ Now, +app.rb+ will be monitored for changes on each incoming HTTP request.
47
41
 
48
- Note that this causes problems if +app.rb+ loads any new libraries that define
49
- constants, as it will unload those constants first. This is why the example
50
- code requires the +roda+ library normally before requiring +app.rb+ using
51
- <tt>Rack::Unreloader</tt>.
42
+ If changes are detected, +Rack::Unreloader+ will
43
+ unload all constants defined inside it and then re-require it
44
+ before proceeding with the request.
52
45
 
53
- However, if +app.rb+ requires more than a single file, it is more
54
- practical to tell <tt>Rack::Unreloader</tt> to only unload specific subclasses:
46
+ == Handling Subclasses
55
47
 
56
- require 'rack/unreloader'
57
- Unreloader = Rack::Unreloader.new(:subclasses=>%w'Roda'){App}
58
- Unreloader.require './app.rb'
59
- run Unreloader
48
+ By default, +Rack::Unreloader+ unloads *all* constants defined in +app.rb+.
49
+ That includes third-party libraries, like +Roda+ or +JSON+ in the example below:
60
50
 
61
- When the +:subclasses+ option is given, only subclasses of the given classes
62
- will be unloaded before reloading the file. It is recommended that
63
- you use a +:subclasses+ option when using <tt>Rack::Unreloader</tt>.
51
+ # app.rb
52
+
53
+ require 'roda'
54
+ require 'json'
55
+
56
+ class App < Roda
57
+ ...
58
+ end
59
+
60
+ Unloading these classes/modules isn't just unnecessary, it's dangerous.
61
+ If your own code depends on them, your app will throw a +NameError+ after
62
+ reloading when it tries to access them.
63
+
64
+ To reload only *subclasses* of +Roda+ (i.e. +App+), use the +:subclasses+
65
+ option:
66
+
67
+ Rack::Unreloader.new(:subclasses=>%w'Roda'){App}
68
+
69
+ == Handling Errors During Reloading
70
+
71
+ By default, +Rack::Unreloader+ instances do not handle exceptions raised
72
+ during reloading, so that it may be rescued elsewhere (e.g. manually or by middleware).
73
+ You can use the +:handle_reload_errors+ option to send the backtrace directly to the
74
+ client as the HTTP response:
75
+
76
+ Rack::Unreloader.new(handle_reload_errors: true){App}
64
77
 
65
78
  == Dependency Handling
66
79
 
@@ -83,7 +96,7 @@ to using:
83
96
 
84
97
  Unreloader.require './models.rb'
85
98
 
86
- The reason that the <tt>Rack::Unreloader</tt> instance is assigned to a constant in
99
+ The reason that the +Rack::Unreloader+ instance is assigned to a constant in
87
100
  +config.ru+ is to make it easy to add reloadable dependencies in this way.
88
101
 
89
102
  It's even a better idea to require this dependency manually in +config.ru+,
@@ -106,7 +119,7 @@ to false if not in development:
106
119
 
107
120
  dev = ENV['RACK_ENV'] == 'development'
108
121
  require 'rack/unreloader'
109
- Unreloader = Rack::Unreloader.new(:reload=>dev){App}
122
+ Unreloader = Rack::Unreloader.new(:subclasses=>%w'Roda Sequel::Model', :reload=>dev){App}
110
123
  Unreloader.require './models.rb'
111
124
  Unreloader.require './app.rb'
112
125
  run(dev ? Unreloader : App)
@@ -207,9 +220,20 @@ The advantage for doing this is that new files added to the directory will be
207
220
  picked up automatically, and files deleted from the directory will be removed
208
221
  automatically. This applies to files in subdirectories of that directory as well.
209
222
 
223
+ The +require+ method also supports a +:delete_hook+ option. This option sets
224
+ a hook that is called when the related file is deleted. This is useful if adding
225
+ a new file or reloading an existing file will handle things correctly, but
226
+ removing the file will not. One common case for this is when you have a shared data
227
+ structure that is updated by the files, where adding or reloading the file will
228
+ update the data structure, but deleting will not, and will leave stale entries in
229
+ the data structure. You can use the +:delete_hook+ option to remove the entries
230
+ related to the file in the data structure:
231
+
232
+ Unreloader.require 'models', :delete_hook=>proc{|f| SHARED_HASH.delete(f)}
233
+
210
234
  == Speeding Things Up
211
235
 
212
- By default, <tt>Rack::Unreloader</tt> uses +ObjectSpace+ before and after requiring each
236
+ By default, +Rack::Unreloader+ uses +ObjectSpace+ before and after requiring each
213
237
  file that it monitors, to see which classes and modules were defined by the
214
238
  require. This is slow for large numbers of files. In general use it isn't an
215
239
  issue as generally only a single file will be changed at a time, but it can
@@ -218,7 +242,7 @@ time.
218
242
 
219
243
  If you want to speed things up, you can provide a block to Rack::Unreloader#require,
220
244
  which will take the file name, and should return the name of the constants or array
221
- of constants to unload. If you do this, <tt>Rack::Unreloader</tt> will no longer need
245
+ of constants to unload. If you do this, +Rack::Unreloader+ will no longer need
222
246
  to use +ObjectSpace+, which substantially speeds up startup. For example, if all of
223
247
  your models just use a capitalized version of the filename:
224
248
 
@@ -229,9 +253,21 @@ decide that instead of specifying the constants, ObjectSpace should be used to
229
253
  automatically determine the constants loaded. You can specify this by having the
230
254
  block return the :ObjectSpace symbol.
231
255
 
256
+ == chroot Support
257
+
258
+ +Rack::Unreloader#strip_path_prefix+ exists for supporting reloading in
259
+ chroot environments, where you chroot an application after it has been fully
260
+ loaded, but still want to pick up changes to files inside the chroot. Example:
261
+
262
+ Unreloader.strip_path_prefix(Dir.pwd)
263
+ Dir.chroot(Dir.pwd)
264
+
265
+ Note that Unreloader.strip_path_prefix also strips the path prefix from
266
+ $LOADED_FEATURES, as that is necessary for correct operation.
267
+
232
268
  == Usage Outside Rack
233
269
 
234
- While <tt>Rack::Unreloader</tt> is usually in the development of rack applications,
270
+ While +Rack::Unreloader+ is usually in the development of rack applications,
235
271
  it doesn't depend on rack. You can just instantiate an instance of Unreloader and
236
272
  use it to handle reloading in any ruby application, just by using the +require+ and
237
273
  +record_dependency+ to set up the metadata, and calling +reload!+ manually to
@@ -262,8 +298,9 @@ for speed when using this library.
262
298
 
263
299
  == Implementation Support
264
300
 
265
- Rack::Unreloader works correctly on Ruby 1.8.7+ and Rubinius. It only works on
266
- JRuby if you use a proc to specify the constants to unload.
301
+ Rack::Unreloader works correctly on Ruby 1.8.7+, JRuby 9.1+, and Rubinius. It
302
+ also works on older versions of JRuby if you use a proc to specify the constants
303
+ to unload.
267
304
 
268
305
  == License
269
306
 
data/Rakefile CHANGED
@@ -12,7 +12,7 @@ end
12
12
 
13
13
  desc "Run specs"
14
14
  task :spec do
15
- sh "#{FileUtils::RUBY} -rubygems -I lib spec/unreloader_spec.rb"
15
+ sh "#{FileUtils::RUBY} #{'-w ' if RUBY_VERSION >= '3'}spec/unreloader_spec.rb"
16
16
  end
17
17
 
18
18
  task :default => :spec
@@ -3,7 +3,7 @@ require 'set'
3
3
  module Rack
4
4
  class Unreloader
5
5
  class Reloader
6
- F = ::File
6
+ File = ::File
7
7
 
8
8
  # Regexp for valid constant names, to prevent code execution.
9
9
  VALID_CONSTANT_NAME_REGEXP = /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/.freeze
@@ -18,13 +18,15 @@ module Rack
18
18
  @classes = opts[:subclasses] ? Array(opts[:subclasses]).map(&:to_s) : %w'Object'
19
19
 
20
20
  # Hash of files being monitored for changes, keyed by absolute path of file name,
21
- # with values being the last modified time (or nil if the file has not yet been loaded).
21
+ # with values being an array containing the last modified time (or nil if the file has
22
+ # not yet been loaded) and the delete hook.
22
23
  @monitor_files = {}
23
24
 
24
25
  # Hash of directories being monitored for changes, keyed by absolute path of directory name,
25
- # with values being the an array with the last modified time (or nil if the directory has not
26
- # yet been loaded), an array of files in the directory, and a block to pass to
27
- # require_dependency for new files.
26
+ # with values being the an array with a hash of modified times for the directory and
27
+ # subdirectories (or nil if the directory has not yet been checked), an array of files in
28
+ # the directory, a block to pass to require_dependency for new files, and the delete_hook
29
+ # for the files in the directory.
28
30
  @monitor_dirs = {}
29
31
 
30
32
  # Hash of procs returning constants defined in files, keyed by absolute path
@@ -53,6 +55,47 @@ module Rack
53
55
  @skip_reload = []
54
56
  end
55
57
 
58
+ # Strip the given path prefix from the internal data structures.
59
+ def strip_path_prefix(path_prefix)
60
+ empty = ''.freeze
61
+
62
+ # Strip the path prefix from $LOADED_FEATURES, otherwise the reloading won't work.
63
+ # Hopefully a future version of ruby will do this automatically when chrooting.
64
+ $LOADED_FEATURES.map!{|s| s.sub(path_prefix, empty)}
65
+
66
+ fix_path = lambda do |s|
67
+ s.sub(path_prefix, empty)
68
+ end
69
+
70
+ [@dependency_order, @skip_reload].each do |a|
71
+ a.map!(&fix_path)
72
+ end
73
+
74
+ [@files, @old_entries].each do |hash|
75
+ hash.each do |k,h|
76
+ h[:features].map!(&fix_path)
77
+ end
78
+ end
79
+
80
+ @monitor_dirs.each_value do |a|
81
+ a[1].map!(&fix_path)
82
+ end
83
+
84
+ @dependencies.each_value do |a|
85
+ a.map!(&fix_path)
86
+ end
87
+
88
+ [@files, @old_entries, @monitor_files, @monitor_dirs, @constants_defined, @dependencies].each do |hash|
89
+ hash.keys.each do |k|
90
+ if k.start_with?(path_prefix)
91
+ hash[fix_path.call(k)] = hash.delete(k)
92
+ end
93
+ end
94
+ end
95
+
96
+ nil
97
+ end
98
+
56
99
  # Unload all reloadable constants and features, and clear the list
57
100
  # of files to monitor.
58
101
  def clear!
@@ -76,7 +119,7 @@ module Rack
76
119
  order.concat(files)
77
120
  order.uniq!
78
121
 
79
- if F.directory?(path)
122
+ if File.directory?(path)
80
123
  (@monitor_files.keys & Unreloader.ruby_files(path)).each do |file|
81
124
  record_dependency(file, files)
82
125
  end
@@ -94,18 +137,29 @@ module Rack
94
137
  check_monitor_dir(dir, changed_files)
95
138
  end
96
139
 
97
- @monitor_files.each do |file, time|
98
- if file_changed?(file, time)
99
- changed_files << file
140
+ removed_files = []
141
+ delete_hooks = []
142
+
143
+ @monitor_files.to_a.each do |file, (time, delete_hook)|
144
+ if File.file?(file)
145
+ if file_changed?(file, time)
146
+ changed_files << file
147
+ end
148
+ else
149
+ delete_hooks << [delete_hook, file] if delete_hook
150
+ removed_files << file
100
151
  end
101
152
  end
102
153
 
154
+ remove_files(removed_files)
155
+ delete_hooks.each{|hook, file| hook.call(file)}
156
+
103
157
  return if changed_files.empty?
104
158
 
105
159
  unless @dependencies.empty?
106
160
  changed_files = reload_files(changed_files)
107
161
  changed_files.flatten!
108
- changed_files.map!{|f| F.directory?(f) ? Unreloader.ruby_files(f) : f}
162
+ changed_files.map!{|f| File.directory?(f) ? Unreloader.ruby_files(f) : f}
109
163
  changed_files.flatten!
110
164
  changed_files.uniq!
111
165
 
@@ -115,7 +169,7 @@ module Rack
115
169
  end
116
170
 
117
171
  unless @skip_reload.empty?
118
- skip_reload = @skip_reload.map{|f| F.directory?(f) ? Unreloader.ruby_files(f) : f}
172
+ skip_reload = @skip_reload.map{|f| File.directory?(f) ? Unreloader.ruby_files(f) : f}
119
173
  skip_reload.flatten!
120
174
  skip_reload.uniq!
121
175
  changed_files -= skip_reload
@@ -128,18 +182,19 @@ module Rack
128
182
 
129
183
  # Require the given dependencies, monitoring them for changes.
130
184
  # Paths should be a file glob or an array of file globs.
131
- def require_dependencies(paths, &block)
185
+ def require_dependencies(paths, opts={}, &block)
132
186
  options = {:cyclic => true}
187
+ delete_hook = opts[:delete_hook]
133
188
  error = nil
134
189
 
135
190
  Unreloader.expand_paths(paths).each do |file|
136
- if F.directory?(file)
137
- @monitor_dirs[file] = [nil, [], block]
191
+ if File.directory?(file)
192
+ @monitor_dirs[file] = [nil, [], block, delete_hook]
138
193
  check_monitor_dir(file)
139
194
  next
140
195
  else
141
196
  @constants_defined[file] = block
142
- @monitor_files[file] = nil
197
+ @monitor_files[file] = [nil, delete_hook]
143
198
  end
144
199
 
145
200
  begin
@@ -185,10 +240,21 @@ module Rack
185
240
  @logger.info(s) if @logger
186
241
  end
187
242
 
243
+ # A hash of modify times for all subdirectories of the given directory.
244
+ def subdir_times(dir)
245
+ h = {}
246
+ Find.find(dir) do |f|
247
+ h[f] = modified_at(f) if File.directory?(f)
248
+ end
249
+ h
250
+ end
251
+
188
252
  # Check a monitored directory for changes, adding new files and removing
189
253
  # deleted files.
190
254
  def check_monitor_dir(dir, changed_files=nil)
191
- time, files, block = @monitor_dirs[dir]
255
+ subdir_times, files, block, delete_hook = md = @monitor_dirs[dir]
256
+ return if subdir_times && subdir_times.all?{|subdir, time| File.directory?(subdir) && modified_at(subdir) == time}
257
+ md[0] = subdir_times(dir)
192
258
 
193
259
  cur_files = Unreloader.ruby_files(dir)
194
260
  return if files == cur_files
@@ -200,14 +266,12 @@ module Rack
200
266
  changed_files.concat(dependency_files(removed_files))
201
267
  end
202
268
 
203
- removed_files.each do |f|
204
- remove(f)
205
- @monitor_files.delete(f)
206
- @dependencies.delete(f)
207
- @dependency_order.delete(f)
269
+ remove_files(removed_files)
270
+ if delete_hook
271
+ removed_files.each{|f| delete_hook.call(f)}
208
272
  end
209
273
 
210
- require_dependencies(new_files, &block)
274
+ require_dependencies(new_files, :delete_hook=>delete_hook, &block)
211
275
 
212
276
  new_files.each do |file|
213
277
  if deps = @dependencies[dir]
@@ -222,6 +286,17 @@ module Rack
222
286
  files.replace(cur_files)
223
287
  end
224
288
 
289
+ # Remove all files in removed_files from the internal data structures,
290
+ # because the file no longer exists.
291
+ def remove_files(removed_files)
292
+ removed_files.each do |f|
293
+ remove(f)
294
+ @monitor_files.delete(f)
295
+ @dependencies.delete(f)
296
+ @dependency_order.delete(f)
297
+ end
298
+ end
299
+
225
300
  # Requires the given file, logging which constants or features are added
226
301
  # by the require, and rolling back the constants and features if there
227
302
  # are any errors.
@@ -289,7 +364,7 @@ module Rack
289
364
  # Store the currently loaded classes and features, so in case of an error
290
365
  # this state can be rolled back to.
291
366
  def prepare(name)
292
- file = remove(name)
367
+ remove(name)
293
368
  @old_entries[name] = {:features => monitored_features}
294
369
  if constants = constants_for(name)
295
370
  defs = constants.select{|c| constant_defined?(c)}
@@ -328,7 +403,7 @@ module Rack
328
403
 
329
404
  @files[name] = entry
330
405
  @old_entries.delete(name)
331
- @monitor_files[name] = modified_at(name)
406
+ @monitor_files[name][0] = modified_at(name)
332
407
 
333
408
  defs, not_defs = entry[:constants].partition{|c| constant_defined?(c)}
334
409
  unless not_defs.empty?
@@ -358,7 +433,7 @@ module Rack
358
433
  rs = Set.new
359
434
 
360
435
  ::ObjectSpace.each_object(Module).each do |mod|
361
- if !mod.name.to_s.empty? && monitored_module?(mod)
436
+ if !(mod.name rescue next).to_s.empty? && monitored_module?(mod)
362
437
  rs << mod
363
438
  end
364
439
  end
@@ -411,7 +486,7 @@ module Rack
411
486
  end
412
487
 
413
488
  # Returns true if the file is new or it's modification time changed.
414
- def file_changed?(file, time = @monitor_files[file])
489
+ def file_changed?(file, time = @monitor_files[file][0])
415
490
  !time || modified_at(file) > time
416
491
  end
417
492
 
@@ -419,7 +494,7 @@ module Rack
419
494
  # to base the reloading on something other than the file's modification
420
495
  # time.
421
496
  def modified_at(file)
422
- F.mtime(file)
497
+ File.mtime(file)
423
498
  end
424
499
  end
425
500
  end
@@ -9,14 +9,14 @@ module Rack
9
9
  MUTEX = Monitor.new
10
10
 
11
11
  # Reference to ::File as File would return Rack::File by default.
12
- F = ::File
12
+ File = ::File
13
13
 
14
14
  # Given the list of paths, find all matching files, or matching ruby files
15
15
  # in subdirecories if given a directory, and return an array of expanded
16
16
  # paths.
17
17
  def self.expand_directory_paths(paths)
18
18
  expand_paths(paths).
19
- map{|f| F.directory?(f) ? ruby_files(f) : f}.
19
+ map{|f| File.directory?(f) ? ruby_files(f) : f}.
20
20
  flatten
21
21
  end
22
22
 
@@ -27,7 +27,7 @@ module Rack
27
27
  flatten.
28
28
  map{|path| Dir.glob(path).sort_by{|filename| filename.count('/')}}.
29
29
  flatten.
30
- map{|path| F.expand_path(path)}.
30
+ map{|path| File.expand_path(path)}.
31
31
  uniq
32
32
  end
33
33
 
@@ -47,6 +47,8 @@ module Rack
47
47
  #
48
48
  # :cooldown :: The number of seconds to wait between checks for changed files.
49
49
  # Defaults to 1. Set to nil/false to not check for changed files.
50
+ # :handle_reload_errors :: Whether reload to handle reload errors by returning
51
+ # a 500 plain text response with the backtrace.
50
52
  # :reload :: Set to false to not setup a reloader, and just have require work
51
53
  # directly. Should be set to false in production mode.
52
54
  # :logger :: A Logger instance which will log information related to reloading.
@@ -58,12 +60,13 @@ module Rack
58
60
  @app_block = block
59
61
  if opts.fetch(:reload, true)
60
62
  @cooldown = opts.fetch(:cooldown, 1)
63
+ @handle_reload_errors = opts[:handle_reload_errors]
61
64
  @last = Time.at(0)
62
65
  Kernel.require 'rack/unreloader/reloader'
63
66
  @reloader = Reloader.new(opts)
64
67
  reload!
65
68
  else
66
- @reloader = @cooldown = false
69
+ @reloader = @cooldown = @handle_reload_errors = false
67
70
  end
68
71
  end
69
72
 
@@ -71,16 +74,25 @@ module Rack
71
74
  # Call the app with the environment.
72
75
  def call(env)
73
76
  if @cooldown && Time.now > @last + @cooldown
74
- MUTEX.synchronize{reload!}
77
+ begin
78
+ MUTEX.synchronize{reload!}
79
+ rescue StandardError, ScriptError => e
80
+ raise unless @handle_reload_errors
81
+ content = "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
82
+ return [500, {'Content-Type' => 'text/plain', 'Content-Length' => content.bytesize.to_s}, [content]]
83
+ end
75
84
  @last = Time.now
76
85
  end
77
86
  @app_block.call.call(env)
78
87
  end
79
88
 
80
89
  # Add a file glob or array of file globs to monitor for changes.
81
- def require(paths, &block)
90
+ # Options:
91
+ # :delete_hook :: When a file being monitored is deleted, call
92
+ # this hook with the path of the deleted file.
93
+ def require(paths, opts={}, &block)
82
94
  if @reloader
83
- @reloader.require_dependencies(paths, &block)
95
+ @reloader.require_dependencies(paths, opts, &block)
84
96
  else
85
97
  Unreloader.expand_directory_paths(paths).each{|f| super(f)}
86
98
  end
@@ -116,5 +128,17 @@ module Rack
116
128
  def reload!
117
129
  @reloader.reload! if @reloader
118
130
  end
131
+
132
+ # Strip the given path prefix from all absolute paths used by the
133
+ # reloader. This is designed when chrooting an application.
134
+ #
135
+ # Options:
136
+ # :strip_core :: Also strips the path prefix from $LOADED_FEATURES and
137
+ # $LOAD_PATH.
138
+ def strip_path_prefix(path_prefix, opts={})
139
+ if @reloader
140
+ @reloader.strip_path_prefix(path_prefix)
141
+ end
142
+ end
119
143
  end
120
144
  end
@@ -0,0 +1,87 @@
1
+ require File.join(File.dirname(File.expand_path(__FILE__)), '../lib/rack/unreloader')
2
+ require 'rubygems'
3
+ $: << 'lib'
4
+ ENV['MT_NO_PLUGINS'] = '1' # Work around stupid autoloading of plugins
5
+ gem 'minitest'
6
+ require 'minitest/global_expectations/autorun'
7
+ require 'minitest/hooks'
8
+
9
+ module ModifiedAt
10
+ def set_modified_time(file, time)
11
+ time = Time.now + time if time.is_a?(Integer)
12
+ modified_times[File.expand_path(file)] = time
13
+ end
14
+
15
+ def modified_times
16
+ @modified_times ||= {}
17
+ end
18
+
19
+ private
20
+
21
+ def modified_at(file)
22
+ modified_times[file] || super
23
+ end
24
+ end
25
+
26
+ class Minitest::Spec
27
+ def code(i)
28
+ "class App; class << self; def call(env) @a end; alias call call; end; @a ||= []; @a << #{i}; end"
29
+ end
30
+
31
+ def update_app(code, file=@filename)
32
+ if ru.reloader
33
+ ru.reloader.set_modified_time(File.dirname(file), @i += 1) unless File.file?(file)
34
+ ru.reloader.set_modified_time(file, @i += 1)
35
+ end
36
+ File.open(file, 'wb'){|f| f.write(code)}
37
+ end
38
+
39
+ def file_delete(file)
40
+ if ru.reloader
41
+ ru.reloader.set_modified_time(File.dirname(file), @i += 1)
42
+ end
43
+ File.delete(file)
44
+ end
45
+
46
+ def logger
47
+ return @logger if @logger
48
+ @logger = []
49
+ def @logger.method_missing(meth, log)
50
+ self << log
51
+ end
52
+ @logger
53
+ end
54
+
55
+ def base_ru(opts={})
56
+ block = opts[:block] || proc{App}
57
+ @ru = Rack::Unreloader.new({:logger=>logger, :cooldown=>0}.merge(opts), &block)
58
+ @ru.reloader.extend ModifiedAt if @ru.reloader
59
+ Object.const_set(:RU, @ru)
60
+ end
61
+
62
+ def ru(opts={})
63
+ return @ru if @ru
64
+ base_ru(opts)
65
+ update_app(opts[:code]||code(1))
66
+ @ru.require @filename
67
+ @ru
68
+ end
69
+
70
+ def log_match(*logs)
71
+ @logger.length.must_equal logs.length
72
+ logs.zip(@logger).each{|l, log| l.is_a?(String) ? log.must_equal(l) : log.must_match(l)}
73
+ end
74
+
75
+ before do
76
+ @i = 0
77
+ @filename = 'spec/app.rb'
78
+ end
79
+
80
+ after do
81
+ ru.reloader.clear! if ru.reloader
82
+ Object.send(:remove_const, :RU)
83
+ Object.send(:remove_const, :App) if defined?(::App)
84
+ Object.send(:remove_const, :App2) if defined?(::App2)
85
+ Dir['spec/app*.rb'].each{|f| File.delete(f)}
86
+ end
87
+ end