class AutoReloader

Constants

ActivatedMoreThanOnce
InvalidUsage
VERSION

Attributes

default_await_before_unload[R]

default_await_before_unload will await for all calls to reload! to finish before calling unload!. This behavior is usually desired in web applications to avoid unloading anything while a request hasn’t been finished, however it won’t work fine if some requests are supposed to remain open, like websockets connections or something like that.

default_delay[R]

default_await_before_unload will await for all calls to reload! to finish before calling unload!. This behavior is usually desired in web applications to avoid unloading anything while a request hasn’t been finished, however it won’t work fine if some requests are supposed to remain open, like websockets connections or something like that.

default_onchange[R]

default_await_before_unload will await for all calls to reload! to finish before calling unload!. This behavior is usually desired in web applications to avoid unloading anything while a request hasn’t been finished, however it won’t work fine if some requests are supposed to remain open, like websockets connections or something like that.

reloadable_paths[R]

default_await_before_unload will await for all calls to reload! to finish before calling unload!. This behavior is usually desired in web applications to avoid unloading anything while a request hasn’t been finished, however it won’t work fine if some requests are supposed to remain open, like websockets connections or something like that.

Public Class Methods

new() click to toggle source
# File lib/auto_reloader.rb, line 37
def initialize
  @activate_lock = Mutex.new
end

Public Instance Methods

activate(reloadable_paths: [], onchange: true, delay: nil, watch_paths: nil, watch_latency: 1, sync_require: false, await_before_unload: true) click to toggle source
# File lib/auto_reloader.rb, line 42
def activate(reloadable_paths: [], onchange: true, delay: nil, watch_paths: nil,
             watch_latency: 1, sync_require: false, await_before_unload: true)
  @activate_lock.synchronize do
    raise ActivatedMoreThanOnce, 'Can only activate Autoreloader once' if @reloadable_paths
    @default_delay = delay
    @default_onchange = onchange
    @default_await_before_unload = await_before_unload
    @watch_latency = watch_latency
    sync_require! if sync_require
    @reload_lock = Mutex.new
    @zero_requests_condition = ConditionVariable.new
    @requests_count = 0
    @top_level_consts_stack = []
    @unload_constants = Set.new
    @unload_files = Set.new
    @last_reloaded = clock_time
    try_listen unless watch_paths == false
    self.reloadable_paths = reloadable_paths
    Object.include RequireOverride
  end
end
async_require!() click to toggle source

See the documentation for sync_require! to understand the reasoning. Async require is the default behavior but could lead to race conditions. If you know your requires will never block it may be a good idea to call sync_require!. If you know what require will block you can call async_require!, require it, and then call sync_require! which will generate a new monitor.

# File lib/auto_reloader.rb, line 79
def async_require!
  @require_lock = nil
end
force_next_reload() click to toggle source
# File lib/auto_reloader.rb, line 173
def force_next_reload
  @force_reload = true
end
maybe_synchronize() { || ... } click to toggle source
# File lib/auto_reloader.rb, line 120
def maybe_synchronize(&block)
  @require_lock ? @require_lock.synchronize(&block) : yield
end
reload!(delay: default_delay, onchange: default_onchange, watch_paths: @watch_paths, await_before_unload: default_await_before_unload) { |!reload_ignored| ... } click to toggle source
# File lib/auto_reloader.rb, line 129
def reload!(delay: default_delay, onchange: default_onchange, watch_paths: @watch_paths,
            await_before_unload: default_await_before_unload)
  if onchange && !block_given?
    raise InvalidUsage, 'A block must be provided to reload! when onchange is true (the default)'
  end

  unless reload_ignored = ignore_reload?(delay, onchange, watch_paths)
    @reload_lock.synchronize do
      @zero_requests_condition.wait(@reload_lock) unless @requests_count == 0
    end if await_before_unload && block_given?
    unload!
  end

  result = nil
  if block_given?
    @reload_lock.synchronize{ @requests_count += 1 }
    begin
      result = yield !reload_ignored
    ensure
      @reload_lock.synchronize{
        @requests_count -= 1
        @zero_requests_condition.signal if @requests_count == 0
      }
    end
    find_mtime
  end
  @last_reloaded = clock_time if delay
  result
end
reloadable_paths=(paths) click to toggle source
# File lib/auto_reloader.rb, line 83
def reloadable_paths=(paths)
  @reloadable_paths = paths.map{|rp| File.expand_path(rp).freeze }.freeze
  setup_listener if @watch_paths
end
require(path) { || ... } click to toggle source
# File lib/auto_reloader.rb, line 88
def require(path, &block)
  was_required = false
  error = nil
  maybe_synchronize do
    @top_level_consts_stack << Set.new
    old_consts = Object.constants
    prev_consts = new_top_level_constants = nil
    begin
      was_required = yield
    rescue Exception => e
      error = e
    ensure
      prev_consts = @top_level_consts_stack.pop
      return false if !error && !was_required # was required already, do nothing

      new_top_level_constants = Object.constants - old_consts - prev_consts.to_a

      (new_top_level_constants.each{|c| safe_remove_constant c }; raise error) if error

      @top_level_consts_stack.each{|c| c.merge new_top_level_constants }

      full_loaded_path = $LOADED_FEATURES.last
      return was_required unless reloadable? full_loaded_path, path
      @reload_lock.synchronize do
        @unload_constants.merge new_top_level_constants
        @unload_files << full_loaded_path
      end
    end
  end
  was_required
end
require_relative(path, fullpath) click to toggle source
# File lib/auto_reloader.rb, line 124
def require_relative(path, fullpath)
  require(fullpath){ Kernel.require fullpath }
end
stop_listener() click to toggle source
# File lib/auto_reloader.rb, line 169
def stop_listener
  @listener.stop if @listener
end
sync_require!() click to toggle source

when concurrent threads require files race conditions may prevent the automatic detection of constants created by a given file. Calling sync_require! will ensure only a single file is required at a single time. However, if a required file blocks (think of a web server) then any requires by a separate thread would be blocked forever (or until the web server shutdowns). That’s why require is async by default even though it would be vulnerable to race conditions.

# File lib/auto_reloader.rb, line 70
def sync_require!
  @require_lock ||= Monitor.new # monitor is like Mutex, but reentrant
end
unload!() click to toggle source
# File lib/auto_reloader.rb, line 159
def unload!
  @force_reload = false
  @reload_lock.synchronize do
    @unload_files.each{|f| $LOADED_FEATURES.delete f }
    @unload_constants.each{|c| safe_remove_constant c }
    @unload_files = Set.new
    @unload_constants = Set.new
  end
end

Private Instance Methods

changed?(watch_paths = @watch_paths) click to toggle source
# File lib/auto_reloader.rb, line 213
def changed?(watch_paths = @watch_paths)
  return false if watch_paths && !@paths_changed
  @paths_changed = false
  return true unless @last_mtime_by_path
  @reload_lock.synchronize do
    return @unload_files.any?{|f| @last_mtime_by_path[f] != safe_mtime(f) }
  end
end
clock_time() click to toggle source
# File lib/auto_reloader.rb, line 195
def clock_time
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
find_mtime() click to toggle source
# File lib/auto_reloader.rb, line 226
def find_mtime
  @reload_lock.synchronize do
    @last_mtime_by_path = {}
    @unload_files.each{|f| @last_mtime_by_path[f] = safe_mtime f }
  end
  @last_mtime_by_path
end
ignore_reload?(delay, onchange, watch_paths = @watch_paths) click to toggle source
# File lib/auto_reloader.rb, line 208
def ignore_reload?(delay, onchange, watch_paths = @watch_paths)
  return false if @force_reload
  (delay && (clock_time - @last_reloaded < delay)) || (onchange && !changed?(watch_paths))
end
reloadable?(fullpath, path) click to toggle source
# File lib/auto_reloader.rb, line 204
def reloadable?(fullpath, path)
  @reloadable_paths.any?{|rp| fullpath.start_with? rp}
end
safe_mtime(path) click to toggle source
# File lib/auto_reloader.rb, line 222
def safe_mtime(path)
  File.mtime(path) if File.exist?(path)
end
safe_remove_constant(constant) click to toggle source
# File lib/auto_reloader.rb, line 234
def safe_remove_constant(constant)
  Object.send :remove_const, constant
rescue NameError # ignore if it has been already removed
end
setup_listener() click to toggle source
# File lib/auto_reloader.rb, line 186
def setup_listener
  @listener.stop if @listener
  @listener = Listen.to(*@reloadable_paths, latency: @watch_latency) do |m, a, r|
    @paths_changed = [m, a, r].any?{|o| o.any? {|f| reloadable?(f, nil) }}
  end
  @listener.start
end
try_listen() click to toggle source
# File lib/auto_reloader.rb, line 179
def try_listen
  Kernel.require 'listen'
  @watch_paths = true
rescue LoadError # ignore
  #puts 'listen is not available. Add it to Gemfile if you want to speed up change detection.'
end