em-posix-spawn 0.1.10

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