rack-unreloader 0.9.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 +7 -0
- data/CHANGELOG +3 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +177 -0
- data/Rakefile +68 -0
- data/lib/rack/unreloader.rb +264 -0
- data/spec/unreloader_spec.rb +145 -0
- metadata +61 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7665e2203b2b2f7ac74d05e6c0b5b2c60d7d89d8
|
4
|
+
data.tar.gz: 19cd1b3fcf725bdaf58db40ba67426c38b3823d1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7810e9036260c1a517f544783e755997deeb759fcb56e05fdc2ee16109de4c74c907f94a019fe42c503610255797d4f04f76060ff8dde732037eb15df52b90a2
|
7
|
+
data.tar.gz: 17a0ea4463d61cfb959e72ce724c28fecba47dc7ac098ab0b01559c17f8e90026436b982da342335610d281f4fd652b3f13bfd5b1d1b10555d8ec590410036df
|
data/CHANGELOG
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2014 Jeremy Evans
|
2
|
+
Copyright (c) 2011 Padrino
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
a copy of this software and associated documentation files (the
|
6
|
+
"Software"), to deal in the Software without restriction, including
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
= Rack::Unreloader
|
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.
|
10
|
+
|
11
|
+
== Installation
|
12
|
+
|
13
|
+
gem install rack-unreloader
|
14
|
+
|
15
|
+
== Source Code
|
16
|
+
|
17
|
+
Source code is available on GitHub at https://github.com/jeremyevans/rack-unreloader
|
18
|
+
|
19
|
+
== Basic Usage
|
20
|
+
|
21
|
+
Assuming a basic web application stored in +app.rb+:
|
22
|
+
|
23
|
+
require 'roda'
|
24
|
+
|
25
|
+
class App < Roda
|
26
|
+
route do |r|
|
27
|
+
"Hello world!"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
With a basic +config.ru+ like this:
|
32
|
+
|
33
|
+
require './app.rb'
|
34
|
+
run App
|
35
|
+
|
36
|
+
Change +config.ru+ to:
|
37
|
+
|
38
|
+
require 'rack-unreloader'
|
39
|
+
Unreloader = Rack::Unreloader.new{App}
|
40
|
+
require 'roda'
|
41
|
+
Unreloader.require './app.rb'
|
42
|
+
run Unreloader
|
43
|
+
|
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.
|
47
|
+
|
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>.
|
52
|
+
|
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:
|
55
|
+
|
56
|
+
require 'rack-unreloader'
|
57
|
+
Unreloader = Rack::Unreloader.new(:subclasses=>%w'Roda'){App}
|
58
|
+
Unreloader.require './app.rb'
|
59
|
+
run Unreloader
|
60
|
+
|
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>.
|
64
|
+
|
65
|
+
== Dependency Handling
|
66
|
+
|
67
|
+
If your +app.rb+ requires a +models.rb+ file that you also want to get reloaded:
|
68
|
+
|
69
|
+
require 'roda'
|
70
|
+
require './models.rb'
|
71
|
+
|
72
|
+
class App < Roda
|
73
|
+
route do |r|
|
74
|
+
"Hello world!"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
You can change +app.rb+ from using:
|
79
|
+
|
80
|
+
require './models.rb'
|
81
|
+
|
82
|
+
to using:
|
83
|
+
|
84
|
+
Unreloader.require './models.rb'
|
85
|
+
|
86
|
+
The reason that the <tt>Rack::Unreloader</tt> instance is assigned to a constant in
|
87
|
+
+config.ru+ is to make it easy to add reloadable dependencies in this way.
|
88
|
+
|
89
|
+
It's even a better idea to require this dependency manually in +config.ru+,
|
90
|
+
before requiring +app.rb+:
|
91
|
+
|
92
|
+
require 'rack-unreloader'
|
93
|
+
Unreloader = Rack::Unreloader.new(:subclasses=>%w'Roda Sequel::Model'){App}
|
94
|
+
Unreloader.require './models.rb'
|
95
|
+
Unreloader.require './app.rb'
|
96
|
+
run Unreloader
|
97
|
+
|
98
|
+
This way, changing your +app.rb+ file will not reload your +models.rb+ file.
|
99
|
+
|
100
|
+
== Only in Development Mode
|
101
|
+
|
102
|
+
In general, you are only going to want to run this in development mode.
|
103
|
+
Assuming you use +RACK_ENV+ to determine development mode, you can change
|
104
|
+
+config.ru+ to:
|
105
|
+
|
106
|
+
if ENV['RACK_ENV'] == 'development'
|
107
|
+
require 'rack-unreloader'
|
108
|
+
Unreloader = Rack::Unreloader.new{App}
|
109
|
+
Unreloader.require './models.rb'
|
110
|
+
Unreloader.require './app.rb'
|
111
|
+
run Unreloader
|
112
|
+
else
|
113
|
+
require './app.rb'
|
114
|
+
run App
|
115
|
+
end
|
116
|
+
|
117
|
+
If there are dependencies that you don't want to require directly in your
|
118
|
+
+config.ru+, but you do want to use <tt>Rack::Unreloader</tt> for them in
|
119
|
+
development, you can do:
|
120
|
+
|
121
|
+
(defined?(Unreloader) ? Unreloader : Kernel).require './models.rb'
|
122
|
+
|
123
|
+
== Modules
|
124
|
+
|
125
|
+
This reloader also handles modules. Since modules do not have superclasses,
|
126
|
+
if you are using the +:subclasses+ option to specify specific subclasses, you
|
127
|
+
need to specify the module name if you want to reload it:
|
128
|
+
|
129
|
+
Unreloader = Rack::Unreloader.new(:subclasses=>%w'MyModule'){App}
|
130
|
+
|
131
|
+
== Requiring
|
132
|
+
|
133
|
+
Rack::Unreloader#require is a little different than require in that it takes
|
134
|
+
a file glob, not a normal require path. For that reason, you must specify
|
135
|
+
the extension when requiring the file, and it will only look in the current
|
136
|
+
directory by default:
|
137
|
+
|
138
|
+
Unreloader.require 'app.rb'
|
139
|
+
|
140
|
+
If you want to require a file in a different directory, you need to provide the
|
141
|
+
full path:
|
142
|
+
|
143
|
+
Unreloader.require '/path/to/app.rb'
|
144
|
+
|
145
|
+
You can use the usual file globbing:
|
146
|
+
|
147
|
+
Unreloader.require 'models/*.rb'
|
148
|
+
|
149
|
+
== History
|
150
|
+
|
151
|
+
Rack::Unreloader was derived from Padrino's reloader. It is significantly smaller
|
152
|
+
as it cuts out a lot of Padrino-specific code, and it forces the user to manually
|
153
|
+
specify what files to monitor.
|
154
|
+
|
155
|
+
== Caveats
|
156
|
+
|
157
|
+
Unloading constants and reloading files has a ton of corner cases that this
|
158
|
+
will not handle correctly. If it isn't doing what you expect, add a logger:
|
159
|
+
|
160
|
+
Rack::Unreloader.new(:logger=>Logger.new($stdout)){App}
|
161
|
+
|
162
|
+
Unloading constants causes issues whenever references to the constant are
|
163
|
+
cached anywhere instead of looking up the constant by name. This is fairly
|
164
|
+
common, and using this library can cause a memory leak in such a case.
|
165
|
+
|
166
|
+
Approaches that load a fresh environment for every request (or a fresh
|
167
|
+
environment anytime there are any changes) are going to be more robust than
|
168
|
+
this approach, but probably slower. Be aware that you are trading robustness
|
169
|
+
for speed when using this library.
|
170
|
+
|
171
|
+
== License
|
172
|
+
|
173
|
+
MIT
|
174
|
+
|
175
|
+
== Maintainer
|
176
|
+
|
177
|
+
Jeremy Evans <code@jeremyevans.net>
|
data/Rakefile
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require "rake"
|
2
|
+
require "rake/clean"
|
3
|
+
|
4
|
+
CLEAN.include ["rack-unreloader-*.gem", "rdoc"]
|
5
|
+
|
6
|
+
desc "Build rack-unreloader gem"
|
7
|
+
task :package=>[:clean] do |p|
|
8
|
+
sh %{#{FileUtils::RUBY} -S gem build rack-unreloader.gemspec}
|
9
|
+
end
|
10
|
+
|
11
|
+
### Specs
|
12
|
+
|
13
|
+
begin
|
14
|
+
begin
|
15
|
+
# RSpec 1
|
16
|
+
require "spec/rake/spectask"
|
17
|
+
spec_class = Spec::Rake::SpecTask
|
18
|
+
spec_files_meth = :spec_files=
|
19
|
+
rescue LoadError
|
20
|
+
# RSpec 2
|
21
|
+
require "rspec/core/rake_task"
|
22
|
+
spec_class = RSpec::Core::RakeTask
|
23
|
+
spec_files_meth = :pattern=
|
24
|
+
end
|
25
|
+
|
26
|
+
spec = lambda do |name, files, d|
|
27
|
+
lib_dir = File.join(File.dirname(File.expand_path(__FILE__)), 'lib')
|
28
|
+
ENV['RUBYLIB'] ? (ENV['RUBYLIB'] += ":#{lib_dir}") : (ENV['RUBYLIB'] = lib_dir)
|
29
|
+
desc d
|
30
|
+
spec_class.new(name) do |t|
|
31
|
+
t.send(spec_files_meth, files)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
task :default => [:spec]
|
36
|
+
spec.call("spec", Dir["spec/*_spec.rb"], "Run specs")
|
37
|
+
rescue LoadError
|
38
|
+
task :default do
|
39
|
+
puts "Must install rspec to run the default task (which runs specs)"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
### RDoc
|
44
|
+
|
45
|
+
RDOC_DEFAULT_OPTS = ["--quiet", "--line-numbers", "--inline-source", '--title', 'Rack::Unreloader: Reload application when files change, unloading constants first']
|
46
|
+
|
47
|
+
begin
|
48
|
+
gem 'hanna-nouveau'
|
49
|
+
RDOC_DEFAULT_OPTS.concat(['-f', 'hanna'])
|
50
|
+
rescue Gem::LoadError
|
51
|
+
end
|
52
|
+
|
53
|
+
rdoc_task_class = begin
|
54
|
+
require "rdoc/task"
|
55
|
+
RDoc::Task
|
56
|
+
rescue LoadError
|
57
|
+
require "rake/rdoctask"
|
58
|
+
Rake::RDocTask
|
59
|
+
end
|
60
|
+
|
61
|
+
RDOC_OPTS = RDOC_DEFAULT_OPTS + ['--main', 'README.rdoc']
|
62
|
+
|
63
|
+
rdoc_task_class.new do |rdoc|
|
64
|
+
rdoc.rdoc_dir = "rdoc"
|
65
|
+
rdoc.options += RDOC_OPTS
|
66
|
+
rdoc.rdoc_files.add %w"README.rdoc CHANGELOG MIT-LICENSE lib/**/*.rb"
|
67
|
+
end
|
68
|
+
|
@@ -0,0 +1,264 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
# Reloading application that unloads constants before reloading the relevant
|
5
|
+
# files, calling the new rack app if it gets reloaded.
|
6
|
+
class Unreloader
|
7
|
+
class Reloader
|
8
|
+
# Reference to ::File as File would return Rack::File by default.
|
9
|
+
F = ::File
|
10
|
+
|
11
|
+
# Regexp for valid constant names, to prevent code execution.
|
12
|
+
VALID_CONSTANT_NAME_REGEXP = /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/.freeze
|
13
|
+
|
14
|
+
# Setup the reloader. Supports :logger and :subclasses options, see
|
15
|
+
# Rack::Unloader.new for details.
|
16
|
+
def initialize(opts={})
|
17
|
+
@logger = opts[:logger]
|
18
|
+
@classes = opts[:subclasses] ? Array(opts[:subclasses]).map{|s| s.to_s} : %w'Object'
|
19
|
+
|
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).
|
22
|
+
@monitor_files = {}
|
23
|
+
|
24
|
+
# Hash keyed by absolute path of file name, storing constants and other
|
25
|
+
# filenames that the key loads. Values should be hashes with :constants
|
26
|
+
# and :features keys, and arrays of values.
|
27
|
+
@files = {}
|
28
|
+
|
29
|
+
# Similar to @files, but stores previous entries, used when rolling back.
|
30
|
+
@old_entries = {}
|
31
|
+
end
|
32
|
+
|
33
|
+
# Tries to find a declared constant with the name specified
|
34
|
+
# in the string. It raises a NameError when the name is not in CamelCase
|
35
|
+
# or is not initialized.
|
36
|
+
def constantize(s)
|
37
|
+
s = s.to_s
|
38
|
+
if m = VALID_CONSTANT_NAME_REGEXP.match(s)
|
39
|
+
Object.module_eval("::#{m[1]}", __FILE__, __LINE__)
|
40
|
+
else
|
41
|
+
log("#{s.inspect} is not a valid constant name!")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Log the given string at info level if there is a logger.
|
46
|
+
def log(s)
|
47
|
+
@logger.info(s) if @logger
|
48
|
+
end
|
49
|
+
|
50
|
+
# If there are any changed files, reload them. If there are no changed
|
51
|
+
# files, do nothing.
|
52
|
+
def reload!
|
53
|
+
@monitor_files.to_a.each do |file, time|
|
54
|
+
if file_changed?(file, time)
|
55
|
+
safe_load(file)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Require the given dependencies, monitoring them for changes.
|
61
|
+
# Paths should be a file glob or an array of file globs.
|
62
|
+
def require_dependencies(paths)
|
63
|
+
options = {:cyclic => true}
|
64
|
+
error = nil
|
65
|
+
|
66
|
+
Array(paths).
|
67
|
+
flatten.
|
68
|
+
map{|path| Dir.glob(path).sort_by{|filename| filename.count('/')}}.
|
69
|
+
flatten.
|
70
|
+
map{|path| F.expand_path(path)}.
|
71
|
+
uniq.
|
72
|
+
each do |file|
|
73
|
+
|
74
|
+
@monitor_files[file] = nil
|
75
|
+
begin
|
76
|
+
safe_load(file, options)
|
77
|
+
rescue NameError, LoadError => error
|
78
|
+
log "Cyclic dependency reload for #{error}"
|
79
|
+
rescue Exception => error
|
80
|
+
break
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
if error
|
85
|
+
log error
|
86
|
+
raise error
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Requires the given file, logging which constants or features are added
|
91
|
+
# by the require, and rolling back the constants and features if there
|
92
|
+
# are any errors.
|
93
|
+
def safe_load(file, options={})
|
94
|
+
return unless @monitor_files.has_key?(file)
|
95
|
+
return unless options[:force] || file_changed?(file)
|
96
|
+
|
97
|
+
prepare(file) # might call #safe_load recursively
|
98
|
+
log "#{@monitor_files[file] ? 'Reloading' : 'Loading'} #{file}"
|
99
|
+
begin
|
100
|
+
require(file)
|
101
|
+
commit(file)
|
102
|
+
rescue Exception
|
103
|
+
if !options[:cyclic]
|
104
|
+
log "Failed to load #{file}; removing partially defined constants"
|
105
|
+
end
|
106
|
+
rollback(file)
|
107
|
+
raise
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Removes the specified constant.
|
112
|
+
def remove_constant(const)
|
113
|
+
base, _, object = const.to_s.rpartition('::')
|
114
|
+
base = base.empty? ? Object : constantize(base)
|
115
|
+
base.send :remove_const, object
|
116
|
+
log "Removed constant #{const}"
|
117
|
+
rescue NameError
|
118
|
+
end
|
119
|
+
|
120
|
+
# Remove a feature if it is being monitored for reloading, so it
|
121
|
+
# can be required again.
|
122
|
+
def remove_feature(file)
|
123
|
+
$LOADED_FEATURES.delete(file) if @monitor_files.has_key?(file)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Unload all reloadable constants and features, and clear the list
|
127
|
+
# of files to monitor.
|
128
|
+
def clear!
|
129
|
+
@files.keys.each do |file|
|
130
|
+
remove(file)
|
131
|
+
remove_feature(file)
|
132
|
+
end
|
133
|
+
@monitor_files = {}
|
134
|
+
@old_entries = {}
|
135
|
+
end
|
136
|
+
|
137
|
+
# Remove the given file, removing any constants and other files loaded
|
138
|
+
# by the file.
|
139
|
+
def remove(name)
|
140
|
+
file = @files[name] || return
|
141
|
+
file[:constants].each{|constant| remove_constant(constant)}
|
142
|
+
file[:features].each{|feature| remove_feature(feature)}
|
143
|
+
@files.delete(name)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Store the currently loaded classes and features, so in case of an error
|
147
|
+
# this state can be rolled back to.
|
148
|
+
def prepare(name)
|
149
|
+
file = remove(name)
|
150
|
+
old_features = Set.new($LOADED_FEATURES)
|
151
|
+
@old_entries[name] = {:constants => all_classes, :features => old_features}
|
152
|
+
features = file && file[:features] || []
|
153
|
+
features.each{|feature| safe_load(feature, :force => true)}
|
154
|
+
remove_feature(name) if old_features.include?(name)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Commit the changed state after requiring the the file, recording the new
|
158
|
+
# classes and features added by the file.
|
159
|
+
def commit(name)
|
160
|
+
entry = {
|
161
|
+
:constants => new_classes(@old_entries[name][:constants]),
|
162
|
+
:features => Set.new($LOADED_FEATURES) - @old_entries[name][:features] - [name]
|
163
|
+
}
|
164
|
+
@files[name] = entry
|
165
|
+
@old_entries.delete(name)
|
166
|
+
@monitor_files[name] = modified_at(name)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Rollback the changes made by requiring the file, restoring the previous state.
|
170
|
+
def rollback(name)
|
171
|
+
new_classes(@old_entries[name][:constants]).each{|klass| remove_constant(klass)}
|
172
|
+
@old_entries.delete(name)
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
|
177
|
+
# Return a set of all classes in the ObjectSpace.
|
178
|
+
def all_classes
|
179
|
+
rs = Set.new
|
180
|
+
|
181
|
+
ObjectSpace.each_object(Module).each do |klass|
|
182
|
+
if monitored_class?(klass)
|
183
|
+
rs << klass
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
rs
|
188
|
+
end
|
189
|
+
|
190
|
+
# Return whether the given klass is a monitored class that could
|
191
|
+
# be unloaded.
|
192
|
+
def monitored_class?(klass)
|
193
|
+
@classes.any? do |c|
|
194
|
+
c = constantize(c) rescue false
|
195
|
+
|
196
|
+
if klass.is_a?(Class)
|
197
|
+
# Reload the class if it is a subclass if the current class
|
198
|
+
(klass < c) rescue false
|
199
|
+
elsif c == Object
|
200
|
+
# If reloading for all classes, reload for all modules as well
|
201
|
+
true
|
202
|
+
else
|
203
|
+
# Otherwise, reload only if the module matches exactly, since
|
204
|
+
# modules don't have superclasses.
|
205
|
+
klass == c
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Return a set of all classes in the ObjectSpace that are not in the
|
211
|
+
# given set of classes.
|
212
|
+
def new_classes(snapshot)
|
213
|
+
all_classes - snapshot
|
214
|
+
end
|
215
|
+
|
216
|
+
# Returns true if the file is new or it's modification time changed.
|
217
|
+
def file_changed?(file, time = @monitor_files[file])
|
218
|
+
!time || modified_at(file) > time
|
219
|
+
end
|
220
|
+
|
221
|
+
# Return the time the file was modified at. This can be overridden
|
222
|
+
# to base the reloading on something other than the file's modification
|
223
|
+
# time.
|
224
|
+
def modified_at(file)
|
225
|
+
F.mtime(file)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# The Rack::Unreloader::Reloader instead related to this instance.
|
230
|
+
attr_reader :reloader
|
231
|
+
|
232
|
+
# Setup the reloader. Options:
|
233
|
+
#
|
234
|
+
# :cooldown :: The number of seconds to wait between checks for changed files.
|
235
|
+
# Defaults to 1.
|
236
|
+
# :logger :: A Logger instance which will log information related to reloading.
|
237
|
+
# :subclasses :: A string or array of strings of class names that should be unloaded.
|
238
|
+
# Any classes that are not subclasses of these classes will not be
|
239
|
+
# unloaded. This also handles modules, but module names given must
|
240
|
+
# match exactly, since modules don't have superclasses.
|
241
|
+
def initialize(opts={}, &block)
|
242
|
+
@app_block = block
|
243
|
+
@cooldown = opts[:cooldown] || 1
|
244
|
+
@last = Time.at(0)
|
245
|
+
@reloader = Reloader.new(opts)
|
246
|
+
@reloader.reload!
|
247
|
+
end
|
248
|
+
|
249
|
+
# If the cooldown time has been passed, reload any application files that have changed.
|
250
|
+
# Call the app with the environment.
|
251
|
+
def call(env)
|
252
|
+
if @cooldown && Time.now > @last + @cooldown
|
253
|
+
Thread.exclusive{@reloader.reload!}
|
254
|
+
@last = Time.now
|
255
|
+
end
|
256
|
+
@app_block.call.call(env)
|
257
|
+
end
|
258
|
+
|
259
|
+
# Add a file glob or array of file globs to monitor for changes.
|
260
|
+
def require(depends)
|
261
|
+
@reloader.require_dependencies(depends)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), '../lib/rack/unreloader')
|
2
|
+
|
3
|
+
module ModifiedAt
|
4
|
+
def set_modified_time(file, time)
|
5
|
+
modified_times[File.expand_path(file)] = time
|
6
|
+
end
|
7
|
+
|
8
|
+
def modified_times
|
9
|
+
@modified_times ||= {}
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def modified_at(file)
|
15
|
+
modified_times[file] || super
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe Rack::Unreloader do
|
20
|
+
def code(i)
|
21
|
+
"class App; def self.call(env) @a end; @a ||= []; @a << #{i}; end"
|
22
|
+
end
|
23
|
+
|
24
|
+
def update_app(code, file=@filename)
|
25
|
+
ru.reloader.set_modified_time(file, @i += 1)
|
26
|
+
File.open(file, 'wb'){|f| f.write(code)}
|
27
|
+
end
|
28
|
+
|
29
|
+
def logger
|
30
|
+
return @logger if @logger
|
31
|
+
@logger = []
|
32
|
+
def @logger.method_missing(meth, log)
|
33
|
+
self << log
|
34
|
+
end
|
35
|
+
@logger
|
36
|
+
end
|
37
|
+
|
38
|
+
def ru(opts={})
|
39
|
+
return @ru if @ru
|
40
|
+
block = opts[:block] || proc{App}
|
41
|
+
@ru = Rack::Unreloader.new({:logger=>logger, :cooldown=>0}.merge(opts), &block)
|
42
|
+
@ru.reloader.extend ModifiedAt
|
43
|
+
Object.const_set(:RU, @ru)
|
44
|
+
update_app(opts[:code]||code(1))
|
45
|
+
yield if block_given?
|
46
|
+
@ru.require 'spec/app.rb'
|
47
|
+
@ru
|
48
|
+
end
|
49
|
+
|
50
|
+
def log_match(*logs)
|
51
|
+
logs.length == @logger.length
|
52
|
+
logs.zip(@logger).each{|l, log| l.is_a?(String) ? log.should == l : log.should =~ l}
|
53
|
+
end
|
54
|
+
|
55
|
+
before do
|
56
|
+
@i = 0
|
57
|
+
@filename = 'spec/app.rb'
|
58
|
+
end
|
59
|
+
|
60
|
+
after do
|
61
|
+
ru.reloader.clear!
|
62
|
+
Object.send(:remove_const, :RU)
|
63
|
+
Object.send(:remove_const, :App) if defined?(::App)
|
64
|
+
Object.send(:remove_const, :App2) if defined?(::App2)
|
65
|
+
Dir['spec/app*.rb'].each{|f| File.delete(f)}
|
66
|
+
end
|
67
|
+
|
68
|
+
it "it should unload constants contained in file and reload file if file changes" do
|
69
|
+
ru.call({}).should == [1]
|
70
|
+
update_app(code(2))
|
71
|
+
ru.call({}).should == [2]
|
72
|
+
log_match(%r{\ALoading.*spec/app\.rb\z}, "Removed constant App", %r{\AReloading.*spec/app\.rb\z})
|
73
|
+
end
|
74
|
+
|
75
|
+
it "it should pickup files added as dependencies" do
|
76
|
+
ru.call({}).should == [1]
|
77
|
+
update_app("RU.require 'spec/app2.rb'; class App; def self.call(env) [@a, App2.call(env)] end; @a ||= []; @a << 2; end")
|
78
|
+
update_app("class App2; def self.call(env) @a end; @a ||= []; @a << 3; end", 'spec/app2.rb')
|
79
|
+
ru.call({}).should == [[2], [3]]
|
80
|
+
update_app("class App2; def self.call(env) @a end; @a ||= []; @a << 4; end", 'spec/app2.rb')
|
81
|
+
ru.call({}).should == [[2], [4]]
|
82
|
+
update_app("RU.require 'spec/app2.rb'; class App; def self.call(env) [@a, App2.call(env)] end; @a ||= []; @a << 2; end")
|
83
|
+
log_match(%r{\ALoading.*spec/app\.rb\z}, "Removed constant App", %r{\AReloading.*spec/app\.rb\z},
|
84
|
+
%r{\ALoading.*spec/app2\.rb\z}, "Removed constant App2", %r{\AReloading.*spec/app2\.rb\z})
|
85
|
+
end
|
86
|
+
|
87
|
+
it "it should support :subclasses option and only unload subclasses of given class" do
|
88
|
+
ru(:subclasses=>'App').call({}).should == [1]
|
89
|
+
update_app("RU.require 'spec/app2.rb'; class App; def self.call(env) [@a, App2.call(env)] end; @a ||= []; @a << 2; end")
|
90
|
+
update_app("class App2 < App; def self.call(env) @a end; @a ||= []; @a << 3; end", 'spec/app2.rb')
|
91
|
+
ru.call({}).should == [[1, 2], [3]]
|
92
|
+
update_app("class App2 < App; def self.call(env) @a end; @a ||= []; @a << 4; end", 'spec/app2.rb')
|
93
|
+
ru.call({}).should == [[1, 2], [4]]
|
94
|
+
update_app("RU.require 'spec/app2.rb'; class App; def self.call(env) [@a, App2.call(env)] end; @a ||= []; @a << 2; end")
|
95
|
+
log_match(%r{\ALoading.*spec/app\.rb\z}, %r{\AReloading.*spec/app\.rb\z},
|
96
|
+
%r{\ALoading.*spec/app2\.rb\z}, "Removed constant App2", %r{\AReloading.*spec/app2\.rb\z})
|
97
|
+
end
|
98
|
+
|
99
|
+
it "it log invalid constant names in :subclasses options" do
|
100
|
+
ru(:subclasses=>%w'1 Object').call({}).should == [1]
|
101
|
+
logger.uniq!
|
102
|
+
log_match('"1" is not a valid constant name!', %r{\ALoading.*spec/app\.rb\z})
|
103
|
+
end
|
104
|
+
|
105
|
+
it "it should unload modules before reloading similar to classes" do
|
106
|
+
ru(:code=>"module App; def self.call(env) @a end; @a ||= []; @a << 1; end").call({}).should == [1]
|
107
|
+
update_app("module App; def self.call(env) @a end; @a ||= []; @a << 2; end")
|
108
|
+
ru.call({}).should == [2]
|
109
|
+
log_match(%r{\ALoading.*spec/app\.rb\z}, "Removed constant App", %r{\AReloading.*spec/app\.rb\z})
|
110
|
+
end
|
111
|
+
|
112
|
+
it "it should unload specific modules by name via :subclasses option" do
|
113
|
+
ru(:subclasses=>'App', :code=>"module App; def self.call(env) @a end; @a ||= []; @a << 1; end").call({}).should == [1]
|
114
|
+
update_app("module App; def self.call(env) @a end; @a ||= []; @a << 2; end")
|
115
|
+
ru.call({}).should == [2]
|
116
|
+
log_match(%r{\ALoading.*spec/app\.rb\z}, "Removed constant App", %r{\AReloading.*spec/app\.rb\z})
|
117
|
+
end
|
118
|
+
|
119
|
+
it "it should not unload modules by name if :subclasses option used and module not present" do
|
120
|
+
ru(:subclasses=>'Foo', :code=>"module App; def self.call(env) @a end; @a ||= []; @a << 1; end").call({}).should == [1]
|
121
|
+
update_app("module App; def self.call(env) @a end; @a ||= []; @a << 2; end")
|
122
|
+
ru.call({}).should == [1, 2]
|
123
|
+
log_match(%r{\ALoading.*spec/app\.rb\z}, %r{\AReloading.*spec/app\.rb\z})
|
124
|
+
end
|
125
|
+
|
126
|
+
it "it unload partially loaded modules if loading fails, and allow future loading" do
|
127
|
+
ru.call({}).should == [1]
|
128
|
+
update_app("module App; def self.call(env) @a end; @a ||= []; raise 'foo'; end")
|
129
|
+
proc{ru.call({})}.should raise_error
|
130
|
+
defined?(::App).should == nil
|
131
|
+
update_app(code(2))
|
132
|
+
ru.call({}).should == [2]
|
133
|
+
log_match(%r{\ALoading.*spec/app\.rb\z}, "Removed constant App", %r{\AReloading.*spec/app\.rb\z},
|
134
|
+
%r{\AFailed to load .*spec/app\.rb; removing partially defined constants\z},
|
135
|
+
"Removed constant App", %r{\AReloading.*spec/app\.rb\z})
|
136
|
+
end
|
137
|
+
|
138
|
+
it "it should unload classes in namespaces" do
|
139
|
+
ru(:code=>"class Array::App; def self.call(env) @a end; @a ||= []; @a << 1; end", :block=>proc{Array::App}).call({}).should == [1]
|
140
|
+
update_app("class Array::App; def self.call(env) @a end; @a ||= []; @a << 2; end")
|
141
|
+
ru.call({}).should == [2]
|
142
|
+
log_match(%r{\ALoading.*spec/app\.rb\z}, "Removed constant Array::App", %r{\AReloading.*spec/app\.rb\z})
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-unreloader
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jeremy Evans
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-08-04 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |
|
14
|
+
Rack::Unreloader is a rack middleware that reloads application files when it
|
15
|
+
detects changes, unloading constants defined in those files before reloading.
|
16
|
+
email: code@jeremyevans.net
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files:
|
20
|
+
- README.rdoc
|
21
|
+
- CHANGELOG
|
22
|
+
- MIT-LICENSE
|
23
|
+
files:
|
24
|
+
- CHANGELOG
|
25
|
+
- MIT-LICENSE
|
26
|
+
- README.rdoc
|
27
|
+
- Rakefile
|
28
|
+
- lib/rack/unreloader.rb
|
29
|
+
- spec/unreloader_spec.rb
|
30
|
+
homepage: http://gihub.com/jeremyevans/rack-unreloader
|
31
|
+
licenses:
|
32
|
+
- MIT
|
33
|
+
metadata: {}
|
34
|
+
post_install_message:
|
35
|
+
rdoc_options:
|
36
|
+
- "--quiet"
|
37
|
+
- "--line-numbers"
|
38
|
+
- "--inline-source"
|
39
|
+
- "--title"
|
40
|
+
- 'Rack::Unreloader: Reload application when files change, unloading constants first'
|
41
|
+
- "--main"
|
42
|
+
- README.rdoc
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
requirements: []
|
56
|
+
rubyforge_project:
|
57
|
+
rubygems_version: 2.2.2
|
58
|
+
signing_key:
|
59
|
+
specification_version: 4
|
60
|
+
summary: Reload application when files change, unloading constants first
|
61
|
+
test_files: []
|