Class: Autorespawn

Inherits:
Object
  • Object
show all
Includes:
Hooks, Hooks::InstanceHooks
Defined in:
lib/autorespawn.rb,
lib/autorespawn/self.rb,
lib/autorespawn/hooks.rb,
lib/autorespawn/slave.rb,
lib/autorespawn/watch.rb,
lib/autorespawn/manager.rb,
lib/autorespawn/version.rb,
lib/autorespawn/exceptions.rb,
lib/autorespawn/program_id.rb,
lib/autorespawn/tracked_file.rb

Overview

Automatically exec’s the current program when one of the source file changes

The exec is done at very-well defined points to avoid weird state, and it is possible to define cleanup handlers beyond Ruby’s at_exit mechanism

Call this method from the entry point of your program, giving it the actual program functionality as a block. The method will exec and spawn subprocesses at will, when needed, and call the block in these subprocesses as required.

At the point of call, all of the program’s dependencies must be already required, as it is on this basis that the auto-reloading will be done

This method does NOT return

Defined Under Namespace

Modules: Hooks Classes: AlreadyRunning, FileNotFound, Manager, NotFinished, NotSlave, ProgramID, Self, Slave, TrackedFile, Watch

Constant Summary collapse

INITIAL_STATE_FD =
"AUTORESPAWN_AUTORELOAD"
SLAVE_RESULT_ENV =
'AUTORESPAWN_SLAVE_RESULT_FD'
SLAVE_INITIAL_STATE_ENV =
'AUTORESPAWN_SLAVE_INITIAL_STATE_FD'
VERSION =
"0.5.1"

Instance Attribute Summary collapse

Hooks collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Hooks

included

Constructor Details

#initialize(*command, name: Autorespawn.name, track_current: false, **options) ⇒ Autorespawn

Returns a new instance of Autorespawn.



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/autorespawn.rb', line 132

def initialize(*command, name: Autorespawn.name, track_current: false, **options)
    if command.empty?
        command = [$0, *ARGV]
    end
    @name = name
    @program_id = Autorespawn.initial_program_id ||
        ProgramID.new

    @process_command_line = [command, options]
    @exceptions = Array.new
    @required_paths = Set.new
    @error_paths = Set.new
    @subcommands = Array.new
    @exit_code = 0
    if track_current
        @required_paths = currently_loaded_files.to_set
    end
end

Instance Attribute Details

#error_pathsSet<Pathname> (readonly)

Set of paths that are part of an error backtrace

This is updated in #requires or #require

Returns:

  • (Set<Pathname>)


127
128
129
# File 'lib/autorespawn.rb', line 127

def error_paths
  @error_paths
end

#exceptionsArray<Exception> (readonly)

Returns exceptions received in a #requires block or in a file required with #require.

Returns:

  • (Array<Exception>)

    exceptions received in a #requires block or in a file required with #require



114
115
116
# File 'lib/autorespawn.rb', line 114

def exceptions
  @exceptions
end

#namenil, Object (readonly)

An arbitrary objcet that can be used to identify the processes/slaves

Returns:

  • (nil, Object)


78
79
80
# File 'lib/autorespawn.rb', line 78

def name
  @name
end

#process_command_line(Array,Hash) (readonly)

The arguments that should be passed to Kernel.exec in standalone mode

Ignored in slave mode

Returns:

  • ((Array,Hash))


85
86
87
# File 'lib/autorespawn.rb', line 85

def process_command_line
  @process_command_line
end

#program_idProgramID (readonly)

Returns object currently known state of files makind this program.

Returns:

  • (ProgramID)

    object currently known state of files makind this program



110
111
112
# File 'lib/autorespawn.rb', line 110

def program_id
  @program_id
end

#required_pathsSet<Pathname> (readonly)

Set of paths that have been required within a #requires block or through #require

Returns:

  • (Set<Pathname>)


120
121
122
# File 'lib/autorespawn.rb', line 120

def required_paths
  @required_paths
end

#subcommandsObject (readonly)

In master/slave mode, the list of subcommands that the master should spawn



130
131
132
# File 'lib/autorespawn.rb', line 130

def subcommands
  @subcommands
end

Class Method Details

.initial_program_idObject



55
56
57
# File 'lib/autorespawn.rb', line 55

def self.initial_program_id
    @initial_program_id
end

.namenil, Object

ID object

An arbitrary object passed to #initialize or #add_slave to identify this process.

Returns:

  • (nil, Object)


46
47
48
# File 'lib/autorespawn.rb', line 46

def self.name
    @name
end

.read_child_stateObject



59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/autorespawn.rb', line 59

def self.read_child_state
    # Delete the envvars first, we really don't want them to leak
    slave_initial_state_fd = ENV.delete(SLAVE_INITIAL_STATE_ENV)
    slave_result_fd = ENV.delete(SLAVE_RESULT_ENV)
    if slave_initial_state_fd
        slave_initial_state_fd = Integer(slave_initial_state_fd)
        io = IO.for_fd(slave_initial_state_fd)
        @name, @initial_program_id = Marshal.load(io)
        io.close
    end
    if slave_result_fd
        @slave_result_fd = Integer(slave_result_fd)
    end
end

.run(*command, **options, &block) ⇒ Object



308
309
310
# File 'lib/autorespawn.rb', line 308

def self.run(*command, **options, &block)
    new(*command, **options).run(&block)
end

.slave?Boolean

Returns:

  • (Boolean)


52
53
54
# File 'lib/autorespawn.rb', line 52

def self.slave?
    !!slave_result_fd
end

.slave_result_fdObject



49
50
51
# File 'lib/autorespawn.rb', line 49

def self.slave_result_fd
    @slave_result_fd
end

Instance Method Details

#add_slave(*cmdline, name: nil, **spawn_options) ⇒ Object

Request that the master spawns these subcommands

Raises:

  • (NotSlave)

    if the script is being executed in standalone mode



200
201
202
# File 'lib/autorespawn.rb', line 200

def add_slave(*cmdline, name: nil, **spawn_options)
    subcommands << [name, cmdline, spawn_options]
end

#at_exit {|exception| ... } ⇒ Object

Register a callback that is called after the block passed to #run has been called, but before the process gets respawned. Meant to perform what hass been done in #run that should be cleaned before respawning.

Yield Parameters:

  • exception (Exception)


104
# File 'lib/autorespawn.rb', line 104

define_hooks :at_respawn

#currently_loaded_filesObject



217
218
219
220
# File 'lib/autorespawn.rb', line 217

def currently_loaded_files
    $LOADED_FEATURES.map { |p| Pathname.new(p) } +
        caller_locations.map { |l| Pathname.new(l.absolute_path) }
end

#dump_initial_state(files) ⇒ Object

Create a pipe and dump the program ID state of the current program there



206
207
208
209
210
211
212
213
214
215
# File 'lib/autorespawn.rb', line 206

def dump_initial_state(files)
    program_id = ProgramID.new
    program_id.register_files(files)

    io = Tempfile.new "autorespawn_initial_state"
    Marshal.dump([name, program_id], io)
    io.flush
    io.rewind
    io
end

#exit_code(value = nil) ⇒ Object

Defines the exit code for this instance



223
224
225
226
227
228
229
# File 'lib/autorespawn.rb', line 223

def exit_code(value = nil)
    if value
        @exit_code = value
    else
        @exit_code
    end
end

#on_exception {|exception| ... } ⇒ Object

Register a callback that is called whenever an exception is rescued by #watch_yield

Yield Parameters:

  • exception (Exception)


95
# File 'lib/autorespawn.rb', line 95

define_hooks :on_exception

#perform_work(all_files, &block) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/autorespawn.rb', line 276

def perform_work(all_files, &block)
    not_tracked = all_files.
        find_all do |p|
            begin !program_id.include?(p)
            rescue FileNotFound
            end
        end

    if not_tracked.empty? && !program_id.changed?
        if exceptions.empty?
            did_yield = true
            watch_yield(&block)
        end

        all_files = required_paths | error_paths
        not_tracked = all_files.
            find_all do |p|
                begin !program_id.include?(p)
                rescue FileNotFound
                end
            end

        if !slave? && not_tracked.empty?
            Watch.new(program_id).wait
        end
        if did_yield
            run_hook :at_respawn
        end
    end
    all_files
end

#require(file) ⇒ Object

Requires one file under the autorespawn supervision

If the require fails, the call to run will not execute its block, instead waiting for the file(s) to change



155
156
157
# File 'lib/autorespawn.rb', line 155

def require(file)
    watch_yield { Kernel.require file }
end

#run(&block) ⇒ Object

Perform the program workd and reexec it when needed

It is the last method you should be calling in your program, providing the program’s actual work in the block. Once the block return, the method will watch for changes and reexec’s it

Exceptions raised by the block are displayed but do not cause the watch to stop

This method does NOT return



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/autorespawn.rb', line 241

def run(&block)
    if slave? || subcommands.empty?
        all_files = required_paths | error_paths
        if block_given? 
            all_files = perform_work(all_files, &block)
        end

        if slave?
            io = IO.for_fd(Autorespawn.slave_result_fd)
            string = Marshal.dump([subcommands, all_files])
            io.write string
            io.flush
            exit exit_code
        else
            io = dump_initial_state(all_files)
            cmdline  = process_command_line[0].dup
            redirect = Hash[io.fileno => io.fileno].merge(process_command_line[1])
            if cmdline.last.kind_of?(Hash)
                redirect = redirect.merge(cmdline.pop)
            end
            Kernel.exec(Hash[SLAVE_INITIAL_STATE_ENV => "#{io.fileno}"], *cmdline, redirect)
        end
    else
        if block_given?
            raise ArgumentError, "cannot call #run with a block after using #add_slave"
        end
        manager = Manager.new
        subcommands.each do |name, command, options|
            manager.add_slave(*command, name: name, **options)
        end
        return manager.run
    end
end

#slave?Boolean

Returns whether we have been spawned by a manager, or in standalone mode

Returns:

  • (Boolean)


193
194
195
# File 'lib/autorespawn.rb', line 193

def slave?
    self.class.slave?
end

#watch_yieldObject

Call to require a bunch of files in a block and add the result to the list of watches



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/autorespawn.rb', line 160

def watch_yield
    current = currently_loaded_files
    new_exceptions = Array.new
    begin
        result = yield
    rescue Interrupt, SystemExit
        raise
    rescue Exception => e
        new_exceptions << e
        run_hook :on_exception, e
        exceptions << e
        # cross-drb exceptions are broken w.r.t. #backtrace_locations. It
        # returns a string in their case. Since it happens only on
        # exceptions that originate from the server (which means a broken
        # Roby codepath), let's just ignore it
        if !e.backtrace_locations.kind_of?(String)
            backtrace = e.backtrace_locations.map { |l| Pathname.new(l.absolute_path) }
        else
            STDERR.puts "Caught what appears to be a cross-drb exception, which should not happen"
            STDERR.puts e.message
            STDERR.puts e.backtrace.join("\n  ")
            backtrace = Array.new
        end
        error_paths.merge(backtrace)
        if e.kind_of?(LoadError) && e.path
            error_paths << Pathname.new(e.path)
        end
    end
    required_paths.merge(currently_loaded_files - current)
    return result, new_exceptions
end