module Runaway

Constants

Child
DEFAULT
INF
KILL
RuntimeExceeded
TERM
USR2
UncleanExit
VERSION

Public Class Methods

spin(must_quit_within: INF, logger: NullLogger, &block_to_run_in_child) click to toggle source
# File lib/runaway.rb, line 17
def self.spin(must_quit_within: INF, logger: NullLogger, &block_to_run_in_child)
  child_pid = fork do
    # Remove anything that was there from the parent
    [TERM, KILL].each { |reset_sig| trap(reset_sig, DEFAULT) }
    block_to_run_in_child.call
  end
  
  started_at = Time.now
  
  has_quit = false
  unclean_exit_error = nil
  waiter_t = Thread.new do
    has_quit, status = Process.wait2(child_pid)
    unless status.exitstatus && status.exitstatus.zero?
      unclean_exit_error = UncleanExit.new("#{child_pid} exited uncleanly: #{status.inspect}")
    end
  end
  waiter_t.abort_on_exception = true
  
  soft_signal = ->(sig) {
    (Process.kill(sig, child_pid) rescue Errno::ESRCH) if !has_quit
  }
  
  begin
    loop do
      sleep 1
      
      break if has_quit
      
      # First check if it has exceeded it's wall clock time allowance
      running_for = Time.now - started_at
      if running_for > must_quit_within
        raise RuntimeExceeded.new('%d did not terminate after %d secs (limited to %d secs)' % [
          child_pid, running_for, must_quit_within])
      end
    end
  rescue Runaway => terminating_error
    logger.error "Terminating %d - %s: %s" % [child_pid, terminating_error.class, terminating_error.message]
    soft_signal[TERM]
    sleep 5
    soft_signal[KILL]
    
    raise terminating_error
  rescue Errno::EBADF, Errno::ESRCH, Errno::EPIPE
    # Could not read from the pipe - the child quit, or could not send signal
  end
  
  # If the loop has terminated the process has certainly quit, and we can join the waiter thread
  waiter_t.join
  
  # If the process exited uncleanly (not with status 0) raise an exception in the parent
  raise unclean_exit_error if unclean_exit_error
  
  :done
end