ruby/open4.rb (187 lines of code) (raw):

# This class provides extended popen3-style functionality # by keeping track of running processes, supporting argument # lists rather than shell command lines, and allowing # +stdin+, +stdout+ and +stderr+ to be configured separately. # # = Example # # require 'open4' # # child = Popen4.new("echo hello; echo there >&2; cat; exit 4") # p child.stdout.readline #=> "hello\n" # p child.stderr.readline #=> "there\n" # child.stdin.puts "Read by cat" # p child.stdout.readline #=> "Read by cat\n" # child.stdin.close # p child.wait.exitstatus #=> 4 # # Beware of the deadlocks that can arise when interacting with # processes in this manner. If you allow +stderr+ to be made into # a pipe (the default here) and do not read from it, a process # will block once it has written a pipe full of data (typically 4k). # In this case, specify <code>:stdout=>false</code> in the constructor. # # To get started, look at the documentation for Popen4.new. # #-- # $Id: /local/dcs/trunk/prog/ruby/open4.rb 645 2005-03-15T15:24:02.589341Z jp $ require 'thread' class Popen4 @@active = [] # call-seq: # Popen4.new(command [, arg, ...] [,options = {}]) # # Starts a process in the background with the same semantics # as <code>Kernel::exec</code>. In summary, if a single argument is given, # the string is passed to the shell, otherwise the second and subsequent # arguments are passed as parameters to _command_ with no shell # expansion. # # Popen4.new("echo *").stdout.read #=> "file1 file2\n" # Popen4.new("echo","*").stdout.read #=> "*\n" # Popen4.new("exit 2;").wait.exitstatus #=> 2 # # Keyword options may given to specify the redirection of # +stdin+, +stdout+ and +stderr+. The objects given should be capable # of being an argument to IO::reopen, +nil+ to specify # no redirection, or a symbol listed below. # # <code>:stdout</code> or <code>:stderr</code> may be specified to make the # file descriptor in question be a copy of an already-allocated # pipe. <code>:null</code> indicates redirection to <code>/dev/null</code>. # # For example, # # child = Popen4.new("command",:stdin=>:null,:stderr=>:stdout) # # will connect +stdin+ to <code>/dev/null</code> and make both +stdout+ and # +stderr+ of the child process available on the <code>child.stdout</code> # stream. # # child = Popen4.new("ssh","somewhere",:stderr=>false) # # The above would be used to make the child process +stdin+ and # +stdout+ available to the Ruby script via pipes, but leave +stderr+ # where it was (a terminal, for example). # # This last example will copy the data between two # file objects (they must be backed by a real file descriptor). # # Popen4.new("cat",:stdin=>file1,:stdout=>file2).wait # # Yields self if given a block and closes the pipes before returning. def initialize(*cmd) Popen4::cleanup if Hash === cmd.last then options = cmd.pop else options = {} end @wait_mutex = Mutex.new @stdin = nil @stdout = nil @stderr = nil @status = nil # List of file handles to close in the parent to_close = [] if options.has_key?(:stdin) then child_stdin = options[:stdin] if child_stdin == :null then child_stdin = File.open("/dev/null","r") to_close << child_stdin end else child_stdin, @stdin = IO::pipe to_close << child_stdin end if options.has_key?(:stdout) then child_stdout = options[:stdout] else @stdout, child_stdout = IO::pipe to_close << child_stdout end if options.has_key?(:stderr) then child_stderr = options[:stderr] else @stderr, child_stderr = IO::pipe to_close << child_stderr end # Handle redirections to /dev/null if child_stdout == :null then child_stdout = File.open("/dev/null","w") to_close << child_stdout end if child_stderr == :null then child_stderr = File.open("/dev/null","w") to_close << child_stderr end # Handle redirections from stdout<->stderr child_stdout = child_stderr if child_stdout == :stderr child_stderr = child_stdout if child_stderr == :stdout @pid = fork do if child_stdin then @stdin.close if @stdin STDIN.reopen(child_stdin) child_stdin.close end if child_stdout then @stdout.close if @stdout STDOUT.reopen(child_stdout) end if child_stderr then @stderr.close if @stderr STDERR.reopen(child_stderr) child_stderr.close end if child_stdout child_stdout.close unless child_stdout.closed? end begin Kernel::exec(*cmd) ensure exit!(1) end end to_close.each { |fd| fd.close } @stdin.sync = true if @stdin Thread.exclusive { @@active << self } if block_given? then begin yield self ensure close end end end # Reap any child processes (by calling poll) as necessary def self.cleanup active = Thread.exclusive { @@active.dup } active.each do |inst| inst.poll end end # File handle for pipes to the slave process. # Will be +nil+ if a corresponding alternative file # was given in the constructor. attr_reader :stdin, :stdout, :stderr # Process ID of the child attr_reader :pid # Close the stdin/stdout/stderr pipes from the child process # (if they were created in the constructor). def close [@stdin, @stdout, @stderr].each do |fp| begin fp.close if fp and not fp.closed? rescue end end end # Wait for the exit status of the process, returning # a <code>Process::Status</code> object. The _flags_ # argument is interpreted as in <code>Process::wait</code>. # # NB: This wait only returns once the process has actually # exited. It does not return for stopped (signaled) processes. def wait(flags=0) @wait_mutex.synchronize do wait_no_lock(flags) end end def wait_no_lock(flags=0) #:nodoc: return @status if @status while result = Process::waitpid2(@pid, flags) # Only return exit status if result[0] == @pid and (result[1].exited? or result[1].signaled?) then @status = result[1] Thread.exclusive { @@active.delete(self) } return @status end end nil end private :wait_no_lock # Test to see if process has exited without blocking. Returns # a <code>Process::Status</code> object or +nil+. def poll if @wait_mutex.try_lock then begin wait_no_lock(Process::WNOHANG) ensure @wait_mutex.unlock end else nil end end alias :status :poll # Send the given signal to the process def kill(signal) Process::kill(signal,@pid) end end if $0 == __FILE__ require 'test/unit' class TC_Open4 < Test::Unit::TestCase def test_default p = Popen4.new('read X;echo hello; echo "X was $X"; echo there>&2; exit 4') p.stdin.puts "asdf" assert_equal "hello",p.stdout.readline.chomp assert_equal "X was asdf",p.stdout.readline.chomp assert_equal "there",p.stderr.readline.chomp assert_equal 4,p.wait.exitstatus p.close end def test_no_stderr p = Popen4.new('echo hello; echo ignore this message >&2',:stderr=>false) assert_equal "hello",p.stdout.readline.chomp assert_equal nil,p.stderr assert_equal 0,p.wait.exitstatus p.close end def test_no_shell Popen4.new('echo','$PATH') do |p| # This should be literal '$PATH' and not expanded by the shell assert_equal "$PATH", p.stdout.readline.chomp end end def test_threaded threads = (0..5).map do |idx| Thread.new do 3.times do Popen4.new("echo A#{idx};sleep 1;echo B#{idx}",:stderr=>false) do |p| assert_equal "A#{idx}", p.stdout.readline.chomp assert_equal "B#{idx}", p.stdout.readline.chomp assert_equal 0, p.wait.exitstatus end end end end threads.each { |t| t.join } end def test_kill p = Popen4.new <<-EOT trap "echo BYE_stderr>&2;echo BYE_stdout" EXIT echo start sleep 10 echo notreached EOT assert_equal "start",p.stdout.readline.chomp sleep 1 p.kill('TERM') assert_equal "BYE_stdout",p.stdout.readline.chomp assert_equal "BYE_stderr",p.stderr.readline.chomp assert_equal nil,p.wait.exitstatus assert_equal Signal.list['TERM'],p.wait.termsig end end end