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