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

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, 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.3.0"

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.



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

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


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

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



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

def exceptions
  @exceptions
end

#namenil, Object (readonly)

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

Returns:

  • (nil, Object)


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

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


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

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



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

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


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

def required_paths
  @required_paths
end

#subcommandsObject (readonly)

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



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

def subcommands
  @subcommands
end

Class Method Details

.initial_program_idObject



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

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)


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

def self.name
    @name
end

.read_child_stateObject



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

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



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

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

.slave?Boolean

Returns:

  • (Boolean)


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

def self.slave?
    !!slave_result_fd
end

.slave_result_fdObject



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

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



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

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)


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

define_hooks :at_respawn

#currently_loaded_filesObject



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

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



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

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



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

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)


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

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.



275
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
# File 'lib/autorespawn.rb', line 275

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



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

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



240
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
# File 'lib/autorespawn.rb', line 240

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)


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

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



159
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
# File 'lib/autorespawn.rb', line 159

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
            e.backtrace.join("\n  ")
            backtrace = Array.new
        end
        error_paths.merge(backtrace)
        if e.kind_of?(LoadError)
            error_paths << e.path
        end
    end
    required_paths.merge(currently_loaded_files - current)
    return result, new_exceptions
end