autorespawn 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +2 -1
- data/README.md +43 -3
- data/autorespawn.gemspec +3 -0
- data/lib/autorespawn.rb +275 -2
- data/lib/autorespawn/exceptions.rb +15 -1
- data/lib/autorespawn/manager.rb +77 -0
- data/lib/autorespawn/program_id.rb +63 -3
- data/lib/autorespawn/slave.rb +178 -0
- data/lib/autorespawn/version.rb +2 -2
- data/lib/autorespawn/watch.rb +1 -64
- metadata +20 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f350f615e8e53d1e5c4ffa35cf876dba59b3f894
|
4
|
+
data.tar.gz: 85ae94deab4d55b202d7ab3595a58e9e4595ddf0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 64fb8e03ca65dcd7e2989c137a8c21ba74eb7dc946871f6c58c633f95fa11038bf96f62193be29d6e7373d2aeb7201207a3d81d829b341341bbd0d1dfc1d7f2c
|
7
|
+
data.tar.gz: 664506705104437abfb05e9f48b2ba4c725548322134078a5bdfac705d291705e7141886865476f8db64fcc7f4506e25a050f5810cfd5f5c725d303281181f09
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,15 +1,20 @@
|
|
1
1
|
# Autorespawn
|
2
2
|
|
3
|
+
[![Build Status](https://travis-ci.org/doudou/autorespawn.svg?branch=master)](https://travis-ci.org/doudou/autorespawn)
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/autorespawn.svg)](http://badge.fury.io/rb/autorespawn)
|
5
|
+
[![Coverage Status](https://coveralls.io/repos/doudou/autorespawn/badge.svg?branch=master&service=github)](https://coveralls.io/github/doudou/autorespawn?branch=master)
|
6
|
+
[![Documentation](http://b.repl.ca/v1/yard-docs-blue.png)](http://rubydoc.info/gems/autorespawn/frames)
|
7
|
+
|
3
8
|
Autorespawn is an implementation of the popular autoreload scheme, which reloads
|
4
9
|
Ruby program files when they change, but instead execs/spawns the underlying
|
5
10
|
program again. This avoids common issues related to the load mechanism.
|
6
11
|
|
7
|
-
## Usage
|
12
|
+
## Standalone Usage
|
8
13
|
|
9
14
|
Require all the files you need autorespawn to watch and then do
|
10
15
|
|
11
16
|
~~~
|
12
|
-
Autorespawn.
|
17
|
+
Autorespawn.run do
|
13
18
|
# Add the program's functionality here
|
14
19
|
end
|
15
20
|
~~~
|
@@ -18,10 +23,45 @@ If you touch ARGV and $0, you will want to pass the program and arguments
|
|
18
23
|
explicitely
|
19
24
|
|
20
25
|
~~~
|
21
|
-
Autorespawn.
|
26
|
+
Autorespawn.run 'program', 'argument0', 'argument1' do
|
27
|
+
end
|
28
|
+
~~~
|
29
|
+
|
30
|
+
## Master/slave mode
|
31
|
+
|
32
|
+
The main usage I designed this gem for was to implement an autotest scheme that
|
33
|
+
does not use #load, and that does restart tests whose dependencies are changed
|
34
|
+
(autotest only looks for the test files). That means a few 100 subcommands that
|
35
|
+
need to be spawned and managed. Not really feasible without some kind of manager
|
36
|
+
behind the scene.
|
37
|
+
|
38
|
+
Autorespawn can be started used in master/slave mode. Some processes would be
|
39
|
+
spawning new subcommands by registering them with the `Autorespawn#add_slave` method, while
|
40
|
+
other are workers. In my autotest prototype, the same script is called in both
|
41
|
+
cases, only the codepaths are different:
|
42
|
+
|
43
|
+
~~~ ruby
|
44
|
+
manager = Autorespawn.new
|
45
|
+
if build_axis.empty? == 1
|
46
|
+
manager.run do
|
47
|
+
# Perform the work
|
48
|
+
end
|
49
|
+
else
|
50
|
+
build_axis.each do |cmdline|
|
51
|
+
manager.add_slave(*cmdline)
|
52
|
+
end
|
53
|
+
manager.run
|
22
54
|
end
|
23
55
|
~~~
|
24
56
|
|
57
|
+
It is safe to use this scheme recursively. Slaves who call `#add_slave` will
|
58
|
+
simply pass the request to the master.
|
59
|
+
|
60
|
+
## TODO
|
61
|
+
|
62
|
+
Manage the dependency between slaves that called `add_slave` and these slaves,
|
63
|
+
i.e. auto-remove the level 2 slaves when the level 1 changes and is respawned.
|
64
|
+
|
25
65
|
## Installation
|
26
66
|
|
27
67
|
Add this line to your application's Gemfile:
|
data/autorespawn.gemspec
CHANGED
@@ -9,6 +9,8 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ["Sylvain Joyeux"]
|
10
10
|
spec.email = ["sylvain.joyeux@m4x.org"]
|
11
11
|
|
12
|
+
spec.required_ruby_version = ">= 2.1.0"
|
13
|
+
|
12
14
|
spec.summary = "functionality to respawn a Ruby program when its source changes"
|
13
15
|
spec.description =<<-EOD
|
14
16
|
This gem implements the functionality to take a signature of the current Ruby
|
@@ -28,4 +30,5 @@ EOD
|
|
28
30
|
spec.add_development_dependency "minitest", ">= 5.0", "~> 5.0"
|
29
31
|
spec.add_development_dependency "fakefs", ">= 0.6", "~> 0.6.0"
|
30
32
|
spec.add_development_dependency 'flexmock', ">= 2.0", '~> 2.0'
|
33
|
+
spec.add_development_dependency "coveralls"
|
31
34
|
end
|
data/lib/autorespawn.rb
CHANGED
@@ -1,8 +1,281 @@
|
|
1
|
+
require 'set'
|
1
2
|
require "autorespawn/version"
|
2
3
|
require "autorespawn/exceptions"
|
3
4
|
require "autorespawn/program_id"
|
4
5
|
require "autorespawn/watch"
|
6
|
+
require "autorespawn/slave"
|
7
|
+
require "autorespawn/manager"
|
5
8
|
|
6
|
-
|
7
|
-
|
9
|
+
# Automatically exec's the current program when one of the source file changes
|
10
|
+
#
|
11
|
+
# The exec is done at very-well defined points to avoid weird state, and it is
|
12
|
+
# possible to define cleanup handlers beyond Ruby's at_exit mechanism
|
13
|
+
#
|
14
|
+
# Call this method from the entry point of your program, giving it the actual
|
15
|
+
# program functionality as a block. The method will exec and spawn subprocesses
|
16
|
+
# at will, when needed, and call the block in these subprocesses as required.
|
17
|
+
#
|
18
|
+
# At the point of call, all of the program's dependencies must be
|
19
|
+
# already required, as it is on this basis that the auto-reloading will
|
20
|
+
# be done
|
21
|
+
#
|
22
|
+
# This method does NOT return
|
23
|
+
#
|
24
|
+
# @param [Array<String>] command the command to be executed. It is
|
25
|
+
# passed as-is to Kernel.spawn and Kernel.exec
|
26
|
+
# @param options keyword options to pass to Kernel.spawn and Kernel.exec
|
27
|
+
class Autorespawn
|
28
|
+
INITIAL_STATE_FD = "AUTORESPAWN_AUTORELOAD"
|
29
|
+
|
30
|
+
SLAVE_RESULT_ENV = 'AUTORESPAWN_SLAVE_RESULT_FD'
|
31
|
+
SLAVE_INITIAL_STATE_ENV = 'AUTORESPAWN_SLAVE_INITIAL_STATE_FD'
|
32
|
+
|
33
|
+
def self.slave_result_fd
|
34
|
+
@slave_result_fd
|
35
|
+
end
|
36
|
+
def self.slave_initial_state_fd
|
37
|
+
@slave_initial_state_fd
|
38
|
+
end
|
39
|
+
def self.slave?
|
40
|
+
!!slave_result_fd
|
41
|
+
end
|
42
|
+
|
43
|
+
# Delete the envvars first, we really don't want them to leak
|
44
|
+
slave_initial_state_fd = ENV.delete(SLAVE_INITIAL_STATE_ENV)
|
45
|
+
slave_result_fd = ENV.delete(SLAVE_RESULT_ENV)
|
46
|
+
|
47
|
+
if slave_initial_state_fd
|
48
|
+
@slave_initial_state_fd = Integer(slave_initial_state_fd)
|
49
|
+
end
|
50
|
+
if slave_result_fd
|
51
|
+
@slave_result_fd = Integer(slave_result_fd)
|
52
|
+
end
|
53
|
+
|
54
|
+
# The arguments that should be passed to Kernel.exec in standalone mode
|
55
|
+
#
|
56
|
+
# Ignored in slave mode
|
57
|
+
#
|
58
|
+
# @return [(Array,Hash)]
|
59
|
+
attr_reader :process_command_line
|
60
|
+
|
61
|
+
# Set of callbacks called just before we respawn the process
|
62
|
+
#
|
63
|
+
# @return [Array<#call>]
|
64
|
+
attr_reader :respawn_handlers
|
65
|
+
|
66
|
+
# @return [ProgramID] object currently known state of files makind this
|
67
|
+
# program
|
68
|
+
attr_reader :program_id
|
69
|
+
|
70
|
+
# @return [Array<Exception>] exceptions received in a {#requires} block or
|
71
|
+
# in a file required with {#require}
|
72
|
+
attr_reader :exceptions
|
73
|
+
|
74
|
+
# Set of paths that have been required within a {#requires} block or through
|
75
|
+
# {#require}
|
76
|
+
#
|
77
|
+
# @return [Set<Pathname>]
|
78
|
+
attr_reader :required_paths
|
79
|
+
|
80
|
+
# Set of paths that are part of an error backtrace
|
81
|
+
#
|
82
|
+
# This is updated in {#requires} or {#require}
|
83
|
+
#
|
84
|
+
# @return [Set<Pathname>]
|
85
|
+
attr_reader :error_paths
|
86
|
+
|
87
|
+
# In master/slave mode, the list of subcommands that the master should spawn
|
88
|
+
attr_reader :subcommands
|
89
|
+
|
90
|
+
def initialize(*command, track_current: false, **options)
|
91
|
+
if command.empty?
|
92
|
+
command = [$0, *ARGV]
|
93
|
+
end
|
94
|
+
@process_command_line = [command, options]
|
95
|
+
@respawn_handlers = Array.new
|
96
|
+
@program_id = ProgramID.new
|
97
|
+
@exceptions = Array.new
|
98
|
+
@required_paths = Set.new
|
99
|
+
@error_paths = Set.new
|
100
|
+
@subcommands = Array.new
|
101
|
+
@exit_code = 0
|
102
|
+
if track_current
|
103
|
+
@required_paths = currently_loaded_files.to_set
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns true if there is an initial state dump
|
108
|
+
def has_initial_state?
|
109
|
+
!!Autorespawn.slave_initial_state_fd
|
110
|
+
end
|
111
|
+
|
112
|
+
# Loads the initial state from STDIN
|
113
|
+
def load_initial_state
|
114
|
+
io = IO.for_fd(Autorespawn.slave_initial_state_fd)
|
115
|
+
@program_id = Marshal.load(io)
|
116
|
+
io.close
|
117
|
+
end
|
118
|
+
|
119
|
+
# Requires one file under the autorespawn supervision
|
120
|
+
#
|
121
|
+
# If the require fails, the call to {.run} will not execute its block,
|
122
|
+
# instead waiting for the file(s) to change
|
123
|
+
def require(file)
|
124
|
+
watch_yield { Kernel.require file }
|
125
|
+
end
|
126
|
+
|
127
|
+
# Call to require a bunch of files in a block and add the result to the list of watches
|
128
|
+
def watch_yield
|
129
|
+
current = currently_loaded_files
|
130
|
+
new_exceptions = Array.new
|
131
|
+
begin
|
132
|
+
result = yield
|
133
|
+
rescue Interrupt, SystemExit
|
134
|
+
raise
|
135
|
+
rescue Exception => e
|
136
|
+
new_exceptions << e
|
137
|
+
exceptions << e
|
138
|
+
backtrace = e.backtrace_locations.map { |l| Pathname.new(l.absolute_path) }
|
139
|
+
error_paths.merge(backtrace)
|
140
|
+
if e.kind_of?(LoadError)
|
141
|
+
error_paths << e.path
|
142
|
+
end
|
143
|
+
end
|
144
|
+
required_paths.merge(currently_loaded_files - current)
|
145
|
+
return result, new_exceptions
|
146
|
+
end
|
147
|
+
|
148
|
+
# Returns whether we have been spawned by a manager, or in standalone mode
|
149
|
+
def slave?
|
150
|
+
self.class.slave?
|
151
|
+
end
|
152
|
+
|
153
|
+
# Request that the master spawns these subcommands
|
154
|
+
#
|
155
|
+
# @raise [NotSlave] if the script is being executed in standalone mode
|
156
|
+
def add_slave(*cmdline, **spawn_options)
|
157
|
+
subcommands << [cmdline, spawn_options]
|
158
|
+
end
|
159
|
+
|
160
|
+
# Create a pipe and dump the program ID state of the current program
|
161
|
+
# there
|
162
|
+
def dump_initial_state(files)
|
163
|
+
program_id = ProgramID.new
|
164
|
+
program_id.register_files(files)
|
165
|
+
|
166
|
+
io = Tempfile.new "autorespawn_initial_state"
|
167
|
+
Marshal.dump(program_id, io)
|
168
|
+
io.flush
|
169
|
+
io.rewind
|
170
|
+
io
|
171
|
+
end
|
172
|
+
|
173
|
+
def currently_loaded_files
|
174
|
+
$LOADED_FEATURES.map { |p| Pathname.new(p) } +
|
175
|
+
caller_locations.map { |l| Pathname.new(l.absolute_path) }
|
176
|
+
end
|
177
|
+
|
178
|
+
# Declares a handler that should be called in a process, just before
|
179
|
+
# exec'ing a fresh process *if* the block has been executed
|
180
|
+
def at_respawn(&block)
|
181
|
+
respawn_handlers << block
|
182
|
+
end
|
183
|
+
|
184
|
+
# Defines the exit code for this instance
|
185
|
+
def exit_code(value = nil)
|
186
|
+
if value
|
187
|
+
@exit_code = value
|
188
|
+
else
|
189
|
+
@exit_code
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Perform the program workd and reexec it when needed
|
194
|
+
#
|
195
|
+
# It is the last method you should be calling in your program, providing the
|
196
|
+
# program's actual work in the block. Once the block return, the method will
|
197
|
+
# watch for changes and reexec's it
|
198
|
+
#
|
199
|
+
# Exceptions raised by the block are displayed but do not cause the watch to
|
200
|
+
# stop
|
201
|
+
#
|
202
|
+
# This method does NOT return
|
203
|
+
def run(&block)
|
204
|
+
if slave? || subcommands.empty?
|
205
|
+
all_files = required_paths | error_paths
|
206
|
+
if block_given?
|
207
|
+
all_files = perform_work(all_files, &block)
|
208
|
+
end
|
209
|
+
|
210
|
+
if slave?
|
211
|
+
io = IO.for_fd(Autorespawn.slave_result_fd)
|
212
|
+
string = Marshal.dump([subcommands, all_files])
|
213
|
+
io.write string
|
214
|
+
io.flush
|
215
|
+
exit exit_code
|
216
|
+
else
|
217
|
+
io = dump_initial_state(all_files)
|
218
|
+
Kernel.exec(Hash[SLAVE_INITIAL_STATE_ENV => "#{io.fileno}"], *process_command_line[0],
|
219
|
+
io.fileno => io.fileno, **process_command_line[1])
|
220
|
+
end
|
221
|
+
else
|
222
|
+
if block_given?
|
223
|
+
raise ArgumentError, "cannot call #run with a block after using #add_slave"
|
224
|
+
end
|
225
|
+
manager = Manager.new
|
226
|
+
subcommands.each do |command, options|
|
227
|
+
manager.add_slave(*command, **options)
|
228
|
+
end
|
229
|
+
return manager.run
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# @api private
|
234
|
+
def perform_work(all_files, &block)
|
235
|
+
if has_initial_state?
|
236
|
+
load_initial_state
|
237
|
+
end
|
238
|
+
|
239
|
+
not_tracked = all_files.
|
240
|
+
find_all do |p|
|
241
|
+
begin !program_id.include?(p)
|
242
|
+
rescue FileNotFound
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
if not_tracked.empty? && !program_id.changed?
|
247
|
+
if exceptions.empty?
|
248
|
+
did_yield = true
|
249
|
+
_, yield_exceptions = watch_yield(&block)
|
250
|
+
yield_exceptions.each do |e|
|
251
|
+
backtrace = (e.backtrace || Array.new).dup
|
252
|
+
first_line = backtrace.shift
|
253
|
+
STDERR.puts "#{e.message}: #{first_line}"
|
254
|
+
STDERR.puts " #{e.backtrace.join("\n ")}"
|
255
|
+
end
|
256
|
+
|
257
|
+
end
|
258
|
+
|
259
|
+
all_files = required_paths | error_paths
|
260
|
+
not_tracked = all_files.
|
261
|
+
find_all do |p|
|
262
|
+
begin !program_id.include?(p)
|
263
|
+
rescue FileNotFound
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
if !slave? && not_tracked.empty?
|
268
|
+
Watch.new(program_id).wait
|
269
|
+
end
|
270
|
+
if did_yield
|
271
|
+
respawn_handlers.each { |b| b.call }
|
272
|
+
end
|
273
|
+
end
|
274
|
+
all_files
|
275
|
+
end
|
276
|
+
|
277
|
+
def self.run(*command, **options, &block)
|
278
|
+
new(*command, **options).run(&block)
|
279
|
+
end
|
8
280
|
end
|
281
|
+
|
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
class Autorespawn
|
2
2
|
# Exception raised when a path cannot be resolved to a file on disk
|
3
3
|
class FileNotFound < RuntimeError
|
4
4
|
# @return [Pathname] the path to resolve
|
@@ -11,4 +11,18 @@ module Autorespawn
|
|
11
11
|
@path, @search_path = path, search_path
|
12
12
|
end
|
13
13
|
end
|
14
|
+
|
15
|
+
# Exception raised when a command that is only available in master/slave
|
16
|
+
# mode is called in standalone mode
|
17
|
+
class NotSlave < RuntimeError
|
18
|
+
end
|
19
|
+
|
20
|
+
# Exception raised when a command that is only available in master/slave
|
21
|
+
# mode is called in standalone mode
|
22
|
+
class NotFinished < RuntimeError
|
23
|
+
end
|
24
|
+
|
25
|
+
# Exception raised in Slave#spawn if the slave is already running
|
26
|
+
class AlreadyRunning < RuntimeError
|
27
|
+
end
|
14
28
|
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
class Autorespawn
|
2
|
+
# Manager of a bunch of autorespawn slaves
|
3
|
+
class Manager
|
4
|
+
# @param [Integer] the number of processes allowed to work in parallel
|
5
|
+
attr_reader :parallel_level
|
6
|
+
# @return [Array<Slave>] declared worker processes, as a hash from
|
7
|
+
# the PID to a Slave object
|
8
|
+
attr_reader :workers
|
9
|
+
# @return [Hash<Slave>] list of active slaves
|
10
|
+
attr_reader :active_slaves
|
11
|
+
|
12
|
+
def initialize(parallel_level: 1)
|
13
|
+
@parallel_level = parallel_level
|
14
|
+
@workers = Array.new
|
15
|
+
@active_slaves = Hash.new
|
16
|
+
end
|
17
|
+
|
18
|
+
# Spawns a worker, i.e. a program that will perform the intended work
|
19
|
+
# and report the program state
|
20
|
+
def add_slave(*cmdline, **spawn_options)
|
21
|
+
slave = Slave.new(*cmdline, **spawn_options)
|
22
|
+
workers << slave
|
23
|
+
slave
|
24
|
+
end
|
25
|
+
|
26
|
+
# @api private
|
27
|
+
#
|
28
|
+
# Collect information about the finished slaves
|
29
|
+
#
|
30
|
+
# @return [Array<Slave>] the slaves that finished
|
31
|
+
def collect_finished_slaves
|
32
|
+
finished_slaves = Array.new
|
33
|
+
while finished_child = Process.waitpid2(-1, Process::WNOHANG)
|
34
|
+
pid, status = *finished_child
|
35
|
+
if slave = active_slaves.delete(pid)
|
36
|
+
finished_slaves << slave
|
37
|
+
slave.finished(status)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
finished_slaves
|
41
|
+
rescue Errno::ECHILD
|
42
|
+
Array.new
|
43
|
+
end
|
44
|
+
|
45
|
+
def run
|
46
|
+
while true
|
47
|
+
poll
|
48
|
+
sleep 1
|
49
|
+
end
|
50
|
+
|
51
|
+
rescue Interrupt
|
52
|
+
ensure
|
53
|
+
active_slaves.values.each do |slave|
|
54
|
+
slave.kill
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Wait for children to terminate and spawns them when needed
|
59
|
+
def poll
|
60
|
+
finished_slaves = collect_finished_slaves
|
61
|
+
new_slaves = Array.new
|
62
|
+
while active_slaves.size < parallel_level
|
63
|
+
if slave_i = workers.index { |s| s.needs_spawn? }
|
64
|
+
slave = workers.delete_at(slave_i)
|
65
|
+
@workers = workers[slave_i..-1] + workers[0, slave_i] + [slave]
|
66
|
+
slave.spawn
|
67
|
+
new_slaves << slave
|
68
|
+
active_slaves[slave.pid] = slave
|
69
|
+
else
|
70
|
+
break
|
71
|
+
end
|
72
|
+
end
|
73
|
+
return new_slaves, finished_slaves
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
@@ -2,7 +2,7 @@ require 'autorespawn/program_id'
|
|
2
2
|
require 'pathname'
|
3
3
|
require 'digest/sha1'
|
4
4
|
|
5
|
-
|
5
|
+
class Autorespawn
|
6
6
|
# Management of the ID of a complete Ruby program
|
7
7
|
#
|
8
8
|
# It basically stores information about all the files that form this
|
@@ -26,6 +26,16 @@ module Autorespawn
|
|
26
26
|
id
|
27
27
|
end
|
28
28
|
|
29
|
+
# Whether this program ID tracks some files
|
30
|
+
def empty?
|
31
|
+
files.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
# Remove all tracked files
|
35
|
+
def clear
|
36
|
+
files.clear
|
37
|
+
end
|
38
|
+
|
29
39
|
# Registers the file information for all loaded features
|
30
40
|
#
|
31
41
|
# @return [void]
|
@@ -44,15 +54,61 @@ module Autorespawn
|
|
44
54
|
end
|
45
55
|
end
|
46
56
|
|
57
|
+
# Register a set of files
|
58
|
+
#
|
59
|
+
# @param [Array<String>] files the list of files
|
60
|
+
# @param [Array<String>] search_path the path to resolve relative paths
|
61
|
+
# @param [Boolean] ignore_not_found whether files that cannot be
|
62
|
+
# resolved are ignored or cause a FileNotFound exception
|
63
|
+
# @return [Boolean] whether the program ID has been modified
|
64
|
+
def register_files(files, search_path = ruby_load_path, ignore_not_found: true)
|
65
|
+
modified = Array.new
|
66
|
+
files.each do |path|
|
67
|
+
begin
|
68
|
+
if full_path = register_file(path, search_path)
|
69
|
+
modified << full_path
|
70
|
+
end
|
71
|
+
rescue FileNotFound
|
72
|
+
raise if !ignore_not_found
|
73
|
+
end
|
74
|
+
end
|
75
|
+
modified
|
76
|
+
end
|
77
|
+
|
47
78
|
# Registers file information for one file
|
48
79
|
#
|
49
80
|
# @param [Pathname] file the path to the file
|
50
|
-
# @return [
|
81
|
+
# @return [Boolean] whether the registration modified the program ID's
|
82
|
+
# state
|
51
83
|
def register_file(file, search_path = ruby_load_path)
|
52
84
|
info = file_info(file, search_path)
|
85
|
+
modified = (files[info.path] != info)
|
53
86
|
files[info.path] = info
|
87
|
+
|
88
|
+
if modified
|
89
|
+
@id = nil
|
90
|
+
info.path
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Update the information about all the files registered on this object
|
95
|
+
def refresh
|
96
|
+
updated = Hash.new
|
97
|
+
files.each_key do |path|
|
98
|
+
next if !path.exist?
|
99
|
+
info = file_info(path)
|
100
|
+
updated[info.path] = info
|
101
|
+
end
|
102
|
+
@files = updated
|
54
103
|
@id = nil
|
55
|
-
|
104
|
+
updated
|
105
|
+
end
|
106
|
+
|
107
|
+
# Enumerate the path of all the files that are being tracked
|
108
|
+
#
|
109
|
+
# @yieldparam [Pathname] path
|
110
|
+
def each_tracked_file(&block)
|
111
|
+
files.keys.each(&block)
|
56
112
|
end
|
57
113
|
|
58
114
|
# Returns a string that can ID this program
|
@@ -78,6 +134,10 @@ module Autorespawn
|
|
78
134
|
false
|
79
135
|
end
|
80
136
|
|
137
|
+
def include?(path, search_path = ruby_load_path)
|
138
|
+
files.has_key?(resolve_file_path(path, search_path))
|
139
|
+
end
|
140
|
+
|
81
141
|
# @api private
|
82
142
|
#
|
83
143
|
# Given a path that may be relative, computes the full path to the
|
@@ -0,0 +1,178 @@
|
|
1
|
+
class Autorespawn
|
2
|
+
# Representation of an autorespawn-aware subprocess that is started by a
|
3
|
+
# {Manager}
|
4
|
+
#
|
5
|
+
# Slaves have two roles: the one of discovery (what are the commands that
|
6
|
+
# need to be started) and the one of
|
7
|
+
class Slave
|
8
|
+
# The currently known program ID
|
9
|
+
attr_reader :program_id
|
10
|
+
# The command line of the subprocess
|
11
|
+
attr_reader :cmdline
|
12
|
+
# Environment that should be set in the subprocess
|
13
|
+
attr_reader :spawn_env
|
14
|
+
# Options that should be passed to Kernel.spawn
|
15
|
+
attr_reader :spawn_options
|
16
|
+
# @return [nil,Integer] pid the PID of the current process or of the
|
17
|
+
# last process if it is finished. It is non-nil only after {#spawn} has
|
18
|
+
# been called
|
19
|
+
attr_reader :pid
|
20
|
+
# @return [Process::Status] the exit status of the last run. Is nil
|
21
|
+
# while the process is running
|
22
|
+
attr_reader :status
|
23
|
+
|
24
|
+
# @return [Array<String>] a list of commands that this slave requests
|
25
|
+
attr_reader :subcommands
|
26
|
+
|
27
|
+
# @api private
|
28
|
+
#
|
29
|
+
# @return [IO] the result I/O
|
30
|
+
attr_reader :result_r
|
31
|
+
# @api private
|
32
|
+
#
|
33
|
+
# @return [String] the result data as received
|
34
|
+
attr_reader :result_buffer
|
35
|
+
|
36
|
+
def initialize(*cmdline, env: Hash.new, **spawn_options)
|
37
|
+
@program_id = ProgramID.new
|
38
|
+
@cmdline = cmdline
|
39
|
+
@needs_spawn = true
|
40
|
+
@spawn_env = env
|
41
|
+
@spawn_options = spawn_options
|
42
|
+
@subcommands = Array.new
|
43
|
+
@pid = nil
|
44
|
+
@status = nil
|
45
|
+
@result_r = nil
|
46
|
+
@result_buffer = nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def inspect
|
50
|
+
"#<Autorespawn::Slave #{object_id.to_s(16)} #{cmdline.join(" ")}>"
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_s; inspect end
|
54
|
+
|
55
|
+
# Start the slave
|
56
|
+
#
|
57
|
+
# @return [Integer] the slave's PID
|
58
|
+
def spawn
|
59
|
+
if running?
|
60
|
+
raise AlreadyRunning, "cannot call #spawn on #{self}: already running"
|
61
|
+
end
|
62
|
+
|
63
|
+
initial_r, initial_w = IO.pipe
|
64
|
+
result_r, result_w = IO.pipe
|
65
|
+
env = self.spawn_env.merge(
|
66
|
+
SLAVE_INITIAL_STATE_ENV => initial_r.fileno.to_s,
|
67
|
+
SLAVE_RESULT_ENV => result_w.fileno.to_s)
|
68
|
+
|
69
|
+
program_id.refresh
|
70
|
+
@needs_spawn = false
|
71
|
+
pid = Kernel.spawn(env, *cmdline, initial_r => initial_r, result_w => result_w, **spawn_options)
|
72
|
+
initial_r.close
|
73
|
+
result_w.close
|
74
|
+
Marshal.dump(program_id, initial_w)
|
75
|
+
|
76
|
+
@pid = pid
|
77
|
+
@status = nil
|
78
|
+
@result_buffer = ''
|
79
|
+
@result_r = result_r
|
80
|
+
pid
|
81
|
+
|
82
|
+
rescue Exception => e
|
83
|
+
if pid
|
84
|
+
Process.kill 'TERM', pid
|
85
|
+
end
|
86
|
+
result_r.close if result_r && !result_r.closed?
|
87
|
+
raise
|
88
|
+
|
89
|
+
ensure
|
90
|
+
initial_r.close if initial_r && !initial_r.closed?
|
91
|
+
initial_w.close if initial_w && !initial_r.closed?
|
92
|
+
result_w.close if result_w && !result_w.closed?
|
93
|
+
end
|
94
|
+
|
95
|
+
# Whether this slave would need to be spawned, either because it has
|
96
|
+
# never be, or because the program ID changed
|
97
|
+
def needs_spawn?
|
98
|
+
@needs_spawn || !status || program_id.changed?
|
99
|
+
end
|
100
|
+
|
101
|
+
# Whether the slave is running
|
102
|
+
def running?
|
103
|
+
pid && !status
|
104
|
+
end
|
105
|
+
|
106
|
+
# Whether the slave has already ran, and is finished
|
107
|
+
def finished?
|
108
|
+
pid && status
|
109
|
+
end
|
110
|
+
|
111
|
+
# Kill the slave
|
112
|
+
#
|
113
|
+
# @param [Boolean] join whether the method should wait for the child to
|
114
|
+
# end
|
115
|
+
# @see join
|
116
|
+
def kill(signal = 'TERM', join: true)
|
117
|
+
Process.kill signal, pid
|
118
|
+
if join
|
119
|
+
self.join
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Wait for the slave to terminate and call {#finished}
|
124
|
+
def join
|
125
|
+
_, status = Process.waitpid2(pid)
|
126
|
+
finished(status)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Whether the slave behaved properly
|
130
|
+
#
|
131
|
+
# This does *not* indicate whether the slave's intended work has been
|
132
|
+
# done, only that it produced the data expected by Autorespawn. To check
|
133
|
+
# the child's success w.r.t. its execution, check {#status}
|
134
|
+
def success?
|
135
|
+
if !status
|
136
|
+
raise NotFinished, "called {#success?} on a #{pid ? 'running' : 'non-started'} child"
|
137
|
+
end
|
138
|
+
@success
|
139
|
+
end
|
140
|
+
|
141
|
+
# @api private
|
142
|
+
#
|
143
|
+
# Announce that the slave already finished, with the given exit status
|
144
|
+
#
|
145
|
+
# @param [Process::Status] the exit status
|
146
|
+
def finished(status)
|
147
|
+
@status = status
|
148
|
+
read_queued_result
|
149
|
+
begin
|
150
|
+
@subcommands, file_list = Marshal.load(result_buffer)
|
151
|
+
@success = true
|
152
|
+
rescue ArgumentError # "Marshal data too short"
|
153
|
+
@subcommands = Array.new
|
154
|
+
file_list = Array.new
|
155
|
+
@success = false
|
156
|
+
end
|
157
|
+
modified = program_id.register_files(file_list)
|
158
|
+
if !modified.empty?
|
159
|
+
@needs_spawn = true
|
160
|
+
end
|
161
|
+
result_r.close
|
162
|
+
modified
|
163
|
+
end
|
164
|
+
|
165
|
+
# @api private
|
166
|
+
#
|
167
|
+
# Queue any pending result data sent by the slave
|
168
|
+
def read_queued_result
|
169
|
+
while true
|
170
|
+
result_buffer << result_r.read_nonblock(1024)
|
171
|
+
end
|
172
|
+
rescue IO::WaitReadable, EOFError
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
|
data/lib/autorespawn/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
class Autorespawn
|
2
|
+
VERSION = "0.2.0"
|
3
3
|
end
|
data/lib/autorespawn/watch.rb
CHANGED
@@ -1,69 +1,6 @@
|
|
1
|
-
|
1
|
+
class Autorespawn
|
2
2
|
# Functionality to watch a program for change
|
3
3
|
class Watch
|
4
|
-
# Create a pipe and dump the program ID state of the current program
|
5
|
-
# there
|
6
|
-
def self.dump_self_id
|
7
|
-
r, w = IO.pipe
|
8
|
-
Marshal.dump(ProgramID.for_self, w)
|
9
|
-
w.flush
|
10
|
-
return r, w
|
11
|
-
end
|
12
|
-
|
13
|
-
# Automatically autoreload this program when one of the source file
|
14
|
-
# changes
|
15
|
-
#
|
16
|
-
# Call this method from the entry point of your program, giving it the
|
17
|
-
# actual program functionality as a block. The method will exec and
|
18
|
-
# spawn subprocesses at will, when needed, and call the block in these
|
19
|
-
# subprocesses as required.
|
20
|
-
#
|
21
|
-
# At the point of call, all of the program's dependencies must be
|
22
|
-
# already required, as it is on this basis that the auto-reloading will
|
23
|
-
# be done
|
24
|
-
#
|
25
|
-
# This method does NOT return
|
26
|
-
#
|
27
|
-
# @param [Array<String>] command the command to be executed. It is
|
28
|
-
# passed as-is to Kernel.spawn and Kernel.exec
|
29
|
-
# @param options keyword options to pass to Kernel.spawn and Kernel.exec
|
30
|
-
def self.autoreload(*command, **options)
|
31
|
-
if !block_given?
|
32
|
-
raise ArgumentError, "you must provide the actions to perform on reload as a block"
|
33
|
-
end
|
34
|
-
|
35
|
-
# Check if we're being called by an autoreload call already
|
36
|
-
if ENV['autorespawn_AUTORELOAD']
|
37
|
-
program_id = Marshal.load(STDIN)
|
38
|
-
if !program_id.changed?
|
39
|
-
# We can do what is required of us and wait for changes
|
40
|
-
yield
|
41
|
-
new(program_id).wait
|
42
|
-
end
|
43
|
-
|
44
|
-
r, w = dump_self_id
|
45
|
-
exec(Hash['autorespawn_AUTORELOAD' => '1'], *command,
|
46
|
-
in: r, **options)
|
47
|
-
else
|
48
|
-
begin
|
49
|
-
r, w = dump_self_id
|
50
|
-
pid = spawn(Hash['autorespawn_AUTORELOAD' => '1'], *command,
|
51
|
-
in: r, pgroup: true, **options)
|
52
|
-
w.close
|
53
|
-
r.close
|
54
|
-
_, status = Process.waitpid2(pid)
|
55
|
-
exit status.exitcode
|
56
|
-
ensure
|
57
|
-
if pid
|
58
|
-
Process.kill 'TERM', pid
|
59
|
-
end
|
60
|
-
if !$!
|
61
|
-
exit 0
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
4
|
# @return [ProgramID] the reference state
|
68
5
|
attr_reader :current_state
|
69
6
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: autorespawn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sylvain Joyeux
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-09-
|
11
|
+
date: 2015-09-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -98,6 +98,20 @@ dependencies:
|
|
98
98
|
- - "~>"
|
99
99
|
- !ruby/object:Gem::Version
|
100
100
|
version: '2.0'
|
101
|
+
- !ruby/object:Gem::Dependency
|
102
|
+
name: coveralls
|
103
|
+
requirement: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
type: :development
|
109
|
+
prerelease: false
|
110
|
+
version_requirements: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0'
|
101
115
|
description: |
|
102
116
|
This gem implements the functionality to take a signature of the current Ruby
|
103
117
|
program (i.e. the current process) and respawn it whenever the source code or
|
@@ -117,7 +131,9 @@ files:
|
|
117
131
|
- autorespawn.gemspec
|
118
132
|
- lib/autorespawn.rb
|
119
133
|
- lib/autorespawn/exceptions.rb
|
134
|
+
- lib/autorespawn/manager.rb
|
120
135
|
- lib/autorespawn/program_id.rb
|
136
|
+
- lib/autorespawn/slave.rb
|
121
137
|
- lib/autorespawn/version.rb
|
122
138
|
- lib/autorespawn/watch.rb
|
123
139
|
homepage: https://github.com/doudou/autorespawn
|
@@ -132,7 +148,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
132
148
|
requirements:
|
133
149
|
- - ">="
|
134
150
|
- !ruby/object:Gem::Version
|
135
|
-
version:
|
151
|
+
version: 2.1.0
|
136
152
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
153
|
requirements:
|
138
154
|
- - ">="
|
@@ -145,3 +161,4 @@ signing_key:
|
|
145
161
|
specification_version: 4
|
146
162
|
summary: functionality to respawn a Ruby program when its source changes
|
147
163
|
test_files: []
|
164
|
+
has_rdoc:
|