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 +4 -0
- data/Gemfile +2 -0
- data/README.md +57 -0
- data/Rakefile +7 -0
- data/em-posix-spawn.gemspec +19 -0
- data/lib/em-posix-spawn.rb +1 -0
- data/lib/em/posix/spawn.rb +2 -0
- data/lib/em/posix/spawn/child.rb +488 -0
- data/lib/em/posix/spawn/version.rb +7 -0
- data/test/test_child.rb +594 -0
- metadata +88 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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,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,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
|
data/test/test_child.rb
ADDED
@@ -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: []
|