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 +4 -4
- data/CHANGELOG.md +24 -0
- data/Rakefile +24 -17
- data/lib/process_executer/monitored_pipe.rb +97 -22
- data/lib/process_executer/options.rb +6 -0
- data/lib/process_executer/version.rb +2 -1
- data/lib/process_executer.rb +17 -12
- data/process_executer.gemspec +9 -6
- metadata +26 -22
- data/lib/process_executer/process.rb +0 -72
- data/lib/process_executer/status.rb +0 -345
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c6d2904b0fd36166eb05a23f8d208b27de0c19bc10284e71e63a1e7055034a7b
|
4
|
+
data.tar.gz: 5ed998bd38182774d63265bd6729c219918acf41755215a172c87b85e4873ebe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
61
|
+
unless RUBY_PLATFORM == 'java'
|
62
|
+
# YARD
|
57
63
|
|
58
|
-
require 'yard'
|
59
|
-
YARD::Rake::YardocTask.new do |t|
|
60
|
-
|
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
|
-
|
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
|
-
|
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
|
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:
|
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
|
-
@
|
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
|
-
#
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
#
|
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
|
-
|
222
|
-
|
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
|
295
|
+
# Read any remaining data from the pipe and close it
|
296
|
+
#
|
227
297
|
# @return [void]
|
228
298
|
# @api private
|
229
|
-
def
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
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
|
data/lib/process_executer.rb
CHANGED
@@ -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
|
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
|
44
|
+
# @param options_hash [Hash] the options to use when exectuting the command
|
40
45
|
#
|
41
|
-
# @return [
|
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 =
|
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 [
|
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
|
-
|
67
|
+
Process.wait2(pid).last
|
63
68
|
end
|
64
69
|
rescue Timeout::Error
|
65
|
-
|
66
|
-
|
70
|
+
Process.kill('KILL', pid)
|
71
|
+
Process.wait2(pid).last
|
67
72
|
end
|
68
73
|
end
|
data/process_executer.gemspec
CHANGED
@@ -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/
|
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/
|
21
|
-
spec.metadata['changelog_uri'] = 'https://github.com/
|
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
|
-
|
47
|
-
|
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.
|
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-
|
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/
|
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/
|
210
|
-
source_code_uri: https://github.com/
|
211
|
-
changelog_uri: https://github.com/
|
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
|