process_executer 1.3.0 → 3.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +171 -61
  4. data/lib/process_executer/destination_base.rb +83 -0
  5. data/lib/process_executer/destinations/child_redirection.rb +23 -0
  6. data/lib/process_executer/destinations/close.rb +23 -0
  7. data/lib/process_executer/destinations/file_descriptor.rb +36 -0
  8. data/lib/process_executer/destinations/file_path.rb +56 -0
  9. data/lib/process_executer/destinations/file_path_mode.rb +60 -0
  10. data/lib/process_executer/destinations/file_path_mode_perms.rb +61 -0
  11. data/lib/process_executer/destinations/io.rb +33 -0
  12. data/lib/process_executer/destinations/monitored_pipe.rb +39 -0
  13. data/lib/process_executer/destinations/stderr.rb +31 -0
  14. data/lib/process_executer/destinations/stdout.rb +31 -0
  15. data/lib/process_executer/destinations/tee.rb +60 -0
  16. data/lib/process_executer/destinations/writer.rb +33 -0
  17. data/lib/process_executer/destinations.rb +70 -0
  18. data/lib/process_executer/errors.rb +134 -0
  19. data/lib/process_executer/monitored_pipe.rb +40 -57
  20. data/lib/process_executer/options/base.rb +240 -0
  21. data/lib/process_executer/options/option_definition.rb +56 -0
  22. data/lib/process_executer/options/run_options.rb +48 -0
  23. data/lib/process_executer/options/spawn_and_wait_options.rb +39 -0
  24. data/lib/process_executer/options/spawn_options.rb +143 -0
  25. data/lib/process_executer/options.rb +7 -163
  26. data/lib/process_executer/result.rb +150 -0
  27. data/lib/process_executer/runner.rb +155 -0
  28. data/lib/process_executer/version.rb +1 -1
  29. data/lib/process_executer.rb +254 -93
  30. metadata +27 -14
  31. data/.tool-versions +0 -1
  32. data/lib/process_executer/command/errors.rb +0 -170
  33. data/lib/process_executer/command/result.rb +0 -77
  34. data/lib/process_executer/command/runner.rb +0 -167
  35. data/lib/process_executer/command.rb +0 -12
  36. data/lib/process_executer/status.rb +0 -70
@@ -1,98 +1,140 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'process_executer/monitored_pipe'
4
- require 'process_executer/options'
5
- require 'process_executer/command'
6
- require 'process_executer/status'
7
-
8
3
  require 'logger'
9
4
  require 'timeout'
10
5
 
6
+ require 'process_executer/destination_base'
7
+ require 'process_executer/destinations'
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
+
11
14
  # The `ProcessExecuter` module provides methods to execute subprocess commands
12
15
  # with enhanced features such as output capture, timeout handling, and custom
13
16
  # environment variables.
14
17
  #
15
18
  # 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.
19
+ #
20
+ # * {run}: Executes a command and returns the result which includes the process
21
+ # status and output
22
+ # * {spawn_and_wait}: a thin wrapper around `Process.spawn` that blocks until the
23
+ # command finishes
18
24
  #
19
25
  # Features:
26
+ #
20
27
  # * Supports executing commands via a shell or directly.
21
28
  # * Captures stdout and stderr to buffers, files, or custom objects.
22
29
  # * Optionally enforces timeouts and terminates long-running commands.
23
- # * Provides detailed status information, including success, failure, or timeout states.
30
+ # * Provides detailed status information, including the command that was run, the
31
+ # options that were given, and success, failure, or timeout states.
24
32
  #
25
33
  # @api public
26
- #
27
34
  module ProcessExecuter
28
- # Execute the given command as a subprocess and return the exit status
35
+ # Run a command in a subprocess, wait for it to finish, then return the result
29
36
  #
30
- # This is a convenience method that calls
37
+ # This method is a thin wrapper around
31
38
  # [Process.spawn](https://docs.ruby-lang.org/en/3.3/Process.html#method-c-spawn)
32
39
  # and blocks until the command terminates.
33
40
  #
34
- # The command will be sent the SIGKILL signal if it does not terminate within
35
- # the specified timeout.
41
+ # A timeout may be specified with the `:timeout_after` option. The command will be
42
+ # sent the SIGKILL signal if it does not terminate within the specified timeout.
36
43
  #
37
44
  # @example
38
- # status = ProcessExecuter.spawn('echo hello')
39
- # status.exited? # => true
40
- # status.success? # => true
41
- # status.timeout? # => false
45
+ # result = ProcessExecuter.spawn_and_wait('echo hello')
46
+ # result.exited? # => true
47
+ # result.success? # => true
48
+ # result.timed_out? # => false
42
49
  #
43
50
  # @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
51
+ # result = ProcessExecuter.spawn_and_wait('sleep 10', timeout_after: 0.01)
52
+ # result.exited? # => false
53
+ # result.success? # => nil
54
+ # result.signaled? # => true
55
+ # result.termsig # => 9
56
+ # result.timed_out? # => true
50
57
  #
51
58
  # @example capturing stdout to a string
52
- # stdout = StringIO.new
53
- # status = ProcessExecuter.spawn('echo hello', out: stdout)
54
- # stdout.string # => "hello"
59
+ # stdout_buffer = StringIO.new
60
+ # stdout_pipe = ProcessExecuter::MonitoredPipe.new(stdout_buffer)
61
+ # result = ProcessExecuter.spawn_and_wait('echo hello', out: stdout_pipe)
62
+ # stdout_buffer.string # => "hello\n"
55
63
  #
56
64
  # @see https://ruby-doc.org/core-3.1.2/Kernel.html#method-i-spawn Kernel.spawn
57
65
  # documentation for valid command and options
58
66
  #
59
- # @see ProcessExecuter::Options#initialize See ProcessExecuter::Options#initialize
60
- # for options that may be specified
67
+ # @see ProcessExecuter::Options#initialize ProcessExecuter::Options#initialize for
68
+ # options that may be specified
61
69
  #
62
70
  # @param command [Array<String>] The command to execute
63
71
  # @param options_hash [Hash] The options to use when executing the command
64
72
  #
65
- # @return [Process::Status] the exit status of the process
73
+ # @return [ProcessExecuter::Result] The result of the completed subprocess
66
74
  #
67
- def self.spawn(*command, **options_hash)
68
- options = ProcessExecuter::Options.new(**options_hash)
75
+ def self.spawn_and_wait(*command, **options_hash)
76
+ options = ProcessExecuter.spawn_and_wait_options(options_hash)
77
+ spawn_and_wait_with_options(command, options)
78
+ end
79
+
80
+ # Run a command in a subprocess, wait for it to finish, then return the result
81
+ #
82
+ # @see ProcessExecuter.spawn_and_wait for full documentation
83
+ #
84
+ # @param command [Array<String>] The command to run
85
+ # @param options [ProcessExecuter::Options::SpawnAndWaitOptions] The options to use when running the command
86
+ #
87
+ # @return [ProcessExecuter::Result] The result of the completed subprocess
88
+ # @api private
89
+ def self.spawn_and_wait_with_options(command, options)
69
90
  pid = Process.spawn(*command, **options.spawn_options)
70
- wait_for_process(pid, options)
91
+ wait_for_process(pid, command, options)
71
92
  end
72
93
 
73
- # Execute the given command as a subprocess, blocking until it finishes
94
+ # Execute the given command as a subprocess blocking until it finishes
95
+ #
96
+ # Works just like {ProcessExecuter.spawn}, but does the following in addition:
97
+ #
98
+ # 1. If nothing is specified for `out`, stdout is captured to a `StringIO` object
99
+ # which can be accessed via the Result object in `result.options.out`. The
100
+ # same applies to `err`.
74
101
  #
75
- # Returns a result object which includes the process's status and output.
102
+ # 2. `out` and `err` are automatically wrapped in a
103
+ # `ProcessExecuter::MonitoredPipe` object so that any object that implements
104
+ # `#write` (or an Array of such objects) can be given for `out` and `err`.
76
105
  #
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:
106
+ # 3. Raises one of the following errors unless `raise_errors` is explicitly set
107
+ # to `false`:
80
108
  #
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
109
+ # * `ProcessExecuter::FailedError` if the command returns a non-zero
110
+ # exitstatus
111
+ # * `ProcessExecuter::SignaledError` if the command exits because of
112
+ # an unhandled signal
113
+ # * `ProcessExecuter::TimeoutError` if the command times out
114
+ #
115
+ # If `raise_errors` is false, the returned Result object will contain the error.
116
+ #
117
+ # 4. Raises a `ProcessExecuter::ProcessIOError` if an exception is raised
118
+ # while collecting subprocess output. This can not be turned off.
119
+ #
120
+ # 5. If a `logger` is provided, it will be used to log:
121
+ #
122
+ # * The command that was executed and its status to `info` level
123
+ # * The stdout and stderr output to `debug` level
124
+ #
125
+ # By default, Logger.new(nil) is used for the logger.
84
126
  #
85
127
  # This method takes two forms:
86
128
  #
87
129
  # 1. The command is executed via a shell when the command is given as a single
88
130
  # string:
89
131
  #
90
- # `ProcessExecuter.run([env, ] command_line, options = {}) ->` {ProcessExecuter::Command::Result}
132
+ # `ProcessExecuter.run([env, ] command_line, options = {}) ->` {ProcessExecuter::Result}
91
133
  #
92
134
  # 2. The command is executed directly (bypassing the shell) when the command and it
93
135
  # arguments are given as an array of strings:
94
136
  #
95
- # `ProcessExecuter.run([env, ] exe_path, *args, options = {}) ->` {ProcessExecuter::Command::Result}
137
+ # `ProcessExecuter.run([env, ] exe_path, *args, options = {}) ->` {ProcessExecuter::Result}
96
138
  #
97
139
  # Optional argument `env` is a hash that affects ENV for the new process; see
98
140
  # [Execution
@@ -102,11 +144,11 @@ module ProcessExecuter
102
144
  #
103
145
  # @example Run a command given as a single string (uses shell)
104
146
  # # The command must be properly shell escaped when passed as a single string.
105
- # command = 'echo "stdout: `pwd`"" && echo "stderr: $HOME" 1>&2'
147
+ # command = 'echo "stdout: `pwd`" && echo "stderr: $HOME" 1>&2'
106
148
  # result = ProcessExecuter.run(command)
107
149
  # result.success? #=> true
108
- # result.stdout.string #=> "stdout: /Users/james/projects/main-branch/process_executer\n"
109
- # result.stderr.string #=> "stderr: /Users/james\n"
150
+ # result.stdout #=> "stdout: /Users/james/projects/main-branch/process_executer\n"
151
+ # result.stderr #=> "stderr: /Users/james\n"
110
152
  #
111
153
  # @example Run a command given as an array of strings (does not use shell)
112
154
  # # The command and its args must be provided as separate strings in the array.
@@ -114,67 +156,65 @@ module ProcessExecuter
114
156
  # command = ['git', 'clone', 'https://github.com/main-branch/process_executer']
115
157
  # result = ProcessExecuter.run(*command)
116
158
  # result.success? #=> true
117
- # result.stdout.string #=> ""
118
- # result.stderr.string #=> "Cloning into 'process_executer'...\n"
159
+ # result.stdout #=> ""
160
+ # result.stderr #=> "Cloning into 'process_executer'...\n"
119
161
  #
120
162
  # @example Run a command with a timeout
121
163
  # command = ['sleep', '1']
122
- # result = ProcessExecuter.run(*command, timeout: 0.01)
123
- # #=> raises ProcessExecuter::Command::TimeoutError which contains the command result
164
+ # result = ProcessExecuter.run(*command, timeout_after: 0.01)
165
+ # #=> raises ProcessExecuter::TimeoutError which contains the command result
124
166
  #
125
167
  # @example Run a command which fails
126
168
  # command = ['exit 1']
127
169
  # result = ProcessExecuter.run(*command)
128
- # #=> raises ProcessExecuter::Command::FailedError which contains the command result
170
+ # #=> raises ProcessExecuter::FailedError which contains the command result
129
171
  #
130
172
  # @example Run a command which exits due to an unhandled signal
131
173
  # command = ['kill -9 $$']
132
174
  # result = ProcessExecuter.run(*command)
133
- # #=> raises ProcessExecuter::Command::SignaledError which contains the command result
175
+ # #=> raises ProcessExecuter::SignaledError which contains the command result
134
176
  #
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.
177
+ # @example Do not raise an error when the command fails
138
178
  # command = ['echo "Some error" 1>&2 && exit 1']
139
179
  # result = ProcessExecuter.run(*command, raise_errors: false)
140
- # # An error is not raised
141
180
  # result.success? #=> false
142
181
  # result.exitstatus #=> 1
143
- # result.stdout.string #=> ""
144
- # result.stderr.string #=> "Some error\n"
182
+ # result.stdout #=> ""
183
+ # result.stderr #=> "Some error\n"
145
184
  #
146
185
  # @example Set environment variables
147
186
  # env = { 'FOO' => 'foo', 'BAR' => 'bar' }
148
187
  # command = 'echo "$FOO$BAR"'
149
188
  # result = ProcessExecuter.run(env, *command)
150
- # result.stdout.string #=> "foobar\n"
189
+ # result.stdout #=> "foobar\n"
151
190
  #
152
191
  # @example Set environment variables when using a command array
153
- # env = { 'GIT_DIR' => '/path/to/.git' }
154
- # command = ['git', 'status']
192
+ # env = { 'FOO' => 'foo', 'BAR' => 'bar' }
193
+ # command = ['ruby', '-e', 'puts ENV["FOO"] + ENV["BAR"]']
155
194
  # result = ProcessExecuter.run(env, *command)
156
- # result.stdout.string #=> "On branch main\nYour branch is ..."
195
+ # result.stdout #=> "foobar\n"
157
196
  #
158
197
  # @example Unset environment variables
159
- # env = { 'GIT_DIR' => nil } # setting to nil unsets the variable in the environment
160
- # command = ['git', 'status']
198
+ # env = { 'FOO' => nil } # setting to nil unsets the variable in the environment
199
+ # command = ['echo "FOO: $FOO"']
161
200
  # result = ProcessExecuter.run(env, *command)
162
- # result.stdout.string #=> "On branch main\nYour branch is ..."
201
+ # result.stdout #=> "FOO: \n"
163
202
  #
164
203
  # @example Reset existing environment variables and add new ones
165
204
  # env = { 'PATH' => '/bin' }
166
205
  # result = ProcessExecuter.run(env, 'echo "Home: $HOME" && echo "Path: $PATH"', unsetenv_others: true)
167
- # result.stdout.string #=> "Home: \n/Path: /bin\n"
206
+ # result.stdout #=> "Home: \n/Path: /bin\n"
168
207
  #
169
208
  # @example Run command in a different directory
170
209
  # command = ['pwd']
171
210
  # result = ProcessExecuter.run(*command, chdir: '/tmp')
172
- # result.stdout.string #=> "/tmp\n"
211
+ # result.stdout #=> "/tmp\n"
173
212
  #
174
213
  # @example Capture stdout and stderr into a single buffer
175
214
  # command = ['echo "stdout" && echo "stderr" 1>&2']
176
- # result = ProcessExecuter.run(*command, merge: true)
177
- # result.stdout.string #=> "stdout\nstderr\n"
215
+ # result = ProcessExecuter.run(*command, [out:, err:]: StringIO.new)
216
+ # result.stdout #=> "stdout\nstderr\n"
217
+ # result.stderr #=> "stdout\nstderr\n"
178
218
  # result.stdout.object_id == result.stderr.object_id #=> true
179
219
  #
180
220
  # @example Capture to an explicit buffer
@@ -184,29 +224,29 @@ module ProcessExecuter
184
224
  # result = ProcessExecuter.run(*command, out: out, err: err)
185
225
  # out.string #=> "stdout\n"
186
226
  # err.string #=> "stderr\n"
187
- # result.stdout.object_id == out.object_id #=> true
188
- # result.stderr.object_id == err.object_id #=> true
189
227
  #
190
228
  # @example Capture to a file
191
229
  # # Same technique can be used for stderr
192
230
  # out = File.open('stdout.txt', 'w')
231
+ # err = StringIO.new
193
232
  # command = ['echo "stdout" && echo "stderr" 1>&2']
194
233
  # result = ProcessExecuter.run(*command, out: out, err: err)
195
234
  # out.close
196
235
  # File.read('stdout.txt') #=> "stdout\n"
197
236
  # # stderr is still captured to a StringIO buffer internally
198
- # result.stderr.string #=> "stderr\n"
237
+ # result.stderr #=> "stderr\n"
199
238
  #
200
- # @example Capture to multiple writers (e.g. files, buffers, STDOUT, etc.)
239
+ # @example Capture to multiple destinations (e.g. files, buffers, STDOUT, etc.)
201
240
  # # Same technique can be used for stderr
202
241
  # out_buffer = StringIO.new
203
242
  # out_file = File.open('stdout.txt', 'w')
204
243
  # command = ['echo "stdout" && echo "stderr" 1>&2']
205
- # result = ProcessExecuter.run(*command, out: [out_buffer, out_file])
244
+ # result = ProcessExecuter.run(*command, out: [:tee, out_buffer, out_file])
206
245
  # # You must manage closing resources you create yourself
207
246
  # out_file.close
208
247
  # out_buffer.string #=> "stdout\n"
209
248
  # File.read('stdout.txt') #=> "stdout\n"
249
+ # result.stdout #=> "stdout\n"
210
250
  #
211
251
  # @param command [Array<String>] The command to run
212
252
  #
@@ -223,18 +263,18 @@ module ProcessExecuter
223
263
  # Otherwise, the command is run bypassing the shell. When bypassing the shell, shell expansions
224
264
  # and redirections are not supported.
225
265
  #
226
- # @param logger [Logger] The logger to use
227
266
  # @param options_hash [Hash] Additional options
228
- # @option options_hash [Numeric] :timeout The maximum seconds to wait for the command to complete
267
+ # @option options_hash [Numeric] :timeout_after The maximum seconds to wait for the
268
+ # command to complete
229
269
  #
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.
270
+ # If zero or nil, the command will not time out. If the command
271
+ # times out, it is killed via a SIGKILL signal. A {ProcessExecuter::TimeoutError}
272
+ # will be raised if the `:raise_errors` option is true.
232
273
  #
233
274
  # If the command does not exit when receiving the SIGKILL signal, this method may hang indefinitely.
234
275
  #
235
276
  # @option options_hash [#write] :out (nil) The object to write stdout to
236
277
  # @option options_hash [#write] :err (nil) The object to write stderr to
237
- # @option options_hash [Boolean] :merge (false) If true, stdout and stderr are written to the same capture buffer
238
278
  # @option options_hash [Boolean] :raise_errors (true) Raise an exception if the command fails
239
279
  # @option options_hash [Boolean] :unsetenv_others (false) If true, unset all environment variables before
240
280
  # applying the new ones
@@ -245,35 +285,156 @@ module ProcessExecuter
245
285
  # @option options_hash [Integer] :umask (nil) Set the umask (see File.umask)
246
286
  # @option options_hash [Boolean] :close_others (false) If true, close non-standard file descriptors
247
287
  # @option options_hash [String] :chdir (nil) The directory to run the command in
288
+ # @option options_hash [Logger] :logger The logger to use
248
289
  #
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
290
+ # @raise [ProcessExecuter::FailedError] if the command returned a non-zero exit status
291
+ # @raise [ProcessExecuter::SignaledError] if the command exited because of an unhandled signal
292
+ # @raise [ProcessExecuter::TimeoutError] if the command timed out
293
+ # @raise [ProcessExecuter::ProcessIOError] if an exception was raised while collecting subprocess output
253
294
  #
254
- # @return [ProcessExecuter::Command::Result] A result object containing the process status and captured output
295
+ # @return [ProcessExecuter::Result] The result of the completed subprocess
255
296
  #
256
- def self.run(*command, logger: Logger.new(nil), **options_hash)
257
- ProcessExecuter::Command::Runner.new(logger).call(*command, **options_hash)
297
+ def self.run(*command, **options_hash)
298
+ options = ProcessExecuter.run_options(options_hash)
299
+ run_with_options(command, options)
300
+ end
301
+
302
+ # Run a command with the given options
303
+ #
304
+ # @see ProcessExecuter.run for full documentation
305
+ #
306
+ # @param command [Array<String>] The command to run
307
+ # @param options [ProcessExecuter::Options::RunOptions] The options to use when running the command
308
+ #
309
+ # @return [ProcessExecuter::Result] The result of the completed subprocess
310
+ #
311
+ # @api private
312
+ def self.run_with_options(command, options)
313
+ ProcessExecuter::Runner.new.call(command, options)
258
314
  end
259
315
 
260
316
  # Wait for process to terminate
261
317
  #
262
- # If a timeout is specified in options, terminate the process after options.timeout seconds.
318
+ # If a `:timeout_after` is specified in options, terminate the process after the
319
+ # specified number of seconds.
263
320
  #
264
321
  # @param pid [Integer] the process ID
265
322
  # @param options [ProcessExecuter::Options] the options used
266
323
  #
267
- # @return [ProcessExecuter::Status] the process status including Process::Status attributes and a timeout flag
324
+ # @return [ProcessExecuter::Result] The result of the completed subprocess
268
325
  #
269
326
  # @api private
270
327
  #
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)
328
+ private_class_method def self.wait_for_process(pid, command, options)
329
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
330
+ process_status, timed_out = wait_for_process_raw(pid, options.timeout_after)
331
+ elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
332
+ ProcessExecuter::Result.new(process_status, command:, options:, timed_out:, elapsed_time:)
333
+ end
334
+
335
+ # Wait for a process to terminate returning the status and timed out flag
336
+ #
337
+ # @param pid [Integer] the process ID
338
+ # @param timeout_after [Numeric, nil] the number of seconds to wait for the process to terminate
339
+ # @return [Array<Process::Status, Boolean>] an array containing the process status and a boolean
340
+ # indicating whether the process timed out
341
+ # @api private
342
+ private_class_method def self.wait_for_process_raw(pid, timeout_after)
343
+ timed_out = false
344
+
345
+ process_status =
346
+ begin
347
+ Timeout.timeout(timeout_after) { Process.wait2(pid).last }
348
+ rescue Timeout::Error
349
+ Process.kill('KILL', pid)
350
+ timed_out = true
351
+ Process.wait2(pid).last
352
+ end
353
+
354
+ [process_status, timed_out]
355
+ end
356
+
357
+ # Convert a hash to a SpawnOptions object
358
+ #
359
+ # @example
360
+ # options_hash = { out: $stdout }
361
+ # options = ProcessExecuter.spawn_options(options_hash) # =>
362
+ # #<ProcessExecuter::Options::SpawnOptions:0x00007f8f9b0b3d20 out: $stdout>
363
+ # ProcessExecuter.spawn_options(options) # =>
364
+ # #<ProcessExecuter::Options::SpawnOptions:0x00007f8f9b0b3d20 out: $stdout>
365
+ #
366
+ # @param obj [Hash, SpawnOptions] the object to be converted
367
+ #
368
+ # @return [SpawnOptions]
369
+ #
370
+ # @raise [ArgumentError] if obj is not a Hash or SpawnOptions
371
+ #
372
+ # @api public
373
+ #
374
+ def self.spawn_options(obj)
375
+ case obj
376
+ when ProcessExecuter::Options::SpawnOptions
377
+ obj
378
+ when Hash
379
+ ProcessExecuter::Options::SpawnOptions.new(**obj)
380
+ else
381
+ raise ArgumentError, "Expected a Hash or ProcessExecuter::Options::SpawnOptions but got a #{obj.class}"
382
+ end
383
+ end
384
+
385
+ # Convert a hash to a SpawnAndWaitOptions object
386
+ #
387
+ # @example
388
+ # options_hash = { out: $stdout }
389
+ # options = ProcessExecuter.spawn_and_wait_options(options_hash) # =>
390
+ # #<ProcessExecuter::Options::SpawnAndWaitOptions:0x00007f8f9b0b3d20 out: $stdout>
391
+ # ProcessExecuter.spawn_and_wait_options(options) # =>
392
+ # #<ProcessExecuter::Options::SpawnAndWaitOptions:0x00007f8f9b0b3d20 out: $stdout>
393
+ #
394
+ # @param obj [Hash, SpawnAndWaitOptions] the object to be converted
395
+ #
396
+ # @return [SpawnAndWaitOptions]
397
+ #
398
+ # @raise [ArgumentError] if obj is not a Hash or SpawnOptions
399
+ #
400
+ # @api public
401
+ #
402
+ def self.spawn_and_wait_options(obj)
403
+ case obj
404
+ when ProcessExecuter::Options::SpawnAndWaitOptions
405
+ obj
406
+ when Hash
407
+ ProcessExecuter::Options::SpawnAndWaitOptions.new(**obj)
408
+ else
409
+ raise ArgumentError, "Expected a Hash or ProcessExecuter::Options::SpawnAndWaitOptions but got a #{obj.class}"
410
+ end
411
+ end
412
+
413
+ # Convert a hash to a RunOptions object
414
+ #
415
+ # @example
416
+ # options_hash = { out: $stdout }
417
+ # options = ProcessExecuter.run_options(options_hash) # =>
418
+ # #<ProcessExecuter::Options::RunOptions:0x00007f8f9b0b3d20 out: $stdout>
419
+ # ProcessExecuter.run_options(options) # =>
420
+ # #<ProcessExecuter::Options::RunOptions:0x00007f8f9b0b3d20 out: $stdout>
421
+ #
422
+ # @param obj [Hash, RunOptions] the object to be converted
423
+ #
424
+ # @return [RunOptions]
425
+ #
426
+ # @raise [ArgumentError] if obj is not a Hash or SpawnOptions
427
+ #
428
+ # @api public
429
+ #
430
+ def self.run_options(obj)
431
+ case obj
432
+ when ProcessExecuter::Options::RunOptions
433
+ obj
434
+ when Hash
435
+ ProcessExecuter::Options::RunOptions.new(**obj)
436
+ else
437
+ raise ArgumentError, "Expected a Hash or ProcessExecuter::Options::RunOptions but got a #{obj.class}"
274
438
  end
275
- rescue Timeout::Error
276
- Process.kill('KILL', pid)
277
- ProcessExecuter::Status.new(Process.wait2(pid).last, true, options.timeout)
278
439
  end
279
440
  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: 3.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-18 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,30 @@ 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/destination_base.rb
220
+ - lib/process_executer/destinations.rb
221
+ - lib/process_executer/destinations/child_redirection.rb
222
+ - lib/process_executer/destinations/close.rb
223
+ - lib/process_executer/destinations/file_descriptor.rb
224
+ - lib/process_executer/destinations/file_path.rb
225
+ - lib/process_executer/destinations/file_path_mode.rb
226
+ - lib/process_executer/destinations/file_path_mode_perms.rb
227
+ - lib/process_executer/destinations/io.rb
228
+ - lib/process_executer/destinations/monitored_pipe.rb
229
+ - lib/process_executer/destinations/stderr.rb
230
+ - lib/process_executer/destinations/stdout.rb
231
+ - lib/process_executer/destinations/tee.rb
232
+ - lib/process_executer/destinations/writer.rb
233
+ - lib/process_executer/errors.rb
225
234
  - lib/process_executer/monitored_pipe.rb
226
235
  - lib/process_executer/options.rb
227
- - lib/process_executer/status.rb
236
+ - lib/process_executer/options/base.rb
237
+ - lib/process_executer/options/option_definition.rb
238
+ - lib/process_executer/options/run_options.rb
239
+ - lib/process_executer/options/spawn_and_wait_options.rb
240
+ - lib/process_executer/options/spawn_options.rb
241
+ - lib/process_executer/result.rb
242
+ - lib/process_executer/runner.rb
228
243
  - lib/process_executer/version.rb
229
244
  - package.json
230
245
  - process_executer.gemspec
@@ -235,10 +250,9 @@ metadata:
235
250
  allowed_push_host: https://rubygems.org
236
251
  homepage_uri: https://github.com/main-branch/process_executer
237
252
  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
253
+ documentation_uri: https://rubydoc.info/gems/process_executer/3.0.0
254
+ changelog_uri: https://rubydoc.info/gems/process_executer/3.0.0/file/CHANGELOG.md
240
255
  rubygems_mfa_required: 'true'
241
- post_install_message:
242
256
  rdoc_options: []
243
257
  require_paths:
244
258
  - lib
@@ -255,8 +269,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
255
269
  requirements:
256
270
  - 'Platform: Mac, Linux, or Windows'
257
271
  - '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:
272
+ rubygems_version: 3.6.2
260
273
  specification_version: 4
261
274
  summary: An API for executing commands in a subprocess
262
275
  test_files: []
data/.tool-versions DELETED
@@ -1 +0,0 @@
1
- ruby 3.3.5