lib/servolux/child.rb
# == Synopsis # Manage a child process spawned via IO#popen and provide a timeout # mechanism to kill the process after some amount time. # # == Details # Ruby provides the IO#popen method to spawn a child process and return an # IO instance connected to the child's stdin and stdout (with stderr # redirected to stdout). The Servolux::Child class adds to this a timeout # thread that will signal the child process after some number of seconds. # If the child exits cleanly before the timeout expires then no signals are # sent to the child. # # A list of signals can be provided which will be sent in succession to the # child until one of them causes the child to exit. The current Ruby thread # suspends for a few seconds to allow each signal to be processed by the # child. By default these signals are SIGTERM, SIGQUIT, SIGKILL and the time # to wait between signals is four seconds. # # The +stop+ method is used to stop the child process (if running) and to # reset the state of the Child instance so that it can be started again. # Stopping the Child instance closes the IO between parent and child # process. # # The +wait+ method is used to wait for the child process to exit. The # Process::Status object is retrieved by the Child and stored as an instance # variable. The +exitstatus+ method (and the other process related methods) # will return non-nil values after the wait method is called. # # == Examples # # child = Servolux::Child.new(:command => 'sleep 120', :timeout => 10) # child.start # child.wait # # child.timed_out? #=> true # child.signaled? #=> true # child.exitstatus #=> nil # class Servolux::Child attr_accessor :command attr_accessor :timeout attr_accessor :signals attr_accessor :suspend attr_reader :io attr_reader :pid # Create a new Child that will execute and manage the +command+ string as # a child process. # # ==== Options # * command <String> # The command that will be executed via IO#popen. # # * timeout <Numeric> # The number of seconds to wait before terminating the child process. # No action is taken if the child process exits normally before the # timeout expires. # # * signals <Array> # A list of signals that will be sent to the child process when the # timeout expires. The signals increase in severity with SIGKILL being # the signal of last resort. # # * suspend <Numeric> # The number of seconds to wait for the child process to respond to # a signal before trying the next one in the list. # def initialize( opts = {} ) @command = opts.getopt :command @timeout = opts.getopt :timeout @signals = opts.getopt :signals, %w[TERM QUIT KILL] @suspend = opts.getopt :suspend, 4 @io = @pid = @status = @thread = @timed_out = nil yield self if block_given? end # Runs the +command+ string as a subprocess; the subprocess’s # standard input and output will be connected to the returned IO object. # The default mode for the new file object is "r", but mode may be set to # any of the modes listed in the description for class IO. # # If a block is given, Ruby will run the +command+ as a child connected to # Ruby with a pipe. Ruby’s end of the pipe will be passed as a parameter # to the block. In this case the value of the block is returned. # def start( mode = 'r', &block ) start_timeout_thread if @timeout @io = IO::popen @command, mode @pid = @io.pid @status = nil return block.call(@io) unless block.nil? @io end # Stop the child process if it is alive. A sequence of +signals+ are sent # to the process until it dies with SIGKILL being the signal of last # resort. # # After this method returns, the IO pipe to the child will be closed and # the stored child PID is set to +nil+. The +start+ method can be safely # called again. # def stop unless @thread.nil? t, @thread = @thread, nil t[:stop] = true t.wakeup.join if t.status end kill if alive? @io.close rescue nil @io = nil self end # Waits for the child process to exit and returns its exit status. The # global variable $? is set to a Process::Status object containing # information on the child process. # def wait( flags = 0 ) return if @io.nil? Process.wait(@pid, flags) @status = $? exitstatus end # Returns +true+ if the child process is alive. Returns +nil+ if the child # process has not been started. # def alive? return if @io.nil? Process.kill(0, @pid) true rescue Errno::ESRCH, Errno::ENOENT false end # Returns +true+ if the child process was killed by the timeout thread. # def timed_out? @timed_out end %w[coredump? exited? signaled? stopped? success? exitstatus stopsig termsig]. each { |method| self.class_eval <<-CODE def #{method} return if @status.nil? @status.#{method} end CODE } private # Attempt to kill the child process by sending the configured +signals+ # and waiting for +suspend+ seconds between each signal; this gives the # child time to respond to the signal. # # Returns +true+ if the child died. Returns +false+ if the child is still # not dead after the last signal was sent. Returns +nil+ if the child was # not running in the first place. # def kill return if @io.nil? existed = false @signals.each do |sig| begin Process.kill sig, @pid existed = true rescue Errno::ESRCH, Errno::ENOENT return(existed ? nil : true) end return true unless alive? sleep @suspend return true unless alive? end return !alive? end def start_timeout_thread @timed_out = false @thread = Thread.new(self) { |child| sleep @timeout unless Thread.current[:stop] if child.alive? child.instance_variable_set(:@timed_out, true) child.__send__(:kill) end end } end end # class Servolux::Child # EOF