process_executer 3.2.4 → 4.0.1

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 +50 -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 +12 -2
  37. metadata +33 -9
  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
 
@@ -51,6 +60,7 @@ Gem::Specification.new do |spec|
51
60
  spec.add_development_dependency 'simplecov-rspec', '~> 0.3'
52
61
 
53
62
  unless RUBY_PLATFORM == 'java'
63
+ spec.add_development_dependency 'irb', '~> 1.6'
54
64
  spec.add_development_dependency 'redcarpet', '~> 3.6'
55
65
  spec.add_development_dependency 'yard', '~> 0.9', '>= 0.9.28'
56
66
  spec.add_development_dependency 'yardstick', '~> 0.9'
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Couball
@@ -163,6 +163,20 @@ dependencies:
163
163
  - - "~>"
164
164
  - !ruby/object:Gem::Version
165
165
  version: '0.3'
166
+ - !ruby/object:Gem::Dependency
167
+ name: irb
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '1.6'
173
+ type: :development
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - "~>"
178
+ - !ruby/object:Gem::Version
179
+ version: '1.6'
166
180
  - !ruby/object:Gem::Dependency
167
181
  name: redcarpet
168
182
  requirement: !ruby/object:Gem::Requirement
@@ -211,7 +225,11 @@ dependencies:
211
225
  - - "~>"
212
226
  - !ruby/object:Gem::Version
213
227
  version: '0.9'
214
- description: An API for executing commands in a subprocess
228
+ description: |
229
+ ProcessExecuter provides a simple API for running commands in a subprocess,
230
+ with options for capturing output, handling timeouts, logging, and more.
231
+ It also provides the MonitoredPipe class which expands the output
232
+ redirection capabilities of Ruby's Process.spawn.
215
233
  email:
216
234
  - jcouball@yahoo.com
217
235
  executables: []
@@ -231,10 +249,14 @@ files:
231
249
  - README.md
232
250
  - Rakefile
233
251
  - lib/process_executer.rb
234
- - lib/process_executer/destination_base.rb
252
+ - lib/process_executer/commands.rb
253
+ - lib/process_executer/commands/run.rb
254
+ - lib/process_executer/commands/run_with_capture.rb
255
+ - lib/process_executer/commands/spawn_with_timeout.rb
235
256
  - lib/process_executer/destinations.rb
236
257
  - lib/process_executer/destinations/child_redirection.rb
237
258
  - lib/process_executer/destinations/close.rb
259
+ - lib/process_executer/destinations/destination_base.rb
238
260
  - lib/process_executer/destinations/file_descriptor.rb
239
261
  - lib/process_executer/destinations/file_path.rb
240
262
  - lib/process_executer/destinations/file_path_mode.rb
@@ -251,10 +273,11 @@ files:
251
273
  - lib/process_executer/options/base.rb
252
274
  - lib/process_executer/options/option_definition.rb
253
275
  - lib/process_executer/options/run_options.rb
254
- - lib/process_executer/options/spawn_and_wait_options.rb
276
+ - lib/process_executer/options/run_with_capture_options.rb
255
277
  - lib/process_executer/options/spawn_options.rb
278
+ - lib/process_executer/options/spawn_with_timeout_options.rb
256
279
  - lib/process_executer/result.rb
257
- - lib/process_executer/runner.rb
280
+ - lib/process_executer/result_with_capture.rb
258
281
  - lib/process_executer/version.rb
259
282
  - package.json
260
283
  - process_executer.gemspec
@@ -266,8 +289,8 @@ metadata:
266
289
  allowed_push_host: https://rubygems.org
267
290
  homepage_uri: https://github.com/main-branch/process_executer
268
291
  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
292
+ documentation_uri: https://rubydoc.info/gems/process_executer/4.0.1
293
+ changelog_uri: https://rubydoc.info/gems/process_executer/4.0.1/file/CHANGELOG.md
271
294
  rubygems_mfa_required: 'true'
272
295
  rdoc_options: []
273
296
  require_paths:
@@ -285,7 +308,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
285
308
  requirements:
286
309
  - 'Platform: Mac, Linux, or Windows'
287
310
  - 'Ruby: MRI 3.1 or later, TruffleRuby 24 or later, or JRuby 9.4 or later'
288
- rubygems_version: 3.6.7
311
+ rubygems_version: 4.0.3
289
312
  specification_version: 4
290
- summary: An API for executing commands in a subprocess
313
+ summary: Enhanced subprocess execution with timeouts, output capture, and flexible
314
+ redirection
291
315
  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