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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/README.md +159 -46
  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 +11 -1
  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 -166
  26. data/lib/process_executer/result.rb +13 -23
  27. data/lib/process_executer/runner.rb +60 -56
  28. data/lib/process_executer/version.rb +1 -1
  29. data/lib/process_executer.rb +136 -26
  30. metadata +23 -4
@@ -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::Options.new(**options_hash)
74
- pid = Process.spawn(*command, **options.spawn_options)
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. 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
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
- # 4. Raises one of the following errors unless `raise_errors` is explicitly set
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
- # 5. Raises a `ProcessExecuter::ProcessIOError` if an exception is raised
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
- # 6. If a `logger` is provided, it will be used to log:
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, merge: true)
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 writers (e.g. files, buffers, STDOUT, etc.)
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
- # @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
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
- def self.run(*command, logger: Logger.new(nil), **options_hash)
288
- ProcessExecuter::Runner.new(logger).call(*command, **options_hash)
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: 2.0.0
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-03-03 00:00:00.000000000 Z
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/2.0.0
235
- changelog_uri: https://rubydoc.info/gems/process_executer/2.0.0/file/CHANGELOG.md
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: