process_executer 0.3.0 → 0.5.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: ae4d5881afe09475a15692bf7110746ff4dfd76a70958775351c6fb510a7fb2f
4
- data.tar.gz: 683508220b6137f2129bf094b823862ae155a7a5b418aced87dd0056bb0eda7f
3
+ metadata.gz: c6d2904b0fd36166eb05a23f8d208b27de0c19bc10284e71e63a1e7055034a7b
4
+ data.tar.gz: 5ed998bd38182774d63265bd6729c219918acf41755215a172c87b85e4873ebe
5
5
  SHA512:
6
- metadata.gz: 506898e021e1bf3b923d3900fb83005836e729daf064885c0d8172a35690fc9f7865d716197ce60cfb6d6a5153817bde385f6b5b38165296b7f1c213a7a6cfe3
7
- data.tar.gz: 3111bfbaf30068de540fb1463a8f862a1d55d6c26f3c47cc90776b2d111a3330601927d4c7e70dcc719ea7c6e60da447c6a2b9b6bddeb89749f44e106a71847e
6
+ metadata.gz: 7aec2cdf98b06acccccfa449e57efbc3d6ea4b7bb9ee833a5f81ee77faa02490c55d5c3cea7c1235295d765eaf3b957b2577e3cf8fae0818d4e60e097218bcc3
7
+ data.tar.gz: a0a95d10b07111be5ca3bc094cfa4255629f43b723fb7ae44ead94615c11cd97af370da9a219e481c1d55eb55fd20f7f3a24e213a96834d91b3f18527a6af8ab
data/CHANGELOG.md CHANGED
@@ -5,6 +5,30 @@ All notable changes to the process_executer gem will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## v0.5.0 (2022-12-12)
9
+
10
+ [Full Changelog](https://github.com/main-branch/process_executer/compare/v0.4.0...v0.5.0)
11
+
12
+ * c6d8de9 Workaround a problem with SimpleCov / JRuby
13
+ * c480b5f Increase time to wait for results from a writer throwing an exception
14
+ * 1934563 Handle exceptions from writers within MonitoredPipe
15
+ * e948ada Increase default chunk_size to 100_000 bytes
16
+ * 5eb2c24 Update documentation for ProcessExecuter#spawn
17
+ * a3a4217 Release v0.4.0
18
+
19
+ ## v0.4.0 (2022-12-06)
20
+
21
+ [Full Changelog](https://github.com/main-branch/process_executer/compare/v0.3.0...v0.4.0)
22
+
23
+ * 9ac17a4 Remove build using jruby-head on windows
24
+ * d36d131 Work around a SimpleCov problem when using JRuby
25
+ * b6b3a19 Remove unused Status and Process classes
26
+ * a5cdf04 Allow 100% coverage check to be skipped
27
+ * a3fa1f5 Output coverage details when coverage is below 100%
28
+ * 6a9a417 Refactor monitor so that closing the pipe is on the monitoring thread
29
+ * 65ee9a2 Add JRuby and Windows builds
30
+ * 2e713e3 Release v0.3.0
31
+
8
32
  ## v0.3.0 (2022-12-01)
9
33
 
10
34
  [Full Changelog](https://github.com/main-branch/process_executer/compare/v0.2.0...v0.3.0)
data/Rakefile CHANGED
@@ -3,7 +3,12 @@
3
3
  # The default task
4
4
 
5
5
  desc 'Run the same tasks that the CI build will run'
6
- task default: %w[spec rubocop yard yard:audit yard:coverage bundle:audit build]
6
+
7
+ if RUBY_PLATFORM == 'java'
8
+ task default: %w[spec rubocop bundle:audit build]
9
+ else
10
+ task default: %w[spec rubocop yard yard:audit yard:coverage bundle:audit build]
11
+ end
7
12
 
8
13
  # Bundler Audit
9
14
 
@@ -53,27 +58,29 @@ end
53
58
 
54
59
  CLEAN << 'rubocop-report.json'
55
60
 
56
- # YARD
61
+ unless RUBY_PLATFORM == 'java'
62
+ # YARD
57
63
 
58
- require 'yard'
59
- YARD::Rake::YardocTask.new do |t|
60
- t.files = %w[lib/**/*.rb examples/**/*]
61
- end
64
+ require 'yard'
65
+ YARD::Rake::YardocTask.new do |t|
66
+ t.files = %w[lib/**/*.rb examples/**/*]
67
+ end
62
68
 
63
- CLEAN << '.yardoc'
64
- CLEAN << 'doc'
69
+ CLEAN << '.yardoc'
70
+ CLEAN << 'doc'
65
71
 
66
- # Yardstick
72
+ # Yardstick
67
73
 
68
- desc 'Run yardstick to show missing YARD doc elements'
69
- task :'yard:audit' do
70
- sh "yardstick 'lib/**/*.rb'"
71
- end
74
+ desc 'Run yardstick to show missing YARD doc elements'
75
+ task :'yard:audit' do
76
+ sh "yardstick 'lib/**/*.rb'"
77
+ end
72
78
 
73
- # Yardstick coverage
79
+ # Yardstick coverage
74
80
 
75
- require 'yardstick/rake/verify'
81
+ require 'yardstick/rake/verify'
76
82
 
77
- Yardstick::Rake::Verify.new(:'yard:coverage') do |verify|
78
- verify.threshold = 100
83
+ Yardstick::Rake::Verify.new(:'yard:coverage') do |verify|
84
+ verify.threshold = 100
85
+ end
79
86
  end
@@ -9,9 +9,12 @@ module ProcessExecuter
9
9
  # When a new MonitoredPipe is created, a pipe is created (via IO.pipe) and
10
10
  # a thread is created to read data written to the pipe.
11
11
  #
12
- # Data that is read from the pipe is written one or more writers passed to
12
+ # Data that is read from that pipe is written one or more writers passed to
13
13
  # `#initialize`.
14
14
  #
15
+ # If any of the writers raise an exception, the monitoring thread will exit, the
16
+ # pipe will be closed, and the exception will be saved in `#exception`.
17
+ #
15
18
  # `#close` must be called to ensure that (1) the pipe is closed, (2) all data is
16
19
  # read from the pipe and written to the writers, and (3) the monitoring thread is
17
20
  # killed.
@@ -52,31 +55,38 @@ module ProcessExecuter
52
55
  # @param writers [Array<#write>] as data is read from the pipe, it is written to these writers
53
56
  # @param chunk_size [Integer] the size of the chunks to read from the pipe
54
57
  #
55
- def initialize(*writers, chunk_size: 1000)
56
- @pipe_reader, @pipe_writer = IO.pipe
57
- @chunk_size = chunk_size
58
+ def initialize(*writers, chunk_size: 100_000)
58
59
  @writers = writers
59
- @thread = Thread.new { monitor_pipe }
60
+ @chunk_size = chunk_size
61
+ @pipe_reader, @pipe_writer = IO.pipe
62
+ @state = :open
63
+ @thread = Thread.new do
64
+ Thread.current.report_on_exception = false
65
+ Thread.current.abort_on_exception = false
66
+ monitor
67
+ end
60
68
  end
61
69
 
62
- # Kill the monitoring thread, read remaining data, and close the pipe
70
+ # Set the state to `:closing` and wait for the state to be set to `:closed`
71
+ #
72
+ # The monitoring thread will see that the state has changed and will close the pipe.
63
73
  #
64
74
  # @example
65
- # require 'stringio'
66
75
  # data_collector = StringIO.new
67
76
  # pipe = ProcessExecuter::MonitoredPipe.new(data_collector)
77
+ # pipe.state #=> :open
68
78
  # pipe.write('Hello World')
69
79
  # pipe.close
80
+ # pipe.state #=> :closed
70
81
  # data_collector.string #=> "Hello World"
71
82
  #
72
83
  # @return [void]
73
84
  #
74
85
  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
86
+ return unless state == :open
87
+
88
+ @state = :closing
89
+ sleep 0.01 until state == :closed
80
90
  end
81
91
 
82
92
  # Return the write end of the pipe so that data can be written to it
@@ -212,26 +222,91 @@ module ProcessExecuter
212
222
  # @return [IO] the write end of the pipe
213
223
  attr_reader :pipe_writer
214
224
 
225
+ # @!attribute [r]
226
+ #
227
+ # The state of the pipe
228
+ #
229
+ # Must be either `:open`, `:closing`, or `:closed`
230
+ #
231
+ # * `:open` - the pipe is open and data can be written to it
232
+ # * `:closing` - the pipe is being closed and data can no longer be written to it
233
+ # * `:closed` - the pipe is closed and data can no longer be written to it
234
+ #
235
+ # @example
236
+ # pipe = ProcessExecuter::MonitoredPipe.new($stdout)
237
+ # pipe.state #=> :open
238
+ # pipe.close
239
+ # pipe.state #=> :closed
240
+ #
241
+ # @return [Symbol] the state of the pipe
242
+ #
243
+ attr_reader :state
244
+
245
+ # @!attribute [r]
246
+ #
247
+ # The exception raised by a writer
248
+ #
249
+ # If an exception is raised by a writer, it is stored here. Otherwise, it is `nil`.
250
+ #
251
+ # @example
252
+ # pipe.exception #=> nil
253
+ #
254
+ # @return [Exception, nil] the exception raised by a writer or `nil` if no exception was raised
255
+ #
256
+ attr_reader :exception
257
+
215
258
  private
216
259
 
217
- # Reads data from the pipe forever until the monitoring thread is killed
260
+ # Read data from the pipe until `#state` is changed to `:closing`
261
+ #
262
+ # The state is changed to `:closed` by calling `#close`.
263
+ #
264
+ # Before this method returns, state is set to `:closed`
265
+ #
266
+ # @return [void]
267
+ # @api private
268
+ def monitor
269
+ monitor_pipe until state == :closing
270
+ close_pipe
271
+ @state = :closed
272
+ end
273
+
274
+ # Read data from the pipe until `#state` is changed to `:closing`
275
+ #
276
+ # Data read from the pipe is written to the writers given to the constructor.
277
+ #
218
278
  # @return [void]
219
279
  # @api private
220
280
  def monitor_pipe
221
- loop do
222
- read_pipe_output if pipe_reader.wait_readable
281
+ new_data = pipe_reader.read_nonblock(chunk_size)
282
+ # SimpleCov under JRuby reports the begin statement as not covered, but it is
283
+ # :nocov:
284
+ begin
285
+ # :nocov:
286
+ writers.each { |w| w.write(new_data) }
287
+ rescue StandardError => e
288
+ @exception = e
289
+ @state = :closing
223
290
  end
291
+ rescue IO::WaitReadable
292
+ pipe_reader.wait_readable(0.01)
224
293
  end
225
294
 
226
- # Read a chunk of data from the pipe and write it to the writers
295
+ # Read any remaining data from the pipe and close it
296
+ #
227
297
  # @return [void]
228
298
  # @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
299
+ def close_pipe
300
+ # Close the write end of the pipe so no more data can be written to it
301
+ pipe_writer.close
302
+ # Read remaining data from pipe_reader (if any)
303
+ # If an exception was already raised by the last call to #write, then don't try to read remaining data
304
+ if exception.nil? && pipe_reader.wait_readable(0.01)
305
+ new_data = pipe_reader.read(chunk_size)
306
+ writers.each { |w| w.write(new_data) }
307
+ end
308
+ # Close the read end of the pipe
309
+ pipe_reader.close
235
310
  end
236
311
  end
237
312
  end
@@ -13,6 +13,10 @@ module ProcessExecuter
13
13
  # @api public
14
14
  #
15
15
  class Options
16
+ # :nocov:
17
+ # SimpleCov on JRuby seems to hav a bug that causes hashes declared on multiple lines
18
+ # to not be counted as covered.
19
+
16
20
  # These options should be passed to `Process.spawn`
17
21
  #
18
22
  # Additionally, any options whose key is an Integer or an IO object will
@@ -49,6 +53,8 @@ module ProcessExecuter
49
53
  timeout: nil
50
54
  }.freeze
51
55
 
56
+ # :nocov:
57
+
52
58
  # All options allowed by this class
53
59
  #
54
60
  ALL_OPTIONS = (SPAWN_OPTIONS + NON_SPAWN_OPTIONS).freeze
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ProcessExecuter
4
- VERSION = '0.3.0'
4
+ # The current Gem version
5
+ VERSION = '0.5.0'
5
6
  end
@@ -2,8 +2,6 @@
2
2
 
3
3
  require 'process_executer/monitored_pipe'
4
4
  require 'process_executer/options'
5
- require 'process_executer/process'
6
- require 'process_executer/status'
7
5
 
8
6
  require 'timeout'
9
7
 
@@ -12,15 +10,17 @@ require 'timeout'
12
10
  # @api public
13
11
  #
14
12
  module ProcessExecuter
15
- # Execute the specified command and return the exit status
13
+ # Execute the specified command as a subprocess and return the exit status
16
14
  #
17
- # This method blocks until the command has terminated or the timeout has been reached.
15
+ # This method blocks until the command has terminated.
16
+ #
17
+ # The command will be send the SIGKILL signal if it does not terminate within
18
+ # the specified timeout.
18
19
  #
19
20
  # @example
20
21
  # status = ProcessExecuter.spawn('echo hello')
21
22
  # status.exited? # => true
22
23
  # status.success? # => true
23
- # stdout.string # => "hello\n"
24
24
  #
25
25
  # @example with a timeout
26
26
  # status = ProcessExecuter.spawn('sleep 10', timeout: 0.01)
@@ -29,6 +29,11 @@ module ProcessExecuter
29
29
  # status.signaled? # => true
30
30
  # status.termsig # => 9
31
31
  #
32
+ # @example capturing stdout to a string
33
+ # stdout = StringIO.new
34
+ # status = ProcessExecuter.spawn('echo hello', out: stdout)
35
+ # stdout.string # => "hello"
36
+ #
32
37
  # @see https://ruby-doc.org/core-3.1.2/Kernel.html#method-i-spawn Kernel.spawn
33
38
  # documentation for valid command and options
34
39
  #
@@ -36,13 +41,13 @@ module ProcessExecuter
36
41
  # for additional options that may be specified
37
42
  #
38
43
  # @param command [Array<String>] the command to execute
39
- # @param options_hash [Hash] the options to use for this execution context
44
+ # @param options_hash [Hash] the options to use when exectuting the command
40
45
  #
41
- # @return [ProcessExecuter::ExecutionContext] the execution context that can run commands
46
+ # @return [Process::Status] the exit status of the proceess
42
47
  #
43
48
  def self.spawn(*command, **options_hash)
44
49
  options = ProcessExecuter::Options.new(**options_hash)
45
- pid = ::Process.spawn(*command, **options.spawn_options)
50
+ pid = Process.spawn(*command, **options.spawn_options)
46
51
  wait_for_process(pid, options)
47
52
  end
48
53
 
@@ -53,16 +58,16 @@ module ProcessExecuter
53
58
  # @param pid [Integer] the process id
54
59
  # @param options [ProcessExecuter::Options] the options used
55
60
  #
56
- # @return [ProcessExecuter::Status] the status of the process
61
+ # @return [Process::Status] the status of the process
57
62
  #
58
63
  # @api private
59
64
  #
60
65
  private_class_method def self.wait_for_process(pid, options)
61
66
  Timeout.timeout(options.timeout) do
62
- ::Process.wait2(pid).last
67
+ Process.wait2(pid).last
63
68
  end
64
69
  rescue Timeout::Error
65
- ::Process.kill('KILL', pid)
66
- ::Process.wait2(pid).last
70
+ Process.kill('KILL', pid)
71
+ Process.wait2(pid).last
67
72
  end
68
73
  end
@@ -10,15 +10,15 @@ Gem::Specification.new do |spec|
10
10
 
11
11
  spec.summary = 'An API for executing commands in a subprocess'
12
12
  spec.description = 'An API for executing commands in a subprocess'
13
- spec.homepage = 'https://github.com/main_branch/process_executer'
13
+ spec.homepage = 'https://github.com/main-branch/process_executer'
14
14
  spec.license = 'MIT'
15
15
  spec.required_ruby_version = '>= 2.7.0'
16
16
 
17
17
  spec.metadata['allowed_push_host'] = 'https://rubygems.org'
18
18
 
19
19
  spec.metadata['homepage_uri'] = spec.homepage
20
- spec.metadata['source_code_uri'] = 'https://github.com/main_branch/process_executer'
21
- spec.metadata['changelog_uri'] = 'https://github.com/main_branch/process_executer'
20
+ spec.metadata['source_code_uri'] = 'https://github.com/main-branch/process_executer'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/main-branch/process_executer'
22
22
 
23
23
  # Specify which files should be added to the gem when it is released.
24
24
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -37,14 +37,17 @@ Gem::Specification.new do |spec|
37
37
  spec.add_development_dependency 'bundler-audit', '~> 0.9'
38
38
  spec.add_development_dependency 'create_github_release', '~> 0.2'
39
39
  spec.add_development_dependency 'rake', '~> 13.0'
40
- spec.add_development_dependency 'redcarpet', '~> 3.5'
41
40
  spec.add_development_dependency 'rspec', '~> 3.10'
42
41
  spec.add_development_dependency 'rubocop', '~> 1.36'
43
42
  spec.add_development_dependency 'simplecov', '~> 0.21'
44
43
  spec.add_development_dependency 'simplecov-lcov', '~> 0.8'
45
44
  spec.add_development_dependency 'solargraph', '~> 0.47'
46
- spec.add_development_dependency 'yard', '~> 0.9'
47
- spec.add_development_dependency 'yardstick', '~> 0.9'
45
+
46
+ unless RUBY_PLATFORM == 'java'
47
+ spec.add_development_dependency 'redcarpet', '~> 3.5'
48
+ spec.add_development_dependency 'yard', '~> 0.9', '>= 0.9.28'
49
+ spec.add_development_dependency 'yardstick', '~> 0.9'
50
+ end
48
51
 
49
52
  # For more information and examples about making a new gem, check out our
50
53
  # guide at: https://bundler.io/guides/creating_gem.html
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.3.0
4
+ version: 0.5.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-12-02 00:00:00.000000000 Z
11
+ date: 2022-12-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bump
@@ -66,20 +66,6 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '13.0'
69
- - !ruby/object:Gem::Dependency
70
- name: redcarpet
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '3.5'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '3.5'
83
69
  - !ruby/object:Gem::Dependency
84
70
  name: rspec
85
71
  requirement: !ruby/object:Gem::Requirement
@@ -150,6 +136,20 @@ dependencies:
150
136
  - - "~>"
151
137
  - !ruby/object:Gem::Version
152
138
  version: '0.47'
139
+ - !ruby/object:Gem::Dependency
140
+ name: redcarpet
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '3.5'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '3.5'
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: yard
155
155
  requirement: !ruby/object:Gem::Requirement
@@ -157,6 +157,9 @@ dependencies:
157
157
  - - "~>"
158
158
  - !ruby/object:Gem::Version
159
159
  version: '0.9'
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: 0.9.28
160
163
  type: :development
161
164
  prerelease: false
162
165
  version_requirements: !ruby/object:Gem::Requirement
@@ -164,6 +167,9 @@ dependencies:
164
167
  - - "~>"
165
168
  - !ruby/object:Gem::Version
166
169
  version: '0.9'
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: 0.9.28
167
173
  - !ruby/object:Gem::Dependency
168
174
  name: yardstick
169
175
  requirement: !ruby/object:Gem::Requirement
@@ -197,18 +203,16 @@ files:
197
203
  - lib/process_executer.rb
198
204
  - lib/process_executer/monitored_pipe.rb
199
205
  - lib/process_executer/options.rb
200
- - lib/process_executer/process.rb
201
- - lib/process_executer/status.rb
202
206
  - lib/process_executer/version.rb
203
207
  - process_executer.gemspec
204
- homepage: https://github.com/main_branch/process_executer
208
+ homepage: https://github.com/main-branch/process_executer
205
209
  licenses:
206
210
  - MIT
207
211
  metadata:
208
212
  allowed_push_host: https://rubygems.org
209
- homepage_uri: https://github.com/main_branch/process_executer
210
- source_code_uri: https://github.com/main_branch/process_executer
211
- changelog_uri: https://github.com/main_branch/process_executer
213
+ homepage_uri: https://github.com/main-branch/process_executer
214
+ source_code_uri: https://github.com/main-branch/process_executer
215
+ changelog_uri: https://github.com/main-branch/process_executer
212
216
  rubygems_mfa_required: 'true'
213
217
  post_install_message:
214
218
  rdoc_options: []
@@ -1,72 +0,0 @@
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
@@ -1,345 +0,0 @@
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