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.
@@ -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/command'
6
- require 'process_executer/status'
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 captures its output and status in a result object.
17
- # * {spawn}: Executes a command and returns its exit status.
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 success, failure, or timeout states.
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
- # Execute the given command as a subprocess and return the exit status
32
+ # Run a command in a subprocess, wait for it to finish, then return the result
29
33
  #
30
- # This is a convenience method that calls
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
- # The command will be sent the SIGKILL signal if it does not terminate within
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
- # status = ProcessExecuter.spawn('echo hello')
39
- # status.exited? # => true
40
- # status.success? # => true
41
- # status.timeout? # => false
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
- # status = ProcessExecuter.spawn('sleep 10', timeout: 0.01)
45
- # status.exited? # => false
46
- # status.success? # => nil
47
- # status.signaled? # => true
48
- # status.termsig # => 9
49
- # status.timeout? # => true
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
- # stdout = StringIO.new
53
- # status = ProcessExecuter.spawn('echo hello', out: stdout)
54
- # stdout.string # => "hello"
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 See ProcessExecuter::Options#initialize
60
- # for options that may be specified
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 [Process::Status] the exit status of the process
70
+ # @return [ProcessExecuter::Result] The result of the completed subprocess
66
71
  #
67
- def self.spawn(*command, **options_hash)
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, blocking until it finishes
78
+ # Execute the given command as a subprocess blocking until it finishes
74
79
  #
75
- # Returns a result object which includes the process's status and output.
80
+ # Works just like {ProcessExecuter.spawn}, but does the following in addition:
76
81
  #
77
- # Supports the same features as
78
- # [Process.spawn](https://docs.ruby-lang.org/en/3.3/Process.html#method-c-spawn).
79
- # In addition, it:
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
- # 1. Blocks until the command exits
82
- # 2. Captures stdout and stderr to a buffer or file
83
- # 3. Optionally kills the command if it exceeds a timeout
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::Command::Result}
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::Command::Result}
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`"" && echo "stderr: $HOME" 1>&2'
134
+ # command = 'echo "stdout: `pwd`" && echo "stderr: $HOME" 1>&2'
106
135
  # result = ProcessExecuter.run(command)
107
136
  # result.success? #=> true
108
- # result.stdout.string #=> "stdout: /Users/james/projects/main-branch/process_executer\n"
109
- # result.stderr.string #=> "stderr: /Users/james\n"
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.string #=> ""
118
- # result.stderr.string #=> "Cloning into 'process_executer'...\n"
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, timeout: 0.01)
123
- # #=> raises ProcessExecuter::Command::TimeoutError which contains the command result
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::Command::FailedError which contains the command result
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::Command::SignaledError which contains the command result
162
+ # #=> raises ProcessExecuter::SignaledError which contains the command result
134
163
  #
135
- # @example Return a result instead of raising an error when `raise_errors` is `false`
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.string #=> ""
144
- # result.stderr.string #=> "Some error\n"
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.string #=> "foobar\n"
176
+ # result.stdout #=> "foobar\n"
151
177
  #
152
178
  # @example Set environment variables when using a command array
153
- # env = { 'GIT_DIR' => '/path/to/.git' }
154
- # command = ['git', 'status']
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.string #=> "On branch main\nYour branch is ..."
182
+ # result.stdout #=> "foobar\n"
157
183
  #
158
184
  # @example Unset environment variables
159
- # env = { 'GIT_DIR' => nil } # setting to nil unsets the variable in the environment
160
- # command = ['git', 'status']
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.string #=> "On branch main\nYour branch is ..."
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.string #=> "Home: \n/Path: /bin\n"
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.string #=> "/tmp\n"
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.string #=> "stdout\nstderr\n"
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.string #=> "stderr\n"
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] :timeout The maximum seconds to wait for the command to complete
257
+ # @option options_hash [Numeric] :timeout_after The maximum seconds to wait for the
258
+ # command to complete
229
259
  #
230
- # If timeout is zero or nil, the command will not time out. If the command
231
- # times out, it is killed via a SIGKILL signal and {ProcessExecuter::Command::TimeoutError} is raised.
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::Command::FailedError] if the command returned a non-zero exit status
250
- # @raise [ProcessExecuter::Command::SignaledError] if the command exited because of an unhandled signal
251
- # @raise [ProcessExecuter::Command::TimeoutError] if the command timed out
252
- # @raise [ProcessExecuter::Command::ProcessIOError] if an exception was raised while collecting subprocess output
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::Command::Result] A result object containing the process status and captured output
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::Command::Runner.new(logger).call(*command, **options_hash)
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 timeout is specified in options, terminate the process after options.timeout seconds.
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::Status] the process status including Process::Status attributes and a timeout flag
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
- Timeout.timeout(options.timeout) do
273
- ProcessExecuter::Status.new(Process.wait2(pid).last, false, options.timeout)
274
- end
275
- rescue Timeout::Error
276
- Process.kill('KILL', pid)
277
- ProcessExecuter::Status.new(Process.wait2(pid).last, true, options.timeout)
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: 1.3.0
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-02-27 00:00:00.000000000 Z
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/command.rb
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/status.rb
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/1.3.0
239
- changelog_uri: https://rubydoc.info/gems/process_executer/1.3.0/file/CHANGELOG.md
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.5.16
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