lib/heroku/executor.rb
module Heroku
class Executor
class Terminate < StandardError
attr_accessor :timeout
def initialize(timeout = 0)
@timeout = timeout
end
end
class << self
# Executes a command and yields output line-by-line.
def run(cmd, options = {}, &block)
lines = []
running_pid = nil
logger = options[:logger]
logger.debug "Running: #{cmd}" if logger
PTY.spawn(cmd) do |r, w, pid|
running_pid = pid
logger.debug "Started: #{pid}" if logger
terminated = false
begin
r.sync = true
read_from(r, pid, options, lines, &block)
rescue Heroku::Executor::Terminate => e
logger.debug "Waiting: #{e.timeout} second(s) to terminate #{pid}" if logger
terminate_process! pid, e.timeout
terminated = true
rescue Errno::EIO, IOError => e
# ignore
rescue PTY::ChildExited => e
logger.debug "Terminated: #{pid}" if logger
terminated = true
raise e
ensure
unless terminated
# wait for process
logger.debug "Waiting: #{pid}" if logger
::Process.wait(pid)
end
end
end
check_exit_status! cmd, running_pid, $?.exitstatus, lines
lines
rescue Errno::ECHILD => e
check_exit_status! cmd, running_pid, $?.exitstatus, lines
lines
rescue PTY::ChildExited => e
check_exit_status! cmd, running_pid, $!.status.exitstatus, lines
lines
rescue Heroku::Commander::Errors::Base => e
logger.debug "Error: #{e.problem}" if logger
raise
rescue Exception => e
logger.debug "#{e.class}: #{e.respond_to?(:problem) ? e.problem : e.message}" if logger
raise Heroku::Commander::Errors::CommandError.new({
:cmd => cmd,
:pid => running_pid,
:status => $?.exitstatus,
:message => e.message,
:inner_exception => e,
:lines => lines
})
end
private
def read_from(r, pid, options, lines, &block)
logger = options[:logger]
while ! r.eof do
line = r.readline
line.strip! if line
logger.debug "#{pid}: #{line}" if logger
if block_given?
yield line
end
lines << line
end
end
def check_exit_status!(cmd, pid, status, lines = nil)
return if ! status || status == 0
raise Heroku::Commander::Errors::CommandError.new({
:cmd => cmd,
:pid => pid,
:status => status,
:message => "The command #{cmd} failed with exit status #{status}.",
:lines => lines
})
end
def terminate_process!(pid, timeout)
if timeout
# Delay terminating of the process, usually to let the output flush.
Thread.new(pid, timeout) do |pid, timeout|
begin
sleep(timeout) if timeout
::Process.kill("TERM", pid)
rescue
end
end
else
::Process.kill("TERM", pid)
end
end
end
end
end