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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +41 -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 +11 -2
- metadata +18 -8
- 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
|
|
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.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:
|
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/
|
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/
|
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/
|
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/
|
270
|
-
changelog_uri: https://rubydoc.info/gems/process_executer/
|
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:
|
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
|