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 +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: []
|