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