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
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
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 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
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 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 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 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
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 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 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
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
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
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
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
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
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 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
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
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
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
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
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
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 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
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 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 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 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 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
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