lib/rexec/task.rb in rexec-1.2.1 vs lib/rexec/task.rb in rexec-1.2.3

- old
+ new

@@ -1,36 +1,47 @@ -# 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. +# Copyright (c) 2007, 2011 Samuel G. D. Williams. <http://www.oriontransfer.co.nz> # -# 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. +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: # -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. require 'thread' module RExec + private + RD = 0 WR = 1 + + public - # This function closes all IO other than $stdin, $stdout, $stderr + # Cloose all IO other than $stdin, $stdout, $stderr (or those given by the argument except) 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 + # Represents a running process, either a child process or a background/daemon process. + # Provides an easy high level interface for managing process life-cycle. class Task private def self.pipes_for_options(options) pipes = [[nil, nil], [nil, nil], [nil, nil]] @@ -70,12 +81,14 @@ end return pipes end - # Close all the supplied pipes + # The standard process pipes. STDPIPES = [STDIN, STDOUT, STDERR] + + # Close all the supplied pipes. def self.close_pipes(*pipes) pipes = pipes.compact.reject{|pipe| STDPIPES.include?(pipe)} pipes.each do |pipe| pipe.close unless pipe.closed? @@ -110,20 +123,19 @@ 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. - # <tt> - # 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 - # </tt> + # + # 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 @@ -145,49 +157,105 @@ return pid.to_i end # Very simple method to spawn a child process - # <tt> - # spawn_child do - # puts "Hello from child!" - # end - # </tt> + # + # 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. + # 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 may write to +output+, and read from +input+ and +error+. # - # = Options = + # Typical usage looks similar to +IO.popen+: + # count = 0 + # result = Task.open(["ls", "-la"], :passthrough => :err) do |task| + # count = task.output.read.split(/\n/).size + # end + # puts "Count: #{count}" if result.exitstatus == 0 # - # 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. - # <tt>:passthrough => :err</tt> - # <tt>:passthrough => [:in, :out, :err]</tt> or <tt>:passthrough => :all</tt> + # The basic command can simply be a string, and this will be passed to +Kernel#exec+ which will perform + # shell expansion on the arguments. # - # We can specify a set of pipes other than the standard ones for redirecting to other things, eg - # <tt>:out => File.open("output.log", "a")</tt> + # If the command passed is an array, this will be executed without shell expansion. # - # If you need to supply a pipe manually, you can do that too: - # <tt>:in => IO.pipe</tt> + # If a +Proc+ (or anything that +respond_to? :call+) is provided, this will be executed in the child + # process. Here is an example of a long running background process: # - # You can specify <tt>:daemonize => true</tt> to cause the child process to detatch. In this - # case you will generally want to specify files for <tt>:in, :out, :err</tt> e.g. - # <tt> - # :in => File.open("/dev/null"), - # :out => File.open("/var/log/my.log", "a"), - # :err => File.open("/var/log/my.err", "a") - # </tt> + # daemon = Proc.new do + # # Long running process + # sleep(1000) + # end + # + # task = Task.open(daemon, :daemonize => true, :in => ..., :out => ..., :err => ...) + # exit(0) + # + # ==== Options + # + # [+:passthrough+, +:in+, +:out+, +:err+] + # The current process (e.g. ruby) has a set of existing pipes +$stdin+, +$stdout+ and + # +$stderr+. These pipes can also be used by the child process. The passthrough option + # allows you to specify which pipes are retained from the parent process by the child. + # + # Typically it is useful to passthrough $stderr, so that errors in the child process + # are printed out in the terminal of the parent process: + # Task.open([...], :passthrough => :err) + # Task.open([...], :passthrough => [:in, :out, :err]) + # Task.open([...], :passthrough => :all) + # + # It is also possible to redirect to files, which can be useful if you want to keep a + # a log file: + # Task.open([...], :out => File.open("output.log")) + # + # The default behaviour is to create a new pipe, but any pipe (e.g. a network socket) + # could be used: + # Task.open([...], :in => IO.pipe) + # + # [+:daemonize+] + # The process that is opened may be detached from the parent process. This allows the + # child process to exist even if the parent process exits. In this case, you will also + # probably want to specify the +:passthrough+ option for log files: + # Task.open([...], + # :daemonize => true, + # :in => File.open("/dev/null"), + # :out => File.open("/var/log/child.log", "a"), + # :err => File.open("/var/log/child.err", "a") + # ) + # + # [+:env+, +:env!+] + # Provide a environment which will be used by the child process. Use +:env+ to update + # the exsting environment and +:env!+ to replace it completely. + # Task.open([...], :env => {'foo' => 'bar'}) + # + # [+:umask+] + # Set the umask for the new process, as per +File.umask+. + # + # [+:chdir+] + # Set the current working directory for the new process, as per +Dir.chdir+. + # + # [+:preflight+] + # Similar to a proc based command, but executed before execing the given process. + # preflight = Proc.new do |command, options| + # # Setup some default state before exec the new process. + # end + # + # Task.open([...], :preflight => preflight) + # + # The options hash is passed directly so you can supply custom arguments to the preflight + # function. + def self.open(command, options = {}, &block) cin, cout, cerr = pipes_for_options(options) spawn = options[:daemonize] ? :spawn_daemon : :spawn_child cid = self.send(spawn) do @@ -195,10 +263,29 @@ STDIN.reopen(cin[RD]) if cin[RD] STDOUT.reopen(cout[WR]) if cout[WR] STDERR.reopen(cerr[WR]) if cerr[WR] + if options[:env!] + ENV.clear + ENV.update(options[:env!]) + elsif options[:env] + ENV.update(options[:env]) + end + + if options[:umask] + File.umask(options[:umask]) + end + + if options[:chdir] + Dir.chdir(options[:chdir]) + end + + if options[:preflight] + preflight.call(command, options) + end + if command.respond_to? :call command.call elsif Array === command # 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. @@ -231,11 +318,11 @@ else return task end end - def initialize(input, output, error, pid) + def initialize(input, output, error, pid) # :nodoc: @input = input @output = output @error = error @pid = pid @@ -244,13 +331,22 @@ @status = :running @result_lock = Mutex.new @result_available = ConditionVariable.new end + # Standard input to the running task. attr :input + + # Standard output from the running task. attr :output + + # Standard error from the running task. attr :error + + # The PID of the running task. attr :pid + + # The status of the task after calling task.wait. attr :result # Returns true if the current task is still running def running? if self.class.running?(@pid)