process_executer 2.0.0 → 3.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 +23 -0
- data/README.md +156 -46
- data/lib/process_executer/destination_base.rb +83 -0
- data/lib/process_executer/destinations/child_redirection.rb +23 -0
- data/lib/process_executer/destinations/close.rb +23 -0
- data/lib/process_executer/destinations/file_descriptor.rb +36 -0
- data/lib/process_executer/destinations/file_path.rb +56 -0
- data/lib/process_executer/destinations/file_path_mode.rb +60 -0
- data/lib/process_executer/destinations/file_path_mode_perms.rb +61 -0
- data/lib/process_executer/destinations/io.rb +33 -0
- data/lib/process_executer/destinations/monitored_pipe.rb +39 -0
- data/lib/process_executer/destinations/stderr.rb +31 -0
- data/lib/process_executer/destinations/stdout.rb +31 -0
- data/lib/process_executer/destinations/tee.rb +60 -0
- data/lib/process_executer/destinations/writer.rb +33 -0
- data/lib/process_executer/destinations.rb +70 -0
- data/lib/process_executer/monitored_pipe.rb +40 -57
- data/lib/process_executer/options/base.rb +240 -0
- data/lib/process_executer/options/option_definition.rb +56 -0
- data/lib/process_executer/options/run_options.rb +48 -0
- data/lib/process_executer/options/spawn_and_wait_options.rb +39 -0
- data/lib/process_executer/options/spawn_options.rb +143 -0
- data/lib/process_executer/options.rb +7 -166
- data/lib/process_executer/result.rb +13 -23
- data/lib/process_executer/runner.rb +58 -50
- data/lib/process_executer/version.rb +1 -1
- data/lib/process_executer.rb +130 -21
- metadata +23 -4
@@ -1,171 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative 'options/base'
|
4
|
+
require_relative 'options/spawn_options'
|
5
|
+
require_relative 'options/spawn_and_wait_options'
|
6
|
+
require_relative 'options/run_options'
|
7
|
+
require_relative 'options/option_definition'
|
4
8
|
|
5
9
|
module ProcessExecuter
|
6
|
-
#
|
7
|
-
|
8
|
-
# Valid options are those accepted by Process.spawn plus the following additions:
|
9
|
-
#
|
10
|
-
# * `:timeout_after`: the number of seconds to allow a process to run before killing it
|
11
|
-
#
|
12
|
-
# @api public
|
13
|
-
#
|
14
|
-
class Options
|
15
|
-
# :nocov:
|
16
|
-
# SimpleCov on JRuby seems to hav a bug that causes hashes declared on multiple lines
|
17
|
-
# to not be counted as covered.
|
18
|
-
|
19
|
-
# These options should be passed to `Process.spawn`
|
20
|
-
#
|
21
|
-
# Additionally, any options whose key is an Integer or an IO object will
|
22
|
-
# be passed to `Process.spawn`.
|
23
|
-
#
|
24
|
-
SPAWN_OPTIONS = %i[
|
25
|
-
in out err unsetenv_others pgroup new_pgroup rlimit_resourcename umask
|
26
|
-
close_others chdir
|
27
|
-
].freeze
|
28
|
-
|
29
|
-
# These options are allowed by `ProcessExecuter.spawn` but should NOT be passed
|
30
|
-
# to `Process.spawn`
|
31
|
-
#
|
32
|
-
NON_SPAWN_OPTIONS = %i[
|
33
|
-
timeout_after raise_errors
|
34
|
-
].freeze
|
35
|
-
|
36
|
-
# Any `SPAWN_OPTIONS` set to `NOT_SET` will not be passed to `Process.spawn`
|
37
|
-
#
|
38
|
-
NOT_SET = :not_set
|
39
|
-
|
40
|
-
# The default values for all options
|
41
|
-
# @return [Hash]
|
42
|
-
DEFAULTS = {
|
43
|
-
in: NOT_SET,
|
44
|
-
out: NOT_SET,
|
45
|
-
err: NOT_SET,
|
46
|
-
unsetenv_others: NOT_SET,
|
47
|
-
pgroup: NOT_SET,
|
48
|
-
new_pgroup: NOT_SET,
|
49
|
-
rlimit_resourcename: NOT_SET,
|
50
|
-
umask: NOT_SET,
|
51
|
-
close_others: NOT_SET,
|
52
|
-
chdir: NOT_SET,
|
53
|
-
raise_errors: true,
|
54
|
-
timeout_after: nil
|
55
|
-
}.freeze
|
56
|
-
|
57
|
-
# :nocov:
|
58
|
-
|
59
|
-
# All options allowed by this class
|
60
|
-
#
|
61
|
-
ALL_OPTIONS = (SPAWN_OPTIONS + NON_SPAWN_OPTIONS).freeze
|
62
|
-
|
63
|
-
# Create accessor functions for all options. Assumes that the options are stored
|
64
|
-
# in a hash named `@options`
|
65
|
-
#
|
66
|
-
ALL_OPTIONS.each do |option|
|
67
|
-
define_method(option) do
|
68
|
-
@options[option]
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
# Create a new Options object
|
73
|
-
#
|
74
|
-
# @example
|
75
|
-
# options = ProcessExecuter::Options.new(out: $stdout, err: $stderr, timeout_after: 10)
|
76
|
-
#
|
77
|
-
# @param options [Hash] Process.spawn options plus additional options listed below.
|
78
|
-
#
|
79
|
-
# See [Process.spawn](https://ruby-doc.org/core/Process.html#method-c-spawn)
|
80
|
-
# for a list of valid options that can be passed to `Process.spawn`.
|
81
|
-
#
|
82
|
-
# @option options [Integer, Float, nil] :timeout_after
|
83
|
-
# Number of seconds to wait for the process to terminate. Any number
|
84
|
-
# may be used, including Floats to specify fractional seconds. A value of 0 or nil
|
85
|
-
# will allow the process to run indefinitely.
|
86
|
-
#
|
87
|
-
def initialize(**options)
|
88
|
-
assert_no_unknown_options(options)
|
89
|
-
@options = DEFAULTS.merge(options)
|
90
|
-
assert_timeout_is_valid
|
91
|
-
end
|
92
|
-
|
93
|
-
# Returns the options to be passed to Process.spawn
|
94
|
-
#
|
95
|
-
# @example
|
96
|
-
# options = ProcessExecuter::Options.new(out: $stdout, err: $stderr, timeout_after: 10)
|
97
|
-
# options.spawn_options # => { out: $stdout, err: $stderr }
|
98
|
-
#
|
99
|
-
# @return [Hash]
|
100
|
-
#
|
101
|
-
def spawn_options
|
102
|
-
{}.tap do |spawn_options|
|
103
|
-
options.each do |option, value|
|
104
|
-
spawn_options[option] = value if include_spawn_option?(option, value)
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
private
|
110
|
-
|
111
|
-
# @!attribute [r]
|
112
|
-
#
|
113
|
-
# Options with values
|
114
|
-
#
|
115
|
-
# All options have values. If an option is not given in the initializer, it
|
116
|
-
# will have the value `NOT_SET`.
|
117
|
-
#
|
118
|
-
# @return [Hash<Symbol, Object>]
|
119
|
-
#
|
120
|
-
# @api private
|
121
|
-
#
|
122
|
-
attr_reader :options
|
123
|
-
|
124
|
-
# Determine if the options hash contains any unknown options
|
125
|
-
# @param options [Hash] the hash of options
|
126
|
-
# @return [void]
|
127
|
-
# @raise [ArgumentError] if the options hash contains any unknown options
|
128
|
-
# @api private
|
129
|
-
def assert_no_unknown_options(options)
|
130
|
-
unknown_options = options.keys.reject { |key| valid_option?(key) }
|
131
|
-
raise ArgumentError, "Unknown options: #{unknown_options.join(', ')}" unless unknown_options.empty?
|
132
|
-
end
|
133
|
-
|
134
|
-
# Raise an error if timeout_after is not a non-negative real number
|
135
|
-
# @return [void]
|
136
|
-
# @raise [ArgumentError] if timeout_after is not a non-negative real number
|
137
|
-
# @api private
|
138
|
-
def assert_timeout_is_valid
|
139
|
-
return if @options[:timeout_after].nil?
|
140
|
-
return if @options[:timeout_after].is_a?(Numeric) &&
|
141
|
-
@options[:timeout_after].real? &&
|
142
|
-
!@options[:timeout_after].negative?
|
143
|
-
|
144
|
-
raise ArgumentError, invalid_timeout_after_message
|
145
|
-
end
|
146
|
-
|
147
|
-
# The message to be used when raising an error for an invalid timeout_after
|
148
|
-
# @return [String]
|
149
|
-
# @api private
|
150
|
-
def invalid_timeout_after_message
|
151
|
-
"timeout_after must be nil or a non-negative real number but was #{options[:timeout_after].pretty_inspect}"
|
152
|
-
end
|
153
|
-
|
154
|
-
# Determine if the given option is a valid option
|
155
|
-
# @param option [Symbol] the option to be tested
|
156
|
-
# @return [Boolean] true if the given option is a valid option
|
157
|
-
# @api private
|
158
|
-
def valid_option?(option)
|
159
|
-
ALL_OPTIONS.include?(option) || option.is_a?(Integer) || option.respond_to?(:fileno)
|
160
|
-
end
|
161
|
-
|
162
|
-
# Determine if the given option should be passed to `Process.spawn`
|
163
|
-
# @param option [Symbol, Integer, IO] the option to be tested
|
164
|
-
# @param value [Object] the value of the option
|
165
|
-
# @return [Boolean] true if the given option should be passed to `Process.spawn`
|
166
|
-
# @api private
|
167
|
-
def include_spawn_option?(option, value)
|
168
|
-
(option.is_a?(Integer) || option.is_a?(IO) || SPAWN_OPTIONS.include?(option)) && value != NOT_SET
|
169
|
-
end
|
170
|
-
end
|
10
|
+
# Options related to spawning or running a command
|
11
|
+
module Options; end
|
171
12
|
end
|
@@ -80,7 +80,9 @@ module ProcessExecuter
|
|
80
80
|
# result.timed_out? # => true
|
81
81
|
# @return [Boolean]
|
82
82
|
#
|
83
|
-
def timed_out?
|
83
|
+
def timed_out?
|
84
|
+
@timed_out
|
85
|
+
end
|
84
86
|
|
85
87
|
# Overrides the default success? method to return nil if the process timed out
|
86
88
|
#
|
@@ -107,9 +109,8 @@ module ProcessExecuter
|
|
107
109
|
|
108
110
|
# Return the captured stdout output
|
109
111
|
#
|
110
|
-
# This output is only returned if the `:out` option
|
111
|
-
# `ProcessExecuter::MonitoredPipe
|
112
|
-
# method (e.g. a StringIO).
|
112
|
+
# This output is only returned if the `:out` option value is a
|
113
|
+
# `ProcessExecuter::MonitoredPipe`.
|
113
114
|
#
|
114
115
|
# @example
|
115
116
|
# # Note that `ProcessExecuter.run` will wrap the given out: object in a
|
@@ -120,22 +121,16 @@ module ProcessExecuter
|
|
120
121
|
# @return [String, nil]
|
121
122
|
#
|
122
123
|
def stdout
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
pipe.writers.each do |writer|
|
127
|
-
return writer.string if writer.respond_to?(:string)
|
128
|
-
end
|
129
|
-
end
|
124
|
+
pipe = options.stdout_redirection_value
|
125
|
+
return nil unless pipe.is_a?(ProcessExecuter::MonitoredPipe)
|
130
126
|
|
131
|
-
|
127
|
+
pipe.destination.string
|
132
128
|
end
|
133
129
|
|
134
130
|
# Return the captured stderr output
|
135
131
|
#
|
136
|
-
# This output is only returned if the `:err` option
|
137
|
-
# `ProcessExecuter::MonitoredPipe
|
138
|
-
# method (e.g. a StringIO).
|
132
|
+
# This output is only returned if the `:err` option value is a
|
133
|
+
# `ProcessExecuter::MonitoredPipe`.
|
139
134
|
#
|
140
135
|
# @example
|
141
136
|
# # Note that `ProcessExecuter.run` will wrap the given err: object in a
|
@@ -146,15 +141,10 @@ module ProcessExecuter
|
|
146
141
|
# @return [String, nil]
|
147
142
|
#
|
148
143
|
def stderr
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
pipe.writers.each do |writer|
|
153
|
-
return writer.string if writer.respond_to?(:string)
|
154
|
-
end
|
155
|
-
end
|
144
|
+
pipe = options.stderr_redirection_value
|
145
|
+
return nil unless pipe.is_a?(ProcessExecuter::MonitoredPipe)
|
156
146
|
|
157
|
-
|
147
|
+
pipe.destination.string
|
158
148
|
end
|
159
149
|
end
|
160
150
|
end
|
@@ -15,24 +15,6 @@ module ProcessExecuter
|
|
15
15
|
# @api public
|
16
16
|
#
|
17
17
|
class Runner
|
18
|
-
# Create a new RunCommand instance
|
19
|
-
#
|
20
|
-
# @example
|
21
|
-
# runner = Runner.new()
|
22
|
-
# result = runner.call('echo', 'hello')
|
23
|
-
#
|
24
|
-
# @param logger [Logger] The logger to use. Defaults to a no-op logger if nil.
|
25
|
-
#
|
26
|
-
def initialize(logger = Logger.new(nil))
|
27
|
-
@logger = logger
|
28
|
-
end
|
29
|
-
|
30
|
-
# The logger to use
|
31
|
-
# @example
|
32
|
-
# runner.logger #=> #<Logger:0x00007f9b1b8b3d20>
|
33
|
-
# @return [Logger]
|
34
|
-
attr_reader :logger
|
35
|
-
|
36
18
|
# Run a command and return the status including stdout and stderr output
|
37
19
|
#
|
38
20
|
# @example
|
@@ -45,20 +27,12 @@ module ProcessExecuter
|
|
45
27
|
# result.stderr # => ""
|
46
28
|
#
|
47
29
|
# @param command [Array<String>] The command to run
|
48
|
-
# @param
|
49
|
-
# @param err [#write, Array<#write>, nil] The object (or array of objects) to which stderr is written
|
50
|
-
# @param merge [Boolean] Write both stdout and stderr into the buffer for stdout
|
51
|
-
# @param options_hash [Hash] Additional options to pass to Process.spawn
|
52
|
-
#
|
53
|
-
# See {ProcessExecuter.run} for a full list of options.
|
30
|
+
# @param options [ProcessExecuter::Options::RunOptions] Options for running the command
|
54
31
|
#
|
55
32
|
# @return [ProcessExecuter::Result] The result of the completed subprocess
|
56
33
|
#
|
57
|
-
def call(
|
58
|
-
|
59
|
-
err ||= (merge ? out : StringIO.new)
|
60
|
-
|
61
|
-
spawn(command, out:, err:, **options_hash).tap { |result| process_result(result) }
|
34
|
+
def call(command, options)
|
35
|
+
spawn(command, options).tap { |result| process_result(result) }
|
62
36
|
end
|
63
37
|
|
64
38
|
private
|
@@ -66,11 +40,7 @@ module ProcessExecuter
|
|
66
40
|
# Wrap the output buffers in pipes and then execute the command
|
67
41
|
#
|
68
42
|
# @param command [Array<String>] The command to execute
|
69
|
-
# @param
|
70
|
-
# @param err [#write, Array<#write>] The object (or array of objects) to which stderr is written
|
71
|
-
# @param options_hash [Hash] Additional options to pass to Process.spawn
|
72
|
-
#
|
73
|
-
# See {ProcessExecuter.run} for a full list of options.
|
43
|
+
# @param options [ProcessExecuter::Options::RunOptions] Options for running the command
|
74
44
|
#
|
75
45
|
# @raise [ProcessExecuter::ProcessIOError] If an exception was raised while collecting subprocess output
|
76
46
|
# @raise [ProcessExecuter::TimeoutError] If the command times out
|
@@ -79,17 +49,47 @@ module ProcessExecuter
|
|
79
49
|
#
|
80
50
|
# @api private
|
81
51
|
#
|
82
|
-
def spawn(command,
|
83
|
-
|
84
|
-
|
85
|
-
out_pipe = ProcessExecuter::MonitoredPipe.new(*out)
|
86
|
-
err_pipe = ProcessExecuter::MonitoredPipe.new(*err)
|
87
|
-
ProcessExecuter.spawn_and_wait(*command, out: out_pipe, err: err_pipe, **options_hash)
|
52
|
+
def spawn(command, options)
|
53
|
+
opened_pipes = wrap_stdout_stderr(options)
|
54
|
+
ProcessExecuter.spawn_and_wait_with_options(command, options)
|
88
55
|
ensure
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
56
|
+
opened_pipes.each { |key, value| close_pipe(command, key, value) }
|
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
|
+
# Close the pipe and raise an error if the pipe raised an exception
|
86
|
+
# @return [void]
|
87
|
+
# @raise [ProcessExecuter::ProcessIOError] If an exception was raised while
|
88
|
+
# collecting subprocess output
|
89
|
+
# @api private
|
90
|
+
def close_pipe(command, option_key, pipe)
|
91
|
+
pipe.close
|
92
|
+
raise_pipe_error(command, option_key, pipe) if pipe.exception
|
93
93
|
end
|
94
94
|
|
95
95
|
# Process the result of the command and return a ProcessExecuter::Result
|
@@ -110,8 +110,16 @@ module ProcessExecuter
|
|
110
110
|
def process_result(result)
|
111
111
|
log_result(result)
|
112
112
|
|
113
|
-
|
113
|
+
raise_errors(result) if result.options.raise_errors
|
114
|
+
end
|
114
115
|
|
116
|
+
# Raise an error if the command failed
|
117
|
+
# @return [void]
|
118
|
+
# @raise [ProcessExecuter::FailedError] If the command failed
|
119
|
+
# @raise [ProcessExecuter::SignaledError] If the command was signaled
|
120
|
+
# @raise [ProcessExecuter::TimeoutError] If the command times out
|
121
|
+
# @api private
|
122
|
+
def raise_errors(result)
|
115
123
|
raise TimeoutError, result if result.timed_out?
|
116
124
|
raise SignaledError, result if result.signaled?
|
117
125
|
raise FailedError, result unless result.success?
|
@@ -123,14 +131,14 @@ module ProcessExecuter
|
|
123
131
|
# @return [void]
|
124
132
|
# @api private
|
125
133
|
def log_result(result)
|
126
|
-
logger.info { "#{result.command} exited with status #{result}" }
|
127
|
-
logger.debug { "stdout:\n#{result.stdout.inspect}\nstderr:\n#{result.stderr.inspect}" }
|
134
|
+
result.options.logger.info { "#{result.command} exited with status #{result}" }
|
135
|
+
result.options.logger.debug { "stdout:\n#{result.stdout.inspect}\nstderr:\n#{result.stderr.inspect}" }
|
128
136
|
end
|
129
137
|
|
130
138
|
# Raise an error when there was exception while collecting the subprocess output
|
131
139
|
#
|
132
140
|
# @param command [Array<String>] The command that was executed
|
133
|
-
# @param
|
141
|
+
# @param option_key [Symbol] The name of the pipe that raised the exception
|
134
142
|
# @param pipe [ProcessExecuter::MonitoredPipe] The pipe that raised the exception
|
135
143
|
#
|
136
144
|
# @raise [ProcessExecuter::ProcessIOError]
|
@@ -139,8 +147,8 @@ module ProcessExecuter
|
|
139
147
|
#
|
140
148
|
# @api private
|
141
149
|
#
|
142
|
-
def raise_pipe_error(command,
|
143
|
-
error = ProcessExecuter::ProcessIOError.new("Pipe Exception for #{command}: #{
|
150
|
+
def raise_pipe_error(command, option_key, pipe)
|
151
|
+
error = ProcessExecuter::ProcessIOError.new("Pipe Exception for #{command}: #{option_key.inspect}")
|
144
152
|
raise(error, cause: pipe.exception)
|
145
153
|
end
|
146
154
|
end
|
data/lib/process_executer.rb
CHANGED
@@ -1,25 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'logger'
|
4
|
+
require 'timeout'
|
5
|
+
|
6
|
+
require 'process_executer/destination_base'
|
7
|
+
require 'process_executer/destinations'
|
3
8
|
require 'process_executer/errors'
|
4
9
|
require 'process_executer/monitored_pipe'
|
5
10
|
require 'process_executer/options'
|
6
11
|
require 'process_executer/result'
|
7
12
|
require 'process_executer/runner'
|
8
13
|
|
9
|
-
require 'logger'
|
10
|
-
require 'timeout'
|
11
|
-
|
12
14
|
# The `ProcessExecuter` module provides methods to execute subprocess commands
|
13
15
|
# with enhanced features such as output capture, timeout handling, and custom
|
14
16
|
# environment variables.
|
15
17
|
#
|
16
18
|
# Methods:
|
19
|
+
#
|
17
20
|
# * {run}: Executes a command and returns the result which includes the process
|
18
21
|
# status and output
|
19
22
|
# * {spawn_and_wait}: a thin wrapper around `Process.spawn` that blocks until the
|
20
23
|
# command finishes
|
21
24
|
#
|
22
25
|
# Features:
|
26
|
+
#
|
23
27
|
# * Supports executing commands via a shell or directly.
|
24
28
|
# * Captures stdout and stderr to buffers, files, or custom objects.
|
25
29
|
# * Optionally enforces timeouts and terminates long-running commands.
|
@@ -27,7 +31,6 @@ require 'timeout'
|
|
27
31
|
# options that were given, and success, failure, or timeout states.
|
28
32
|
#
|
29
33
|
# @api public
|
30
|
-
#
|
31
34
|
module ProcessExecuter
|
32
35
|
# Run a command in a subprocess, wait for it to finish, then return the result
|
33
36
|
#
|
@@ -70,7 +73,20 @@ module ProcessExecuter
|
|
70
73
|
# @return [ProcessExecuter::Result] The result of the completed subprocess
|
71
74
|
#
|
72
75
|
def self.spawn_and_wait(*command, **options_hash)
|
73
|
-
options = ProcessExecuter
|
76
|
+
options = ProcessExecuter.spawn_and_wait_options(options_hash)
|
77
|
+
spawn_and_wait_with_options(command, options)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Run a command in a subprocess, wait for it to finish, then return the result
|
81
|
+
#
|
82
|
+
# @see ProcessExecuter.spawn_and_wait for full documentation
|
83
|
+
#
|
84
|
+
# @param command [Array<String>] The command to run
|
85
|
+
# @param options [ProcessExecuter::Options::SpawnAndWaitOptions] The options to use when running the command
|
86
|
+
#
|
87
|
+
# @return [ProcessExecuter::Result] The result of the completed subprocess
|
88
|
+
# @api private
|
89
|
+
def self.spawn_and_wait_with_options(command, options)
|
74
90
|
pid = Process.spawn(*command, **options.spawn_options)
|
75
91
|
wait_for_process(pid, command, options)
|
76
92
|
end
|
@@ -83,14 +99,11 @@ module ProcessExecuter
|
|
83
99
|
# which can be accessed via the Result object in `result.options.out`. The
|
84
100
|
# same applies to `err`.
|
85
101
|
#
|
86
|
-
# 2.
|
87
|
-
# buffer.
|
88
|
-
#
|
89
|
-
# 3. `out` and `err` are automatically wrapped in a
|
102
|
+
# 2. `out` and `err` are automatically wrapped in a
|
90
103
|
# `ProcessExecuter::MonitoredPipe` object so that any object that implements
|
91
104
|
# `#write` (or an Array of such objects) can be given for `out` and `err`.
|
92
105
|
#
|
93
|
-
#
|
106
|
+
# 3. Raises one of the following errors unless `raise_errors` is explicitly set
|
94
107
|
# to `false`:
|
95
108
|
#
|
96
109
|
# * `ProcessExecuter::FailedError` if the command returns a non-zero
|
@@ -101,10 +114,10 @@ module ProcessExecuter
|
|
101
114
|
#
|
102
115
|
# If `raise_errors` is false, the returned Result object will contain the error.
|
103
116
|
#
|
104
|
-
#
|
117
|
+
# 4. Raises a `ProcessExecuter::ProcessIOError` if an exception is raised
|
105
118
|
# while collecting subprocess output. This can not be turned off.
|
106
119
|
#
|
107
|
-
#
|
120
|
+
# 5. If a `logger` is provided, it will be used to log:
|
108
121
|
#
|
109
122
|
# * The command that was executed and its status to `info` level
|
110
123
|
# * The stdout and stderr output to `debug` level
|
@@ -199,7 +212,7 @@ module ProcessExecuter
|
|
199
212
|
#
|
200
213
|
# @example Capture stdout and stderr into a single buffer
|
201
214
|
# command = ['echo "stdout" && echo "stderr" 1>&2']
|
202
|
-
# result = ProcessExecuter.run(*command,
|
215
|
+
# result = ProcessExecuter.run(*command, [out:, err:]: StringIO.new)
|
203
216
|
# result.stdout #=> "stdout\nstderr\n"
|
204
217
|
# result.stderr #=> "stdout\nstderr\n"
|
205
218
|
# result.stdout.object_id == result.stderr.object_id #=> true
|
@@ -223,18 +236,16 @@ module ProcessExecuter
|
|
223
236
|
# # stderr is still captured to a StringIO buffer internally
|
224
237
|
# result.stderr #=> "stderr\n"
|
225
238
|
#
|
226
|
-
# @example Capture to multiple
|
239
|
+
# @example Capture to multiple destinations (e.g. files, buffers, STDOUT, etc.)
|
227
240
|
# # Same technique can be used for stderr
|
228
241
|
# out_buffer = StringIO.new
|
229
242
|
# out_file = File.open('stdout.txt', 'w')
|
230
243
|
# command = ['echo "stdout" && echo "stderr" 1>&2']
|
231
|
-
# result = ProcessExecuter.run(*command, out: [out_buffer, out_file])
|
244
|
+
# result = ProcessExecuter.run(*command, out: [:tee, out_buffer, out_file])
|
232
245
|
# # You must manage closing resources you create yourself
|
233
246
|
# out_file.close
|
234
247
|
# out_buffer.string #=> "stdout\n"
|
235
248
|
# File.read('stdout.txt') #=> "stdout\n"
|
236
|
-
# # Since one of the out writers has a #string method, Result#stdout will
|
237
|
-
# # return the string from that writer
|
238
249
|
# result.stdout #=> "stdout\n"
|
239
250
|
#
|
240
251
|
# @param command [Array<String>] The command to run
|
@@ -252,7 +263,6 @@ module ProcessExecuter
|
|
252
263
|
# Otherwise, the command is run bypassing the shell. When bypassing the shell, shell expansions
|
253
264
|
# and redirections are not supported.
|
254
265
|
#
|
255
|
-
# @param logger [Logger] The logger to use
|
256
266
|
# @param options_hash [Hash] Additional options
|
257
267
|
# @option options_hash [Numeric] :timeout_after The maximum seconds to wait for the
|
258
268
|
# command to complete
|
@@ -265,7 +275,6 @@ module ProcessExecuter
|
|
265
275
|
#
|
266
276
|
# @option options_hash [#write] :out (nil) The object to write stdout to
|
267
277
|
# @option options_hash [#write] :err (nil) The object to write stderr to
|
268
|
-
# @option options_hash [Boolean] :merge (false) If true, stdout and stderr are written to the same capture buffer
|
269
278
|
# @option options_hash [Boolean] :raise_errors (true) Raise an exception if the command fails
|
270
279
|
# @option options_hash [Boolean] :unsetenv_others (false) If true, unset all environment variables before
|
271
280
|
# applying the new ones
|
@@ -276,6 +285,7 @@ module ProcessExecuter
|
|
276
285
|
# @option options_hash [Integer] :umask (nil) Set the umask (see File.umask)
|
277
286
|
# @option options_hash [Boolean] :close_others (false) If true, close non-standard file descriptors
|
278
287
|
# @option options_hash [String] :chdir (nil) The directory to run the command in
|
288
|
+
# @option options_hash [Logger] :logger The logger to use
|
279
289
|
#
|
280
290
|
# @raise [ProcessExecuter::FailedError] if the command returned a non-zero exit status
|
281
291
|
# @raise [ProcessExecuter::SignaledError] if the command exited because of an unhandled signal
|
@@ -284,8 +294,23 @@ module ProcessExecuter
|
|
284
294
|
#
|
285
295
|
# @return [ProcessExecuter::Result] The result of the completed subprocess
|
286
296
|
#
|
287
|
-
def self.run(*command,
|
288
|
-
ProcessExecuter
|
297
|
+
def self.run(*command, **options_hash)
|
298
|
+
options = ProcessExecuter.run_options(options_hash)
|
299
|
+
run_with_options(command, options)
|
300
|
+
end
|
301
|
+
|
302
|
+
# Run a command with the given options
|
303
|
+
#
|
304
|
+
# @see ProcessExecuter.run for full documentation
|
305
|
+
#
|
306
|
+
# @param command [Array<String>] The command to run
|
307
|
+
# @param options [ProcessExecuter::Options::RunOptions] The options to use when running the command
|
308
|
+
#
|
309
|
+
# @return [ProcessExecuter::Result] The result of the completed subprocess
|
310
|
+
#
|
311
|
+
# @api private
|
312
|
+
def self.run_with_options(command, options)
|
313
|
+
ProcessExecuter::Runner.new.call(command, options)
|
289
314
|
end
|
290
315
|
|
291
316
|
# Wait for process to terminate
|
@@ -328,4 +353,88 @@ module ProcessExecuter
|
|
328
353
|
|
329
354
|
[process_status, timed_out]
|
330
355
|
end
|
356
|
+
|
357
|
+
# Convert a hash to a SpawnOptions object
|
358
|
+
#
|
359
|
+
# @example
|
360
|
+
# options_hash = { out: $stdout }
|
361
|
+
# options = ProcessExecuter.spawn_options(options_hash) # =>
|
362
|
+
# #<ProcessExecuter::Options::SpawnOptions:0x00007f8f9b0b3d20 out: $stdout>
|
363
|
+
# ProcessExecuter.spawn_options(options) # =>
|
364
|
+
# #<ProcessExecuter::Options::SpawnOptions:0x00007f8f9b0b3d20 out: $stdout>
|
365
|
+
#
|
366
|
+
# @param obj [Hash, SpawnOptions] the object to be converted
|
367
|
+
#
|
368
|
+
# @return [SpawnOptions]
|
369
|
+
#
|
370
|
+
# @raise [ArgumentError] if obj is not a Hash or SpawnOptions
|
371
|
+
#
|
372
|
+
# @api public
|
373
|
+
#
|
374
|
+
def self.spawn_options(obj)
|
375
|
+
case obj
|
376
|
+
when ProcessExecuter::Options::SpawnOptions
|
377
|
+
obj
|
378
|
+
when Hash
|
379
|
+
ProcessExecuter::Options::SpawnOptions.new(**obj)
|
380
|
+
else
|
381
|
+
raise ArgumentError, "Expected a Hash or ProcessExecuter::Options::SpawnOptions but got a #{obj.class}"
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
# Convert a hash to a SpawnAndWaitOptions object
|
386
|
+
#
|
387
|
+
# @example
|
388
|
+
# options_hash = { out: $stdout }
|
389
|
+
# options = ProcessExecuter.spawn_and_wait_options(options_hash) # =>
|
390
|
+
# #<ProcessExecuter::Options::SpawnAndWaitOptions:0x00007f8f9b0b3d20 out: $stdout>
|
391
|
+
# ProcessExecuter.spawn_and_wait_options(options) # =>
|
392
|
+
# #<ProcessExecuter::Options::SpawnAndWaitOptions:0x00007f8f9b0b3d20 out: $stdout>
|
393
|
+
#
|
394
|
+
# @param obj [Hash, SpawnAndWaitOptions] the object to be converted
|
395
|
+
#
|
396
|
+
# @return [SpawnAndWaitOptions]
|
397
|
+
#
|
398
|
+
# @raise [ArgumentError] if obj is not a Hash or SpawnOptions
|
399
|
+
#
|
400
|
+
# @api public
|
401
|
+
#
|
402
|
+
def self.spawn_and_wait_options(obj)
|
403
|
+
case obj
|
404
|
+
when ProcessExecuter::Options::SpawnAndWaitOptions
|
405
|
+
obj
|
406
|
+
when Hash
|
407
|
+
ProcessExecuter::Options::SpawnAndWaitOptions.new(**obj)
|
408
|
+
else
|
409
|
+
raise ArgumentError, "Expected a Hash or ProcessExecuter::Options::SpawnAndWaitOptions but got a #{obj.class}"
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
# Convert a hash to a RunOptions object
|
414
|
+
#
|
415
|
+
# @example
|
416
|
+
# options_hash = { out: $stdout }
|
417
|
+
# options = ProcessExecuter.run_options(options_hash) # =>
|
418
|
+
# #<ProcessExecuter::Options::RunOptions:0x00007f8f9b0b3d20 out: $stdout>
|
419
|
+
# ProcessExecuter.run_options(options) # =>
|
420
|
+
# #<ProcessExecuter::Options::RunOptions:0x00007f8f9b0b3d20 out: $stdout>
|
421
|
+
#
|
422
|
+
# @param obj [Hash, RunOptions] the object to be converted
|
423
|
+
#
|
424
|
+
# @return [RunOptions]
|
425
|
+
#
|
426
|
+
# @raise [ArgumentError] if obj is not a Hash or SpawnOptions
|
427
|
+
#
|
428
|
+
# @api public
|
429
|
+
#
|
430
|
+
def self.run_options(obj)
|
431
|
+
case obj
|
432
|
+
when ProcessExecuter::Options::RunOptions
|
433
|
+
obj
|
434
|
+
when Hash
|
435
|
+
ProcessExecuter::Options::RunOptions.new(**obj)
|
436
|
+
else
|
437
|
+
raise ArgumentError, "Expected a Hash or ProcessExecuter::Options::RunOptions but got a #{obj.class}"
|
438
|
+
end
|
439
|
+
end
|
331
440
|
end
|