# Copyright (c) 2007 Samuel Williams. Released under the GNU GPLv3. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . class String # Helper for turning a string into a shell argument def to_arg match(/\s/) ? dump : self end def to_cmd return self end end class Array # Helper for turning an array of items into a command line string # ["ls", "-la", "/My Path"].to_cmd => "ls -la \"/My Path\"" def to_cmd collect{ |a| a.to_arg }.join(" ") end end class Pathname # Helper for turning a pathname into a command line string def to_cmd to_s end end module RExec RD = 0 WR = 1 # This function closes all IO other than $stdin, $stdout, $stderr def self.close_io(except = [$stdin, $stdout, $stderr]) # Make sure all file descriptors are closed ObjectSpace.each_object(IO) do |io| unless except.include?(io) io.close rescue nil end end end class Task private def self.pipes_for_options(options) pipes = [[nil, nil], [nil, nil], [nil, nil]] if options[:passthrough] passthrough = options[:passthrough] if passthrough == :all passthrough = [:in, :out, :err] elsif passthrough.kind_of?(Symbol) passthrough = [passthrough] end passthrough.each do |name| case(name) when :in options[:in] = $stdin when :out options[:out] = $stdout when :err options[:err] = $stderr end end end modes = [RD, WR, WR] {:in => 0, :out => 1, :err => 2}.each do |name, idx| m = modes[idx] p = options[name] if p.kind_of?(IO) pipes[idx][m] = p elsif p.kind_of?(Array) and p.size == 2 pipes[idx] = p else pipes[idx] = IO.pipe end end return pipes end # Close all the supplied pipes def close_pipes(*pipes) pipes.compact! pipes.each do |pipe| pipe.close unless pipe.closed? end end # Dump any remaining data from the pipes, until they are closed. def dump_pipes(*pipes) pipes.compact! pipes.delete_if { |pipe| pipe.closed? } # Dump any output that was not consumed (errors, etc) while pipes.size > 0 result = IO.select(pipes) result[0].each do |pipe| if pipe.closed? || pipe.eof? pipes.delete(pipe) next end $stderr.puts pipe.readline.chomp end end end public # Returns true if the given pid is a current process def self.running?(pid) gpid = Process.getpgid(pid) rescue nil return gpid != nil ? true : false end # Very simple method to spawn a child daemon. A daemon is detatched from the controlling tty, and thus is # not killed when the parent process finishes. # # spawn_daemon do # Dir.chdir("/") # File.umask 0000 # puts "Hello from daemon!" # sleep(600) # puts "This code will not quit when parent process finishes..." # puts "...but $stdout might be closed unless you set it to a file." # end # def self.spawn_daemon(&block) pid_pipe = IO.pipe fork do Process.setsid exit if fork # Send the pid back to the parent pid_pipe[RD].close pid_pipe[WR].write(Process.pid.to_s) pid_pipe[WR].close yield exit(0) end pid_pipe[WR].close pid = pid_pipe[RD].read pid_pipe[RD].close return pid.to_i end # Very simple method to spawn a child process # # spawn_child do # puts "Hello from child!" # end # def self.spawn_child(&block) pid = fork do yield exit!(0) end return pid end # Open a process. Similar to IO.popen, but provides a much more generic interface to stdin, stdout, # stderr and the pid. We also attempt to tidy up as much as possible given some kind of error or # exception. You are expected to write to output, and read from input and error. # # = Options = # # We can specify a pipe that will be redirected to the current processes pipe. A typical one is # :err, so that errors in the child process are printed directly to $stderr of the parent process. # :passthrough => :err # :passthrough => [:in, :out, :err] or :passthrough => :all # # We can specify a set of pipes other than the standard ones for redirecting to other things, eg # :out => File.open("output.log", "a") # # If you need to supply a pipe manually, you can do that too: # :in => IO.pipe # # You can specify :daemon => true to cause the child process to detatch. In this # case you will generally want to specify files for :in, :out, :err e.g. # # :in => File.open("/dev/null"), # :out => File.open("/var/log/my.log", "a"), # :err => File.open("/var/log/my.err", "a") # def self.open(command, options = {}, &block) cin, cout, cerr = pipes_for_options(options) stdpipes = [STDIN, STDOUT, STDERR] spawn = options[:daemonize] ? :spawn_daemon : :spawn_child cid = self.send(spawn) do [cin[WR], cout[RD], cerr[RD]].compact.each { |pipe| pipe.close } STDIN.reopen(cin[RD]) if cin[RD] and !stdpipes.include?(cin[RD]) STDOUT.reopen(cout[WR]) if cout[WR] and !stdpipes.include?(cout[WR]) STDERR.reopen(cerr[WR]) if cerr[WR] and !stdpipes.include?(cerr[WR]) if command.respond_to? :call command.call else # If command is a Pathname, we need to convert it to an absolute path if possible, # otherwise if it is relative it might cause problems. if command.respond_to? :realpath command = command.realpath end if command.respond_to? :to_cmd exec(command.to_cmd) else exec(command.to_s) end end end # Don't close stdin, stdout, stderr. [cin[RD], cout[WR], cerr[WR]].compact.each { |pipe| pipe.close unless stdpipes.include?(pipe) } task = Task.new(cin[WR], cout[RD], cerr[RD], cid) if block_given? begin yield task task.close_input return task.wait ensure task.stop end else return task end end def initialize(input, output, error, pid) @input = input @output = output @error = error @pid = pid @result = nil end attr :input attr :output attr :error attr :pid attr :result def running? return self.class.running?(@pid) end # Close all connections to the child process def close close_pipes(@input, @output, @error) end # Close input pipe to child process (if applicable) def close_input @input.close if @input and !@input.closed? end # Send a signal to the child process def kill(signal = "INT") Process.kill("INT", @pid) end # Wait for the child process to finish, return the exit status. def wait begin close_input _pid, @result = Process.wait2(@pid) dump_pipes(@output, @error) ensure close_pipes(@input, @output, @error) end return @result end # Forcefully stop the child process. def stop # The process has already been stoped/waited upon return if @result begin close_input kill wait dump_pipes(@output, @error) ensure close_pipes(@output, @error) end end end end