process_executer 1.2.0 → 1.3.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/.yardopts +0 -2
- data/CHANGELOG.md +12 -0
- data/README.md +31 -0
- data/Rakefile +11 -6
- data/lib/process_executer/command/errors.rb +170 -0
- data/lib/process_executer/command/result.rb +77 -0
- data/lib/process_executer/command/runner.rb +167 -0
- data/lib/process_executer/command.rb +12 -0
- data/lib/process_executer/monitored_pipe.rb +46 -9
- data/lib/process_executer/status.rb +31 -6
- data/lib/process_executer/version.rb +1 -1
- data/lib/process_executer.rb +215 -13
- metadata +8 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d2bd50be1f3683bc2c8210451c45e270e7072992ec48897a8bbb16cf10790c8b
|
4
|
+
data.tar.gz: e0d1ba5a7c5571b0059db55537726b68baf6017a66941883953ece63f5a58b53
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bdaab34abbd99de650933f367eedcb40e7a2538d1f48ab8eda6f5df5da3d5f1748543eb28f54f21cc3557c64d34575001da2158caf2f30cc768bec88f657e16c
|
7
|
+
data.tar.gz: 920227450b1da0c56aa3987a2ff1bbeb2c73cd093d04c1318ebb39d02302a408682d0d40c1668eb1f9b37d71f58fe47a6c4b101b75e701fdbaf3db2cdb845b26
|
data/.yardopts
CHANGED
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,18 @@ All notable changes to the process_executer gem will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## v1.3.0 (2025-02-26)
|
9
|
+
|
10
|
+
[Full Changelog](https://github.com/main-branch/process_executer/compare/v1.2.0..v1.3.0)
|
11
|
+
|
12
|
+
Changes since v1.2.0:
|
13
|
+
|
14
|
+
* d1e189b build: add Ruby 3.4 to the CI workflow
|
15
|
+
* e805dfc feat: implement ProcessExecuter.run_command
|
16
|
+
* bad822f fix: update the yard build in the rake file and update included files
|
17
|
+
* 6fbdc5e feat: allow #spawn to accept file descriptors for redirection destination
|
18
|
+
* d745685 test: make it so that tests do not give unnecessary output
|
19
|
+
|
8
20
|
## v1.2.0 (2024-10-10)
|
9
21
|
|
10
22
|
[Full Changelog](https://github.com/main-branch/process_executer/compare/v1.1.2..v1.2.0)
|
data/README.md
CHANGED
@@ -11,6 +11,7 @@ Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?log
|
|
11
11
|
[](https://main-branch.slack.com/archives/C07NG2BPG8Y)
|
12
12
|
|
13
13
|
* [Usage](#usage)
|
14
|
+
* [ProcessExecuter.run](#processexecuterrun)
|
14
15
|
* [ProcessExecuter::MonitoredPipe](#processexecutermonitoredpipe)
|
15
16
|
* [ProcessExecuter.spawn](#processexecuterspawn)
|
16
17
|
* [Installation](#installation)
|
@@ -29,6 +30,36 @@ gem is hosted on RubyGems.org. Read below of an overview and several examples.
|
|
29
30
|
|
30
31
|
This gem contains the following important classes:
|
31
32
|
|
33
|
+
### ProcessExecuter.run
|
34
|
+
|
35
|
+
`ProcessExecuter.run` execute the given command as a subprocess blocking until it is finished.
|
36
|
+
|
37
|
+
A Result object is returned which includes the process's status and output.
|
38
|
+
|
39
|
+
Supports the same features as
|
40
|
+
[Process.spawn](https://docs.ruby-lang.org/en/3.3/Process.html#method-c-spawn).
|
41
|
+
In addition, it (1) blocks until the command has exited, (2) captures stdout and
|
42
|
+
stderr to a buffer or file, and (3) can optionally kill the command if it exceeds
|
43
|
+
an timeout.
|
44
|
+
|
45
|
+
This command takes two forms:
|
46
|
+
|
47
|
+
1. When passing a single string the command is passed to a shell:
|
48
|
+
|
49
|
+
`ProcessExecuter.run([env, ] command_line, options = {}) ->` {ProcessExecuter::Command::Result}
|
50
|
+
|
51
|
+
2. When passing an array of strings the command is run directly (bypassing the shell):
|
52
|
+
|
53
|
+
`ProcessExecuter.run([env, ] exe_path, *args, options = {}) ->` {ProcessExecuter::Command::Result}
|
54
|
+
|
55
|
+
Argument env, if given, is a hash that affects ENV for the new process; see
|
56
|
+
[Execution
|
57
|
+
Environment](https://docs.ruby-lang.org/en/3.3/Process.html#module-Process-label-Execution+Environment).
|
58
|
+
|
59
|
+
Argument options is a hash of options for the new process; see the options listed below.
|
60
|
+
|
61
|
+
See comprehensive examples in the YARD documentation for this method.
|
62
|
+
|
32
63
|
### ProcessExecuter::MonitoredPipe
|
33
64
|
|
34
65
|
`ProcessExecuter::MonitoredPipe` streams data sent through a pipe to one or more writers.
|
data/Rakefile
CHANGED
@@ -7,7 +7,7 @@ desc 'Run the same tasks that the CI build will run'
|
|
7
7
|
if RUBY_PLATFORM == 'java'
|
8
8
|
task default: %w[spec rubocop bundle:audit build]
|
9
9
|
else
|
10
|
-
task default: %w[spec rubocop yard
|
10
|
+
task default: %w[spec rubocop yard bundle:audit build]
|
11
11
|
end
|
12
12
|
|
13
13
|
# Bundler Audit
|
@@ -50,28 +50,33 @@ RuboCop::RakeTask.new
|
|
50
50
|
# YARD
|
51
51
|
|
52
52
|
unless RUBY_PLATFORM == 'java'
|
53
|
-
#
|
53
|
+
# yard:build
|
54
54
|
|
55
55
|
require 'yard'
|
56
|
-
|
57
|
-
|
56
|
+
|
57
|
+
YARD::Rake::YardocTask.new('yard:build') do |t|
|
58
|
+
t.files = %w[lib/**/*.rb]
|
59
|
+
t.stats_options = ['--list-undoc']
|
58
60
|
end
|
59
61
|
|
60
62
|
CLEAN << '.yardoc'
|
61
63
|
CLEAN << 'doc'
|
62
64
|
|
63
|
-
#
|
65
|
+
# yard:audit
|
64
66
|
|
65
67
|
desc 'Run yardstick to show missing YARD doc elements'
|
66
68
|
task :'yard:audit' do
|
67
69
|
sh "yardstick 'lib/**/*.rb'"
|
68
70
|
end
|
69
71
|
|
70
|
-
#
|
72
|
+
# yard:coverage
|
71
73
|
|
72
74
|
require 'yardstick/rake/verify'
|
73
75
|
|
74
76
|
Yardstick::Rake::Verify.new(:'yard:coverage') do |verify|
|
75
77
|
verify.threshold = 100
|
78
|
+
verify.require_exact_threshold = false
|
76
79
|
end
|
80
|
+
|
81
|
+
task yard: %i[yard:build yard:audit yard:coverage]
|
77
82
|
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Layout/LineLength
|
4
|
+
|
5
|
+
module ProcessExecuter
|
6
|
+
module Command
|
7
|
+
# Base class for all ProcessExecuter::Command errors
|
8
|
+
#
|
9
|
+
# It is recommended to rescue `ProcessExecuter::Command::Error` to catch any
|
10
|
+
# runtime error raised by this gem unless you need more specific error handling.
|
11
|
+
#
|
12
|
+
# Custom errors are arranged in the following class hierarchy:
|
13
|
+
#
|
14
|
+
# ```text
|
15
|
+
# ::StandardError
|
16
|
+
# └─> Error
|
17
|
+
# ├─> CommandError
|
18
|
+
# │ ├─> FailedError
|
19
|
+
# │ └─> SignaledError
|
20
|
+
# │ └─> TimeoutError
|
21
|
+
# └─> ProcessIOError
|
22
|
+
# ```
|
23
|
+
#
|
24
|
+
# | Error Class | Description |
|
25
|
+
# | --- | --- |
|
26
|
+
# | `Error` | This catch-all error serves as the base class for other custom errors. |
|
27
|
+
# | `CommandError` | A subclass of this error is raised when there is a problem executing a command. |
|
28
|
+
# | `FailedError` | Raised when the command exits with a non-zero status code. |
|
29
|
+
# | `SignaledError` | Raised when the command is terminated as a result of receiving a signal. This could happen if the process is forcibly terminated or if there is a serious system error. |
|
30
|
+
# | `TimeoutError` | This is a specific type of `SignaledError` that is raised when the command times out and is killed via the SIGKILL signal. Raised when the operation takes longer than the specified timeout duration (if provided). |
|
31
|
+
# | `ProcessIOError` | Raised when an error was encountered reading or writing to the command's subprocess. |
|
32
|
+
#
|
33
|
+
# @example Rescuing any error
|
34
|
+
# begin
|
35
|
+
# ProcessExecuter.run_command('git', 'status')
|
36
|
+
# rescue ProcessExecuter::Command::Error => e
|
37
|
+
# puts "An error occurred: #{e.message}"
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# @example Rescuing a timeout error
|
41
|
+
# begin
|
42
|
+
# timeout_duration = 0.1 # seconds
|
43
|
+
# ProcessExecuter.run_command('sleep', '1', timeout: timeout_duration)
|
44
|
+
# rescue ProcessExecuter::TimeoutError => e # Catch the more specific error first!
|
45
|
+
# puts "Command took too long and timed out: #{e}"
|
46
|
+
# rescue ProcessExecuter::Error => e
|
47
|
+
# puts "Some other error occured: #{e}"
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# @api public
|
51
|
+
#
|
52
|
+
class Error < ::StandardError; end
|
53
|
+
|
54
|
+
# Raised when a command fails or exits because of an uncaught signal
|
55
|
+
#
|
56
|
+
# The command executed, status, stdout, and stderr are available from this
|
57
|
+
# object.
|
58
|
+
#
|
59
|
+
# The Gem will raise a more specific error for each type of failure:
|
60
|
+
#
|
61
|
+
# * {FailedError}: when the command exits with a non-zero status
|
62
|
+
# * {SignaledError}: when the command exits because of an uncaught signal
|
63
|
+
# * {TimeoutError}: when the command times out
|
64
|
+
#
|
65
|
+
# @api public
|
66
|
+
#
|
67
|
+
class CommandError < ProcessExecuter::Command::Error
|
68
|
+
# Create a CommandError object
|
69
|
+
#
|
70
|
+
# @example
|
71
|
+
# `exit 1` # set $? appropriately for this example
|
72
|
+
# result = ProcessExecuter::Command::Result.new(%w[git status], $?, 'stdout', 'stderr')
|
73
|
+
# error = ProcessExecuter::Command::CommandError.new(result)
|
74
|
+
# error.to_s #=> '["git", "status"], status: pid 89784 exit 1, stderr: "stderr"'
|
75
|
+
#
|
76
|
+
# @param result [Result] The result of the command including the command,
|
77
|
+
# status, stdout, and stderr
|
78
|
+
#
|
79
|
+
def initialize(result)
|
80
|
+
@result = result
|
81
|
+
super(error_message)
|
82
|
+
end
|
83
|
+
|
84
|
+
# The human readable representation of this error
|
85
|
+
#
|
86
|
+
# @example
|
87
|
+
# error.error_message #=> '["git", "status"], status: pid 89784 exit 1, stderr: "stderr"'
|
88
|
+
#
|
89
|
+
# @return [String]
|
90
|
+
#
|
91
|
+
def error_message
|
92
|
+
"#{result.command}, status: #{result}, stderr: #{result.stderr_to_s.inspect}"
|
93
|
+
end
|
94
|
+
|
95
|
+
# @attribute [r] result
|
96
|
+
#
|
97
|
+
# The result of the command including the command, its status and its output
|
98
|
+
#
|
99
|
+
# @example
|
100
|
+
# error.result #=> #<ProcessExecuter::Command::Result:0x00007f9b1b8b3d20>
|
101
|
+
#
|
102
|
+
# @return [Result]
|
103
|
+
#
|
104
|
+
attr_reader :result
|
105
|
+
end
|
106
|
+
|
107
|
+
# Raised when the command returns a non-zero exitstatus
|
108
|
+
#
|
109
|
+
# @api public
|
110
|
+
#
|
111
|
+
class FailedError < ProcessExecuter::Command::CommandError; end
|
112
|
+
|
113
|
+
# Raised when the command exits because of an uncaught signal
|
114
|
+
#
|
115
|
+
# @api public
|
116
|
+
#
|
117
|
+
class SignaledError < ProcessExecuter::Command::CommandError; end
|
118
|
+
|
119
|
+
# Raised when the command takes longer than the configured timeout
|
120
|
+
#
|
121
|
+
# @example
|
122
|
+
# result.status.timeout? #=> true
|
123
|
+
#
|
124
|
+
# @api public
|
125
|
+
#
|
126
|
+
class TimeoutError < ProcessExecuter::Command::SignaledError
|
127
|
+
# Create a TimeoutError object
|
128
|
+
#
|
129
|
+
# @example
|
130
|
+
# command = %w[sleep 10]
|
131
|
+
# timeout_duration = 1
|
132
|
+
# status = ProcessExecuter.spawn(*command, timeout: timeout_duration)
|
133
|
+
# result = Result.new(command, status, 'stdout', 'err output')
|
134
|
+
# error = TimeoutError.new(result, timeout_duration)
|
135
|
+
# error.error_message
|
136
|
+
# #=> '["sleep", "10"], status: pid 70144 SIGKILL (signal 9), stderr: "err output", timed out after 1s'
|
137
|
+
#
|
138
|
+
# @param result [Result] The result of the command including the git command,
|
139
|
+
# status, stdout, and stderr
|
140
|
+
#
|
141
|
+
# @param timeout_duration [Numeric] The duration the subprocess was allowed
|
142
|
+
# to run before being terminated
|
143
|
+
#
|
144
|
+
def initialize(result, timeout_duration)
|
145
|
+
@timeout_duration = timeout_duration
|
146
|
+
super(result)
|
147
|
+
end
|
148
|
+
|
149
|
+
# The amount of time the subprocess was allowed to run before being killed
|
150
|
+
#
|
151
|
+
# @example
|
152
|
+
# `kill -9 $$` # set $? appropriately for this example
|
153
|
+
# result = Result.new(%w[git status], $?, '', "killed")
|
154
|
+
# error = TimeoutError.new(result, 10)
|
155
|
+
# error.timeout_duration #=> 10
|
156
|
+
#
|
157
|
+
# @return [Numeric]
|
158
|
+
#
|
159
|
+
attr_reader :timeout_duration
|
160
|
+
end
|
161
|
+
|
162
|
+
# Raised when the output of a command can not be read
|
163
|
+
#
|
164
|
+
# @api public
|
165
|
+
#
|
166
|
+
class ProcessIOError < ProcessExecuter::Command::Error; end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# rubocop:enable Layout/LineLength
|
@@ -0,0 +1,77 @@
|
|
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
|
@@ -0,0 +1,167 @@
|
|
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
|
@@ -0,0 +1,12 @@
|
|
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
|
@@ -281,19 +281,56 @@ module ProcessExecuter
|
|
281
281
|
# @api private
|
282
282
|
def monitor_pipe
|
283
283
|
new_data = pipe_reader.read_nonblock(chunk_size)
|
284
|
-
|
285
|
-
# :nocov:
|
286
|
-
begin
|
287
|
-
# :nocov:
|
288
|
-
writers.each { |w| w.write(new_data) }
|
289
|
-
rescue StandardError => e
|
290
|
-
@exception = e
|
291
|
-
@state = :closing
|
292
|
-
end
|
284
|
+
write_data(new_data)
|
293
285
|
rescue IO::WaitReadable
|
294
286
|
pipe_reader.wait_readable(0.001)
|
295
287
|
end
|
296
288
|
|
289
|
+
# Check if the writer is a file descriptor
|
290
|
+
#
|
291
|
+
# @param writer [#write] the writer to check
|
292
|
+
# @return [Boolean] true if the writer is a file descriptor
|
293
|
+
# @api private
|
294
|
+
def file_descriptor?(writer) = writer.is_a?(Integer) || writer.is_a?(Symbol)
|
295
|
+
|
296
|
+
# Write the data read from the pipe to all destinations
|
297
|
+
#
|
298
|
+
# If an exception is raised by a writer, set the state to `:closing`
|
299
|
+
# so that the pipe can be closed.
|
300
|
+
#
|
301
|
+
# @param data [String] the data read from the pipe
|
302
|
+
# @return [void]
|
303
|
+
# @api private
|
304
|
+
def write_data(data)
|
305
|
+
writers.each do |w|
|
306
|
+
file_descriptor?(w) ? write_data_to_fd(w, data) : w.write(data)
|
307
|
+
end
|
308
|
+
rescue StandardError => e
|
309
|
+
@exception = e
|
310
|
+
@state = :closing
|
311
|
+
end
|
312
|
+
|
313
|
+
# Write data to the given file_descriptor correctly handling stdout and stderr
|
314
|
+
# @param file_descriptor [Integer, Symbol] the file descriptor to write to (either an integer or :out or :err)
|
315
|
+
# @param data [String] the data to write
|
316
|
+
# @return [void]
|
317
|
+
# @api private
|
318
|
+
def write_data_to_fd(file_descriptor, data)
|
319
|
+
# The case line is not marked as not covered only when using TruffleRuby
|
320
|
+
# :nocov:
|
321
|
+
case file_descriptor
|
322
|
+
# :nocov:
|
323
|
+
when :out, 1
|
324
|
+
$stdout.write data
|
325
|
+
when :err, 2
|
326
|
+
$stderr.write data
|
327
|
+
else
|
328
|
+
io = IO.open(file_descriptor, mode: 'a', autoclose: false)
|
329
|
+
io.write(data)
|
330
|
+
io.close
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
297
334
|
# Read any remaining data from the pipe and close it
|
298
335
|
#
|
299
336
|
# @return [void]
|
@@ -15,6 +15,7 @@ module ProcessExecuter
|
|
15
15
|
#
|
16
16
|
# @param status [Process::Status] the status to delegate to
|
17
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
|
18
19
|
#
|
19
20
|
# @example
|
20
21
|
# status = Process.wait2(pid).last
|
@@ -23,23 +24,47 @@ module ProcessExecuter
|
|
23
24
|
#
|
24
25
|
# @api public
|
25
26
|
#
|
26
|
-
def initialize(status, timeout)
|
27
|
+
def initialize(status, timeout, timeout_duration)
|
27
28
|
super(status)
|
28
29
|
@timeout = timeout
|
30
|
+
@timeout_duration = timeout_duration
|
29
31
|
end
|
30
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
|
+
|
31
39
|
# @!attribute [r] timeout?
|
32
|
-
#
|
33
40
|
# True if the process timed out and was sent the SIGKILL signal
|
34
|
-
#
|
35
41
|
# @example
|
36
42
|
# status = ProcessExecuter.spawn('sleep 10', timeout: 0.01)
|
37
43
|
# status.timeout? # => true
|
38
|
-
#
|
39
44
|
# @return [Boolean]
|
40
45
|
#
|
41
|
-
# @api public
|
42
|
-
#
|
43
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
|
44
69
|
end
|
45
70
|
end
|
data/lib/process_executer.rb
CHANGED
@@ -2,21 +2,36 @@
|
|
2
2
|
|
3
3
|
require 'process_executer/monitored_pipe'
|
4
4
|
require 'process_executer/options'
|
5
|
+
require 'process_executer/command'
|
5
6
|
require 'process_executer/status'
|
6
7
|
|
8
|
+
require 'logger'
|
7
9
|
require 'timeout'
|
8
10
|
|
9
|
-
#
|
11
|
+
# The `ProcessExecuter` module provides methods to execute subprocess commands
|
12
|
+
# with enhanced features such as output capture, timeout handling, and custom
|
13
|
+
# environment variables.
|
14
|
+
#
|
15
|
+
# Methods:
|
16
|
+
# * {run}: Executes a command and captures its output and status in a result object.
|
17
|
+
# * {spawn}: Executes a command and returns its exit status.
|
18
|
+
#
|
19
|
+
# Features:
|
20
|
+
# * Supports executing commands via a shell or directly.
|
21
|
+
# * Captures stdout and stderr to buffers, files, or custom objects.
|
22
|
+
# * Optionally enforces timeouts and terminates long-running commands.
|
23
|
+
# * Provides detailed status information, including success, failure, or timeout states.
|
10
24
|
#
|
11
25
|
# @api public
|
12
26
|
#
|
13
27
|
module ProcessExecuter
|
14
|
-
# Execute the
|
28
|
+
# Execute the given command as a subprocess and return the exit status
|
15
29
|
#
|
16
|
-
# This is a convenience method that calls
|
17
|
-
#
|
30
|
+
# This is a convenience method that calls
|
31
|
+
# [Process.spawn](https://docs.ruby-lang.org/en/3.3/Process.html#method-c-spawn)
|
32
|
+
# and blocks until the command terminates.
|
18
33
|
#
|
19
|
-
# The command will be
|
34
|
+
# The command will be sent the SIGKILL signal if it does not terminate within
|
20
35
|
# the specified timeout.
|
21
36
|
#
|
22
37
|
# @example
|
@@ -44,10 +59,10 @@ module ProcessExecuter
|
|
44
59
|
# @see ProcessExecuter::Options#initialize See ProcessExecuter::Options#initialize
|
45
60
|
# for options that may be specified
|
46
61
|
#
|
47
|
-
# @param command [Array<String>]
|
48
|
-
# @param options_hash [Hash]
|
62
|
+
# @param command [Array<String>] The command to execute
|
63
|
+
# @param options_hash [Hash] The options to use when executing the command
|
49
64
|
#
|
50
|
-
# @return [Process::Status] the exit status of the
|
65
|
+
# @return [Process::Status] the exit status of the process
|
51
66
|
#
|
52
67
|
def self.spawn(*command, **options_hash)
|
53
68
|
options = ProcessExecuter::Options.new(**options_hash)
|
@@ -55,23 +70,210 @@ module ProcessExecuter
|
|
55
70
|
wait_for_process(pid, options)
|
56
71
|
end
|
57
72
|
|
73
|
+
# Execute the given command as a subprocess, blocking until it finishes
|
74
|
+
#
|
75
|
+
# Returns a result object which includes the process's status and output.
|
76
|
+
#
|
77
|
+
# Supports the same features as
|
78
|
+
# [Process.spawn](https://docs.ruby-lang.org/en/3.3/Process.html#method-c-spawn).
|
79
|
+
# In addition, it:
|
80
|
+
#
|
81
|
+
# 1. Blocks until the command exits
|
82
|
+
# 2. Captures stdout and stderr to a buffer or file
|
83
|
+
# 3. Optionally kills the command if it exceeds a timeout
|
84
|
+
#
|
85
|
+
# This method takes two forms:
|
86
|
+
#
|
87
|
+
# 1. The command is executed via a shell when the command is given as a single
|
88
|
+
# string:
|
89
|
+
#
|
90
|
+
# `ProcessExecuter.run([env, ] command_line, options = {}) ->` {ProcessExecuter::Command::Result}
|
91
|
+
#
|
92
|
+
# 2. The command is executed directly (bypassing the shell) when the command and it
|
93
|
+
# arguments are given as an array of strings:
|
94
|
+
#
|
95
|
+
# `ProcessExecuter.run([env, ] exe_path, *args, options = {}) ->` {ProcessExecuter::Command::Result}
|
96
|
+
#
|
97
|
+
# Optional argument `env` is a hash that affects ENV for the new process; see
|
98
|
+
# [Execution
|
99
|
+
# Environment](https://docs.ruby-lang.org/en/3.3/Process.html#module-Process-label-Execution+Environment).
|
100
|
+
#
|
101
|
+
# Argument `options` is a hash of options for the new process. See the options listed below.
|
102
|
+
#
|
103
|
+
# @example Run a command given as a single string (uses shell)
|
104
|
+
# # The command must be properly shell escaped when passed as a single string.
|
105
|
+
# command = 'echo "stdout: `pwd`"" && echo "stderr: $HOME" 1>&2'
|
106
|
+
# result = ProcessExecuter.run(command)
|
107
|
+
# result.success? #=> true
|
108
|
+
# result.stdout.string #=> "stdout: /Users/james/projects/main-branch/process_executer\n"
|
109
|
+
# result.stderr.string #=> "stderr: /Users/james\n"
|
110
|
+
#
|
111
|
+
# @example Run a command given as an array of strings (does not use shell)
|
112
|
+
# # The command and its args must be provided as separate strings in the array.
|
113
|
+
# # Shell expansions and redirections are not supported.
|
114
|
+
# command = ['git', 'clone', 'https://github.com/main-branch/process_executer']
|
115
|
+
# result = ProcessExecuter.run(*command)
|
116
|
+
# result.success? #=> true
|
117
|
+
# result.stdout.string #=> ""
|
118
|
+
# result.stderr.string #=> "Cloning into 'process_executer'...\n"
|
119
|
+
#
|
120
|
+
# @example Run a command with a timeout
|
121
|
+
# command = ['sleep', '1']
|
122
|
+
# result = ProcessExecuter.run(*command, timeout: 0.01)
|
123
|
+
# #=> raises ProcessExecuter::Command::TimeoutError which contains the command result
|
124
|
+
#
|
125
|
+
# @example Run a command which fails
|
126
|
+
# command = ['exit 1']
|
127
|
+
# result = ProcessExecuter.run(*command)
|
128
|
+
# #=> raises ProcessExecuter::Command::FailedError which contains the command result
|
129
|
+
#
|
130
|
+
# @example Run a command which exits due to an unhandled signal
|
131
|
+
# command = ['kill -9 $$']
|
132
|
+
# result = ProcessExecuter.run(*command)
|
133
|
+
# #=> raises ProcessExecuter::Command::SignaledError which contains the command result
|
134
|
+
#
|
135
|
+
# @example Return a result instead of raising an error when `raise_errors` is `false`
|
136
|
+
# # By setting `raise_errors` to `false`, exceptions will not be raised even
|
137
|
+
# # if the command fails.
|
138
|
+
# command = ['echo "Some error" 1>&2 && exit 1']
|
139
|
+
# result = ProcessExecuter.run(*command, raise_errors: false)
|
140
|
+
# # An error is not raised
|
141
|
+
# result.success? #=> false
|
142
|
+
# result.exitstatus #=> 1
|
143
|
+
# result.stdout.string #=> ""
|
144
|
+
# result.stderr.string #=> "Some error\n"
|
145
|
+
#
|
146
|
+
# @example Set environment variables
|
147
|
+
# env = { 'FOO' => 'foo', 'BAR' => 'bar' }
|
148
|
+
# command = 'echo "$FOO$BAR"'
|
149
|
+
# result = ProcessExecuter.run(env, *command)
|
150
|
+
# result.stdout.string #=> "foobar\n"
|
151
|
+
#
|
152
|
+
# @example Set environment variables when using a command array
|
153
|
+
# env = { 'GIT_DIR' => '/path/to/.git' }
|
154
|
+
# command = ['git', 'status']
|
155
|
+
# result = ProcessExecuter.run(env, *command)
|
156
|
+
# result.stdout.string #=> "On branch main\nYour branch is ..."
|
157
|
+
#
|
158
|
+
# @example Unset environment variables
|
159
|
+
# env = { 'GIT_DIR' => nil } # setting to nil unsets the variable in the environment
|
160
|
+
# command = ['git', 'status']
|
161
|
+
# result = ProcessExecuter.run(env, *command)
|
162
|
+
# result.stdout.string #=> "On branch main\nYour branch is ..."
|
163
|
+
#
|
164
|
+
# @example Reset existing environment variables and add new ones
|
165
|
+
# env = { 'PATH' => '/bin' }
|
166
|
+
# result = ProcessExecuter.run(env, 'echo "Home: $HOME" && echo "Path: $PATH"', unsetenv_others: true)
|
167
|
+
# result.stdout.string #=> "Home: \n/Path: /bin\n"
|
168
|
+
#
|
169
|
+
# @example Run command in a different directory
|
170
|
+
# command = ['pwd']
|
171
|
+
# result = ProcessExecuter.run(*command, chdir: '/tmp')
|
172
|
+
# result.stdout.string #=> "/tmp\n"
|
173
|
+
#
|
174
|
+
# @example Capture stdout and stderr into a single buffer
|
175
|
+
# command = ['echo "stdout" && echo "stderr" 1>&2']
|
176
|
+
# result = ProcessExecuter.run(*command, merge: true)
|
177
|
+
# result.stdout.string #=> "stdout\nstderr\n"
|
178
|
+
# result.stdout.object_id == result.stderr.object_id #=> true
|
179
|
+
#
|
180
|
+
# @example Capture to an explicit buffer
|
181
|
+
# out = StringIO.new
|
182
|
+
# err = StringIO.new
|
183
|
+
# command = ['echo "stdout" && echo "stderr" 1>&2']
|
184
|
+
# result = ProcessExecuter.run(*command, out: out, err: err)
|
185
|
+
# out.string #=> "stdout\n"
|
186
|
+
# err.string #=> "stderr\n"
|
187
|
+
# result.stdout.object_id == out.object_id #=> true
|
188
|
+
# result.stderr.object_id == err.object_id #=> true
|
189
|
+
#
|
190
|
+
# @example Capture to a file
|
191
|
+
# # Same technique can be used for stderr
|
192
|
+
# out = File.open('stdout.txt', 'w')
|
193
|
+
# command = ['echo "stdout" && echo "stderr" 1>&2']
|
194
|
+
# result = ProcessExecuter.run(*command, out: out, err: err)
|
195
|
+
# out.close
|
196
|
+
# File.read('stdout.txt') #=> "stdout\n"
|
197
|
+
# # stderr is still captured to a StringIO buffer internally
|
198
|
+
# result.stderr.string #=> "stderr\n"
|
199
|
+
#
|
200
|
+
# @example Capture to multiple writers (e.g. files, buffers, STDOUT, etc.)
|
201
|
+
# # Same technique can be used for stderr
|
202
|
+
# out_buffer = StringIO.new
|
203
|
+
# out_file = File.open('stdout.txt', 'w')
|
204
|
+
# command = ['echo "stdout" && echo "stderr" 1>&2']
|
205
|
+
# result = ProcessExecuter.run(*command, out: [out_buffer, out_file])
|
206
|
+
# # You must manage closing resources you create yourself
|
207
|
+
# out_file.close
|
208
|
+
# out_buffer.string #=> "stdout\n"
|
209
|
+
# File.read('stdout.txt') #=> "stdout\n"
|
210
|
+
#
|
211
|
+
# @param command [Array<String>] The command to run
|
212
|
+
#
|
213
|
+
# If the first element of command is a Hash, it is added to the ENV of
|
214
|
+
# the new process. See [Execution Environment](https://ruby-doc.org/3.3.6/Process.html#module-Process-label-Execution+Environment)
|
215
|
+
# for more details. The env hash is then removed from the command array.
|
216
|
+
#
|
217
|
+
# If the first and only (remaining) command element is a string, it is passed to
|
218
|
+
# a subshell if it begins with a shell reserved word, contains special built-ins,
|
219
|
+
# or includes shell metacharacters.
|
220
|
+
#
|
221
|
+
# Care must be taken to properly escape shell metacharacters in the command string.
|
222
|
+
#
|
223
|
+
# Otherwise, the command is run bypassing the shell. When bypassing the shell, shell expansions
|
224
|
+
# and redirections are not supported.
|
225
|
+
#
|
226
|
+
# @param logger [Logger] The logger to use
|
227
|
+
# @param options_hash [Hash] Additional options
|
228
|
+
# @option options_hash [Numeric] :timeout The maximum seconds to wait for the command to complete
|
229
|
+
#
|
230
|
+
# If timeout is zero or nil, the command will not time out. If the command
|
231
|
+
# times out, it is killed via a SIGKILL signal and {ProcessExecuter::Command::TimeoutError} is raised.
|
232
|
+
#
|
233
|
+
# If the command does not exit when receiving the SIGKILL signal, this method may hang indefinitely.
|
234
|
+
#
|
235
|
+
# @option options_hash [#write] :out (nil) The object to write stdout to
|
236
|
+
# @option options_hash [#write] :err (nil) The object to write stderr to
|
237
|
+
# @option options_hash [Boolean] :merge (false) If true, stdout and stderr are written to the same capture buffer
|
238
|
+
# @option options_hash [Boolean] :raise_errors (true) Raise an exception if the command fails
|
239
|
+
# @option options_hash [Boolean] :unsetenv_others (false) If true, unset all environment variables before
|
240
|
+
# applying the new ones
|
241
|
+
# @option options_hash [true, Integer, nil] :pgroup (nil) true or 0: new process group; non-zero: join
|
242
|
+
# the group, nil: existing group
|
243
|
+
# @option options_hash [Boolean] :new_pgroup (nil) Create a new process group (Windows only)
|
244
|
+
# @option options_hash [Integer] :rlimit_resource_name (nil) Set resource limits (see Process.setrlimit)
|
245
|
+
# @option options_hash [Integer] :umask (nil) Set the umask (see File.umask)
|
246
|
+
# @option options_hash [Boolean] :close_others (false) If true, close non-standard file descriptors
|
247
|
+
# @option options_hash [String] :chdir (nil) The directory to run the command in
|
248
|
+
#
|
249
|
+
# @raise [ProcessExecuter::Command::FailedError] if the command returned a non-zero exit status
|
250
|
+
# @raise [ProcessExecuter::Command::SignaledError] if the command exited because of an unhandled signal
|
251
|
+
# @raise [ProcessExecuter::Command::TimeoutError] if the command timed out
|
252
|
+
# @raise [ProcessExecuter::Command::ProcessIOError] if an exception was raised while collecting subprocess output
|
253
|
+
#
|
254
|
+
# @return [ProcessExecuter::Command::Result] A result object containing the process status and captured output
|
255
|
+
#
|
256
|
+
def self.run(*command, logger: Logger.new(nil), **options_hash)
|
257
|
+
ProcessExecuter::Command::Runner.new(logger).call(*command, **options_hash)
|
258
|
+
end
|
259
|
+
|
58
260
|
# Wait for process to terminate
|
59
261
|
#
|
60
|
-
# If a timeout is
|
262
|
+
# If a timeout is specified in options, terminate the process after options.timeout seconds.
|
61
263
|
#
|
62
|
-
# @param pid [Integer] the process
|
264
|
+
# @param pid [Integer] the process ID
|
63
265
|
# @param options [ProcessExecuter::Options] the options used
|
64
266
|
#
|
65
|
-
# @return [ProcessExecuter::Status] the status
|
267
|
+
# @return [ProcessExecuter::Status] the process status including Process::Status attributes and a timeout flag
|
66
268
|
#
|
67
269
|
# @api private
|
68
270
|
#
|
69
271
|
private_class_method def self.wait_for_process(pid, options)
|
70
272
|
Timeout.timeout(options.timeout) do
|
71
|
-
ProcessExecuter::Status.new(Process.wait2(pid).last, false)
|
273
|
+
ProcessExecuter::Status.new(Process.wait2(pid).last, false, options.timeout)
|
72
274
|
end
|
73
275
|
rescue Timeout::Error
|
74
276
|
Process.kill('KILL', pid)
|
75
|
-
ProcessExecuter::Status.new(Process.wait2(pid).last, true)
|
277
|
+
ProcessExecuter::Status.new(Process.wait2(pid).last, true, options.timeout)
|
76
278
|
end
|
77
279
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: process_executer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- James Couball
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-02-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler-audit
|
@@ -218,6 +218,10 @@ files:
|
|
218
218
|
- README.md
|
219
219
|
- Rakefile
|
220
220
|
- lib/process_executer.rb
|
221
|
+
- lib/process_executer/command.rb
|
222
|
+
- lib/process_executer/command/errors.rb
|
223
|
+
- lib/process_executer/command/result.rb
|
224
|
+
- lib/process_executer/command/runner.rb
|
221
225
|
- lib/process_executer/monitored_pipe.rb
|
222
226
|
- lib/process_executer/options.rb
|
223
227
|
- lib/process_executer/status.rb
|
@@ -231,8 +235,8 @@ metadata:
|
|
231
235
|
allowed_push_host: https://rubygems.org
|
232
236
|
homepage_uri: https://github.com/main-branch/process_executer
|
233
237
|
source_code_uri: https://github.com/main-branch/process_executer
|
234
|
-
documentation_uri: https://rubydoc.info/gems/process_executer/1.
|
235
|
-
changelog_uri: https://rubydoc.info/gems/process_executer/1.
|
238
|
+
documentation_uri: https://rubydoc.info/gems/process_executer/1.3.0
|
239
|
+
changelog_uri: https://rubydoc.info/gems/process_executer/1.3.0/file/CHANGELOG.md
|
236
240
|
rubygems_mfa_required: 'true'
|
237
241
|
post_install_message:
|
238
242
|
rdoc_options: []
|