process_executer 1.3.0 → 2.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/CHANGELOG.md +8 -0
- data/README.md +23 -23
- data/lib/process_executer/errors.rb +134 -0
- data/lib/process_executer/options.rb +17 -14
- data/lib/process_executer/result.rb +160 -0
- data/lib/process_executer/runner.rb +147 -0
- data/lib/process_executer/version.rb +1 -1
- data/lib/process_executer.rb +135 -83
- metadata +8 -14
- data/.tool-versions +0 -1
- data/lib/process_executer/command/errors.rb +0 -170
- data/lib/process_executer/command/result.rb +0 -77
- data/lib/process_executer/command/runner.rb +0 -167
- data/lib/process_executer/command.rb +0 -12
- data/lib/process_executer/status.rb +0 -70
data/lib/process_executer.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'process_executer/errors'
|
3
4
|
require 'process_executer/monitored_pipe'
|
4
5
|
require 'process_executer/options'
|
5
|
-
require 'process_executer/
|
6
|
-
require 'process_executer/
|
6
|
+
require 'process_executer/result'
|
7
|
+
require 'process_executer/runner'
|
7
8
|
|
8
9
|
require 'logger'
|
9
10
|
require 'timeout'
|
@@ -13,86 +14,114 @@ require 'timeout'
|
|
13
14
|
# environment variables.
|
14
15
|
#
|
15
16
|
# Methods:
|
16
|
-
# * {run}: Executes a command and
|
17
|
-
#
|
17
|
+
# * {run}: Executes a command and returns the result which includes the process
|
18
|
+
# status and output
|
19
|
+
# * {spawn_and_wait}: a thin wrapper around `Process.spawn` that blocks until the
|
20
|
+
# command finishes
|
18
21
|
#
|
19
22
|
# Features:
|
20
23
|
# * Supports executing commands via a shell or directly.
|
21
24
|
# * Captures stdout and stderr to buffers, files, or custom objects.
|
22
25
|
# * Optionally enforces timeouts and terminates long-running commands.
|
23
|
-
# * Provides detailed status information, including
|
26
|
+
# * Provides detailed status information, including the command that was run, the
|
27
|
+
# options that were given, and success, failure, or timeout states.
|
24
28
|
#
|
25
29
|
# @api public
|
26
30
|
#
|
27
31
|
module ProcessExecuter
|
28
|
-
#
|
32
|
+
# Run a command in a subprocess, wait for it to finish, then return the result
|
29
33
|
#
|
30
|
-
# This is a
|
34
|
+
# This method is a thin wrapper around
|
31
35
|
# [Process.spawn](https://docs.ruby-lang.org/en/3.3/Process.html#method-c-spawn)
|
32
36
|
# and blocks until the command terminates.
|
33
37
|
#
|
34
|
-
#
|
35
|
-
# the specified timeout.
|
38
|
+
# A timeout may be specified with the `:timeout_after` option. The command will be
|
39
|
+
# sent the SIGKILL signal if it does not terminate within the specified timeout.
|
36
40
|
#
|
37
41
|
# @example
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
+
# result = ProcessExecuter.spawn_and_wait('echo hello')
|
43
|
+
# result.exited? # => true
|
44
|
+
# result.success? # => true
|
45
|
+
# result.timed_out? # => false
|
42
46
|
#
|
43
47
|
# @example with a timeout
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
48
|
+
# result = ProcessExecuter.spawn_and_wait('sleep 10', timeout_after: 0.01)
|
49
|
+
# result.exited? # => false
|
50
|
+
# result.success? # => nil
|
51
|
+
# result.signaled? # => true
|
52
|
+
# result.termsig # => 9
|
53
|
+
# result.timed_out? # => true
|
50
54
|
#
|
51
55
|
# @example capturing stdout to a string
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
56
|
+
# stdout_buffer = StringIO.new
|
57
|
+
# stdout_pipe = ProcessExecuter::MonitoredPipe.new(stdout_buffer)
|
58
|
+
# result = ProcessExecuter.spawn_and_wait('echo hello', out: stdout_pipe)
|
59
|
+
# stdout_buffer.string # => "hello\n"
|
55
60
|
#
|
56
61
|
# @see https://ruby-doc.org/core-3.1.2/Kernel.html#method-i-spawn Kernel.spawn
|
57
62
|
# documentation for valid command and options
|
58
63
|
#
|
59
|
-
# @see ProcessExecuter::Options#initialize
|
60
|
-
#
|
64
|
+
# @see ProcessExecuter::Options#initialize ProcessExecuter::Options#initialize for
|
65
|
+
# options that may be specified
|
61
66
|
#
|
62
67
|
# @param command [Array<String>] The command to execute
|
63
68
|
# @param options_hash [Hash] The options to use when executing the command
|
64
69
|
#
|
65
|
-
# @return [
|
70
|
+
# @return [ProcessExecuter::Result] The result of the completed subprocess
|
66
71
|
#
|
67
|
-
def self.
|
72
|
+
def self.spawn_and_wait(*command, **options_hash)
|
68
73
|
options = ProcessExecuter::Options.new(**options_hash)
|
69
74
|
pid = Process.spawn(*command, **options.spawn_options)
|
70
|
-
wait_for_process(pid, options)
|
75
|
+
wait_for_process(pid, command, options)
|
71
76
|
end
|
72
77
|
|
73
|
-
# Execute the given command as a subprocess
|
78
|
+
# Execute the given command as a subprocess blocking until it finishes
|
74
79
|
#
|
75
|
-
#
|
80
|
+
# Works just like {ProcessExecuter.spawn}, but does the following in addition:
|
76
81
|
#
|
77
|
-
#
|
78
|
-
#
|
79
|
-
#
|
82
|
+
# 1. If nothing is specified for `out`, stdout is captured to a `StringIO` object
|
83
|
+
# which can be accessed via the Result object in `result.options.out`. The
|
84
|
+
# same applies to `err`.
|
80
85
|
#
|
81
|
-
#
|
82
|
-
#
|
83
|
-
#
|
86
|
+
# 2. If `merge` is set to `true`, stdout and stderr are captured to the same
|
87
|
+
# buffer.
|
88
|
+
#
|
89
|
+
# 3. `out` and `err` are automatically wrapped in a
|
90
|
+
# `ProcessExecuter::MonitoredPipe` object so that any object that implements
|
91
|
+
# `#write` (or an Array of such objects) can be given for `out` and `err`.
|
92
|
+
#
|
93
|
+
# 4. Raises one of the following errors unless `raise_errors` is explicitly set
|
94
|
+
# to `false`:
|
95
|
+
#
|
96
|
+
# * `ProcessExecuter::FailedError` if the command returns a non-zero
|
97
|
+
# exitstatus
|
98
|
+
# * `ProcessExecuter::SignaledError` if the command exits because of
|
99
|
+
# an unhandled signal
|
100
|
+
# * `ProcessExecuter::TimeoutError` if the command times out
|
101
|
+
#
|
102
|
+
# If `raise_errors` is false, the returned Result object will contain the error.
|
103
|
+
#
|
104
|
+
# 5. Raises a `ProcessExecuter::ProcessIOError` if an exception is raised
|
105
|
+
# while collecting subprocess output. This can not be turned off.
|
106
|
+
#
|
107
|
+
# 6. If a `logger` is provided, it will be used to log:
|
108
|
+
#
|
109
|
+
# * The command that was executed and its status to `info` level
|
110
|
+
# * The stdout and stderr output to `debug` level
|
111
|
+
#
|
112
|
+
# By default, Logger.new(nil) is used for the logger.
|
84
113
|
#
|
85
114
|
# This method takes two forms:
|
86
115
|
#
|
87
116
|
# 1. The command is executed via a shell when the command is given as a single
|
88
117
|
# string:
|
89
118
|
#
|
90
|
-
# `ProcessExecuter.run([env, ] command_line, options = {}) ->` {ProcessExecuter::
|
119
|
+
# `ProcessExecuter.run([env, ] command_line, options = {}) ->` {ProcessExecuter::Result}
|
91
120
|
#
|
92
121
|
# 2. The command is executed directly (bypassing the shell) when the command and it
|
93
122
|
# arguments are given as an array of strings:
|
94
123
|
#
|
95
|
-
# `ProcessExecuter.run([env, ] exe_path, *args, options = {}) ->` {ProcessExecuter::
|
124
|
+
# `ProcessExecuter.run([env, ] exe_path, *args, options = {}) ->` {ProcessExecuter::Result}
|
96
125
|
#
|
97
126
|
# Optional argument `env` is a hash that affects ENV for the new process; see
|
98
127
|
# [Execution
|
@@ -102,11 +131,11 @@ module ProcessExecuter
|
|
102
131
|
#
|
103
132
|
# @example Run a command given as a single string (uses shell)
|
104
133
|
# # The command must be properly shell escaped when passed as a single string.
|
105
|
-
# command = 'echo "stdout: `pwd`"
|
134
|
+
# command = 'echo "stdout: `pwd`" && echo "stderr: $HOME" 1>&2'
|
106
135
|
# result = ProcessExecuter.run(command)
|
107
136
|
# result.success? #=> true
|
108
|
-
# result.stdout
|
109
|
-
# result.stderr
|
137
|
+
# result.stdout #=> "stdout: /Users/james/projects/main-branch/process_executer\n"
|
138
|
+
# result.stderr #=> "stderr: /Users/james\n"
|
110
139
|
#
|
111
140
|
# @example Run a command given as an array of strings (does not use shell)
|
112
141
|
# # The command and its args must be provided as separate strings in the array.
|
@@ -114,67 +143,65 @@ module ProcessExecuter
|
|
114
143
|
# command = ['git', 'clone', 'https://github.com/main-branch/process_executer']
|
115
144
|
# result = ProcessExecuter.run(*command)
|
116
145
|
# result.success? #=> true
|
117
|
-
# result.stdout
|
118
|
-
# result.stderr
|
146
|
+
# result.stdout #=> ""
|
147
|
+
# result.stderr #=> "Cloning into 'process_executer'...\n"
|
119
148
|
#
|
120
149
|
# @example Run a command with a timeout
|
121
150
|
# command = ['sleep', '1']
|
122
|
-
# result = ProcessExecuter.run(*command,
|
123
|
-
# #=> raises ProcessExecuter::
|
151
|
+
# result = ProcessExecuter.run(*command, timeout_after: 0.01)
|
152
|
+
# #=> raises ProcessExecuter::TimeoutError which contains the command result
|
124
153
|
#
|
125
154
|
# @example Run a command which fails
|
126
155
|
# command = ['exit 1']
|
127
156
|
# result = ProcessExecuter.run(*command)
|
128
|
-
# #=> raises ProcessExecuter::
|
157
|
+
# #=> raises ProcessExecuter::FailedError which contains the command result
|
129
158
|
#
|
130
159
|
# @example Run a command which exits due to an unhandled signal
|
131
160
|
# command = ['kill -9 $$']
|
132
161
|
# result = ProcessExecuter.run(*command)
|
133
|
-
# #=> raises ProcessExecuter::
|
162
|
+
# #=> raises ProcessExecuter::SignaledError which contains the command result
|
134
163
|
#
|
135
|
-
# @example
|
136
|
-
# # By setting `raise_errors` to `false`, exceptions will not be raised even
|
137
|
-
# # if the command fails.
|
164
|
+
# @example Do not raise an error when the command fails
|
138
165
|
# command = ['echo "Some error" 1>&2 && exit 1']
|
139
166
|
# result = ProcessExecuter.run(*command, raise_errors: false)
|
140
|
-
# # An error is not raised
|
141
167
|
# result.success? #=> false
|
142
168
|
# result.exitstatus #=> 1
|
143
|
-
# result.stdout
|
144
|
-
# result.stderr
|
169
|
+
# result.stdout #=> ""
|
170
|
+
# result.stderr #=> "Some error\n"
|
145
171
|
#
|
146
172
|
# @example Set environment variables
|
147
173
|
# env = { 'FOO' => 'foo', 'BAR' => 'bar' }
|
148
174
|
# command = 'echo "$FOO$BAR"'
|
149
175
|
# result = ProcessExecuter.run(env, *command)
|
150
|
-
# result.stdout
|
176
|
+
# result.stdout #=> "foobar\n"
|
151
177
|
#
|
152
178
|
# @example Set environment variables when using a command array
|
153
|
-
# env = { '
|
154
|
-
# command = ['
|
179
|
+
# env = { 'FOO' => 'foo', 'BAR' => 'bar' }
|
180
|
+
# command = ['ruby', '-e', 'puts ENV["FOO"] + ENV["BAR"]']
|
155
181
|
# result = ProcessExecuter.run(env, *command)
|
156
|
-
# result.stdout
|
182
|
+
# result.stdout #=> "foobar\n"
|
157
183
|
#
|
158
184
|
# @example Unset environment variables
|
159
|
-
# env = { '
|
160
|
-
# command = ['
|
185
|
+
# env = { 'FOO' => nil } # setting to nil unsets the variable in the environment
|
186
|
+
# command = ['echo "FOO: $FOO"']
|
161
187
|
# result = ProcessExecuter.run(env, *command)
|
162
|
-
# result.stdout
|
188
|
+
# result.stdout #=> "FOO: \n"
|
163
189
|
#
|
164
190
|
# @example Reset existing environment variables and add new ones
|
165
191
|
# env = { 'PATH' => '/bin' }
|
166
192
|
# result = ProcessExecuter.run(env, 'echo "Home: $HOME" && echo "Path: $PATH"', unsetenv_others: true)
|
167
|
-
# result.stdout
|
193
|
+
# result.stdout #=> "Home: \n/Path: /bin\n"
|
168
194
|
#
|
169
195
|
# @example Run command in a different directory
|
170
196
|
# command = ['pwd']
|
171
197
|
# result = ProcessExecuter.run(*command, chdir: '/tmp')
|
172
|
-
# result.stdout
|
198
|
+
# result.stdout #=> "/tmp\n"
|
173
199
|
#
|
174
200
|
# @example Capture stdout and stderr into a single buffer
|
175
201
|
# command = ['echo "stdout" && echo "stderr" 1>&2']
|
176
202
|
# result = ProcessExecuter.run(*command, merge: true)
|
177
|
-
# result.stdout
|
203
|
+
# result.stdout #=> "stdout\nstderr\n"
|
204
|
+
# result.stderr #=> "stdout\nstderr\n"
|
178
205
|
# result.stdout.object_id == result.stderr.object_id #=> true
|
179
206
|
#
|
180
207
|
# @example Capture to an explicit buffer
|
@@ -184,18 +211,17 @@ module ProcessExecuter
|
|
184
211
|
# result = ProcessExecuter.run(*command, out: out, err: err)
|
185
212
|
# out.string #=> "stdout\n"
|
186
213
|
# err.string #=> "stderr\n"
|
187
|
-
# result.stdout.object_id == out.object_id #=> true
|
188
|
-
# result.stderr.object_id == err.object_id #=> true
|
189
214
|
#
|
190
215
|
# @example Capture to a file
|
191
216
|
# # Same technique can be used for stderr
|
192
217
|
# out = File.open('stdout.txt', 'w')
|
218
|
+
# err = StringIO.new
|
193
219
|
# command = ['echo "stdout" && echo "stderr" 1>&2']
|
194
220
|
# result = ProcessExecuter.run(*command, out: out, err: err)
|
195
221
|
# out.close
|
196
222
|
# File.read('stdout.txt') #=> "stdout\n"
|
197
223
|
# # stderr is still captured to a StringIO buffer internally
|
198
|
-
# result.stderr
|
224
|
+
# result.stderr #=> "stderr\n"
|
199
225
|
#
|
200
226
|
# @example Capture to multiple writers (e.g. files, buffers, STDOUT, etc.)
|
201
227
|
# # Same technique can be used for stderr
|
@@ -207,6 +233,9 @@ module ProcessExecuter
|
|
207
233
|
# out_file.close
|
208
234
|
# out_buffer.string #=> "stdout\n"
|
209
235
|
# File.read('stdout.txt') #=> "stdout\n"
|
236
|
+
# # Since one of the out writers has a #string method, Result#stdout will
|
237
|
+
# # return the string from that writer
|
238
|
+
# result.stdout #=> "stdout\n"
|
210
239
|
#
|
211
240
|
# @param command [Array<String>] The command to run
|
212
241
|
#
|
@@ -225,10 +254,12 @@ module ProcessExecuter
|
|
225
254
|
#
|
226
255
|
# @param logger [Logger] The logger to use
|
227
256
|
# @param options_hash [Hash] Additional options
|
228
|
-
# @option options_hash [Numeric] :
|
257
|
+
# @option options_hash [Numeric] :timeout_after The maximum seconds to wait for the
|
258
|
+
# command to complete
|
229
259
|
#
|
230
|
-
# If
|
231
|
-
# times out, it is killed via a SIGKILL signal
|
260
|
+
# If zero or nil, the command will not time out. If the command
|
261
|
+
# times out, it is killed via a SIGKILL signal. A {ProcessExecuter::TimeoutError}
|
262
|
+
# will be raised if the `:raise_errors` option is true.
|
232
263
|
#
|
233
264
|
# If the command does not exit when receiving the SIGKILL signal, this method may hang indefinitely.
|
234
265
|
#
|
@@ -246,34 +277,55 @@ module ProcessExecuter
|
|
246
277
|
# @option options_hash [Boolean] :close_others (false) If true, close non-standard file descriptors
|
247
278
|
# @option options_hash [String] :chdir (nil) The directory to run the command in
|
248
279
|
#
|
249
|
-
# @raise [ProcessExecuter::
|
250
|
-
# @raise [ProcessExecuter::
|
251
|
-
# @raise [ProcessExecuter::
|
252
|
-
# @raise [ProcessExecuter::
|
280
|
+
# @raise [ProcessExecuter::FailedError] if the command returned a non-zero exit status
|
281
|
+
# @raise [ProcessExecuter::SignaledError] if the command exited because of an unhandled signal
|
282
|
+
# @raise [ProcessExecuter::TimeoutError] if the command timed out
|
283
|
+
# @raise [ProcessExecuter::ProcessIOError] if an exception was raised while collecting subprocess output
|
253
284
|
#
|
254
|
-
# @return [ProcessExecuter::
|
285
|
+
# @return [ProcessExecuter::Result] The result of the completed subprocess
|
255
286
|
#
|
256
287
|
def self.run(*command, logger: Logger.new(nil), **options_hash)
|
257
|
-
ProcessExecuter::
|
288
|
+
ProcessExecuter::Runner.new(logger).call(*command, **options_hash)
|
258
289
|
end
|
259
290
|
|
260
291
|
# Wait for process to terminate
|
261
292
|
#
|
262
|
-
# If a
|
293
|
+
# If a `:timeout_after` is specified in options, terminate the process after the
|
294
|
+
# specified number of seconds.
|
263
295
|
#
|
264
296
|
# @param pid [Integer] the process ID
|
265
297
|
# @param options [ProcessExecuter::Options] the options used
|
266
298
|
#
|
267
|
-
# @return [ProcessExecuter::
|
299
|
+
# @return [ProcessExecuter::Result] The result of the completed subprocess
|
268
300
|
#
|
269
301
|
# @api private
|
270
302
|
#
|
271
|
-
private_class_method def self.wait_for_process(pid, options)
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
303
|
+
private_class_method def self.wait_for_process(pid, command, options)
|
304
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
305
|
+
process_status, timed_out = wait_for_process_raw(pid, options.timeout_after)
|
306
|
+
elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
307
|
+
ProcessExecuter::Result.new(process_status, command:, options:, timed_out:, elapsed_time:)
|
308
|
+
end
|
309
|
+
|
310
|
+
# Wait for a process to terminate returning the status and timed out flag
|
311
|
+
#
|
312
|
+
# @param pid [Integer] the process ID
|
313
|
+
# @param timeout_after [Numeric, nil] the number of seconds to wait for the process to terminate
|
314
|
+
# @return [Array<Process::Status, Boolean>] an array containing the process status and a boolean
|
315
|
+
# indicating whether the process timed out
|
316
|
+
# @api private
|
317
|
+
private_class_method def self.wait_for_process_raw(pid, timeout_after)
|
318
|
+
timed_out = false
|
319
|
+
|
320
|
+
process_status =
|
321
|
+
begin
|
322
|
+
Timeout.timeout(timeout_after) { Process.wait2(pid).last }
|
323
|
+
rescue Timeout::Error
|
324
|
+
Process.kill('KILL', pid)
|
325
|
+
timed_out = true
|
326
|
+
Process.wait2(pid).last
|
327
|
+
end
|
328
|
+
|
329
|
+
[process_status, timed_out]
|
278
330
|
end
|
279
331
|
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: process_executer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- James Couball
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date: 2025-
|
10
|
+
date: 2025-03-03 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: bundler-audit
|
@@ -210,7 +209,6 @@ files:
|
|
210
209
|
- ".markdownlint.yml"
|
211
210
|
- ".rspec"
|
212
211
|
- ".rubocop.yml"
|
213
|
-
- ".tool-versions"
|
214
212
|
- ".yardopts"
|
215
213
|
- CHANGELOG.md
|
216
214
|
- Gemfile
|
@@ -218,13 +216,11 @@ files:
|
|
218
216
|
- README.md
|
219
217
|
- Rakefile
|
220
218
|
- lib/process_executer.rb
|
221
|
-
- lib/process_executer/
|
222
|
-
- lib/process_executer/command/errors.rb
|
223
|
-
- lib/process_executer/command/result.rb
|
224
|
-
- lib/process_executer/command/runner.rb
|
219
|
+
- lib/process_executer/errors.rb
|
225
220
|
- lib/process_executer/monitored_pipe.rb
|
226
221
|
- lib/process_executer/options.rb
|
227
|
-
- lib/process_executer/
|
222
|
+
- lib/process_executer/result.rb
|
223
|
+
- lib/process_executer/runner.rb
|
228
224
|
- lib/process_executer/version.rb
|
229
225
|
- package.json
|
230
226
|
- process_executer.gemspec
|
@@ -235,10 +231,9 @@ metadata:
|
|
235
231
|
allowed_push_host: https://rubygems.org
|
236
232
|
homepage_uri: https://github.com/main-branch/process_executer
|
237
233
|
source_code_uri: https://github.com/main-branch/process_executer
|
238
|
-
documentation_uri: https://rubydoc.info/gems/process_executer/
|
239
|
-
changelog_uri: https://rubydoc.info/gems/process_executer/
|
234
|
+
documentation_uri: https://rubydoc.info/gems/process_executer/2.0.0
|
235
|
+
changelog_uri: https://rubydoc.info/gems/process_executer/2.0.0/file/CHANGELOG.md
|
240
236
|
rubygems_mfa_required: 'true'
|
241
|
-
post_install_message:
|
242
237
|
rdoc_options: []
|
243
238
|
require_paths:
|
244
239
|
- lib
|
@@ -255,8 +250,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
255
250
|
requirements:
|
256
251
|
- 'Platform: Mac, Linux, or Windows'
|
257
252
|
- 'Ruby: MRI 3.1 or later, TruffleRuby 24 or later, or JRuby 9.4 or later'
|
258
|
-
rubygems_version: 3.
|
259
|
-
signing_key:
|
253
|
+
rubygems_version: 3.6.2
|
260
254
|
specification_version: 4
|
261
255
|
summary: An API for executing commands in a subprocess
|
262
256
|
test_files: []
|
data/.tool-versions
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
ruby 3.3.5
|
@@ -1,170 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# rubocop:disable Layout/LineLength
|
4
|
-
|
5
|
-
module ProcessExecuter
|
6
|
-
module Command
|
7
|
-
# Base class for all ProcessExecuter::Command errors
|
8
|
-
#
|
9
|
-
# It is recommended to rescue `ProcessExecuter::Command::Error` to catch any
|
10
|
-
# runtime error raised by this gem unless you need more specific error handling.
|
11
|
-
#
|
12
|
-
# Custom errors are arranged in the following class hierarchy:
|
13
|
-
#
|
14
|
-
# ```text
|
15
|
-
# ::StandardError
|
16
|
-
# └─> Error
|
17
|
-
# ├─> CommandError
|
18
|
-
# │ ├─> FailedError
|
19
|
-
# │ └─> SignaledError
|
20
|
-
# │ └─> TimeoutError
|
21
|
-
# └─> ProcessIOError
|
22
|
-
# ```
|
23
|
-
#
|
24
|
-
# | Error Class | Description |
|
25
|
-
# | --- | --- |
|
26
|
-
# | `Error` | This catch-all error serves as the base class for other custom errors. |
|
27
|
-
# | `CommandError` | A subclass of this error is raised when there is a problem executing a command. |
|
28
|
-
# | `FailedError` | Raised when the command exits with a non-zero status code. |
|
29
|
-
# | `SignaledError` | Raised when the command is terminated as a result of receiving a signal. This could happen if the process is forcibly terminated or if there is a serious system error. |
|
30
|
-
# | `TimeoutError` | This is a specific type of `SignaledError` that is raised when the command times out and is killed via the SIGKILL signal. Raised when the operation takes longer than the specified timeout duration (if provided). |
|
31
|
-
# | `ProcessIOError` | Raised when an error was encountered reading or writing to the command's subprocess. |
|
32
|
-
#
|
33
|
-
# @example Rescuing any error
|
34
|
-
# begin
|
35
|
-
# ProcessExecuter.run_command('git', 'status')
|
36
|
-
# rescue ProcessExecuter::Command::Error => e
|
37
|
-
# puts "An error occurred: #{e.message}"
|
38
|
-
# end
|
39
|
-
#
|
40
|
-
# @example Rescuing a timeout error
|
41
|
-
# begin
|
42
|
-
# timeout_duration = 0.1 # seconds
|
43
|
-
# ProcessExecuter.run_command('sleep', '1', timeout: timeout_duration)
|
44
|
-
# rescue ProcessExecuter::TimeoutError => e # Catch the more specific error first!
|
45
|
-
# puts "Command took too long and timed out: #{e}"
|
46
|
-
# rescue ProcessExecuter::Error => e
|
47
|
-
# puts "Some other error occured: #{e}"
|
48
|
-
# end
|
49
|
-
#
|
50
|
-
# @api public
|
51
|
-
#
|
52
|
-
class Error < ::StandardError; end
|
53
|
-
|
54
|
-
# Raised when a command fails or exits because of an uncaught signal
|
55
|
-
#
|
56
|
-
# The command executed, status, stdout, and stderr are available from this
|
57
|
-
# object.
|
58
|
-
#
|
59
|
-
# The Gem will raise a more specific error for each type of failure:
|
60
|
-
#
|
61
|
-
# * {FailedError}: when the command exits with a non-zero status
|
62
|
-
# * {SignaledError}: when the command exits because of an uncaught signal
|
63
|
-
# * {TimeoutError}: when the command times out
|
64
|
-
#
|
65
|
-
# @api public
|
66
|
-
#
|
67
|
-
class CommandError < ProcessExecuter::Command::Error
|
68
|
-
# Create a CommandError object
|
69
|
-
#
|
70
|
-
# @example
|
71
|
-
# `exit 1` # set $? appropriately for this example
|
72
|
-
# result = ProcessExecuter::Command::Result.new(%w[git status], $?, 'stdout', 'stderr')
|
73
|
-
# error = ProcessExecuter::Command::CommandError.new(result)
|
74
|
-
# error.to_s #=> '["git", "status"], status: pid 89784 exit 1, stderr: "stderr"'
|
75
|
-
#
|
76
|
-
# @param result [Result] The result of the command including the command,
|
77
|
-
# status, stdout, and stderr
|
78
|
-
#
|
79
|
-
def initialize(result)
|
80
|
-
@result = result
|
81
|
-
super(error_message)
|
82
|
-
end
|
83
|
-
|
84
|
-
# The human readable representation of this error
|
85
|
-
#
|
86
|
-
# @example
|
87
|
-
# error.error_message #=> '["git", "status"], status: pid 89784 exit 1, stderr: "stderr"'
|
88
|
-
#
|
89
|
-
# @return [String]
|
90
|
-
#
|
91
|
-
def error_message
|
92
|
-
"#{result.command}, status: #{result}, stderr: #{result.stderr_to_s.inspect}"
|
93
|
-
end
|
94
|
-
|
95
|
-
# @attribute [r] result
|
96
|
-
#
|
97
|
-
# The result of the command including the command, its status and its output
|
98
|
-
#
|
99
|
-
# @example
|
100
|
-
# error.result #=> #<ProcessExecuter::Command::Result:0x00007f9b1b8b3d20>
|
101
|
-
#
|
102
|
-
# @return [Result]
|
103
|
-
#
|
104
|
-
attr_reader :result
|
105
|
-
end
|
106
|
-
|
107
|
-
# Raised when the command returns a non-zero exitstatus
|
108
|
-
#
|
109
|
-
# @api public
|
110
|
-
#
|
111
|
-
class FailedError < ProcessExecuter::Command::CommandError; end
|
112
|
-
|
113
|
-
# Raised when the command exits because of an uncaught signal
|
114
|
-
#
|
115
|
-
# @api public
|
116
|
-
#
|
117
|
-
class SignaledError < ProcessExecuter::Command::CommandError; end
|
118
|
-
|
119
|
-
# Raised when the command takes longer than the configured timeout
|
120
|
-
#
|
121
|
-
# @example
|
122
|
-
# result.status.timeout? #=> true
|
123
|
-
#
|
124
|
-
# @api public
|
125
|
-
#
|
126
|
-
class TimeoutError < ProcessExecuter::Command::SignaledError
|
127
|
-
# Create a TimeoutError object
|
128
|
-
#
|
129
|
-
# @example
|
130
|
-
# command = %w[sleep 10]
|
131
|
-
# timeout_duration = 1
|
132
|
-
# status = ProcessExecuter.spawn(*command, timeout: timeout_duration)
|
133
|
-
# result = Result.new(command, status, 'stdout', 'err output')
|
134
|
-
# error = TimeoutError.new(result, timeout_duration)
|
135
|
-
# error.error_message
|
136
|
-
# #=> '["sleep", "10"], status: pid 70144 SIGKILL (signal 9), stderr: "err output", timed out after 1s'
|
137
|
-
#
|
138
|
-
# @param result [Result] The result of the command including the git command,
|
139
|
-
# status, stdout, and stderr
|
140
|
-
#
|
141
|
-
# @param timeout_duration [Numeric] The duration the subprocess was allowed
|
142
|
-
# to run before being terminated
|
143
|
-
#
|
144
|
-
def initialize(result, timeout_duration)
|
145
|
-
@timeout_duration = timeout_duration
|
146
|
-
super(result)
|
147
|
-
end
|
148
|
-
|
149
|
-
# The amount of time the subprocess was allowed to run before being killed
|
150
|
-
#
|
151
|
-
# @example
|
152
|
-
# `kill -9 $$` # set $? appropriately for this example
|
153
|
-
# result = Result.new(%w[git status], $?, '', "killed")
|
154
|
-
# error = TimeoutError.new(result, 10)
|
155
|
-
# error.timeout_duration #=> 10
|
156
|
-
#
|
157
|
-
# @return [Numeric]
|
158
|
-
#
|
159
|
-
attr_reader :timeout_duration
|
160
|
-
end
|
161
|
-
|
162
|
-
# Raised when the output of a command can not be read
|
163
|
-
#
|
164
|
-
# @api public
|
165
|
-
#
|
166
|
-
class ProcessIOError < ProcessExecuter::Command::Error; end
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
# rubocop:enable Layout/LineLength
|