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.6.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.



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

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


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

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



117
118
119
# File 'lib/autorespawn.rb', line 117

def exceptions
  @exceptions
end

#namenil, Object (readonly)

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

Returns:

  • (nil, Object)


81
82
83
# File 'lib/autorespawn.rb', line 81

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


88
89
90
# File 'lib/autorespawn.rb', line 88

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



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

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


123
124
125
# File 'lib/autorespawn.rb', line 123

def required_paths
  @required_paths
end

#subcommandsObject (readonly)

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



133
134
135
# File 'lib/autorespawn.rb', line 133

def subcommands
  @subcommands
end

Class Method Details

.initial_program_idObject



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

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)


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

def self.name
    @name
end

.read_child_stateObject



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

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)
        size = io.read(4).unpack('L<').first
        @name, @initial_program_id = Marshal.load(io.read(size))
        io.close
    end
    if slave_result_fd
        @slave_result_fd = Integer(slave_result_fd)
    end
end

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



314
315
316
# File 'lib/autorespawn.rb', line 314

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

.slave?Boolean

Returns:

  • (Boolean)


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

def self.slave?
    !!slave_result_fd
end

.slave_result_fdObject



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

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



203
204
205
# File 'lib/autorespawn.rb', line 203

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)


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

define_hooks :at_respawn

#currently_loaded_filesObject



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

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



209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/autorespawn.rb', line 209

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

    io = Tempfile.new "autorespawn_initial_state"
    initial_info = Marshal.dump([name, program_id])
    io.write([initial_info.size].pack("L<"))
    io.write(initial_info)
    io.flush
    io.rewind
    io
end

#exit_code(value = nil) ⇒ Object

Defines the exit code for this instance



229
230
231
232
233
234
235
# File 'lib/autorespawn.rb', line 229

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)


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

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.



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
307
308
309
310
311
312
# File 'lib/autorespawn.rb', line 282

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



158
159
160
# File 'lib/autorespawn.rb', line 158

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



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
274
275
276
277
278
279
# File 'lib/autorespawn.rb', line 247

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)


196
197
198
# File 'lib/autorespawn.rb', line 196

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



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
191
192
193
# File 'lib/autorespawn.rb', line 163

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