fork 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -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.
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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: