# Copyright (c) 2008 Michael Fellinger m.fellinger@gmail.com # All files in this distribution are subject to the terms of the Ruby license. module Ramaze # High performant source reloader # # This class acts as Rack middleware. # # It does not depend on Ramaze itself, but you might have to adjust the # Reloader::Hooks module or include your own module to override the hooks. # You also might have to set the Log constant. # # Currently, it uses RInotify if available and falls back to using File.stat. # # Please note that this will not reload files in the background, it does so # only when actively called # In case of Ramaze it is performing a check/reload cycle at the start of # every request, but also respects a cool down time, during which nothing will # be done. # # After every reload the OPTIONS hash will be checked for changed options and # assigned to the instance, so you may change options during the lifetime of # your application. # # A number of hooks will be executed during the reload cycle, see # Ramaze::ReloaderHooks for more information. class Reloader OPTIONS = { # At most check every n seconds # nil/false will never trigger the reload cycle # 0 will cycle on every call :cooldown => 2, # Compiled files cannot be reloaded during runtime :ignore => /\.so$/, # Run cycle in a Thread.exclusive, by default no threads are used. :thread => false, # If you assign a block here it will be instance_evaled instead of # calling cycle. This allows you to use for example EventMachine for # well performing asynchronous cycling. :control => nil, # lambda{ cycle }, } begin begin gem('RInotify', '>=0.9') # is older version ok? rescue NoMethodError # Kernel::gem might simply be not available end require 'rinotify' require 'ramaze/reloader/watch_inotify' Watcher = WatchInotify rescue LoadError # stat always available require 'ramaze/reloader/watch_stat' Watcher = WatchStat end def initialize(app) @app = app @files = {} @watcher = Watcher.new options_reload end def options_reload @cooldown, @ignore, @control, @thread = OPTIONS.values_at(:cooldown, :ignore, :control, :thread) end def call(env) options_reload @watcher.call(@cooldown) do if @control instance_eval(&@control) elsif @thread Thread.exclusive{ cycle } else cycle end end @app.call(env) end def cycle before_cycle rotation{|file| @watcher.watch(file) } @watcher.changed_files{|f| safe_load(f) } after_cycle end # A safe Kernel::load, issuing the hooks depending on the results def safe_load(file) before_safe_load(file) load(file) after_safe_load_succeed(file) rescue Object => ex Log.error(ex) after_safe_load_failed(file, ex) end def rotation files = [$0, __FILE__, *$LOADED_FEATURES].uniq paths = ['./', *$LOAD_PATH].uniq files.each do |file| next if file =~ @ignore if not @files.has_key?(file) and path = figure_path(file, paths) @files[file] = path yield path end end end def figure_path(file, paths) if Pathname.new(file).absolute? return File.exist?(file) ? file : nil end paths.each do |possible_path| full_path = File.join(possible_path, file) return full_path if File.exist?(full_path) end nil end # Holds hooks that are called before and after #cycle and #safe_load module Hooks # Overwrite to add actions before the reload rotation is started. def before_cycle end # Overwrite to add actions after the reload rotation has ended. def after_cycle end # Overwrite to add actions before a file is Kernel::load-ed def before_safe_load(file) Log.debug("reload #{file}") end # Overwrite to add actions after a file is Kernel::load-ed successfully, # by default we clean the Cache for compiled templates and resolved actions. def after_safe_load_succeed(file) Cache.clear_after_reload after_safe_load(file) end # Overwrite to add custom hook in addition to default Cache cleaning def after_safe_load(file) end # Overwrite to add actions after a file is Kernel::load-ed unsuccessfully, # by default we output an error-message with the exception. def after_safe_load_failed(file, error) Log.error(error) end end include Hooks end end