process_executer 1.3.0 → 2.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/CHANGELOG.md +8 -0
- data/README.md +23 -23
- data/lib/process_executer/errors.rb +134 -0
- data/lib/process_executer/options.rb +17 -14
- data/lib/process_executer/result.rb +160 -0
- data/lib/process_executer/runner.rb +147 -0
- data/lib/process_executer/version.rb +1 -1
- data/lib/process_executer.rb +135 -83
- metadata +8 -14
- data/.tool-versions +0 -1
- data/lib/process_executer/command/errors.rb +0 -170
- data/lib/process_executer/command/result.rb +0 -77
- data/lib/process_executer/command/runner.rb +0 -167
- data/lib/process_executer/command.rb +0 -12
- data/lib/process_executer/status.rb +0 -70
@@ -1,77 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'delegate'
|
4
|
-
|
5
|
-
module ProcessExecuter
|
6
|
-
module Command
|
7
|
-
# A wrapper around {ProcessExecuter::Status} which adds captured command output
|
8
|
-
#
|
9
|
-
# This class is used to represent the result of a subprocess execution, combining
|
10
|
-
# the process status with the captured output for easier access and manipulation.
|
11
|
-
#
|
12
|
-
# Features:
|
13
|
-
# * Provides access to the process's status, stdout, and stderr.
|
14
|
-
# * Allows conversion of stdout and stderr buffers to strings.
|
15
|
-
#
|
16
|
-
# @example Create a Result object
|
17
|
-
# status = ProcessExecuter.spawn(*command, timeout:, out:, err:)
|
18
|
-
# result = ProcessExecuter::Command::Result.new(command, status, out_buffer.string, err_buffer.string)
|
19
|
-
#
|
20
|
-
# @api public
|
21
|
-
#
|
22
|
-
class Result < SimpleDelegator
|
23
|
-
# Create a new Result object
|
24
|
-
# @example
|
25
|
-
# status = ProcessExecuter.spawn(*command, timeout:, out:, err:)
|
26
|
-
# Result.new(command, status, out_buffer.string, err_buffer.string)
|
27
|
-
# @param command [Array<String>] The command that was executed
|
28
|
-
# @param status [ProcessExecuter::Status] The status of the process
|
29
|
-
# @param stdout [String] The stdout output from the process
|
30
|
-
# @param stderr [String] The stderr output from the process
|
31
|
-
def initialize(command, status, stdout, stderr)
|
32
|
-
super(status)
|
33
|
-
@command = command
|
34
|
-
@stdout = stdout
|
35
|
-
@stderr = stderr
|
36
|
-
end
|
37
|
-
|
38
|
-
# The command that was run
|
39
|
-
# @example
|
40
|
-
# result.command #=> %w[git status]
|
41
|
-
# @return [Array<String>]
|
42
|
-
attr_reader :command
|
43
|
-
|
44
|
-
# The captured stdout output from the process
|
45
|
-
# @example
|
46
|
-
# result.stdout #=> "On branch master\nnothing to commit, working tree clean\n"
|
47
|
-
# @return [String]
|
48
|
-
attr_reader :stdout
|
49
|
-
|
50
|
-
# The captured stderr output from the process
|
51
|
-
# @example
|
52
|
-
# result.stderr #=> "ERROR: file not found"
|
53
|
-
# @return [String]
|
54
|
-
attr_reader :stderr
|
55
|
-
|
56
|
-
# Return the stdout output as a string
|
57
|
-
# @example When stdout is a StringIO containing "Hello World"
|
58
|
-
# result.stdout_to_s #=> "Hello World"
|
59
|
-
# @example When stdout is a File object
|
60
|
-
# result.stdout_to_s #=> #<File:/tmp/output.txt>
|
61
|
-
# @return [String, Object] Returns a String if stdout is a StringIO; otherwise, returns the stdout object
|
62
|
-
def stdout_to_s
|
63
|
-
stdout.respond_to?(:string) ? stdout.string : stdout
|
64
|
-
end
|
65
|
-
|
66
|
-
# Return the stderr output as a string
|
67
|
-
# @example When stderr is a StringIO containing "Hello World"
|
68
|
-
# result.stderr_to_s #=> "Hello World"
|
69
|
-
# @example When stderr is a File object
|
70
|
-
# result.stderr_to_s #=> #<File:/tmp/output.txt>
|
71
|
-
# @return [String, Object] Returns a String if stderr is a StringIO; otherwise, returns the stderr object
|
72
|
-
def stderr_to_s
|
73
|
-
stderr.respond_to?(:string) ? stderr.string : stderr
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
@@ -1,167 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'errors'
|
4
|
-
require_relative 'result'
|
5
|
-
|
6
|
-
module ProcessExecuter
|
7
|
-
module Command
|
8
|
-
# The `Runner` class executes subprocess commands and captures their status and output.
|
9
|
-
#
|
10
|
-
# It does the following:
|
11
|
-
# - Run commands (`call`) with options for capturing output, handling timeouts, and merging stdout/stderr.
|
12
|
-
# - Process command results, including logging and error handling.
|
13
|
-
# - Raise detailed exceptions for common command failures, such as timeouts or subprocess errors.
|
14
|
-
#
|
15
|
-
# This class is used internally by {ProcessExecuter.run}.
|
16
|
-
#
|
17
|
-
# @api public
|
18
|
-
#
|
19
|
-
class Runner
|
20
|
-
# Create a new RunCommand instance
|
21
|
-
#
|
22
|
-
# @example
|
23
|
-
# runner = Runner.new()
|
24
|
-
# status = runner.call('echo', 'hello')
|
25
|
-
#
|
26
|
-
# @param logger [Logger] The logger to use. Defaults to a no-op logger if nil.
|
27
|
-
#
|
28
|
-
def initialize(logger)
|
29
|
-
@logger = logger || Logger.new(nil)
|
30
|
-
end
|
31
|
-
|
32
|
-
# The logger to use
|
33
|
-
# @example
|
34
|
-
# runner.logger #=> #<Logger:0x00007f9b1b8b3d20>
|
35
|
-
# @return [Logger]
|
36
|
-
attr_reader :logger
|
37
|
-
|
38
|
-
# rubocop:disable Metrics/ParameterLists
|
39
|
-
|
40
|
-
# Run a command and return the status including stdout and stderr output
|
41
|
-
#
|
42
|
-
# @example
|
43
|
-
# command = %w[git status]
|
44
|
-
# status = run(command)
|
45
|
-
# status.success? # => true
|
46
|
-
# status.exitstatus # => 0
|
47
|
-
# status.out # => "On branch master\nnothing to commit, working tree clean\n"
|
48
|
-
# status.err # => ""
|
49
|
-
#
|
50
|
-
# @param command [Array<String>] The command to run
|
51
|
-
# @param out [#write] The object to which stdout is written
|
52
|
-
# @param err [#write] The object to which stderr is written
|
53
|
-
# @param merge [Boolean] Write both stdout and stderr into the buffer for stdout
|
54
|
-
# @param raise_errors [Boolean] Raise an exception if the command fails
|
55
|
-
# @param options_hash [Hash] Additional options to pass to Process.spawn
|
56
|
-
#
|
57
|
-
# See {ProcessExecuter.run} for a full list of options.
|
58
|
-
#
|
59
|
-
# @return [ProcessExecuter::Command::Result] The status of the subprocess and captured stdout and stderr output
|
60
|
-
#
|
61
|
-
def call(*command, out: nil, err: nil, merge: false, raise_errors: true, **options_hash)
|
62
|
-
out ||= StringIO.new
|
63
|
-
err ||= (merge ? out : StringIO.new)
|
64
|
-
|
65
|
-
status = spawn(command, out:, err:, **options_hash)
|
66
|
-
|
67
|
-
process_result(command, status, out, err, options_hash[:timeout], raise_errors)
|
68
|
-
end
|
69
|
-
|
70
|
-
# rubocop:enable Metrics/ParameterLists
|
71
|
-
|
72
|
-
private
|
73
|
-
|
74
|
-
# Wrap the output buffers in pipes and then execute the command
|
75
|
-
#
|
76
|
-
# @param command [Array<String>] The command to execute
|
77
|
-
# @param out [#write] The object to which stdout is written
|
78
|
-
# @param err [#write] The object to which stderr is written
|
79
|
-
# @param options_hash [Hash] Additional options to pass to Process.spawn
|
80
|
-
#
|
81
|
-
# See {ProcessExecuter.run} for a full list of options.
|
82
|
-
#
|
83
|
-
# @raise [ProcessExecuter::Command::ProcessIOError] If an exception was raised while collecting subprocess output
|
84
|
-
# @raise [ProcessExecuter::Command::TimeoutError] If the command times out
|
85
|
-
#
|
86
|
-
# @return [ProcessExecuter::Status] The status of the completed subprocess
|
87
|
-
#
|
88
|
-
# @api private
|
89
|
-
#
|
90
|
-
def spawn(command, out:, err:, **options_hash)
|
91
|
-
out = [out] unless out.is_a?(Array)
|
92
|
-
err = [err] unless err.is_a?(Array)
|
93
|
-
out_pipe = ProcessExecuter::MonitoredPipe.new(*out)
|
94
|
-
err_pipe = ProcessExecuter::MonitoredPipe.new(*err)
|
95
|
-
ProcessExecuter.spawn(*command, out: out_pipe, err: err_pipe, **options_hash)
|
96
|
-
ensure
|
97
|
-
out_pipe.close
|
98
|
-
err_pipe.close
|
99
|
-
raise_pipe_error(command, :stdout, out_pipe) if out_pipe.exception
|
100
|
-
raise_pipe_error(command, :stderr, err_pipe) if err_pipe.exception
|
101
|
-
end
|
102
|
-
|
103
|
-
# rubocop:disable Metrics/ParameterLists
|
104
|
-
|
105
|
-
# Process the result of the command and return a ProcessExecuter::Command::Result
|
106
|
-
#
|
107
|
-
# Log the command and result, and raise an error if the command failed.
|
108
|
-
#
|
109
|
-
# @param command [Array<String>] The git command that was executed
|
110
|
-
# @param status [Process::Status] The status of the completed subprocess
|
111
|
-
# @param out [#write] The object that stdout was written to
|
112
|
-
# @param err [#write] The object that stderr was written to
|
113
|
-
# @param timeout [Numeric, nil] The maximum seconds to wait for the command to complete
|
114
|
-
# @param raise_errors [Boolean] Raise an exception if the command fails
|
115
|
-
#
|
116
|
-
# @return [ProcessExecuter::Command::Result] The status of the subprocess and captured stdout and stderr output
|
117
|
-
#
|
118
|
-
# @raise [ProcessExecuter::Command::FailedError] If the command failed
|
119
|
-
# @raise [ProcessExecuter::Command::SignaledError] If the command was signaled
|
120
|
-
# @raise [ProcessExecuter::Command::TimeoutError] If the command times out
|
121
|
-
# @raise [ProcessExecuter::Command::ProcessIOError] If an exception was raised while collecting subprocess output
|
122
|
-
#
|
123
|
-
# @api private
|
124
|
-
#
|
125
|
-
def process_result(command, status, out, err, timeout, raise_errors)
|
126
|
-
Result.new(command, status, out, err).tap do |result|
|
127
|
-
log_result(result)
|
128
|
-
|
129
|
-
if raise_errors
|
130
|
-
raise TimeoutError.new(result, timeout) if status.timeout?
|
131
|
-
raise SignaledError, result if status.signaled?
|
132
|
-
raise FailedError, result unless status.success?
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
# rubocop:enable Metrics/ParameterLists
|
138
|
-
|
139
|
-
# Log the command and result of the subprocess
|
140
|
-
# @param result [ProcessExecuter::Command::Result] the result of the command including
|
141
|
-
# the command, status, stdout, and stderr
|
142
|
-
# @return [void]
|
143
|
-
# @api private
|
144
|
-
def log_result(result)
|
145
|
-
logger.info { "#{result.command} exited with status #{result}" }
|
146
|
-
logger.debug { "stdout:\n#{result.stdout_to_s.inspect}\nstderr:\n#{result.stderr_to_s.inspect}" }
|
147
|
-
end
|
148
|
-
|
149
|
-
# Raise an error when there was exception while collecting the subprocess output
|
150
|
-
#
|
151
|
-
# @param command [Array<String>] The command that was executed
|
152
|
-
# @param pipe_name [Symbol] The name of the pipe that raised the exception
|
153
|
-
# @param pipe [ProcessExecuter::MonitoredPipe] The pipe that raised the exception
|
154
|
-
#
|
155
|
-
# @raise [ProcessExecuter::Command::ProcessIOError]
|
156
|
-
#
|
157
|
-
# @return [void] This method always raises an error
|
158
|
-
#
|
159
|
-
# @api private
|
160
|
-
#
|
161
|
-
def raise_pipe_error(command, pipe_name, pipe)
|
162
|
-
error = ProcessExecuter::Command::ProcessIOError.new("Pipe Exception for #{command}: #{pipe_name}")
|
163
|
-
raise(error, cause: pipe.exception)
|
164
|
-
end
|
165
|
-
end
|
166
|
-
end
|
167
|
-
end
|
@@ -1,12 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ProcessExecuter
|
4
|
-
# This module contains classes for implementing ProcessExecuter.run_command
|
5
|
-
module Command; end
|
6
|
-
end
|
7
|
-
|
8
|
-
require_relative 'command/errors'
|
9
|
-
require_relative 'command/result'
|
10
|
-
require_relative 'command/runner'
|
11
|
-
|
12
|
-
# Runs a command and returns the result
|
@@ -1,70 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'delegate'
|
4
|
-
require 'forwardable'
|
5
|
-
|
6
|
-
module ProcessExecuter
|
7
|
-
# A simple delegator for Process::Status that adds a `timeout?` attribute
|
8
|
-
#
|
9
|
-
# @api public
|
10
|
-
#
|
11
|
-
class Status < SimpleDelegator
|
12
|
-
extend Forwardable
|
13
|
-
|
14
|
-
# Create a new Status object from a Process::Status and timeout flag
|
15
|
-
#
|
16
|
-
# @param status [Process::Status] the status to delegate to
|
17
|
-
# @param timeout [Boolean] true if the process timed out
|
18
|
-
# @param timeout_duration [Numeric, nil] The secs the command ran before being killed OR o or nil for no timeout
|
19
|
-
#
|
20
|
-
# @example
|
21
|
-
# status = Process.wait2(pid).last
|
22
|
-
# timeout = false
|
23
|
-
# ProcessExecuter::Status.new(status, timeout)
|
24
|
-
#
|
25
|
-
# @api public
|
26
|
-
#
|
27
|
-
def initialize(status, timeout, timeout_duration)
|
28
|
-
super(status)
|
29
|
-
@timeout = timeout
|
30
|
-
@timeout_duration = timeout_duration
|
31
|
-
end
|
32
|
-
|
33
|
-
# The secs the command ran before being killed OR o or nil for no timeout
|
34
|
-
# @example
|
35
|
-
# status.timeout_duration #=> 10
|
36
|
-
# @return [Numeric, nil]
|
37
|
-
attr_reader :timeout_duration
|
38
|
-
|
39
|
-
# @!attribute [r] timeout?
|
40
|
-
# True if the process timed out and was sent the SIGKILL signal
|
41
|
-
# @example
|
42
|
-
# status = ProcessExecuter.spawn('sleep 10', timeout: 0.01)
|
43
|
-
# status.timeout? # => true
|
44
|
-
# @return [Boolean]
|
45
|
-
#
|
46
|
-
def timeout? = @timeout
|
47
|
-
|
48
|
-
# Overrides the default success? method to return nil if the process timed out
|
49
|
-
#
|
50
|
-
# This is because when a timeout occurs, Windows will still return true
|
51
|
-
# @example
|
52
|
-
# status = ProcessExecuter.spawn('sleep 10', timeout: 0.01)
|
53
|
-
# status.success? # => nil
|
54
|
-
# @return [Boolean, nil]
|
55
|
-
#
|
56
|
-
def success?
|
57
|
-
return nil if timeout? # rubocop:disable Style/ReturnNilInPredicateMethodDefinition
|
58
|
-
|
59
|
-
super
|
60
|
-
end
|
61
|
-
|
62
|
-
# Return a string representation of the status
|
63
|
-
# @example
|
64
|
-
# status.to_s #=> "pid 70144 SIGKILL (signal 9) timed out after 10s"
|
65
|
-
# @return [String]
|
66
|
-
def to_s
|
67
|
-
"#{super}#{timeout? ? " timed out after #{timeout_duration}s" : ''}"
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|