process_executer 3.2.4 → 4.0.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/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +41 -0
- data/README.md +177 -134
- data/lib/process_executer/commands/run.rb +124 -0
- data/lib/process_executer/commands/run_with_capture.rb +148 -0
- data/lib/process_executer/commands/spawn_with_timeout.rb +163 -0
- data/lib/process_executer/commands.rb +11 -0
- data/lib/process_executer/destinations/child_redirection.rb +5 -4
- data/lib/process_executer/destinations/close.rb +5 -4
- data/lib/process_executer/destinations/destination_base.rb +73 -0
- data/lib/process_executer/destinations/file_descriptor.rb +10 -6
- data/lib/process_executer/destinations/file_path.rb +12 -6
- data/lib/process_executer/destinations/file_path_mode.rb +10 -6
- data/lib/process_executer/destinations/file_path_mode_perms.rb +12 -5
- data/lib/process_executer/destinations/io.rb +10 -5
- data/lib/process_executer/destinations/monitored_pipe.rb +10 -5
- data/lib/process_executer/destinations/stderr.rb +8 -4
- data/lib/process_executer/destinations/stdout.rb +8 -4
- data/lib/process_executer/destinations/tee.rb +24 -17
- data/lib/process_executer/destinations/writer.rb +12 -7
- data/lib/process_executer/destinations.rb +32 -17
- data/lib/process_executer/errors.rb +50 -26
- data/lib/process_executer/monitored_pipe.rb +128 -59
- data/lib/process_executer/options/base.rb +118 -82
- data/lib/process_executer/options/option_definition.rb +5 -1
- data/lib/process_executer/options/run_options.rb +13 -12
- data/lib/process_executer/options/run_with_capture_options.rb +156 -0
- data/lib/process_executer/options/spawn_options.rb +31 -30
- data/lib/process_executer/options/{spawn_and_wait_options.rb → spawn_with_timeout_options.rb} +11 -7
- data/lib/process_executer/options.rb +3 -1
- data/lib/process_executer/result.rb +35 -77
- data/lib/process_executer/result_with_capture.rb +62 -0
- data/lib/process_executer/version.rb +2 -1
- data/lib/process_executer.rb +384 -346
- data/process_executer.gemspec +11 -2
- metadata +18 -8
- data/lib/process_executer/destination_base.rb +0 -83
- data/lib/process_executer/runner.rb +0 -144
data/lib/process_executer.rb
CHANGED
@@ -1,441 +1,479 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
require 'process_executer/errors'
|
9
|
-
require 'process_executer/monitored_pipe'
|
10
|
-
require 'process_executer/options'
|
11
|
-
require 'process_executer/result'
|
12
|
-
require 'process_executer/runner'
|
13
|
-
|
14
|
-
# The `ProcessExecuter` module provides methods to execute subprocess commands
|
15
|
-
# with enhanced features such as output capture, timeout handling, and custom
|
16
|
-
# environment variables.
|
3
|
+
# The {ProcessExecuter} module provides extended versions of
|
4
|
+
# [Process.spawn](https://docs.ruby-lang.org/en/3.4/Process.html#method-c-spawn) that
|
5
|
+
# block while the command is executing. These methods provide enhanced features such
|
6
|
+
# as timeout handling, more flexible redirection options, logging, error raising, and
|
7
|
+
# output capturing.
|
17
8
|
#
|
18
|
-
#
|
9
|
+
# The interface of these methods is the same as the standard library
|
10
|
+
# [Process.spawn](https://docs.ruby-lang.org/en/3.4/Process.html#method-c-spawn)
|
11
|
+
# method, but with additional options and features.
|
19
12
|
#
|
20
|
-
#
|
21
|
-
# status and output
|
22
|
-
# * {spawn_and_wait}: a thin wrapper around `Process.spawn` that blocks until the
|
23
|
-
# command finishes
|
13
|
+
# These methods are:
|
24
14
|
#
|
25
|
-
#
|
15
|
+
# * {spawn_with_timeout}: Extends
|
16
|
+
# [Process.spawn](https://docs.ruby-lang.org/en/3.4/Process.html#method-c-spawn) to
|
17
|
+
# run a command and wait (with timeout) for it to finish
|
18
|
+
# * {run}: Extends {ProcessExecuter.spawn_with_timeout}, adding more flexible
|
19
|
+
# redirection and other options
|
20
|
+
# * {run_with_capture}: Extends {ProcessExecuter.run}, automatically captures stdout and stderr
|
26
21
|
#
|
27
|
-
#
|
28
|
-
# * Captures stdout and stderr to buffers, files, or custom objects.
|
29
|
-
# * Optionally enforces timeouts and terminates long-running commands.
|
30
|
-
# * Provides detailed status information, including the command that was run, the
|
31
|
-
# options that were given, and success, failure, or timeout states.
|
22
|
+
# See the {ProcessExecuter::Error} class for the error architecture for this module.
|
32
23
|
#
|
33
24
|
# @api public
|
34
25
|
module ProcessExecuter
|
35
|
-
#
|
26
|
+
# Extends `Process.spawn` to run command and wait (with timeout) for it to finish
|
27
|
+
#
|
28
|
+
# Accepts all [Process.spawn execution
|
29
|
+
# options](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Execution+Options)
|
30
|
+
# and the additional option `timeout_after`:
|
31
|
+
#
|
32
|
+
# * `timeout_after: <Numeric, nil>`: the amount of time (in seconds) to wait before
|
33
|
+
# signaling the process with SIGKILL. 0 or nil means no timeout.
|
36
34
|
#
|
37
|
-
#
|
38
|
-
# [Process
|
39
|
-
#
|
35
|
+
# Returns a {Result} object. The {Result} class is a decorator for
|
36
|
+
# [Process::Status](https://docs.ruby-lang.org/en/3.4/Process/Status.html) that
|
37
|
+
# provides additional attributes about the command's status. This includes the
|
38
|
+
# {Result#command command} that was run, the {Result#options options} used to run
|
39
|
+
# it, {Result#elapsed_time elapsed_time} of the command, and whether the command
|
40
|
+
# {Result#timed_out? timed_out?}.
|
40
41
|
#
|
41
|
-
#
|
42
|
-
# sent the SIGKILL signal if it does not terminate within the specified timeout.
|
42
|
+
# @overload spawn_with_timeout(*command, **options_hash)
|
43
43
|
#
|
44
|
-
#
|
45
|
-
#
|
44
|
+
# @param command [Array<String>] see [Process module, Argument `command_line` or
|
45
|
+
# `exe_path`](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Argument+command_line)
|
46
|
+
# and [Process module, Argument
|
47
|
+
# `args`](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Arguments+args)
|
48
|
+
#
|
49
|
+
# If the first value is a Hash, it is treated as the environment hash. See
|
50
|
+
# [Process module, Execution Environment](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Execution+Environment).
|
51
|
+
#
|
52
|
+
# @param options_hash [Hash] In addition to the options documented in [Process
|
53
|
+
# module, Execution
|
54
|
+
# Options](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Execution+Options),
|
55
|
+
# the following options are supported: `:timeout_after`
|
56
|
+
#
|
57
|
+
# @option options_hash [Numeric] :timeout_after the amount of time (in seconds)
|
58
|
+
# to wait before signaling the process with SIGKILL
|
59
|
+
#
|
60
|
+
# @overload spawn_with_timeout(*command, options)
|
61
|
+
#
|
62
|
+
# @param command [Array<String>] see [Process module, Argument `command_line` or
|
63
|
+
# `exe_path`](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Argument+command_line)
|
64
|
+
# and [Process module, Argument
|
65
|
+
# `args`](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Arguments+args)
|
66
|
+
#
|
67
|
+
# If the first value is a Hash, it is treated as the environment hash. See
|
68
|
+
# [Process module, Execution Environment](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Execution+Environment).
|
69
|
+
#
|
70
|
+
# @param options [ProcessExecuter::Options::SpawnWithTimeoutOptions]
|
71
|
+
#
|
72
|
+
# @example command line given as a single string
|
73
|
+
# result = ProcessExecuter.spawn_with_timeout('echo "3\n2\n1" | sort')
|
46
74
|
# result.exited? # => true
|
47
75
|
# result.success? # => true
|
76
|
+
# result.exitstatus # => 0
|
48
77
|
# result.timed_out? # => false
|
49
78
|
#
|
79
|
+
# @example command given as an exe_path and args
|
80
|
+
# result = ProcessExecuter.spawn_with_timeout('ping', '-c', '1', 'localhost')
|
81
|
+
#
|
50
82
|
# @example with a timeout
|
51
|
-
# result = ProcessExecuter.
|
83
|
+
# result = ProcessExecuter.spawn_with_timeout('sleep 10', timeout_after: 0.01)
|
52
84
|
# result.exited? # => false
|
53
85
|
# result.success? # => nil
|
54
86
|
# result.signaled? # => true
|
55
87
|
# result.termsig # => 9
|
56
88
|
# result.timed_out? # => true
|
57
89
|
#
|
58
|
-
# @example
|
90
|
+
# @example with a env hash
|
91
|
+
# env = { 'EXITSTATUS' => '1' }
|
92
|
+
# result = ProcessExecuter.spawn_with_timeout(env, 'exit $EXITSTATUS')
|
93
|
+
# result.success? # => false
|
94
|
+
# result.exitstatus # => 1
|
95
|
+
#
|
96
|
+
# @example capture stdout to a StringIO buffer
|
59
97
|
# stdout_buffer = StringIO.new
|
60
98
|
# stdout_pipe = ProcessExecuter::MonitoredPipe.new(stdout_buffer)
|
61
|
-
#
|
62
|
-
#
|
99
|
+
# begin
|
100
|
+
# result = ProcessExecuter.spawn_with_timeout('echo "3\n2\n1" | sort', out: stdout_pipe)
|
101
|
+
# stdout_buffer.string # => "1\n2\n3\n"
|
102
|
+
# ensure
|
103
|
+
# stdout_pipe.close
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
# @raise [ProcessExecuter::ArgumentError] If the command or an option is not valid
|
63
107
|
#
|
64
|
-
#
|
65
|
-
#
|
108
|
+
# Raised if an invalid option key or value is given, or both an options object
|
109
|
+
# and options_hash are given.
|
66
110
|
#
|
67
|
-
# @
|
68
|
-
#
|
111
|
+
# @raise [ProcessExecuter::SpawnError] `Process.spawn` raised an error before the
|
112
|
+
# command was run
|
69
113
|
#
|
70
|
-
#
|
71
|
-
#
|
114
|
+
# Raised if the
|
115
|
+
# [Process.spawn](https://docs.ruby-lang.org/en/3.4/Process.html#method-c-spawn)
|
116
|
+
# method raises an error before the command is run.
|
72
117
|
#
|
73
|
-
# @return [ProcessExecuter::Result]
|
118
|
+
# @return [ProcessExecuter::Result]
|
74
119
|
#
|
75
|
-
|
76
|
-
|
77
|
-
|
120
|
+
# @api public
|
121
|
+
#
|
122
|
+
def self.spawn_with_timeout(*command, **options_hash)
|
123
|
+
command, options = command_and_options(Options::SpawnWithTimeoutOptions, command, options_hash)
|
124
|
+
ProcessExecuter::Commands::SpawnWithTimeout.new(command, options).call
|
78
125
|
end
|
79
126
|
|
80
|
-
#
|
127
|
+
# Extends {spawn_with_timeout}, adding more flexible redirection and other options
|
81
128
|
#
|
82
|
-
#
|
129
|
+
# Accepts all [Process.spawn execution
|
130
|
+
# options](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Execution+Options),
|
131
|
+
# the additional options defined by {spawn_with_timeout}, and the additional
|
132
|
+
# options `raise_errors` and `logger`:
|
83
133
|
#
|
84
|
-
#
|
85
|
-
#
|
134
|
+
# * `raise_errors: <Boolean>` makes execution errors an exception if true (default
|
135
|
+
# is `true`)
|
136
|
+
# * `logger: <Logger>` logs the command and its result at `:info` level using the
|
137
|
+
# given logger (default is not to log)
|
86
138
|
#
|
87
|
-
#
|
88
|
-
#
|
89
|
-
|
90
|
-
|
91
|
-
pid = Process.spawn(*command, **options.spawn_options)
|
92
|
-
rescue StandardError => e
|
93
|
-
raise ProcessExecuter::SpawnError, "Failed to spawn process: #{e.message}"
|
94
|
-
end
|
95
|
-
wait_for_process(pid, command, options)
|
96
|
-
end
|
97
|
-
|
98
|
-
# Execute the given command as a subprocess blocking until it finishes
|
139
|
+
# Internally, this method wraps stdout and stderr redirection options in a
|
140
|
+
# {MonitoredPipe}, enabling more flexible output handling. It allows any object
|
141
|
+
# that responds to `#write` to be used as a destination and supports multiple
|
142
|
+
# destinations using the form `[:tee, destination, ...]`.
|
99
143
|
#
|
100
|
-
#
|
144
|
+
# When the command exits with a non-zero exit status or does not exit normally, one
|
145
|
+
# of the following errors will be raised unless the option `raise_errors: false` is
|
146
|
+
# explicitly given:
|
101
147
|
#
|
102
|
-
#
|
103
|
-
#
|
104
|
-
#
|
148
|
+
# * {ProcessExecuter::FailedError} if the command returns a non-zero exitstatus
|
149
|
+
# * {ProcessExecuter::SignaledError} if the command exits because of an unhandled
|
150
|
+
# signal
|
151
|
+
# * {ProcessExecuter::TimeoutError} if the command times out
|
105
152
|
#
|
106
|
-
#
|
107
|
-
#
|
108
|
-
# `#write` (or an Array of such objects) can be given for `out` and `err`.
|
153
|
+
# These errors all have a {CommandError#result result} attribute that contains the
|
154
|
+
# {ProcessExecuter::Result} object for this command.
|
109
155
|
#
|
110
|
-
#
|
111
|
-
#
|
156
|
+
# If `raise_errors: false` is given and there was an error, the returned
|
157
|
+
# {ProcessExecuter::Result} object indicates what the error is via its
|
158
|
+
# [success?](https://docs.ruby-lang.org/en/3.4/Process/Status.html#method-i-success-3F),
|
159
|
+
# [signaled?](https://docs.ruby-lang.org/en/3.4/Process/Status.html#method-i-signaled-3F),
|
160
|
+
# or {Result#timed_out? timed_out?} attributes.
|
112
161
|
#
|
113
|
-
#
|
114
|
-
#
|
115
|
-
# * `ProcessExecuter::SignaledError` if the command exits because of
|
116
|
-
# an unhandled signal
|
117
|
-
# * `ProcessExecuter::TimeoutError` if the command times out
|
162
|
+
# A {ProcessExecuter::ProcessIOError} is raised if an exception occurs while
|
163
|
+
# collecting subprocess output.
|
118
164
|
#
|
119
|
-
#
|
165
|
+
# Giving the option `raise_errors: false` will not suppress
|
166
|
+
# {ProcessExecuter::ProcessIOError}, {ProcessExecuter::SpawnError}, or
|
167
|
+
# {ProcessExecuter::ArgumentError} errors.
|
120
168
|
#
|
121
|
-
#
|
122
|
-
#
|
169
|
+
# @example capture stdout to a StringIO buffer
|
170
|
+
# out_buffer = StringIO.new
|
171
|
+
# result = ProcessExecuter.run('echo HELLO', out: out_buffer)
|
172
|
+
# out_buffer.string #=> "HELLO\n"
|
173
|
+
#
|
174
|
+
# @example with :raise_errors set to true
|
175
|
+
# begin
|
176
|
+
# result = ProcessExecuter.run('exit 1', raise_errors: true)
|
177
|
+
# rescue ProcessExecuter::FailedError => e
|
178
|
+
# e.result.exitstatus #=> 1
|
179
|
+
# end
|
180
|
+
#
|
181
|
+
# @example with :raise_errors set to false
|
182
|
+
# result = ProcessExecuter.run('exit 1', raise_errors: false)
|
183
|
+
# result.exitstatus #=> 1
|
123
184
|
#
|
124
|
-
#
|
185
|
+
# @example with a logger
|
186
|
+
# logger_buffer = StringIO.new
|
187
|
+
# logger = Logger.new(logger_buffer, level: :info)
|
188
|
+
# result = ProcessExecuter.run('echo HELLO', logger: logger)
|
189
|
+
# logger_buffer.string #=> "INFO -- : PID 5555: [\"echo HELLO\"] exited with status pid 5555 exit 0\n"
|
125
190
|
#
|
126
|
-
#
|
127
|
-
# * The stdout and stderr output to `debug` level
|
191
|
+
# @overload run(*command, **options_hash)
|
128
192
|
#
|
129
|
-
#
|
193
|
+
# @param command [Array<String>] see [Process module, Argument `command_line` or
|
194
|
+
# `exe_path`](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Argument+command_line)
|
195
|
+
# and [Process module, Argument
|
196
|
+
# `args`](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Arguments+args)
|
130
197
|
#
|
131
|
-
#
|
198
|
+
# If the first value is a Hash, it is treated as the environment hash. See
|
199
|
+
# [Process module, Execution Environment](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Execution+Environment).
|
132
200
|
#
|
133
|
-
#
|
134
|
-
#
|
201
|
+
# @param [Hash] options_hash in addition to the options supported by
|
202
|
+
# {spawn_with_timeout}, the following options may be given: `:raise_errors` and
|
203
|
+
# `:logger`
|
135
204
|
#
|
136
|
-
#
|
205
|
+
# @option options_hash [Boolean] :raise_errors if true, an error will be raised
|
206
|
+
# if the command fails
|
137
207
|
#
|
138
|
-
#
|
139
|
-
#
|
208
|
+
# @option options_hash [Logger] :logger a logger to use for logging the command
|
209
|
+
# and its result at the info level
|
140
210
|
#
|
141
|
-
#
|
211
|
+
# @option options_hash [Numeric] :timeout_after the amount of time (in seconds)
|
212
|
+
# to wait before signaling the process with SIGKILL
|
142
213
|
#
|
143
|
-
#
|
144
|
-
# [Execution
|
145
|
-
# Environment](https://docs.ruby-lang.org/en/3.3/Process.html#module-Process-label-Execution+Environment).
|
214
|
+
# @overload run(*command, options)
|
146
215
|
#
|
147
|
-
#
|
216
|
+
# @param command [Array<String>] see [Process module, Argument `command_line` or
|
217
|
+
# `exe_path`](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Argument+command_line)
|
218
|
+
# and [Process module, Argument
|
219
|
+
# `args`](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Arguments+args)
|
148
220
|
#
|
149
|
-
#
|
150
|
-
#
|
151
|
-
# command = 'echo "stdout: `pwd`" && echo "stderr: $HOME" 1>&2'
|
152
|
-
# result = ProcessExecuter.run(command)
|
153
|
-
# result.success? #=> true
|
154
|
-
# result.stdout #=> "stdout: /Users/james/projects/main-branch/process_executer\n"
|
155
|
-
# result.stderr #=> "stderr: /Users/james\n"
|
221
|
+
# If the first value is a Hash, it is treated as the environment hash. See
|
222
|
+
# [Process module, Execution Environment](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Execution+Environment).
|
156
223
|
#
|
157
|
-
#
|
158
|
-
# # The command and its args must be provided as separate strings in the array.
|
159
|
-
# # Shell expansions and redirections are not supported.
|
160
|
-
# command = ['git', 'clone', 'https://github.com/main-branch/process_executer']
|
161
|
-
# result = ProcessExecuter.run(*command)
|
162
|
-
# result.success? #=> true
|
163
|
-
# result.stdout #=> ""
|
164
|
-
# result.stderr #=> "Cloning into 'process_executer'...\n"
|
224
|
+
# @param options [ProcessExecuter::Options::RunOptions]
|
165
225
|
#
|
166
|
-
# @
|
167
|
-
# command = ['sleep', '1']
|
168
|
-
# result = ProcessExecuter.run(*command, timeout_after: 0.01)
|
169
|
-
# #=> raises ProcessExecuter::TimeoutError which contains the command result
|
226
|
+
# @raise [ProcessExecuter::ArgumentError] If the command or an option is not valid
|
170
227
|
#
|
171
|
-
#
|
172
|
-
#
|
173
|
-
# result = ProcessExecuter.run(*command)
|
174
|
-
# #=> raises ProcessExecuter::FailedError which contains the command result
|
228
|
+
# Raised if an invalid option key or value is given, or both an options object
|
229
|
+
# and options_hash are given.
|
175
230
|
#
|
176
|
-
# @
|
177
|
-
# command
|
178
|
-
# result = ProcessExecuter.run(*command)
|
179
|
-
# #=> raises ProcessExecuter::SignaledError which contains the command result
|
231
|
+
# @raise [ProcessExecuter::SpawnError] `Process.spawn` raised an error before the
|
232
|
+
# command was run
|
180
233
|
#
|
181
|
-
#
|
182
|
-
#
|
183
|
-
#
|
184
|
-
#
|
185
|
-
#
|
186
|
-
#
|
187
|
-
#
|
188
|
-
#
|
189
|
-
#
|
190
|
-
#
|
191
|
-
#
|
192
|
-
#
|
193
|
-
#
|
194
|
-
#
|
195
|
-
# @
|
196
|
-
#
|
197
|
-
#
|
198
|
-
# result = ProcessExecuter.run(env, *command)
|
199
|
-
# result.stdout #=> "foobar\n"
|
200
|
-
#
|
201
|
-
# @example Unset environment variables
|
202
|
-
# env = { 'FOO' => nil } # setting to nil unsets the variable in the environment
|
203
|
-
# command = ['echo "FOO: $FOO"']
|
204
|
-
# result = ProcessExecuter.run(env, *command)
|
205
|
-
# result.stdout #=> "FOO: \n"
|
206
|
-
#
|
207
|
-
# @example Reset existing environment variables and add new ones
|
208
|
-
# env = { 'PATH' => '/bin' }
|
209
|
-
# result = ProcessExecuter.run(env, 'echo "Home: $HOME" && echo "Path: $PATH"', unsetenv_others: true)
|
210
|
-
# result.stdout #=> "Home: \n/Path: /bin\n"
|
211
|
-
#
|
212
|
-
# @example Run command in a different directory
|
213
|
-
# command = ['pwd']
|
214
|
-
# result = ProcessExecuter.run(*command, chdir: '/tmp')
|
215
|
-
# result.stdout #=> "/tmp\n"
|
216
|
-
#
|
217
|
-
# @example Capture stdout and stderr into a single buffer
|
218
|
-
# command = ['echo "stdout" && echo "stderr" 1>&2']
|
219
|
-
# result = ProcessExecuter.run(*command, [out:, err:]: StringIO.new)
|
220
|
-
# result.stdout #=> "stdout\nstderr\n"
|
221
|
-
# result.stderr #=> "stdout\nstderr\n"
|
222
|
-
# result.stdout.object_id == result.stderr.object_id #=> true
|
223
|
-
#
|
224
|
-
# @example Capture to an explicit buffer
|
225
|
-
# out = StringIO.new
|
226
|
-
# err = StringIO.new
|
227
|
-
# command = ['echo "stdout" && echo "stderr" 1>&2']
|
228
|
-
# result = ProcessExecuter.run(*command, out: out, err: err)
|
229
|
-
# out.string #=> "stdout\n"
|
230
|
-
# err.string #=> "stderr\n"
|
231
|
-
#
|
232
|
-
# @example Capture to a file
|
233
|
-
# # Same technique can be used for stderr
|
234
|
-
# out = File.open('stdout.txt', 'w')
|
235
|
-
# err = StringIO.new
|
236
|
-
# command = ['echo "stdout" && echo "stderr" 1>&2']
|
237
|
-
# result = ProcessExecuter.run(*command, out: out, err: err)
|
238
|
-
# out.close
|
239
|
-
# File.read('stdout.txt') #=> "stdout\n"
|
240
|
-
# # stderr is still captured to a StringIO buffer internally
|
241
|
-
# result.stderr #=> "stderr\n"
|
242
|
-
#
|
243
|
-
# @example Capture to multiple destinations (e.g. files, buffers, STDOUT, etc.)
|
244
|
-
# # Same technique can be used for stderr
|
245
|
-
# out_buffer = StringIO.new
|
246
|
-
# out_file = File.open('stdout.txt', 'w')
|
247
|
-
# command = ['echo "stdout" && echo "stderr" 1>&2']
|
248
|
-
# result = ProcessExecuter.run(*command, out: [:tee, out_buffer, out_file])
|
249
|
-
# # You must manage closing resources you create yourself
|
250
|
-
# out_file.close
|
251
|
-
# out_buffer.string #=> "stdout\n"
|
252
|
-
# File.read('stdout.txt') #=> "stdout\n"
|
253
|
-
# result.stdout #=> "stdout\n"
|
254
|
-
#
|
255
|
-
# @param command [Array<String>] The command to run
|
256
|
-
#
|
257
|
-
# If the first element of command is a Hash, it is added to the ENV of
|
258
|
-
# the new process. See [Execution Environment](https://ruby-doc.org/3.3.6/Process.html#module-Process-label-Execution+Environment)
|
259
|
-
# for more details. The env hash is then removed from the command array.
|
260
|
-
#
|
261
|
-
# If the first and only (remaining) command element is a string, it is passed to
|
262
|
-
# a subshell if it begins with a shell reserved word, contains special built-ins,
|
263
|
-
# or includes shell metacharacters.
|
264
|
-
#
|
265
|
-
# Care must be taken to properly escape shell metacharacters in the command string.
|
266
|
-
#
|
267
|
-
# Otherwise, the command is run bypassing the shell. When bypassing the shell, shell expansions
|
268
|
-
# and redirections are not supported.
|
269
|
-
#
|
270
|
-
# @param options_hash [Hash] Additional options
|
271
|
-
# @option options_hash [Numeric] :timeout_after The maximum seconds to wait for the
|
272
|
-
# command to complete
|
273
|
-
#
|
274
|
-
# If zero or nil, the command will not time out. If the command
|
275
|
-
# times out, it is killed via a SIGKILL signal. A {ProcessExecuter::TimeoutError}
|
276
|
-
# will be raised if the `:raise_errors` option is true.
|
277
|
-
#
|
278
|
-
# If the command does not exit when receiving the SIGKILL signal, this method may hang indefinitely.
|
279
|
-
#
|
280
|
-
# @option options_hash [#write] :out (nil) The object to write stdout to
|
281
|
-
# @option options_hash [#write] :err (nil) The object to write stderr to
|
282
|
-
# @option options_hash [Boolean] :raise_errors (true) Raise an exception if the command fails
|
283
|
-
# @option options_hash [Boolean] :unsetenv_others (false) If true, unset all environment variables before
|
284
|
-
# applying the new ones
|
285
|
-
# @option options_hash [true, Integer, nil] :pgroup (nil) true or 0: new process group; non-zero: join
|
286
|
-
# the group, nil: existing group
|
287
|
-
# @option options_hash [Boolean] :new_pgroup (nil) Create a new process group (Windows only)
|
288
|
-
# @option options_hash [Integer] :rlimit_resource_name (nil) Set resource limits (see Process.setrlimit)
|
289
|
-
# @option options_hash [Integer] :umask (nil) Set the umask (see File.umask)
|
290
|
-
# @option options_hash [Boolean] :close_others (false) If true, close non-standard file descriptors
|
291
|
-
# @option options_hash [String] :chdir (nil) The directory to run the command in
|
292
|
-
# @option options_hash [Logger] :logger The logger to use
|
293
|
-
#
|
294
|
-
# @raise [ProcessExecuter::Error] if the command could not be executed or failed
|
295
|
-
#
|
296
|
-
# @return [ProcessExecuter::Result] The result of the completed subprocess
|
234
|
+
# Raised if the
|
235
|
+
# [Process.spawn](https://docs.ruby-lang.org/en/3.4/Process.html#method-c-spawn)
|
236
|
+
# method raises an error before the command is run.
|
237
|
+
#
|
238
|
+
# @raise [ProcessExecuter::FailedError] If the command ran and failed
|
239
|
+
#
|
240
|
+
# @raise [ProcessExecuter::SignaledError] If the command ran and terminated due to
|
241
|
+
# an unhandled signal
|
242
|
+
#
|
243
|
+
# @raise [ProcessExecuter::TimeoutError] If the command timed out
|
244
|
+
#
|
245
|
+
# @raise [ProcessExecuter::ProcessIOError] If there was an exception while
|
246
|
+
# collecting subprocess output
|
247
|
+
#
|
248
|
+
# @return [ProcessExecuter::Result]
|
249
|
+
#
|
250
|
+
# @api public
|
297
251
|
#
|
298
252
|
def self.run(*command, **options_hash)
|
299
|
-
options =
|
300
|
-
|
253
|
+
command, options = command_and_options(Options::RunOptions, command, options_hash)
|
254
|
+
ProcessExecuter::Commands::Run.new(command, options).call
|
301
255
|
end
|
302
256
|
|
303
|
-
#
|
257
|
+
# Extends {run}, automatically capturing stdout and stderr
|
258
|
+
#
|
259
|
+
# Accepts all [Process.spawn execution
|
260
|
+
# options](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Execution+Options),
|
261
|
+
# the additional options defined by {spawn_with_timeout} and {run}, and the
|
262
|
+
# additional options `merge_output`, `encoding`, `stdout_encoding`, and
|
263
|
+
# `stderr_encoding`:
|
264
|
+
#
|
265
|
+
# * `merge_output: <Boolean>` if true merges stdout and stderr into a single
|
266
|
+
# capture buffer (default is false)
|
267
|
+
# * `encoding: <Encoding>` sets the encoding for both stdout and stderr captures
|
268
|
+
# (default is `Encoding::UTF_8`)
|
269
|
+
# * `stdout_encoding: <Encoding>` sets the encoding for the stdout capture and, if
|
270
|
+
# not nil, overrides the `encoding` option for stdout (default is nil)
|
271
|
+
# * `stderr_encoding: <Encoding>` sets the encoding for the stderr capture and, if
|
272
|
+
# not nil, overrides the `encoding` option for stderr (default is nil)
|
304
273
|
#
|
305
|
-
#
|
274
|
+
# The captured output is accessed in the returned object's `#stdout` and `#stderr`
|
275
|
+
# methods. Merged output (if the `merged_output: true` option is given) is accessed
|
276
|
+
# in the `#stdout` method.
|
306
277
|
#
|
307
|
-
#
|
308
|
-
#
|
278
|
+
# stdout and stderr redirection destinations may be given by the user (e.g. `out:
|
279
|
+
# <destination>` or `err: <destination>`). These redirections will receive the
|
280
|
+
# output in addition to the internal capture.
|
309
281
|
#
|
310
|
-
#
|
282
|
+
# Unless told otherwise, the internally captured output is assumed to be in UTF-8
|
283
|
+
# encoding. This assumption can be changed with the `encoding`,
|
284
|
+
# `stdout_encoding`, or `stderr_encoding` options. These options accept any
|
285
|
+
# encoding objects returned by `Encoding.list` or their String equivalent given by
|
286
|
+
# `#to_s`.
|
311
287
|
#
|
312
|
-
#
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
# Wait for process to terminate
|
288
|
+
# The bytes captured are not transcoded. They are interpreted as being in the
|
289
|
+
# specified encoding. The user will have to check the validity of the
|
290
|
+
# encoding by calling `#valid_encoding?` on the captured output (e.g.,
|
291
|
+
# `result.stdout.valid_encoding?`).
|
318
292
|
#
|
319
|
-
#
|
320
|
-
#
|
293
|
+
# A `ProcessExecuter::ArgumentError` will be raised if both an options object and
|
294
|
+
# an options_hash are given.
|
321
295
|
#
|
322
|
-
# @
|
323
|
-
#
|
296
|
+
# @example capture stdout and stderr
|
297
|
+
# result =
|
298
|
+
# ProcessExecuter.run_with_capture('echo HELLO; echo ERROR >&2')
|
299
|
+
# result.stdout #=> "HELLO\n" result.stderr #=> "ERROR\n"
|
324
300
|
#
|
325
|
-
# @
|
301
|
+
# @example merge stdout and stderr
|
302
|
+
# result = ProcessExecuter.run_with_capture('echo HELLO; echo ERROR >&2', merge_output: true)
|
303
|
+
# # order of output is not guaranteed
|
304
|
+
# result.stdout #=> "HELLO\nERROR\n" result.stderr #=> ""
|
326
305
|
#
|
327
|
-
# @
|
306
|
+
# @example default encoding
|
307
|
+
# result = ProcessExecuter.run_with_capture('echo HELLO')
|
308
|
+
# result.stdout #=> "HELLO\n"
|
309
|
+
# result.stdout.encoding #=> #<Encoding:UTF-8>
|
310
|
+
# result.stdout.valid_encoding? #=> true
|
328
311
|
#
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
end
|
335
|
-
|
336
|
-
# Wait for a process to terminate returning the status and timed out flag
|
312
|
+
# @example custom encoding
|
313
|
+
# result = ProcessExecuter.run_with_capture('echo HELLO', encoding: Encoding::ISO_8859_1)
|
314
|
+
# result.stdout #=> "HELLO\n"
|
315
|
+
# result.stdout.encoding #=> #<Encoding:ISO-8859-1>
|
316
|
+
# result.stdout.valid_encoding? #=> true
|
337
317
|
#
|
338
|
-
# @
|
339
|
-
#
|
340
|
-
#
|
341
|
-
#
|
342
|
-
#
|
343
|
-
|
344
|
-
timed_out = false
|
345
|
-
|
346
|
-
process_status =
|
347
|
-
begin
|
348
|
-
Timeout.timeout(timeout_after) { Process.wait2(pid).last }
|
349
|
-
rescue Timeout::Error
|
350
|
-
Process.kill('KILL', pid)
|
351
|
-
timed_out = true
|
352
|
-
Process.wait2(pid).last
|
353
|
-
end
|
354
|
-
|
355
|
-
[process_status, timed_out]
|
356
|
-
end
|
357
|
-
|
358
|
-
# Convert a hash to a SpawnOptions object
|
318
|
+
# @example custom encoding with invalid bytes
|
319
|
+
# File.binwrite('output.txt', "\xFF\xFE") # little-endian BOM marker is not valid UTF-8
|
320
|
+
# result = ProcessExecuter.run_with_capture('cat output.txt')
|
321
|
+
# result.stdout #=> "\xFF\xFE"
|
322
|
+
# result.stdout.encoding #=> #<Encoding:UTF-8>
|
323
|
+
# result.stdout.valid_encoding? #=> false
|
359
324
|
#
|
360
|
-
# @
|
361
|
-
# options_hash = { out: $stdout }
|
362
|
-
# options = ProcessExecuter.spawn_options(options_hash) # =>
|
363
|
-
# #<ProcessExecuter::Options::SpawnOptions:0x00007f8f9b0b3d20 out: $stdout>
|
364
|
-
# ProcessExecuter.spawn_options(options) # =>
|
365
|
-
# #<ProcessExecuter::Options::SpawnOptions:0x00007f8f9b0b3d20 out: $stdout>
|
325
|
+
# @overload run_with_capture(*command, **options_hash)
|
366
326
|
#
|
367
|
-
#
|
327
|
+
# @param command [Array<String>] see [Process module, Argument `command_line` or
|
328
|
+
# `exe_path`](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Argument+command_line)
|
329
|
+
# and [Process module, Argument
|
330
|
+
# `args`](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Arguments+args)
|
368
331
|
#
|
369
|
-
#
|
332
|
+
# If the first value is a Hash, it is treated as the environment hash. See
|
333
|
+
# [Process module, Execution Environment](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Execution+Environment).
|
370
334
|
#
|
371
|
-
#
|
335
|
+
# @param options_hash [Hash] in addition to the options supported by {run},
|
336
|
+
# `merge_output` may be given
|
372
337
|
#
|
373
|
-
#
|
338
|
+
# @option options_hash [Boolean] :merge_output if true, stdout and stderr will be
|
339
|
+
# merged into a single capture buffer
|
374
340
|
#
|
375
|
-
|
376
|
-
|
377
|
-
when ProcessExecuter::Options::SpawnOptions
|
378
|
-
obj
|
379
|
-
when Hash
|
380
|
-
ProcessExecuter::Options::SpawnOptions.new(**obj)
|
381
|
-
else
|
382
|
-
raise ArgumentError, "Expected a Hash or ProcessExecuter::Options::SpawnOptions but got a #{obj.class}"
|
383
|
-
end
|
384
|
-
end
|
385
|
-
|
386
|
-
# Convert a hash to a SpawnAndWaitOptions object
|
341
|
+
# @option options_hash [Encoding, String] :encoding the encoding to assume for
|
342
|
+
# the internal stdout and stderr captures
|
387
343
|
#
|
388
|
-
#
|
389
|
-
#
|
390
|
-
# options = ProcessExecuter.spawn_and_wait_options(options_hash) # =>
|
391
|
-
# #<ProcessExecuter::Options::SpawnAndWaitOptions:0x00007f8f9b0b3d20 out: $stdout>
|
392
|
-
# ProcessExecuter.spawn_and_wait_options(options) # =>
|
393
|
-
# #<ProcessExecuter::Options::SpawnAndWaitOptions:0x00007f8f9b0b3d20 out: $stdout>
|
344
|
+
# The default is `Encoding::UTF_8`. This option is overridden by the `stdout_encoding`
|
345
|
+
# and `stderr_encoding` options if they are given and not nil.
|
394
346
|
#
|
395
|
-
#
|
347
|
+
# @option options_hash [Encoding, String, nil] :stdout_encoding the encoding to
|
348
|
+
# assume for the internal stdout capture
|
396
349
|
#
|
397
|
-
#
|
350
|
+
# The default is nil, which means the `encoding` option is used. If this option is
|
351
|
+
# is not nil, it is used instead of the `encoding` option.
|
398
352
|
#
|
399
|
-
#
|
353
|
+
# @option options_hash [Encoding, String, nil] :stderr_encoding the encoding to
|
354
|
+
# assume for the internal stderr capture
|
400
355
|
#
|
401
|
-
#
|
356
|
+
# The default is nil, which means the `encoding` option is used. If this option
|
357
|
+
# is not nil, it is used instead of the `encoding` option.
|
402
358
|
#
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
#
|
359
|
+
# @overload run_with_capture(*command, options)
|
360
|
+
#
|
361
|
+
# @param command [Array<String>] see [Process module, Argument `command_line` or
|
362
|
+
# `exe_path`](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Argument+command_line)
|
363
|
+
# and [Process module, Argument
|
364
|
+
# `args`](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Arguments+args)
|
365
|
+
#
|
366
|
+
# If the first value is a Hash, it is treated as the environment hash. See
|
367
|
+
# [Process module, Execution Environment](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Execution+Environment).
|
368
|
+
#
|
369
|
+
# @param options [ProcessExecuter::Options::RunWithCaptureOptions]
|
370
|
+
#
|
371
|
+
# @raise [ProcessExecuter::ArgumentError] If the command or an option is not valid
|
415
372
|
#
|
416
|
-
#
|
417
|
-
# options_hash
|
418
|
-
# options = ProcessExecuter.run_options(options_hash) # =>
|
419
|
-
# #<ProcessExecuter::Options::RunOptions:0x00007f8f9b0b3d20 out: $stdout>
|
420
|
-
# ProcessExecuter.run_options(options) # =>
|
421
|
-
# #<ProcessExecuter::Options::RunOptions:0x00007f8f9b0b3d20 out: $stdout>
|
373
|
+
# Raised if an invalid option key or value is given, or both an options object
|
374
|
+
# and options_hash are given.
|
422
375
|
#
|
423
|
-
# @
|
376
|
+
# @raise [ProcessExecuter::SpawnError] `Process.spawn` raised an error before the
|
377
|
+
# command was run
|
424
378
|
#
|
425
|
-
#
|
379
|
+
# Raised if the
|
380
|
+
# [Process.spawn](https://docs.ruby-lang.org/en/3.4/Process.html#method-c-spawn)
|
381
|
+
# method raises an error before the command is run.
|
426
382
|
#
|
427
|
-
# @raise [
|
383
|
+
# @raise [ProcessExecuter::FailedError] If the command ran and failed
|
384
|
+
#
|
385
|
+
# @raise [ProcessExecuter::SignaledError] If the command ran and terminated due to
|
386
|
+
# an unhandled signal
|
387
|
+
#
|
388
|
+
# @raise [ProcessExecuter::TimeoutError] If the command timed out
|
389
|
+
#
|
390
|
+
# @raise [ProcessExecuter::ProcessIOError] If there was an exception while
|
391
|
+
# collecting subprocess output
|
392
|
+
#
|
393
|
+
# @return [ProcessExecuter::ResultWithCapture]
|
394
|
+
#
|
395
|
+
# Where `#stdout` and `#stderr` are strings whose encoding is determined by the
|
396
|
+
# `:encoding`, `:stdout_encoding`, or `:stderr_encoding` options.
|
428
397
|
#
|
429
398
|
# @api public
|
430
399
|
#
|
431
|
-
def self.
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
400
|
+
def self.run_with_capture(*command, **options_hash)
|
401
|
+
command, options = command_and_options(Options::RunWithCaptureOptions, command, options_hash)
|
402
|
+
ProcessExecuter::Commands::RunWithCapture.new(command, options).call
|
403
|
+
end
|
404
|
+
|
405
|
+
# Takes a command and options_hash to determine the options object
|
406
|
+
#
|
407
|
+
# To support either passing an options object or an options_hash, this method takes
|
408
|
+
# a command and an options_hash and returns the command (with the trailing options
|
409
|
+
# object removed if one is given) and and options object.
|
410
|
+
#
|
411
|
+
# @example options hash not empty
|
412
|
+
# command, options = ProcessExecuter.command_and_options(
|
413
|
+
# ProcessExecuter::Options::RunOptions,
|
414
|
+
# ['echo hello'],
|
415
|
+
# { out: $stdout }
|
416
|
+
# )
|
417
|
+
# command #=> ['echo hello']
|
418
|
+
# options #=> a new RunOptions instance initialized with the options hash
|
419
|
+
#
|
420
|
+
# @example options_hash empty, command DOES NOT end with an options object
|
421
|
+
# command, options = ProcessExecuter.command_and_options(
|
422
|
+
# ProcessExecuter::Options::RunOptions,
|
423
|
+
# ['echo hello'],
|
424
|
+
# {}
|
425
|
+
# )
|
426
|
+
# command #=> ['echo hello']
|
427
|
+
# options #=> a new RunOptions instance initialized with defaults
|
428
|
+
#
|
429
|
+
# @example options_hash empty, command ends with an options object
|
430
|
+
# command, options = ProcessExecuter.command_and_options(
|
431
|
+
# ProcessExecuter::Options::RunOptions,
|
432
|
+
# ['echo hello', ProcessExecuter::Options::RunOptions.new(out: $stdout)],
|
433
|
+
# {}
|
434
|
+
# )
|
435
|
+
# command #=> ['echo hello'] # options object is removed
|
436
|
+
# options #=> the RunOptions object from command[-1]
|
437
|
+
#
|
438
|
+
# @param options_class [Class] the class of the options object
|
439
|
+
#
|
440
|
+
# @param command [Array] the command to be executed (possibly with an instance of
|
441
|
+
# options_class at the end)
|
442
|
+
#
|
443
|
+
# @param options_hash [Hash] the (possibly empty) hash of options
|
444
|
+
#
|
445
|
+
# @return [Array] An array containing two elements: the command and an options object
|
446
|
+
#
|
447
|
+
# The command is an array of strings and the options is an instance of the
|
448
|
+
# specified options_class.
|
449
|
+
#
|
450
|
+
# @raise [ProcessExecuter::ArgumentError] If both an options object and an
|
451
|
+
# options_hash are given
|
452
|
+
#
|
453
|
+
# @api private
|
454
|
+
#
|
455
|
+
private_class_method def self.command_and_options(options_class, command, options_hash)
|
456
|
+
if command[-1].is_a?(options_class) && !options_hash.empty?
|
457
|
+
raise ProcessExecuter::ArgumentError, 'Provide either an options object or an options hash, not both.'
|
458
|
+
end
|
459
|
+
|
460
|
+
if !options_hash.empty?
|
461
|
+
[command, options_class.new(**options_hash)]
|
462
|
+
elsif command[-1].is_a?(options_class)
|
463
|
+
[command[..-2], command[-1]]
|
437
464
|
else
|
438
|
-
|
465
|
+
[command, options_class.new]
|
439
466
|
end
|
440
467
|
end
|
441
468
|
end
|
469
|
+
|
470
|
+
require 'logger'
|
471
|
+
require 'timeout'
|
472
|
+
|
473
|
+
require 'process_executer/commands'
|
474
|
+
require 'process_executer/destinations'
|
475
|
+
require 'process_executer/errors'
|
476
|
+
require 'process_executer/monitored_pipe'
|
477
|
+
require 'process_executer/options'
|
478
|
+
require 'process_executer/result'
|
479
|
+
require 'process_executer/result_with_capture'
|