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.
@@ -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