process-helper 1.0.0

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