rack-unreloader 1.4.0 → 1.8.0
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.
- checksums.yaml +5 -5
- data/CHANGELOG +22 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +79 -42
- data/Rakefile +1 -1
- data/lib/rack/unreloader/reloader.rb +102 -27
- data/lib/rack/unreloader.rb +31 -7
- data/spec/spec_helper.rb +87 -0
- data/spec/strip_paths_spec.rb +858 -0
- data/spec/unreloader_spec.rb +85 -79
- metadata +25 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f6fefe4d57aa2ea927929699e2a6aba2e8979cea284b0784fee379404b6f1726
|
4
|
+
data.tar.gz: 3aa21b88447194616e29d1ea2cf1181f102f5d58e6a29f4a11696a83d826a506
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/README.rdoc
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
= Rack::Unreloader
|
2
2
|
|
3
|
-
Rack::Unreloader is a
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
22
|
-
|
23
|
-
require 'roda'
|
22
|
+
Before:
|
24
23
|
|
25
|
-
|
26
|
-
route do |r|
|
27
|
-
"Hello world!"
|
28
|
-
end
|
29
|
-
end
|
24
|
+
# config.ru
|
30
25
|
|
31
|
-
|
26
|
+
require './app'
|
32
27
|
|
33
|
-
require './app.rb'
|
34
28
|
run App
|
35
29
|
|
36
|
-
|
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
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
54
|
-
practical to tell <tt>Rack::Unreloader</tt> to only unload specific subclasses:
|
46
|
+
== Handling Subclasses
|
55
47
|
|
56
|
-
|
57
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
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
|
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,
|
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,
|
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
|
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
|
266
|
-
JRuby if you use a proc to specify the constants
|
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
@@ -3,7 +3,7 @@ require 'set'
|
|
3
3
|
module Rack
|
4
4
|
class Unreloader
|
5
5
|
class Reloader
|
6
|
-
|
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
|
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
|
26
|
-
# yet been
|
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
|
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
|
-
|
98
|
-
|
99
|
-
|
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|
|
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|
|
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
|
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
|
-
|
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
|
204
|
-
|
205
|
-
|
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
|
-
|
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
|
-
|
497
|
+
File.mtime(file)
|
423
498
|
end
|
424
499
|
end
|
425
500
|
end
|
data/lib/rack/unreloader.rb
CHANGED
@@ -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
|
-
|
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|
|
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|
|
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
|
-
|
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
|
-
|
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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|