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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +50 -0
- data/README.md +177 -134
- data/lib/process_executer/commands/run.rb +124 -0
- data/lib/process_executer/commands/run_with_capture.rb +148 -0
- data/lib/process_executer/commands/spawn_with_timeout.rb +163 -0
- data/lib/process_executer/commands.rb +11 -0
- data/lib/process_executer/destinations/child_redirection.rb +5 -4
- data/lib/process_executer/destinations/close.rb +5 -4
- data/lib/process_executer/destinations/destination_base.rb +73 -0
- data/lib/process_executer/destinations/file_descriptor.rb +10 -6
- data/lib/process_executer/destinations/file_path.rb +12 -6
- data/lib/process_executer/destinations/file_path_mode.rb +10 -6
- data/lib/process_executer/destinations/file_path_mode_perms.rb +12 -5
- data/lib/process_executer/destinations/io.rb +10 -5
- data/lib/process_executer/destinations/monitored_pipe.rb +10 -5
- data/lib/process_executer/destinations/stderr.rb +8 -4
- data/lib/process_executer/destinations/stdout.rb +8 -4
- data/lib/process_executer/destinations/tee.rb +24 -17
- data/lib/process_executer/destinations/writer.rb +12 -7
- data/lib/process_executer/destinations.rb +32 -17
- data/lib/process_executer/errors.rb +50 -26
- data/lib/process_executer/monitored_pipe.rb +128 -59
- data/lib/process_executer/options/base.rb +118 -82
- data/lib/process_executer/options/option_definition.rb +5 -1
- data/lib/process_executer/options/run_options.rb +13 -12
- data/lib/process_executer/options/run_with_capture_options.rb +156 -0
- data/lib/process_executer/options/spawn_options.rb +31 -30
- data/lib/process_executer/options/{spawn_and_wait_options.rb → spawn_with_timeout_options.rb} +11 -7
- data/lib/process_executer/options.rb +3 -1
- data/lib/process_executer/result.rb +35 -77
- data/lib/process_executer/result_with_capture.rb +62 -0
- data/lib/process_executer/version.rb +2 -1
- data/lib/process_executer.rb +384 -346
- data/process_executer.gemspec +12 -2
- metadata +33 -9
- data/lib/process_executer/destination_base.rb +0 -83
- data/lib/process_executer/runner.rb +0 -144
data/process_executer.gemspec
CHANGED
|
@@ -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 =
|
|
12
|
-
|
|
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:
|
|
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:
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
270
|
-
changelog_uri: https://rubydoc.info/gems/process_executer/
|
|
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:
|
|
311
|
+
rubygems_version: 4.0.3
|
|
289
312
|
specification_version: 4
|
|
290
|
-
summary:
|
|
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
|