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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ea386cb98108e534b9f65033e7f664ec13321893
4
- data.tar.gz: 7a8e260d15d67f91563da5505f6c42878548301e
3
+ metadata.gz: f350f615e8e53d1e5c4ffa35cf876dba59b3f894
4
+ data.tar.gz: 85ae94deab4d55b202d7ab3595a58e9e4595ddf0
5
5
  SHA512:
6
- metadata.gz: 56804a4ae5f3410c656a0e95d35c1504a215c2bdee3769f65b51496df699c4c4238c1c2dba2490a5c5bca524f7b4ed752f4a32b14f70758c2184bf37f18bb7d2
7
- data.tar.gz: 7d2735d95211f6e70b4ad28ca5204e292bd2775dfdebdff8961332d6b5b84c4d37680d63af48a657fb617f11bb206d967201a70b287888b5182b6ce7946fcba1
6
+ metadata.gz: 64fb8e03ca65dcd7e2989c137a8c21ba74eb7dc946871f6c58c633f95fa11038bf96f62193be29d6e7373d2aeb7201207a3d81d829b341341bbd0d1dfc1d7f2c
7
+ data.tar.gz: 664506705104437abfb05e9f48b2ba4c725548322134078a5bdfac705d291705e7141886865476f8db64fcc7f4506e25a050f5810cfd5f5c725d303281181f09
@@ -1,4 +1,5 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.1.2
3
+ - 2.1.6
4
+ - 2.2.2
4
5
  before_install: gem install bundler -v 1.10.5
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.autorespawn do
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.autorespawn 'program', 'argument0', 'argument1' do
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:
@@ -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
@@ -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
- module Autorespawn
7
- # Your code goes here...
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
- module Autorespawn
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
- module Autorespawn
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 [FileInfo] the file's information
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
- info
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
+
@@ -1,3 +1,3 @@
1
- module Autorespawn
2
- VERSION = "0.1.0"
1
+ class Autorespawn
2
+ VERSION = "0.2.0"
3
3
  end
@@ -1,69 +1,6 @@
1
- module Autorespawn
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.1.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-12 00:00:00.000000000 Z
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: '0'
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: