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.4.1'

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
# 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!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.


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

def async_require!
  @require_lock = nil
end

#force_next_reloadObject


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

def force_next_reload
  @force_reload = true
end

#maybe_synchronize(&block) ⇒ Object


120
121
122
# 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) ⇒ Object


129
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
# 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

#require(path, &block) ⇒ Object


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


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

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

#stop_listenerObject


169
170
171
# File 'lib/auto_reloader.rb', line 169

def stop_listener
  @listener.stop if @listener
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.


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

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

#unload!Object


159
160
161
162
163
164
165
166
167
# 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