process_executer 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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