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 +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
|