process_executer 4.1.0 → 4.1.1
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/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +14 -0
- data/lib/process_executer/commands/run.rb +58 -46
- data/lib/process_executer/commands/run_with_capture.rb +66 -49
- data/lib/process_executer/commands/spawn_with_timeout.rb +36 -6
- data/lib/process_executer/monitored_pipe.rb +15 -6
- data/lib/process_executer/options/base.rb +20 -8
- data/lib/process_executer/options/run_with_capture_options.rb +34 -65
- data/lib/process_executer/version.rb +1 -1
- data/release-please-config.json +0 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a6dbea0ce77694bd73783f7191fc9b32957fefda87f9db6108d97ebf26bb1a9f
|
|
4
|
+
data.tar.gz: e710378f301c56c649063d6bfef0525f0c0c8dcf6adf6329e12184ec24d02894
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 756a9aacc5d173fa2cedd68f178c9e321e141d3ef75f61ddcfe7e3ebe67f236a0603192f24c49aaedbc0dcf209d61dd16b593477f67d974fa1f2e875ec17dd7d
|
|
7
|
+
data.tar.gz: a9ce354984f37839c54abfb453eef726d6467467dc529aaa2108643c2e153005022b097b9a1ecfea368391ff9ebbf8ba20b32b9cd73db11c5076394824d81528
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to the process_executer gem will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [4.1.1](https://github.com/main-branch/process_executer/compare/v4.1.0...v4.1.1) (2026-08-31)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Other Changes
|
|
12
|
+
|
|
13
|
+
* Characterize validation ordering when unknown and invalid options are given together ([#192](https://github.com/main-branch/process_executer/issues/192)) ([15d6e06](https://github.com/main-branch/process_executer/commit/15d6e063170484f309efd30867238b867484288b))
|
|
14
|
+
* Collapse encoding validators and clarify capture-redirection assembly ([#194](https://github.com/main-branch/process_executer/issues/194)) ([bab8610](https://github.com/main-branch/process_executer/commit/bab86101ac4cdbd3747e72d0d7f841623a6e9b20))
|
|
15
|
+
* Contribute subclass redirections via an internal_redirections hook ([#190](https://github.com/main-branch/process_executer/issues/190)) ([8ecbda5](https://github.com/main-branch/process_executer/commit/8ecbda54874de933ab07a64d91e0a07e17f7ece0))
|
|
16
|
+
* Derive the process-group isolation decision from a single source ([#191](https://github.com/main-branch/process_executer/issues/191)) ([5d8ac7d](https://github.com/main-branch/process_executer/commit/5d8ac7dc38a369a7fe6d6cf3d05bd1eec799f3dc))
|
|
17
|
+
* Describe every source of MonitoredPipe#exception ([#193](https://github.com/main-branch/process_executer/issues/193)) ([333b7bd](https://github.com/main-branch/process_executer/commit/333b7bd4c552c246d7d7505b5aa3e1a123ac5f9b))
|
|
18
|
+
* Extract shared validate! from Options::Base#initialize and #merge! ([#192](https://github.com/main-branch/process_executer/issues/192)) ([422956a](https://github.com/main-branch/process_executer/commit/422956acaab46e29ecc1373d017d719ea6eb497f))
|
|
19
|
+
* Remove the release-as 4.1.0 override from the release-please config ([65de5b8](https://github.com/main-branch/process_executer/commit/65de5b8b4a7f926c4e71b094ead5ff9abc6ff102))
|
|
20
|
+
* Track opened pipes as per-call instance state instead of an out-parameter ([#190](https://github.com/main-branch/process_executer/issues/190)) ([f279f1e](https://github.com/main-branch/process_executer/commit/f279f1e0672b91b966ee73cbadf00749522e488d))
|
|
21
|
+
|
|
8
22
|
## [4.1.0](https://github.com/main-branch/process_executer/compare/v4.0.4...v4.1.0) (2026-08-30)
|
|
9
23
|
|
|
10
24
|
|
|
@@ -21,29 +21,16 @@ module ProcessExecuter
|
|
|
21
21
|
# destination. This means that you can redirect to a StringIO which is not possible
|
|
22
22
|
# with `Process.spawn`.
|
|
23
23
|
#
|
|
24
|
-
# The wrapper pipes are kept in
|
|
25
|
-
# user's options only when `Process.spawn` is called.
|
|
26
|
-
#
|
|
27
|
-
#
|
|
24
|
+
# The wrapper pipes are kept in internal per-call state that is combined
|
|
25
|
+
# with the user's options only when `Process.spawn` is called. Subclasses
|
|
26
|
+
# contribute additional redirections by overriding `#internal_redirections`.
|
|
27
|
+
# The options object the caller gave is never modified, so it can be reused
|
|
28
|
+
# for another run and `result.options` returns the destinations as the user
|
|
29
|
+
# configured them.
|
|
28
30
|
#
|
|
29
31
|
# @api private
|
|
30
32
|
#
|
|
31
33
|
class Run < SpawnWithTimeout
|
|
32
|
-
# Create a new Run instance
|
|
33
|
-
#
|
|
34
|
-
# @example
|
|
35
|
-
# options = ProcessExecuter::Options::RunOptions.new(raise_errors: true)
|
|
36
|
-
# result = ProcessExecuter::Commands::Run.new('echo hello', options).call
|
|
37
|
-
# result.success? # => true
|
|
38
|
-
#
|
|
39
|
-
# @param command [Array<String>] The command to run in the subprocess
|
|
40
|
-
# @param options [ProcessExecuter::Options::RunOptions] The options to use when running the command
|
|
41
|
-
#
|
|
42
|
-
def initialize(command, options)
|
|
43
|
-
super
|
|
44
|
-
@redirection_overrides = {}
|
|
45
|
-
end
|
|
46
|
-
|
|
47
34
|
# Run a command and return the result
|
|
48
35
|
#
|
|
49
36
|
# Wrap the stdout and stderr redirection destinations in pipes and then execute
|
|
@@ -72,29 +59,60 @@ module ProcessExecuter
|
|
|
72
59
|
# @return [ProcessExecuter::Result] The result of the completed subprocess
|
|
73
60
|
#
|
|
74
61
|
def call
|
|
75
|
-
opened_pipes = {}
|
|
76
|
-
|
|
62
|
+
@opened_pipes = {}
|
|
63
|
+
@redirection_overrides = internal_redirections
|
|
64
|
+
wrap_stdout_stderr
|
|
77
65
|
super.tap do
|
|
78
66
|
log_result
|
|
79
67
|
raise_errors if options.raise_errors
|
|
80
68
|
end
|
|
81
69
|
ensure
|
|
82
|
-
close_pipes_and_check_errors(
|
|
70
|
+
close_pipes_and_check_errors($ERROR_INFO)
|
|
83
71
|
end
|
|
84
72
|
|
|
85
73
|
private
|
|
86
74
|
|
|
87
75
|
# Redirection options to apply on top of the user's options at spawn time
|
|
88
76
|
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
77
|
+
# Reset at the start of each {#call}: seeded with
|
|
78
|
+
# {#internal_redirections} and then updated by {#wrap_stdout_stderr},
|
|
79
|
+
# which replaces each eligible destination with its {MonitoredPipe}
|
|
80
|
+
# wrapper. Keeping these here instead of writing them into {#options}
|
|
92
81
|
# leaves the caller's options object unmodified.
|
|
93
82
|
#
|
|
83
|
+
# Unlike {#opened_pipes}, this hash may hold values that are not
|
|
84
|
+
# {MonitoredPipe}s (for example, the `[:child, 1]` redirection a subclass
|
|
85
|
+
# adds for merged output).
|
|
86
|
+
#
|
|
94
87
|
# @return [Hash<Object, Object>]
|
|
95
88
|
#
|
|
96
89
|
attr_reader :redirection_overrides
|
|
97
90
|
|
|
91
|
+
# Redirections this class adds on top of the user's options
|
|
92
|
+
#
|
|
93
|
+
# Called once at the start of each {#call} to seed
|
|
94
|
+
# {#redirection_overrides}, before {#wrap_stdout_stderr} runs. Returns an
|
|
95
|
+
# empty hash; subclasses override this method to contribute their own
|
|
96
|
+
# redirections (such as {RunWithCapture}'s capture redirections) instead
|
|
97
|
+
# of mutating this object's state.
|
|
98
|
+
#
|
|
99
|
+
# @return [Hash<Object, Object>]
|
|
100
|
+
#
|
|
101
|
+
def internal_redirections = {}
|
|
102
|
+
|
|
103
|
+
# The wrapper pipes created by {#wrap_stdout_stderr}
|
|
104
|
+
#
|
|
105
|
+
# Keyed by the redirection option key. Reset at the start of each {#call}. By construction this hash contains
|
|
106
|
+
# exactly the pipes this object created during the current call -- never
|
|
107
|
+
# a destination supplied by the caller -- so `#call`'s ensure block can
|
|
108
|
+
# close everything in it without ever closing a caller-owned pipe (a
|
|
109
|
+
# caller's own {MonitoredPipe} destination is wrapped like any other
|
|
110
|
+
# destination, and only the wrapper is recorded here).
|
|
111
|
+
#
|
|
112
|
+
# @return [Hash<Object, ProcessExecuter::MonitoredPipe>]
|
|
113
|
+
#
|
|
114
|
+
attr_reader :opened_pipes
|
|
115
|
+
|
|
98
116
|
# The options to pass to Process.spawn
|
|
99
117
|
#
|
|
100
118
|
# The user's spawn options with the redirection destinations replaced by
|
|
@@ -106,20 +124,19 @@ module ProcessExecuter
|
|
|
106
124
|
|
|
107
125
|
# Wrap the stdout and stderr redirection options with a MonitoredPipe
|
|
108
126
|
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
# if creating a later pipe raises, the caller's ensure block can close the
|
|
115
|
-
# pipes created so far.
|
|
127
|
+
# Each wrapper pipe is recorded in two collections with different roles:
|
|
128
|
+
# {#opened_pipes}, the pipes this object owns and must close, and
|
|
129
|
+
# {#redirection_overrides}, the destinations to hand `Process.spawn`.
|
|
130
|
+
# Neither is written into {#options}, so the caller's options object is
|
|
131
|
+
# not modified.
|
|
116
132
|
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
133
|
+
# Each pipe is recorded as soon as it is created so that, if creating a
|
|
134
|
+
# later pipe raises, `#call`'s ensure block can close the pipes created
|
|
135
|
+
# so far.
|
|
119
136
|
#
|
|
120
|
-
# @return [
|
|
137
|
+
# @return [void]
|
|
121
138
|
#
|
|
122
|
-
def wrap_stdout_stderr
|
|
139
|
+
def wrap_stdout_stderr
|
|
123
140
|
effective_redirections.each do |key, value|
|
|
124
141
|
next unless should_wrap?(key, value)
|
|
125
142
|
|
|
@@ -127,7 +144,6 @@ module ProcessExecuter
|
|
|
127
144
|
opened_pipes[key] = wrapped_destination
|
|
128
145
|
redirection_overrides[key] = wrapped_destination
|
|
129
146
|
end
|
|
130
|
-
opened_pipes
|
|
131
147
|
end
|
|
132
148
|
|
|
133
149
|
# The options as given by the user with {#redirection_overrides} applied
|
|
@@ -136,14 +152,12 @@ module ProcessExecuter
|
|
|
136
152
|
#
|
|
137
153
|
def effective_redirections = options.to_h.merge(redirection_overrides)
|
|
138
154
|
|
|
139
|
-
# Close the
|
|
155
|
+
# Close the opened pipes and raise any pipe error unless already unwinding
|
|
140
156
|
#
|
|
141
157
|
# When `in_flight_error` is set, `#call` is unwinding from an exception
|
|
142
158
|
# and that exception (not a pipe destination error or a pipe cleanup
|
|
143
159
|
# error) must be the one the caller sees, so nothing is raised here.
|
|
144
160
|
#
|
|
145
|
-
# @param opened_pipes [Hash<Object, ProcessExecuter::MonitoredPipe>] the pipes to close
|
|
146
|
-
#
|
|
147
161
|
# @param in_flight_error [Exception, nil] the exception `#call` is unwinding from, if any
|
|
148
162
|
#
|
|
149
163
|
# @raise [ProcessExecuter::ProcessIOError] if a pipe recorded a destination
|
|
@@ -153,8 +167,8 @@ module ProcessExecuter
|
|
|
153
167
|
#
|
|
154
168
|
# @return [void]
|
|
155
169
|
#
|
|
156
|
-
def close_pipes_and_check_errors(
|
|
157
|
-
close_error = close_pipes
|
|
170
|
+
def close_pipes_and_check_errors(in_flight_error)
|
|
171
|
+
close_error = close_pipes
|
|
158
172
|
return if in_flight_error
|
|
159
173
|
|
|
160
174
|
opened_pipes.each do |option_key, pipe|
|
|
@@ -164,7 +178,7 @@ module ProcessExecuter
|
|
|
164
178
|
raise close_error if close_error
|
|
165
179
|
end
|
|
166
180
|
|
|
167
|
-
# Close the
|
|
181
|
+
# Close the opened pipes, continuing if closing one of them raises
|
|
168
182
|
#
|
|
169
183
|
# Closing continues past a failure so that one pipe's error does not leak
|
|
170
184
|
# the monitoring threads and file descriptors of the pipes after it.
|
|
@@ -176,11 +190,9 @@ module ProcessExecuter
|
|
|
176
190
|
# whose drain is cut short by the deadline records it via
|
|
177
191
|
# {MonitoredPipe#truncated?}.
|
|
178
192
|
#
|
|
179
|
-
# @param opened_pipes [Hash<Object, ProcessExecuter::MonitoredPipe>] the pipes to close
|
|
180
|
-
#
|
|
181
193
|
# @return [StandardError, nil] the first error raised while closing, or nil if none was raised
|
|
182
194
|
#
|
|
183
|
-
def close_pipes
|
|
195
|
+
def close_pipes
|
|
184
196
|
first_close_error = nil
|
|
185
197
|
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + MonitoredPipe::DEFAULT_CLOSE_TIMEOUT
|
|
186
198
|
opened_pipes.each_value do |pipe|
|
|
@@ -50,8 +50,6 @@ module ProcessExecuter
|
|
|
50
50
|
@stderr_buffer = StringIO.new
|
|
51
51
|
stderr_buffer.set_encoding(options.effective_stderr_encoding)
|
|
52
52
|
|
|
53
|
-
add_capture_redirections
|
|
54
|
-
|
|
55
53
|
begin
|
|
56
54
|
super
|
|
57
55
|
ensure
|
|
@@ -89,10 +87,11 @@ module ProcessExecuter
|
|
|
89
87
|
)
|
|
90
88
|
end
|
|
91
89
|
|
|
92
|
-
#
|
|
90
|
+
# The stdout and stderr capture redirections
|
|
93
91
|
#
|
|
94
|
-
#
|
|
95
|
-
# options
|
|
92
|
+
# Called by {Run#call} to seed the redirection overrides, so the capture
|
|
93
|
+
# redirections are layered on top of the user's options at spawn time
|
|
94
|
+
# without writing into {#options} or mutating the parent's state.
|
|
96
95
|
#
|
|
97
96
|
# When the user gives a combined redirection whose key covers both stdout
|
|
98
97
|
# and stderr (e.g. `[:out, :err] => destination`), a single capture
|
|
@@ -100,80 +99,98 @@ module ProcessExecuter
|
|
|
100
99
|
# {#stdout_buffer} and {#stderr_buffer} is left empty, mirroring the
|
|
101
100
|
# `merge_output: true` contract.
|
|
102
101
|
#
|
|
103
|
-
# @return [
|
|
102
|
+
# @return [Hash<Object, Object>]
|
|
104
103
|
#
|
|
105
|
-
def
|
|
104
|
+
def internal_redirections
|
|
106
105
|
if options.combined_stdout_and_stderr_redirection?
|
|
107
|
-
|
|
106
|
+
combined_capture_redirection
|
|
108
107
|
else
|
|
109
|
-
|
|
108
|
+
stdout_and_stderr_capture_redirections
|
|
110
109
|
end
|
|
111
110
|
end
|
|
112
111
|
|
|
113
|
-
#
|
|
112
|
+
# A single capture redirection for a combined stdout/stderr key
|
|
114
113
|
#
|
|
115
114
|
# Both streams are interleaved into {#stdout_buffer}; {#stderr_buffer} is
|
|
116
115
|
# left empty.
|
|
117
116
|
#
|
|
118
|
-
# @return [
|
|
117
|
+
# @return [Hash<Object, Object>]
|
|
119
118
|
#
|
|
120
|
-
def
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
def combined_capture_redirection
|
|
120
|
+
tee_capture_redirection(
|
|
121
|
+
options.stdout_redirection_source, options.stdout_redirection_destination, stdout_buffer
|
|
123
122
|
)
|
|
124
123
|
end
|
|
125
124
|
|
|
126
|
-
#
|
|
125
|
+
# Separate capture redirections for stdout and stderr
|
|
127
126
|
#
|
|
128
127
|
# If `merge_output: true` was given, stderr is redirected into stdout so
|
|
129
128
|
# both streams are interleaved into {#stdout_buffer}.
|
|
130
129
|
#
|
|
131
|
-
# @return [
|
|
130
|
+
# @return [Hash<Object, Object>]
|
|
132
131
|
#
|
|
133
|
-
def
|
|
134
|
-
|
|
135
|
-
err = options.merge_output ? [:child, 1] : stderr_buffer
|
|
136
|
-
|
|
137
|
-
redirection_overrides.merge!(
|
|
138
|
-
capture_option(:out, stdout_redirection_source, stdout_redirection_destination, out),
|
|
139
|
-
capture_option(:err, stderr_redirection_source, stderr_redirection_destination, err)
|
|
140
|
-
)
|
|
132
|
+
def stdout_and_stderr_capture_redirections
|
|
133
|
+
stdout_capture_redirection.merge(stderr_capture_redirection)
|
|
141
134
|
end
|
|
142
135
|
|
|
143
|
-
# The
|
|
144
|
-
#
|
|
145
|
-
|
|
136
|
+
# The redirection that captures stdout into {#stdout_buffer}
|
|
137
|
+
#
|
|
138
|
+
# Tees the buffer onto the user's stdout redirection if one was given;
|
|
139
|
+
# otherwise installs the plain default capture redirection.
|
|
140
|
+
#
|
|
141
|
+
# @return [Hash<Object, Object>]
|
|
142
|
+
#
|
|
143
|
+
def stdout_capture_redirection
|
|
144
|
+
source = options.stdout_redirection_source
|
|
145
|
+
return default_capture_redirection(:out, stdout_buffer) unless source
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def stderr_redirection_source = options.stderr_redirection_source
|
|
147
|
+
tee_capture_redirection(source, options.stdout_redirection_destination, stdout_buffer)
|
|
148
|
+
end
|
|
150
149
|
|
|
151
|
-
# The
|
|
152
|
-
#
|
|
153
|
-
|
|
150
|
+
# The redirection that captures stderr
|
|
151
|
+
#
|
|
152
|
+
# Stderr is captured into {#stderr_buffer}, or into stdout (and thereby
|
|
153
|
+
# {#stdout_buffer}) if `merge_output: true` was given. Tees the capture
|
|
154
|
+
# destination onto the user's stderr redirection if one was given;
|
|
155
|
+
# otherwise installs the plain default capture redirection.
|
|
156
|
+
#
|
|
157
|
+
# @return [Hash<Object, Object>]
|
|
158
|
+
#
|
|
159
|
+
def stderr_capture_redirection
|
|
160
|
+
capture_destination = options.merge_output ? [:child, 1] : stderr_buffer
|
|
161
|
+
source = options.stderr_redirection_source
|
|
162
|
+
return default_capture_redirection(:err, capture_destination) unless source
|
|
154
163
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def stderr_redirection_destination = options.stderr_redirection_destination
|
|
164
|
+
tee_capture_redirection(source, options.stderr_redirection_destination, capture_destination)
|
|
165
|
+
end
|
|
158
166
|
|
|
159
|
-
#
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
#
|
|
167
|
+
# Tee the capture destination onto the redirection the user gave
|
|
168
|
+
#
|
|
169
|
+
# If the user's destination is already a tee, the capture destination is
|
|
170
|
+
# added to it; otherwise the user's destination and the capture
|
|
171
|
+
# destination are wrapped in a new tee.
|
|
172
|
+
#
|
|
173
|
+
# @param source [Symbol, Integer, Array] The redirection source the user gave
|
|
174
|
+
# @param destination [Object] The redirection destination the user gave
|
|
163
175
|
# @param capture_destination [Object] The additional destination to capture output to
|
|
164
|
-
# @return [Hash] The
|
|
165
|
-
def
|
|
166
|
-
if
|
|
167
|
-
|
|
168
|
-
{ given_source => given_destination + [capture_destination] }
|
|
169
|
-
else
|
|
170
|
-
{ given_source => [:tee, given_destination, capture_destination] }
|
|
171
|
-
end
|
|
176
|
+
# @return [Hash] The redirection to merge into options
|
|
177
|
+
def tee_capture_redirection(source, destination, capture_destination)
|
|
178
|
+
if Destinations::Tee.handles?(destination)
|
|
179
|
+
{ source => destination + [capture_destination] }
|
|
172
180
|
else
|
|
173
|
-
{
|
|
181
|
+
{ source => [:tee, destination, capture_destination] }
|
|
174
182
|
end
|
|
175
183
|
end
|
|
176
184
|
|
|
185
|
+
# The plain capture redirection used when the user gave no redirection
|
|
186
|
+
#
|
|
187
|
+
# @param source [Symbol] The redirection source (:out or :err)
|
|
188
|
+
# @param capture_destination [Object] The destination to capture output to
|
|
189
|
+
# @return [Hash] The redirection to merge into options
|
|
190
|
+
def default_capture_redirection(source, capture_destination)
|
|
191
|
+
{ source => capture_destination }
|
|
192
|
+
end
|
|
193
|
+
|
|
177
194
|
# Log the captured command output to the given logger at debug level
|
|
178
195
|
# @return [Void]
|
|
179
196
|
def log_command_output
|
|
@@ -48,7 +48,8 @@ module ProcessExecuter
|
|
|
48
48
|
#
|
|
49
49
|
def call
|
|
50
50
|
begin
|
|
51
|
-
@
|
|
51
|
+
@effective_spawn_options = spawn_options
|
|
52
|
+
@pid = Process.spawn(*command, **effective_spawn_options)
|
|
52
53
|
rescue StandardError => e
|
|
53
54
|
raise ProcessExecuter::SpawnError, "Failed to spawn process: #{e.message}"
|
|
54
55
|
end
|
|
@@ -132,6 +133,17 @@ module ProcessExecuter
|
|
|
132
133
|
#
|
|
133
134
|
def spawn_options = options.spawn_options.merge(process_group_options)
|
|
134
135
|
|
|
136
|
+
# The spawn options that were passed to Process.spawn
|
|
137
|
+
#
|
|
138
|
+
# Captured once by {#call} -- after any {#spawn_options} additions a
|
|
139
|
+
# subclass contributed -- so the kill path inspects the options actually
|
|
140
|
+
# used instead of recomputing the merge. nil until {#call} spawns the
|
|
141
|
+
# subprocess; the kill path only runs after that.
|
|
142
|
+
#
|
|
143
|
+
# @return [Hash, nil]
|
|
144
|
+
#
|
|
145
|
+
attr_reader :effective_spawn_options
|
|
146
|
+
|
|
135
147
|
# Spawn options that place the subprocess into its own process group
|
|
136
148
|
#
|
|
137
149
|
# When `timeout_after` is set to a value that can fire (`nil` and `0`
|
|
@@ -141,6 +153,13 @@ module ProcessExecuter
|
|
|
141
153
|
# direct child. Empty when no timeout can fire or when the caller gave a
|
|
142
154
|
# `pgroup`/`new_pgroup` option themselves (their setting is honored).
|
|
143
155
|
#
|
|
156
|
+
# This method never reflects a subclass's {#spawn_options} override, so
|
|
157
|
+
# {#isolated_in_new_process_group?} never counts an option a subclass
|
|
158
|
+
# contributes as isolation by this class -- though such an option can
|
|
159
|
+
# still make the subprocess a process group leader (see
|
|
160
|
+
# {#process_group_leader?}) -- and a subclass that removes the option
|
|
161
|
+
# added here prevents the isolation (and its cleanup) altogether.
|
|
162
|
+
#
|
|
144
163
|
# A new process group is a background group for any terminal the
|
|
145
164
|
# subprocess inherits, so an interactive subprocess that reads the
|
|
146
165
|
# terminal is stopped by `SIGTTIN` and then killed when the timeout
|
|
@@ -148,6 +167,11 @@ module ProcessExecuter
|
|
|
148
167
|
# needs an interactive subprocess to stay in the foreground process
|
|
149
168
|
# group can pass their own `pgroup` option.
|
|
150
169
|
#
|
|
170
|
+
# Deterministic: the result depends only on {#options} -- not mutated
|
|
171
|
+
# during {#call} -- and the platform, so the kill path's
|
|
172
|
+
# {#isolated_in_new_process_group?} re-read agrees with the value that
|
|
173
|
+
# was merged into the spawn options.
|
|
174
|
+
#
|
|
151
175
|
# @return [Hash]
|
|
152
176
|
#
|
|
153
177
|
def process_group_options
|
|
@@ -302,19 +326,25 @@ module ProcessExecuter
|
|
|
302
326
|
# @return [Boolean]
|
|
303
327
|
#
|
|
304
328
|
def process_group_leader?
|
|
305
|
-
[true, 0].include?(
|
|
329
|
+
[true, 0].include?(effective_spawn_options[:pgroup]) || effective_spawn_options[:new_pgroup] == true
|
|
306
330
|
end
|
|
307
331
|
|
|
308
332
|
# Whether this class isolated the subprocess into its own process group
|
|
309
333
|
#
|
|
310
|
-
# True when
|
|
311
|
-
#
|
|
312
|
-
#
|
|
334
|
+
# True when {#process_group_options} -- the single source of truth for
|
|
335
|
+
# the isolation decision -- added a process group option and the
|
|
336
|
+
# subprocess actually became a new process group leader
|
|
337
|
+
# ({#process_group_leader?} over the captured options). The leader
|
|
338
|
+
# check matters only when a subclass's {#spawn_options} override
|
|
339
|
+
# removed or overrode the added option: then no isolation happened and
|
|
340
|
+
# the abandoned-wait cleanup must leave the subprocess alone. False
|
|
341
|
+
# when the subprocess's process group (if any) came from a
|
|
342
|
+
# `pgroup`/`new_pgroup` option the caller supplied.
|
|
313
343
|
#
|
|
314
344
|
# @return [Boolean]
|
|
315
345
|
#
|
|
316
346
|
def isolated_in_new_process_group?
|
|
317
|
-
|
|
347
|
+
!process_group_options.empty? && process_group_leader?
|
|
318
348
|
end
|
|
319
349
|
|
|
320
350
|
# Send SIGKILL to the subprocess's process group
|
|
@@ -44,9 +44,10 @@ module ProcessExecuter
|
|
|
44
44
|
# a thread is created to read data written to the pipe. As data is read from the pipe,
|
|
45
45
|
# it is written to the destination provided in the MonitoredPipe initializer.
|
|
46
46
|
#
|
|
47
|
-
# If
|
|
48
|
-
#
|
|
49
|
-
#
|
|
47
|
+
# If an exception (of any class, not just `StandardError`) is raised while
|
|
48
|
+
# collecting output -- by the destination's `#write`, by the monitor loop,
|
|
49
|
+
# or by pipe cleanup -- the monitoring thread exits, the pipe is closed, and
|
|
50
|
+
# the exception is saved in {#exception}.
|
|
50
51
|
#
|
|
51
52
|
# > **⚠️ WARNING**
|
|
52
53
|
# >
|
|
@@ -347,14 +348,22 @@ module ProcessExecuter
|
|
|
347
348
|
|
|
348
349
|
# @!attribute [r]
|
|
349
350
|
#
|
|
350
|
-
# The exception
|
|
351
|
+
# The first exception recorded while collecting output
|
|
351
352
|
#
|
|
352
|
-
#
|
|
353
|
+
# Any failure while collecting output is recorded here, not only a
|
|
354
|
+
# destination error: the destination's `#write` raising, the monitor loop
|
|
355
|
+
# raising, or pipe cleanup raising. `nil` if no exception was raised.
|
|
356
|
+
#
|
|
357
|
+
# When more than one exception is raised, the first one *recorded* wins
|
|
358
|
+
# and the rest are discarded. Recording order has one corner: when the
|
|
359
|
+
# monitor loop raises and pipe cleanup then also raises, the cleanup
|
|
360
|
+
# error is the one recorded, because the monitor-loop exception is still
|
|
361
|
+
# in flight while cleanup runs and reaches its recording site last.
|
|
353
362
|
#
|
|
354
363
|
# @example
|
|
355
364
|
# pipe.exception #=> nil
|
|
356
365
|
#
|
|
357
|
-
# @return [Exception, nil] the
|
|
366
|
+
# @return [Exception, nil] the first recorded exception or `nil` if no exception was raised
|
|
358
367
|
#
|
|
359
368
|
attr_reader :exception
|
|
360
369
|
|
|
@@ -62,10 +62,8 @@ module ProcessExecuter
|
|
|
62
62
|
#
|
|
63
63
|
def initialize(**options_hash)
|
|
64
64
|
@options_hash = allowed_options.transform_values(&:default).merge(options_hash)
|
|
65
|
-
@errors = []
|
|
66
|
-
assert_no_unknown_options
|
|
67
65
|
define_accessor_methods
|
|
68
|
-
|
|
66
|
+
validate!
|
|
69
67
|
end
|
|
70
68
|
|
|
71
69
|
# All the allowed options as a hash whose keys are the option names
|
|
@@ -183,13 +181,10 @@ module ProcessExecuter
|
|
|
183
181
|
def merge!(*other_options_hashes)
|
|
184
182
|
original_options_hash = @options_hash
|
|
185
183
|
@options_hash = original_options_hash.dup.merge!(*other_options_hashes)
|
|
186
|
-
|
|
187
|
-
assert_no_unknown_options
|
|
188
|
-
validate_options
|
|
184
|
+
validate!
|
|
189
185
|
self
|
|
190
186
|
rescue ProcessExecuter::ArgumentError
|
|
191
187
|
@options_hash = original_options_hash
|
|
192
|
-
@errors = []
|
|
193
188
|
raise
|
|
194
189
|
end
|
|
195
190
|
|
|
@@ -262,6 +257,23 @@ module ProcessExecuter
|
|
|
262
257
|
#
|
|
263
258
|
attr_reader :options_hash
|
|
264
259
|
|
|
260
|
+
# Run the full validation sequence over the current options hash
|
|
261
|
+
#
|
|
262
|
+
# Checks for unknown options and then validates option values, leaving
|
|
263
|
+
# `errors` empty on exit whether validation passes or raises.
|
|
264
|
+
#
|
|
265
|
+
# @return [void]
|
|
266
|
+
# @raise [ProcessExecuter::ArgumentError] if an unknown option or an
|
|
267
|
+
# invalid option value is found
|
|
268
|
+
# @api private
|
|
269
|
+
def validate!
|
|
270
|
+
@errors = []
|
|
271
|
+
assert_no_unknown_options
|
|
272
|
+
validate_options
|
|
273
|
+
ensure
|
|
274
|
+
@errors = []
|
|
275
|
+
end
|
|
276
|
+
|
|
265
277
|
# Raise an argument error for invalid option values
|
|
266
278
|
# @return [void]
|
|
267
279
|
# @raise [ProcessExecuter::ArgumentError] if any invalid option values are found
|
|
@@ -296,7 +308,7 @@ module ProcessExecuter
|
|
|
296
308
|
return if unknown_options.empty?
|
|
297
309
|
|
|
298
310
|
raise(
|
|
299
|
-
ArgumentError,
|
|
311
|
+
ProcessExecuter::ArgumentError,
|
|
300
312
|
"Unknown option#{'s' if unknown_options.count > 1}: #{unknown_options.join(', ')}"
|
|
301
313
|
)
|
|
302
314
|
end
|
|
@@ -31,9 +31,7 @@ module ProcessExecuter
|
|
|
31
31
|
#
|
|
32
32
|
# @api private
|
|
33
33
|
#
|
|
34
|
-
def effective_stdout_encoding
|
|
35
|
-
canonical_encoding(stdout_encoding || encoding || DEFAULT_ENCODING)
|
|
36
|
-
end
|
|
34
|
+
def effective_stdout_encoding = effective_encoding(stdout_encoding)
|
|
37
35
|
|
|
38
36
|
# Determines the character encoding to use for stderr
|
|
39
37
|
#
|
|
@@ -49,12 +47,27 @@ module ProcessExecuter
|
|
|
49
47
|
#
|
|
50
48
|
# @api private
|
|
51
49
|
#
|
|
52
|
-
def effective_stderr_encoding
|
|
53
|
-
canonical_encoding(stderr_encoding || encoding || DEFAULT_ENCODING)
|
|
54
|
-
end
|
|
50
|
+
def effective_stderr_encoding = effective_encoding(stderr_encoding)
|
|
55
51
|
|
|
56
52
|
private
|
|
57
53
|
|
|
54
|
+
# The encoding to use for a stream given its stream-specific option value
|
|
55
|
+
#
|
|
56
|
+
# Prioritizes the stream-specific value if set, otherwise falls back to
|
|
57
|
+
# `encoding`, and finally defaults to `DEFAULT_ENCODING`. The result is
|
|
58
|
+
# canonicalized with {#canonical_encoding}.
|
|
59
|
+
#
|
|
60
|
+
# @param stream_encoding [Encoding, String, Symbol, nil] the value of
|
|
61
|
+
# `stdout_encoding` or `stderr_encoding`
|
|
62
|
+
#
|
|
63
|
+
# @return [Encoding, nil] nil if the value names an unknown encoding
|
|
64
|
+
#
|
|
65
|
+
# @api private
|
|
66
|
+
#
|
|
67
|
+
def effective_encoding(stream_encoding)
|
|
68
|
+
canonical_encoding(stream_encoding || encoding || DEFAULT_ENCODING)
|
|
69
|
+
end
|
|
70
|
+
|
|
58
71
|
# The options allowed for objects of this class
|
|
59
72
|
# @return [Array<OptionDefinition>]
|
|
60
73
|
# @api private
|
|
@@ -148,70 +161,26 @@ module ProcessExecuter
|
|
|
148
161
|
end
|
|
149
162
|
|
|
150
163
|
# Note an error if the encoding option is not valid
|
|
151
|
-
# @param key [Symbol] the option key
|
|
152
|
-
# @param value [Object] the option value
|
|
153
|
-
# @return [Void]
|
|
154
|
-
# @api private
|
|
155
|
-
def validate_encoding_option(key, value)
|
|
156
|
-
return unless valid_encoding_type?(key, value)
|
|
157
|
-
|
|
158
|
-
return if value.nil? || value.is_a?(Encoding)
|
|
159
|
-
|
|
160
|
-
validate_encoding_symbol(key, value) if value.is_a?(Symbol)
|
|
161
|
-
|
|
162
|
-
validate_encoding_string(key, value) if value.is_a?(String)
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
# False if the value is not a valid encoding type, true otherwise
|
|
166
|
-
#
|
|
167
|
-
# @param key [Symbol] the option key
|
|
168
164
|
#
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
# @api private
|
|
174
|
-
#
|
|
175
|
-
def valid_encoding_type?(key, value)
|
|
176
|
-
return true if value.nil? || value.is_a?(Encoding) || value.is_a?(Symbol) || value.is_a?(String)
|
|
177
|
-
|
|
178
|
-
errors << "#{key} must be an Encoding object, String, Symbol (:binary, :default_external), " \
|
|
179
|
-
"or nil, but was #{value.inspect}"
|
|
180
|
-
|
|
181
|
-
false
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Note an error if the encoding symbol is not valid
|
|
165
|
+
# `nil`, an Encoding object, `:binary`, and `:default_external` are
|
|
166
|
+
# valid as given. A String is valid if {#canonical_encoding} recognizes
|
|
167
|
+
# it as an encoding name. Any other value is invalid.
|
|
185
168
|
#
|
|
186
169
|
# @param key [Symbol] the option key
|
|
187
|
-
#
|
|
188
|
-
# @param value [Symbol] the option value
|
|
189
|
-
#
|
|
170
|
+
# @param value [Object] the option value
|
|
190
171
|
# @return [Void]
|
|
191
|
-
#
|
|
192
172
|
# @api private
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
#
|
|
205
|
-
# @param value [String] the option value
|
|
206
|
-
#
|
|
207
|
-
# @return [void]
|
|
208
|
-
#
|
|
209
|
-
# @api private
|
|
210
|
-
#
|
|
211
|
-
def validate_encoding_string(key, value)
|
|
212
|
-
Encoding.find(value)
|
|
213
|
-
rescue ::ArgumentError
|
|
214
|
-
errors << "#{key} specifies an unknown encoding name: #{value.inspect}"
|
|
173
|
+
def validate_encoding_option(key, value)
|
|
174
|
+
case value
|
|
175
|
+
when nil, Encoding, :binary, :default_external then nil
|
|
176
|
+
when Symbol
|
|
177
|
+
errors << "#{key} when given as a symbol must be :binary or :default_external, but was #{value.inspect}"
|
|
178
|
+
when String
|
|
179
|
+
errors << "#{key} specifies an unknown encoding name: #{value.inspect}" if canonical_encoding(value).nil?
|
|
180
|
+
else
|
|
181
|
+
errors << "#{key} must be an Encoding object, String, Symbol (:binary, :default_external), " \
|
|
182
|
+
"or nil, but was #{value.inspect}"
|
|
183
|
+
end
|
|
215
184
|
end
|
|
216
185
|
end
|
|
217
186
|
end
|
data/release-please-config.json
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: process_executer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.1.
|
|
4
|
+
version: 4.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- James Couball
|
|
@@ -295,8 +295,8 @@ metadata:
|
|
|
295
295
|
allowed_push_host: https://rubygems.org
|
|
296
296
|
homepage_uri: https://github.com/main-branch/process_executer
|
|
297
297
|
source_code_uri: https://github.com/main-branch/process_executer
|
|
298
|
-
documentation_uri: https://rubydoc.info/gems/process_executer/4.1.
|
|
299
|
-
changelog_uri: https://rubydoc.info/gems/process_executer/4.1.
|
|
298
|
+
documentation_uri: https://rubydoc.info/gems/process_executer/4.1.1
|
|
299
|
+
changelog_uri: https://rubydoc.info/gems/process_executer/4.1.1/file/CHANGELOG.md
|
|
300
300
|
rubygems_mfa_required: 'true'
|
|
301
301
|
rdoc_options: []
|
|
302
302
|
require_paths:
|