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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +41 -0
  4. data/README.md +177 -134
  5. data/lib/process_executer/commands/run.rb +124 -0
  6. data/lib/process_executer/commands/run_with_capture.rb +148 -0
  7. data/lib/process_executer/commands/spawn_with_timeout.rb +163 -0
  8. data/lib/process_executer/commands.rb +11 -0
  9. data/lib/process_executer/destinations/child_redirection.rb +5 -4
  10. data/lib/process_executer/destinations/close.rb +5 -4
  11. data/lib/process_executer/destinations/destination_base.rb +73 -0
  12. data/lib/process_executer/destinations/file_descriptor.rb +10 -6
  13. data/lib/process_executer/destinations/file_path.rb +12 -6
  14. data/lib/process_executer/destinations/file_path_mode.rb +10 -6
  15. data/lib/process_executer/destinations/file_path_mode_perms.rb +12 -5
  16. data/lib/process_executer/destinations/io.rb +10 -5
  17. data/lib/process_executer/destinations/monitored_pipe.rb +10 -5
  18. data/lib/process_executer/destinations/stderr.rb +8 -4
  19. data/lib/process_executer/destinations/stdout.rb +8 -4
  20. data/lib/process_executer/destinations/tee.rb +24 -17
  21. data/lib/process_executer/destinations/writer.rb +12 -7
  22. data/lib/process_executer/destinations.rb +32 -17
  23. data/lib/process_executer/errors.rb +50 -26
  24. data/lib/process_executer/monitored_pipe.rb +128 -59
  25. data/lib/process_executer/options/base.rb +118 -82
  26. data/lib/process_executer/options/option_definition.rb +5 -1
  27. data/lib/process_executer/options/run_options.rb +13 -12
  28. data/lib/process_executer/options/run_with_capture_options.rb +156 -0
  29. data/lib/process_executer/options/spawn_options.rb +31 -30
  30. data/lib/process_executer/options/{spawn_and_wait_options.rb → spawn_with_timeout_options.rb} +11 -7
  31. data/lib/process_executer/options.rb +3 -1
  32. data/lib/process_executer/result.rb +35 -77
  33. data/lib/process_executer/result_with_capture.rb +62 -0
  34. data/lib/process_executer/version.rb +2 -1
  35. data/lib/process_executer.rb +384 -346
  36. data/process_executer.gemspec +11 -2
  37. metadata +18 -8
  38. data/lib/process_executer/destination_base.rb +0 -83
  39. data/lib/process_executer/runner.rb +0 -144
@@ -1,19 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'spawn_and_wait_options'
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 the `ProcessExecuter.run`
8
+ # Define options for {ProcessExecuter.run}
9
9
  #
10
10
  # @api public
11
11
  #
12
- class RunOptions < SpawnAndWaitOptions
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
- # Validate the raise_errors option value
30
- # @return [String, nil] the error message if the value is not valid
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
- # Validate the logger option value
39
- # @return [String, nil] the error message if the value is not valid
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
- # Validate Process.spawn options and return Process.spawn options
8
+ # Defines and validates options accepted by `Process.spawn`
9
9
  #
10
- # Allow subclasses to add additional options that are not passed to `Process.spawn`
10
+ # Allows subclasses to add additional options that are not passed to `Process.spawn`.
11
11
  #
12
- # Valid options are those accepted by Process.spawn.
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
- # :nocov: SimpleCov on JRuby reports hashes declared on multiple lines as not covered
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, err: $stderr, timeout_after: 10)
33
- # options.spawn_options # => { out: $stdout, err: $stderr }
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
- options.each do |option_key, value|
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 stdout_redirection_key
75
- options.keys.find { |option_key| option_key if stdout_redirection?(option_key) }
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
- # Determine the value of the redirection option for stdout
79
- # @return [Object]
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 stdout_redirection_value
82
- options[stdout_redirection_key]
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 stderr_redirection_key
95
- options.keys.find { |option_key| option_key if stderr_redirection?(option_key) }
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 the value of the redirection option for stderr
99
- # @return [Object]
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 stderr_redirection_value
102
- options[stderr_redirection_key]
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
- # @example Adding new options in a subclass
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 [Symbol, Integer, IO] the option to be tested
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 [Symbol] the option to be tested
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)
@@ -5,30 +5,34 @@ require_relative 'option_definition'
5
5
 
6
6
  module ProcessExecuter
7
7
  module Options
8
- # Define options for the `ProcessExecuter.spawn_and_wait`
8
+ # Defines options for {ProcessExecuter.spawn_with_timeout}
9
9
  #
10
10
  # @api public
11
11
  #
12
- class SpawnAndWaitOptions < SpawnOptions
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
- # Raise an error unless timeout_after is nil or a non-negative real number
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
- # @raise [ArgumentError] if timeout_after is not a non-negative real number
32
+ #
30
33
  # @api private
31
- def validate_timeout_after
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/spawn_and_wait_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 secs the command ran
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(timeout_after: 0.5)
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
- # Timeout.timeout(options.timeout_after) do
34
- # _pid, status = Process.wait2(pid)
35
- # rescue Timeout::Error
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
- # @api public
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
- # @return [Hash]
66
- # @api public
61
+ #
62
+ # @return [ProcessExecuter::Options::Base]
63
+ #
67
64
  attr_reader :options
68
65
 
69
- # The secs the command ran
66
+ # The seconds the command ran
70
67
  # @example
71
- # result.elapsed_time #=> 10
72
- # @return [Numeric, nil]
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.spawn('sleep 10', timeout_after: 0.01)
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
- def timed_out?
84
- @timed_out
85
- end
79
+ attr_reader :timed_out
80
+ alias timed_out? timed_out
86
81
 
87
- # Overrides the default success? method to return nil if the process timed out
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.spawn('sleep 10', timeout_after: 0.01)
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.to_s #=> "pid 70144 SIGKILL (signal 9) timed out after 10s"
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
- # This output is only returned if the `:out` option value is a
113
- # `ProcessExecuter::MonitoredPipe`.
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
@@ -2,5 +2,6 @@
2
2
 
3
3
  module ProcessExecuter
4
4
  # The current Gem version
5
- VERSION = '3.2.4'
5
+ # @return [String]
6
+ VERSION = '4.0.0'
6
7
  end