class Scmd::Command

Constants

DEFAULT_STOP_TIMEOUT
READ_CHECK_TIMEOUT
READ_SIZE

Attributes

cmd_str[R]
env[R]
exitstatus[R]
options[R]
pid[R]
stderr[R]
stdout[R]

Public Class Methods

new(cmd_str, opts = nil) click to toggle source
# File lib/scmd/command.rb, line 21
def initialize(cmd_str, opts = nil)
  opts ||= {}
  @cmd_str = cmd_str
  @env     = stringify_hash(opts[:env] || {})
  @options = opts[:options] || {}
  reset_attrs
end

Public Instance Methods

inspect() click to toggle source
# File lib/scmd/command.rb, line 117
def inspect
  reference = "0x0%x" % (object_id << 1)
  "#<#{self.class}:#{reference}"\
  " @cmd_str=#{cmd_str.inspect}"\
  " @exitstatus=#{@exitstatus.inspect}>"
end
kill(signal = nil) click to toggle source
# File lib/scmd/command.rb, line 98
def kill(signal = nil)
  return unless running?

  send_kill(signal)
  wait # indefinitely until cmd is killed
end
run(input = nil) click to toggle source
# File lib/scmd/command.rb, line 29
def run(input = nil)
  begin
    run!(input)
  rescue
    RunError
  end
  self
end
run!(input = nil) click to toggle source
# File lib/scmd/command.rb, line 38
def run!(input = nil)
  start_err_msg, start_err_bt = nil, nil
  begin
    start(input)
  rescue => ex
    start_err_msg, start_err_bt = ex.message, ex.backtrace
  ensure
    wait # indefinitely until cmd is done running
    unless success?
      raise RunError.new(start_err_msg || @stderr, start_err_bt || caller)
    end
  end

  self
end
running?() click to toggle source
# File lib/scmd/command.rb, line 105
def running?
  !@child_process.nil?
end
start(input = nil) click to toggle source
# File lib/scmd/command.rb, line 54
def start(input = nil)
  setup_run

  @pid = @child_process.pid.to_i
  @child_process.write(input)
  @read_output_thread = Thread.new do
    while @child_process.check_for_exit
      begin
        read_output
      rescue EOFError # rubocop:disable Lint/SuppressedException
      end
    end
    @stop_w.write_nonblock(".")
  end
end
stop(timeout = nil) click to toggle source
# File lib/scmd/command.rb, line 87
def stop(timeout = nil)
  return unless running?

  send_term
  begin
    wait(timeout || DEFAULT_STOP_TIMEOUT)
  rescue TimeoutError
    kill
  end
end
success?() click to toggle source
# File lib/scmd/command.rb, line 109
def success?
  @exitstatus == 0
end
to_s() click to toggle source
# File lib/scmd/command.rb, line 113
def to_s
  @cmd_str.to_s
end
wait(timeout = nil) click to toggle source
# File lib/scmd/command.rb, line 70
def wait(timeout = nil)
  return unless running?

  wait_for_exit(timeout)
  if @child_process.running?
    kill
    raise(TimeoutError, "`#{@cmd_str}` timed out (#{timeout}s).")
  end
  @read_output_thread.join

  @stdout << @child_process.flush_stdout
  @stderr << @child_process.flush_stderr
  @exitstatus = @child_process.exitstatus

  teardown_run
end

Private Instance Methods

read_output() click to toggle source
# File lib/scmd/command.rb, line 126
def read_output
  @child_process.read(READ_SIZE) do |out, err|
    @stdout += out
    @stderr += err
  end
end
reset_attrs() click to toggle source
# File lib/scmd/command.rb, line 138
def reset_attrs
  @stdout, @stderr, @pid, @exitstatus = +"", +"", nil, nil
end
send_kill(signal = nil) click to toggle source
# File lib/scmd/command.rb, line 161
def send_kill(signal = nil)
  send_signal(signal || "KILL")
end
send_signal(sig) click to toggle source
# File lib/scmd/command.rb, line 165
def send_signal(sig)
  return unless running?
  @child_process.send_signal(sig)
end
send_term() click to toggle source
# File lib/scmd/command.rb, line 157
def send_term
  send_signal "TERM"
end
setup_run() click to toggle source
# File lib/scmd/command.rb, line 142
def setup_run
  reset_attrs
  @stop_r, @stop_w = IO.pipe
  @read_output_thread = nil
  @child_process = ChildProcess.new(@cmd_str, @env, @options)
end
stringify_hash(hash) click to toggle source
# File lib/scmd/command.rb, line 170
def stringify_hash(hash)
  hash.inject({}) do |h, (k, v)|
    h.merge(k.to_s => v.to_s)
  end
end
teardown_run() click to toggle source
# File lib/scmd/command.rb, line 149
def teardown_run
  @child_process.teardown
  [@stop_r, @stop_w].each{ |fd| fd.close if fd && !fd.closed? }
  @stop_r, @stop_w = nil, nil
  @child_process, @read_output_thread = nil, nil
  true
end
wait_for_exit(timeout) click to toggle source
# File lib/scmd/command.rb, line 133
def wait_for_exit(timeout)
  ios, _, _ = IO.select([@stop_r], nil, nil, timeout)
  @stop_r.read_nonblock(1) if ios&.include?(@stop_r)
end