Class: AutoReloader

Inherits:
Object
  • Object
show all
Extended by:
SingleForwardable
Includes:
Singleton
Defined in:
lib/auto_reloader.rb,
lib/auto_reloader/version.rb

Defined Under Namespace

Modules: RequireOverride

Constant Summary collapse

ActivatedMoreThanOnce =
Class.new RuntimeError
InvalidUsage =
Class.new RuntimeError
VERSION =
'0.5.0'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeAutoReloader

Returns a new instance of AutoReloader.



37
38
39
# File 'lib/auto_reloader.rb', line 37

def initialize
  @activate_lock = Mutex.new
end

Instance Attribute Details

#default_await_before_unloadObject (readonly)

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.



20
21
22
# File 'lib/auto_reloader.rb', line 20

def default_await_before_unload
  @default_await_before_unload
end

#default_delayObject (readonly)

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.



20
21
22
# File 'lib/auto_reloader.rb', line 20

def default_delay
  @default_delay
end

#default_onchangeObject (readonly)

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.



20
21
22
# File 'lib/auto_reloader.rb', line 20

def default_onchange
  @default_onchange
end

#reloadable_pathsObject

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.



20
21
22
# File 'lib/auto_reloader.rb', line 20

def reloadable_paths
  @reloadable_paths
end

Instance Method Details

#activate(reloadable_paths: [], onchange: true, delay: nil, watch_paths: nil, watch_latency: 1, sync_require: false, await_before_unload: true) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# 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
    @unload_hooks = []
    @last_reloaded = clock_time
    try_listen unless watch_paths == false
    self.reloadable_paths = reloadable_paths
    Object.include RequireOverride
  end
end

#async_require!Object

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.



80
81
82
# File 'lib/auto_reloader.rb', line 80

def async_require!
  @require_lock = nil
end

#force_next_reloadObject



192
193
194
# File 'lib/auto_reloader.rb', line 192

def force_next_reload
  @force_reload = true
end

#maybe_synchronize(&block) ⇒ Object



121
122
123
# File 'lib/auto_reloader.rb', line 121

def maybe_synchronize(&block)
  @require_lock ? @require_lock.synchronize(&block) : yield
end

#register_unload_hook(&hook) ⇒ Object

Raises:



172
173
174
175
176
177
# File 'lib/auto_reloader.rb', line 172

def register_unload_hook(&hook)
  raise InvalidUsage, "A block is required for register_unload_hook" unless block_given?
  @reload_lock.synchronize do
    @unload_hooks << hook
  end
end

#reload!(delay: default_delay, onchange: default_onchange, watch_paths: @watch_paths, await_before_unload: default_await_before_unload) ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/auto_reloader.rb', line 130

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

#require(path, &block) ⇒ Object



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/auto_reloader.rb', line 89

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) ⇒ Object



125
126
127
# File 'lib/auto_reloader.rb', line 125

def require_relative(path, fullpath)
  require(fullpath){ Kernel.require fullpath }
end

#run_unload_hook(hook) ⇒ Object



179
180
181
182
183
# File 'lib/auto_reloader.rb', line 179

def run_unload_hook(hook)
  hook.call
rescue => e
  puts "Failed to run unload hook in AutoReloader: #{e.message}.\n\n#{e.backtrace.join("\n")}"
end

#stop_listenerObject



185
186
187
188
189
190
# File 'lib/auto_reloader.rb', line 185

def stop_listener
  @reload_lock.synchronize do
    @listener.stop if @listener
    @listener = nil
  end
end

#sync_require!Object

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.



71
72
73
# File 'lib/auto_reloader.rb', line 71

def sync_require!
  @require_lock ||= Monitor.new # monitor is like Mutex, but reentrant
end

#unload!Object



160
161
162
163
164
165
166
167
168
169
170
# File 'lib/auto_reloader.rb', line 160

def unload!
  @force_reload = false
  @reload_lock.synchronize do
    @unload_hooks.reverse_each{|h| run_unload_hook h }
    @unload_files.each{|f| $LOADED_FEATURES.delete f }
    @unload_constants.each{|c| safe_remove_constant c }
    @unload_hooks = []
    @unload_files = Set.new
    @unload_constants = Set.new
  end
end