class Camping::Loader::Reloader
The Camping Reloader¶ ↑
Camping apps are generally small and predictable. Many Camping apps are contained within a single file. Larger apps are split into a handful of other Ruby libraries within the same directory.
Since Camping apps (and their dependencies) are loaded with Ruby’s require method, there is a record of them in $LOADED_FEATURES. Which leaves a perfect space for this class to manage auto-reloading an app if any of its immediate dependencies changes.
Wrapping Your Apps¶ ↑
Since bin/camping and the Camping::Server class already use the Reloader, you probably don’t need to hack it on your own. But, if you’re rolling your own situation, here’s how.
Rather than this:
require 'yourapp'
Use this:
require 'camping/reloader' reloader = Camping::Reloader.new('/path/to/yourapp.rb') blog = reloader.apps[:Blog] wiki = reloader.apps[:Wiki]
The blog and wiki objects will behave exactly like your Blog and Wiki, but they will update themselves if yourapp.rb changes.
You can also give Reloader more than one script.
Constants
- Loaders
- Reloader
The
CampingReloader¶ ↑Campingapps are generally small and predictable. ManyCampingapps are contained within a single file. Larger apps are split into a handful of other Ruby libraries within the same directory.Since
Campingapps (and their dependencies) are loaded with Ruby’s require method, there is a record of them in $LOADED_FEATURES. Which leaves a perfect space for this class to manage auto-reloading an app if any of its immediate dependencies changes.Wrapping Your Apps¶ ↑
Since bin/camping and the
Camping::Serverclass already use theReloader, you probably don’t need to hack it on your own. But, if you’re rolling your own situation, here’s how.Rather than this:
require 'yourapp'
Use this:
require 'camping/reloader' reloader = Camping::Reloader.new('/path/to/yourapp.rb') blog = reloader.apps[:Blog] wiki = reloader.apps[:Wiki]
The
blogandwikiobjects will behave exactly like your Blog and Wiki, but they will update themselves if yourapp.rb changes.You can also give
Reloadermore than one script.
Attributes
Public Class Methods
# File lib/camping/loader.rb, line 41 def initialize(file=nil, &blk) @file = file @mtime = Time.at(0) @requires = [] @apps = {} @callback = blk @root = Dir.pwd @file = @root + '/camp.rb' if @file == nil @zeit = Zeitwerk::Loader.new Loaders << @zeit # setup Zeit for this reloader setup_zeit(@zeit) dirs = [@root] dirs << "#{@root}/apps" if Dir.exist? "#{@root}/apps" dirs << "#{@root}/lib" if Dir.exist? "#{@root}/lib" # setup recursive listener on the apps and lib directories from the source script. @listener = Listen.to(*dirs) do |modified, added, removed| @mtime = Time.now reload! end start end
Public Instance Methods
Checks if both scripts watches the same file.
# File lib/camping/loader.rb, line 174 def ==(other) @file == other.file end
# File lib/camping/loader.rb, line 178 def apps if @app { name => @app } else @apps end end
verifies that we can add a directory to the loader. used for testing to prevent multiple loaders from watching the same directory.
# File lib/camping/loader.rb, line 199 def can_add_directory(directory) if Dir.exist?(directory) Loaders.each do |loader| return false if loader.dirs.include? directory end true else false end end
Splits the descendent files and folders found in a given directory for eager loading and recursion. If the given directory doesn’t exist or is empty, then nothing is returned.
# File lib/camping/loader.rb, line 212 def folders_and_files_in(directory) directory = directory + "/*" # unless directory [Dir.glob(directory).select {|f| !File.directory? f }, Dir.glob(directory).select {|f| File.directory? f }] end
Figures out the full path of a required file.
# File lib/camping/loader.rb, line 239 def full_path(req) return req if File.exist?(req) dir = $LOAD_PATH.detect { |l| File.exist?(File.join(l, req)) } if dir File.expand_path(req, File.expand_path(dir)) else req end end
remove_constants called inside this.
# File lib/camping/loader.rb, line 84 def load_everything() all_requires = $LOADED_FEATURES.dup all_apps = Camping::Apps.dup # Zeitwerk will Autoload stuff, which is great. But we don't want Zeitwerk # autoloading when we evaluate the camp.rb file because it will try to # autoload any controllers and helpers that we've defined in there from # the descendant /apps and /lib directory, which then make it break. @zeit.unload load_file @zeit.setup reload_directory("#{@root}/apps") reload_directory("#{@root}/lib") Camping.make_camp ensure @requires = [] new_apps = Camping::Apps - all_apps @apps = new_apps.inject({}) do |hash, app| if file = app.options[:__FILE__] full = File.expand_path(file) @requires << [file, full] end key = app.name.to_sym hash[key] = app if !@apps.include?(key) @callback.call(app) if @callback app.create if app.respond_to?(:create) app.kindling if app.respond_to?(:kindling) end hash end ($LOADED_FEATURES - all_requires).each do |req| full = full_path(req) @requires << [req, full] # if dirs.any? { |x| full.index(x) == 0 } end @mtime = mtime self end
Rack::Builder is mainly used to parse a config.ru file and to build a rack app with middleware from that.
# File lib/camping/loader.rb, line 135 def load_file if @file =~ /\.ru$/ @app = Rack::Builder.parse_file(@file) else load(@file) end @requires << [@file, File.expand_path(@file)] end
# File lib/camping/loader.rb, line 232 def mtime @requires.map do |(path, full)| File.mtime(full) end.reject {|t| t > Time.now }.max || Time.now end
# File lib/camping/loader.rb, line 74 def name @name ||= begin base = @file.dup base = File.dirname(base) if base =~ /\bconfig\.ru$/ base.sub!(/\.[^.]+/, '') File.basename(base).to_sym end end
# File lib/camping/loader.rb, line 71 def pause = @listener.pause def start = @listener.start def name @name ||= begin base = @file.dup base = File.dirname(base) if base =~ /\bconfig\.ru$/ base.sub!(/\.[^.]+/, '') File.basename(base).to_sym end end # remove_constants called inside this. def load_everything() all_requires = $LOADED_FEATURES.dup all_apps = Camping::Apps.dup # Zeitwerk will Autoload stuff, which is great. But we don't want Zeitwerk # autoloading when we evaluate the camp.rb file because it will try to # autoload any controllers and helpers that we've defined in there from # the descendant /apps and /lib directory, which then make it break. @zeit.unload load_file @zeit.setup reload_directory("#{@root}/apps") reload_directory("#{@root}/lib") Camping.make_camp ensure @requires = [] new_apps = Camping::Apps - all_apps @apps = new_apps.inject({}) do |hash, app| if file = app.options[:__FILE__] full = File.expand_path(file) @requires << [file, full] end key = app.name.to_sym hash[key] = app if !@apps.include?(key) @callback.call(app) if @callback app.create if app.respond_to?(:create) app.kindling if app.respond_to?(:kindling) end hash end ($LOADED_FEATURES - all_requires).each do |req| full = full_path(req) @requires << [req, full] # if dirs.any? { |x| full.index(x) == 0 } end @mtime = mtime self end # load_file # # Rack::Builder is mainly used to parse a config.ru file and to # build a rack app with middleware from that. def load_file if @file =~ /\.ru$/ @app = Rack::Builder.parse_file(@file) else load(@file) end @requires << [@file, File.expand_path(@file)] end # removes all constants recursively included using this script as a root. # so everything in /apps, and /lib in relation from this script. def remove_constants @requires.each do |(path, full)| $LOADED_FEATURES.delete(path) unless path.match? "concurrent-ruby" end @apps.each do |name, app| Camping::Apps.delete(app) Object.send :remove_const, name end.dup ensure @apps.clear @requires.clear end # Reloads the file if needed. No harm is done by calling this multiple # times, so feel free call just to be sure. def reload return if @mtime >= mtime rescue nil reload! end # Force a reload. def reload! remove_constants load_everything end # Checks if both scripts watches the same file. def ==(other) @file == other.file end def apps if @app { name => @app } else @apps end end private # sets up Zeit autoloading for the script locations. def setup_zeit(loader) loader.push_dir("#{@root}/apps") if can_add_directory "#{@root}/apps" loader.push_dir("#{@root}/lib") if can_add_directory "#{@root}/lib" loader.ignore("#{@root}/lib/camping-unabridged.rb") loader.enable_reloading if ENV['environment'] == 'development' loader.setup end # verifies that we can add a directory to the loader. # used for testing to prevent multiple loaders from watching the same directory. def can_add_directory(directory) if Dir.exist?(directory) Loaders.each do |loader| return false if loader.dirs.include? directory end true else false end end # Splits the descendent files and folders found in a given directory for eager loading and recursion. # If the given directory doesn't exist or is empty, then nothing is returned. def folders_and_files_in(directory) directory = directory + "/*" # unless directory [Dir.glob(directory).select {|f| !File.directory? f }, Dir.glob(directory).select {|f| File.directory? f }] end # Reloads a directory recursively. loading more shallow files before deeper files. # If the given directory doesn't exist or is empty, then nothing happens. def reload_directory(directory) files, folders = folders_and_files_in(directory) files.each {|file| next if file.include? "unabridged" @requires << [file, File.expand_path(file)] load file } folders.each {|folder| reload_directory folder } end def mtime @requires.map do |(path, full)| File.mtime(full) end.reject {|t| t > Time.now }.max || Time.now end # Figures out the full path of a required file. def full_path(req) return req if File.exist?(req) dir = $LOAD_PATH.detect { |l| File.exist?(File.join(l, req)) } if dir File.expand_path(req, File.expand_path(dir)) else req end end end Reloader = Loader end
pass through methods to the Listener. for testing purposes.
# File lib/camping/loader.rb, line 69 def processing_events? = @listener.processing? def stop = @listener.stop def pause = @listener.pause def start = @listener.start def name @name ||= begin base = @file.dup base = File.dirname(base) if base =~ /\bconfig\.ru$/ base.sub!(/\.[^.]+/, '') File.basename(base).to_sym end end # remove_constants called inside this. def load_everything() all_requires = $LOADED_FEATURES.dup all_apps = Camping::Apps.dup # Zeitwerk will Autoload stuff, which is great. But we don't want Zeitwerk # autoloading when we evaluate the camp.rb file because it will try to # autoload any controllers and helpers that we've defined in there from # the descendant /apps and /lib directory, which then make it break. @zeit.unload load_file @zeit.setup reload_directory("#{@root}/apps") reload_directory("#{@root}/lib") Camping.make_camp ensure @requires = [] new_apps = Camping::Apps - all_apps @apps = new_apps.inject({}) do |hash, app| if file = app.options[:__FILE__] full = File.expand_path(file) @requires << [file, full] end key = app.name.to_sym hash[key] = app if !@apps.include?(key) @callback.call(app) if @callback app.create if app.respond_to?(:create) app.kindling if app.respond_to?(:kindling) end hash end ($LOADED_FEATURES - all_requires).each do |req| full = full_path(req) @requires << [req, full] # if dirs.any? { |x| full.index(x) == 0 } end @mtime = mtime self end # load_file # # Rack::Builder is mainly used to parse a config.ru file and to # build a rack app with middleware from that. def load_file if @file =~ /\.ru$/ @app = Rack::Builder.parse_file(@file) else load(@file) end @requires << [@file, File.expand_path(@file)] end # removes all constants recursively included using this script as a root. # so everything in /apps, and /lib in relation from this script. def remove_constants @requires.each do |(path, full)| $LOADED_FEATURES.delete(path) unless path.match? "concurrent-ruby" end @apps.each do |name, app| Camping::Apps.delete(app) Object.send :remove_const, name end.dup ensure @apps.clear @requires.clear end # Reloads the file if needed. No harm is done by calling this multiple # times, so feel free call just to be sure. def reload return if @mtime >= mtime rescue nil reload! end # Force a reload. def reload! remove_constants load_everything end # Checks if both scripts watches the same file. def ==(other) @file == other.file end def apps if @app { name => @app } else @apps end end private # sets up Zeit autoloading for the script locations. def setup_zeit(loader) loader.push_dir("#{@root}/apps") if can_add_directory "#{@root}/apps" loader.push_dir("#{@root}/lib") if can_add_directory "#{@root}/lib" loader.ignore("#{@root}/lib/camping-unabridged.rb") loader.enable_reloading if ENV['environment'] == 'development' loader.setup end # verifies that we can add a directory to the loader. # used for testing to prevent multiple loaders from watching the same directory. def can_add_directory(directory) if Dir.exist?(directory) Loaders.each do |loader| return false if loader.dirs.include? directory end true else false end end # Splits the descendent files and folders found in a given directory for eager loading and recursion. # If the given directory doesn't exist or is empty, then nothing is returned. def folders_and_files_in(directory) directory = directory + "/*" # unless directory [Dir.glob(directory).select {|f| !File.directory? f }, Dir.glob(directory).select {|f| File.directory? f }] end # Reloads a directory recursively. loading more shallow files before deeper files. # If the given directory doesn't exist or is empty, then nothing happens. def reload_directory(directory) files, folders = folders_and_files_in(directory) files.each {|file| next if file.include? "unabridged" @requires << [file, File.expand_path(file)] load file } folders.each {|folder| reload_directory folder } end def mtime @requires.map do |(path, full)| File.mtime(full) end.reject {|t| t > Time.now }.max || Time.now end # Figures out the full path of a required file. def full_path(req) return req if File.exist?(req) dir = $LOAD_PATH.detect { |l| File.exist?(File.join(l, req)) } if dir File.expand_path(req, File.expand_path(dir)) else req end end end Reloader =
Reloads the file if needed. No harm is done by calling this multiple times, so feel free call just to be sure.
# File lib/camping/loader.rb, line 162 def reload return if @mtime >= mtime rescue nil reload! end
Force a reload.
# File lib/camping/loader.rb, line 168 def reload! remove_constants load_everything end
Reloads a directory recursively. loading more shallow files before deeper files. If the given directory doesn’t exist or is empty, then nothing happens.
# File lib/camping/loader.rb, line 220 def reload_directory(directory) files, folders = folders_and_files_in(directory) files.each {|file| next if file.include? "unabridged" @requires << [file, File.expand_path(file)] load file } folders.each {|folder| reload_directory folder } end
removes all constants recursively included using this script as a root. so everything in /apps, and /lib in relation from this script.
# File lib/camping/loader.rb, line 146 def remove_constants @requires.each do |(path, full)| $LOADED_FEATURES.delete(path) unless path.match? "concurrent-ruby" end @apps.each do |name, app| Camping::Apps.delete(app) Object.send :remove_const, name end.dup ensure @apps.clear @requires.clear end
sets up Zeit autoloading for the script locations.
# File lib/camping/loader.rb, line 189 def setup_zeit(loader) loader.push_dir("#{@root}/apps") if can_add_directory "#{@root}/apps" loader.push_dir("#{@root}/lib") if can_add_directory "#{@root}/lib" loader.ignore("#{@root}/lib/camping-unabridged.rb") loader.enable_reloading if ENV['environment'] == 'development' loader.setup end
# File lib/camping/loader.rb, line 72 def start = @listener.start def name @name ||= begin base = @file.dup base = File.dirname(base) if base =~ /\bconfig\.ru$/ base.sub!(/\.[^.]+/, '') File.basename(base).to_sym end end # remove_constants called inside this. def load_everything() all_requires = $LOADED_FEATURES.dup all_apps = Camping::Apps.dup # Zeitwerk will Autoload stuff, which is great. But we don't want Zeitwerk # autoloading when we evaluate the camp.rb file because it will try to # autoload any controllers and helpers that we've defined in there from # the descendant /apps and /lib directory, which then make it break. @zeit.unload load_file @zeit.setup reload_directory("#{@root}/apps") reload_directory("#{@root}/lib") Camping.make_camp ensure @requires = [] new_apps = Camping::Apps - all_apps @apps = new_apps.inject({}) do |hash, app| if file = app.options[:__FILE__] full = File.expand_path(file) @requires << [file, full] end key = app.name.to_sym hash[key] = app if !@apps.include?(key) @callback.call(app) if @callback app.create if app.respond_to?(:create) app.kindling if app.respond_to?(:kindling) end hash end ($LOADED_FEATURES - all_requires).each do |req| full = full_path(req) @requires << [req, full] # if dirs.any? { |x| full.index(x) == 0 } end @mtime = mtime self end # load_file # # Rack::Builder is mainly used to parse a config.ru file and to # build a rack app with middleware from that. def load_file if @file =~ /\.ru$/ @app = Rack::Builder.parse_file(@file) else load(@file) end @requires << [@file, File.expand_path(@file)] end # removes all constants recursively included using this script as a root. # so everything in /apps, and /lib in relation from this script. def remove_constants @requires.each do |(path, full)| $LOADED_FEATURES.delete(path) unless path.match? "concurrent-ruby" end @apps.each do |name, app| Camping::Apps.delete(app) Object.send :remove_const, name end.dup ensure @apps.clear @requires.clear end # Reloads the file if needed. No harm is done by calling this multiple # times, so feel free call just to be sure. def reload return if @mtime >= mtime rescue nil reload! end # Force a reload. def reload! remove_constants load_everything end # Checks if both scripts watches the same file. def ==(other) @file == other.file end def apps if @app { name => @app } else @apps end end private # sets up Zeit autoloading for the script locations. def setup_zeit(loader) loader.push_dir("#{@root}/apps") if can_add_directory "#{@root}/apps" loader.push_dir("#{@root}/lib") if can_add_directory "#{@root}/lib" loader.ignore("#{@root}/lib/camping-unabridged.rb") loader.enable_reloading if ENV['environment'] == 'development' loader.setup end # verifies that we can add a directory to the loader. # used for testing to prevent multiple loaders from watching the same directory. def can_add_directory(directory) if Dir.exist?(directory) Loaders.each do |loader| return false if loader.dirs.include? directory end true else false end end # Splits the descendent files and folders found in a given directory for eager loading and recursion. # If the given directory doesn't exist or is empty, then nothing is returned. def folders_and_files_in(directory) directory = directory + "/*" # unless directory [Dir.glob(directory).select {|f| !File.directory? f }, Dir.glob(directory).select {|f| File.directory? f }] end # Reloads a directory recursively. loading more shallow files before deeper files. # If the given directory doesn't exist or is empty, then nothing happens. def reload_directory(directory) files, folders = folders_and_files_in(directory) files.each {|file| next if file.include? "unabridged" @requires << [file, File.expand_path(file)] load file } folders.each {|folder| reload_directory folder } end def mtime @requires.map do |(path, full)| File.mtime(full) end.reject {|t| t > Time.now }.max || Time.now end # Figures out the full path of a required file. def full_path(req) return req if File.exist?(req) dir = $LOAD_PATH.detect { |l| File.exist?(File.join(l, req)) } if dir File.expand_path(req, File.expand_path(dir)) else req end end end
# File lib/camping/loader.rb, line 70 def stop = @listener.stop def pause = @listener.pause def start = @listener.start def name @name ||= begin base = @file.dup base = File.dirname(base) if base =~ /\bconfig\.ru$/ base.sub!(/\.[^.]+/, '') File.basename(base).to_sym end end # remove_constants called inside this. def load_everything() all_requires = $LOADED_FEATURES.dup all_apps = Camping::Apps.dup # Zeitwerk will Autoload stuff, which is great. But we don't want Zeitwerk # autoloading when we evaluate the camp.rb file because it will try to # autoload any controllers and helpers that we've defined in there from # the descendant /apps and /lib directory, which then make it break. @zeit.unload load_file @zeit.setup reload_directory("#{@root}/apps") reload_directory("#{@root}/lib") Camping.make_camp ensure @requires = [] new_apps = Camping::Apps - all_apps @apps = new_apps.inject({}) do |hash, app| if file = app.options[:__FILE__] full = File.expand_path(file) @requires << [file, full] end key = app.name.to_sym hash[key] = app if !@apps.include?(key) @callback.call(app) if @callback app.create if app.respond_to?(:create) app.kindling if app.respond_to?(:kindling) end hash end ($LOADED_FEATURES - all_requires).each do |req| full = full_path(req) @requires << [req, full] # if dirs.any? { |x| full.index(x) == 0 } end @mtime = mtime self end # load_file # # Rack::Builder is mainly used to parse a config.ru file and to # build a rack app with middleware from that. def load_file if @file =~ /\.ru$/ @app = Rack::Builder.parse_file(@file) else load(@file) end @requires << [@file, File.expand_path(@file)] end # removes all constants recursively included using this script as a root. # so everything in /apps, and /lib in relation from this script. def remove_constants @requires.each do |(path, full)| $LOADED_FEATURES.delete(path) unless path.match? "concurrent-ruby" end @apps.each do |name, app| Camping::Apps.delete(app) Object.send :remove_const, name end.dup ensure @apps.clear @requires.clear end # Reloads the file if needed. No harm is done by calling this multiple # times, so feel free call just to be sure. def reload return if @mtime >= mtime rescue nil reload! end # Force a reload. def reload! remove_constants load_everything end # Checks if both scripts watches the same file. def ==(other) @file == other.file end def apps if @app { name => @app } else @apps end end private # sets up Zeit autoloading for the script locations. def setup_zeit(loader) loader.push_dir("#{@root}/apps") if can_add_directory "#{@root}/apps" loader.push_dir("#{@root}/lib") if can_add_directory "#{@root}/lib" loader.ignore("#{@root}/lib/camping-unabridged.rb") loader.enable_reloading if ENV['environment'] == 'development' loader.setup end # verifies that we can add a directory to the loader. # used for testing to prevent multiple loaders from watching the same directory. def can_add_directory(directory) if Dir.exist?(directory) Loaders.each do |loader| return false if loader.dirs.include? directory end true else false end end # Splits the descendent files and folders found in a given directory for eager loading and recursion. # If the given directory doesn't exist or is empty, then nothing is returned. def folders_and_files_in(directory) directory = directory + "/*" # unless directory [Dir.glob(directory).select {|f| !File.directory? f }, Dir.glob(directory).select {|f| File.directory? f }] end # Reloads a directory recursively. loading more shallow files before deeper files. # If the given directory doesn't exist or is empty, then nothing happens. def reload_directory(directory) files, folders = folders_and_files_in(directory) files.each {|file| next if file.include? "unabridged" @requires << [file, File.expand_path(file)] load file } folders.each {|folder| reload_directory folder } end def mtime @requires.map do |(path, full)| File.mtime(full) end.reject {|t| t > Time.now }.max || Time.now end # Figures out the full path of a required file. def full_path(req) return req if File.exist?(req) dir = $LOAD_PATH.detect { |l| File.exist?(File.join(l, req)) } if dir File.expand_path(req, File.expand_path(dir)) else req end end end Reloader = Loader