process_executer 2.0.0 → 3.1.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 +32 -0
- data/README.md +159 -46
- data/lib/process_executer/destination_base.rb +83 -0
- data/lib/process_executer/destinations/child_redirection.rb +23 -0
- data/lib/process_executer/destinations/close.rb +23 -0
- data/lib/process_executer/destinations/file_descriptor.rb +36 -0
- data/lib/process_executer/destinations/file_path.rb +56 -0
- data/lib/process_executer/destinations/file_path_mode.rb +60 -0
- data/lib/process_executer/destinations/file_path_mode_perms.rb +61 -0
- data/lib/process_executer/destinations/io.rb +33 -0
- data/lib/process_executer/destinations/monitored_pipe.rb +39 -0
- data/lib/process_executer/destinations/stderr.rb +31 -0
- data/lib/process_executer/destinations/stdout.rb +31 -0
- data/lib/process_executer/destinations/tee.rb +60 -0
- data/lib/process_executer/destinations/writer.rb +33 -0
- data/lib/process_executer/destinations.rb +70 -0
- data/lib/process_executer/errors.rb +11 -1
- data/lib/process_executer/monitored_pipe.rb +40 -57
- data/lib/process_executer/options/base.rb +240 -0
- data/lib/process_executer/options/option_definition.rb +56 -0
- data/lib/process_executer/options/run_options.rb +48 -0
- data/lib/process_executer/options/spawn_and_wait_options.rb +39 -0
- data/lib/process_executer/options/spawn_options.rb +143 -0
- data/lib/process_executer/options.rb +7 -166
- data/lib/process_executer/result.rb +13 -23
- data/lib/process_executer/runner.rb +60 -56
- data/lib/process_executer/version.rb +1 -1
- data/lib/process_executer.rb +136 -26
- metadata +23 -4
data/lib/process_executer.rb
CHANGED
@@ -1,25 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'logger'
|
4
|
+
require 'timeout'
|
5
|
+
|
6
|
+
require 'process_executer/destination_base'
|
7
|
+
require 'process_executer/destinations'
|
3
8
|
require 'process_executer/errors'
|
4
9
|
require 'process_executer/monitored_pipe'
|
5
10
|
require 'process_executer/options'
|
6
11
|
require 'process_executer/result'
|
7
12
|
require 'process_executer/runner'
|
8
13
|
|
9
|
-
require 'logger'
|
10
|
-
require 'timeout'
|
11
|
-
|
12
14
|
# The `ProcessExecuter` module provides methods to execute subprocess commands
|
13
15
|
# with enhanced features such as output capture, timeout handling, and custom
|
14
16
|
# environment variables.
|
15
17
|
#
|
16
18
|
# Methods:
|
19
|
+
#
|
17
20
|
# * {run}: Executes a command and returns the result which includes the process
|
18
21
|
# status and output
|
19
22
|
# * {spawn_and_wait}: a thin wrapper around `Process.spawn` that blocks until the
|
20
23
|
# command finishes
|
21
24
|
#
|
22
25
|
# Features:
|
26
|
+
#
|
23
27
|
# * Supports executing commands via a shell or directly.
|
24
28
|
# * Captures stdout and stderr to buffers, files, or custom objects.
|
25
29
|
# * Optionally enforces timeouts and terminates long-running commands.
|
@@ -27,7 +31,6 @@ require 'timeout'
|
|
27
31
|
# options that were given, and success, failure, or timeout states.
|
28
32
|
#
|
29
33
|
# @api public
|
30
|
-
#
|
31
34
|
module ProcessExecuter
|
32
35
|
# Run a command in a subprocess, wait for it to finish, then return the result
|
33
36
|
#
|
@@ -70,8 +73,25 @@ module ProcessExecuter
|
|
70
73
|
# @return [ProcessExecuter::Result] The result of the completed subprocess
|
71
74
|
#
|
72
75
|
def self.spawn_and_wait(*command, **options_hash)
|
73
|
-
options = ProcessExecuter
|
74
|
-
|
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)
|
90
|
+
begin
|
91
|
+
pid = Process.spawn(*command, **options.spawn_options)
|
92
|
+
rescue StandardError => e
|
93
|
+
raise ProcessExecuter::SpawnError, "Failed to spawn process: #{e.message}"
|
94
|
+
end
|
75
95
|
wait_for_process(pid, command, options)
|
76
96
|
end
|
77
97
|
|
@@ -83,14 +103,11 @@ module ProcessExecuter
|
|
83
103
|
# which can be accessed via the Result object in `result.options.out`. The
|
84
104
|
# same applies to `err`.
|
85
105
|
#
|
86
|
-
# 2.
|
87
|
-
# buffer.
|
88
|
-
#
|
89
|
-
# 3. `out` and `err` are automatically wrapped in a
|
106
|
+
# 2. `out` and `err` are automatically wrapped in a
|
90
107
|
# `ProcessExecuter::MonitoredPipe` object so that any object that implements
|
91
108
|
# `#write` (or an Array of such objects) can be given for `out` and `err`.
|
92
109
|
#
|
93
|
-
#
|
110
|
+
# 3. Raises one of the following errors unless `raise_errors` is explicitly set
|
94
111
|
# to `false`:
|
95
112
|
#
|
96
113
|
# * `ProcessExecuter::FailedError` if the command returns a non-zero
|
@@ -101,10 +118,10 @@ module ProcessExecuter
|
|
101
118
|
#
|
102
119
|
# If `raise_errors` is false, the returned Result object will contain the error.
|
103
120
|
#
|
104
|
-
#
|
121
|
+
# 4. Raises a `ProcessExecuter::ProcessIOError` if an exception is raised
|
105
122
|
# while collecting subprocess output. This can not be turned off.
|
106
123
|
#
|
107
|
-
#
|
124
|
+
# 5. If a `logger` is provided, it will be used to log:
|
108
125
|
#
|
109
126
|
# * The command that was executed and its status to `info` level
|
110
127
|
# * The stdout and stderr output to `debug` level
|
@@ -199,7 +216,7 @@ module ProcessExecuter
|
|
199
216
|
#
|
200
217
|
# @example Capture stdout and stderr into a single buffer
|
201
218
|
# command = ['echo "stdout" && echo "stderr" 1>&2']
|
202
|
-
# result = ProcessExecuter.run(*command,
|
219
|
+
# result = ProcessExecuter.run(*command, [out:, err:]: StringIO.new)
|
203
220
|
# result.stdout #=> "stdout\nstderr\n"
|
204
221
|
# result.stderr #=> "stdout\nstderr\n"
|
205
222
|
# result.stdout.object_id == result.stderr.object_id #=> true
|
@@ -223,18 +240,16 @@ module ProcessExecuter
|
|
223
240
|
# # stderr is still captured to a StringIO buffer internally
|
224
241
|
# result.stderr #=> "stderr\n"
|
225
242
|
#
|
226
|
-
# @example Capture to multiple
|
243
|
+
# @example Capture to multiple destinations (e.g. files, buffers, STDOUT, etc.)
|
227
244
|
# # Same technique can be used for stderr
|
228
245
|
# out_buffer = StringIO.new
|
229
246
|
# out_file = File.open('stdout.txt', 'w')
|
230
247
|
# command = ['echo "stdout" && echo "stderr" 1>&2']
|
231
|
-
# result = ProcessExecuter.run(*command, out: [out_buffer, out_file])
|
248
|
+
# result = ProcessExecuter.run(*command, out: [:tee, out_buffer, out_file])
|
232
249
|
# # You must manage closing resources you create yourself
|
233
250
|
# out_file.close
|
234
251
|
# out_buffer.string #=> "stdout\n"
|
235
252
|
# 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
253
|
# result.stdout #=> "stdout\n"
|
239
254
|
#
|
240
255
|
# @param command [Array<String>] The command to run
|
@@ -252,7 +267,6 @@ module ProcessExecuter
|
|
252
267
|
# Otherwise, the command is run bypassing the shell. When bypassing the shell, shell expansions
|
253
268
|
# and redirections are not supported.
|
254
269
|
#
|
255
|
-
# @param logger [Logger] The logger to use
|
256
270
|
# @param options_hash [Hash] Additional options
|
257
271
|
# @option options_hash [Numeric] :timeout_after The maximum seconds to wait for the
|
258
272
|
# command to complete
|
@@ -265,7 +279,6 @@ module ProcessExecuter
|
|
265
279
|
#
|
266
280
|
# @option options_hash [#write] :out (nil) The object to write stdout to
|
267
281
|
# @option options_hash [#write] :err (nil) The object to write stderr to
|
268
|
-
# @option options_hash [Boolean] :merge (false) If true, stdout and stderr are written to the same capture buffer
|
269
282
|
# @option options_hash [Boolean] :raise_errors (true) Raise an exception if the command fails
|
270
283
|
# @option options_hash [Boolean] :unsetenv_others (false) If true, unset all environment variables before
|
271
284
|
# applying the new ones
|
@@ -276,16 +289,29 @@ module ProcessExecuter
|
|
276
289
|
# @option options_hash [Integer] :umask (nil) Set the umask (see File.umask)
|
277
290
|
# @option options_hash [Boolean] :close_others (false) If true, close non-standard file descriptors
|
278
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
|
297
|
+
#
|
298
|
+
def self.run(*command, **options_hash)
|
299
|
+
options = ProcessExecuter.run_options(options_hash)
|
300
|
+
run_with_options(command, options)
|
301
|
+
end
|
302
|
+
|
303
|
+
# Run a command with the given options
|
279
304
|
#
|
280
|
-
# @
|
281
|
-
#
|
282
|
-
# @
|
283
|
-
# @
|
305
|
+
# @see ProcessExecuter.run for full documentation
|
306
|
+
#
|
307
|
+
# @param command [Array<String>] The command to run
|
308
|
+
# @param options [ProcessExecuter::Options::RunOptions] The options to use when running the command
|
284
309
|
#
|
285
310
|
# @return [ProcessExecuter::Result] The result of the completed subprocess
|
286
311
|
#
|
287
|
-
|
288
|
-
|
312
|
+
# @api private
|
313
|
+
def self.run_with_options(command, options)
|
314
|
+
ProcessExecuter::Runner.new.call(command, options)
|
289
315
|
end
|
290
316
|
|
291
317
|
# Wait for process to terminate
|
@@ -328,4 +354,88 @@ module ProcessExecuter
|
|
328
354
|
|
329
355
|
[process_status, timed_out]
|
330
356
|
end
|
357
|
+
|
358
|
+
# Convert a hash to a SpawnOptions object
|
359
|
+
#
|
360
|
+
# @example
|
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>
|
366
|
+
#
|
367
|
+
# @param obj [Hash, SpawnOptions] the object to be converted
|
368
|
+
#
|
369
|
+
# @return [SpawnOptions]
|
370
|
+
#
|
371
|
+
# @raise [ArgumentError] if obj is not a Hash or SpawnOptions
|
372
|
+
#
|
373
|
+
# @api public
|
374
|
+
#
|
375
|
+
def self.spawn_options(obj)
|
376
|
+
case obj
|
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
|
387
|
+
#
|
388
|
+
# @example
|
389
|
+
# options_hash = { out: $stdout }
|
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>
|
394
|
+
#
|
395
|
+
# @param obj [Hash, SpawnAndWaitOptions] the object to be converted
|
396
|
+
#
|
397
|
+
# @return [SpawnAndWaitOptions]
|
398
|
+
#
|
399
|
+
# @raise [ArgumentError] if obj is not a Hash or SpawnOptions
|
400
|
+
#
|
401
|
+
# @api public
|
402
|
+
#
|
403
|
+
def self.spawn_and_wait_options(obj)
|
404
|
+
case obj
|
405
|
+
when ProcessExecuter::Options::SpawnAndWaitOptions
|
406
|
+
obj
|
407
|
+
when Hash
|
408
|
+
ProcessExecuter::Options::SpawnAndWaitOptions.new(**obj)
|
409
|
+
else
|
410
|
+
raise ArgumentError, "Expected a Hash or ProcessExecuter::Options::SpawnAndWaitOptions but got a #{obj.class}"
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
# Convert a hash to a RunOptions object
|
415
|
+
#
|
416
|
+
# @example
|
417
|
+
# options_hash = { out: $stdout }
|
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>
|
422
|
+
#
|
423
|
+
# @param obj [Hash, RunOptions] the object to be converted
|
424
|
+
#
|
425
|
+
# @return [RunOptions]
|
426
|
+
#
|
427
|
+
# @raise [ArgumentError] if obj is not a Hash or SpawnOptions
|
428
|
+
#
|
429
|
+
# @api public
|
430
|
+
#
|
431
|
+
def self.run_options(obj)
|
432
|
+
case obj
|
433
|
+
when ProcessExecuter::Options::RunOptions
|
434
|
+
obj
|
435
|
+
when Hash
|
436
|
+
ProcessExecuter::Options::RunOptions.new(**obj)
|
437
|
+
else
|
438
|
+
raise ArgumentError, "Expected a Hash or ProcessExecuter::Options::RunOptions but got a #{obj.class}"
|
439
|
+
end
|
440
|
+
end
|
331
441
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: process_executer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- James Couball
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-04-01 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: bundler-audit
|
@@ -216,9 +216,28 @@ files:
|
|
216
216
|
- README.md
|
217
217
|
- Rakefile
|
218
218
|
- lib/process_executer.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
|
219
233
|
- lib/process_executer/errors.rb
|
220
234
|
- lib/process_executer/monitored_pipe.rb
|
221
235
|
- lib/process_executer/options.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
|
222
241
|
- lib/process_executer/result.rb
|
223
242
|
- lib/process_executer/runner.rb
|
224
243
|
- lib/process_executer/version.rb
|
@@ -231,8 +250,8 @@ metadata:
|
|
231
250
|
allowed_push_host: https://rubygems.org
|
232
251
|
homepage_uri: https://github.com/main-branch/process_executer
|
233
252
|
source_code_uri: https://github.com/main-branch/process_executer
|
234
|
-
documentation_uri: https://rubydoc.info/gems/process_executer/
|
235
|
-
changelog_uri: https://rubydoc.info/gems/process_executer/
|
253
|
+
documentation_uri: https://rubydoc.info/gems/process_executer/3.1.0
|
254
|
+
changelog_uri: https://rubydoc.info/gems/process_executer/3.1.0/file/CHANGELOG.md
|
236
255
|
rubygems_mfa_required: 'true'
|
237
256
|
rdoc_options: []
|
238
257
|
require_paths:
|