fork 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.txt +8 -0
- data/README.markdown +68 -0
- data/Rakefile +10 -0
- data/fork.gemspec +41 -0
- data/lib/fork.rb +637 -0
- data/lib/fork/version.rb +15 -0
- data/test/lib/helper.rb +16 -0
- data/test/runner.rb +20 -0
- data/test/unit/fork.rb +84 -0
- metadata +63 -0
data/LICENSE.txt
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
Copyright (c) 2012, Stefan Rusterholz <stefan.rusterholz@gmail.com>
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
5
|
+
|
6
|
+
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
7
|
+
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
8
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.markdown
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
README
|
2
|
+
======
|
3
|
+
|
4
|
+
|
5
|
+
Summary
|
6
|
+
-------
|
7
|
+
Represents forks (child processes) as objects and makes interaction with forks easy.
|
8
|
+
|
9
|
+
|
10
|
+
Features
|
11
|
+
--------
|
12
|
+
|
13
|
+
* Object oriented usage of forks
|
14
|
+
* Easy-to-use implementation of future (`Fork.future { computation }.call # => result`)
|
15
|
+
* Provides facilities for IO between parent and fork
|
16
|
+
* Supports sending ruby objects to the forked process
|
17
|
+
* Supports reading ruby objects from the forked process
|
18
|
+
|
19
|
+
|
20
|
+
Installation
|
21
|
+
------------
|
22
|
+
`gem install fork`
|
23
|
+
|
24
|
+
|
25
|
+
Usage
|
26
|
+
-----
|
27
|
+
|
28
|
+
An example using a future:
|
29
|
+
|
30
|
+
def fib(n) n < 2 ? n : fib(n-1)+fib(n-2); end # <-- bad implementation of fibonacci
|
31
|
+
future = Fork.future do
|
32
|
+
fib(35)
|
33
|
+
end
|
34
|
+
# do something expensive in the parent process
|
35
|
+
puts future.call # this blocks, until the fork finished, and returns the last value
|
36
|
+
|
37
|
+
|
38
|
+
A more complex example, using some of Fork's features:
|
39
|
+
|
40
|
+
# Create a fork with two-directional IO, which returns values and raises
|
41
|
+
# exceptions in the parent process.
|
42
|
+
fork = Fork.new :to_fork, :from_fork do |fork|
|
43
|
+
while received = fork.receive_object
|
44
|
+
p :fork_received => received
|
45
|
+
end
|
46
|
+
end
|
47
|
+
fork.execute # spawn child process and start executing
|
48
|
+
fork.send_object(123)
|
49
|
+
puts "Fork runs as process with pid #{fork.pid}"
|
50
|
+
fork.send_object(nil) # terminate the fork
|
51
|
+
fork.wait # wait until the fork is indeed terminated
|
52
|
+
puts "Fork is dead, as expected" if fork.dead?
|
53
|
+
|
54
|
+
|
55
|
+
Links
|
56
|
+
-----
|
57
|
+
|
58
|
+
* [Online API Documentation](http://rdoc.info/github/apeiros/fork/)
|
59
|
+
* [Public Repository](https://github.com/apeiros/fork)
|
60
|
+
* [Bug Reporting](https://github.com/apeiros/fork/issues)
|
61
|
+
* [RubyGems Site](https://rubygems.org/gems/fork)
|
62
|
+
|
63
|
+
|
64
|
+
License
|
65
|
+
-------
|
66
|
+
|
67
|
+
You can use this code under the {file:LICENSE.txt BSD-2-Clause License}, free of charge.
|
68
|
+
If you need a different license, please ask the author.
|
data/Rakefile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.expand_path('../rake/lib', __FILE__))
|
2
|
+
Dir.glob(File.expand_path('../rake/tasks/**/*.{rake,task,rb}', __FILE__)) do |task_file|
|
3
|
+
begin
|
4
|
+
import task_file
|
5
|
+
rescue LoadError => e
|
6
|
+
warn "Failed to load task file #{task_file}"
|
7
|
+
warn " #{e.class} #{e.message}"
|
8
|
+
warn " #{e.backtrace.first}"
|
9
|
+
end
|
10
|
+
end
|
data/fork.gemspec
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "fork"
|
5
|
+
s.version = "1.0.0"
|
6
|
+
s.authors = "Stefan Rusterholz"
|
7
|
+
s.email = "stefan.rusterholz@gmail.com"
|
8
|
+
s.homepage = "https://github.com/apeiros/fork"
|
9
|
+
|
10
|
+
s.summary = <<-SUMMARY.gsub(/^ /, '').chomp
|
11
|
+
Represents forks (child processes) as objects and makes interaction with forks easy.
|
12
|
+
SUMMARY
|
13
|
+
s.description = <<-DESCRIPTION.gsub(/^ /, '').chomp
|
14
|
+
Represents forks (child processes) as objects and makes interaction with forks easy.
|
15
|
+
It provides a simple interface to create forked futures, get the return value of the
|
16
|
+
fork, get an exception raised in the fork, and to send objects between parent and
|
17
|
+
forked process.
|
18
|
+
DESCRIPTION
|
19
|
+
|
20
|
+
s.files =
|
21
|
+
Dir['bin/**/*'] +
|
22
|
+
Dir['examples/**/*'] +
|
23
|
+
Dir['lib/**/*'] +
|
24
|
+
Dir['rake/**/*'] +
|
25
|
+
Dir['test/**/*'] +
|
26
|
+
Dir['*.gemspec'] +
|
27
|
+
%w[
|
28
|
+
LICENSE.txt
|
29
|
+
Rakefile
|
30
|
+
README.markdown
|
31
|
+
]
|
32
|
+
|
33
|
+
if File.directory?('bin') then
|
34
|
+
executables = Dir.chdir('bin') { Dir.glob('**/*').select { |f| File.executable?(f) } }
|
35
|
+
s.executables = executables unless executables.empty?
|
36
|
+
end
|
37
|
+
|
38
|
+
s.required_rubygems_version = Gem::Requirement.new("> 1.3.1")
|
39
|
+
s.rubygems_version = "1.3.1"
|
40
|
+
s.specification_version = 3
|
41
|
+
end
|
data/lib/fork.rb
ADDED
@@ -0,0 +1,637 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
require 'fork/version'
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
# An object representing a fork, containing data about it like pid, exit_status,
|
10
|
+
# exception etc.
|
11
|
+
#
|
12
|
+
# It also provides facilities for parent and child process to communicate
|
13
|
+
# with each other.
|
14
|
+
#
|
15
|
+
# @example Usage
|
16
|
+
# def fib(n) n < 2 ? n : fib(n-1)+fib(n-2); end # <-- bad implementation of fibonacci
|
17
|
+
# fork = Fork.new :return do
|
18
|
+
# fib(35)
|
19
|
+
# end
|
20
|
+
# fork.execute
|
21
|
+
# puts "Forked child process with pid #{fork.pid} is currently #{fork.alive? ? 'alive' : 'dead'}"
|
22
|
+
# puts fork.return_value # this blocks, until the fork finished, and returns the last value
|
23
|
+
#
|
24
|
+
# @example The same, but a bit simpler
|
25
|
+
# def fib(n) n < 2 ? n : fib(n-1)+fib(n-2); end # <-- bad implementation of fibonacci
|
26
|
+
# fork = Fork.execute :return do
|
27
|
+
# fib(35)
|
28
|
+
# end
|
29
|
+
# puts fork.return_value # this blocks, until the fork finished, and returns the last value
|
30
|
+
#
|
31
|
+
# @example And the simplest version, if all you care about is the return value
|
32
|
+
# def fib(n) n < 2 ? n : fib(n-1)+fib(n-2); end # <-- bad implementation of fibonacci
|
33
|
+
# future = Fork.future do
|
34
|
+
# fib(35)
|
35
|
+
# end
|
36
|
+
# puts future.call # this blocks, until the fork finished, and returns the last value
|
37
|
+
#
|
38
|
+
# @note
|
39
|
+
# You should only interact between parent and fork by the means provided by the Fork
|
40
|
+
# class.
|
41
|
+
class Fork
|
42
|
+
|
43
|
+
# Exceptions that have to be ignored in the child's handling of exceptions
|
44
|
+
IgnoreExceptions = [::SystemExit]
|
45
|
+
|
46
|
+
# Raised when a fork raises an exception that can't be dumped
|
47
|
+
# This is the case if the exception is either an anonymous class or
|
48
|
+
# contains undumpable data (anonymous ancestor, added state, …)
|
49
|
+
class UndumpableException < StandardError; end
|
50
|
+
|
51
|
+
# Raised when you try to do something which would have required creating
|
52
|
+
# the fork instance with a specific flag which wasn't provided.
|
53
|
+
class FlagNotSpecified < StandardError; end
|
54
|
+
|
55
|
+
# Raised when you try to write to/read from a fork which is not running yet or not
|
56
|
+
# anymore.
|
57
|
+
class NotRunning < StandardError; end
|
58
|
+
|
59
|
+
# The default flags Fork#initialize uses
|
60
|
+
DefaultFlags = Hash.new { |_hash, key| raise ArgumentError, "Unknown flag #{key}" }.merge({
|
61
|
+
:exceptions => false,
|
62
|
+
:death_notice => false,
|
63
|
+
:return => false,
|
64
|
+
:to_fork => false,
|
65
|
+
:from_fork => false,
|
66
|
+
:ctrl => false,
|
67
|
+
})
|
68
|
+
|
69
|
+
# Reads an object sent via Fork.read_marshalled from the passed io.
|
70
|
+
# Raises EOFError if the io was closed on the remote end.
|
71
|
+
#
|
72
|
+
# @return [Object] The deserialized object which was sent through the IO
|
73
|
+
#
|
74
|
+
# @see Fork.write_marshalled Implements the opposite operation: writing an object on an IO.
|
75
|
+
def self.read_marshalled(io)
|
76
|
+
size = io.read(4)
|
77
|
+
raise EOFError unless size
|
78
|
+
size = size.unpack("I").first
|
79
|
+
marshalled = io.read(size)
|
80
|
+
Marshal.load(marshalled)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Writes an object in serialized form to the passed IO.
|
84
|
+
# Important: certain objects are not marshallable, e.g. IOs, Procs and
|
85
|
+
# anonymous modules and classes.
|
86
|
+
#
|
87
|
+
# @return [Integer] The number of bytes written to the IO (see IO#write)
|
88
|
+
#
|
89
|
+
# @see Fork.read_marshalled Implements the opposite operation: writing an object on an IO.
|
90
|
+
def self.write_marshalled(io, obj)
|
91
|
+
marshalled = Marshal.dump(obj)
|
92
|
+
io.write([marshalled.size].pack("I"))
|
93
|
+
io.write(marshalled)
|
94
|
+
end
|
95
|
+
|
96
|
+
# A simple forked-future implementation. Will process the block in a fork,
|
97
|
+
# blocks upon request of the result until the result is present.
|
98
|
+
# If the forked code raises an exception, invoking call on the proc will raise that
|
99
|
+
# exception in the parent process.
|
100
|
+
#
|
101
|
+
# @param args
|
102
|
+
# All parameters passed to Fork.future are passed on to the block.
|
103
|
+
#
|
104
|
+
# @return [Proc]
|
105
|
+
# A lambda which upon invoking #call will block until the result of the block is
|
106
|
+
# calculated.
|
107
|
+
#
|
108
|
+
# @example Usage
|
109
|
+
# # A
|
110
|
+
# Fork.future { 1 }.call # => 1
|
111
|
+
#
|
112
|
+
# # B
|
113
|
+
# result = Fork.future { sleep 2; 1 } # assume a complex computation instead of sleep(2)
|
114
|
+
# sleep 2 # assume another complex computation
|
115
|
+
# start = Time.now
|
116
|
+
# result.call # => 1
|
117
|
+
# elapsed_time = Time.now-start # => <1s as the work was done parallely
|
118
|
+
def self.future(*args)
|
119
|
+
obj = execute :return => true do |parent|
|
120
|
+
yield(*args)
|
121
|
+
end
|
122
|
+
|
123
|
+
lambda { obj.return_value }
|
124
|
+
end
|
125
|
+
|
126
|
+
# A simple forked-callback implementation. Will process the block in a fork,
|
127
|
+
# block until it has finished processing and returns the return value of the
|
128
|
+
# block.
|
129
|
+
# This can be useful if you want to process something that will (or might)
|
130
|
+
# irreversibly corrupt the environment. Doing that in a subprocess will leave
|
131
|
+
# the parent untouched.
|
132
|
+
#
|
133
|
+
# @param args
|
134
|
+
# All parameters passed to Fork.return are passed on to the block.
|
135
|
+
#
|
136
|
+
# @return
|
137
|
+
# Returns the result of the block.
|
138
|
+
#
|
139
|
+
# @example Usage
|
140
|
+
# Fork.return { 1 } # => 1
|
141
|
+
def self.return(*args)
|
142
|
+
obj = execute :return => true do |parent|
|
143
|
+
yield(*args)
|
144
|
+
end
|
145
|
+
obj.return_value
|
146
|
+
end
|
147
|
+
|
148
|
+
# Create a Fork instance and immediatly start executing it.
|
149
|
+
# Equivalent to just call Fork.new(*args) { ... }.execute
|
150
|
+
#
|
151
|
+
# Returns the Fork instance.
|
152
|
+
# See Fork#initialize
|
153
|
+
def self.execute(*args, &block)
|
154
|
+
new(*args, &block).execute
|
155
|
+
end
|
156
|
+
|
157
|
+
# The process id of the fork
|
158
|
+
#
|
159
|
+
# @note
|
160
|
+
# You *must not* directly interact with the forked process using the pid.
|
161
|
+
# This may lead to unexpected conflicts with Fork's internal mechanisms.
|
162
|
+
attr_reader :pid
|
163
|
+
|
164
|
+
# Readable IO
|
165
|
+
# Allows the parent to read data from the fork, and the fork to read data from the
|
166
|
+
# parent.
|
167
|
+
# Requires the :to_fork and/or :from_fork flag to be set.
|
168
|
+
attr_reader :readable_io
|
169
|
+
|
170
|
+
# Writable IO
|
171
|
+
# Allows the parent to write data to the fork, and the fork to write data to the parent.
|
172
|
+
# Requires the :to_fork and/or :from_fork flag to be set.
|
173
|
+
attr_reader :writable_io
|
174
|
+
|
175
|
+
# Control IO (reserved for exception and death-notice passing)
|
176
|
+
attr_reader :ctrl
|
177
|
+
|
178
|
+
# Create a new Fork instance.
|
179
|
+
# @param [Symbol, Hash] flags
|
180
|
+
# Tells the fork what facilities to provide. You can pass the flags either as a list
|
181
|
+
# of symbols, or as a Hash, or even mixed (the hash must be the last argument then).
|
182
|
+
#
|
183
|
+
# Valid flags are:
|
184
|
+
# * :return Make the value of the last expression (return value) available to
|
185
|
+
# the parent process
|
186
|
+
# * :exceptions Pass exceptions of the fork to the parent, making it available via
|
187
|
+
# Fork#exception
|
188
|
+
# * :death_notice Send the parent process an information when done processing
|
189
|
+
# * :to_fork You can write to the Fork from the parent process, and read in the
|
190
|
+
# child process
|
191
|
+
# * :from_fork You can read from the Fork in the parent process and write in the
|
192
|
+
# child process
|
193
|
+
# * :ctrl Provides an additional IO for control mechanisms
|
194
|
+
#
|
195
|
+
# Some flags implicitly set other flags. For example, :return will set :exceptions and
|
196
|
+
# :ctrl, :exceptions will set :ctrl and :death_notice will also set :ctrl.
|
197
|
+
#
|
198
|
+
# The subprocess is not immediatly executed, you must invoke #execute on the Fork
|
199
|
+
# instance in order to get it executed. Only then #pid, #in, #out and #ctrl will be
|
200
|
+
# available. Also all IO related methods won't work before that.
|
201
|
+
def initialize(*flags, &block)
|
202
|
+
raise ArgumentError, "No block given" unless block
|
203
|
+
if flags.last.is_a?(Hash) then
|
204
|
+
@flags = DefaultFlags.merge(flags.pop)
|
205
|
+
else
|
206
|
+
@flags = DefaultFlags.dup
|
207
|
+
end
|
208
|
+
flags.each do |flag|
|
209
|
+
raise ArgumentError, "Unknown flag #{flag.inspect}" unless @flags.has_key?(flag)
|
210
|
+
@flags[flag] = true
|
211
|
+
end
|
212
|
+
@flags[:ctrl] = true if @flags.values_at(:exceptions, :death_notice, :return).any?
|
213
|
+
@flags[:exceptions] = true if @flags[:return]
|
214
|
+
|
215
|
+
@parent = true
|
216
|
+
@alive = nil
|
217
|
+
@pid = nil
|
218
|
+
@process_status = nil
|
219
|
+
@readable_io = nil
|
220
|
+
@writable_io = nil
|
221
|
+
@ctrl = nil
|
222
|
+
@block = block
|
223
|
+
end
|
224
|
+
|
225
|
+
# Creates the fork (subprocess) and starts executing it.
|
226
|
+
#
|
227
|
+
# @return [self]
|
228
|
+
def execute
|
229
|
+
ctrl_read, ctrl_write, fork_read, parent_write, parent_read, fork_write = nil
|
230
|
+
|
231
|
+
fork_read, parent_write = binary_pipe if @flags[:to_fork]
|
232
|
+
parent_read, fork_write = binary_pipe if @flags[:from_fork]
|
233
|
+
ctrl_read, ctrl_write = binary_pipe if @flags[:ctrl]
|
234
|
+
|
235
|
+
@alive = true
|
236
|
+
|
237
|
+
pid = Process.fork do
|
238
|
+
@parent = false
|
239
|
+
parent_write.close if parent_write
|
240
|
+
parent_read.close if parent_read
|
241
|
+
ctrl_read.close if ctrl_read
|
242
|
+
complete!(Process.pid, fork_read, fork_write, ctrl_write)
|
243
|
+
|
244
|
+
child_process
|
245
|
+
end
|
246
|
+
|
247
|
+
fork_write.close if fork_write
|
248
|
+
fork_read.close if fork_read
|
249
|
+
ctrl_write.close if ctrl_write
|
250
|
+
complete!(pid, parent_read, parent_write, ctrl_read)
|
251
|
+
|
252
|
+
self
|
253
|
+
end
|
254
|
+
|
255
|
+
# @return [Boolean] Whether this fork sends the final exception to the parent
|
256
|
+
def handle_exceptions?
|
257
|
+
@flags[:exceptions]
|
258
|
+
end
|
259
|
+
|
260
|
+
# @return [Boolean] Whether this fork sends a death notice to the parent
|
261
|
+
def death_notice?
|
262
|
+
@flags[:death_notice]
|
263
|
+
end
|
264
|
+
|
265
|
+
# @return [Boolean] Whether this forks terminal value is returned to the parent
|
266
|
+
def returns?
|
267
|
+
@flags[:return]
|
268
|
+
end
|
269
|
+
|
270
|
+
# @return [Boolean] Whether the other process can write to this process.
|
271
|
+
def has_in?
|
272
|
+
@flags[parent? ? :from_fork : :to_fork]
|
273
|
+
end
|
274
|
+
|
275
|
+
# @return [Boolean] Whether this process can write to the other process.
|
276
|
+
def has_out?
|
277
|
+
@flags[parent? ? :to_fork : :from_fork]
|
278
|
+
end
|
279
|
+
|
280
|
+
# @return [Boolean] Whether parent and fork use a control-io.
|
281
|
+
def has_ctrl?
|
282
|
+
@flags[:ctrl]
|
283
|
+
end
|
284
|
+
|
285
|
+
# @return [Boolean]
|
286
|
+
# Whether the current code is executed in the parent of the fork.
|
287
|
+
def parent?
|
288
|
+
@parent
|
289
|
+
end
|
290
|
+
|
291
|
+
# @return [Boolean]
|
292
|
+
# Whether the current code is executed in the fork, as opposed to the parent.
|
293
|
+
def fork?
|
294
|
+
!@parent
|
295
|
+
end
|
296
|
+
|
297
|
+
# Sets the io to communicate with the parent/child
|
298
|
+
def complete!(pid, readable_io, writable_io, ctrl_io) # :nodoc:
|
299
|
+
raise "Can't call complete! more than once" if @pid
|
300
|
+
@pid = pid
|
301
|
+
@readable_io = readable_io
|
302
|
+
@writable_io = writable_io
|
303
|
+
@ctrl = ctrl_io
|
304
|
+
end
|
305
|
+
|
306
|
+
# Process::Status for dead forks, nil for live forks
|
307
|
+
def process_status(blocking=true)
|
308
|
+
@process_status || begin
|
309
|
+
_wait(blocking)
|
310
|
+
@process_status
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
# The exit status of this fork.
|
315
|
+
# See Process::Status#exitstatus
|
316
|
+
def exit_status(blocking=true)
|
317
|
+
@exit_status || begin
|
318
|
+
_wait(blocking)
|
319
|
+
@exit_status
|
320
|
+
rescue NotRunning
|
321
|
+
raise if blocking # calling exit status on a not-yet started fork is an exception, nil otherwise
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
# Blocks until the fork has exited.
|
326
|
+
#
|
327
|
+
# @return [Boolean]
|
328
|
+
# Whether the fork exited with a successful exit status (status code 0).
|
329
|
+
def success?
|
330
|
+
exit_status.zero?
|
331
|
+
end
|
332
|
+
|
333
|
+
# Blocks until the fork has exited.
|
334
|
+
#
|
335
|
+
# @return [Boolean]
|
336
|
+
# Whether the fork exited with an unsuccessful exit status (status code != 0).
|
337
|
+
def failure?
|
338
|
+
!success?
|
339
|
+
end
|
340
|
+
|
341
|
+
# The exception that terminated the fork
|
342
|
+
# Requires the :exceptions flag to be set when creating the fork.
|
343
|
+
def exception(blocking=true)
|
344
|
+
@exception || begin
|
345
|
+
raise FlagNotSpecified, "You must set the :exceptions flag when forking in order to use this" unless handle_exceptions?
|
346
|
+
_wait(blocking)
|
347
|
+
@exception
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
# Blocks until the fork returns
|
352
|
+
def return_value(blocking=true)
|
353
|
+
@return_value || begin
|
354
|
+
raise FlagNotSpecified, "You must set the :return flag when forking in order to use this" unless returns?
|
355
|
+
_wait(blocking)
|
356
|
+
raise @exception if @exception
|
357
|
+
@return_value
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
# Whether this fork is still running (= is alive) or already exited.
|
362
|
+
def alive?
|
363
|
+
@pid && !exit_status(false)
|
364
|
+
end
|
365
|
+
|
366
|
+
# Whether this fork is still running or already exited (= is dead).
|
367
|
+
def dead?
|
368
|
+
!alive?
|
369
|
+
end
|
370
|
+
|
371
|
+
# In the parent process: read data from the fork.
|
372
|
+
# In the forked process: read data from the parent.
|
373
|
+
# Works just like IO#gets.
|
374
|
+
#
|
375
|
+
# @return [String, nil] The data that the forked/parent process has written.
|
376
|
+
def gets(*args)
|
377
|
+
@readable_io.gets(*args)
|
378
|
+
rescue NoMethodError
|
379
|
+
raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
|
380
|
+
raise FlagNotSpecified, "You must set the :to_fork flag when forking in order to use this" unless @readable_io
|
381
|
+
raise
|
382
|
+
end
|
383
|
+
|
384
|
+
# In the parent process: read data from the fork.
|
385
|
+
# In the forked process: read data from the parent.
|
386
|
+
# Works just like IO#read.
|
387
|
+
#
|
388
|
+
# @return [String, nil] The data that the forked/parent process has written.
|
389
|
+
def read(*args)
|
390
|
+
@readable_io.read(*args)
|
391
|
+
rescue NoMethodError
|
392
|
+
raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
|
393
|
+
raise FlagNotSpecified, "You must set the :to_fork flag when forking in order to use this" unless @readable_io
|
394
|
+
raise
|
395
|
+
end
|
396
|
+
|
397
|
+
# In the parent process: read data from the fork.
|
398
|
+
# In the forked process: read data from the parent.
|
399
|
+
# Works just like IO#read_nonblock.
|
400
|
+
#
|
401
|
+
# @return [String, nil] The data that the forked/parent process has written.
|
402
|
+
def read_nonblock(*args)
|
403
|
+
@readable_io.read_nonblock(*args)
|
404
|
+
rescue NoMethodError
|
405
|
+
raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
|
406
|
+
raise FlagNotSpecified, "You must set the :to_fork flag when forking in order to use this" unless @readable_io
|
407
|
+
raise
|
408
|
+
end
|
409
|
+
|
410
|
+
# In the parent process: read on object sent by the fork.
|
411
|
+
# In the forked process: read on object sent by the parent.
|
412
|
+
#
|
413
|
+
# @return [Object] The object that the forked/parent process has sent.
|
414
|
+
#
|
415
|
+
# @see Fork#send_object An example can be found in the docs of Fork#send_object.
|
416
|
+
def receive_object
|
417
|
+
Fork.read_marshalled(@readable_io)
|
418
|
+
rescue NoMethodError
|
419
|
+
raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
|
420
|
+
raise FlagNotSpecified, "You must set the :from_fork flag when forking in order to use this" unless @readable_io
|
421
|
+
raise
|
422
|
+
end
|
423
|
+
|
424
|
+
# In the parent process: Write to the fork.
|
425
|
+
# In the forked process: Write to the parent.
|
426
|
+
# Works just like IO#puts
|
427
|
+
#
|
428
|
+
# @return [nil]
|
429
|
+
def puts(*args)
|
430
|
+
@writable_io.puts(*args)
|
431
|
+
rescue NoMethodError
|
432
|
+
raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
|
433
|
+
raise FlagNotSpecified, "You must set the :from_fork flag when forking in order to use this" unless @writable_io
|
434
|
+
raise
|
435
|
+
end
|
436
|
+
|
437
|
+
# In the parent process: Write to the fork.
|
438
|
+
# In the forked process: Write to the parent.
|
439
|
+
# Works just like IO#write
|
440
|
+
#
|
441
|
+
# @return [Integer] The number of bytes written
|
442
|
+
def write(*args)
|
443
|
+
@writable_io.write(*args)
|
444
|
+
rescue NoMethodError
|
445
|
+
raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
|
446
|
+
raise FlagNotSpecified, "You must set the :from_fork flag when forking in order to use this" unless @writable_io
|
447
|
+
raise
|
448
|
+
end
|
449
|
+
|
450
|
+
# Read a single instruction sent via @ctrl, used by :exception, :death_notice and
|
451
|
+
# :return_value
|
452
|
+
#
|
453
|
+
# @return [self]
|
454
|
+
def read_remaining_ctrl(_wait_upon_eof=true) # :nodoc:
|
455
|
+
loop do # EOFError will terminate this loop
|
456
|
+
instruction, data = *Fork.read_marshalled(@ctrl)
|
457
|
+
case instruction
|
458
|
+
when :exception
|
459
|
+
@exception = data
|
460
|
+
when :death_notice
|
461
|
+
_wait if _wait_upon_eof
|
462
|
+
_wait_upon_eof = false
|
463
|
+
when :return_value
|
464
|
+
@return_value = data
|
465
|
+
else
|
466
|
+
raise "Unknown control instruction #{instruction} in fork #{fork}"
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
self
|
471
|
+
rescue EOFError # closed
|
472
|
+
_wait(false) if _wait_upon_eof # update
|
473
|
+
|
474
|
+
self
|
475
|
+
rescue NoMethodError
|
476
|
+
raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
|
477
|
+
raise FlagNotSpecified, "You must set the :ctrl flag when forking in order to use this" unless @ctrl
|
478
|
+
raise
|
479
|
+
end
|
480
|
+
|
481
|
+
# Sends an object to the parent process.
|
482
|
+
# The parent process can read it using Fork#receive_object.
|
483
|
+
#
|
484
|
+
# @example Usage
|
485
|
+
# Demo = Struct.new(:a, :b, :c)
|
486
|
+
# fork = Fork.new :from_fork do |parent|
|
487
|
+
# parent.send_object({:a => 'little', :nested => ['hash']})
|
488
|
+
# parent.send_object(Demo.new(1, :two, "three"))
|
489
|
+
# end
|
490
|
+
# p :received => fork.receive_object # -> {:received=>{:a=>"little", :nested=>["hash"]}}
|
491
|
+
# p :received => fork.receive_object # -> {:received=>#<struct Demo a=1, b=:two, c="three">}
|
492
|
+
#
|
493
|
+
# @see Fork#receive_object Fork#receive_object implements the opposite.
|
494
|
+
def send_object(obj)
|
495
|
+
Fork.write_marshalled(@writable_io, obj)
|
496
|
+
self
|
497
|
+
rescue NoMethodError
|
498
|
+
raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
|
499
|
+
raise FlagNotSpecified, "You must set the :from_fork flag when forking in order to use this" unless @writable_io
|
500
|
+
raise
|
501
|
+
end
|
502
|
+
|
503
|
+
# Wait for this fork to terminate.
|
504
|
+
# Returns self
|
505
|
+
#
|
506
|
+
# @example Usage
|
507
|
+
# start = Time.now
|
508
|
+
# fork = Fork.new do sleep 20 end
|
509
|
+
# fork.wait
|
510
|
+
# (Time.now-start).floor # => 20
|
511
|
+
def wait
|
512
|
+
_wait unless @process_status
|
513
|
+
self
|
514
|
+
end
|
515
|
+
|
516
|
+
# Sends the (SIG)HUP signal to this fork.
|
517
|
+
# This is "gently asking the process to terminate".
|
518
|
+
# This gives the process a chance to perform some cleanup.
|
519
|
+
# See Fork#kill!, Fork#signal, Process.kill
|
520
|
+
def kill
|
521
|
+
raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
|
522
|
+
Process.kill("HUP", @pid)
|
523
|
+
end
|
524
|
+
|
525
|
+
# Sends the (SIG)KILL signal to this fork.
|
526
|
+
# The process will be immediatly terminated and will not have a chance to
|
527
|
+
# do any cleanup.
|
528
|
+
# See Fork#kill, Fork#signal, Process.kill
|
529
|
+
def kill!
|
530
|
+
raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
|
531
|
+
Process.kill("KILL", @pid)
|
532
|
+
end
|
533
|
+
|
534
|
+
# Sends the given signal to this fork
|
535
|
+
# See Fork#kill, Fork#kill!, Process.kill
|
536
|
+
def signal(sig)
|
537
|
+
raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
|
538
|
+
Process.kill(sig, @pid)
|
539
|
+
end
|
540
|
+
|
541
|
+
# Close all IOs
|
542
|
+
def close # :nodoc:
|
543
|
+
raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
|
544
|
+
@readable_io.close if @readable_io
|
545
|
+
@writable_io.close if @writable_io
|
546
|
+
@ctrl.close if @ctrl
|
547
|
+
end
|
548
|
+
|
549
|
+
# @private
|
550
|
+
# Duping a fork instance is prohibited. See Object#dup.
|
551
|
+
def dup # :nodoc:
|
552
|
+
raise TypeError, "can't dup #{self.class}"
|
553
|
+
end
|
554
|
+
|
555
|
+
# @private
|
556
|
+
# Cloning a fork instance is prohibited. See Object#clone.
|
557
|
+
def clone # :nodoc:
|
558
|
+
raise TypeError, "can't clone #{self.class}"
|
559
|
+
end
|
560
|
+
|
561
|
+
# @private
|
562
|
+
# See Object#inspect
|
563
|
+
def inspect # :nodoc:
|
564
|
+
sprintf "#<%p pid=%p alive=%p>", self.class, @pid, @alive
|
565
|
+
end
|
566
|
+
|
567
|
+
private
|
568
|
+
# @private
|
569
|
+
# Work around issues in 1.9.3-p194 (it has difficulties with the encoding settings of
|
570
|
+
# the pipes).
|
571
|
+
#
|
572
|
+
# @return [Array<IO>]
|
573
|
+
# Returns a pair of IO instances, just like IO::pipe. The IO's encoding is set to
|
574
|
+
# binary.
|
575
|
+
def binary_pipe
|
576
|
+
in_io, out_io = IO.pipe(Encoding::BINARY)
|
577
|
+
in_io.set_encoding(Encoding::BINARY)
|
578
|
+
out_io.set_encoding(Encoding::BINARY)
|
579
|
+
|
580
|
+
[in_io, out_io]
|
581
|
+
end
|
582
|
+
|
583
|
+
# @private
|
584
|
+
# Internal wait method that waits for the forked process to exit and collects
|
585
|
+
# information when the process exits.
|
586
|
+
#
|
587
|
+
# @param [Boolean] blocking
|
588
|
+
# If blocking is true, the method blocks until the fork exits, otherwise it
|
589
|
+
# will return immediately.
|
590
|
+
#
|
591
|
+
# @return [self]
|
592
|
+
def _wait(blocking=true)
|
593
|
+
raise NotRunning unless @pid
|
594
|
+
|
595
|
+
_, status = *Process.wait2(@pid, blocking ? 0 : Process::WNOHANG)
|
596
|
+
if status then
|
597
|
+
@process_status = status
|
598
|
+
@exit_status = status.exitstatus
|
599
|
+
read_remaining_ctrl if has_ctrl?
|
600
|
+
end
|
601
|
+
rescue Errno::ECHILD # can happen if the process is already collected
|
602
|
+
raise "Can't determine exit status of #{self}, make sure to not interfere with process handling externally" unless @process_status
|
603
|
+
self
|
604
|
+
end
|
605
|
+
|
606
|
+
# @private
|
607
|
+
#
|
608
|
+
# Embedds the forked code into everything needed to handle return value, exceptions,
|
609
|
+
# cleanup etc.
|
610
|
+
def child_process
|
611
|
+
return_value = @block.call(self)
|
612
|
+
Fork.write_marshalled(@ctrl, [:return_value, return_value]) if returns?
|
613
|
+
rescue *IgnoreExceptions
|
614
|
+
raise # reraise ignored exceptions as-is
|
615
|
+
rescue Exception => e
|
616
|
+
$stdout.puts "Exception in child #{$$}: #{e}", *e.backtrace.first(5)
|
617
|
+
if handle_exceptions?
|
618
|
+
begin
|
619
|
+
Fork.write_marshalled(@ctrl, [:exception, e])
|
620
|
+
rescue TypeError # dumping the exception was not possible, try to extract as much information as possible
|
621
|
+
class_name = String(e.class.name) rescue "<<Unable to extract classname>>"
|
622
|
+
class_name = "<<No classname>>" if class_name.empty?
|
623
|
+
message = String(e.message) rescue "<<Unable to extract message>>"
|
624
|
+
backtrace = Array(e.backtrace).map { |line| String(line) rescue "<<bogus backtrace-line>>" } rescue ["<<Unable to extract backtrace>>"]
|
625
|
+
rewritten = UndumpableException.new("Could not send original exception to parent. Original exception #{class_name}: '#{message}'")
|
626
|
+
rewritten.set_backtrace backtrace
|
627
|
+
Fork.write_marshalled(@ctrl, [:exception, rewritten])
|
628
|
+
rescue Exception
|
629
|
+
# Something entirely unexpceted happened, ensure at least that we exit with status 1
|
630
|
+
end
|
631
|
+
end
|
632
|
+
exit! 1
|
633
|
+
ensure
|
634
|
+
Fork.write_marshalled(@ctrl, [:death_notice]) if death_notice?
|
635
|
+
close
|
636
|
+
end
|
637
|
+
end
|
data/lib/fork/version.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'rubygems/version' # newer rubygems use this
|
5
|
+
rescue LoadError
|
6
|
+
require 'gem/version' # older rubygems use this
|
7
|
+
end
|
8
|
+
|
9
|
+
|
10
|
+
|
11
|
+
class Fork
|
12
|
+
|
13
|
+
# The currently required version of the Fork gem
|
14
|
+
Version = Gem::Version.new("1.0.0")
|
15
|
+
end
|
data/test/lib/helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
class Test::Unit::TestCase
|
4
|
+
def self.test(desc, &impl)
|
5
|
+
define_method("test #{desc}", &impl)
|
6
|
+
end
|
7
|
+
|
8
|
+
def capture_stdout
|
9
|
+
captured = StringIO.new
|
10
|
+
$stdout = captured
|
11
|
+
yield
|
12
|
+
captured.string
|
13
|
+
ensure
|
14
|
+
$stdout = STDOUT
|
15
|
+
end
|
16
|
+
end
|
data/test/runner.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# run with `ruby test/runner.rb`
|
2
|
+
# if you only want to run a single test-file: `ruby test/runner.rb testfile.rb`
|
3
|
+
|
4
|
+
$LOAD_PATH << File.expand_path('../../lib', __FILE__)
|
5
|
+
$LOAD_PATH << File.expand_path('../../test/lib', __FILE__)
|
6
|
+
TEST_DIR = File.expand_path('../../test', __FILE__)
|
7
|
+
|
8
|
+
require 'test/unit'
|
9
|
+
require 'helper'
|
10
|
+
|
11
|
+
if ENV['COVERAGE']
|
12
|
+
require 'simplecov'
|
13
|
+
SimpleCov.start
|
14
|
+
end
|
15
|
+
|
16
|
+
units = ARGV.empty? ? Dir["#{TEST_DIR}/unit/**/*.rb"] : ARGV
|
17
|
+
|
18
|
+
units.each do |unit|
|
19
|
+
load unit
|
20
|
+
end
|
data/test/unit/fork.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'fork'
|
2
|
+
|
3
|
+
class ForkTest < Test::Unit::TestCase
|
4
|
+
test "Examples from readme" do
|
5
|
+
fork = Fork.new :to_fork, :from_fork do |fork|
|
6
|
+
while received = fork.receive_object
|
7
|
+
p :fork_received => received
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
output = capture_stdout do
|
12
|
+
fork.execute # spawn child process and start executing
|
13
|
+
fork.send_object(123)
|
14
|
+
puts "Fork runs as process with pid #{fork.pid}"
|
15
|
+
fork.send_object(nil) # terminate the fork
|
16
|
+
fork.wait # wait until the fork is indeed terminated
|
17
|
+
puts "Fork is dead, as expected" if fork.dead?
|
18
|
+
end
|
19
|
+
|
20
|
+
assert_match(/Fork runs as process with pid \d+\nFork is dead, as expected\n/, output)
|
21
|
+
assert fork.success?
|
22
|
+
end
|
23
|
+
|
24
|
+
test "Examples from Fork class docs" do
|
25
|
+
def fib(n) n < 2 ? n : fib(n-1)+fib(n-2); end # <-- bad implementation of fibonacci
|
26
|
+
fork = Fork.new :return do
|
27
|
+
fib(20)
|
28
|
+
end
|
29
|
+
fork.execute
|
30
|
+
assert fork.pid
|
31
|
+
assert fork.alive?
|
32
|
+
assert fork.return_value
|
33
|
+
|
34
|
+
fork = Fork.execute :return do
|
35
|
+
fib(20)
|
36
|
+
end
|
37
|
+
assert fork.return_value
|
38
|
+
|
39
|
+
future = Fork.future do
|
40
|
+
fib(20)
|
41
|
+
end
|
42
|
+
assert future.call
|
43
|
+
end
|
44
|
+
|
45
|
+
test "Examples from Fork.future docs" do
|
46
|
+
assert_equal 1, Fork.future { 1 }.call
|
47
|
+
|
48
|
+
assert_nothing_raised do
|
49
|
+
result = Fork.future { sleep 0.5; 1 } # assume a complex computation instead of sleep(2)
|
50
|
+
sleep 0.5 # assume another complex computation
|
51
|
+
start = Time.now
|
52
|
+
assert_equal 1, result.call # => 1
|
53
|
+
elapsed_time = Time.now-start # => <1s as the work was done parallely
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
test "Examples from Fork.return docs" do
|
58
|
+
assert_equal 1, Fork.return { 1 }
|
59
|
+
end
|
60
|
+
|
61
|
+
test "Examples from Fork#send_object docs" do
|
62
|
+
Demo = Struct.new(:a, :b, :c)
|
63
|
+
fork = Fork.new :from_fork do |parent|
|
64
|
+
parent.send_object({:a => 'little', :nested => ['hash']})
|
65
|
+
parent.send_object(Demo.new(1, :two, "three"))
|
66
|
+
end
|
67
|
+
fork.execute
|
68
|
+
assert_equal({:a=>"little", :nested=>["hash"]}, fork.receive_object)
|
69
|
+
assert_equal(Demo.new(1, :two, "three"), fork.receive_object)
|
70
|
+
end
|
71
|
+
|
72
|
+
test "Fork.future { value }.call returns value" do
|
73
|
+
value = 15
|
74
|
+
assert_equal value, Fork.future { value }.call
|
75
|
+
end
|
76
|
+
|
77
|
+
test "Fork.future { value }.call returns value, even when the process is already gone" do
|
78
|
+
value = 15
|
79
|
+
future = Fork.future { value }
|
80
|
+
sleep(0.5)
|
81
|
+
result = future.call
|
82
|
+
assert_equal value, result
|
83
|
+
end
|
84
|
+
end
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fork
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Stefan Rusterholz
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-05-12 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: ! 'Represents forks (child processes) as objects and makes interaction
|
15
|
+
with forks easy.
|
16
|
+
|
17
|
+
It provides a simple interface to create forked futures, get the return value of
|
18
|
+
the
|
19
|
+
|
20
|
+
fork, get an exception raised in the fork, and to send objects between parent and
|
21
|
+
|
22
|
+
forked process.'
|
23
|
+
email: stefan.rusterholz@gmail.com
|
24
|
+
executables: []
|
25
|
+
extensions: []
|
26
|
+
extra_rdoc_files: []
|
27
|
+
files:
|
28
|
+
- lib/fork/version.rb
|
29
|
+
- lib/fork.rb
|
30
|
+
- test/lib/helper.rb
|
31
|
+
- test/runner.rb
|
32
|
+
- test/unit/fork.rb
|
33
|
+
- fork.gemspec
|
34
|
+
- LICENSE.txt
|
35
|
+
- Rakefile
|
36
|
+
- README.markdown
|
37
|
+
homepage: https://github.com/apeiros/fork
|
38
|
+
licenses: []
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options: []
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ! '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>'
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.3.1
|
55
|
+
requirements: []
|
56
|
+
rubyforge_project:
|
57
|
+
rubygems_version: 1.8.24
|
58
|
+
signing_key:
|
59
|
+
specification_version: 3
|
60
|
+
summary: Represents forks (child processes) as objects and makes interaction with
|
61
|
+
forks easy.
|
62
|
+
test_files: []
|
63
|
+
has_rdoc:
|