class TTY::Command::ProcessRunner

Constants

BUFSIZE

The buffer size for reading stdout and stderr

Attributes

cmd[R]

the command to be spawned

Public Class Methods

new(cmd, printer, &block) click to toggle source

Initialize a Runner object

@param [Printer] printer

the printer to use for logging

@api private

# File lib/tty/command/process_runner.rb, line 21
def initialize(cmd, printer, &block)
  @cmd     = cmd
  @timeout = cmd.options[:timeout]
  @input   = cmd.options[:input]
  @signal  = cmd.options[:signal] || "SIGKILL"
  @binmode = cmd.options[:binmode]
  @printer = printer
  @block   = block
end

Public Instance Methods

run!() click to toggle source

Execute child process

Write the input if provided to the child's stdin and read the contents of both the stdout and stderr.

If a block is provided then yield the stdout and stderr content as its being read.

@api public

# File lib/tty/command/process_runner.rb, line 40
def run!
  @printer.print_command_start(cmd)
  start = Time.now

  pid, stdin, stdout, stderr = ChildProcess.spawn(cmd)

  write_stream(stdin, @input)

  stdout_data, stderr_data = read_streams(stdout, stderr)

  status = waitpid(pid)
  runtime = Time.now - start

  @printer.print_command_exit(cmd, status, runtime)

  Result.new(status, stdout_data, stderr_data, runtime)
ensure
  [stdin, stdout, stderr].each { |fd| fd.close if fd && !fd.closed? }
  if pid # Ensure no zombie processes
    ::Process.detach(pid)
    terminate(pid)
  end
end
terminate(pid) click to toggle source

Stop a process marked by pid

@param [Integer] pid

@api public

# File lib/tty/command/process_runner.rb, line 69
def terminate(pid)
  ::Process.kill(@signal, pid) rescue nil
end

Private Instance Methods

handle_timeout(runtime) click to toggle source

@api private

# File lib/tty/command/process_runner.rb, line 79
def handle_timeout(runtime)
  return unless @timeout

  t = @timeout - runtime
  raise TimeoutExceeded if t < 0.0
end
read_stream(stream, buffer) click to toggle source
# File lib/tty/command/process_runner.rb, line 157
def read_stream(stream, buffer)
  Thread.new do
    if Thread.current.respond_to?(:report_on_exception)
      Thread.current.report_on_exception = false
    end
    Thread.current[:cmd_start] = Time.now
    readers = [stream]

    while readers.any?
      ready = IO.select(readers, nil, readers, @timeout)
      raise TimeoutExceeded if ready.nil?

      ready[0].each do |reader|
        begin
          line = reader.readpartial(BUFSIZE)
          buffer.(line)

          # control total time spent reading
          runtime = Time.now - Thread.current[:cmd_start]
          handle_timeout(runtime)
        rescue Errno::EAGAIN, Errno::EINTR
        rescue EOFError, Errno::EPIPE, Errno::EIO # thrown by PTY
          readers.delete(reader)
          reader.close
        end
      end
    end
  end
end
read_streams(stdout, stderr) click to toggle source

Read stdout & stderr streams in the background

@param [IO] stdout @param [IO] stderr

@api private

# File lib/tty/command/process_runner.rb, line 127
def read_streams(stdout, stderr)
  stdout_data = []
  stderr_data = Truncator.new

  out_buffer = ->(line) {
    stdout_data << line
    @printer.print_command_out_data(cmd, line)
    @block.(line, nil) if @block
  }

  err_buffer = ->(line) {
    stderr_data << line
    @printer.print_command_err_data(cmd, line)
    @block.(nil, line) if @block
  }

  stdout_thread = read_stream(stdout, out_buffer)
  stderr_thread = read_stream(stderr, err_buffer)

  stdout_thread.join
  stderr_thread.join

  encoding = @binmode ? Encoding::BINARY : Encoding::UTF_8

  [
    stdout_data.join.force_encoding(encoding),
    stderr_data.read.dup.force_encoding(encoding)
  ]
end
waitpid(pid) click to toggle source

@api private

# File lib/tty/command/process_runner.rb, line 188
def waitpid(pid)
  _pid, status = ::Process.waitpid2(pid, ::Process::WUNTRACED)
  status.exitstatus || status.termsig if _pid
rescue Errno::ECHILD
  # In JRuby, waiting on a finished pid raises.
end
write_stream(stream, input) click to toggle source

Write the input to the process stdin

@api private

# File lib/tty/command/process_runner.rb, line 89
def write_stream(stream, input)
  start = Time.now
  writers = [input && stream].compact

  while writers.any?
    ready = IO.select(nil, writers, writers, @timeout)
    raise TimeoutExceeded if ready.nil?

    ready[1].each do |writer|
      begin
        err   = nil
        size  = writer.write(@input)
        input = input.byteslice(size..-1)
      rescue IO::WaitWritable
      rescue Errno::EPIPE => err
        # The pipe closed before all input written
        # Probably process exited prematurely
        writer.close
        writers.delete(writer)
      end
      if err || input.bytesize == 0
        writer.close
        writers.delete(writer)
      end

      # control total time spent writing
      runtime = Time.now - start
      handle_timeout(runtime)
    end
  end
end