class Kommando

Constants

VERSION

Public Class Methods

new(cmd, opts={}) click to toggle source
# File lib/kommando.rb, line 56
def initialize(cmd, opts={})
  Thread.abort_on_exception=true

  @cmd = cmd
  @shell = @cmd.start_with? "$"
  @stdout = Kommando::Stdout.new @shell
  @stdin = Kommando::Stdin.new

  @output_stdout = opts[:output] == true
  @output_file = if opts[:output].class == String
    opts[:output]
  end

  @timeout = if opts[:timeout].class == Float
    opts[:timeout]
  elsif opts[:timeout].class.to_s == "Integer" || opts[:timeout].class.to_s == "Fixnum"
    opts[:timeout].to_f
  else
    @timeout = @@timeout
  end

  @timeout_happened = false
  @kill_happened = false
  @rescue_happened = false

  @env = opts[:env] || {}

  @code = nil
  @executed = false
  @process_completed = false

  if opts[:retry]
    if opts[:retry][:times]
      @retry_times_total = opts[:retry][:times]
      @retry_time = @retry_times_total
    end
    if opts[:retry][:sleep]
      @retry_sleep = opts[:retry][:sleep]
    end
  end

  @start_fired = false

  @thread = nil
  @pid = nil

  @whens = {}
  @when = When.new(self)

  if @@whens
    @@whens.instance_variable_get("@whens").each_pair do |event_name, blocks|
      blocks.each do |block|
        @when.register event_name, block
      end
    end
  end
end
puts(cmd, opts={}) click to toggle source
# File lib/kommando.rb, line 31
def puts(cmd, opts={})
  k = Kommando.new cmd, opts
  k.run
  Kernel.puts k.out
  k
end
run(cmd, opts={}) click to toggle source
# File lib/kommando.rb, line 19
def run(cmd, opts={})
  k = Kommando.new cmd, opts
  k.run
  k
end
run_async(cmd, opts={}) click to toggle source
# File lib/kommando.rb, line 25
def run_async(cmd, opts={})
  k = Kommando.new cmd, opts
  k.run_async
  k
end
timeout() click to toggle source
# File lib/kommando.rb, line 38
def timeout
  @@timeout
end
timeout=(value) click to toggle source
# File lib/kommando.rb, line 42
def timeout=(value)
  @@timeout=value
end
when(event_name, &block) click to toggle source
# File lib/kommando.rb, line 46
def when(event_name, &block)
  @@whens ||= Kommando::When.new
  @@whens.register event_name, block
end
when=(w) click to toggle source
# File lib/kommando.rb, line 51
def when=(w)
  @@whens = w
end

Public Instance Methods

code() click to toggle source
# File lib/kommando.rb, line 308
def code
  @code
end
in() click to toggle source
# File lib/kommando.rb, line 312
def in
  @stdin
end
kill() click to toggle source
# File lib/kommando.rb, line 120
def kill
  begin
    Process.kill('KILL', @pid)
  rescue Errno::ESRCH => ex
    #raise ex # see if happens
  end

  @kill_happened = true
  begin
    Timeout.timeout(1) do
      sleep 0.001 until @code # let finalize
    end
  rescue Timeout::Error => ex
    raise ex # let's see if happens
  end
end
out() click to toggle source
# File lib/kommando.rb, line 304
def out
  @stdout.to_s
end
run() click to toggle source
# File lib/kommando.rb, line 137
def run
  return false if @executed
  @executed = true

  command, *args = if @shell
    trash, line = @cmd.split "$", 2
    line.lstrip!

    if File.exist? "/bin/bash"
      ["bash", "-c", line]
    else
      ["sh", "-c", line]
    end
  else
    @cmd.split " "
  end

  @env.each_pair do |k,v|
    ENV[k.to_s] = v
  end

  interpolated_args = []
  if @shell
    interpolated_args << args.shift
    shell_line = args[0]

    to_be_interpolated = shell_line.scan(/\$[^\s]*/)
    to_be_interpolated.each do |to_interpolate|
      if ENV[to_interpolate]
        shell_line.gsub!("${to_interpolate}", ENV[to_interpolate])
      else
        shell_line.gsub!("${to_interpolate}", "")
      end
    end

    interpolated_args << shell_line
  else
    args.each do |arg|
      interpolated_args << if arg.start_with? "$"
        env_name = arg.split("$")[1]
        ENV[env_name]
      else
        arg
      end
    end
  end

  begin
    debug "pty before spawn"
    make_pty_testable.spawn(command, *interpolated_args) do |stdout, stdin, pid|
      debug "pty in spawn"

      @pid = pid

      if @output_file
        stdout_file = File.open @output_file, 'w'
        stdout_file.sync = true
      end

      thread_stdin = nil
      self.when :start do
        thread_stdin = Thread.new do
          while true do
            break if @process_completed
            # c = nil
            # Timeout.timeout(1) do
            c = @stdin.getc
            #end

            unless c
              sleep 0.01
              next
            end

            stdin.write c
          end
        end
      end


      debug "thread_stdin started"

      unless @start_fired
        debug "when :start firing"
        @when.fire :start
        debug "when :start fired"
      else
        debug "when :start NOT fired, as :start has already been fired"
      end

      if @timeout
        begin
          Timeout.timeout(@timeout) do
            process_stdout(pid, stdout, stdout_file)
          end
        rescue Timeout::Error
          Process.kill('KILL', pid)
          @timeout_happened = true
        end
      else
        process_stdout(pid, stdout, stdout_file)
      end
      @process_completed = true
      debug "thread_stdin joining"
      thread_stdin.join
      debug "thread_stdin joined"
      stdout_file.close if @output_file
    end

  rescue RuntimeError => ex
    if ex.message == "can't get Master/Slave device"
      #suppress, weird stuff.
      @rescue_happened = true
    else
      raise ex
    end
  rescue ThreadError => ex
    if ex.message == "can't create Thread: Resource temporarily unavailable"
      if @retry_time && @retry_time > 0
        @executed = false
        @retry_time -= 1
        sleep @retry_sleep if @retry_sleep
        @when.fire :retry
        return run
      end

      raise_after_callbacks(ex)
    else
      raise_after_callbacks(ex)
    end
  rescue Errno::ENOENT => ex
    @when.fire :error
    raise Kommando::Error, "Command '#{command}' not found"
  ensure
    @code = if @timeout_happened
      1
    elsif @kill_happened
      137
    else
      begin
        Process.wait @pid if @pid
      rescue Errno::ECHILD => ex
        # safe to supress, I guess
      end

      if $?
        $?.exitstatus
      else
        137   # sometimes with ruby2.1 ? (maybefix now?)
      end
    end

    @when.fire :error if @rescue_happened
  end

  @when.fire :timeout if @timeout_happened
  @when.fire :exit
  if @code == 0
    @when.fire :success
  else
    @when.fire :failed
  end

  debug "run returning true"
  true
end
run_async() click to toggle source
# File lib/kommando.rb, line 114
def run_async
  @thread = Thread.new do
    run
  end
end
wait() click to toggle source
# File lib/kommando.rb, line 316
def wait
  debug "k.wait starting"
  exited = false
  self.when :exit do
    exited = true
  end
  sleep 0.0001 until exited
  debug "k.wait done"
end
when(event, &block) click to toggle source
# File lib/kommando.rb, line 326
def when(event, &block)
  @when.register event, block
  self
end

Private Instance Methods

debug(msg) click to toggle source
# File lib/kommando.rb, line 333
def debug(msg)
  return unless ENV["DEBUG"]
  print "|#{msg}"
end
make_pty_testable() click to toggle source
# File lib/kommando.rb, line 345
def make_pty_testable
  PTY
end
process_stdout(pid, stdout, stdout_file) click to toggle source
# File lib/kommando.rb, line 349
def process_stdout(pid, stdout, stdout_file)
  flushing = false
  while true do
    debug "process_stdout started"
    begin
      Process.getpgid(pid)
    rescue Errno::ESRCH => ex
      flushing = true
    end

    c = nil
    begin
      c = stdout.getc
    rescue Errno::EIO
      # Linux http://stackoverflow.com/a/7263243
      # TODO: only try-catch on linux?
      break
    end

    break if flushing == true && c == nil
    next unless c

    @stdout << c
    print c if @output_stdout
    stdout_file.write c if @output_file
  end
end
raise_after_callbacks(exception) click to toggle source
# File lib/kommando.rb, line 338
def raise_after_callbacks(exception)
  @when.fire :error
  @when.fire :exit
  @when.fire :failed
  raise exception
end