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)