progeny 0.1.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE.txt +23 -0
- data/README.md +196 -0
- data/lib/progeny/command.rb +272 -0
- data/lib/progeny/version.rb +3 -0
- data/lib/progeny.rb +4 -0
- metadata +81 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e16d4fb0220a00c09eba23fc3b5f43cf72facb831a8d70f8afb2e4c48507b964
|
4
|
+
data.tar.gz: 6f852ef9c5fb2f0dafd6a153cc9045b5d37021f189e7da18d2ec137b7d9d3338
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2f808a8bcdae021ab23ef31ad18df2c3d39b359f9b3752bc5924e143a00757359e64ff0586c617b1e8908cec1bb5e0f9b1076834edce27595e1733f76ddcdbc6
|
7
|
+
data.tar.gz: 97bbb478f5c1ad1fabd55491784da44562d28d909be6f9c67586643f3e740fc1189e298cb2ea05a3ae46b0726a299f2d1cdba0307e19ffec31c83e3a41e7d049
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
## [Unreleased]
|
2
|
+
|
3
|
+
## [0.1.0] - 2023-05-03
|
4
|
+
|
5
|
+
- Initial release. Forked `posix-spawn` gem.
|
6
|
+
|
7
|
+
### Changed
|
8
|
+
|
9
|
+
- `Progeny::Command#new` and `Progeny::Command#build` only take actual `Hash`
|
10
|
+
objects as the `options` argument. We will not call `#to_hash` on the object
|
11
|
+
like `POSIX::Spawn::Child` did.
|
12
|
+
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Copyright (c) 2011 by Ryan Tomayko <r@tomayko.com>
|
2
|
+
and Aman Gupta <aman@tmm1.net>
|
3
|
+
Copyright (c) 2023 by Luan Vieira <luanv@me.com>
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person ob-
|
6
|
+
taining a copy of this software and associated documentation
|
7
|
+
files (the "Software"), to deal in the Software without restric-
|
8
|
+
tion, including without limitation the rights to use, copy, modi-
|
9
|
+
fy, merge, publish, distribute, sublicense, and/or sell copies of
|
10
|
+
the Software, and to permit persons to whom the Software is fur-
|
11
|
+
nished to do so, subject to the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
18
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONIN-
|
19
|
+
FRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
21
|
+
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
22
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
23
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
# progeny
|
2
|
+
|
3
|
+
Spawn child processes with simple string based standard input/output/error
|
4
|
+
stream handling in an easy-to-use interface.
|
5
|
+
|
6
|
+
## Progeny::Command
|
7
|
+
|
8
|
+
The `Progeny::Command` class includes logic for executing child processes and
|
9
|
+
reading/writing from their standard input, output, and error streams. It's
|
10
|
+
designed to take all input in a single string and provides all output as single
|
11
|
+
strings and is therefore not well-suited to streaming large quantities of data
|
12
|
+
in and out of commands. That said, it has some benefits:
|
13
|
+
|
14
|
+
- **Simple** - requires little code for simple stream input and capture.
|
15
|
+
- **Internally non-blocking** (using `select(2)`) - handles all pipe hang cases
|
16
|
+
due to exceeding `PIPE_BUF` limits on one or more streams.
|
17
|
+
- **Uses Ruby under the hood** - It leverages Ruby's `Open3#popen3` and `Process.spawn`
|
18
|
+
behind the scenes so it's widely supported and consistently getting performance updates.
|
19
|
+
|
20
|
+
`Progeny::Command` takes the [standard `Process::spawn`
|
21
|
+
arguments](https://ruby-doc.org/current/Process.html#method-c-spawn) when
|
22
|
+
instantiated, and runs the process to completion after writing all input and
|
23
|
+
reading all output:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
require 'progeny'
|
27
|
+
child = Progeny::Command.new('git', '--help')
|
28
|
+
```
|
29
|
+
|
30
|
+
Retrieve process output written to stdout / stderr, or inspect the process's
|
31
|
+
exit status:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
child.out
|
35
|
+
# => "usage: git [--version] [--exec-path[=GIT_EXEC_PATH]]\n ..."
|
36
|
+
child.err
|
37
|
+
# => ""
|
38
|
+
child.status
|
39
|
+
# => #<Process::Status: pid=80718,exited(0)>
|
40
|
+
```
|
41
|
+
|
42
|
+
Use the `:input` option to write data on the new process's stdin immediately
|
43
|
+
after spawning:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
child = Progeny::Command.new('bc', :input => '40 + 2')
|
47
|
+
child.out
|
48
|
+
# => "42\n"
|
49
|
+
```
|
50
|
+
|
51
|
+
Additional options can be used to specify the maximum output size (`:max`) and
|
52
|
+
time of execution (`:timeout`) before the child process is aborted. See the
|
53
|
+
`Progeny::Command` docs for more info.
|
54
|
+
|
55
|
+
#### Reading Partial Results
|
56
|
+
|
57
|
+
`Progeny::Command` spawns the process immediately when instantiated.
|
58
|
+
As a result, if it is interrupted by an exception (either from reaching the
|
59
|
+
maximum output size, the time limit, or another factor), it is not possible to
|
60
|
+
access the `out` or `err` results because the constructor did not complete.
|
61
|
+
|
62
|
+
If you want to get the `out` and `err` data that was available when the process
|
63
|
+
was interrupted, use the `Progeny::Command` alternate form to create the child
|
64
|
+
without immediately spawning the process. Call `exec!` to run the command at a
|
65
|
+
place where you can catch any exceptions:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
child = Progeny::Command.build('git', 'log', :max => 100)
|
69
|
+
begin
|
70
|
+
child.exec!
|
71
|
+
rescue Progeny::MaximumOutputExceeded
|
72
|
+
# limit was reached
|
73
|
+
end
|
74
|
+
child.out
|
75
|
+
# => "commit fa54abe139fd045bf6dc1cc259c0f4c06a9285bb\n..."
|
76
|
+
```
|
77
|
+
|
78
|
+
Please note that when the `MaximumOutputExceeded` exception is raised, the
|
79
|
+
actual combined `out` and `err` data may be a bit longer than the `:max`
|
80
|
+
value due to internal buffering.
|
81
|
+
|
82
|
+
## Why fork posix-spawn
|
83
|
+
|
84
|
+
This gem is a fork of the
|
85
|
+
[`posix-spawn`](https://github.com/rtomayko/posix-spawn) gem. Originally,
|
86
|
+
`posix-spawn` was developed as a higher-performance alternative to Ruby's
|
87
|
+
built-in `Process.spawn` method. It achieved this by utilizing the
|
88
|
+
[`posix_spawn()`](https://man7.org/linux/man-pages/man3/posix_spawn.3.html)
|
89
|
+
system call, which resulted in significant performance improvements, as
|
90
|
+
demonstrated in the benchmarks below.
|
91
|
+
|
92
|
+
However, the performance advantage of `posix-spawn` diminished with the release
|
93
|
+
of Ruby 2.2. In this version, Ruby transitioned from using the `fork()` system
|
94
|
+
call to [`vfork()`](https://man7.org/linux/man-pages/man2/vfork.2.html), which
|
95
|
+
creates a new process without copying the parent process's page tables, leading
|
96
|
+
to enhanced performance.
|
97
|
+
|
98
|
+
The following benchmarks illustrate the performance comparison:
|
99
|
+
|
100
|
+
- Performance comparison at the time of `posix-spawn` creation (Ruby used `fork` + `exec`):
|
101
|
+

|
102
|
+
|
103
|
+
Source: [`posix-spawn` README](https://github.com/rtomayko/posix-spawn/blob/313f2abd4b9b5e737615178e0b353114481b9ab8/README.md#benchmarks)
|
104
|
+
|
105
|
+
- Current performance comparison (Ruby's built-in functionality is now more performant):
|
106
|
+

|
107
|
+
|
108
|
+
Source: Generated with the script in [this gist](https://gist.github.com/luanzeba/a1ebe2497ed4e2fb6491fd1780a52440) on a Debian 11 (bullseye) x86_64 machine.
|
109
|
+
|
110
|
+
For that reason, we decided to delete all of the custom spawn implementations
|
111
|
+
in the original gem: `POSIX::Spawn#spawn`, `POSIX::Spawn#popen4`,
|
112
|
+
`Kernel#system`, and <code>Kernel#\`</code>.
|
113
|
+
|
114
|
+
However, we didn't want to completely remove our use of `posix-spawn` because
|
115
|
+
we really enjoy the interface provided by `POSIX::Spawn::Child`. That's how
|
116
|
+
`progeny` came to be. It maintains all of the functionality provided by
|
117
|
+
`POSIX::Spawn::Child` under a new namespace: `Progeny::Command`.
|
118
|
+
|
119
|
+
## How to migrate from `posix-spawn` to `progeny`
|
120
|
+
|
121
|
+
1. Remove all usage of `POSIX::Spawn` as a Mixin.
|
122
|
+
Progeny does not include a Mixin so if you're including `POSIX::Spawn` in any
|
123
|
+
classes like so:
|
124
|
+
```ruby
|
125
|
+
require 'posix/spawn'
|
126
|
+
|
127
|
+
class YourSpawnerClass
|
128
|
+
include POSIX::Spawn
|
129
|
+
|
130
|
+
# [...]
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
You will need to remove the include statements and replace any use of `#spawn`
|
135
|
+
with Ruby's native `Process.spawn` and `#popen4` with Open3's `#popen3`.
|
136
|
+
```diff
|
137
|
+
- require 'posix/spawn'
|
138
|
+
+ require 'open3'
|
139
|
+
|
140
|
+
class YourSpawnerClass
|
141
|
+
- include POSIX::Spawn
|
142
|
+
|
143
|
+
def speak(message)
|
144
|
+
- pid = spawn('echo', message)
|
145
|
+
+ pid = Process.spawn('echo', message)
|
146
|
+
Process::waitpid(pid)
|
147
|
+
end
|
148
|
+
|
149
|
+
def calculate(expression)
|
150
|
+
- pid, in, out, err = popen4('bc')
|
151
|
+
+ in, out, err, wait_thr = Open3.popen3('bc')
|
152
|
+
+ pid = wait_thr[:pid] # if pid is needed
|
153
|
+
in.write(expression)
|
154
|
+
in.close
|
155
|
+
out.read
|
156
|
+
ensure
|
157
|
+
[in, out, err].each { |io| io.close if !io.closed? }
|
158
|
+
- Process::waitpid(pid)
|
159
|
+
- $?
|
160
|
+
+ wait_thr.value
|
161
|
+
end
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
165
|
+
2. Find and replace in Gemfile
|
166
|
+
```diff
|
167
|
+
- gem 'posix-spawn'
|
168
|
+
+ gem 'progeny'
|
169
|
+
```
|
170
|
+
3. Find and replace `POSIX::Spawn::Child` with `Progeny::Command` and any `POSIX::Spawn` exceptions with `Progeny`
|
171
|
+
```diff
|
172
|
+
class GitDiff
|
173
|
+
def compare(from_sha, to_sha)
|
174
|
+
- child = POSIX::Spawn::Child.new("git", "diff #{from_sha}..#{to_sha}")
|
175
|
+
+ child = Progeny::Command.new("git", "diff #{from_sha}..#{to_sha}")
|
176
|
+
child.out
|
177
|
+
end
|
178
|
+
|
179
|
+
def compare_to_remote_head
|
180
|
+
- child = POSIX::Spawn::Child.build('git', 'diff origin/main HEAD')
|
181
|
+
+ child = Progeny::Command.build('git', 'diff origin/main HEAD')
|
182
|
+
begin
|
183
|
+
child.exec!
|
184
|
+
- rescue POSIX::Spawn::MaximumOutputExceeded
|
185
|
+
+ rescue Progeny::MaximumOutputExceeded
|
186
|
+
# limit was reached
|
187
|
+
end
|
188
|
+
child.out
|
189
|
+
end
|
190
|
+
end
|
191
|
+
```
|
192
|
+
|
193
|
+
4. Confirm all is working as expected
|
194
|
+
`bundle install` and make sure your tests are passing. If you encounter any
|
195
|
+
issues feel free to open an issue or a PR.
|
196
|
+
|
@@ -0,0 +1,272 @@
|
|
1
|
+
require "open3"
|
2
|
+
|
3
|
+
module Progeny
|
4
|
+
# Progeny::Command includes logic for executing child processes and
|
5
|
+
# reading/writing from their standard input, output, and error streams. It's
|
6
|
+
# designed to take all input in a single string and provides all output
|
7
|
+
# (stderr and stdout) as single strings and is therefore not well-suited
|
8
|
+
# to streaming large quantities of data in and out of commands.
|
9
|
+
#
|
10
|
+
# Create and run a process to completion:
|
11
|
+
#
|
12
|
+
# >> child = Progeny::Command.new('git', '--help')
|
13
|
+
#
|
14
|
+
# Retrieve stdout or stderr output:
|
15
|
+
#
|
16
|
+
# >> child.out
|
17
|
+
# => "usage: git [--version] [--exec-path[=GIT_EXEC_PATH]]\n ..."
|
18
|
+
# >> child.err
|
19
|
+
# => ""
|
20
|
+
#
|
21
|
+
# Check process exit status information:
|
22
|
+
#
|
23
|
+
# >> child.status
|
24
|
+
# => #<Process::Status: pid=80718,exited(0)>
|
25
|
+
#
|
26
|
+
# To write data on the new process's stdin immediately after spawning:
|
27
|
+
#
|
28
|
+
# >> child = Progeny::Command.new('bc', :input => '40 + 2')
|
29
|
+
# >> child.out
|
30
|
+
# "42\n"
|
31
|
+
#
|
32
|
+
# To access output from the process even if an exception was raised:
|
33
|
+
#
|
34
|
+
# >> child = Progeny::Command.build('git', 'log', :max => 1000)
|
35
|
+
# >> begin
|
36
|
+
# ?> child.exec!
|
37
|
+
# ?> rescue Progeny::MaximumOutputExceeded
|
38
|
+
# ?> # just so you know
|
39
|
+
# ?> end
|
40
|
+
# >> child.out
|
41
|
+
# "... first 1000 characters of log output ..."
|
42
|
+
|
43
|
+
##
|
44
|
+
# Exception raised when the total number of bytes output on the command's
|
45
|
+
# stderr and stdout streams exceeds the maximum output size (:max option).
|
46
|
+
class MaximumOutputExceeded < StandardError
|
47
|
+
end
|
48
|
+
|
49
|
+
# Exception raised when timeout is exceeded.
|
50
|
+
class TimeoutExceeded < StandardError
|
51
|
+
end
|
52
|
+
|
53
|
+
class Command
|
54
|
+
# Spawn a new process, write all input and read all output, and wait for
|
55
|
+
# the program to exit. Supports the standard spawn interface:
|
56
|
+
# new([env], command, [argv1, ...], [options])
|
57
|
+
#
|
58
|
+
# The following options are supported in addition to the standard
|
59
|
+
# Process.spawn options:
|
60
|
+
#
|
61
|
+
# :input => str Write str to the new process's standard input.
|
62
|
+
# :timeout => int Maximum number of seconds to allow the process
|
63
|
+
# to execute before aborting with a TimeoutExceeded
|
64
|
+
# exception.
|
65
|
+
# :max => total Maximum number of bytes of output to allow the
|
66
|
+
# process to generate before aborting with a
|
67
|
+
# MaximumOutputExceeded exception.
|
68
|
+
# :pgroup_kill => bool Boolean specifying whether to kill the process
|
69
|
+
# group (true) or individual process (false, default).
|
70
|
+
# Setting this option true implies :pgroup => true.
|
71
|
+
#
|
72
|
+
# Returns a new Command instance whose underlying process has already
|
73
|
+
# executed to completion. The out, err, and status attributes are
|
74
|
+
# immediately available.
|
75
|
+
def initialize(*args)
|
76
|
+
if args.last.is_a?(Hash)
|
77
|
+
options = args.pop.dup
|
78
|
+
else
|
79
|
+
options = {}
|
80
|
+
end
|
81
|
+
|
82
|
+
if args.first.is_a?(Hash)
|
83
|
+
@env = args.shift
|
84
|
+
else
|
85
|
+
@env = {}
|
86
|
+
end
|
87
|
+
@env.merge!(options.delete(:env)) if options.key?(:env)
|
88
|
+
@argv = args
|
89
|
+
@options = options.dup
|
90
|
+
@input = @options.delete(:input)
|
91
|
+
@timeout = @options.delete(:timeout)
|
92
|
+
@max = @options.delete(:max)
|
93
|
+
if @options.delete(:pgroup_kill)
|
94
|
+
@pgroup_kill = true
|
95
|
+
@options[:pgroup] = true
|
96
|
+
end
|
97
|
+
@options.delete(:chdir) if @options[:chdir].nil?
|
98
|
+
exec! if !@options.delete(:noexec)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Set up a new process to spawn, but do not actually spawn it.
|
102
|
+
#
|
103
|
+
# Invoke this just like the normal constructor to set up a process
|
104
|
+
# to be run. Call `exec!` to actually run the child process, send
|
105
|
+
# the input, read the output, and wait for completion. Use this
|
106
|
+
# alternative way of constructing a Progency::Command if you want
|
107
|
+
# to read any partial output from the child process even after an
|
108
|
+
# exception.
|
109
|
+
#
|
110
|
+
# child = Progency::Command.build(... arguments ...)
|
111
|
+
# child.exec!
|
112
|
+
#
|
113
|
+
# The arguments are the same as the regular constructor.
|
114
|
+
#
|
115
|
+
# Returns a new Command instance but does not run the underlying process.
|
116
|
+
def self.build(*args)
|
117
|
+
options =
|
118
|
+
if args.last.is_a?(Hash)
|
119
|
+
args.pop.dup
|
120
|
+
else
|
121
|
+
{}
|
122
|
+
end
|
123
|
+
new(*(args + [{ :noexec => true }.merge(options)]))
|
124
|
+
end
|
125
|
+
|
126
|
+
# All data written to the child process's stdout stream as a String.
|
127
|
+
attr_reader :out
|
128
|
+
|
129
|
+
# All data written to the child process's stderr stream as a String.
|
130
|
+
attr_reader :err
|
131
|
+
|
132
|
+
# A Process::Status object with information on how the child exited.
|
133
|
+
attr_reader :status
|
134
|
+
|
135
|
+
# Total command execution time (wall-clock time)
|
136
|
+
attr_reader :runtime
|
137
|
+
|
138
|
+
# The pid of the spawned child process. This is unlikely to be a valid
|
139
|
+
# current pid since Command#exec! doesn't return until the process finishes
|
140
|
+
# and is reaped.
|
141
|
+
attr_reader :pid
|
142
|
+
|
143
|
+
# Determine if the process did exit with a zero exit status.
|
144
|
+
def success?
|
145
|
+
@status && @status.success?
|
146
|
+
end
|
147
|
+
|
148
|
+
# Execute command, write input, and read output. This is called
|
149
|
+
# immediately when a new instance of this object is created, or
|
150
|
+
# can be called explicitly when creating the Command via `build`.
|
151
|
+
def exec!
|
152
|
+
stdin, stdout, stderr, wait_thread = Open3.popen3(@env, *(@argv + [@options]))
|
153
|
+
@pid = wait_thread[:pid]
|
154
|
+
|
155
|
+
# async read from all streams into buffers
|
156
|
+
read_and_write(@input, stdin, stdout, stderr, @timeout, @max)
|
157
|
+
|
158
|
+
# wait_thr.value waits for the termination of the process and returns exit status
|
159
|
+
@status = wait_thread.value
|
160
|
+
rescue Object
|
161
|
+
[stdin, stdout, stderr].each { |fd| fd.close rescue nil }
|
162
|
+
if @status.nil?
|
163
|
+
if !@pgroup_kill
|
164
|
+
::Process.kill('TERM', pid) rescue nil
|
165
|
+
else
|
166
|
+
::Process.kill('-TERM', pid) rescue nil
|
167
|
+
end
|
168
|
+
@status = wait_thread.value rescue nil
|
169
|
+
end
|
170
|
+
raise
|
171
|
+
ensure
|
172
|
+
# let's be absolutely certain these are closed
|
173
|
+
[stdin, stdout, stderr].each { |fd| fd.close rescue nil }
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
# Maximum buffer size for reading
|
178
|
+
BUFSIZE = (32 * 1024)
|
179
|
+
|
180
|
+
# Start a select loop writing any input on the child's stdin and reading
|
181
|
+
# any output from the child's stdout or stderr.
|
182
|
+
#
|
183
|
+
# input - String input to write on stdin. May be nil.
|
184
|
+
# stdin - The write side IO object for the child's stdin stream.
|
185
|
+
# stdout - The read side IO object for the child's stdout stream.
|
186
|
+
# stderr - The read side IO object for the child's stderr stream.
|
187
|
+
# timeout - An optional Numeric specifying the total number of seconds
|
188
|
+
# the read/write operations should occur for.
|
189
|
+
#
|
190
|
+
# Returns an [out, err] tuple where both elements are strings with all
|
191
|
+
# data written to the stdout and stderr streams, respectively.
|
192
|
+
# Raises TimeoutExceeded when all data has not been read / written within
|
193
|
+
# the duration specified in the timeout argument.
|
194
|
+
# Raises MaximumOutputExceeded when the total number of bytes output
|
195
|
+
# exceeds the amount specified by the max argument.
|
196
|
+
def read_and_write(input, stdin, stdout, stderr, timeout=nil, max=nil)
|
197
|
+
max = nil if max && max <= 0
|
198
|
+
@out, @err = '', ''
|
199
|
+
|
200
|
+
# force all string and IO encodings to BINARY under 1.9 for now
|
201
|
+
if @out.respond_to?(:force_encoding) and stdin.respond_to?(:set_encoding)
|
202
|
+
[stdin, stdout, stderr].each do |fd|
|
203
|
+
fd.set_encoding('BINARY', 'BINARY')
|
204
|
+
end
|
205
|
+
@out.force_encoding('BINARY')
|
206
|
+
@err.force_encoding('BINARY')
|
207
|
+
input = input.dup.force_encoding('BINARY') if input
|
208
|
+
end
|
209
|
+
|
210
|
+
timeout = nil if timeout && timeout <= 0.0
|
211
|
+
@runtime = 0.0
|
212
|
+
start = Time.now
|
213
|
+
|
214
|
+
readers = [stdout, stderr]
|
215
|
+
writers =
|
216
|
+
if input
|
217
|
+
[stdin]
|
218
|
+
else
|
219
|
+
stdin.close
|
220
|
+
[]
|
221
|
+
end
|
222
|
+
slice_method = input.respond_to?(:byteslice) ? :byteslice : :slice
|
223
|
+
t = timeout
|
224
|
+
|
225
|
+
while readers.any? || writers.any?
|
226
|
+
ready = IO.select(readers, writers, readers + writers, t)
|
227
|
+
raise TimeoutExceeded if ready.nil?
|
228
|
+
|
229
|
+
# write to stdin stream
|
230
|
+
ready[1].each do |fd|
|
231
|
+
begin
|
232
|
+
boom = nil
|
233
|
+
size = fd.write_nonblock(input)
|
234
|
+
input = input.send(slice_method, size..-1)
|
235
|
+
rescue Errno::EPIPE => boom
|
236
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
237
|
+
end
|
238
|
+
if boom || input.bytesize == 0
|
239
|
+
stdin.close
|
240
|
+
writers.delete(stdin)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# read from stdout and stderr streams
|
245
|
+
ready[0].each do |fd|
|
246
|
+
buf = (fd == stdout) ? @out : @err
|
247
|
+
begin
|
248
|
+
buf << fd.readpartial(BUFSIZE)
|
249
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
250
|
+
rescue EOFError
|
251
|
+
readers.delete(fd)
|
252
|
+
fd.close
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# keep tabs on the total amount of time we've spent here
|
257
|
+
@runtime = Time.now - start
|
258
|
+
if timeout
|
259
|
+
t = timeout - @runtime
|
260
|
+
raise TimeoutExceeded if t < 0.0
|
261
|
+
end
|
262
|
+
|
263
|
+
# maybe we've hit our max output
|
264
|
+
if max && ready[0].any? && (@out.size + @err.size) > max
|
265
|
+
raise MaximumOutputExceeded
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
[@out, @err]
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
data/lib/progeny.rb
ADDED
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: progeny
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Luan Vieira
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-05-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: minitest
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '13.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '13.0'
|
41
|
+
description: Spawn child processes without managing IO streams, zombie processes and
|
42
|
+
other details.
|
43
|
+
email:
|
44
|
+
- luanv@me.com
|
45
|
+
executables: []
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- CHANGELOG.md
|
50
|
+
- LICENSE.txt
|
51
|
+
- README.md
|
52
|
+
- lib/progeny.rb
|
53
|
+
- lib/progeny/command.rb
|
54
|
+
- lib/progeny/version.rb
|
55
|
+
homepage: https://github.com/luanzeba/progeny
|
56
|
+
licenses:
|
57
|
+
- MIT
|
58
|
+
metadata:
|
59
|
+
homepage_uri: https://github.com/luanzeba/progeny
|
60
|
+
source_code_uri: https://github.com/luanzeba/progeny
|
61
|
+
changelog_uri: https://github.com/luanzeba/progeny/blob/main/CHANGELOG.md
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options: []
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: 2.7.0
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
requirements: []
|
77
|
+
rubygems_version: 3.4.10
|
78
|
+
signing_key:
|
79
|
+
specification_version: 4
|
80
|
+
summary: A popen3 wrapper with a nice interface and extra options.
|
81
|
+
test_files: []
|