background_process 1.1 → 1.2
Sign up to get free protection for your applications and to get access to all the features.
data/README.textile
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
h1. Background Process
|
2
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
|
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
5
|
fork and native UNIX I/O streams.
|
6
6
|
|
7
7
|
"Click here for Complete Documentation":http://rdoc.info/projects/timcharper/background_process
|
@@ -50,6 +50,33 @@ h2. Common signal names
|
|
50
50
|
| USR1 | 30 |
|
51
51
|
| USR2 | 31 |
|
52
52
|
|
53
|
+
h2. A word about buffered output
|
54
|
+
|
55
|
+
Some processes (like Ruby) buffer their output if they detect that they aren't
|
56
|
+
attached to a tty. This means that unless the program explicitly calls
|
57
|
+
STDOUT.flush, much of the output won't be available for reading until the
|
58
|
+
buffer is full, forcing it to be flushed.
|
59
|
+
|
60
|
+
You can force the output of a ruby script to be unbuffered by using a wrapper
|
61
|
+
like the following:
|
62
|
+
|
63
|
+
<pre>
|
64
|
+
BackgroundProcess.run %(ruby -e 'STDERR.sync, STDOUT.sync = true, true; $0="./server.rb"; load "server.rb"')
|
65
|
+
</pre>
|
66
|
+
|
67
|
+
If no such workaround is available, this gem provides PTYBackgroundProcess
|
68
|
+
which will wrap the command any pseudo-terminal interface. This has the
|
69
|
+
advantage of forcing many programs to not buffer their output, but at a
|
70
|
+
disadvantage too (you can't get the exit status of the process, you lose any
|
71
|
+
output that hasn't been read yet when the process exits, and stderr gets
|
72
|
+
merged into stdout). It seems there should be ways to work around these
|
73
|
+
limitations, but I wasn't able to figure out an way to do it (without
|
74
|
+
resorting to C code at least). Given your circumstances, this may fit your
|
75
|
+
needs and be a viable solution to you.
|
76
|
+
|
77
|
+
Please see the documentation for BackgroundProcess.run and
|
78
|
+
PTYBackgroundProcess.run for further information.
|
79
|
+
|
53
80
|
h2. Author
|
54
81
|
|
55
82
|
Tim Harper, on behalf of Lead Media Partners
|
data/lib/background_process.rb
CHANGED
@@ -1,14 +1,27 @@
|
|
1
1
|
class BackgroundProcess
|
2
2
|
attr_reader :stdin, :stdout, :stderr, :pid
|
3
3
|
|
4
|
-
# Initialize a BackgroundProcess task. Don't do this. Use BackgroundProcess.run instead
|
5
|
-
def initialize(pid, stdin, stdout, stderr)
|
4
|
+
# Initialize a BackgroundProcess task. Don't do this. Use BackgroundProcess.run or BackgroundProcess.run_pty instead
|
5
|
+
def initialize(pid, stdin, stdout, stderr = nil)
|
6
6
|
@pid, @stdin, @stdout, @stderr = pid, stdin, stdout, stderr
|
7
7
|
ObjectSpace.define_finalizer(self) { kill }
|
8
8
|
end
|
9
9
|
|
10
|
-
|
11
|
-
|
10
|
+
|
11
|
+
# Run a command, connecting it's IO streams (stdin, sterr, stdout) via IO pipes,
|
12
|
+
# which are not tty IO streams.
|
13
|
+
#
|
14
|
+
# Because of this, some programs (like ruby) will buffer their output and only
|
15
|
+
# make it available when it's explicitely flushed (with IO#flush or when the
|
16
|
+
# buffer gets full). This behavior can be overridden by setting the streams to
|
17
|
+
# sync, like this:
|
18
|
+
#
|
19
|
+
# STDOUT.sync, STDERR.sync = true, true
|
20
|
+
#
|
21
|
+
# If you can't control the program and have it explicitly flush its output when it
|
22
|
+
# should, or you can't tell the streams to run in sync mode, see
|
23
|
+
# PTYBackgroundProcess.run for a workaround.
|
24
|
+
def self.run(command)
|
12
25
|
command = sanitize_params(command) if command.is_a?(Array)
|
13
26
|
child_stdin, parent_stdin = IO::pipe
|
14
27
|
parent_stdout, child_stdout = IO::pipe
|
@@ -80,12 +93,7 @@ class BackgroundProcess
|
|
80
93
|
# * 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
94
|
# * &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
95
|
def detect(which = :both, timeout = nil, &block)
|
83
|
-
streams =
|
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
|
96
|
+
streams = select_streams(which)
|
89
97
|
BackgroundProcess::IOHelpers.detect(streams, timeout, &block)
|
90
98
|
end
|
91
99
|
|
@@ -94,4 +102,13 @@ class BackgroundProcess
|
|
94
102
|
def self.sanitize_params(params)
|
95
103
|
params.map { |p| p.gsub(' ', '\ ') }.join(" ")
|
96
104
|
end
|
105
|
+
|
106
|
+
def select_streams(which)
|
107
|
+
case which
|
108
|
+
when :stdout then [stdout]
|
109
|
+
when :stderr then [stderr]
|
110
|
+
when :both then [stdout, stderr]
|
111
|
+
else raise(ArgumentError, "invalid stream specification: #{which}")
|
112
|
+
end.compact
|
113
|
+
end
|
97
114
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'pty'
|
2
|
+
|
3
|
+
class PTYBackgroundProcess < BackgroundProcess
|
4
|
+
# Runs a subprocess in a pseudo terminal, tricking a program into not
|
5
|
+
# buffering its output.
|
6
|
+
#
|
7
|
+
# A great write up on pseudo-terminals here:
|
8
|
+
# http://stackoverflow.com/questions/1154846/continuously-read-from-stdout-of-external-process-in-ruby
|
9
|
+
#
|
10
|
+
# It has the following disadvantages:
|
11
|
+
# * You can't get the exit status
|
12
|
+
# * When the process dies, whatever output you haven't read yet is lost.
|
13
|
+
# * stderr is merged into stdout
|
14
|
+
def self.run(command)
|
15
|
+
thread = Thread.new do # why run PTY separate thread? When a PTY instance
|
16
|
+
# dies, it raises PTY::ChildExited on the thread that
|
17
|
+
# spawned it, interrupting whatever happens to be
|
18
|
+
# running at the time
|
19
|
+
PTY.spawn(command) do |output, input, pid|
|
20
|
+
begin
|
21
|
+
bp = new(pid, input, output)
|
22
|
+
Thread.current[:background_process] = bp
|
23
|
+
bp.wait
|
24
|
+
rescue Exception => e
|
25
|
+
puts e
|
26
|
+
puts e.backtrace
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
sleep 0.01 until thread[:background_process]
|
31
|
+
thread[:background_process]
|
32
|
+
end
|
33
|
+
|
34
|
+
def stderr
|
35
|
+
raise ArgumentError, "stderr is merged into stdout with PTY subprocesses"
|
36
|
+
end
|
37
|
+
|
38
|
+
def wait(timeout = nil)
|
39
|
+
begin
|
40
|
+
Timeout.timeout(timeout) do
|
41
|
+
Process.wait(@pid)
|
42
|
+
end
|
43
|
+
rescue Timeout::Error
|
44
|
+
nil
|
45
|
+
rescue PTY::ChildExited
|
46
|
+
true
|
47
|
+
rescue Errno::ECHILD
|
48
|
+
true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def exitstatus
|
53
|
+
raise ArgumentError, "exitstatus is not available for PTY subprocesses"
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
def select_streams(which)
|
58
|
+
case which
|
59
|
+
when :stderr then stderr # let stderr throw the exception
|
60
|
+
when :stdout, :both then [stdout]
|
61
|
+
else raise(ArgumentError, "invalid stream specification: #{which}")
|
62
|
+
end.compact
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe PTYBackgroundProcess do
|
4
|
+
describe ".run" do
|
5
|
+
it "runs a subprocess with streams that are identified as tty's" do
|
6
|
+
process = PTYBackgroundProcess.run("env PS1='$' sh")
|
7
|
+
process.stdin.should be_tty
|
8
|
+
process.stdout.should be_tty
|
9
|
+
process.kill
|
10
|
+
end
|
11
|
+
|
12
|
+
it "allows bidirectional communication with the process" do
|
13
|
+
process = PTYBackgroundProcess.run("env PS1='$' sh")
|
14
|
+
process.stdout.getc # wait for the prompt
|
15
|
+
process.stdin.puts "echo Hello World"
|
16
|
+
process.stdout.gets.chomp.should == "echo Hello World"
|
17
|
+
process.stdout.gets.chomp.should == "Hello World"
|
18
|
+
process.stdin.puts "exit"
|
19
|
+
process.wait
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#exitstatus" do
|
24
|
+
it "raises an error if you query it" do
|
25
|
+
process = PTYBackgroundProcess.run("exit 1")
|
26
|
+
lambda {process.exitstatus}.should raise_error(ArgumentError, /not available/)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#stderr" do
|
31
|
+
it "raises if you try to access it" do
|
32
|
+
process = PTYBackgroundProcess.run("exit 1")
|
33
|
+
lambda {process.stderr}.should raise_error(ArgumentError, /merged.+stdout/)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#detect" do
|
38
|
+
it "you would expect" do
|
39
|
+
process = PTYBackgroundProcess.run("bash -c 'echo output; echo error 1>&2'")
|
40
|
+
process.detect { |line| true if line =~ /output/ }.should be_true
|
41
|
+
process.detect { |line| true if line =~ /error/ }.should be_true
|
42
|
+
end
|
43
|
+
|
44
|
+
it "raises if you try to select :stderr only" do
|
45
|
+
process = PTYBackgroundProcess.run("bash -c 'echo output; echo error 1>&2'")
|
46
|
+
lambda { process.detect(:stderr) { |line| true } }.should raise_error(ArgumentError, /merged.+stdout/)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
metadata
CHANGED
@@ -4,8 +4,8 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 1
|
7
|
-
-
|
8
|
-
version: "1.
|
7
|
+
- 2
|
8
|
+
version: "1.2"
|
9
9
|
platform: ruby
|
10
10
|
authors:
|
11
11
|
- Tim Harper
|
@@ -13,7 +13,7 @@ autorequire:
|
|
13
13
|
bindir: bin
|
14
14
|
cert_chain: []
|
15
15
|
|
16
|
-
date: 2010-04-
|
16
|
+
date: 2010-04-28 00:00:00 -06:00
|
17
17
|
default_executable:
|
18
18
|
dependencies: []
|
19
19
|
|
@@ -32,9 +32,11 @@ files:
|
|
32
32
|
- MIT-LICENSE
|
33
33
|
- lib/background_process/background_process.rb
|
34
34
|
- lib/background_process/io_helpers.rb
|
35
|
+
- lib/background_process/pty_background_process.rb
|
35
36
|
- lib/background_process.rb
|
36
37
|
- spec/background_process/background_process_spec.rb
|
37
38
|
- spec/background_process/io_helpers_spec.rb
|
39
|
+
- spec/background_process/pty_background_process_spec.rb
|
38
40
|
- spec/spec_helper.rb
|
39
41
|
has_rdoc: true
|
40
42
|
homepage: http://github.com/timcharper/background_process
|
@@ -70,4 +72,5 @@ summary: background_process
|
|
70
72
|
test_files:
|
71
73
|
- spec/background_process/background_process_spec.rb
|
72
74
|
- spec/background_process/io_helpers_spec.rb
|
75
|
+
- spec/background_process/pty_background_process_spec.rb
|
73
76
|
- spec/spec_helper.rb
|