module Padrino::Reloader
def changed?
Returns true if any file changes are detected and populates the MTIMES cache
#
def changed? changed = false rotation do |file, mtime| new_file = MTIMES[file].nil? previous_mtime = MTIMES[file] changed = true if new_file || mtime > previous_mtime end changed end
def clear!
Remove files and classes loaded with stat
#
def clear! clear_modification_times clear_loaded_classes clear_loaded_files_and_features end
def clear_loaded_classes
def clear_loaded_classes LOADED_CLASSES.each do |file, klasses| klasses.each { |klass| remove_constant(klass) } LOADED_CLASSES.delete(file) end end
def clear_loaded_files_and_features
def clear_loaded_files_and_features LOADED_FILES.each do |file, dependencies| dependencies.each { |dependency| $LOADED_FEATURES.delete(dependency) } $LOADED_FEATURES.delete(file) end end
def clear_modification_times
loaded features/files/mtimes
Clear instance variables that keep track of
##
def clear_modification_times MTIMES.clear end
def exclude
Default excluded directories at Padrino.root are: test, spec, features, tmp, config, db and public
Specified folders can be excluded from the code reload detection process.
#
def exclude @_exclude ||= %w(test spec tmp features config public db).map { |path| Padrino.root(path) } end
def exclude_constants
Specified constants can be excluded from the code unloading process.
#
def exclude_constants @_exclude_constants ||= Set.new end
def figure_path(file)
Returns true if the file is defined in our padrino root
#
def figure_path(file) return file if Pathname.new(file).absolute? $:.each do |path| found = File.join(path, file) return File.expand_path(found) if File.exist?(found) end file end
def files_for_rotation
Creates an array of paths for use in #rotation
#
def files_for_rotation files = Padrino.load_paths.map { |path| Dir["#{path}/**/*.rb"] }.flatten files = files | Padrino.mounted_apps.map { |app| app.app_file } files = files | Padrino.mounted_apps.map { |app| app.app_obj.dependencies }.flatten end
def in_root?(file)
Returns true if file is in our Padrino.root
#
def in_root?(file) # This is better but slow: # Pathname.new(Padrino.root).find { |f| File.identical?(Padrino.root(f), figure_path(file)) } figure_path(file).index(Padrino.root) == 0 end
def include_constants
Default included constants are: [none]
Specified constants can be configured to be reloaded on every request.
#
def include_constants @_include_constants ||= Set.new end
def lock!
We lock dependencies sets to prevent reloading of protected constants
#
def lock! klasses = ObjectSpace.classes do |klass| klass._orig_klass_name.split('::')[0] end klasses = klasses | Padrino.mounted_apps.map { |app| app.app_class } Padrino::Reloader.exclude_constants.merge(klasses) end
def modification_time(file)
Macro for mtime query
##
def modification_time(file) MTIMES[file] end
def mounted_apps_of(file)
Can be an array because in one app.rb we can define multiple Padrino::Appplications
Return the mounted_apps providing the app location
#
def mounted_apps_of(file) file = figure_path(file) Padrino.mounted_apps.find_all { |app| File.identical?(file, app.app_file) } end
def process_loaded_file(*args)
Tracks loaded file features/classes/constants
##
def process_loaded_file(*args) options = args.extract_options! new_constants = options[:constants] files = options[:files] file = options[:file] # Store the file details LOADED_CLASSES[file] = new_constants LOADED_FILES[file] = Set.new($LOADED_FEATURES) - files - [file] # Track only features in our Padrino.root LOADED_FILES[file].delete_if { |feature| !in_root?(feature) } end
def reload!
Reload all files with changes detected.
#
def reload! # Detect changed files rotation do |file, mtime| # Retrive the last modified time new_file = MTIMES[file].nil? previous_mtime = MTIMES[file] ||= mtime logger.devel "Detected a new file #{file}" if new_file # We skip to next file if it is not new and not modified next unless new_file || mtime > previous_mtime # Now we can reload our file apps = mounted_apps_of(file) if apps.present? apps.each { |app| app.app_obj.reload! } else safe_load(file, :force => new_file) # Reload also apps Padrino.mounted_apps.each do |app| app.app_obj.reload! if app.app_obj.dependencies.include?(file) end end end end
def reload_deps_of_file(file)
Safe load dependencies of a file
##
def reload_deps_of_file(file) if features = LOADED_FILES.delete(file) features.each { |feature| safe_load(feature, :force => true) } end end
def remove_constant(const)
Removes the specified class and constant.
#
def remove_constant(const) return if exclude_constants.any? { |c| const._orig_klass_name.index(c) == 0 } && !include_constants.any? { |c| const._orig_klass_name.index(c) == 0 } begin parts = const.to_s.sub(/^::(Object)?/, 'Object::').split('::') object = parts.pop base = parts.empty? ? Object : Inflector.constantize(parts * '::') base.send :remove_const, object logger.devel "Removed constant: #{const} from #{base}" rescue NameError; end end
def remove_loaded_file_classes(file)
Removes all classes declared in the specified file
#
def remove_loaded_file_classes(file) if klasses = LOADED_CLASSES.delete(file) klasses.each { |klass| remove_constant(klass) } end end
def remove_loaded_file_features(file)
Remove all loaded fatures with our file
#
def remove_loaded_file_features(file) if features = LOADED_FILES[file] features.each { |feature| $LOADED_FEATURES.delete(feature) } end end
def rotation
and monitors them for any changes.
Searches Ruby files in your +Padrino.load_paths+ , Padrino::Application.load_paths
#
def rotation files_for_rotation.uniq.map do |file| file = File.expand_path(file) next if Padrino::Reloader.exclude.any? { |base| file.index(base) == 0 } || !File.exist?(file) yield file, File.mtime(file) end.compact end
def safe_load(file, options={})
A safe Kernel::require which issues the necessary hooks depending on results
#
def safe_load(file, options={}) began_at = Time.now force = options[:force] file = figure_path(file) reload = should_reload?(file) m_time = modification_time(file) return if !force && m_time && !reload remove_loaded_file_classes(file) remove_loaded_file_features(file) # Duplicate objects and loaded features before load file klasses = ObjectSpace.classes files = Set.new($LOADED_FEATURES.dup) reload_deps_of_file(file) # And finally load the specified file begin logger.devel :loading, began_at, file if !reload logger.debug :reload, began_at, file if reload $LOADED_FEATURES.delete(file) if files.include?(file) Padrino::Utils.silence_output loaded = false require(file) loaded = true update_modification_time(file) rescue SyntaxError => e logger.error "Cannot require #{file} due to a syntax error: #{e.message}" ensure Padrino::Utils.unsilence_output new_constants = ObjectSpace.new_classes(klasses) if loaded process_loaded_file(:file => file, :constants => new_constants, :files => files) else logger.devel "Failed to load #{file}; removing partially defined constants" unload_constants(new_constants) end end end
def should_reload?(file)
Check if file was changed or if force a reload
#
def should_reload?(file) MTIMES[file] && File.mtime(file) > MTIMES[file] end
def unload_constants(new_constants)
Unloads all constants in new_constants
##
def unload_constants(new_constants) new_constants.each { |klass| remove_constant(klass) } end
def update_modification_time(file)
Macro for mtime update
##
def update_modification_time(file) MTIMES[file] = File.mtime(file) end