em-posix-spawn 0.1.10

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.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
data/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # `em-posix-spawn`
2
+
3
+ This module provides an interface to `POSIX::Spawn` for EventMachine. In
4
+ particular, it contains an EventMachine equivalent to `POSIX::Spawn::Child`.
5
+ This class encapsulates writing to the child process its stdin and reading from
6
+ both its stdout and stderr. Only when the process has exited, it triggers a
7
+ callback to notify others of its completion. Just as `POSIX::Spawn::Child`,
8
+ this module allows the caller to include limits for execution time and number
9
+ of bytes read from stdout and stderr.
10
+
11
+ # Usage
12
+
13
+ Please refer to the documentation of `POSIX::Spawn::Child` for the complete set
14
+ of options that can be passed when creating `Child`.
15
+
16
+ ```ruby
17
+ require "em/posix/spawn"
18
+
19
+ EM.run {
20
+ p = EM::POSIX::Spawn::Child.new("echo something")
21
+
22
+ p.callback {
23
+ puts "Child process echo'd: #{p.out.inspect}"
24
+ EM.stop
25
+ }
26
+
27
+ p.errback { |err|
28
+ puts "Error running child process: #{err.inspect}"
29
+ EM.stop
30
+ }
31
+
32
+ # Add callbacks to listen to the child process' output streams.
33
+ listeners = p.add_streams_listener { |listener, data|
34
+ # Do something with the data.
35
+ # Use listener.name to get the name of the stream.
36
+ # Use listener.closed? to check if listener is closed.
37
+ # This block is called exactly once after the listener is closed.
38
+ }
39
+
40
+ # Optionally, wait for all the listeners to be closed.
41
+ while !listeners.all?(&:closed?) {
42
+ ...
43
+ }
44
+
45
+ # Sends SIGTERM to the process, and SIGKILL after 5 seconds.
46
+ # Returns true if this kill was successful, false otherwise.
47
+ # The timeout is optional, default timeout is 0 (immediate SIGKILL
48
+ # after SIGTERM).
49
+ p.kill(5)
50
+ }
51
+ ```
52
+
53
+ # Credit
54
+
55
+ The implementation for `EM::POSIX::Spawn::Child` and its tests are based on the
56
+ implementation and tests for `POSIX::Spawn::Child`, which is Copyright (c) 2011
57
+ by Ryan Tomayko <r@tomayko.com> and Aman Gupta <aman@tmm1.net>.
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new 'test' do |t|
6
+ t.test_files = FileList['test/test_*.rb']
7
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "em/posix/spawn/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "em-posix-spawn"
7
+ s.version = EventMachine::POSIX::Spawn::VERSION
8
+
9
+ s.authors = ["Pieter Noordhuis"]
10
+ s.email = ["pcnoordhuis@gmail.com"]
11
+ s.summary = "EventMachine-aware POSIX::Spawn::Child"
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+ s.require_paths = ["lib"]
15
+
16
+ s.add_runtime_dependency "eventmachine"
17
+ s.add_runtime_dependency "posix-spawn"
18
+ s.add_development_dependency "rake"
19
+ end
@@ -0,0 +1 @@
1
+ require "em/posix/spawn"
@@ -0,0 +1,2 @@
1
+ require "em/posix/spawn/version"
2
+ require "em/posix/spawn/child"
@@ -0,0 +1,488 @@
1
+ require 'eventmachine'
2
+ require 'posix/spawn'
3
+
4
+ module EventMachine
5
+
6
+ module POSIX
7
+
8
+ module Spawn
9
+
10
+ include ::POSIX::Spawn
11
+
12
+ class Child
13
+
14
+ include Spawn
15
+ include Deferrable
16
+
17
+ # Spawn a new process, write all input and read all output. Supports
18
+ # the standard spawn interface as described in the POSIX::Spawn module
19
+ # documentation:
20
+ #
21
+ # new([env], command, [argv1, ...], [options])
22
+ #
23
+ # The following options are supported in addition to the standard
24
+ # POSIX::Spawn options:
25
+ #
26
+ # :input => str Write str to the new process's standard input.
27
+ # :timeout => int Maximum number of seconds to allow the process
28
+ # to execute before aborting with a TimeoutExceeded
29
+ # exception.
30
+ # :max => total Maximum number of bytes of output to allow the
31
+ # process to generate before aborting with a
32
+ # MaximumOutputExceeded exception.
33
+ # :prepend_stdout => str Data to prepend to stdout
34
+ # :prepend_stderr => str Data to prepend to stderr
35
+ #
36
+ # Returns a new Child instance that is being executed. The object
37
+ # includes the Deferrable module, and executes the success callback
38
+ # when the process has exited, or the failure callback when the process
39
+ # was killed because of exceeding the timeout, or exceeding the maximum
40
+ # number of bytes to read from stdout and stderr combined. Once the
41
+ # success callback is triggered, this objects's out, err and status
42
+ # attributes are available. Clients can register callbacks to listen to
43
+ # updates from out and err streams of the process.
44
+ def initialize(*args)
45
+ @env, @argv, options = extract_process_spawn_arguments(*args)
46
+ @options = options.dup
47
+ @input = @options.delete(:input)
48
+ @timeout = @options.delete(:timeout)
49
+ @max = @options.delete(:max)
50
+ @discard_output = @options.delete(:discard_output)
51
+ @prepend_stdout = @options.delete(:prepend_stdout) || ""
52
+ @prepend_stderr = @options.delete(:prepend_stderr) || ""
53
+ @options.delete(:chdir) if @options[:chdir].nil?
54
+
55
+ exec!
56
+ end
57
+
58
+ # All data written to the child process's stdout stream as a String.
59
+ attr_reader :out
60
+
61
+ # All data written to the child process's stderr stream as a String.
62
+ attr_reader :err
63
+
64
+ # A Process::Status object with information on how the child exited.
65
+ attr_reader :status
66
+
67
+ # Total command execution time (wall-clock time)
68
+ attr_reader :runtime
69
+
70
+ attr_reader :pid
71
+
72
+ # Determine if the process did exit with a zero exit status.
73
+ def success?
74
+ @status && @status.success?
75
+ end
76
+
77
+ # Determine if the process has already terminated.
78
+ def terminated?
79
+ !! @status
80
+ end
81
+
82
+ # Send the SIGTERM signal to the process.
83
+ # Then send the SIGKILL signal to the process after the
84
+ # specified timeout.
85
+ def kill(timeout = 0)
86
+ return false if terminated? || @sigkill_timer
87
+ timeout ||= 0
88
+ request_termination
89
+ @sigkill_timer = Timer.new(timeout) {
90
+ ::Process.kill('KILL', @pid) rescue nil
91
+ }
92
+
93
+ true
94
+ end
95
+
96
+ # Send the SIGTERM signal to the process.
97
+ #
98
+ # Returns the Process::Status object obtained by reaping the process.
99
+ def request_termination
100
+ @sigterm_timer.cancel if @sigterm_timer
101
+ ::Process.kill('TERM', @pid) rescue nil
102
+ end
103
+
104
+ def add_streams_listener(&listener)
105
+ [@cout.after_read(&listener), @cerr.after_read(&listener)]
106
+ end
107
+
108
+ class SignalHandler
109
+
110
+ def self.setup!
111
+ @instance ||= begin
112
+ new.tap do |instance|
113
+ instance.setup!
114
+ end
115
+ end
116
+ end
117
+
118
+ def self.teardown!
119
+ if @instance
120
+ @instance.teardown!
121
+ @instance = nil
122
+ end
123
+ end
124
+
125
+ def self.instance
126
+ @instance
127
+ end
128
+
129
+ def initialize
130
+ @pid_callback = {}
131
+ @pid_to_process_status = {}
132
+ end
133
+
134
+ def setup!
135
+ @pipe = ::IO.pipe
136
+ @notifier = ::EM.watch @pipe[0], SignalNotifier, self
137
+ @notifier.notify_readable = true
138
+
139
+ @prev_handler = ::Signal.trap(:CHLD) do
140
+ begin
141
+ @pipe[1].write_nonblock("x")
142
+ rescue IO::WaitWritable
143
+ end
144
+
145
+ @prev_handler.call
146
+ end
147
+
148
+ @prev_handler ||= lambda { |*_| ; }
149
+ end
150
+
151
+ def teardown!
152
+ ::Signal.trap(:CHLD, &@prev_handler)
153
+
154
+ @notifier.detach if ::EM.reactor_running?
155
+ @pipe[0].close rescue nil
156
+ @pipe[1].close rescue nil
157
+ end
158
+
159
+ def pid_callback(pid, &blk)
160
+ @pid_callback[pid] = blk
161
+ end
162
+
163
+ def pid_to_process_status(pid)
164
+ @pid_to_process_status.delete(pid)
165
+ end
166
+
167
+ def signal
168
+ # The SIGCHLD handler may not be called exactly once for every
169
+ # child. I.e., multiple children exiting concurrently may trigger
170
+ # only one SIGCHLD in the parent. Therefore, reap all processes
171
+ # that can be reaped.
172
+ while pid = ::Process.wait(-1, ::Process::WNOHANG)
173
+ @pid_to_process_status[pid] = $?
174
+ blk = @pid_callback.delete(pid)
175
+ EM.next_tick(&blk) if blk
176
+ end
177
+ rescue ::Errno::ECHILD
178
+ end
179
+
180
+ class SignalNotifier < ::EM::Connection
181
+ def initialize(handler)
182
+ @handler = handler
183
+ end
184
+
185
+ def notify_readable
186
+ begin
187
+ @io.read_nonblock(65536)
188
+ rescue IO::WaitReadable
189
+ end
190
+
191
+ @handler.signal
192
+ end
193
+ end
194
+ end
195
+
196
+ # Execute command, write input, and read output. This is called
197
+ # immediately when a new instance of this object is initialized.
198
+ def exec!
199
+ # The signal handler MUST be installed before spawning a new process
200
+ SignalHandler.setup!
201
+
202
+ if RUBY_PLATFORM =~ /linux/i && @options.delete(:close_others)
203
+ @options[:in] = :in
204
+ @options[:out] = :out
205
+ @options[:err] = :err
206
+
207
+ ::Dir.glob("/proc/%d/fd/*" % Process.pid).map do |file|
208
+ fd = File.basename(file).to_i
209
+
210
+ if fd > 2
211
+ @options[fd] = :close
212
+ end
213
+ end
214
+ end
215
+
216
+ @pid, stdin, stdout, stderr = popen4(@env, *(@argv + [@options]))
217
+ @start = Time.now
218
+
219
+ # Don't leak into processes spawned after us.
220
+ [stdin, stdout, stderr].each { |io| io.close_on_exec = true }
221
+
222
+ # watch fds
223
+ @cin = EM.watch stdin, WritableStream, (@input || "").dup, "stdin"
224
+ @cout = EM.watch stdout, ReadableStream, @prepend_stdout, "stdout", @discard_output
225
+ @cerr = EM.watch stderr, ReadableStream, @prepend_stderr, "stderr", @discard_output
226
+
227
+ # register events
228
+ @cin.notify_writable = true
229
+ @cout.notify_readable = true
230
+ @cerr.notify_readable = true
231
+
232
+ # keep track of open fds
233
+ in_flight = [@cin, @cout, @cerr].compact
234
+ in_flight.each { |io|
235
+ # force binary encoding
236
+ io.force_encoding
237
+
238
+ # register finalize hook
239
+ io.callback { in_flight.delete(io) }
240
+ }
241
+
242
+ failure = nil
243
+
244
+ # keep track of max output
245
+ max = @max
246
+ if max && max > 0
247
+ check_buffer_size = lambda { |listener, _|
248
+ if !terminated? && !listener.closed?
249
+ if @cout.buffer.size + @cerr.buffer.size > max
250
+ failure = MaximumOutputExceeded
251
+ in_flight.each(&:close)
252
+ in_flight.clear
253
+ request_termination
254
+ end
255
+ end
256
+ }
257
+
258
+ @cout.after_read(&check_buffer_size)
259
+ @cerr.after_read(&check_buffer_size)
260
+ end
261
+
262
+ # request termination of process when it doesn't terminate
263
+ # in time
264
+ timeout = @timeout
265
+ if timeout && timeout > 0
266
+ @sigterm_timer = Timer.new(timeout) {
267
+ failure = TimeoutExceeded
268
+ in_flight.each(&:close)
269
+ in_flight.clear
270
+ request_termination
271
+ }
272
+ end
273
+
274
+ # run block when pid is reaped
275
+ SignalHandler.instance.pid_callback(@pid) {
276
+ @sigterm_timer.cancel if @sigterm_timer
277
+ @sigkill_timer.cancel if @sigkill_timer
278
+ @runtime = Time.now - @start
279
+ @status = SignalHandler.instance.pid_to_process_status(@pid)
280
+
281
+ in_flight.each do |io|
282
+ # Trigger final read to make sure buffer is drained
283
+ if io.respond_to?(:notify_readable)
284
+ io.notify_readable
285
+ end
286
+
287
+ io.close
288
+ end
289
+
290
+ in_flight.clear
291
+
292
+ @out = @cout.buffer
293
+ @err = @cerr.buffer
294
+
295
+ if failure
296
+ set_deferred_failure failure
297
+ else
298
+ set_deferred_success
299
+ end
300
+ }
301
+ end
302
+
303
+ class Stream < Connection
304
+
305
+ include Deferrable
306
+
307
+ attr_reader :buffer
308
+
309
+ def initialize(buffer, name)
310
+ @buffer = buffer
311
+ @name = name
312
+ @closed = false
313
+ end
314
+
315
+ def force_encoding
316
+ if @buffer.respond_to?(:force_encoding)
317
+ @io.set_encoding('BINARY', 'BINARY')
318
+ @buffer.force_encoding('BINARY')
319
+ end
320
+ end
321
+
322
+ def close
323
+ return if closed?
324
+
325
+
326
+ # NB: Defer detach to the next tick, because EventMachine blows up
327
+ # when a file descriptor is attached and detached in the same
328
+ # tick. This can happen when the child process dies in the same
329
+ # tick it started, and the `#waitpid` loop in the signal
330
+ # handler picks it up afterwards. The signal handler, in turn,
331
+ # queues the child's callback to the executed via
332
+ # `EM#next_tick`. If the blocks queued by `EM#next_tick` are
333
+ # executed after that, still in the same tick, the child's file
334
+ # descriptors can be detached in the same tick they were
335
+ # attached.
336
+ EM.next_tick do
337
+ # NB: The ordering here is important. If we're using epoll,
338
+ # detach() attempts to deregister the associated fd via
339
+ # EPOLL_CTL_DEL and marks the EventableDescriptor for
340
+ # deletion upon completion of the iteration of the event
341
+ # loop. However, if the fd was closed before calling
342
+ # detach(), epoll_ctl() will sometimes return EBADFD and fail
343
+ # to remove the fd. This can lead to epoll_wait() returning
344
+ # an event whose data pointer is invalid (since it was
345
+ # deleted in a prior iteration of the event loop).
346
+ detach
347
+ @io.close rescue nil
348
+ end
349
+
350
+ @closed = true
351
+ end
352
+
353
+ def closed?
354
+ @closed
355
+ end
356
+ end
357
+
358
+ class ReadableStream < Stream
359
+
360
+ class Listener
361
+
362
+ attr_reader :name
363
+
364
+ def initialize(name, &block)
365
+ @name = name
366
+ @block = block
367
+ @offset = 0
368
+ end
369
+
370
+ # Sends the part of the buffer that has not yet been sent.
371
+ def call(buffer)
372
+ return if @block.nil?
373
+
374
+ to_call = @block
375
+ to_call.call(self, slice_from_buffer(buffer))
376
+ end
377
+
378
+ # Sends the part of the buffer that has not yet been sent,
379
+ # after closing the listener. After this, the listener
380
+ # will not receive any more calls.
381
+ def close(buffer = "")
382
+ return if @block.nil?
383
+
384
+ to_call, @block = @block, nil
385
+ to_call.call(self, slice_from_buffer(buffer))
386
+ end
387
+
388
+ def closed?
389
+ @block.nil?
390
+ end
391
+
392
+ private
393
+
394
+ def slice_from_buffer(buffer)
395
+ to_be_sent = buffer.slice(@offset..-1)
396
+ to_be_sent ||= ""
397
+ @offset = buffer.length
398
+ to_be_sent
399
+ end
400
+ end
401
+
402
+ # Maximum buffer size for reading
403
+ BUFSIZE = (64 * 1024)
404
+
405
+ def initialize(buffer, name, discard_output = false, &block)
406
+ super(buffer, name, &block)
407
+ @discard_output = discard_output
408
+ @after_read = []
409
+ end
410
+
411
+ def close
412
+ # Ensure that the listener receives the entire buffer if it
413
+ # attaches to the process only just before the stream is closed.
414
+ @after_read.each do |listener|
415
+ listener.close(@buffer)
416
+ end
417
+
418
+ @after_read.clear
419
+
420
+ super
421
+ end
422
+
423
+ def after_read(&block)
424
+ if block
425
+ listener = Listener.new(@name, &block)
426
+ if @closed
427
+ # If this stream is already closed, then close the listener in
428
+ # the next Event Machine tick. This ensures that the listener
429
+ # receives the entire buffer if it attaches to the process only
430
+ # after its completion.
431
+ EM.next_tick do
432
+ listener.close(@buffer)
433
+ end
434
+ elsif !@buffer.empty?
435
+ # If this stream's buffer is non-empty, pass it to the listener
436
+ # in the next tick to avoid having to wait for the next piece
437
+ # of data to be read.
438
+ EM.next_tick do
439
+ listener.call(@buffer)
440
+ end
441
+ end
442
+
443
+ @after_read << listener
444
+ listener
445
+ end
446
+ end
447
+
448
+ def notify_readable
449
+ # Close and detach are decoupled, check if this notification is
450
+ # supposed to go through.
451
+ return if closed?
452
+
453
+ begin
454
+ out = @io.read_nonblock(BUFSIZE)
455
+ @buffer << out unless @discard_output
456
+ @after_read.each { |listener| listener.call(@buffer) }
457
+ rescue Errno::EAGAIN, Errno::EINTR
458
+ rescue EOFError
459
+ close
460
+ set_deferred_success
461
+ end
462
+ end
463
+ end
464
+
465
+ class WritableStream < Stream
466
+
467
+ def notify_writable
468
+ # Close and detach are decoupled, check if this notification is
469
+ # supposed to go through.
470
+ return if closed?
471
+
472
+ begin
473
+ boom = nil
474
+ size = @io.write_nonblock(@buffer)
475
+ @buffer = @buffer[size, @buffer.size]
476
+ rescue Errno::EPIPE => boom
477
+ rescue Errno::EAGAIN, Errno::EINTR
478
+ end
479
+ if boom || @buffer.size == 0
480
+ close
481
+ set_deferred_success
482
+ end
483
+ end
484
+ end
485
+ end
486
+ end
487
+ end
488
+ end
@@ -0,0 +1,7 @@
1
+ module EventMachine
2
+ module POSIX
3
+ module Spawn
4
+ VERSION = "0.1.10"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,594 @@
1
+ # coding: UTF-8
2
+
3
+ require 'test/unit'
4
+ require 'em/posix/spawn/child'
5
+
6
+ module Helpers
7
+
8
+ def em(options = {})
9
+ raise "no block given" unless block_given?
10
+ timeout = options[:timeout] ||= 1.0
11
+
12
+ ::EM.run do
13
+ quantum = 0.005
14
+ ::EM.set_quantum(quantum * 1000) # Lowest possible timer resolution
15
+ ::EM.set_heartbeat_interval(quantum) # Timeout connections asap
16
+ ::EM.add_timer(timeout) { raise "timeout" }
17
+ yield
18
+ end
19
+
20
+ ::EM::POSIX::Spawn::Child::SignalHandler.teardown!
21
+ end
22
+
23
+ def done
24
+ raise "reactor not running" if !::EM.reactor_running?
25
+
26
+ ::EM.next_tick do
27
+ # Assert something to show a spec-pass
28
+ assert true
29
+ ::EM.stop_event_loop
30
+ end
31
+ end
32
+ end
33
+
34
+ class ChildTest < Test::Unit::TestCase
35
+
36
+ include ::EM::POSIX::Spawn
37
+ include Helpers
38
+
39
+ def teardown
40
+ ::EM::POSIX::Spawn::Child::SignalHandler.teardown!
41
+ end
42
+
43
+ def test_sanity
44
+ assert_same ::EM::POSIX::Spawn::Child, Child
45
+ end
46
+
47
+ def test_argv_string_uses_sh
48
+ em do
49
+ p = Child.new("echo via /bin/sh")
50
+ p.callback do
51
+ assert p.success?
52
+ assert_equal "via /bin/sh\n", p.out
53
+ done
54
+ end
55
+ end
56
+ end
57
+
58
+ def test_stdout
59
+ em do
60
+ p = Child.new('echo', 'boom')
61
+ p.callback do
62
+ assert_equal "boom\n", p.out
63
+ assert_equal "", p.err
64
+ done
65
+ end
66
+ end
67
+ end
68
+
69
+ def test_stderr
70
+ em do
71
+ p = Child.new('echo boom 1>&2')
72
+ p.callback do
73
+ assert_equal "", p.out
74
+ assert_equal "boom\n", p.err
75
+ done
76
+ end
77
+ end
78
+ end
79
+
80
+ def test_status
81
+ em do
82
+ p = Child.new('exit 3')
83
+ p.callback do
84
+ assert !p.status.success?
85
+ assert_equal 3, p.status.exitstatus
86
+ done
87
+ end
88
+ end
89
+ end
90
+
91
+ def test_env
92
+ em do
93
+ p = Child.new({ 'FOO' => 'BOOYAH' }, 'echo $FOO')
94
+ p.callback do
95
+ assert_equal "BOOYAH\n", p.out
96
+ done
97
+ end
98
+ end
99
+ end
100
+
101
+ def test_chdir
102
+ em do
103
+ p = Child.new("pwd", :chdir => File.dirname(Dir.pwd))
104
+ p.callback do
105
+ assert_equal File.dirname(Dir.pwd) + "\n", p.out
106
+ done
107
+ end
108
+ end
109
+ end
110
+
111
+ def test_input
112
+ input = "HEY NOW\n" * 100_000 # 800K
113
+
114
+ em do
115
+ p = Child.new('wc', '-l', :input => input)
116
+ p.callback do
117
+ assert_equal 100_000, p.out.strip.to_i
118
+ done
119
+ end
120
+ end
121
+ end
122
+
123
+ def test_max
124
+ em do
125
+ p = Child.new('yes', :max => 100_000)
126
+ p.callback { fail }
127
+ p.errback do |err|
128
+ assert_equal MaximumOutputExceeded, err
129
+ done
130
+ end
131
+ end
132
+ end
133
+
134
+ def test_discard_output
135
+ em do
136
+ p = Child.new('echo hi', :discard_output => true)
137
+ p.callback do
138
+ assert_equal 0, p.out.size
139
+ assert_equal 0, p.err.size
140
+ done
141
+ end
142
+ end
143
+ end
144
+
145
+ def test_max_with_child_hierarchy
146
+ em do
147
+ p = Child.new('/bin/sh', '-c', 'yes', :max => 100_000)
148
+ p.callback { fail }
149
+ p.errback do |err|
150
+ assert_equal MaximumOutputExceeded, err
151
+ done
152
+ end
153
+ end
154
+ end
155
+
156
+ def test_max_with_stubborn_child
157
+ em do
158
+ p = Child.new("trap '' TERM; yes", :max => 100_000)
159
+ p.callback { fail }
160
+ p.errback do |err|
161
+ assert_equal MaximumOutputExceeded, err
162
+ done
163
+ end
164
+ end
165
+ end
166
+
167
+ def test_timeout
168
+ em do
169
+ start = Time.now
170
+ p = Child.new('sleep', '1', :timeout => 0.05)
171
+ p.callback { fail }
172
+ p.errback do |err|
173
+ assert_equal TimeoutExceeded, err
174
+ assert (Time.now-start) <= 0.2
175
+ done
176
+ end
177
+ end
178
+ end
179
+
180
+ def test_timeout_with_child_hierarchy
181
+ em do
182
+ p = Child.new('/bin/sh', '-c', 'sleep 1', :timeout => 0.05)
183
+ p.callback { fail }
184
+ p.errback do |err|
185
+ assert_equal TimeoutExceeded, err
186
+ done
187
+ end
188
+ end
189
+ end
190
+
191
+ def test_lots_of_input_and_lots_of_output_at_the_same_time
192
+ input = "stuff on stdin \n" * 1_000
193
+ command = "
194
+ while read line
195
+ do
196
+ echo stuff on stdout;
197
+ echo stuff on stderr 1>&2;
198
+ done
199
+ "
200
+
201
+ em do
202
+ p = Child.new(command, :input => input)
203
+ p.callback do
204
+ assert_equal input.size, p.out.size
205
+ assert_equal input.size, p.err.size
206
+ assert p.success?
207
+ done
208
+ end
209
+ end
210
+ end
211
+
212
+ def test_input_cannot_be_written_due_to_broken_pipe
213
+ input = "1" * 100_000
214
+
215
+ em do
216
+ p = Child.new('false', :input => input)
217
+ p.callback do
218
+ assert !p.success?
219
+ done
220
+ end
221
+ end
222
+ end
223
+
224
+ def test_utf8_input
225
+ input = "hålø"
226
+
227
+ em do
228
+ p = Child.new('cat', :input => input)
229
+ p.callback do
230
+ assert p.success?
231
+ done
232
+ end
233
+ end
234
+ end
235
+
236
+ def test_many_pending_processes
237
+ EM.epoll
238
+
239
+ em do
240
+ target = 100
241
+ finished = 0
242
+
243
+ finish = lambda do |p|
244
+ finished += 1
245
+
246
+ if finished == target
247
+ done
248
+ end
249
+ end
250
+
251
+ spawn = lambda do |i|
252
+ EM.next_tick do
253
+ if i < target
254
+ p = Child.new('sleep %.6f' % (rand(10_000) / 1_000_000.0))
255
+ p.callback { finish.call(p) }
256
+ spawn.call(i+1)
257
+ end
258
+ end
259
+ end
260
+
261
+ spawn.call(0)
262
+ end
263
+ end
264
+
265
+ # This tries to exercise faulty EventMachine behavior.
266
+ # EventMachine crashes when a file descriptor is attached and
267
+ # detached in the same event loop tick.
268
+ def test_short_lived_process_started_from_io_callback
269
+ EM.epoll
270
+
271
+ em do
272
+ m = Module.new do
273
+ def initialize(handlers)
274
+ @handlers = handlers
275
+ end
276
+
277
+ def notify_readable
278
+ begin
279
+ @io.read_nonblock(1)
280
+ @handlers[:readable].call
281
+ rescue EOFError
282
+ @handlers[:eof].call
283
+ end
284
+ end
285
+ end
286
+
287
+ r, w = IO.pipe
288
+
289
+ s = lambda do
290
+ Child.new("echo")
291
+ end
292
+
293
+ l = EM.watch(r, m, :readable => s, :eof => method(:done))
294
+ l.notify_readable = true
295
+
296
+ # Trigger listener (it reads one byte per tick)
297
+ w.write_nonblock("x" * 100)
298
+ w.close
299
+ end
300
+ end
301
+
302
+ # Tests if expected listeners are returned by
303
+ # Child#add_stream_listeners(&block).
304
+ def test_add_listeners
305
+ em do
306
+ p = Child.new("printf ''")
307
+
308
+ listeners = p.add_streams_listener { |*args| }
309
+
310
+ assert listeners
311
+ assert_equal 2, listeners.size
312
+ listeners = listeners.sort_by { |x| x.name }
313
+
314
+ assert !listeners[0].closed?
315
+ assert "stderr", listeners[0].name
316
+
317
+ assert !listeners[1].closed?
318
+ assert "stdout", listeners[1].name
319
+
320
+ p.callback do
321
+ assert p.success?
322
+ done
323
+ end
324
+ end
325
+ end
326
+
327
+ def test_listener_closed_on_exceeding_max_output
328
+ em do
329
+ p = Child.new("yes", :max => 2)
330
+
331
+ listeners = p.add_streams_listener do |listener, data|
332
+ if listener.closed?
333
+ listeners.delete(listener)
334
+ end
335
+ end
336
+
337
+ p.errback do
338
+ assert listeners.empty?
339
+ done
340
+ end
341
+ end
342
+ end
343
+
344
+ def test_listener_closed_on_exceeding_timeout
345
+ em do
346
+ p = Child.new("sleep 0.1", :timeout => 0.05)
347
+
348
+ listeners = p.add_streams_listener do |listener, data|
349
+ if listener.closed?
350
+ listeners.delete(listener)
351
+ end
352
+ end
353
+
354
+ p.errback do
355
+ assert listeners.empty?
356
+ done
357
+ end
358
+ end
359
+ end
360
+
361
+ # Tests if a listener correctly receives stream updates after it attaches to a
362
+ # process that has already finished execution without producing any output in
363
+ # its stdout and stderr.
364
+ def test_listener_empty_streams_completed_process
365
+ em do
366
+ p = Child.new("printf ''")
367
+ p.callback do
368
+ assert p.success?
369
+
370
+ num_calls = 0
371
+ listeners = p.add_streams_listener do |listener, data|
372
+ assert listeners.include?(listener)
373
+ assert listener.closed?
374
+
375
+ assert data.empty?
376
+
377
+ listeners.delete(listener)
378
+ num_calls += 1
379
+ # The test times out if listeners are not called required number
380
+ # of times.
381
+ done if num_calls == 2
382
+ end
383
+ end
384
+ end
385
+ end
386
+
387
+ # Tests if a listener correctly receives out and err stream updates after it
388
+ # attaches to a process that has already finished execution, and has produced
389
+ # some output in its stdout and stderr.
390
+ def test_listener_nonempty_streams_completed_process
391
+ em do
392
+ p = Child.new("printf test >& 1; printf test >& 2")
393
+ p.callback do
394
+ assert p.success?
395
+
396
+ num_calls = 0
397
+ listeners = p.add_streams_listener do |listener, data|
398
+ assert listeners.include?(listener)
399
+ assert listener.closed?
400
+
401
+ assert_equal "test", data
402
+
403
+ listeners.delete(listener)
404
+ num_calls += 1
405
+
406
+ # The test times out if listeners are not called required number
407
+ # of times.
408
+ done if num_calls == 2
409
+ end
410
+ end
411
+ end
412
+ end
413
+
414
+ # Tests if a listener correctly receives incremental stream updates after it
415
+ # attaches to an active process that produces large output in stdout.
416
+ def test_listener_large_stdout
417
+ output_a = "a" * 1024 * 32
418
+ output_b = "b" * 1024 * 32
419
+
420
+ em do
421
+ p = Child.new("printf #{output_a}; sleep 0.1; printf #{output_b}")
422
+ received_data = ''
423
+ listeners = p.add_streams_listener do |listener, data|
424
+ assert listener
425
+ assert data
426
+ if listener.name == "stdout"
427
+ received_data << data
428
+ end
429
+ end
430
+
431
+ p.callback do
432
+ assert p.success?
433
+ assert "#{output_a}#{output_b}", received_data
434
+ done
435
+ end
436
+ end
437
+ end
438
+
439
+ # Tests if multiple listeners correctly receives stream updates after they
440
+ # attached to the same process.
441
+ def test_listener_nonempty_streams_active_process
442
+ em do
443
+ command = ['A', 'B', 'C'].map do |e|
444
+ 'printf %s; sleep 0.01' % e
445
+ end.join(';')
446
+
447
+ p = Child.new(command)
448
+
449
+ data = ['', '']
450
+ closed = [false, false]
451
+ called = false
452
+ p.add_streams_listener do |listener_outer, data_outer|
453
+ data[0] << data_outer
454
+ if listener_outer.closed?
455
+ closed[0] = true
456
+ end
457
+ unless called
458
+ EM.next_tick do
459
+ p.add_streams_listener do |listener_inner, data_inner|
460
+ data[1] << data_inner
461
+ if listener_inner.closed?
462
+ closed[1] = true
463
+ end
464
+ end
465
+ end
466
+
467
+ called = true
468
+ end
469
+ end
470
+
471
+ p.callback do
472
+ assert p.success?
473
+ assert_equal "ABC", data[0]
474
+ assert_equal "ABC", data[1]
475
+ done
476
+ end
477
+ end
478
+ end
479
+
480
+ # Tests if a listener receives the current buffer when it attaches to a process.
481
+ def test_listener_is_called_with_buffer_first
482
+ em do
483
+ command = "printf A; sleep 0.1"
484
+ command << "; printf B; sleep 0.1"
485
+ command << "; printf C; sleep 0.1"
486
+ p = Child.new(command)
487
+
488
+ i = 0
489
+ p.add_streams_listener do |listener_outer, data_outer|
490
+ i += 1
491
+
492
+ case i
493
+ when 1
494
+ assert_equal listener_outer.name, "stdout"
495
+ assert_equal data_outer, "A"
496
+
497
+ # Add streams listener from fresh stack to avoid mutating @after_read while iterating
498
+ EM.next_tick do
499
+ j = 0
500
+ p.add_streams_listener do |listener_inner, data_inner|
501
+ j += 1
502
+
503
+ case j
504
+ when 1
505
+ assert_equal "stdout", listener_inner.name
506
+ assert_equal "A", data_inner
507
+ when 2
508
+ assert_equal "stdout", listener_inner.name
509
+ assert_equal "B", data_inner
510
+ when 3
511
+ assert_equal "stdout", listener_inner.name
512
+ assert_equal "C", data_inner
513
+ done
514
+ end
515
+ end
516
+ end
517
+ end
518
+ end
519
+ end
520
+ end
521
+
522
+ # Test if duplicate kill is ignored.
523
+ def test_duplicate_kill
524
+ em do
525
+ command = "trap ':' TERM; while :; do :; done"
526
+ p = Child.new(command)
527
+ p.callback do
528
+ done
529
+ end
530
+
531
+ sleep 0.005
532
+ assert p.kill(0.005)
533
+ assert !p.kill(0.005)
534
+ end
535
+ end
536
+
537
+ # Test if kill on terminated job is ignored
538
+ def test_kill_terminated_job
539
+ em do
540
+ command = "printf ''"
541
+ p = Child.new(command)
542
+ p.callback do
543
+ assert !p.kill(1)
544
+ done
545
+ end
546
+ end
547
+ end
548
+
549
+ # Test kill on active job.
550
+ def test_kill_active_job
551
+ em do
552
+ command = "trap ':' TERM; while :; do :; done"
553
+ p = Child.new(command)
554
+ p.callback do
555
+ done
556
+ end
557
+
558
+ sleep 0.005
559
+ assert p.kill(0.005)
560
+ end
561
+ end
562
+
563
+ def test_close_others_false
564
+ r, w = IO.pipe
565
+
566
+ em do
567
+ p = Child.new("ls /proc/$$/fd")
568
+ p.callback do
569
+ fds = p.out.split.map(&:to_i)
570
+ assert !fds.empty?
571
+
572
+ assert fds.include?(r.fileno)
573
+ assert fds.include?(w.fileno)
574
+ done
575
+ end
576
+ end
577
+ end
578
+
579
+ def test_close_others_true
580
+ r, w = IO.pipe
581
+
582
+ em do
583
+ p = Child.new("ls /proc/$$/fd", :close_others => true)
584
+ p.callback do
585
+ fds = p.out.split.map(&:to_i)
586
+ assert !fds.empty?
587
+
588
+ assert !fds.include?(r.fileno)
589
+ assert !fds.include?(w.fileno)
590
+ done
591
+ end
592
+ end
593
+ end
594
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: em-posix-spawn
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.10
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Pieter Noordhuis
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-01-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: eventmachine
16
+ requirement: &78497090 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *78497090
25
+ - !ruby/object:Gem::Dependency
26
+ name: posix-spawn
27
+ requirement: &78496880 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *78496880
36
+ - !ruby/object:Gem::Dependency
37
+ name: rake
38
+ requirement: &78496670 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *78496670
47
+ description:
48
+ email:
49
+ - pcnoordhuis@gmail.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - .gitignore
55
+ - Gemfile
56
+ - README.md
57
+ - Rakefile
58
+ - em-posix-spawn.gemspec
59
+ - lib/em-posix-spawn.rb
60
+ - lib/em/posix/spawn.rb
61
+ - lib/em/posix/spawn/child.rb
62
+ - lib/em/posix/spawn/version.rb
63
+ - test/test_child.rb
64
+ homepage:
65
+ licenses: []
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ! '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubyforge_project:
84
+ rubygems_version: 1.8.11
85
+ signing_key:
86
+ specification_version: 3
87
+ summary: EventMachine-aware POSIX::Spawn::Child
88
+ test_files: []