process_executer 3.2.4 → 4.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +41 -0
  4. data/README.md +177 -134
  5. data/lib/process_executer/commands/run.rb +124 -0
  6. data/lib/process_executer/commands/run_with_capture.rb +148 -0
  7. data/lib/process_executer/commands/spawn_with_timeout.rb +163 -0
  8. data/lib/process_executer/commands.rb +11 -0
  9. data/lib/process_executer/destinations/child_redirection.rb +5 -4
  10. data/lib/process_executer/destinations/close.rb +5 -4
  11. data/lib/process_executer/destinations/destination_base.rb +73 -0
  12. data/lib/process_executer/destinations/file_descriptor.rb +10 -6
  13. data/lib/process_executer/destinations/file_path.rb +12 -6
  14. data/lib/process_executer/destinations/file_path_mode.rb +10 -6
  15. data/lib/process_executer/destinations/file_path_mode_perms.rb +12 -5
  16. data/lib/process_executer/destinations/io.rb +10 -5
  17. data/lib/process_executer/destinations/monitored_pipe.rb +10 -5
  18. data/lib/process_executer/destinations/stderr.rb +8 -4
  19. data/lib/process_executer/destinations/stdout.rb +8 -4
  20. data/lib/process_executer/destinations/tee.rb +24 -17
  21. data/lib/process_executer/destinations/writer.rb +12 -7
  22. data/lib/process_executer/destinations.rb +32 -17
  23. data/lib/process_executer/errors.rb +50 -26
  24. data/lib/process_executer/monitored_pipe.rb +128 -59
  25. data/lib/process_executer/options/base.rb +118 -82
  26. data/lib/process_executer/options/option_definition.rb +5 -1
  27. data/lib/process_executer/options/run_options.rb +13 -12
  28. data/lib/process_executer/options/run_with_capture_options.rb +156 -0
  29. data/lib/process_executer/options/spawn_options.rb +31 -30
  30. data/lib/process_executer/options/{spawn_and_wait_options.rb → spawn_with_timeout_options.rb} +11 -7
  31. data/lib/process_executer/options.rb +3 -1
  32. data/lib/process_executer/result.rb +35 -77
  33. data/lib/process_executer/result_with_capture.rb +62 -0
  34. data/lib/process_executer/version.rb +2 -1
  35. data/lib/process_executer.rb +384 -346
  36. data/process_executer.gemspec +11 -2
  37. metadata +18 -8
  38. data/lib/process_executer/destination_base.rb +0 -83
  39. data/lib/process_executer/runner.rb +0 -144
@@ -8,8 +8,17 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ['James Couball']
9
9
  spec.email = ['jcouball@yahoo.com']
10
10
 
11
- spec.summary = 'An API for executing commands in a subprocess'
12
- spec.description = 'An API for executing commands in a subprocess'
11
+ spec.summary = <<~SUMMARY
12
+ Enhanced subprocess execution with timeouts, output capture, and flexible redirection
13
+ SUMMARY
14
+
15
+ spec.description = <<~DESCRIPTION
16
+ ProcessExecuter provides a simple API for running commands in a subprocess,
17
+ with options for capturing output, handling timeouts, logging, and more.
18
+ It also provides the MonitoredPipe class which expands the output
19
+ redirection capabilities of Ruby's Process.spawn.
20
+ DESCRIPTION
21
+
13
22
  spec.license = 'MIT'
14
23
  spec.required_ruby_version = '>= 3.1.0'
15
24
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: process_executer
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.4
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Couball
@@ -211,7 +211,11 @@ dependencies:
211
211
  - - "~>"
212
212
  - !ruby/object:Gem::Version
213
213
  version: '0.9'
214
- description: An API for executing commands in a subprocess
214
+ description: |
215
+ ProcessExecuter provides a simple API for running commands in a subprocess,
216
+ with options for capturing output, handling timeouts, logging, and more.
217
+ It also provides the MonitoredPipe class which expands the output
218
+ redirection capabilities of Ruby's Process.spawn.
215
219
  email:
216
220
  - jcouball@yahoo.com
217
221
  executables: []
@@ -231,10 +235,14 @@ files:
231
235
  - README.md
232
236
  - Rakefile
233
237
  - lib/process_executer.rb
234
- - lib/process_executer/destination_base.rb
238
+ - lib/process_executer/commands.rb
239
+ - lib/process_executer/commands/run.rb
240
+ - lib/process_executer/commands/run_with_capture.rb
241
+ - lib/process_executer/commands/spawn_with_timeout.rb
235
242
  - lib/process_executer/destinations.rb
236
243
  - lib/process_executer/destinations/child_redirection.rb
237
244
  - lib/process_executer/destinations/close.rb
245
+ - lib/process_executer/destinations/destination_base.rb
238
246
  - lib/process_executer/destinations/file_descriptor.rb
239
247
  - lib/process_executer/destinations/file_path.rb
240
248
  - lib/process_executer/destinations/file_path_mode.rb
@@ -251,10 +259,11 @@ files:
251
259
  - lib/process_executer/options/base.rb
252
260
  - lib/process_executer/options/option_definition.rb
253
261
  - lib/process_executer/options/run_options.rb
254
- - lib/process_executer/options/spawn_and_wait_options.rb
262
+ - lib/process_executer/options/run_with_capture_options.rb
255
263
  - lib/process_executer/options/spawn_options.rb
264
+ - lib/process_executer/options/spawn_with_timeout_options.rb
256
265
  - lib/process_executer/result.rb
257
- - lib/process_executer/runner.rb
266
+ - lib/process_executer/result_with_capture.rb
258
267
  - lib/process_executer/version.rb
259
268
  - package.json
260
269
  - process_executer.gemspec
@@ -266,8 +275,8 @@ metadata:
266
275
  allowed_push_host: https://rubygems.org
267
276
  homepage_uri: https://github.com/main-branch/process_executer
268
277
  source_code_uri: https://github.com/main-branch/process_executer
269
- documentation_uri: https://rubydoc.info/gems/process_executer/3.2.4
270
- changelog_uri: https://rubydoc.info/gems/process_executer/3.2.4/file/CHANGELOG.md
278
+ documentation_uri: https://rubydoc.info/gems/process_executer/4.0.0
279
+ changelog_uri: https://rubydoc.info/gems/process_executer/4.0.0/file/CHANGELOG.md
271
280
  rubygems_mfa_required: 'true'
272
281
  rdoc_options: []
273
282
  require_paths:
@@ -287,5 +296,6 @@ requirements:
287
296
  - 'Ruby: MRI 3.1 or later, TruffleRuby 24 or later, or JRuby 9.4 or later'
288
297
  rubygems_version: 3.6.7
289
298
  specification_version: 4
290
- summary: An API for executing commands in a subprocess
299
+ summary: Enhanced subprocess execution with timeouts, output capture, and flexible
300
+ redirection
291
301
  test_files: []
@@ -1,83 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ProcessExecuter
4
- # Base class for all destination handlers
5
- #
6
- # Provides the common interface and functionality for all destination
7
- # classes that handle different types of output redirection.
8
- #
9
- # @api private
10
- class DestinationBase
11
- # Initializes a new destination handler
12
- #
13
- # @param destination [Object] the destination to write to
14
- # @return [DestinationBase] a new destination handler instance
15
- def initialize(destination)
16
- @destination = destination
17
- @data_written = []
18
- end
19
-
20
- # The destination object this handler manages
21
- #
22
- # @return [Object] the destination object
23
- attr_reader :destination
24
-
25
- # The data written to the destination
26
- #
27
- # @return [Array<String>] the data written to the destination
28
- attr_reader :data_written
29
-
30
- # The data written to the destination as a single string
31
- # @return [String]
32
- def string
33
- data_written.join
34
- end
35
-
36
- # Writes data to the destination
37
- #
38
- # This is an abstract method that must be implemented by subclasses.
39
- #
40
- # @param data [String] the data to write
41
- # @return [void]
42
- # @raise [NotImplementedError] if the subclass doesn't implement this method
43
- def write(data)
44
- @data_written << data
45
- end
46
-
47
- # Closes the destination if necessary
48
- #
49
- # By default, this method does nothing. Subclasses should override
50
- # this method if they need to perform cleanup.
51
- #
52
- # @return [void]
53
- def close; end
54
-
55
- # Determines if this class can handle the given destination
56
- #
57
- # This is an abstract class method that must be implemented by subclasses.
58
- #
59
- # @param destination [Object] the destination to check
60
- # @return [Boolean] true if this class can handle the destination
61
- # @raise [NotImplementedError] if the subclass doesn't implement this method
62
- def self.handles?(destination)
63
- raise NotImplementedError
64
- end
65
-
66
- # Determines if this destination class can be wrapped by MonitoredPipe
67
- #
68
- # All destination types can be wrapped by MonitoredPipe unless they explicitly
69
- # opt out.
70
- #
71
- # @return [Boolean]
72
- # @api private
73
- def self.compatible_with_monitored_pipe? = true
74
-
75
- # Determines if this destination instance can be wrapped by MonitoredPipe
76
- #
77
- # @return [Boolean]
78
- # @api private
79
- def compatible_with_monitored_pipe?
80
- self.class.compatible_with_monitored_pipe?
81
- end
82
- end
83
- end
@@ -1,144 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'errors'
4
-
5
- module ProcessExecuter
6
- # The `Runner` class executes subprocess commands and captures their status and output.
7
- #
8
- # It does the following:
9
- # - Run commands (`call`) with options for capturing output, handling timeouts, and merging stdout/stderr.
10
- # - Process command results, including logging and error handling.
11
- # - Raise detailed exceptions for common command failures, such as timeouts or subprocess errors.
12
- #
13
- # This class is used internally by {ProcessExecuter.run}.
14
- #
15
- # @api public
16
- #
17
- class Runner
18
- # Run a command and return the status including stdout and stderr output
19
- #
20
- # @example
21
- # runner = ProcessExecuter::Runner.new()
22
- # result = runner.call('echo hello')
23
- # result = ProcessExecuter.run('echo hello')
24
- # result.success? # => true
25
- # result.exitstatus # => 0
26
- # result.stdout # => "hello\n"
27
- # result.stderr # => ""
28
- #
29
- # @param command [Array<String>] The command to run
30
- # @param options [ProcessExecuter::Options::RunOptions] Options for running the command
31
- #
32
- # @return [ProcessExecuter::Result] The result of the completed subprocess
33
- #
34
- def call(command, options)
35
- spawn(command, options).tap { |result| process_result(result) }
36
- end
37
-
38
- private
39
-
40
- # Wrap the output buffers in pipes and then execute the command
41
- #
42
- # @param command [Array<String>] The command to execute
43
- # @param options [ProcessExecuter::Options::RunOptions] Options for running the command
44
- #
45
- # @raise [ProcessExecuter::Error] if the command could not be executed or failed
46
- #
47
- # @return [ProcessExecuter::Result] The result of the completed subprocess
48
- #
49
- # @api private
50
- #
51
- def spawn(command, options)
52
- opened_pipes = wrap_stdout_stderr(options)
53
- ProcessExecuter.spawn_and_wait_with_options(command, options)
54
- ensure
55
- opened_pipes.each_value(&:close)
56
- opened_pipes.each { |option_key, pipe| raise_pipe_error(command, option_key, pipe) }
57
- end
58
-
59
- # Wrap the stdout and stderr redirection options with a MonitoredPipe
60
- # @param options [ProcessExecuter::Options::RunOptions] Options for running the command
61
- # @return [Hash<Object, ProcessExecuter::MonitoredPipe>] The opened pipes (the Object is the option key)
62
- # @api private
63
- def wrap_stdout_stderr(options)
64
- options.each_with_object({}) do |key_value, opened_pipes|
65
- key, value = key_value
66
-
67
- next unless should_wrap?(options, key, value)
68
-
69
- wrapped_destination = ProcessExecuter::MonitoredPipe.new(value)
70
- opened_pipes[key] = wrapped_destination
71
- options.merge!(key => wrapped_destination)
72
- end
73
- end
74
-
75
- # Should the redirection option be wrapped by a MonitoredPipe
76
- # @param key [Object] The option key
77
- # @param value [Object] The option value
78
- # @return [Boolean] Whether the option should be wrapped
79
- # @api private
80
- def should_wrap?(options, key, value)
81
- (options.stdout_redirection?(key) || options.stderr_redirection?(key)) &&
82
- ProcessExecuter::Destinations.compatible_with_monitored_pipe?(value)
83
- end
84
-
85
- # Process the result of the command and return a ProcessExecuter::Result
86
- #
87
- # Log the command and result, and raise an error if the command failed.
88
- #
89
- # @param result [ProcessExecuter::Result] The result of the command
90
- #
91
- # @return [Void]
92
- #
93
- # @raise [ProcessExecuter::Error] if the command could not be executed or failed
94
- #
95
- # @api private
96
- #
97
- def process_result(result)
98
- log_result(result)
99
-
100
- raise_errors(result) if result.options.raise_errors
101
- end
102
-
103
- # Raise an error if the command failed
104
- # @return [void]
105
- # @raise [ProcessExecuter::FailedError] If the command failed
106
- # @raise [ProcessExecuter::SignaledError] If the command was signaled
107
- # @raise [ProcessExecuter::TimeoutError] If the command times out
108
- # @api private
109
- def raise_errors(result)
110
- raise TimeoutError, result if result.timed_out?
111
- raise SignaledError, result if result.signaled?
112
- raise FailedError, result unless result.success?
113
- end
114
-
115
- # Log the result of running the command
116
- # @param result [ProcessExecuter::Result] the result of the command including
117
- # the command, status, stdout, and stderr
118
- # @return [void]
119
- # @api private
120
- def log_result(result)
121
- result.options.logger.info { "#{result.command} exited with status #{result}" }
122
- result.options.logger.debug { "stdout:\n#{result.stdout.inspect}\nstderr:\n#{result.stderr.inspect}" }
123
- end
124
-
125
- # Raise an error when there was exception while collecting the subprocess output
126
- #
127
- # @param command [Array<String>] The command that was executed
128
- # @param option_key [Symbol] The name of the pipe that raised the exception
129
- # @param pipe [ProcessExecuter::MonitoredPipe] The pipe that raised the exception
130
- #
131
- # @raise [ProcessExecuter::ProcessIOError]
132
- #
133
- # @return [void] This method always raises an error
134
- #
135
- # @api private
136
- #
137
- def raise_pipe_error(command, option_key, pipe)
138
- return unless pipe.exception
139
-
140
- error = ProcessExecuter::ProcessIOError.new("Pipe Exception for #{command}: #{option_key.inspect}")
141
- raise(error, cause: pipe.exception)
142
- end
143
- end
144
- end