autorespawn 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
[](https://travis-ci.org/doudou/autorespawn)
|
4
|
+
[](http://badge.fury.io/rb/autorespawn)
|
5
|
+
[](https://coveralls.io/github/doudou/autorespawn?branch=master)
|
6
|
+
[](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:
|