background_process 1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.textile +53 -0
- data/lib/background_process/background_process.rb +97 -0
- data/lib/background_process/io_helpers.rb +23 -0
- data/lib/background_process.rb +2 -0
- data/spec/background_process/background_process_spec.rb +114 -0
- data/spec/background_process/io_helpers_spec.rb +30 -0
- data/spec/spec_helper.rb +1 -0
- metadata +73 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Tim Harper
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
h1. Background Process
|
2
|
+
|
3
|
+
This is like popen4, but provides several convenience methods for interacting
|
4
|
+
with the process. It only works on UNIX and Ruby implementations that support
|
5
|
+
fork and native UNIX I/O streams.
|
6
|
+
|
7
|
+
Example:
|
8
|
+
|
9
|
+
<pre>
|
10
|
+
process = BackgroundProcess.run("sh -c 'sleep 1; exit 1'")
|
11
|
+
process.running? # => true
|
12
|
+
process.wait # => #<Process::Status: pid=34774,exited(1)>
|
13
|
+
process.running? #=> true
|
14
|
+
process.exitstatus # => 1
|
15
|
+
</pre>
|
16
|
+
|
17
|
+
<pre>
|
18
|
+
process = BackgroundProcess.run("sh -c 'sleep 1; exit 1'")
|
19
|
+
process.kill("KILL") # => true
|
20
|
+
process.running? # => false
|
21
|
+
process.exitstatus # => nil
|
22
|
+
</pre>
|
23
|
+
|
24
|
+
<pre>
|
25
|
+
process = BackgroundProcess.run("sh -c '
|
26
|
+
echo Service Starting
|
27
|
+
sleep 2
|
28
|
+
echo Service Started 1>&2
|
29
|
+
'")
|
30
|
+
if process.detect(:stderr, 10) { |line| line.include?("Service Started") }
|
31
|
+
puts "Service was started!"
|
32
|
+
else
|
33
|
+
puts "Service failed to start!"
|
34
|
+
end
|
35
|
+
process.kill
|
36
|
+
</pre>
|
37
|
+
|
38
|
+
h2. Common signal names
|
39
|
+
|
40
|
+
| Name | id |
|
41
|
+
| HUP | 1 |
|
42
|
+
| INT | 2 |
|
43
|
+
| QUIT | 3 |
|
44
|
+
| ABRT | 6 |
|
45
|
+
| KILL | 9 |
|
46
|
+
| ALRM | 14 |
|
47
|
+
| TERM | 15 |
|
48
|
+
| USR1 | 30 |
|
49
|
+
| USR2 | 31 |
|
50
|
+
|
51
|
+
h2. Author
|
52
|
+
|
53
|
+
Tim Harper, on behalf of Lead Media Partners
|
@@ -0,0 +1,97 @@
|
|
1
|
+
class BackgroundProcess
|
2
|
+
attr_reader :stdin, :stdout, :stderr, :pid
|
3
|
+
|
4
|
+
# Initialize a BackgroundProcess task. Don't do this. Use BackgroundProcess.run instead
|
5
|
+
def initialize(pid, stdin, stdout, stderr)
|
6
|
+
@pid, @stdin, @stdout, @stderr = pid, stdin, stdout, stderr
|
7
|
+
ObjectSpace.define_finalizer(self) { kill }
|
8
|
+
end
|
9
|
+
|
10
|
+
# Run a BackgroundProcess
|
11
|
+
def self.run(command, &block)
|
12
|
+
command = sanitize_params(command) if command.is_a?(Array)
|
13
|
+
child_stdin, parent_stdin = IO::pipe
|
14
|
+
parent_stdout, child_stdout = IO::pipe
|
15
|
+
parent_stderr, child_stderr = IO::pipe
|
16
|
+
|
17
|
+
pid = Kernel.fork do
|
18
|
+
[parent_stdin, parent_stdout, parent_stderr].each { |io| io.close }
|
19
|
+
|
20
|
+
STDIN.reopen(child_stdin)
|
21
|
+
STDOUT.reopen(child_stdout)
|
22
|
+
STDERR.reopen(child_stderr)
|
23
|
+
|
24
|
+
[child_stdin, child_stdout, child_stderr].each { |io| io.close }
|
25
|
+
|
26
|
+
exec command
|
27
|
+
end
|
28
|
+
|
29
|
+
[child_stdin, child_stdout, child_stderr].each { |io| io.close }
|
30
|
+
parent_stdin.sync = true
|
31
|
+
|
32
|
+
new(pid, parent_stdin, parent_stdout, parent_stderr)
|
33
|
+
end
|
34
|
+
|
35
|
+
# send a signal to the process. If the processes and running, do nothing.
|
36
|
+
# Valid signals are those in Signal.list. Default is "TERM"
|
37
|
+
def kill(signal = 'TERM')
|
38
|
+
if running?
|
39
|
+
Process.kill(Signal.list[signal], @pid)
|
40
|
+
true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Sends the interrupt signal to the process. The equivalent of pressing control-C in it.
|
45
|
+
def interrupt
|
46
|
+
kill('INT')
|
47
|
+
end
|
48
|
+
|
49
|
+
# asks the operating system is the process still exists.
|
50
|
+
def running?
|
51
|
+
return false unless @pid
|
52
|
+
Process.getpgid(@pid)
|
53
|
+
true
|
54
|
+
rescue Errno::ESRCH
|
55
|
+
false
|
56
|
+
end
|
57
|
+
|
58
|
+
# waits for the process to finish. Freeze the process so it can rest in peace.
|
59
|
+
# You should call this on every background job you create to avoid a flood of
|
60
|
+
# zombie processes. (Processes won't go away until they are waited on)
|
61
|
+
def wait(timeout = nil)
|
62
|
+
@exit_status ||= Timeout.timeout(timeout) do
|
63
|
+
Process.wait(@pid)
|
64
|
+
$?
|
65
|
+
end
|
66
|
+
rescue Timeout::Error
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
|
70
|
+
# Waits for the process to terminate, and then returns the exit status
|
71
|
+
def exitstatus
|
72
|
+
wait && wait.exitstatus
|
73
|
+
end
|
74
|
+
|
75
|
+
# Calls block each time a line is available in the specified output buffer(s) and returns the first non-false value
|
76
|
+
# By default, both stdout and stderr are monitored.
|
77
|
+
#
|
78
|
+
# Args:
|
79
|
+
# * which: which streams to monitor. valid values are :stdout, :stderr, or :both.
|
80
|
+
# * timeout: Total time in seconds to run detect for. If result not found within this time, abort and return nil. Pass nil for no timeout.
|
81
|
+
# * &block: the block to call. If block takes two arguments, it will pass both the stream that received the input (an instance of IO, not the symbol), and the line read from the buffer.
|
82
|
+
def detect(which = :both, timeout = nil, &block)
|
83
|
+
streams = case which
|
84
|
+
when :stdout then [stdout]
|
85
|
+
when :stderr then [stderr]
|
86
|
+
when :both then [stdout, stderr]
|
87
|
+
else raise(ArgumentError, "invalid stream specification: #{which}")
|
88
|
+
end
|
89
|
+
BackgroundProcess::IOHelpers.detect(streams, timeout, &block)
|
90
|
+
end
|
91
|
+
|
92
|
+
protected
|
93
|
+
# It's protected. What do you care? :P
|
94
|
+
def self.sanitize_params(params)
|
95
|
+
params.map { |p| p.gsub(' ', '\ ') }.join(" ")
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module BackgroundProcess::IOHelpers
|
2
|
+
extend self
|
3
|
+
def detect(streams = [], timeout = nil, &block)
|
4
|
+
begin
|
5
|
+
Timeout::timeout(timeout) do
|
6
|
+
# Something that should be interrupted if it takes too much time...
|
7
|
+
while true
|
8
|
+
available_streams, _, _ = Kernel.select(streams, nil, nil, 1)
|
9
|
+
available_streams.each do |s|
|
10
|
+
content = s.gets
|
11
|
+
if result = (block.arity == 1 ? yield(content) : yield(s, content))
|
12
|
+
return result
|
13
|
+
end
|
14
|
+
end if available_streams
|
15
|
+
end
|
16
|
+
end
|
17
|
+
true
|
18
|
+
rescue Timeout::Error
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe BackgroundProcess do
|
4
|
+
describe ".run" do
|
5
|
+
it "runs a command and returns a background process with properly attached IO pipes" do
|
6
|
+
process = BackgroundProcess.run("printf 'hi'")
|
7
|
+
process.stdout.readline.should == "hi"
|
8
|
+
|
9
|
+
process = BackgroundProcess.run("printf 'hi' 1>&2")
|
10
|
+
process.stderr.readline.should == "hi"
|
11
|
+
|
12
|
+
process = BackgroundProcess.run(%(sh -c 'read hi; printf "$hi"'))
|
13
|
+
process.stdin.puts "hello, world"
|
14
|
+
process.stdin.flush
|
15
|
+
process.stdout.readline.should == "hello, world"
|
16
|
+
end
|
17
|
+
|
18
|
+
it "accepts an array, and properly handles spaces" do
|
19
|
+
process = BackgroundProcess.run(['sh', '-c', 'printf hi'])
|
20
|
+
process.stdout.readline.should == "hi"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#running?" do
|
25
|
+
it "reports when the process is running" do
|
26
|
+
process = BackgroundProcess.run("sleep 0.1")
|
27
|
+
process.should be_running
|
28
|
+
process.wait
|
29
|
+
sleep 0.1
|
30
|
+
process.should_not be_running
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "#kill" do
|
35
|
+
it "kills a process" do
|
36
|
+
started_at = Time.now
|
37
|
+
process = BackgroundProcess.run("sleep 4")
|
38
|
+
sleep(0.1)
|
39
|
+
process.kill("KILL")
|
40
|
+
process.wait
|
41
|
+
(Time.now - started_at).should < 4.0
|
42
|
+
process.should_not be_running
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#exitstatus" do
|
47
|
+
it "returns the exit status of a process after it exits." do
|
48
|
+
process = BackgroundProcess.run("bash -c 'sleep 1; exit 1'")
|
49
|
+
process.exitstatus.should == 1
|
50
|
+
process.exitstatus.should == 1
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "#wait" do
|
55
|
+
it "waits for a process with timeout" do
|
56
|
+
process = BackgroundProcess.run("sleep 3")
|
57
|
+
started_waiting = Time.now
|
58
|
+
process.wait(0.5).should be_false
|
59
|
+
(Time.now - started_waiting).should be_close(0.5, 0.1)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "#detect" do
|
64
|
+
it "calls the provided block for every line outputted, and returns the first non-false value" do
|
65
|
+
process = BackgroundProcess.run("bash -c 'a=0; while sleep 0.1; do a=$(($a + 1)); echo $a; done'")
|
66
|
+
result = process.detect do |line|
|
67
|
+
"golden" if line.strip == "3"
|
68
|
+
end
|
69
|
+
result.should == "golden"
|
70
|
+
process.kill
|
71
|
+
end
|
72
|
+
|
73
|
+
it "yields the stream if two parameters are provided on the block" do
|
74
|
+
process = BackgroundProcess.run("bash -c 'a=0; while sleep 0.1; do a=$(($a + 1)); echo $a 1>&2; done'")
|
75
|
+
result = process.detect(:both, 1) do |stream, line|
|
76
|
+
"golden" if stream == process.stderr && line.strip == "3"
|
77
|
+
end
|
78
|
+
result.should == "golden"
|
79
|
+
process.kill
|
80
|
+
end
|
81
|
+
|
82
|
+
it "aborts if the provided timeout is reached" do
|
83
|
+
process = BackgroundProcess.run("sleep 2")
|
84
|
+
result = process.detect(:both, 0.1) do |stream, line|
|
85
|
+
true
|
86
|
+
end
|
87
|
+
result.should be_nil
|
88
|
+
process.kill
|
89
|
+
end
|
90
|
+
|
91
|
+
it "monitors the specified stream" do
|
92
|
+
process = BackgroundProcess.run("bash -c 'a=0; while sleep 0.1; do a=$(($a + 1)); echo $a; echo $a 1>&2; done'")
|
93
|
+
output = []
|
94
|
+
process.detect(:stdout) do |line|
|
95
|
+
output << line.to_i
|
96
|
+
true if line.to_i == 3
|
97
|
+
end
|
98
|
+
|
99
|
+
process.detect(:stderr) do |line|
|
100
|
+
output << line.to_i
|
101
|
+
true if line.to_i == 3
|
102
|
+
end
|
103
|
+
|
104
|
+
output.should == [1, 2, 3, 1, 2, 3]
|
105
|
+
end
|
106
|
+
|
107
|
+
it "never yields if nothing occurs on specified streams" do
|
108
|
+
process = BackgroundProcess.run("bash -c 'a=0; while sleep 0.1; do a=$(($a + 1)); echo $a; done'")
|
109
|
+
process.detect(:stderr, 1) do |line|
|
110
|
+
raise(Spec::Expectations::ExpectationNotMetError, "expected to not yield the block")
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe BackgroundProcess::IOHelpers do
|
4
|
+
describe "#detect" do
|
5
|
+
it "iterates over the lines from each stream as they are available, and returns when a non-false value is reached" do
|
6
|
+
process = BackgroundProcess.run("bash -c 'a=0; while sleep 0.1; do a=$(($a + 1)); echo $a; echo $a 1>&2; done'")
|
7
|
+
outputs = {:out => [], :err => []}
|
8
|
+
|
9
|
+
result = BackgroundProcess::IOHelpers.detect([process.stderr, process.stdout], 1) do |io, line|
|
10
|
+
outputs[io == process.stderr ? :err : :out] << line.to_i
|
11
|
+
true if outputs.values.map {|o| o.length }.uniq == [5]
|
12
|
+
end
|
13
|
+
outputs[:out].should == (1..5).to_a
|
14
|
+
outputs[:err].should == (1..5).to_a
|
15
|
+
result.should be_true
|
16
|
+
end
|
17
|
+
|
18
|
+
it "gives up after the timeout is reached" do
|
19
|
+
process = BackgroundProcess.run("bash -c 'sleep 2; echo done'")
|
20
|
+
started_at = Time.now
|
21
|
+
|
22
|
+
result = BackgroundProcess::IOHelpers.detect([process.stderr, process.stdout], 0.5) do |io, line|
|
23
|
+
true if line.strip == "done"
|
24
|
+
end
|
25
|
+
result.should be_nil
|
26
|
+
(Time.now - started_at).should < 0.6
|
27
|
+
process.kill
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/../lib/background_process"
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: background_process
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 0
|
8
|
+
version: "1.0"
|
9
|
+
platform: ruby
|
10
|
+
authors:
|
11
|
+
- Tim Harper
|
12
|
+
autorequire:
|
13
|
+
bindir: bin
|
14
|
+
cert_chain: []
|
15
|
+
|
16
|
+
date: 2010-04-21 00:00:00 -06:00
|
17
|
+
default_executable:
|
18
|
+
dependencies: []
|
19
|
+
|
20
|
+
description: A library for spawning and interacting with UNIX processes
|
21
|
+
email:
|
22
|
+
- timcharper+bp@gmail.com
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files:
|
28
|
+
- MIT-LICENSE
|
29
|
+
- README.textile
|
30
|
+
files:
|
31
|
+
- README.textile
|
32
|
+
- MIT-LICENSE
|
33
|
+
- lib/background_process/background_process.rb
|
34
|
+
- lib/background_process/io_helpers.rb
|
35
|
+
- lib/background_process.rb
|
36
|
+
- spec/background_process/background_process_spec.rb
|
37
|
+
- spec/background_process/io_helpers_spec.rb
|
38
|
+
- spec/spec_helper.rb
|
39
|
+
has_rdoc: true
|
40
|
+
homepage: http://github.com/timcharper/background_process
|
41
|
+
licenses: []
|
42
|
+
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options:
|
45
|
+
- --main
|
46
|
+
- README.textile
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
segments:
|
54
|
+
- 0
|
55
|
+
version: "0"
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
segments:
|
61
|
+
- 0
|
62
|
+
version: "0"
|
63
|
+
requirements: []
|
64
|
+
|
65
|
+
rubyforge_project: background_process
|
66
|
+
rubygems_version: 1.3.6
|
67
|
+
signing_key:
|
68
|
+
specification_version: 3
|
69
|
+
summary: background_process
|
70
|
+
test_files:
|
71
|
+
- spec/background_process/background_process_spec.rb
|
72
|
+
- spec/background_process/io_helpers_spec.rb
|
73
|
+
- spec/spec_helper.rb
|