exec_service 0.0.0 → 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 +4 -4
- data/.yardopts +10 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.md +21 -0
- data/README.md +101 -7
- data/lib/exec_service/controller.rb +359 -0
- data/lib/exec_service/executor.rb +962 -0
- data/lib/exec_service/opts.rb +102 -0
- data/lib/exec_service/result.rb +191 -0
- data/lib/exec_service/version.rb +9 -0
- data/lib/exec_service.rb +511 -6
- metadata +40 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 33a8ad6fafa08f5efe31aba23468497bace9859453d3a12d23189c73eb57cc4e
|
|
4
|
+
data.tar.gz: c38f315c6ed811d2e284847473c184e01dba57847e05266c9c88b4706214626a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab846b2e66984966fdddff8a9ed7c815473484f80508eeeedc42fc2bc48e3756529e332efb497d8913a62db3412c1063589c37a98f91e28c3404bf8622494938
|
|
7
|
+
data.tar.gz: 82f3c1943930fffe8d0b96d48087e648e0b39ff8dba5601b1c595e6dde362ca16cc987cc924a381534676ba3553285af24f942550433914ec8c56bf857207d18
|
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# License
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Daniel Azuma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
21
|
+
IN THE SOFTWARE.
|
data/README.md
CHANGED
|
@@ -1,9 +1,103 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ExecService
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
`ExecService` is a full-featured Ruby service class for running and managing
|
|
4
|
+
subprocesses. It provides a rich interface for spawning processes, controlling
|
|
5
|
+
and monitoring those processes, setting up and redirecting streams, and
|
|
6
|
+
interpreting results. It also provides shortcuts for common cases such as
|
|
7
|
+
invoking Ruby in a subprocess or capturing output in a string. Use
|
|
8
|
+
`ExecService` when existing interfaces such as the `system()` method or the
|
|
9
|
+
`Open3` library are not sufficiently powerful or expressive for your needs.
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
`
|
|
11
|
+
## Getting started
|
|
12
|
+
|
|
13
|
+
Install `ExecService` via the
|
|
14
|
+
[exec_service gem](https://rubygems.org/gems/exec_service).
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
% gem install exec_service
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
or add it to your Gemfile:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
gem "exec_service"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
To use the service, instantiate `ExecService`, and call the convenient methods
|
|
27
|
+
on it to spawn subprocesses:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require "exec_service"
|
|
31
|
+
exec_service = ExecService.new
|
|
32
|
+
git_version = exec_service.capture(["git", "--version"]).chomp
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
* Execute subprocesses in the foreground (i.e. blocking until completion) or
|
|
38
|
+
background (returning immediately)
|
|
39
|
+
* Fork (execute a proc in a subprocess) or spawn (specify a command to run)
|
|
40
|
+
* Environment setup for the subprocess, including
|
|
41
|
+
* Working directory
|
|
42
|
+
* Environment variables
|
|
43
|
+
* Optionally disabling any existing bundle
|
|
44
|
+
* Umask
|
|
45
|
+
* Process group
|
|
46
|
+
* Rich process control interface, including:
|
|
47
|
+
* Access to process state and results
|
|
48
|
+
* Read/write access to non-redirected streams
|
|
49
|
+
* Signalling
|
|
50
|
+
* Joining
|
|
51
|
+
* Robust setup and redirect of streams, including:
|
|
52
|
+
* Inheriting parent process streams
|
|
53
|
+
* Redirecting to/from files
|
|
54
|
+
* Redirecting to/from pipes
|
|
55
|
+
* Redirecting to/from arbitrary IO objects
|
|
56
|
+
* Redirecting error to out and vice versa
|
|
57
|
+
* Reading input from strings
|
|
58
|
+
* Capturing output streams
|
|
59
|
+
* Tees for output streams
|
|
60
|
+
* Redirecting to/from null
|
|
61
|
+
* Closing streams
|
|
62
|
+
* Rich result reporting, including exit status, signals, and exceptions
|
|
63
|
+
* Convenience methods for common use cases, including:
|
|
64
|
+
* Simple output captures
|
|
65
|
+
* Running Ruby processes
|
|
66
|
+
* Executing a string in the shell
|
|
67
|
+
* Customizable logging
|
|
68
|
+
|
|
69
|
+
## Contributing
|
|
70
|
+
|
|
71
|
+
Development is done in GitHub at https://github.com/dazuma/exec_service.
|
|
72
|
+
|
|
73
|
+
* To file issues: https://github.com/dazuma/exec_service/issues.
|
|
74
|
+
* For questions and discussion, please do not file an issue. Instead, use the
|
|
75
|
+
discussions feature: https://github.com/dazuma/exec_service/discussions.
|
|
76
|
+
* Pull requests are welcome, but in general please open an issue first before
|
|
77
|
+
contributing significant changes.
|
|
78
|
+
|
|
79
|
+
The library uses [toys](https://dazuma.github.io/toys) for testing and CI. To
|
|
80
|
+
run the test suite, `gem install toys` and then run `toys ci`. You can also run
|
|
81
|
+
unit tests, rubocop, and build tests independently.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
Copyright 2026 Daniel Azuma
|
|
86
|
+
|
|
87
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
88
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
89
|
+
in the Software without restriction, including without limitation the rights
|
|
90
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
91
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
92
|
+
furnished to do so, subject to the following conditions:
|
|
93
|
+
|
|
94
|
+
The above copyright notice and this permission notice shall be included in
|
|
95
|
+
all copies or substantial portions of the Software.
|
|
96
|
+
|
|
97
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
98
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
99
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
100
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
101
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
102
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
103
|
+
IN THE SOFTWARE.
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ExecService
|
|
4
|
+
##
|
|
5
|
+
# An object that controls a subprocess. This object is returned from an
|
|
6
|
+
# execution running in the background, or is yielded to a control block
|
|
7
|
+
# for an execution running in the foreground.
|
|
8
|
+
# You can use this object to interact with the subcommand's streams,
|
|
9
|
+
# send signals to the process, and get its result.
|
|
10
|
+
#
|
|
11
|
+
class Controller
|
|
12
|
+
##
|
|
13
|
+
# The subcommand's name.
|
|
14
|
+
# @return [Object]
|
|
15
|
+
#
|
|
16
|
+
attr_reader :name
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# The subcommand's standard input stream (which can be written to).
|
|
20
|
+
#
|
|
21
|
+
# @return [IO] if the command was configured with `in: :controller`
|
|
22
|
+
# @return [nil] if the command was not configured with
|
|
23
|
+
# `in: :controller`
|
|
24
|
+
#
|
|
25
|
+
attr_reader :in
|
|
26
|
+
|
|
27
|
+
##
|
|
28
|
+
# The subcommand's standard output stream (which can be read from).
|
|
29
|
+
#
|
|
30
|
+
# @return [IO] if the command was configured with `out: :controller`
|
|
31
|
+
# @return [nil] if the command was not configured with
|
|
32
|
+
# `out: :controller`
|
|
33
|
+
#
|
|
34
|
+
attr_reader :out
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# The subcommand's standard error stream (which can be read from).
|
|
38
|
+
#
|
|
39
|
+
# @return [IO] if the command was configured with `err: :controller`
|
|
40
|
+
# @return [nil] if the command was not configured with
|
|
41
|
+
# `err: :controller`
|
|
42
|
+
#
|
|
43
|
+
attr_reader :err
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# The process ID.
|
|
47
|
+
#
|
|
48
|
+
# Exactly one of {#exception} and {#pid} will be non-nil.
|
|
49
|
+
#
|
|
50
|
+
# @return [Integer] if the process start was successful
|
|
51
|
+
# @return [nil] if the process could not be started.
|
|
52
|
+
#
|
|
53
|
+
attr_reader :pid
|
|
54
|
+
|
|
55
|
+
##
|
|
56
|
+
# The exception raised when the process failed to start.
|
|
57
|
+
#
|
|
58
|
+
# Exactly one of {#exception} and {#pid} will be non-nil.
|
|
59
|
+
#
|
|
60
|
+
# @return [Exception] if the process failed to start.
|
|
61
|
+
# @return [nil] if the process start was successful.
|
|
62
|
+
#
|
|
63
|
+
attr_reader :exception
|
|
64
|
+
|
|
65
|
+
##
|
|
66
|
+
# Captures the remaining data in the given stream.
|
|
67
|
+
# After calling this, do not read directly from the stream.
|
|
68
|
+
#
|
|
69
|
+
# @param which [:out,:err] Which stream to capture
|
|
70
|
+
#
|
|
71
|
+
# @return [self] if the stream was captured
|
|
72
|
+
# @return [nil] if the stream was not captured because the process has
|
|
73
|
+
# completed or did not start successfully
|
|
74
|
+
#
|
|
75
|
+
def capture(which)
|
|
76
|
+
@streams_mutex.synchronize do
|
|
77
|
+
return nil unless @streams_open
|
|
78
|
+
stream = stream_for(which, allow_in: false)
|
|
79
|
+
@join_threads << ::Thread.new do
|
|
80
|
+
data = stream.read
|
|
81
|
+
@captures_mutex.synchronize do
|
|
82
|
+
@captures[which] = data
|
|
83
|
+
end
|
|
84
|
+
ensure
|
|
85
|
+
stream.close
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
self
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
##
|
|
92
|
+
# Captures the remaining data in the standard output stream.
|
|
93
|
+
# After calling this, do not read directly from the stream.
|
|
94
|
+
#
|
|
95
|
+
# @return [self]
|
|
96
|
+
#
|
|
97
|
+
def capture_out
|
|
98
|
+
capture(:out)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
##
|
|
102
|
+
# Captures the remaining data in the standard error stream.
|
|
103
|
+
# After calling this, do not read directly from the stream.
|
|
104
|
+
#
|
|
105
|
+
# @return [self]
|
|
106
|
+
#
|
|
107
|
+
def capture_err
|
|
108
|
+
capture(:err)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
##
|
|
112
|
+
# Redirects the remainder of the given stream.
|
|
113
|
+
#
|
|
114
|
+
# You can specify the stream as an IO or IO-like object, or as a file
|
|
115
|
+
# specified by its path. If specifying a file, you can optionally
|
|
116
|
+
# provide the mode and permissions for the call to `File#open`. You can
|
|
117
|
+
# also specify the value `:null` to indicate the null file.
|
|
118
|
+
#
|
|
119
|
+
# If the stream is redirected to an IO-like object, it is _not_ closed
|
|
120
|
+
# when the process is completed. (If it is redirected to a file
|
|
121
|
+
# specified by path, the file is closed on completion.)
|
|
122
|
+
#
|
|
123
|
+
# After calling this, do not interact directly with the stream.
|
|
124
|
+
#
|
|
125
|
+
# @param which [:in,:out,:err] Which stream to redirect
|
|
126
|
+
# @param io [IO,StringIO,String,:null] Where to redirect the stream
|
|
127
|
+
# @param io_args [Object...] The mode and permissions for opening the
|
|
128
|
+
# file, if redirecting to/from a file.
|
|
129
|
+
#
|
|
130
|
+
# @return [self] if the stream was redirected
|
|
131
|
+
# @return [nil] if the stream was not redirected because the process
|
|
132
|
+
# has completed or did not start successfully
|
|
133
|
+
#
|
|
134
|
+
def redirect(which, io, *io_args)
|
|
135
|
+
@streams_mutex.synchronize do
|
|
136
|
+
return nil unless @streams_open
|
|
137
|
+
io = ::File::NULL if io == :null
|
|
138
|
+
close_afterward = false
|
|
139
|
+
if io.is_a?(::String)
|
|
140
|
+
io_args = which == :in ? ["r"] : ["w"] if io_args.empty?
|
|
141
|
+
io = ::File.open(io, *io_args)
|
|
142
|
+
close_afterward = true
|
|
143
|
+
end
|
|
144
|
+
stream = stream_for(which, allow_in: true)
|
|
145
|
+
@join_threads << ::Thread.new do
|
|
146
|
+
if which == :in
|
|
147
|
+
::IO.copy_stream(io, stream)
|
|
148
|
+
else
|
|
149
|
+
::IO.copy_stream(stream, io)
|
|
150
|
+
end
|
|
151
|
+
ensure
|
|
152
|
+
stream.close
|
|
153
|
+
io.close if close_afterward
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
self
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
##
|
|
160
|
+
# Redirects the remainder of the standard input stream.
|
|
161
|
+
#
|
|
162
|
+
# You can specify the stream as an IO or IO-like object, or as a file
|
|
163
|
+
# specified by its path. If specifying a file, you can optionally
|
|
164
|
+
# provide the mode and permissions for the call to `File#open`. You can
|
|
165
|
+
# also specify the value `:null` to indicate the null file.
|
|
166
|
+
#
|
|
167
|
+
# After calling this, do not interact directly with the stream.
|
|
168
|
+
#
|
|
169
|
+
# @param io [IO,StringIO,String,:null] Where to redirect the stream
|
|
170
|
+
# @param io_args [Object...] The mode and permissions for opening the
|
|
171
|
+
# file, if redirecting from a file.
|
|
172
|
+
#
|
|
173
|
+
# @return [self] if the stream was redirected
|
|
174
|
+
# @return [nil] if the stream was not redirected because the process
|
|
175
|
+
# has completed or did not start successfully
|
|
176
|
+
#
|
|
177
|
+
def redirect_in(io, *io_args)
|
|
178
|
+
redirect(:in, io, *io_args)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
##
|
|
182
|
+
# Redirects the remainder of the standard output stream.
|
|
183
|
+
#
|
|
184
|
+
# You can specify the stream as an IO or IO-like object, or as a file
|
|
185
|
+
# specified by its path. If specifying a file, you can optionally
|
|
186
|
+
# provide the mode and permissions for the call to `File#open`. You can
|
|
187
|
+
# also specify the value `:null` to indicate the null file.
|
|
188
|
+
#
|
|
189
|
+
# After calling this, do not interact directly with the stream.
|
|
190
|
+
#
|
|
191
|
+
# @param io [IO,StringIO,String,:null] Where to redirect the stream
|
|
192
|
+
# @param io_args [Object...] The mode and permissions for opening the
|
|
193
|
+
# file, if redirecting to a file.
|
|
194
|
+
#
|
|
195
|
+
# @return [self] if the stream was redirected
|
|
196
|
+
# @return [nil] if the stream was not redirected because the process
|
|
197
|
+
# has completed or did not start successfully
|
|
198
|
+
#
|
|
199
|
+
def redirect_out(io, *io_args)
|
|
200
|
+
redirect(:out, io, *io_args)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
##
|
|
204
|
+
# Redirects the remainder of the standard error stream.
|
|
205
|
+
#
|
|
206
|
+
# You can specify the stream as an IO or IO-like object, or as a file
|
|
207
|
+
# specified by its path. If specifying a file, you can optionally
|
|
208
|
+
# provide the mode and permissions for the call to `File#open`. You can
|
|
209
|
+
# also specify the value `:null` to indicate the null file.
|
|
210
|
+
#
|
|
211
|
+
# After calling this, do not interact directly with the stream.
|
|
212
|
+
#
|
|
213
|
+
# @param io [IO,StringIO,String,:null] Where to redirect the stream
|
|
214
|
+
# @param io_args [Object...] The mode and permissions for opening the
|
|
215
|
+
# file, if redirecting to a file.
|
|
216
|
+
#
|
|
217
|
+
# @return [self] if the stream was redirected
|
|
218
|
+
# @return [nil] if the stream was not redirected because the process
|
|
219
|
+
# has completed or did not start successfully
|
|
220
|
+
#
|
|
221
|
+
def redirect_err(io, *io_args)
|
|
222
|
+
redirect(:err, io, *io_args)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
##
|
|
226
|
+
# Send the given signal to the process. The signal can be specified
|
|
227
|
+
# by name or number.
|
|
228
|
+
#
|
|
229
|
+
# @param sig [Integer,String] The signal to send.
|
|
230
|
+
# @return [self]
|
|
231
|
+
#
|
|
232
|
+
def kill(sig)
|
|
233
|
+
::Process.kill(sig, pid) if pid
|
|
234
|
+
self
|
|
235
|
+
end
|
|
236
|
+
alias signal kill
|
|
237
|
+
|
|
238
|
+
##
|
|
239
|
+
# Determine whether the subcommand is still executing
|
|
240
|
+
#
|
|
241
|
+
# @return [boolean]
|
|
242
|
+
#
|
|
243
|
+
def executing?
|
|
244
|
+
@completion_thread&.status ? true : false
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
##
|
|
248
|
+
# Wait for the subcommand to complete, and return a result object.
|
|
249
|
+
#
|
|
250
|
+
# @param timeout [Numeric,nil] The timeout in seconds, or `nil` to
|
|
251
|
+
# wait indefinitely.
|
|
252
|
+
# @return [ExecService::Result] The result object
|
|
253
|
+
# @return [nil] if a timeout occurred.
|
|
254
|
+
#
|
|
255
|
+
def result(timeout: nil)
|
|
256
|
+
return nil if @completion_thread && !@completion_thread.join(timeout)
|
|
257
|
+
# @completion_thread sets @result, so the final value is guaranteed
|
|
258
|
+
# to be stable once the thread has joined above.
|
|
259
|
+
@result
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
##
|
|
263
|
+
# @private
|
|
264
|
+
#
|
|
265
|
+
def initialize(name:, controller_streams:, captures:, pid_or_exception:,
|
|
266
|
+
join_threads:, background_callback:, captures_mutex:)
|
|
267
|
+
@name = name
|
|
268
|
+
@in = controller_streams[:in]
|
|
269
|
+
@out = controller_streams[:out]
|
|
270
|
+
@err = controller_streams[:err]
|
|
271
|
+
@captures = captures
|
|
272
|
+
@join_threads = join_threads
|
|
273
|
+
@background_callback = background_callback
|
|
274
|
+
@captures_mutex = captures_mutex
|
|
275
|
+
@streams_open = false
|
|
276
|
+
@streams_mutex = ::Mutex.new
|
|
277
|
+
@pid = @exception = @completion_thread = @result = nil
|
|
278
|
+
case pid_or_exception
|
|
279
|
+
when ::Integer
|
|
280
|
+
@pid = pid_or_exception
|
|
281
|
+
@streams_open = true
|
|
282
|
+
@completion_thread = ::Thread.new do
|
|
283
|
+
_pid, status = ::Process.wait2(@pid)
|
|
284
|
+
cleanup(status)
|
|
285
|
+
end
|
|
286
|
+
when ::Exception
|
|
287
|
+
@exception = pid_or_exception
|
|
288
|
+
cleanup(nil)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
##
|
|
293
|
+
# Close the controller's input stream, if any.
|
|
294
|
+
#
|
|
295
|
+
# @private
|
|
296
|
+
#
|
|
297
|
+
def close_in_stream
|
|
298
|
+
@streams_mutex.synchronize do
|
|
299
|
+
@in&.close
|
|
300
|
+
end
|
|
301
|
+
self
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
##
|
|
305
|
+
# Close the controller's output streams, if any.
|
|
306
|
+
#
|
|
307
|
+
# @private
|
|
308
|
+
#
|
|
309
|
+
def close_out_streams
|
|
310
|
+
@streams_mutex.synchronize do
|
|
311
|
+
@out&.close
|
|
312
|
+
@err&.close
|
|
313
|
+
end
|
|
314
|
+
self
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
private
|
|
318
|
+
|
|
319
|
+
##
|
|
320
|
+
# Cleanup after the child process ends.
|
|
321
|
+
# Blocks any further captures/redirects, joins all stream processing
|
|
322
|
+
# threads, and sets the result. Also kicks off the callback if run in
|
|
323
|
+
# the background.
|
|
324
|
+
#
|
|
325
|
+
def cleanup(status)
|
|
326
|
+
@streams_mutex.synchronize do
|
|
327
|
+
@streams_open = false
|
|
328
|
+
end
|
|
329
|
+
@join_threads.each(&:join)
|
|
330
|
+
@result = Result.new(@name, @captures[:out], @captures[:err], status, @exception)
|
|
331
|
+
if @background_callback
|
|
332
|
+
::Thread.new do
|
|
333
|
+
@background_callback.call(@result)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def stream_for(which, allow_in: false)
|
|
339
|
+
stream = nil
|
|
340
|
+
case which
|
|
341
|
+
when :out
|
|
342
|
+
stream = @out
|
|
343
|
+
@out = nil
|
|
344
|
+
when :err
|
|
345
|
+
stream = @err
|
|
346
|
+
@err = nil
|
|
347
|
+
when :in
|
|
348
|
+
if allow_in
|
|
349
|
+
stream = @in
|
|
350
|
+
@in = nil
|
|
351
|
+
end
|
|
352
|
+
else
|
|
353
|
+
raise ::ArgumentError, "Unknown stream #{which}"
|
|
354
|
+
end
|
|
355
|
+
raise ::ArgumentError, "Stream #{which} not available" unless stream
|
|
356
|
+
stream
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|