process-helper 1.0.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 +7 -0
- data/lib/process-helper.rb +163 -0
- data/spec/process_helper_spec.rb +147 -0
- data/spec/spec_helper.rb +1 -0
- metadata +65 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 431e13e66be2c781ee2d2e0d8206870561def2f5
|
4
|
+
data.tar.gz: 5aee51f7852b3f5d9761efb0e21eea406aba6750
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6c5db494b6bbb22a99e13f0062d6874275c937a7475471f76fcdec2116c7f6b6dda355a1c63d885534f5e69ff05e69c55f87ff80468dbae3a2c3ef5adc251048
|
7
|
+
data.tar.gz: f2e2a59c501eb7bbfe687a35be7c413b25fa54c8874ef200dfd0496104247bd46068b4d7d8095f2715346b037dd3a6b2b0ae0c0c55bcf764e2afa1f4d4ac7cdb
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# rubocop:disable Style/FileName
|
2
|
+
require 'English'
|
3
|
+
|
4
|
+
module ProcessHelper
|
5
|
+
class ProcessHelper
|
6
|
+
attr_reader :pid, :exit_status
|
7
|
+
|
8
|
+
# opts can contain:
|
9
|
+
# :print_lines:: echo the STDOUT and STDERR of the process to STDOUT as well as capturing them.
|
10
|
+
# :poll_rate:: rate, in seconds (default is 0.25) to poll the capturered output for matching lines.
|
11
|
+
def initialize(opts = {})
|
12
|
+
@opts =
|
13
|
+
{
|
14
|
+
:print_lines => false
|
15
|
+
}.merge!(opts)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Starts a process defined by `command_and_args`
|
19
|
+
# command_and_args:: Array with the first element as the process to start, the rest being the parameters to the new process.
|
20
|
+
# output_to_wait_for:: Regex containing expected output, +start+ will block until a line of STDOUT matches this regex, if this is nil, then no waiting occurs.
|
21
|
+
# wait_timeout:: Timeout while waiting for particular output.
|
22
|
+
# env:: Hash of extra environment variables with which to start the process.
|
23
|
+
def start(command_and_args = [], output_to_wait_for = nil, wait_timeout = nil, env = {})
|
24
|
+
out_r, out_w = IO.pipe
|
25
|
+
err_r, err_w = IO.pipe
|
26
|
+
@pid = spawn(env, *command_and_args, :out => out_w, :err => err_w)
|
27
|
+
out_w.close
|
28
|
+
err_w.close
|
29
|
+
|
30
|
+
log = []
|
31
|
+
|
32
|
+
@out_log = ProcessLog.new(out_r, @opts, log).start
|
33
|
+
@err_log = ProcessLog.new(err_r, @opts).start
|
34
|
+
|
35
|
+
@out_log.wait_for_output(output_to_wait_for, :timeout => wait_timeout) if output_to_wait_for
|
36
|
+
end
|
37
|
+
|
38
|
+
# returns true if the process exited with an exit code of 0.
|
39
|
+
def wait_for_exit
|
40
|
+
@out_log.wait
|
41
|
+
@err_log.wait
|
42
|
+
|
43
|
+
Process.wait @pid
|
44
|
+
@exit_status = $CHILD_STATUS
|
45
|
+
|
46
|
+
@pid = nil
|
47
|
+
@exit_status == 0
|
48
|
+
end
|
49
|
+
|
50
|
+
# Send the specified signal to the wrapped process.
|
51
|
+
def kill(signal = 'TERM')
|
52
|
+
Process.kill(signal, @pid)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Gets an array containing all the lines for the specified output stream.
|
56
|
+
# +which+ can be either of:
|
57
|
+
# * +:out+
|
58
|
+
# * +:err+
|
59
|
+
def get_log(which)
|
60
|
+
_get_log(which).to_a
|
61
|
+
end
|
62
|
+
|
63
|
+
# Gets an array containing all the lines for the specified stream, emptying the stored buffer.
|
64
|
+
# +which+ can be either of:
|
65
|
+
# * +:out+
|
66
|
+
# * +:err+
|
67
|
+
def get_log!(which)
|
68
|
+
_get_log(which).drain
|
69
|
+
end
|
70
|
+
|
71
|
+
# Blocks the current thread until the specified regex has been matched in the output.
|
72
|
+
# +which+ can be either of:
|
73
|
+
# * +:out+
|
74
|
+
# * +:err+
|
75
|
+
# opts can contain:
|
76
|
+
# :timeout:: timeout in seconds to wait for the specified output.
|
77
|
+
# :poll_rate:: rate, in seconds (default is 0.25) to poll the capturered output for matching lines.
|
78
|
+
def wait_for_output(which, regexp, opts = {})
|
79
|
+
_get_log(which).wait_for_output(regexp, opts)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def _get_log(which)
|
85
|
+
case which
|
86
|
+
when :out
|
87
|
+
@out_log
|
88
|
+
when :err
|
89
|
+
@err_log
|
90
|
+
else
|
91
|
+
fail "Unknown log '#{which}'"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class ProcessLog
|
97
|
+
def initialize(io, opts, prefill = [])
|
98
|
+
@io = io
|
99
|
+
@lines = prefill.dup
|
100
|
+
@mutex = Mutex.new
|
101
|
+
@opts = opts
|
102
|
+
end
|
103
|
+
|
104
|
+
def start
|
105
|
+
@thread = Thread.new do
|
106
|
+
@io.each_line do |l|
|
107
|
+
l = TimestampedString.new(l)
|
108
|
+
STDOUT.puts l if @opts[:print_lines]
|
109
|
+
@mutex.synchronize { @lines.push l }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
self
|
113
|
+
end
|
114
|
+
|
115
|
+
def wait_for_output(regex, opts = {})
|
116
|
+
opts = { :poll_rate => 0.25 }.merge(opts)
|
117
|
+
opts[:timeout] ||= 30
|
118
|
+
cutoff = DateTime.now + Rational(opts[:timeout].to_i, 86_400)
|
119
|
+
until _any_line_matches(regex)
|
120
|
+
sleep(opts[:poll_rate])
|
121
|
+
fail "Timeout of #{opts[:timeout]} seconds exceeded while waiting for output that matches '#{regex}'" if DateTime.now > cutoff
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def wait
|
126
|
+
@thread.join
|
127
|
+
@thread = nil
|
128
|
+
self
|
129
|
+
end
|
130
|
+
|
131
|
+
def to_a
|
132
|
+
@mutex.synchronize { @lines.dup }
|
133
|
+
end
|
134
|
+
|
135
|
+
def drain
|
136
|
+
@mutex.synchronize do
|
137
|
+
r = @lines.dup
|
138
|
+
@lines.clear
|
139
|
+
r
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def to_s
|
144
|
+
@mutex.synchronize { @lines.join '' }
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def _any_line_matches(regex)
|
150
|
+
to_a.any? { |line| line.match(regex) }
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
class TimestampedString < String
|
155
|
+
attr_reader :time
|
156
|
+
|
157
|
+
def initialize(string, time = nil)
|
158
|
+
time ||= Time.now
|
159
|
+
super(string)
|
160
|
+
@time = time
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ProcessHelper
|
4
|
+
describe ProcessHelper do
|
5
|
+
context 'Things...' do
|
6
|
+
it 'It should start a process and capture the exit status (success)' do
|
7
|
+
process = ProcessHelper.new
|
8
|
+
process.start('true')
|
9
|
+
expect(process.wait_for_exit).to eq(true)
|
10
|
+
expect(process.exit_status).to eq(0)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'It should start a process and capture the exit status (failure)' do
|
14
|
+
process = ProcessHelper.new
|
15
|
+
process.start('false')
|
16
|
+
expect(process.wait_for_exit).to eq(false)
|
17
|
+
expect(process.exit_status).to eq(1 << 8)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'It should pass arguments' do
|
21
|
+
process = ProcessHelper.new
|
22
|
+
process.start(['sh', '-c', 'exit 4'])
|
23
|
+
expect(process.wait_for_exit).to eq(false)
|
24
|
+
expect(process.exit_status).to eq(4 << 8)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'It capture the exit status (signal)' do
|
28
|
+
process = ProcessHelper.new
|
29
|
+
process.start(%w(sleep 10))
|
30
|
+
process.kill
|
31
|
+
expect(process.wait_for_exit).to eq(false)
|
32
|
+
expect(Signal.signame(process.exit_status.to_i)).to eq('TERM')
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'It should expose stdout' do
|
36
|
+
process = ProcessHelper.new
|
37
|
+
process.start(['sh', '-c', 'echo hello ; echo there'])
|
38
|
+
process.wait_for_exit
|
39
|
+
out = process.get_log(:out)
|
40
|
+
expect(out.count).to eq(2)
|
41
|
+
expect(out[0]).to eq("hello\n")
|
42
|
+
expect(out[1]).to eq("there\n")
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'It should expose stderr' do
|
46
|
+
process = ProcessHelper.new
|
47
|
+
process.start(['sh', '-c', 'echo hello >&2; echo there >&2'])
|
48
|
+
process.wait_for_exit
|
49
|
+
err = process.get_log(:err)
|
50
|
+
expect(err.count).to eq(2)
|
51
|
+
expect(err[0]).to eq("hello\n")
|
52
|
+
expect(err[1]).to eq("there\n")
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'It should expose the pid' do
|
56
|
+
process = ProcessHelper.new
|
57
|
+
process.start(['sh', '-c', 'echo this is process $$'])
|
58
|
+
pid = process.pid
|
59
|
+
process.wait_for_exit
|
60
|
+
out = process.get_log(:out)
|
61
|
+
expect(out[0]).to eq("this is process #{pid}\n")
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'It should timestamp the logs' do
|
65
|
+
t0 = Time.now
|
66
|
+
process = ProcessHelper.new
|
67
|
+
process.start(['sh', '-c', 'echo a ; sleep 1 ; echo b ; sleep 1 ; echo c'])
|
68
|
+
process.wait_for_exit
|
69
|
+
t1 = Time.now
|
70
|
+
|
71
|
+
out = process.get_log(:out)
|
72
|
+
expect(out[0].time).to be >= t0
|
73
|
+
# For some reason my attempts to use be_within failed...
|
74
|
+
expect(out[1].time).to be > out[0].time - 1.1
|
75
|
+
expect(out[1].time).to be < out[0].time + 1.1
|
76
|
+
expect(out[2].time).to be > out[1].time - 1.1
|
77
|
+
expect(out[2].time).to be < out[1].time + 1.1
|
78
|
+
expect(t1).to be >= out[2].time
|
79
|
+
|
80
|
+
expect(out.join('')).to eq("a\nb\nc\n")
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'It should wait for a pattern in stdout' do
|
84
|
+
t0 = Time.now
|
85
|
+
process = ProcessHelper.new
|
86
|
+
process.start(
|
87
|
+
['sh', '-c', 'echo frog >&2 ; sleep 1 ; echo cat ; sleep 1 ; echo dog ; sleep 1 ; echo frog'],
|
88
|
+
/fro/
|
89
|
+
)
|
90
|
+
t1 = Time.now
|
91
|
+
|
92
|
+
expect(t1).to be > (t0 + 3)
|
93
|
+
expect(t1).to be < (t0 + 5)
|
94
|
+
|
95
|
+
startup_log = process.get_log(:out)
|
96
|
+
expect(startup_log.size).to eq(3)
|
97
|
+
expect(startup_log[-1]).to eq("frog\n")
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'It should be able to arbitrarly wait for output' do
|
101
|
+
t0 = Time.now
|
102
|
+
process = ProcessHelper.new
|
103
|
+
process.start(
|
104
|
+
['sh', '-c', 'echo frog >&2 ; sleep 1 ; echo cat ; sleep 1 ; echo dog ; sleep 1 ; echo frog; sleep 3; echo goat'],
|
105
|
+
/fro/
|
106
|
+
)
|
107
|
+
t1 = Time.now
|
108
|
+
|
109
|
+
expect(t1).to be > t0 + 3
|
110
|
+
expect(t1).to be < t0 + 5
|
111
|
+
|
112
|
+
process.wait_for_output(:out, /goat/)
|
113
|
+
t2 = Time.now
|
114
|
+
expect(t2).to be > t0 + 6
|
115
|
+
expect(t2).to be < t0 + 8
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'It should support draining the logs' do
|
119
|
+
process = ProcessHelper.new
|
120
|
+
process.start(
|
121
|
+
['bash', '-c', 'for ((i=0; $i<10; i=$i+1)) ; do echo out $i ; echo err $i >&2 ; sleep 1 ; done']
|
122
|
+
)
|
123
|
+
|
124
|
+
sleep 3
|
125
|
+
out = process.get_log! :out
|
126
|
+
err = process.get_log! :err
|
127
|
+
expect(out.size).to be > 1
|
128
|
+
expect(err.size).to be > 1
|
129
|
+
|
130
|
+
process.wait_for_exit
|
131
|
+
out2 = process.get_log :out
|
132
|
+
err2 = process.get_log :err
|
133
|
+
|
134
|
+
expect((out + out2).size).to eq(10)
|
135
|
+
expect((err + err2).size).to eq(10)
|
136
|
+
|
137
|
+
out3 = process.get_log! :out
|
138
|
+
err3 = process.get_log! :err
|
139
|
+
expect(out3).to eq(out2)
|
140
|
+
expect(err3).to eq(err2)
|
141
|
+
|
142
|
+
expect(process.get_log(:out).size).to eq(0)
|
143
|
+
expect(process.get_log(:err).size).to eq(0)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative '../lib/process-helper'
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: process-helper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- alex hutter
|
8
|
+
- andrew wheat
|
9
|
+
- tristan hill
|
10
|
+
- robert shield
|
11
|
+
autorequire:
|
12
|
+
bindir: bin
|
13
|
+
cert_chain: []
|
14
|
+
date: 2015-06-29 00:00:00.000000000 Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: rubocop
|
18
|
+
requirement: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - "~>"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description: Utility for managing sub processes.
|
31
|
+
email: []
|
32
|
+
executables: []
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- lib/process-helper.rb
|
37
|
+
- spec/process_helper_spec.rb
|
38
|
+
- spec/spec_helper.rb
|
39
|
+
homepage: https://github.com/bbc/process-helper
|
40
|
+
licenses:
|
41
|
+
- Apache 2
|
42
|
+
metadata: {}
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
requirements: []
|
58
|
+
rubyforge_project:
|
59
|
+
rubygems_version: 2.2.2
|
60
|
+
signing_key:
|
61
|
+
specification_version: 4
|
62
|
+
summary: Utility for managing sub processes.
|
63
|
+
test_files:
|
64
|
+
- spec/process_helper_spec.rb
|
65
|
+
- spec/spec_helper.rb
|