process_executer 3.2.4 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +41 -0
- data/README.md +177 -134
- data/lib/process_executer/commands/run.rb +124 -0
- data/lib/process_executer/commands/run_with_capture.rb +148 -0
- data/lib/process_executer/commands/spawn_with_timeout.rb +163 -0
- data/lib/process_executer/commands.rb +11 -0
- data/lib/process_executer/destinations/child_redirection.rb +5 -4
- data/lib/process_executer/destinations/close.rb +5 -4
- data/lib/process_executer/destinations/destination_base.rb +73 -0
- data/lib/process_executer/destinations/file_descriptor.rb +10 -6
- data/lib/process_executer/destinations/file_path.rb +12 -6
- data/lib/process_executer/destinations/file_path_mode.rb +10 -6
- data/lib/process_executer/destinations/file_path_mode_perms.rb +12 -5
- data/lib/process_executer/destinations/io.rb +10 -5
- data/lib/process_executer/destinations/monitored_pipe.rb +10 -5
- data/lib/process_executer/destinations/stderr.rb +8 -4
- data/lib/process_executer/destinations/stdout.rb +8 -4
- data/lib/process_executer/destinations/tee.rb +24 -17
- data/lib/process_executer/destinations/writer.rb +12 -7
- data/lib/process_executer/destinations.rb +32 -17
- data/lib/process_executer/errors.rb +50 -26
- data/lib/process_executer/monitored_pipe.rb +128 -59
- data/lib/process_executer/options/base.rb +118 -82
- data/lib/process_executer/options/option_definition.rb +5 -1
- data/lib/process_executer/options/run_options.rb +13 -12
- data/lib/process_executer/options/run_with_capture_options.rb +156 -0
- data/lib/process_executer/options/spawn_options.rb +31 -30
- data/lib/process_executer/options/{spawn_and_wait_options.rb → spawn_with_timeout_options.rb} +11 -7
- data/lib/process_executer/options.rb +3 -1
- data/lib/process_executer/result.rb +35 -77
- data/lib/process_executer/result_with_capture.rb +62 -0
- data/lib/process_executer/version.rb +2 -1
- data/lib/process_executer.rb +384 -346
- data/process_executer.gemspec +11 -2
- metadata +18 -8
- data/lib/process_executer/destination_base.rb +0 -83
- data/lib/process_executer/runner.rb +0 -144
@@ -1,19 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative 'spawn_with_timeout_options'
|
4
4
|
require_relative 'option_definition'
|
5
5
|
|
6
6
|
module ProcessExecuter
|
7
7
|
module Options
|
8
|
-
# Define options for
|
8
|
+
# Define options for {ProcessExecuter.run}
|
9
9
|
#
|
10
10
|
# @api public
|
11
11
|
#
|
12
|
-
class RunOptions <
|
12
|
+
class RunOptions < SpawnWithTimeoutOptions
|
13
13
|
private
|
14
14
|
|
15
|
-
# :nocov: SimpleCov on JRuby reports the last with the last argument line is not covered
|
16
|
-
|
17
15
|
# The options allowed for objects of this class
|
18
16
|
# @return [Array<OptionDefinition>]
|
19
17
|
# @api private
|
@@ -24,21 +22,24 @@ module ProcessExecuter
|
|
24
22
|
OptionDefinition.new(:logger, default: Logger.new(nil), validator: method(:validate_logger))
|
25
23
|
].freeze
|
26
24
|
end
|
27
|
-
# :nocov:
|
28
25
|
|
29
|
-
#
|
30
|
-
# @
|
26
|
+
# Note an error if raise_errors is not true or false
|
27
|
+
# @param _key [Symbol] the option key (not used)
|
28
|
+
# @param _value [Object] the option value (not used)
|
29
|
+
# @return [Void]
|
31
30
|
# @api private
|
32
|
-
def validate_raise_errors
|
31
|
+
def validate_raise_errors(_key, _value)
|
33
32
|
return if [true, false].include?(raise_errors)
|
34
33
|
|
35
34
|
errors << "raise_errors must be true or false but was #{raise_errors.inspect}"
|
36
35
|
end
|
37
36
|
|
38
|
-
#
|
39
|
-
# @
|
37
|
+
# Note an error if the logger option is not valid
|
38
|
+
# @param _key [Symbol] the option key (not used)
|
39
|
+
# @param _value [Object] the option value (not used)
|
40
|
+
# @return [Void]
|
40
41
|
# @api private
|
41
|
-
def validate_logger
|
42
|
+
def validate_logger(_key, _value)
|
42
43
|
return if logger.respond_to?(:info) && logger.respond_to?(:debug)
|
43
44
|
|
44
45
|
errors << "logger must respond to #info and #debug but was #{logger.inspect}"
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'option_definition'
|
4
|
+
require_relative 'run_options'
|
5
|
+
|
6
|
+
module ProcessExecuter
|
7
|
+
module Options
|
8
|
+
# Define options for {ProcessExecuter.run_with_capture}
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
#
|
12
|
+
class RunWithCaptureOptions < RunOptions
|
13
|
+
# The default encoding used for stdout and stderr
|
14
|
+
# if no other encoding is specified.
|
15
|
+
#
|
16
|
+
# @return [Encoding]
|
17
|
+
#
|
18
|
+
DEFAULT_ENCODING = Encoding::UTF_8
|
19
|
+
|
20
|
+
# Determines the character encoding to use for stdout
|
21
|
+
#
|
22
|
+
# It prioritizes `stdout_encoding` if set, otherwise falls back to
|
23
|
+
# `encoding`, and finally defaults to `DEFAULT_ENCODING` if neither
|
24
|
+
# is available.
|
25
|
+
#
|
26
|
+
# @return [Encoding]
|
27
|
+
#
|
28
|
+
# @api private
|
29
|
+
#
|
30
|
+
def effective_stdout_encoding
|
31
|
+
stdout_encoding || encoding || DEFAULT_ENCODING
|
32
|
+
end
|
33
|
+
|
34
|
+
# Determines the character encoding to use for stderr
|
35
|
+
#
|
36
|
+
# It prioritizes `stderr_encoding` if set, otherwise falls back to
|
37
|
+
# `encoding`, and finally defaults to `DEFAULT_ENCODING` if neither
|
38
|
+
# is available.
|
39
|
+
#
|
40
|
+
# @return [Encoding]
|
41
|
+
#
|
42
|
+
# @api private
|
43
|
+
#
|
44
|
+
def effective_stderr_encoding
|
45
|
+
stderr_encoding || encoding || DEFAULT_ENCODING
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# The options allowed for objects of this class
|
51
|
+
# @return [Array<OptionDefinition>]
|
52
|
+
# @api private
|
53
|
+
def define_options
|
54
|
+
[
|
55
|
+
*super,
|
56
|
+
OptionDefinition.new(:merge_output, default: false, validator: method(:validate_merge_output)),
|
57
|
+
OptionDefinition.new(:encoding, default: DEFAULT_ENCODING, validator: method(:validate_encoding_option)),
|
58
|
+
OptionDefinition.new(:stdout_encoding, default: nil, validator: method(:validate_encoding_option)),
|
59
|
+
OptionDefinition.new(:stderr_encoding, default: nil, validator: method(:validate_encoding_option))
|
60
|
+
].freeze
|
61
|
+
end
|
62
|
+
|
63
|
+
# Note any errors in the merge_output option
|
64
|
+
#
|
65
|
+
# Possible errors include:
|
66
|
+
# - if the merge_output value is not a Boolean
|
67
|
+
# - if merge_output: true and a stderr redirection is given
|
68
|
+
# - if merge_output: true and stdout and stderr encodings are different
|
69
|
+
#
|
70
|
+
# @param _key [Symbol] the option key (not used)
|
71
|
+
# @param _value [Object] the option value (not used)
|
72
|
+
# @return [Void]
|
73
|
+
# @api private
|
74
|
+
def validate_merge_output(_key, _value)
|
75
|
+
unless [true, false].include?(merge_output)
|
76
|
+
errors << "merge_output must be true or false but was #{merge_output.inspect}"
|
77
|
+
end
|
78
|
+
|
79
|
+
return unless merge_output == true
|
80
|
+
|
81
|
+
errors << 'Cannot give merge_output: true AND a stderr redirection' if stderr_redirection_source
|
82
|
+
|
83
|
+
return if effective_stdout_encoding == effective_stderr_encoding
|
84
|
+
|
85
|
+
errors << 'Cannot give merge_output: true AND give different encodings for stdout and stderr'
|
86
|
+
end
|
87
|
+
|
88
|
+
# Note an error if the encoding option is not valid
|
89
|
+
# @param key [Symbol] the option key
|
90
|
+
# @param value [Object] the option value
|
91
|
+
# @return [Void]
|
92
|
+
# @api private
|
93
|
+
def validate_encoding_option(key, value)
|
94
|
+
return unless valid_encoding_type?(key, value)
|
95
|
+
|
96
|
+
return if value.nil? || value.is_a?(Encoding)
|
97
|
+
|
98
|
+
validate_encoding_symbol(key, value) if value.is_a?(Symbol)
|
99
|
+
|
100
|
+
validate_encoding_string(key, value) if value.is_a?(String)
|
101
|
+
end
|
102
|
+
|
103
|
+
# False if the value is not a valid encoding type, true otherwise
|
104
|
+
#
|
105
|
+
# @param key [Symbol] the option key
|
106
|
+
#
|
107
|
+
# @param value [Object] the option value
|
108
|
+
#
|
109
|
+
# @return [Boolean]
|
110
|
+
#
|
111
|
+
# @api private
|
112
|
+
#
|
113
|
+
def valid_encoding_type?(key, value)
|
114
|
+
return true if value.nil? || value.is_a?(Encoding) || value.is_a?(Symbol) || value.is_a?(String)
|
115
|
+
|
116
|
+
errors << "#{key} must be an Encoding object, String, Symbol (:binary, :default_external), " \
|
117
|
+
"or nil, but was #{value.inspect}"
|
118
|
+
|
119
|
+
false
|
120
|
+
end
|
121
|
+
|
122
|
+
# Note an error if the encoding symbol is not valid
|
123
|
+
#
|
124
|
+
# @param key [Symbol] the option key
|
125
|
+
#
|
126
|
+
# @param value [Symbol] the option value
|
127
|
+
#
|
128
|
+
# @return [Void]
|
129
|
+
#
|
130
|
+
# @api private
|
131
|
+
#
|
132
|
+
def validate_encoding_symbol(key, value)
|
133
|
+
return if %i[binary default_external].include?(value)
|
134
|
+
|
135
|
+
errors << "#{key} when given as a symbol must be :binary or :default_external, " \
|
136
|
+
"but was #{value.inspect}"
|
137
|
+
end
|
138
|
+
|
139
|
+
# Note an error if the encoding string is not valid
|
140
|
+
#
|
141
|
+
# @param key [Symbol] the option key
|
142
|
+
#
|
143
|
+
# @param value [String] the option value
|
144
|
+
#
|
145
|
+
# @return [void]
|
146
|
+
#
|
147
|
+
# @api private
|
148
|
+
#
|
149
|
+
def validate_encoding_string(key, value)
|
150
|
+
Encoding.find(value)
|
151
|
+
rescue ::ArgumentError
|
152
|
+
errors << "#{key} specifies an unknown encoding name: #{value.inspect}"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -5,16 +5,22 @@ require_relative 'option_definition'
|
|
5
5
|
|
6
6
|
module ProcessExecuter
|
7
7
|
module Options
|
8
|
-
#
|
8
|
+
# Defines and validates options accepted by `Process.spawn`
|
9
9
|
#
|
10
|
-
#
|
10
|
+
# Allows subclasses to add additional options that are not passed to `Process.spawn`.
|
11
11
|
#
|
12
|
-
#
|
12
|
+
# Provides a method (#spawn_options) to retrieve only those options directly
|
13
|
+
# applicable to Process.spawn.
|
13
14
|
#
|
14
15
|
# @api public
|
15
16
|
#
|
16
17
|
class SpawnOptions < Base
|
17
|
-
#
|
18
|
+
# Options that are passed to Process.spawn
|
19
|
+
#
|
20
|
+
# They are not passed if the value is :not_set
|
21
|
+
#
|
22
|
+
# @return [Array<OptionDefinition>]
|
23
|
+
#
|
18
24
|
SPAWN_OPTIONS = [
|
19
25
|
OptionDefinition.new(:unsetenv_others, default: :not_set),
|
20
26
|
OptionDefinition.new(:pgroup, default: :not_set),
|
@@ -24,19 +30,21 @@ module ProcessExecuter
|
|
24
30
|
OptionDefinition.new(:close_others, default: :not_set),
|
25
31
|
OptionDefinition.new(:chdir, default: :not_set)
|
26
32
|
].freeze
|
27
|
-
# :nocov:
|
28
33
|
|
29
34
|
# Returns the options to be passed to Process.spawn
|
30
35
|
#
|
36
|
+
# Any options added by subclasses that are not part of the SPAWN_OPTIONS or
|
37
|
+
# are not a redirection option will not be included in the returned hash.
|
38
|
+
#
|
31
39
|
# @example
|
32
|
-
# options = ProcessExecuter::Options.new(out: $stdout,
|
33
|
-
# options.spawn_options # => { out: $stdout,
|
40
|
+
# options = ProcessExecuter::Options::SpawnOptions.new(out: $stdout, chdir: '/tmp')
|
41
|
+
# options.spawn_options # => { out: $stdout, chdir: '/tmp' }
|
34
42
|
#
|
35
43
|
# @return [Hash]
|
36
44
|
#
|
37
45
|
def spawn_options
|
38
46
|
{}.tap do |spawn_options|
|
39
|
-
|
47
|
+
options_hash.each do |option_key, value|
|
40
48
|
spawn_options[option_key] = value if include_spawn_option?(option_key, value)
|
41
49
|
end
|
42
50
|
end
|
@@ -71,15 +79,15 @@ module ProcessExecuter
|
|
71
79
|
# Determine the option key that indicates a redirection option for stdout
|
72
80
|
# @return [Symbol, Integer, IO, Array, nil] nil if not found
|
73
81
|
# @api private
|
74
|
-
def
|
75
|
-
|
82
|
+
def stdout_redirection_source
|
83
|
+
options_hash.keys.find { |option_key| option_key if stdout_redirection?(option_key) }
|
76
84
|
end
|
77
85
|
|
78
|
-
#
|
79
|
-
# @return [
|
86
|
+
# Return the redirection destination for stdout
|
87
|
+
# @return [Symbol, Integer, IO, Array, nil] nil if stdout is not redirected
|
80
88
|
# @api private
|
81
|
-
def
|
82
|
-
|
89
|
+
def stdout_redirection_destination
|
90
|
+
(key = stdout_redirection_source) ? options_hash[key] : nil
|
83
91
|
end
|
84
92
|
|
85
93
|
# Determine if the given option key indicates a redirection option for stderr
|
@@ -91,29 +99,22 @@ module ProcessExecuter
|
|
91
99
|
# Determine the option key that indicates a redirection option for stderr
|
92
100
|
# @return [Symbol, Integer, IO, Array, nil] nil if not found
|
93
101
|
# @api private
|
94
|
-
def
|
95
|
-
|
102
|
+
def stderr_redirection_source
|
103
|
+
options_hash.keys.find { |option_key| option_key if stderr_redirection?(option_key) }
|
96
104
|
end
|
97
105
|
|
98
|
-
# Determine
|
99
|
-
# @return [
|
106
|
+
# Determine redirection destination for stderr if it exists
|
107
|
+
# @return [Symbol, Integer, IO, Array, nil] nil if stderr is not redirected
|
100
108
|
# @api private
|
101
|
-
def
|
102
|
-
|
109
|
+
def stderr_redirection_destination
|
110
|
+
(key = stderr_redirection_source) ? options_hash[key] : nil
|
103
111
|
end
|
104
112
|
|
105
113
|
private
|
106
114
|
|
107
115
|
# Define the allowed options
|
108
116
|
#
|
109
|
-
# @
|
110
|
-
# class MyOptions < SpawnOptions
|
111
|
-
# def define_options
|
112
|
-
# super.merge(timeout_after: { default: nil, validator: nil })
|
113
|
-
# end
|
114
|
-
# end
|
115
|
-
#
|
116
|
-
# @return [Hash<Symbol, Hash>]
|
117
|
+
# @return [Array<OptionDefinition>]
|
117
118
|
#
|
118
119
|
# @api private
|
119
120
|
def define_options
|
@@ -121,7 +122,7 @@ module ProcessExecuter
|
|
121
122
|
end
|
122
123
|
|
123
124
|
# Determine if the given option should be passed to `Process.spawn`
|
124
|
-
# @param option_key [
|
125
|
+
# @param option_key [Object] the option to be tested
|
125
126
|
# @param value [Object] the value of the option
|
126
127
|
# @return [Boolean] true if the given option should be passed to `Process.spawn`
|
127
128
|
# @api private
|
@@ -132,7 +133,7 @@ module ProcessExecuter
|
|
132
133
|
end
|
133
134
|
|
134
135
|
# Spawn allows IO object and integers as options
|
135
|
-
# @param option_key [
|
136
|
+
# @param option_key [Object] the option to be tested
|
136
137
|
# @return [Boolean] true if the given option is a valid option
|
137
138
|
# @api private
|
138
139
|
def valid_option?(option_key)
|
data/lib/process_executer/options/{spawn_and_wait_options.rb → spawn_with_timeout_options.rb}
RENAMED
@@ -5,30 +5,34 @@ require_relative 'option_definition'
|
|
5
5
|
|
6
6
|
module ProcessExecuter
|
7
7
|
module Options
|
8
|
-
#
|
8
|
+
# Defines options for {ProcessExecuter.spawn_with_timeout}
|
9
9
|
#
|
10
10
|
# @api public
|
11
11
|
#
|
12
|
-
class
|
12
|
+
class SpawnWithTimeoutOptions < SpawnOptions
|
13
13
|
private
|
14
14
|
|
15
15
|
# The options allowed for objects of this class
|
16
16
|
# @return [Array<OptionDefinition>]
|
17
17
|
# @api private
|
18
18
|
def define_options
|
19
|
-
# :nocov: SimpleCov on JRuby reports the last with the last argument line is not covered
|
20
19
|
[
|
21
20
|
*super,
|
22
21
|
OptionDefinition.new(:timeout_after, default: nil, validator: method(:validate_timeout_after))
|
23
22
|
].freeze
|
24
|
-
# :nocov:
|
25
23
|
end
|
26
24
|
|
27
|
-
#
|
25
|
+
# Note an error if timeout_after is not nil or a non-negative real number
|
26
|
+
#
|
27
|
+
# @param _key [Symbol] the option key (not used)
|
28
|
+
#
|
29
|
+
# @param _value [Object] the option value (not used)
|
30
|
+
#
|
28
31
|
# @return [void]
|
29
|
-
#
|
32
|
+
#
|
30
33
|
# @api private
|
31
|
-
|
34
|
+
#
|
35
|
+
def validate_timeout_after(_key, _value)
|
32
36
|
return if timeout_after.nil?
|
33
37
|
return if timeout_after.is_a?(Numeric) && timeout_after.real? && !timeout_after.negative?
|
34
38
|
|
@@ -2,11 +2,13 @@
|
|
2
2
|
|
3
3
|
require_relative 'options/base'
|
4
4
|
require_relative 'options/spawn_options'
|
5
|
-
require_relative 'options/
|
5
|
+
require_relative 'options/spawn_with_timeout_options'
|
6
6
|
require_relative 'options/run_options'
|
7
|
+
require_relative 'options/run_with_capture_options'
|
7
8
|
require_relative 'options/option_definition'
|
8
9
|
|
9
10
|
module ProcessExecuter
|
10
11
|
# Options related to spawning or running a command
|
12
|
+
# @api public
|
11
13
|
module Options; end
|
12
14
|
end
|
@@ -7,9 +7,7 @@ module ProcessExecuter
|
|
7
7
|
#
|
8
8
|
# * `command`: the command that was used to spawn the process
|
9
9
|
# * `options`: the options that were used to spawn the process
|
10
|
-
# * `elapsed_time`: the
|
11
|
-
# * `stdout`: the captured stdout output
|
12
|
-
# * `stderr`: the captured stderr output
|
10
|
+
# * `elapsed_time`: the seconds the command ran
|
13
11
|
# * `timed_out?`: true if the process timed out
|
14
12
|
#
|
15
13
|
# @api public
|
@@ -17,31 +15,25 @@ module ProcessExecuter
|
|
17
15
|
class Result < SimpleDelegator
|
18
16
|
# Create a new Result object
|
19
17
|
#
|
20
|
-
# @param status [Process::Status] the status to delegate to
|
21
|
-
# @param command [Array] the command that was used to spawn the process
|
22
|
-
# @param options [ProcessExecuter::Options] the options that were used to spawn the process
|
23
|
-
# @param timed_out [Boolean] true if the process timed out
|
24
|
-
# @param elapsed_time [Numeric] the secs the command ran
|
25
|
-
#
|
26
18
|
# @example
|
27
19
|
# command = ['sleep 1']
|
28
|
-
# options = ProcessExecuter::Options.new
|
29
|
-
# start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
30
|
-
# timed_out = false
|
31
|
-
# status = nil
|
20
|
+
# options = ProcessExecuter::Options::SpawnOptions.new
|
32
21
|
# pid = Process.spawn(*command, **options.spawn_options)
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
# Process.kill('KILL', pid)
|
37
|
-
# timed_out = true
|
38
|
-
# _pid, status = Process.wait2(pid)
|
39
|
-
# end
|
40
|
-
# elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
22
|
+
# _pid, status = Process.wait2(pid)
|
23
|
+
# timed_out = false
|
24
|
+
# elapsed_time = 0.01
|
41
25
|
#
|
42
26
|
# ProcessExecuter::Result.new(status, command:, options:, timed_out:, elapsed_time:)
|
43
27
|
#
|
44
|
-
# @
|
28
|
+
# @param status [Process::Status] the status to delegate to
|
29
|
+
#
|
30
|
+
# @param command [Array] the command that was used to spawn the process
|
31
|
+
#
|
32
|
+
# @param options [ProcessExecuter::Options::Base] the options that were used to spawn the process
|
33
|
+
#
|
34
|
+
# @param timed_out [Boolean] true if the process timed out
|
35
|
+
#
|
36
|
+
# @param elapsed_time [Numeric] the seconds the command ran
|
45
37
|
#
|
46
38
|
def initialize(status, command:, options:, timed_out:, elapsed_time:)
|
47
39
|
super(status)
|
@@ -59,39 +51,42 @@ module ProcessExecuter
|
|
59
51
|
attr_reader :command
|
60
52
|
|
61
53
|
# The options that were used to spawn the process
|
54
|
+
#
|
62
55
|
# @see Process.spawn
|
56
|
+
#
|
63
57
|
# @example
|
58
|
+
# # Looks like a hash, but is actually an object that derives from
|
59
|
+
# # ProcessExecuter::Options::Base
|
64
60
|
# result.options #=> { chdir: '/path/to/repo', timeout_after: 0.5 }
|
65
|
-
#
|
66
|
-
# @
|
61
|
+
#
|
62
|
+
# @return [ProcessExecuter::Options::Base]
|
63
|
+
#
|
67
64
|
attr_reader :options
|
68
65
|
|
69
|
-
# The
|
66
|
+
# The seconds the command ran
|
70
67
|
# @example
|
71
|
-
# result.elapsed_time #=> 10
|
72
|
-
# @return [Numeric
|
73
|
-
# @api public
|
68
|
+
# result.elapsed_time #=> 10.0
|
69
|
+
# @return [Numeric]
|
74
70
|
attr_reader :elapsed_time
|
75
71
|
|
76
72
|
# @!attribute [r] timed_out?
|
77
73
|
# True if the process timed out and was sent the SIGKILL signal
|
78
74
|
# @example
|
79
|
-
# result = ProcessExecuter.
|
75
|
+
# result = ProcessExecuter.spawn_with_timeout('sleep 10', timeout_after: 0.01)
|
80
76
|
# result.timed_out? # => true
|
81
77
|
# @return [Boolean]
|
82
78
|
#
|
83
|
-
|
84
|
-
|
85
|
-
end
|
79
|
+
attr_reader :timed_out
|
80
|
+
alias timed_out? timed_out
|
86
81
|
|
87
|
-
# Overrides the default success
|
82
|
+
# Overrides the default `success?` method to return `nil` if the process timed out
|
88
83
|
#
|
89
84
|
# This is because when a timeout occurs, Windows will still return true.
|
90
85
|
#
|
91
86
|
# @example
|
92
|
-
# result = ProcessExecuter.
|
87
|
+
# result = ProcessExecuter.spawn_with_timeout('sleep 10', timeout_after: 0.01)
|
93
88
|
# result.success? # => nil
|
94
|
-
# @return [true, nil]
|
89
|
+
# @return [true, false, nil]
|
95
90
|
#
|
96
91
|
def success?
|
97
92
|
return nil if timed_out? # rubocop:disable Style/ReturnNilInPredicateMethodDefinition
|
@@ -101,50 +96,13 @@ module ProcessExecuter
|
|
101
96
|
|
102
97
|
# Return a string representation of the result
|
103
98
|
# @example
|
104
|
-
# result
|
99
|
+
# result = ProcessExecuter.spawn_with_timeout('sleep 10', timeout_after: 1)
|
100
|
+
# # This message is platform dependent, but will look like this on Linux:
|
101
|
+
# result.to_s #=> "pid 70144 SIGKILL (signal 9) timed out after 1s"
|
105
102
|
# @return [String]
|
106
|
-
def to_s
|
107
|
-
"#{super}#{timed_out? ? " timed out after #{options.timeout_after}s" : ''}"
|
108
|
-
end
|
109
|
-
|
110
|
-
# Return the captured stdout output
|
111
103
|
#
|
112
|
-
|
113
|
-
|
114
|
-
#
|
115
|
-
# @example
|
116
|
-
# # Note that `ProcessExecuter.run` will wrap the given out: object in a
|
117
|
-
# # ProcessExecuter::MonitoredPipe
|
118
|
-
# result = ProcessExecuter.run('echo hello': out: StringIO.new)
|
119
|
-
# result.stdout #=> "hello\n"
|
120
|
-
#
|
121
|
-
# @return [String, nil]
|
122
|
-
#
|
123
|
-
def stdout
|
124
|
-
pipe = options.stdout_redirection_value
|
125
|
-
return nil unless pipe.is_a?(ProcessExecuter::MonitoredPipe)
|
126
|
-
|
127
|
-
pipe.destination.string
|
128
|
-
end
|
129
|
-
|
130
|
-
# Return the captured stderr output
|
131
|
-
#
|
132
|
-
# This output is only returned if the `:err` option value is a
|
133
|
-
# `ProcessExecuter::MonitoredPipe`.
|
134
|
-
#
|
135
|
-
# @example
|
136
|
-
# # Note that `ProcessExecuter.run` will wrap the given err: object in a
|
137
|
-
# # ProcessExecuter::MonitoredPipe
|
138
|
-
# result = ProcessExecuter.run('echo ERROR 1>&2', err: StringIO.new)
|
139
|
-
# resuilt.stderr #=> "ERROR\n"
|
140
|
-
#
|
141
|
-
# @return [String, nil]
|
142
|
-
#
|
143
|
-
def stderr
|
144
|
-
pipe = options.stderr_redirection_value
|
145
|
-
return nil unless pipe.is_a?(ProcessExecuter::MonitoredPipe)
|
146
|
-
|
147
|
-
pipe.destination.string
|
104
|
+
def to_s
|
105
|
+
"#{super}#{" timed out after #{options.timeout_after}s" if timed_out?}"
|
148
106
|
end
|
149
107
|
end
|
150
108
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'delegate'
|
4
|
+
|
5
|
+
module ProcessExecuter
|
6
|
+
# A decorator for ProcessExecuter::Result that adds the following attributes:
|
7
|
+
#
|
8
|
+
# * `stdout`: the captured stdout of the command
|
9
|
+
# * `stderr`: the captured stderr of the command
|
10
|
+
#
|
11
|
+
# @api public
|
12
|
+
#
|
13
|
+
class ResultWithCapture < SimpleDelegator
|
14
|
+
# Create a new ResultWithCapture object
|
15
|
+
#
|
16
|
+
# @param result [ProcessExecuter::Result] the result to delegate to
|
17
|
+
# @param stdout_buffer [StringIO] the captured stdout
|
18
|
+
# @param stderr_buffer [StringIO] the captured stderr
|
19
|
+
#
|
20
|
+
# @example manually create a ResultWithCapture instance
|
21
|
+
# stdout_buffer = StringIO.new
|
22
|
+
# stderr_buffer = StringIO.new
|
23
|
+
# command = ['echo HELLO; echo ERROR >&2']
|
24
|
+
# result = ProcessExecuter.run(*command, out: stdout_buffer, err: stderr_buffer)
|
25
|
+
# result_with_capture = ProcessExecuter::ResultWithCapture.new(result, stdout_buffer:, stderr_buffer:)
|
26
|
+
#
|
27
|
+
# # Normally, you would use the `run_with_capture` method to create a
|
28
|
+
# # ResultWithCapture instance. The above code is equivalent to:
|
29
|
+
#
|
30
|
+
# result_with_capture = ProcessExecuter.run_with_capture('echo HELLO; echo ERROR >&2')
|
31
|
+
#
|
32
|
+
def initialize(result, stdout_buffer:, stderr_buffer:)
|
33
|
+
super(result)
|
34
|
+
@stdout_buffer = stdout_buffer
|
35
|
+
@stderr_buffer = stderr_buffer
|
36
|
+
end
|
37
|
+
|
38
|
+
# The buffer used to capture stdout
|
39
|
+
# @example
|
40
|
+
# result.stdout_buffer #=> #<StringIO:0x00007f8c1b0a2d80>
|
41
|
+
# @return [StringIO]
|
42
|
+
attr_reader :stdout_buffer
|
43
|
+
|
44
|
+
# The captured stdout of the command
|
45
|
+
# @example
|
46
|
+
# result.stdout #=> "HELLO\n"
|
47
|
+
# @return [String]
|
48
|
+
def stdout = @stdout_buffer.string
|
49
|
+
|
50
|
+
# The buffer used to capture stderr
|
51
|
+
# @example
|
52
|
+
# result.stderr_buffer #=> #<StringIO:0x00007f8c1b0a2d80>
|
53
|
+
# @return [StringIO]
|
54
|
+
attr_reader :stderr_buffer
|
55
|
+
|
56
|
+
# The captured stderr of the command
|
57
|
+
# @example
|
58
|
+
# result.stderr #=> "ERROR\n"
|
59
|
+
# @return [String]
|
60
|
+
def stderr = @stderr_buffer.string
|
61
|
+
end
|
62
|
+
end
|