dblock/heroku-commander

View on GitHub
lib/heroku/executor.rb

Summary

Maintainability
C
1 day
Test Coverage
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