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
@@ -1,2 +1,3 @@
1
1
  require File.dirname(__FILE__) + "/background_process/background_process"
2
+ require File.dirname(__FILE__) + "/background_process/pty_background_process"
2
3
  require File.dirname(__FILE__) + "/background_process/io_helpers"
@@ -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
- # Run a BackgroundProcess
11
- def self.run(command, &block)
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 = 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
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
- - 1
8
- version: "1.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-22 00:00:00 -06:00
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