process-helper 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|