Class: Rack::Unreloader

Inherits:
Object
  • Object
show all
Defined in:
lib/rack/unreloader.rb,
lib/rack/unreloader/reloader.rb,
lib/rack/unreloader/autoload_reloader.rb

Overview

Reloading application that unloads constants before reloading the relevant files, calling the new rack app if it gets reloaded.

Defined Under Namespace

Classes: AutoloadReloader, Reloader

Constant Summary collapse

MUTEX =

Mutex used to synchronize reloads

Monitor.new
File =

Reference to ::File as File may return Rack::File by default.

::File
VALID_CONSTANT_NAME_REGEXP =

Regexp for valid constant names, to prevent code execution.

/\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}, &block) ⇒ Unreloader

Setup the reloader. Options:

:autoload

Whether to allow autoloading. If not set to true, calls to autoload will eagerly require the related files instead of autoloading.

:cooldown

The number of seconds to wait between checks for changed files. Defaults to 1. Set to nil/false to not check for changed files.

:handle_reload_errors

Whether reload to handle reload errors by returning a 500 plain text response with the backtrace.

:reload

Set to false to not setup a reloader, and just have require work directly. Should be set to false in production mode.

:logger

A Logger instance which will log information related to reloading.

:subclasses

A string or array of strings of class names that should be unloaded. Any classes that are not subclasses of these classes will not be unloaded. This also handles modules, but module names given must match exactly, since modules don’t have superclasses.



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/rack/unreloader.rb', line 103

def initialize(opts={}, &block)
  @app_block = block
  @autoload = opts[:autoload]
  @logger = opts[:logger]
  if opts.fetch(:reload, true)
    @cooldown = opts.fetch(:cooldown, 1)
    @handle_reload_errors = opts[:handle_reload_errors]
    @last = Time.at(0)
    if @autoload
      require_relative('unreloader/autoload_reloader')
      @reloader = AutoloadReloader.new(opts)
    else
      require_relative('unreloader/reloader')
      @reloader = Reloader.new(opts)
    end
    reload!
  else
    @reloader = @cooldown = @handle_reload_errors = false
  end
end

Instance Attribute Details

#reloaderObject (readonly)

The Rack::Unreloader::Reloader instead related to this instance, if one.



86
87
88
# File 'lib/rack/unreloader.rb', line 86

def reloader
  @reloader
end

Class Method Details

.autoload_constants(objs, file, logger) ⇒ Object

Autoload the file for the given objects. objs should be a string, symbol, or array of them holding a Ruby constant name. Access to the constant will load the related file. A non-nil logger will have output logged to it.



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/rack/unreloader.rb', line 50

def self.autoload_constants(objs, file, logger)
  strings = Array(objs).map(&:to_s)
  if strings.empty?
    # Remove file from $LOADED_FEATURES if there are no constants to autoload.
    # In general that is because the file is part of another class that will
    # handle loading the file separately, and if that class is reloaded, we
    # want to remove the loaded feature so the file can get loaded again.
    $LOADED_FEATURES.delete(file)
  else
    logger.info("Setting up autoload for #{file}: #{strings.join(' ')}") if logger
    strings.each do |s|
      obj, mod = split_autoload(s)

      if obj
        obj.autoload(mod, file)
      elsif logger
        logger.info("Invalid constant name: #{s}")
      end
    end
  end
end

.expand_directory_paths(paths) ⇒ Object

Given the list of paths, find all matching files, or matching ruby files in subdirecories if given a directory, and return an array of expanded paths.



20
21
22
23
24
25
# File 'lib/rack/unreloader.rb', line 20

def self.expand_directory_paths(paths)
  paths = expand_paths(paths)
  paths.map!{|f| File.directory?(f) ? ruby_files(f) : f}
  paths.flatten!
  paths
end

.expand_paths(paths) ⇒ Object

Given the path glob or array of path globs, find all matching files or directories, and return an array of expanded paths.



29
30
31
32
33
34
35
36
# File 'lib/rack/unreloader.rb', line 29

def self.expand_paths(paths)
  paths = Array(paths).flatten
  paths.map!{|path| Dir.glob(path).sort_by!{|filename| filename.count('/')}}
  paths.flatten!
  paths.map!{|path| File.expand_path(path)}
  paths.uniq!
  paths
end

.ruby_files(dir) ⇒ Object

The .rb files in the given directory or any subdirectory.



39
40
41
42
43
44
45
# File 'lib/rack/unreloader.rb', line 39

def self.ruby_files(dir)
  files = []
  Find.find(dir) do |f|
    files << f if f =~ /\.rb\z/
  end
  files.sort
end

.split_autoload(mod_string) ⇒ Object

Split the given string into an array. The first is a module/class to add the autoload to, and the second is the name of the constant to be autoloaded.



74
75
76
77
78
79
80
81
82
83
# File 'lib/rack/unreloader.rb', line 74

def self.split_autoload(mod_string)
  if m = VALID_CONSTANT_NAME_REGEXP.match(mod_string)
    ns, sep, mod = m[1].rpartition('::')
    if sep.empty?
      [Object, mod]
    else
      [Object.module_eval("::#{ns}", __FILE__, __LINE__), mod]
    end
  end
end

Instance Method Details

#autoload(paths, opts = {}, &block) ⇒ Object

Add a file glob or array of file global to autoload and monitor for changes. A block is required. It will be called with the path to be autoloaded, and should return the symbol for the constant name to autoload. Accepts the same options as #require.

Raises:

  • (ArgumentError)


168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/rack/unreloader.rb', line 168

def autoload(paths, opts={}, &block)
  raise ArgumentError, "block required" unless block

  if @autoload
    if @reloader
      @reloader.autoload_dependencies(paths, opts, &block)
    else
      Unreloader.expand_directory_paths(paths).each{|f| Unreloader.autoload_constants(yield(f), f, @logger)}
    end
  else
    require(paths, opts, &block)
  end
end

#autoload?Boolean

Whether the unreloader is setup for autoloading. If false, autoloads are treated as requires.

Returns:

  • (Boolean)


148
149
150
# File 'lib/rack/unreloader.rb', line 148

def autoload?
  !!@autoload
end

#call(env) ⇒ Object

If the cooldown time has been passed, reload any application files that have changed. Call the app with the environment.



126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/rack/unreloader.rb', line 126

def call(env)
  if @cooldown && Time.now > @last + @cooldown
    begin
      MUTEX.synchronize{reload!}
    rescue StandardError, ScriptError => e
      raise unless @handle_reload_errors
      content = "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
      return [500, {'Content-Type' => 'text/plain', 'Content-Length' => content.bytesize.to_s}, [content]]
    end
    @last = Time.now
  end
  @app_block.call.call(env)
end

#record_dependency(dependency, *files) ⇒ Object

Records that each path in files depends on dependency. If there is a modification to dependency, all related files will be reloaded after dependency is reloaded. Both dependency and each entry in files can be an array of path globs.



186
187
188
189
190
191
192
193
# File 'lib/rack/unreloader.rb', line 186

def record_dependency(dependency, *files)
  if @reloader
    files = Unreloader.expand_paths(files)
    Unreloader.expand_paths(dependency).each do |path|
      @reloader.record_dependency(path, files)
    end
  end
end

#record_split_class(main_file, *files) ⇒ Object

Record that a class is split into multiple files. main_file should be the main file for the class, which should require all of the other files. files should be a list of all other files that make up the class.



198
199
200
201
202
203
204
205
206
# File 'lib/rack/unreloader.rb', line 198

def record_split_class(main_file, *files)
  if @reloader
    files = Unreloader.expand_paths(files)
    files.each do |file|
      record_dependency(file, main_file)
    end
    @reloader.skip_reload(files)
  end
end

#reload!Object

Reload the application, checking for changed files and reloading them.



209
210
211
# File 'lib/rack/unreloader.rb', line 209

def reload!
  @reloader.reload! if @reloader
end

#reload?Boolean

Whether the unreloader is setup for reloading. If false, no reloading is done after the initial require.

Returns:

  • (Boolean)


142
143
144
# File 'lib/rack/unreloader.rb', line 142

def reload?
  !!@reloader
end

#require(paths, opts = {}, &block) ⇒ Object

Add a file glob or array of file globs to monitor for changes. Options:

:delete_hook

When a file being monitored is deleted, call this hook with the path of the deleted file.



156
157
158
159
160
161
162
# File 'lib/rack/unreloader.rb', line 156

def require(paths, opts={}, &block)
  if @reloader
    @reloader.require_dependencies(paths, opts, &block)
  else
    Unreloader.expand_directory_paths(paths).each{|f| super(f)}
  end
end