background_process 1.0

Sign up to get free protection for your applications and to get access to all the features.
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,2 @@
1
+ require File.dirname(__FILE__) + "/background_process/background_process"
2
+ require File.dirname(__FILE__) + "/background_process/io_helpers"
@@ -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
@@ -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