process_executer 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 86a47c55d9ddea5ca45bd4126af5e2e09a50348e44d65ef089e6f4eab69eb302
4
- data.tar.gz: 06b0a7ea4a9a134b5554b4dd673ecc087cf21d255e57dd18aca164644e4df644
3
+ metadata.gz: ae4d5881afe09475a15692bf7110746ff4dfd76a70958775351c6fb510a7fb2f
4
+ data.tar.gz: 683508220b6137f2129bf094b823862ae155a7a5b418aced87dd0056bb0eda7f
5
5
  SHA512:
6
- metadata.gz: 5625c64aee302c872b097ffd4cf88ddaf3b28ebcfd5d1cfc52dacd53ab9ef4cbb323b7d6b985db3038ce2f86eb2891deb6245d7b047bd219e35d09415fa15fa0
7
- data.tar.gz: 970147ff7974931dacaa0701ca32f5abcc540ade99ed8ca2b1b70b73cfa215bb09540931fc182fb67db13cbc920e17e3b55c9d07180fea98ed6bc0571aed6568
6
+ metadata.gz: 506898e021e1bf3b923d3900fb83005836e729daf064885c0d8172a35690fc9f7865d716197ce60cfb6d6a5153817bde385f6b5b38165296b7f1c213a7a6cfe3
7
+ data.tar.gz: 3111bfbaf30068de540fb1463a8f862a1d55d6c26f3c47cc90776b2d111a3330601927d4c7e70dcc719ea7c6e60da447c6a2b9b6bddeb89749f44e106a71847e
data/CHANGELOG.md CHANGED
@@ -1,6 +1,26 @@
1
+ # Changelog
2
+
3
+ All notable changes to the process_executer gem will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## v0.3.0 (2022-12-01)
9
+
10
+ [Full Changelog](https://github.com/main-branch/process_executer/compare/v0.2.0...v0.3.0)
11
+
12
+ * 6e2cdf1 Completely refactor to a single ProcessExecuter.spawn method (#7)
13
+ * 6da57ec Add CodeClimate badges to README.md (#6)
14
+ * eebd6ae Add front matter and v0.1.0 release to changelog (#5)
15
+ * 78cb9e5 Release v0.2.0
16
+
1
17
  ## v0.2.0 (2022-11-16)
2
18
 
3
19
  [Full Changelog](https://github.com/main-branch/process_executer/compare/v0.1.0...v0.2.0)
4
20
 
5
21
  * 8b70ac0 Use the create_github_release gem to make the release PR (#2)
6
22
  * 4b2700e Add ProcessExecuter#execute to execute a command and return the result (#1)
23
+
24
+ ## v0.1.0 (2022-10-20)
25
+
26
+ Initial release of an empty project
data/README.md CHANGED
@@ -1,6 +1,67 @@
1
1
  # The ProcessExecuter Gem
2
2
 
3
- An API for executing commands in a subprocess
3
+ [![Gem Version](https://badge.fury.io/rb/process_executer.svg)](https://badge.fury.io/rb/process_executer)
4
+ [![Build Status](https://github.com/main-branch/process_executer/workflows/Ruby/badge.svg?branch=main)](https://github.com/main-branch/process_executer/actions?query=workflow%3ARuby)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/0b5c67e5c2a773009cd0/maintainability)](https://codeclimate.com/github/main-branch/process_executer/maintainability)
6
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/0b5c67e5c2a773009cd0/test_coverage)](https://codeclimate.com/github/main-branch/process_executer/test_coverage)
7
+
8
+ ## Features
9
+
10
+ This gem contains the following features:
11
+
12
+ ### ProcessExecuter::MonitoredPipe
13
+
14
+ `ProcessExecuter::MonitoredPipe` streams data sent through a pipe to one or more writers.
15
+
16
+ When a new `MonitoredPipe` is created, an pipe is created (via IO.pipe) and
17
+ a thread is created which reads data as it is written written to the pipe.
18
+
19
+ Data that is read from the pipe is written one or more writers passed to
20
+ `MonitoredPipe#initialize`.
21
+
22
+ This is useful for streaming process output (stdout and/or stderr) to anything that has a
23
+ `#write` method: a string buffer, a file, or stdout/stderr as seen in the following example:
24
+
25
+ ```ruby
26
+ require 'stringio'
27
+ require 'process_executer'
28
+
29
+ output_buffer = StringIO.new
30
+ out_pipe = ProcessExecuter::MonitoredPipe.new(output_buffer)
31
+ pid, status = Process.wait2(Process.spawn('echo "Hello World"', out: out_pipe))
32
+ output_buffer.string #=> "Hello World\n"
33
+ ```
34
+
35
+ `MonitoredPipe#initialize` can take more than one writer so that pipe output can be
36
+ streamed (or `tee`d) to multiple writers at the same time:
37
+
38
+ ```ruby
39
+ require 'stringio'
40
+ require 'process_executer'
41
+
42
+ output_buffer = StringIO.new
43
+ output_file = File.open('process.out', 'w')
44
+ out_pipe = ProcessExecuter::MonitoredPipe.new(output_buffer, output_file)
45
+ pid, status = Process.wait2(Process.spawn('echo "Hello World"', out: out_pipe))
46
+ output_file.close
47
+ output_buffer.string #=> "Hello World\n"
48
+ File.read('process.out') #=> "Hello World\n"
49
+ ```
50
+
51
+ Since the data is streamed, any object that implements `#write` can be used. For insance,
52
+ you can use it to parse process output as a stream which might be useful for long XML
53
+ or JSON output.
54
+
55
+ ### ProcessExecuter.spawn
56
+
57
+ `ProcessExecuter.spawn` has the same interface as `Process.spawn` but has two
58
+ important behaviorial differences:
59
+
60
+ 1. It blocks until the subprocess finishes
61
+ 2. A timeout can be specified using the `:timeout` option
62
+
63
+ If the command does not terminate before the timeout, the process is killed by
64
+ sending it the SIGKILL signal.
4
65
 
5
66
  ## Installation
6
67
 
@@ -37,27 +98,6 @@ commits and the created tag, and push the `.gem` file to
37
98
  Bug reports and pull requests are welcome on our
38
99
  [GitHub issue tracker](https://github.com/main-branch/process_executer)
39
100
 
40
- ## Feature Checklist
41
-
42
- Here is the 1.0 feature checklist:
43
-
44
- * [x] Run a command
45
- * [x] Collect the command's stdout/stderr to a string
46
- * [x] Passthru the command's stdout/stderr to this process's stdout/stderr
47
- * [ ] Command execution timeout
48
- * [ ] Redirect stdout/stderr to a named file
49
- * [ ] Redirect stdout/stderr to a named file with open mode
50
- * [ ] Redirect stdout/stderr to a named file with open mode and permissions
51
- * [ ] Redirect stdout/stderr to an open File object
52
- * [ ] Merge stdout & stderr
53
- * [ ] Redirect a file to stdin
54
- * [ ] Redirect from a butter to stdin
55
- * [ ] Binary vs. text mode for stdin/stdout/stderr
56
- * [ ] Environment isolation like Process.spawn
57
- * [ ] Pass options to Process.spawn (chdir, umask, pgroup, etc.)
58
- * [ ] Don't allow optionis to Process.spawn that would break the functionality
59
- (:in, :out, :err, integer, #fileno, :close_others)
60
-
61
101
  ## License
62
102
 
63
103
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+ require 'io/wait'
5
+
6
+ module ProcessExecuter
7
+ # Stream data sent through a pipe to one or more writers
8
+ #
9
+ # When a new MonitoredPipe is created, a pipe is created (via IO.pipe) and
10
+ # a thread is created to read data written to the pipe.
11
+ #
12
+ # Data that is read from the pipe is written one or more writers passed to
13
+ # `#initialize`.
14
+ #
15
+ # `#close` must be called to ensure that (1) the pipe is closed, (2) all data is
16
+ # read from the pipe and written to the writers, and (3) the monitoring thread is
17
+ # killed.
18
+ #
19
+ # @example Collect pipe data into a string
20
+ # pipe_data = StringIO.new
21
+ # begin
22
+ # pipe = MonitoredPipe.new(pipe_data)
23
+ # pipe.write("Hello World")
24
+ # ensure
25
+ # pipe.close
26
+ # end
27
+ # pipe_data.string #=> "Hello World"
28
+ #
29
+ # @example Collect pipe data into a string AND a file
30
+ # pipe_data_string = StringIO.new
31
+ # pipe_data_file = File.open("pipe_data.txt", "w")
32
+ # begin
33
+ # pipe = MonitoredPipe.new(pipe_data_string, pipe_data_file)
34
+ # pipe.write("Hello World")
35
+ # ensure
36
+ # pipe.close
37
+ # end
38
+ # pipe_data_string.string #=> "Hello World"
39
+ # File.read("pipe_data.txt") #=> "Hello World"
40
+ #
41
+ # @api public
42
+ #
43
+ class MonitoredPipe
44
+ # Create a new monitored pipe
45
+ #
46
+ # Creates a IO.pipe and starts a monitoring thread to read data written to the pipe.
47
+ #
48
+ # @example
49
+ # data_collector = StringIO.new
50
+ # pipe = ProcessExecuter::MonitoredPipe.new(data_collector)
51
+ #
52
+ # @param writers [Array<#write>] as data is read from the pipe, it is written to these writers
53
+ # @param chunk_size [Integer] the size of the chunks to read from the pipe
54
+ #
55
+ def initialize(*writers, chunk_size: 1000)
56
+ @pipe_reader, @pipe_writer = IO.pipe
57
+ @chunk_size = chunk_size
58
+ @writers = writers
59
+ @thread = Thread.new { monitor_pipe }
60
+ end
61
+
62
+ # Kill the monitoring thread, read remaining data, and close the pipe
63
+ #
64
+ # @example
65
+ # require 'stringio'
66
+ # data_collector = StringIO.new
67
+ # pipe = ProcessExecuter::MonitoredPipe.new(data_collector)
68
+ # pipe.write('Hello World')
69
+ # pipe.close
70
+ # data_collector.string #=> "Hello World"
71
+ #
72
+ # @return [void]
73
+ #
74
+ def close
75
+ thread.kill
76
+ thread.join
77
+ pipe_writer.close
78
+ read_pipe_output if pipe_reader.wait_readable(0)
79
+ pipe_reader.close
80
+ end
81
+
82
+ # Return the write end of the pipe so that data can be written to it
83
+ #
84
+ # Data written to this end of the pipe will be read by the monitor thread and
85
+ # written to the writers passed to `#initialize`.
86
+ #
87
+ # This is so we can provide a MonitoredPipe to Process.spawn as a FD
88
+ #
89
+ # @example
90
+ # require 'stringio'
91
+ # data_collector = StringIO.new
92
+ # pipe = ProcessExecuter::MonitoredPipe.new(data_collector)
93
+ # pipe.to_io.write('Hello World')
94
+ # pipe.close
95
+ # data_collector.string #=> "Hello World"
96
+ #
97
+ # @return [IO] the write end of the pipe
98
+ #
99
+ # @api private
100
+ #
101
+ def to_io
102
+ pipe_writer
103
+ end
104
+
105
+ # @!attribute [r] fileno
106
+ #
107
+ # The file descriptor for the write end of the pipe
108
+ #
109
+ # @example
110
+ # require 'stringio'
111
+ # data_collector = StringIO.new
112
+ # pipe = ProcessExecuter::MonitoredPipe.new(data_collector)
113
+ # pipe.fileno == pipe.to_io.fileno #=> true
114
+ #
115
+ # @return [Integer] the file descriptor for the write end of the pipe
116
+ #
117
+ # @api private
118
+ #
119
+ def fileno
120
+ pipe_writer.fileno
121
+ end
122
+
123
+ # Writes data to the pipe so that it can be read by the monitor thread
124
+ #
125
+ # Primarily used for testing.
126
+ #
127
+ # @example
128
+ # require 'stringio'
129
+ # data_collector = StringIO.new
130
+ # pipe = ProcessExecuter::MonitoredPipe.new(data_collector)
131
+ # pipe.write('Hello World')
132
+ # pipe.close
133
+ # data_collector.string #=> "Hello World"
134
+ #
135
+ # @param data [String] the data to write to the pipe
136
+ #
137
+ # @return [Integer] the number of bytes written to the pipe
138
+ #
139
+ # @api private
140
+ #
141
+ def write(data)
142
+ pipe_writer.write(data)
143
+ end
144
+
145
+ # @!attribute [r]
146
+ #
147
+ # The size of the chunks to read from the pipe
148
+ #
149
+ # @example
150
+ # require 'stringio'
151
+ # data_collector = StringIO.new
152
+ # pipe = ProcessExecuter::MonitoredPipe.new(data_collector)
153
+ # pipe.chunk_size #=> 1000
154
+ #
155
+ # @return [Integer] the size of the chunks to read from the pipe
156
+ #
157
+ attr_reader :chunk_size
158
+
159
+ # @!attribute [r]
160
+ #
161
+ # An array of writers to write data that is read from the pipe
162
+ #
163
+ # @example with one writer
164
+ # require 'stringio'
165
+ # data_collector = StringIO.new
166
+ # pipe = ProcessExecuter::MonitoredPipe.new(data_collector)
167
+ # pipe.writers #=> [data_collector]
168
+ #
169
+ # @example with an array of writers
170
+ # require 'stringio'
171
+ # data_collector1 = StringIO.new
172
+ # data_collector2 = StringIO.new
173
+ # pipe = ProcessExecuter::MonitoredPipe.new(data_collector1, data_collector2)
174
+ # pipe.writers #=> [data_collector1, data_collector2]]
175
+ #
176
+ # @return [Array<#write>]
177
+ #
178
+ attr_reader :writers
179
+
180
+ # @!attribute [r]
181
+ #
182
+ # The thread that monitors the pipe
183
+ #
184
+ # @example
185
+ # require 'stringio'
186
+ # data_collector = StringIO.new
187
+ # pipe = ProcessExecuter::MonitoredPipe.new(data_collector)
188
+ # pipe.thread #=> #<Thread:0x00007f8b1a0b0e00>
189
+ #
190
+ # @return [Thread]
191
+ attr_reader :thread
192
+
193
+ # @!attribute [r]
194
+ #
195
+ # The read end of the pipe
196
+ #
197
+ # @example
198
+ # pipe = ProcessExecuter::MonitoredPipe.new($stdout)
199
+ # pipe.pipe_reader #=> #<IO:fd 11>
200
+ #
201
+ # @return [IO]
202
+ attr_reader :pipe_reader
203
+
204
+ # @!attribute [r]
205
+ #
206
+ # The write end of the pipe
207
+ #
208
+ # @example
209
+ # pipe = ProcessExecuter::MonitoredPipe.new($stdout)
210
+ # pipe.pipe_writer #=> #<IO:fd 12>
211
+ #
212
+ # @return [IO] the write end of the pipe
213
+ attr_reader :pipe_writer
214
+
215
+ private
216
+
217
+ # Reads data from the pipe forever until the monitoring thread is killed
218
+ # @return [void]
219
+ # @api private
220
+ def monitor_pipe
221
+ loop do
222
+ read_pipe_output if pipe_reader.wait_readable
223
+ end
224
+ end
225
+
226
+ # Read a chunk of data from the pipe and write it to the writers
227
+ # @return [void]
228
+ # @api private
229
+ def read_pipe_output
230
+ new_data = pipe_reader.read_nonblock(chunk_size)
231
+ # puts "Received new data: #{new_data.inspect} from #{pipe_reader.inspect}"
232
+ writers.each { |w| w.write(new_data) }
233
+ rescue EOFError, IO::EAGAINWaitReadable
234
+ # No output to read at this time
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'ostruct'
5
+
6
+ module ProcessExecuter
7
+ # Validate ProcessExecuter::Executer#spawn options and return Process.spawn options
8
+ #
9
+ # Valid options are those accepted by Process.spawn plus the following additions:
10
+ #
11
+ # * `:timeout`:
12
+ #
13
+ # @api public
14
+ #
15
+ class Options
16
+ # These options should be passed to `Process.spawn`
17
+ #
18
+ # Additionally, any options whose key is an Integer or an IO object will
19
+ # be passed to `Process.spawn`.
20
+ #
21
+ SPAWN_OPTIONS = %i[
22
+ in out err unsetenv_others pgroup new_pgroup rlimit_resourcename umask
23
+ close_others chdir
24
+ ].freeze
25
+
26
+ # These options are allowed but should NOT be passed to `Process.spawn`
27
+ #
28
+ NON_SPAWN_OPTIONS = %i[
29
+ timeout
30
+ ].freeze
31
+
32
+ # Any `SPAWN_OPTIONS`` set to this value will not be passed to `Process.spawn`
33
+ #
34
+ NOT_SET = :not_set
35
+
36
+ # The default values for all options
37
+ # @return [Hash]
38
+ DEFAULTS = {
39
+ in: NOT_SET,
40
+ out: NOT_SET,
41
+ err: NOT_SET,
42
+ unsetenv_others: NOT_SET,
43
+ pgroup: NOT_SET,
44
+ new_pgroup: NOT_SET,
45
+ rlimit_resourcename: NOT_SET,
46
+ umask: NOT_SET,
47
+ close_others: NOT_SET,
48
+ chdir: NOT_SET,
49
+ timeout: nil
50
+ }.freeze
51
+
52
+ # All options allowed by this class
53
+ #
54
+ ALL_OPTIONS = (SPAWN_OPTIONS + NON_SPAWN_OPTIONS).freeze
55
+
56
+ # Create accessor functions for all options. Assumes that the options are stored
57
+ # in a hash named `@options`
58
+ #
59
+ ALL_OPTIONS.each do |option|
60
+ define_method(option) do
61
+ @options[option]
62
+ end
63
+ end
64
+
65
+ # Create a new Options object
66
+ #
67
+ # @example
68
+ # options = ProcessExecuter::Options.new(out: $stdout, err: $stderr, timeout: 10)
69
+ #
70
+ # @param options [Hash] Process.spawn options plus additional options listed below.
71
+ #
72
+ # See [Process.spawn](https://ruby-doc.org/core/Process.html#method-c-spawn)
73
+ # for a list of valid.
74
+ #
75
+ # @option options [Integer, Float, nil] :timeout
76
+ # Number of seconds to wait for the process to terminate. Any number
77
+ # may be used, including Floats to specify fractional seconds. A value of 0 or nil
78
+ # will allow the process to run indefinitely.
79
+ #
80
+ def initialize(**options)
81
+ assert_no_unknown_options(options)
82
+ @options = DEFAULTS.merge(options)
83
+ end
84
+
85
+ # Returns the options to be passed to Process.spawn
86
+ #
87
+ # @example
88
+ # options = ProcessExecuter::Options.new(out: $stdout, err: $stderr, timeout: 10)
89
+ # options.spawn_options # => { out: $stdout, err: $stderr }
90
+ #
91
+ # @return [Hash]
92
+ #
93
+ def spawn_options
94
+ {}.tap do |spawn_options|
95
+ options.each do |option, value|
96
+ spawn_options[option] = value if include_spawn_option?(option, value)
97
+ end
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ # @!attribute [r]
104
+ #
105
+ # Options with values
106
+ #
107
+ # All options have values. If an option is not given in the initializer, it
108
+ # will have the value `NOT_SET`.
109
+ #
110
+ # @return [Hash<Symbol, Object>]
111
+ #
112
+ # @api private
113
+ #
114
+ attr_reader :options
115
+
116
+ # Determine if the options hash contains any unknown options
117
+ # @param options [Hash] the hash of options
118
+ # @return [void]
119
+ # @raise [ArgumentError] if the options hash contains any unknown options
120
+ # @api private
121
+ def assert_no_unknown_options(options)
122
+ unknown_options = options.keys.reject { |key| valid_option?(key) }
123
+ raise ArgumentError, "Unknown options: #{unknown_options.join(', ')}" unless unknown_options.empty?
124
+ end
125
+
126
+ # Determine if the given option is a valid option
127
+ # @param option [Symbol] the option to be tested
128
+ # @return [Boolean] true if the given option is a valid option
129
+ # @api private
130
+ def valid_option?(option)
131
+ ALL_OPTIONS.include?(option) || option.is_a?(Integer) || option.respond_to?(:fileno)
132
+ end
133
+
134
+ # Determine if the given option should be passed to `Process.spawn`
135
+ # @param option [Symbol, Integer, IO] the option to be tested
136
+ # @param value [Object] the value of the option
137
+ # @return [Boolean] true if the given option should be passed to `Process.spawn`
138
+ # @api private
139
+ def include_spawn_option?(option, value)
140
+ (option.is_a?(Integer) || option.is_a?(IO) || SPAWN_OPTIONS.include?(option)) && value != NOT_SET
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProcessExecuter
4
+ # Spawns a process and knows how to check if the process is terminated
5
+ #
6
+ # This class is not currently used in this Gem.
7
+ #
8
+ # @api public
9
+ #
10
+ class Process
11
+ # Spawns a new process using Process.spawn
12
+ #
13
+ # @example
14
+ # command = ['echo', 'hello world']
15
+ # options = { chdir: '/tmp' }
16
+ # process = ProcessExecuter::Process.new(*command, **options)
17
+ # process.pid # => 12345
18
+ # process.terminated? # => true
19
+ # process.status # => #<Process::Status: pid 12345 exit 0>
20
+ #
21
+ # @see https://ruby-doc.org/core/Process.html#method-c-spawn Process.spawn documentation
22
+ #
23
+ # @param command [Array] the command to execute
24
+ # @param spawn_options [Hash] the options to pass to Process.spawn
25
+ #
26
+ def initialize(*command, **spawn_options)
27
+ @pid = ::Process.spawn(*command, **spawn_options)
28
+ end
29
+
30
+ # @!attribute [r]
31
+ #
32
+ # The id of the process
33
+ #
34
+ # @example
35
+ # ProcessExecuter::Process.new('echo', 'hello world').pid # => 12345
36
+ #
37
+ # @return [Integer] The id of the process
38
+ #
39
+ attr_reader :pid
40
+
41
+ # @!attribute [r]
42
+ #
43
+ # The exit status of the process or `nil` if the process has not terminated
44
+ #
45
+ # @example
46
+ # ProcessExecuter::Process.new('echo', 'hello world').status # => #<Process::Status: pid 12345 exit 0>
47
+ #
48
+ # @return [::Process::Status, nil]
49
+ #
50
+ # The status is set only when `terminated?` is called and returns `true`.
51
+ #
52
+ attr_reader :status
53
+
54
+ # Return true if the process has terminated
55
+ #
56
+ # If the proces has terminated, `#status` is set to the exit status of the process.
57
+ #
58
+ # @example
59
+ # process = ProcessExecuter::Process.new('echo', 'hello world')
60
+ # sleep 1
61
+ # process.terminated? # => true
62
+ #
63
+ # @return [Boolean] true if the process has terminated
64
+ #
65
+ def terminated?
66
+ return true if @status
67
+
68
+ _pid, @status = ::Process.wait2(pid, ::Process::WNOHANG)
69
+ !@status.nil?
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,345 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProcessExecuter
4
+ # A replacement for Process::Status that can be used to mock the exit status of a process
5
+ #
6
+ # This class is not currently used in this Gem.
7
+ #
8
+ # Process::Status encapsulates the information on the status of a running or
9
+ # terminated system process. The built-in variable $? is either nil or a
10
+ # Process::Status object.
11
+ #
12
+ # ```ruby
13
+ # fork { exit 99 } #=> 26557
14
+ # Process.wait #=> 26557
15
+ # $?.class #=> Process::Status
16
+ # $?.to_i #=> 25344
17
+ # $? >> 8 #=> 99
18
+ # $?.stopped? #=> false
19
+ # $?.exited? #=> true
20
+ # $?.exitstatus #=> 99
21
+ # ```
22
+ #
23
+ # Posix systems record information on processes using a 16-bit integer. The
24
+ # lower bits record the process status (stopped, exited, signaled) and the
25
+ # upper bits possibly contain additional information (for example the program's
26
+ # return code in the case of exited processes). Pre Ruby 1.8, these bits were
27
+ # exposed directly to the Ruby program. Ruby now encapsulates these in a
28
+ # Process::Status object. To maximize compatibility, however, these objects
29
+ # retain a bit-oriented interface. In the descriptions that follow, when we
30
+ # talk about the integer value of stat, we're referring to this 16 bit value.
31
+ #
32
+ # @api public
33
+ #
34
+ class Status
35
+ # Create a new Status object
36
+ #
37
+ # @example
38
+ # status = ProcessExecuter::Status.new(999, 0)
39
+ # status.exited? # => true
40
+ # status.success? # => true
41
+ # status.exitstatus # => 0
42
+ #
43
+ def initialize(pid, stat)
44
+ @pid = pid
45
+ @stat = stat
46
+ end
47
+
48
+ # @!attribute
49
+ #
50
+ # The pid of the process
51
+ #
52
+ # @example
53
+ # status = ProcessExecuter::Status.new(999, 0)
54
+ # status.pid # => 999
55
+ #
56
+ # @return [Integer]
57
+ #
58
+ # @api public
59
+ #
60
+ attr_reader :pid
61
+
62
+ # @!attribute
63
+ #
64
+ # The status code of the process
65
+ #
66
+ # @example
67
+ # status = ProcessExecuter::Status.new(999, 123)
68
+ # status.stat # => 123
69
+ #
70
+ # @return [Integer]
71
+ #
72
+ # @api public
73
+ #
74
+ attr_reader :stat
75
+
76
+ # Logical AND of the bits in stat with `other`
77
+ #
78
+ # @example Process ended due to an uncaught signal 11 with a core dump
79
+ # status = ProcessExecuter::Status.new(999, 139)
80
+ # status & 127 # => 11 => the uncaught signal
81
+ # !(status & 128).zero? # => true => indicating a core dump
82
+ #
83
+ # @param other [Integer] the value to AND with stat
84
+ #
85
+ # @return [Integer] the result of the AND operation
86
+ #
87
+ def &(other)
88
+ stat & other
89
+ end
90
+
91
+ # Compare stat to `other`
92
+ #
93
+ # @example Process exited normally with exitstatus 99
94
+ # status = ProcessExecuter::Status.new(999, 25_344)
95
+ # status == 25_344 # => true
96
+ #
97
+ # @param other [Integer] the value to compare stat to
98
+ #
99
+ # @return [Boolean] true if stat == other, false otherwise
100
+ #
101
+ def ==(other)
102
+ stat == other
103
+ end
104
+
105
+ # rubocop:disable Naming/BinaryOperatorParameterName
106
+
107
+ # Shift the bits in stat right `num` places
108
+ #
109
+ # @example Process exited normally with exitstatus 99
110
+ # status = ProcessExecuter::Status.new(999, 25_344)
111
+ # status >> 8 # => 99
112
+ #
113
+ # @param num [Integer] the number of places to shift stat
114
+ #
115
+ # @return [Integer] the result of the shift operation
116
+ #
117
+ def >>(num)
118
+ stat >> num
119
+ end
120
+
121
+ # rubocop:enable Naming/BinaryOperatorParameterName
122
+
123
+ # Returns true if the process generated a coredump upon termination
124
+ #
125
+ # Not available on all platforms.
126
+ #
127
+ # @example process exited normally with exitstatus 99
128
+ # status = ProcessExecuter::Status.new(999, 25_344)
129
+ # status.coredump? # => false
130
+ #
131
+ # @example process ended due to an uncaught signal 11 with a core dump
132
+ # status = ProcessExecuter::Status.new(999, 139)
133
+ # status.coredump? # => true
134
+ #
135
+ # @return [Boolean] true if stat generated a coredump when it terminated
136
+ #
137
+ def coredump?
138
+ !(stat & 128).zero?
139
+ end
140
+
141
+ # Returns true if the process exited normally
142
+ #
143
+ # This happens when the process uses an exit() call or runs to the end of the program.
144
+ #
145
+ # @example process exited normally with exitstatus 0
146
+ # status = ProcessExecuter::Status.new(999, 0)
147
+ # status.exited? # => true
148
+ #
149
+ # @example process exited normally with exitstatus 99
150
+ # status = ProcessExecuter::Status.new(999, 25_344)
151
+ # status.exited? # => true
152
+ #
153
+ # @example process ended due to an uncaught signal 11 with a core dump
154
+ # status = ProcessExecuter::Status.new(999, 139)
155
+ # status.exited? # => false
156
+ #
157
+ # @return [Boolean] true if the process exited normally
158
+ #
159
+ def exited?
160
+ (stat & 127).zero?
161
+ end
162
+
163
+ # Returns the exit status of the process
164
+ #
165
+ # Returns nil if the process did not exit normally (when `#exited?` is false).
166
+ #
167
+ # @example process exited normally with exitstatus 99
168
+ # status = ProcessExecuter::Status.new(999, 25_344)
169
+ # status.exitstatus # => 99
170
+ #
171
+ # @return [Integer, nil] the exit status of the process
172
+ #
173
+ def exitstatus
174
+ stat >> 8 if exited?
175
+ end
176
+
177
+ # Returns true if the process was successful
178
+ #
179
+ # This means that `exited?` is true and `#exitstatus` is 0.
180
+ #
181
+ # Returns nil if the process did not exit normally (when `#exited?` is false).
182
+ #
183
+ # @example process exited normally with exitstatus 0
184
+ # status = ProcessExecuter::Status.new(999, 0)
185
+ # status.success? # => true
186
+ #
187
+ # @example process exited normally with exitstatus 99
188
+ # status = ProcessExecuter::Status.new(999, 25_344)
189
+ # status.success? # => false
190
+ #
191
+ # @example process ended due to an uncaught signal 11 with a core dump
192
+ # status = ProcessExecuter::Status.new(999, 139)
193
+ # status.success? # => nil
194
+ #
195
+ # @return [Boolean, nil] true if successful, false if unsuccessful, nil if the process did not exit normally
196
+ #
197
+ def success?
198
+ exitstatus.zero? if exited?
199
+ end
200
+
201
+ # Returns true if the process was stopped
202
+ #
203
+ # @example with a stopped process with signal 17
204
+ # status = ProcessExecuter::Status.new(999, 4_479)
205
+ # status.stopped? # => true
206
+ #
207
+ # @example process exited normally with exitstatus 99
208
+ # status = ProcessExecuter::Status.new(999, 25_344)
209
+ # status.stopped? # => false
210
+ #
211
+ # @example process ended due to an uncaught signal 11 with a core dump
212
+ # status = ProcessExecuter::Status.new(999, 139)
213
+ # status.stopped? # => false
214
+ #
215
+ # @return [Boolean] true if the process was stopped, false otherwise
216
+ #
217
+ def stopped?
218
+ (stat & 127) == 127
219
+ end
220
+
221
+ # The signal number that casused the process to stop
222
+ #
223
+ # Returns nil if the process is not stopped.
224
+ #
225
+ # @example with a stopped process with signal 17
226
+ # status = ProcessExecuter::Status.new(999, 4_479)
227
+ # status.stopsig # => 17
228
+ #
229
+ # @example process exited normally with exitstatus 99
230
+ # status = ProcessExecuter::Status.new(999, 25_344)
231
+ # status.stopsig # => nil
232
+ #
233
+ # @return [Integer, nil] the signal number that caused the process to stop or nil
234
+ #
235
+ def stopsig
236
+ stat >> 8 if stopped?
237
+ end
238
+
239
+ # Returns true if stat terminated because of an uncaught signal
240
+ #
241
+ # @example process ended due to an uncaught signal 9
242
+ # status = ProcessExecuter::Status.new(999, 9)
243
+ # status.signaled? # => true
244
+ #
245
+ # @example process exited normally with exitstatus 0
246
+ # status = ProcessExecuter::Status.new(999, 0)
247
+ # status.signaled? # => false
248
+ #
249
+ # @return [Boolean] true if stat terminated because of an uncaught signal, false otherwise
250
+ #
251
+ def signaled?
252
+ ![0, 127].include?(stat & 127)
253
+ end
254
+
255
+ # Returns the number of the signal that caused the process to terminate
256
+ #
257
+ # Returns nil if the process exited normally or is stopped.
258
+ #
259
+ # @example process ended due to an uncaught signal 9
260
+ # status = ProcessExecuter::Status.new(999, 9)
261
+ # status.termsig # => 9
262
+ #
263
+ # @example process exited normally with exitstatus 0
264
+ # status = ProcessExecuter::Status.new(999, 0)
265
+ # status.termsig # => nil
266
+ #
267
+ # @return [Integer, nil] the signal number that caused the process to terminate or nil
268
+ #
269
+ def termsig
270
+ stat & 127 if signaled?
271
+ end
272
+
273
+ # Returns the bits in stat as an Integer
274
+ #
275
+ # @example with a stopped process with signal 17
276
+ # status = ProcessExecuter::Status.new(999, 4_479)
277
+ # status.to_i # => 4_479
278
+ #
279
+ # @return [Integer] the bits in stat
280
+ #
281
+ def to_i
282
+ stat
283
+ end
284
+
285
+ # Show the status type, pid, and exit status as a string
286
+ #
287
+ # @example with a stopped process with signal 17
288
+ # status = ProcessExecuter::Status.new(999, 4_479)
289
+ # status.to_s # => "pid 999 stopped SIGSTOP (signal 17)"
290
+ #
291
+ # @return [String] the status type, pid, and exit status as a string
292
+ #
293
+ def to_s
294
+ type_to_s + (coredump? ? ' (core dumped)' : '')
295
+ end
296
+
297
+ # Show the status type, pid, and exit status as a string
298
+ #
299
+ # @example with a stopped process with signal 17
300
+ # status = ProcessExecuter::Status.new(999, 4_479)
301
+ # status.inspect # => "#<ProcessExecuter::Status pid 999 stopped SIGSTOP (signal 17)>"
302
+ #
303
+ # @return [String] the status type, pid, and exit status as a string
304
+ #
305
+ def inspect
306
+ "#<#{self.class} #{self}>"
307
+ end
308
+
309
+ private
310
+
311
+ # The string representation of a status based on how it was terminated
312
+ # @return [String] the string representation
313
+ # @api private
314
+ def type_to_s
315
+ if signaled?
316
+ signaled_to_s
317
+ elsif exited?
318
+ exited_to_s
319
+ elsif stopped?
320
+ stopped_to_s
321
+ end
322
+ end
323
+
324
+ # The string representation of a signaled process
325
+ # @return [String] the string representation of a signaled process
326
+ # @api private
327
+ def signaled_to_s
328
+ "pid #{pid} SIG#{Signal.signame(termsig)} (signal #{termsig})"
329
+ end
330
+
331
+ # The string representation of an exited process
332
+ # @return [String] the string representation of an exited process
333
+ # @api private
334
+ def exited_to_s
335
+ "pid #{pid} exit #{exitstatus}"
336
+ end
337
+
338
+ # The string representation of a stopped process
339
+ # @return [String] the string representation of a stopped process
340
+ # @api private
341
+ def stopped_to_s
342
+ "pid #{pid} stopped SIG#{Signal.signame(stopsig)} (signal #{stopsig})"
343
+ end
344
+ end
345
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ProcessExecuter
4
- VERSION = '0.2.0'
3
+ module ProcessExecuter
4
+ VERSION = '0.3.0'
5
5
  end
@@ -1,225 +1,68 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Style/SingleLineMethods
3
+ require 'process_executer/monitored_pipe'
4
+ require 'process_executer/options'
5
+ require 'process_executer/process'
6
+ require 'process_executer/status'
4
7
 
5
- require 'process_executer/result'
6
- # require 'nio'
8
+ require 'timeout'
7
9
 
8
- # Execute a process and capture the output to a string, a file, and/or
9
- # pass the output through to this process's stdout/stderr.
10
+ # Execute a command in a subprocess and optionally capture its output
10
11
  #
11
12
  # @api public
12
13
  #
13
- class ProcessExecuter
14
- # stdout collected from the command
14
+ module ProcessExecuter
15
+ # Execute the specified command and return the exit status
15
16
  #
16
- # @example
17
- # command = ProcessExecuter.new
18
- # command.execute('echo hello')
19
- # command.out # => "hello\n"
20
- #
21
- # @return [String, nil] nil if collect_out? is false
22
- #
23
- attr_reader :out
24
-
25
- # stderr collected from the command
26
- #
27
- # @example
28
- # command = ProcessExecuter.new
29
- # command.execute('echo hello 1>&2')
30
- # command.err # => "hello\n"
31
- #
32
- # @return [String, nil] nil if collect_err? is false
33
- #
34
- attr_reader :err
35
-
36
- # The status of the command process
37
- #
38
- # Will be `nil` if the command has not completed execution.
39
- #
40
- # @example
41
- # command = ProcessExecuter.new
42
- # command.execute('echo hello')
43
- # command.status # => #<Process::Status: pid 86235 exit 0>
44
- #
45
- # @return [Process::Status, nil]
46
- #
47
- attr_reader :status
48
-
49
- # Show the command's stdout on this process's stdout
50
- #
51
- # @example
52
- # command = ProcessExecuter.new(passthru_out: true)
53
- # command.passthru_out? # => true
54
- #
55
- # @return [Boolean]
56
- #
57
- def passthru_out?; !!@passthru_out; end
58
-
59
- # Show the command's stderr on this process's stderr
60
- #
61
- # @example
62
- # command = ProcessExecuter.new(passthru_err: true)
63
- # command.passthru_err? # => true
64
- #
65
- # @return [Boolean]
66
- #
67
- def passthru_err?; !!@passthru_err; end
68
-
69
- # Collect the command's stdout the :out string (default is true)
17
+ # This method blocks until the command has terminated or the timeout has been reached.
70
18
  #
71
19
  # @example
72
- # command = ProcessExecuter.new(collect_out: false)
73
- # command.collect_out? # => false
74
- #
75
- # @return [Boolean]
76
- #
77
- def collect_out?; !!@collect_out; end
78
-
79
- # Collect the command's stderr the :err string (default is true)
20
+ # status = ProcessExecuter.spawn('echo hello')
21
+ # status.exited? # => true
22
+ # status.success? # => true
23
+ # stdout.string # => "hello\n"
80
24
  #
81
- # @example
82
- # command = ProcessExecuter.new(collect_err: false)
83
- # command.collect_err? # => false
25
+ # @example with a timeout
26
+ # status = ProcessExecuter.spawn('sleep 10', timeout: 0.01)
27
+ # status.exited? # => false
28
+ # status.success? # => nil
29
+ # status.signaled? # => true
30
+ # status.termsig # => 9
84
31
  #
85
- # @return [Boolean]
32
+ # @see https://ruby-doc.org/core-3.1.2/Kernel.html#method-i-spawn Kernel.spawn
33
+ # documentation for valid command and options
86
34
  #
87
- def collect_err?; !!@collect_err; end
88
-
89
- # Create a new ProcessExecuter
35
+ # @see ProcessExecuter::Options#initialize See ProcessExecuter::Options#initialize
36
+ # for additional options that may be specified
90
37
  #
91
- # @example
92
- # command = ProcessExecuter.new(passthru_out: false, passthru_err: false)
38
+ # @param command [Array<String>] the command to execute
39
+ # @param options_hash [Hash] the options to use for this execution context
93
40
  #
94
- # @param passthru_out [Boolean] show the command's stdout on this process's stdout
95
- # @param passthru_err [Boolean] show the command's stderr on this process's stderr
96
- # @param collect_out [Boolean] collect the command's stdout the :out string
97
- # @param collect_err [Boolean] collect the command's stderr the :err string
41
+ # @return [ProcessExecuter::ExecutionContext] the execution context that can run commands
98
42
  #
99
- def initialize(passthru_out: false, passthru_err: false, collect_out: true, collect_err: true)
100
- @passthru_out = passthru_out
101
- @passthru_err = passthru_err
102
- @collect_out = collect_out
103
- @collect_err = collect_err
43
+ def self.spawn(*command, **options_hash)
44
+ options = ProcessExecuter::Options.new(**options_hash)
45
+ pid = ::Process.spawn(*command, **options.spawn_options)
46
+ wait_for_process(pid, options)
104
47
  end
105
48
 
106
- # rubocop:disable Metrics/AbcSize
107
- # rubocop:disable Metrics/MethodLength
108
-
109
- # Execute the given command in a subprocess
49
+ # Wait for process to terminate
110
50
  #
111
- # See Process.spawn for acceptable values for command and options.
51
+ # If a timeout is speecified in options, kill the process after options.timeout seconds.
112
52
  #
113
- # Do no specify the following options: :in, :out, :err, integer, #fileno, :close_others.
53
+ # @param pid [Integer] the process id
54
+ # @param options [ProcessExecuter::Options] the options used
114
55
  #
115
- # @example Execute a command as a single string
116
- # result = ProcessExecuter.new.execute('echo hello')
56
+ # @return [ProcessExecuter::Status] the status of the process
117
57
  #
118
- # @example Execute a command as with each argument as a separate string
119
- # result = ProcessExecuter.new.execute('echo', 'hello')
120
- #
121
- # @example Execute a command in a specific directory
122
- # result = ProcessExecuter.new.execute('pwd', chdir: '/tmp')
123
- # result.out # => "/tmp\n"
124
- #
125
- # @example Execute a command with specific environment variables
126
- # result = ProcessExecuter.new.execute({ 'FOO' => 'bar' }, 'echo $FOO' )
127
- # result.out # => "bar\n"
128
- #
129
- # @param command [String, Array<String>] the command to pass to Process.spawn
130
- # @param options [Hash] options to pass to Process.spawn
131
- #
132
- # @return [ProcessExecuter::Result] the result of the command execution
133
- #
134
- def execute(*command, **options)
135
- @status = nil
136
- @out = (collect_out? ? '' : nil)
137
- @err = (collect_err? ? '' : nil)
138
-
139
- out_reader, out_writer = IO.pipe
140
- err_reader, err_writer = IO.pipe
141
-
142
- options[:out] = out_writer
143
- options[:err] = err_writer
144
-
145
- pid = Process.spawn(*command, options)
146
-
147
- loop do
148
- read_command_output(out_reader, err_reader)
149
-
150
- _pid, @status = Process.wait2(pid, Process::WNOHANG)
151
- break if @status
152
-
153
- # puts "finished_pid: #{finished_pid}"
154
- # puts "status: #{status}"
155
-
156
- # puts 'starting select'
157
- # readers, writers, exceptions = IO.select([stdout_reader, stderr_reader], nil, nil, 0.1)
158
- IO.select([out_reader, err_reader], nil, nil, 0.05)
159
-
160
- # puts "readers: #{readers}"
161
- # puts "writers: #{writers}"
162
- # puts "exceptions: #{exceptions}"
163
-
164
- # break unless readers || writers || exceptions
165
-
166
- _pid, @status = Process.wait2(pid, Process::WNOHANG)
167
- break if @status
168
-
169
- # puts "finished_pid: #{finished_pid}"
170
- # puts "status: #{status}"
171
- end
172
-
173
- out_writer.close
174
- err_writer.close
175
-
176
- # Read whatever is left over after the process terminates
177
- read_command_output(out_reader, err_reader)
178
- ProcessExecuter::Result.new(@status, @out, @err)
179
- end
180
-
181
- # rubocop:enable Metrics/AbcSize
182
- # rubocop:enable Metrics/MethodLength
183
-
184
- private
185
-
186
- # Read output from the given readers
187
- # @return [void]
188
58
  # @api private
189
- def read_command_output(out_reader, err_reader)
190
- loop do
191
- # Keep reading until there is nothing left to read
192
- break unless read_out(out_reader) || read_err(err_reader)
59
+ #
60
+ private_class_method def self.wait_for_process(pid, options)
61
+ Timeout.timeout(options.timeout) do
62
+ ::Process.wait2(pid).last
193
63
  end
194
- end
195
-
196
- # Read stdout from the given reader
197
- # @return [void]
198
- # @api private
199
- def read_out(reader)
200
- new_data = reader.read_nonblock(1024)
201
- # puts "new_stdout: '#{new_data}'"
202
- @out += new_data if new_data && collect_out?
203
- puts new_data if new_data && passthru_out?
204
- true
205
- rescue EOFError, IO::EAGAINWaitReadable
206
- # Nothing to read at this time
207
- false
208
- end
209
-
210
- # Read stderr from the given reader
211
- # @return [void]
212
- # @api private
213
- def read_err(reader)
214
- new_data = reader.read_nonblock(1024)
215
- # puts "new_stderr: '#{new_data}'"
216
- @err += new_data if new_data && collect_err?
217
- warn new_data if new_data && passthru_err?
218
- true
219
- rescue EOFError, IO::EAGAINWaitReadable
220
- # Nothing to read at this time
221
- false
64
+ rescue Timeout::Error
65
+ ::Process.kill('KILL', pid)
66
+ ::Process.wait2(pid).last
222
67
  end
223
68
  end
224
-
225
- # rubocop:enable Style/SingleLineMethods
@@ -41,6 +41,7 @@ Gem::Specification.new do |spec|
41
41
  spec.add_development_dependency 'rspec', '~> 3.10'
42
42
  spec.add_development_dependency 'rubocop', '~> 1.36'
43
43
  spec.add_development_dependency 'simplecov', '~> 0.21'
44
+ spec.add_development_dependency 'simplecov-lcov', '~> 0.8'
44
45
  spec.add_development_dependency 'solargraph', '~> 0.47'
45
46
  spec.add_development_dependency 'yard', '~> 0.9'
46
47
  spec.add_development_dependency 'yardstick', '~> 0.9'
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: 0.2.0
4
+ version: 0.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: 2022-11-16 00:00:00.000000000 Z
11
+ date: 2022-12-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bump
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0.21'
125
+ - !ruby/object:Gem::Dependency
126
+ name: simplecov-lcov
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.8'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.8'
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: solargraph
127
141
  requirement: !ruby/object:Gem::Requirement
@@ -181,7 +195,10 @@ files:
181
195
  - README.md
182
196
  - Rakefile
183
197
  - lib/process_executer.rb
184
- - lib/process_executer/result.rb
198
+ - lib/process_executer/monitored_pipe.rb
199
+ - lib/process_executer/options.rb
200
+ - lib/process_executer/process.rb
201
+ - lib/process_executer/status.rb
185
202
  - lib/process_executer/version.rb
186
203
  - process_executer.gemspec
187
204
  homepage: https://github.com/main_branch/process_executer
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class ProcessExecuter
4
- # The result of executing a command
5
- #
6
- # @api public
7
- #
8
- class Result
9
- # The status of the command process
10
- #
11
- # @example
12
- # status # => #<Process::Status: pid 86235 exit 0>
13
- # out = 'hello'
14
- # err = 'ERROR'
15
- # command = ProcessExecuter.new(status, out, err)
16
- # command.status # => #<Process::Status: pid 86235 exit 0>
17
- #
18
- # @return [Process::Status]
19
- #
20
- attr_reader :status
21
-
22
- # The command's stdout (if collected)
23
- #
24
- # @example
25
- # status # => #<Process::Status: pid 86235 exit 0>
26
- # out = 'hello'
27
- # err = 'ERROR'
28
- # command = ProcessExecuter.new(status, out, err)
29
- # command.out # => "hello\n"
30
- #
31
- # @return [String, nil]
32
- #
33
- attr_reader :out
34
-
35
- # The command's stderr (if collected)
36
- #
37
- # @example
38
- # status # => #<Process::Status: pid 86235 exit 0>
39
- # out = 'hello'
40
- # err = 'ERROR'
41
- # command = ProcessExecuter.new(status, out, err)
42
- # command.out # => "ERROR\n"
43
- #
44
- # @return [String, nil]
45
- #
46
- attr_reader :err
47
-
48
- # Create a new Result object
49
- #
50
- # @example
51
- # status # => #<Process::Status: pid 86235 exit 0>
52
- # out = 'hello'
53
- # err = 'ERROR'
54
- # command = ProcessExecuter.new(status, out, err)
55
- #
56
- # @param status [Process::Status] the status of the command process
57
- # @param out [String, nil] the command's stdout (if collected)
58
- # @param err [String, nil] the command's stderr (if collected)
59
- #
60
- # @return [ProcessExecuter::Result] the result object
61
- #
62
- def initialize(status, out, err)
63
- @status = status
64
- @out = out
65
- @err = err
66
- end
67
- end
68
- end