class Rack::Unreloader::Reloader

Constants

F
FORCE

Options hash to force loading of files even if they haven't changed.

VALID_CONSTANT_NAME_REGEXP

Regexp for valid constant names, to prevent code execution.

Public Class Methods

new(opts={}) click to toggle source

Setup the reloader. Supports :logger and :subclasses options, see Rack::Unloader.new for details.

   # File lib/rack/unreloader/reloader.rb
16 def initialize(opts={})
17   @logger = opts[:logger]
18   @classes = opts[:subclasses] ?  Array(opts[:subclasses]).map(&: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 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.
28   @monitor_dirs = {}
29 
30   # Hash of procs returning constants defined in files, keyed by absolute path
31   # of file name.  If there is no proc, must call ObjectSpace before and after
32   # loading files to detect changes, which is slower.
33   @constants_defined = {}
34 
35   # Hash keyed by absolute path of file name, storing constants and other
36   # filenames that the key loads.  Values should be hashes with :constants
37   # and :features keys, and arrays of values.
38   @files = {}
39 
40   # Similar to @files, but stores previous entries, used when rolling back.
41   @old_entries = {}
42 
43   # Records dependencies on files.  Keys are absolute paths, values are arrays of absolute paths,
44   # where each entry depends on the key, so that if the key path is modified, all values are
45   # reloaded.
46   @dependencies = {}
47 
48   # Array of the order in which to load dependencies
49   @dependency_order = []
50 
51   # Array of absolute paths which should be unloaded, but not reloaded on changes,
52   # because files that depend on them will load them automatically.
53   @skip_reload = []
54 end

Public Instance Methods

clear!() click to toggle source

Unload all reloadable constants and features, and clear the list of files to monitor.

    # File lib/rack/unreloader/reloader.rb
 99 def clear!
100   @files.keys.each do |file|
101     remove(file)
102   end
103   @monitor_files = {}
104   @old_entries = {}
105 end
record_dependency(path, files) click to toggle source

Record a dependency the given files, such that each file in files depends on path. If path changes, each file in files should be reloaded as well.

    # File lib/rack/unreloader/reloader.rb
110 def record_dependency(path, files)
111   files = (@dependencies[path] ||= []).concat(files)
112   files.uniq!
113 
114   order = @dependency_order
115   i = order.find_index{|v| files.include?(v)} || -1
116   order.insert(i, path)
117   order.concat(files)
118   order.uniq!
119 
120   if F.directory?(path)
121     (@monitor_files.keys & Unreloader.ruby_files(path)).each do |file|
122       record_dependency(file, files)
123     end
124   end
125 
126   nil
127 end
reload!() click to toggle source

If there are any changed files, reload them. If there are no changed files, do nothing.

    # File lib/rack/unreloader/reloader.rb
131 def reload!
132   changed_files = []
133 
134   @monitor_dirs.keys.each do |dir|
135     check_monitor_dir(dir, changed_files)
136   end
137 
138   removed_files = []
139 
140   @monitor_files.to_a.each do |file, time|
141     if F.file?(file)
142       if file_changed?(file, time)
143         changed_files << file
144       end
145     else
146       removed_files << file
147     end
148   end
149 
150   remove_files(removed_files)
151 
152   return if changed_files.empty?
153 
154   unless @dependencies.empty?
155     changed_files = reload_files(changed_files)
156     changed_files.flatten!
157     changed_files.map!{|f| F.directory?(f) ? Unreloader.ruby_files(f) : f}
158     changed_files.flatten!
159     changed_files.uniq!
160     
161     order = @dependency_order
162     order &= changed_files
163     changed_files = order + (changed_files - order)
164   end
165 
166   unless @skip_reload.empty?
167     skip_reload = @skip_reload.map{|f| F.directory?(f) ? Unreloader.ruby_files(f) : f}
168     skip_reload.flatten!
169     skip_reload.uniq!
170     changed_files -= skip_reload
171   end
172 
173   changed_files.each do |file|
174     safe_load(file, FORCE)
175   end
176 end
require_dependencies(paths, &block) click to toggle source

Require the given dependencies, monitoring them for changes. Paths should be a file glob or an array of file globs.

    # File lib/rack/unreloader/reloader.rb
180 def require_dependencies(paths, &block)
181   options = {:cyclic => true}
182   error = nil 
183 
184   Unreloader.expand_paths(paths).each do |file|
185     if F.directory?(file)
186       @monitor_dirs[file] = [nil, [], block]
187       check_monitor_dir(file)
188       next
189     else
190       @constants_defined[file] = block
191       @monitor_files[file] = nil
192     end
193 
194     begin
195       safe_load(file, options)
196     rescue NameError, LoadError => error
197       log "Cyclic dependency reload for #{error}"
198     rescue Exception => error
199       break
200     end
201   end
202 
203   if error
204     log error
205     raise error
206   end
207 end
skip_reload(files) click to toggle source

Skip reloading the given files. Should only be used if other files depend on these files and the other files require these files when loaded.

    # File lib/rack/unreloader/reloader.rb
212 def skip_reload(files)
213   @skip_reload.concat(files)
214   @skip_reload.uniq!
215   nil
216 end
strip_path_prefix(path_prefix) click to toggle source

Strip the given path prefix from the internal data structures.

   # File lib/rack/unreloader/reloader.rb
57 def strip_path_prefix(path_prefix)
58   empty = ''.freeze
59 
60   # Strip the path prefix from $LOADED_FEATURES, otherwise the reloading won't work.
61   # Hopefully a future version of ruby will do this automatically when chrooting.
62   $LOADED_FEATURES.map!{|s| s.sub(path_prefix, empty)}
63 
64   fix_path = lambda do |s|
65     s.sub(path_prefix, empty)
66   end
67 
68   [@dependency_order, @skip_reload].each do |a|
69     a.map!(&fix_path)
70   end
71 
72   [@files, @old_entries].each do |hash|
73     hash.each do |k,h|
74       h[:features].map!(&fix_path)
75     end
76   end
77   
78   @monitor_dirs.each_value do |a|
79     a[1].map!(&fix_path)
80   end
81 
82   @dependencies.each_value do |a|
83     a.map!(&fix_path)
84   end
85 
86   [@files, @old_entries, @monitor_files, @monitor_dirs, @constants_defined, @dependencies].each do |hash|
87     hash.keys.each do |k|
88       if k.start_with?(path_prefix)
89         hash[fix_path.call(k)] = hash.delete(k)
90       end
91     end
92   end
93 
94   nil
95 end

Private Instance Methods

all_classes() click to toggle source

Return a set of all classes in the ObjectSpace.

    # File lib/rack/unreloader/reloader.rb
412 def all_classes
413   rs = Set.new
414 
415   ::ObjectSpace.each_object(Module).each do |mod|
416     if !mod.name.to_s.empty? && monitored_module?(mod)
417       rs << mod
418     end
419   end
420 
421   rs
422 end
check_monitor_dir(dir, changed_files=nil) click to toggle source

Check a monitored directory for changes, adding new files and removing deleted files.

    # File lib/rack/unreloader/reloader.rb
239 def check_monitor_dir(dir, changed_files=nil)
240   time, files, block = @monitor_dirs[dir]
241 
242   cur_files = Unreloader.ruby_files(dir)
243   return if files == cur_files
244 
245   removed_files = files - cur_files
246   new_files = cur_files - files
247 
248   if changed_files
249     changed_files.concat(dependency_files(removed_files))
250   end
251 
252   remove_files(removed_files)
253 
254   require_dependencies(new_files, &block)
255 
256   new_files.each do |file|
257     if deps = @dependencies[dir]
258       record_dependency(file, deps)
259     end
260   end
261 
262   if changed_files
263     changed_files.concat(dependency_files(new_files))
264   end
265 
266   files.replace(cur_files)
267 end
commit(name) click to toggle source

Commit the changed state after requiring the the file, recording the new classes and features added by the file.

    # File lib/rack/unreloader/reloader.rb
381 def commit(name)
382   entry = {:features => monitored_features - @old_entries[name][:features] - [name], :constants=>constants_loaded_by(name)}
383 
384   @files[name] = entry
385   @old_entries.delete(name)
386   @monitor_files[name] = modified_at(name)
387 
388   defs, not_defs = entry[:constants].partition{|c| constant_defined?(c)}
389   unless not_defs.empty?
390     log "Constants not defined after loading #{name}: #{not_defs.join(' ')}"
391   end
392   unless defs.empty?
393     log("New classes in #{name}: #{defs.join(' ')}") 
394   end
395   unless entry[:features].empty?
396     log("New features in #{name}: #{entry[:features].to_a.join(' ')}") 
397   end
398 end
constant_defined?(const) click to toggle source

True if the constant is already defined, false if not

    # File lib/rack/unreloader/reloader.rb
337 def constant_defined?(const)
338   constantize(const)
339   true
340 rescue
341   false
342 end
constantize(s) click to toggle source

Tries to find a declared constant with the name specified in the string. It raises a NameError when the name is not in CamelCase or is not initialized.

    # File lib/rack/unreloader/reloader.rb
223 def constantize(s)
224   s = s.to_s
225   if m = VALID_CONSTANT_NAME_REGEXP.match(s)
226     Object.module_eval("::#{m[1]}", __FILE__, __LINE__)
227   else
228     log("Invalid constant name: #{s}")
229   end
230 end
constants_for(name) click to toggle source

Returns nil if ObjectSpace should be used to load the constants. Returns an array of constant name symbols loaded by the file if they have been manually specified.

    # File lib/rack/unreloader/reloader.rb
362 def constants_for(name)
363   if (pr = @constants_defined[name]) && (constants = pr.call(name)) != :ObjectSpace
364     Array(constants)
365   end
366 end
constants_loaded_by(name) click to toggle source

The constants that were loaded by the given file. If ObjectSpace was used to check all classes loaded previously, then check for new classes loaded since. If the constants were explicitly specified, then use them directly

    # File lib/rack/unreloader/reloader.rb
371 def constants_loaded_by(name)
372   if @old_entries[name][:all_classes]
373     new_classes(@old_entries[name][:all_classes])
374   else
375     @old_entries[name][:constants]
376   end
377 end
dependency_files(changed_files) click to toggle source

The dependencies for the changed files, excluding the files themselves.

    # File lib/rack/unreloader/reloader.rb
456 def dependency_files(changed_files)
457   files = reload_files(changed_files)
458   files.flatten!
459   files - changed_files
460 end
file_changed?(file, time = @monitor_files[file]) click to toggle source

Returns true if the file is new or it's modification time changed.

    # File lib/rack/unreloader/reloader.rb
469 def file_changed?(file, time = @monitor_files[file])
470   !time || modified_at(file) > time
471 end
log(s) click to toggle source

Log the given string at info level if there is a logger.

    # File lib/rack/unreloader/reloader.rb
233 def log(s)
234   @logger.info(s) if @logger
235 end
modified_at(file) click to toggle source

Return the time the file was modified at. This can be overridden to base the reloading on something other than the file's modification time.

    # File lib/rack/unreloader/reloader.rb
476 def modified_at(file)
477   F.mtime(file)
478 end
monitored_features() click to toggle source

The current loaded features that are being monitored

    # File lib/rack/unreloader/reloader.rb
407 def monitored_features
408   Set.new($LOADED_FEATURES) & @monitor_files.keys
409 end
monitored_module?(mod) click to toggle source

Return whether the given klass is a monitored class that could be unloaded.

    # File lib/rack/unreloader/reloader.rb
426 def monitored_module?(mod)
427   @classes.any? do |c|
428     c = constantize(c) rescue false
429 
430     if mod.is_a?(Class)
431       # Reload the class if it is a subclass if the current class
432       (mod < c) rescue false
433     elsif c == Object
434       # If reloading for all classes, reload for all modules as well
435       true
436     else
437       # Otherwise, reload only if the module matches exactly, since
438       # modules don't have superclasses.
439       mod == c
440     end
441   end
442 end
new_classes(snapshot) click to toggle source

Return a set of all classes in the ObjectSpace that are not in the given set of classes.

    # File lib/rack/unreloader/reloader.rb
464 def new_classes(snapshot)
465   all_classes - snapshot
466 end
prepare(name) click to toggle source

Store the currently loaded classes and features, so in case of an error this state can be rolled back to.

    # File lib/rack/unreloader/reloader.rb
346 def prepare(name)
347   file = remove(name)
348   @old_entries[name] = {:features => monitored_features}
349   if constants = constants_for(name)
350     defs = constants.select{|c| constant_defined?(c)}
351     unless defs.empty?
352       log "Constants already defined before loading #{name}: #{defs.join(" ")}"
353     end
354     @old_entries[name][:constants] = constants
355   else
356     @old_entries[name][:all_classes] = all_classes
357   end
358 end
reload_files(changed_files) click to toggle source

Recursively reload dependencies for the changed files.

    # File lib/rack/unreloader/reloader.rb
445 def reload_files(changed_files)
446   changed_files.map do |file|
447     if deps = @dependencies[file]
448       [file] + reload_files(deps)
449     else
450       file
451     end
452   end
453 end
remove(name) click to toggle source

Remove the given file, removing any constants and other files loaded by the file.

    # File lib/rack/unreloader/reloader.rb
322 def remove(name)
323   file = @files[name] || return
324   remove_feature(name) if $LOADED_FEATURES.include?(name)
325   file[:features].each{|feature| remove_feature(feature)}
326   remove_constants(name){file[:constants]}
327   @files.delete(name)
328 end
remove_constant(const) click to toggle source

Removes the specified constant.

    # File lib/rack/unreloader/reloader.rb
302 def remove_constant(const)
303   base, _, object = const.to_s.rpartition('::')
304   base = base.empty? ? Object : constantize(base)
305   base.send :remove_const, object
306   log "Removed constant #{const}"
307 rescue NameError
308   log "Error removing constant: #{const}"
309 end
remove_constants(name) { || ... } click to toggle source

Remove constants defined in file. Uses the stored block if there is one for the file name, or the given block.

    # File lib/rack/unreloader/reloader.rb
332 def remove_constants(name)
333   yield.each{|constant| remove_constant(constant)}
334 end
remove_feature(file) click to toggle source

Remove a feature if it is being monitored for reloading, so it can be required again.

    # File lib/rack/unreloader/reloader.rb
313 def remove_feature(file)
314   if @monitor_files.has_key?(file)
315     log "Unloading #{file}"
316     $LOADED_FEATURES.delete(file)
317   end
318 end
remove_files(removed_files) click to toggle source

Remove all files in removed_files from the internal data structures, because the file no longer exists.

    # File lib/rack/unreloader/reloader.rb
271 def remove_files(removed_files)
272   removed_files.each do |f|
273     remove(f)
274     @monitor_files.delete(f)
275     @dependencies.delete(f)
276     @dependency_order.delete(f)
277   end
278 end
rollback(name) click to toggle source

Rollback the changes made by requiring the file, restoring the previous state.

    # File lib/rack/unreloader/reloader.rb
401 def rollback(name)
402   remove_constants(name){constants_loaded_by(name)}
403   @old_entries.delete(name)
404 end
safe_load(file, options={}) click to toggle source

Requires the given file, logging which constants or features are added by the require, and rolling back the constants and features if there are any errors.

    # File lib/rack/unreloader/reloader.rb
283 def safe_load(file, options={})
284   return unless @monitor_files.has_key?(file)
285   return unless options[:force] || file_changed?(file)
286 
287   prepare(file) # might call #safe_load recursively
288   log "Loading #{file}"
289   begin
290     require(file)
291     commit(file)
292   rescue Exception
293     if !options[:cyclic]
294       log "Failed to load #{file}; removing partially defined constants"
295     end
296     rollback(file)
297     raise
298   end
299 end