class Abalone::Terminal

Public Class Methods

new(settings, ws, params) click to toggle source
# File lib/abalone/terminal.rb, line 6
def initialize(settings, ws, params)
  @settings = settings
  @ws       = ws
  @params   = params
  @modes    = nil
  @buffer   = Abalone::Buffer.new

  ENV['TERM'] ||= 'xterm-256color' # make sure we've got a somewhat sane environment

  if settings.respond_to?(:bannerfile)
    @ws.send({'data' => File.read(settings.bannerfile).encode(crlf_newline: true)}.to_json)
    @ws.send({'data' => "\r\n\r\n"}.to_json)
  end

  reader, @writer, @pid = PTY.spawn(*shell_command)
  @writer.winsize = [24,80]

  # there must be some form of event driven pty interaction, EM or some gem maybe?
  reader.sync = true
  @term = Thread.new do
    carry = []
    loop do
      begin
        PTY.check(@pid, true)
        output = reader.read_nonblock(512).unpack('C*') # we read non-blocking to stream data as quickly as we can
        last_low = output.rindex { |x| x < 128 } # find the last low bit
        trailing = last_low +1

        # use inclusive slices here
        data  = (carry + output[0..last_low]).pack('C*').force_encoding('UTF-8') # repack into a string up until the last low bit
        carry = output[trailing..-1]             # save the any remaining high bits and partial chars for next go-round

        @ws.send({'data' => data}.to_json)

      rescue IO::WaitReadable
        IO.select([reader])
        retry

      rescue PTY::ChildExited => e
        warn('Terminal has exited!')
        @ws.close_connection

        @timer.terminate rescue nil
        @timer.join rescue nil
        Thread.exit
      end

      sleep(0.05)
    end
  end

  if @settings.respond_to? :timeout
    @timer = Thread.new do
      expiration = Time.now + @settings.timeout
      loop do
        remaining = expiration - Time.now
        if remaining < 0
          terminate!
          Thread.exit
        end

        format = (remaining > 3600) ? "%H:%M:%S" : "%M:%S"
        time = {
          'event' => 'time',
          'data'  => Time.at(remaining).utc.strftime(format),
        }
        @ws.send(time.to_json)
        sleep 1
      end
    end
  end
end

Public Instance Methods

alive?() click to toggle source
# File lib/abalone/terminal.rb, line 79
def alive?
  @term.alive?
end
modes=(message) click to toggle source
# File lib/abalone/terminal.rb, line 109
def modes=(message)
  raise 'Invalid modes data type' unless message.is_a? Hash
  @modes = message.select do |key, val|
    ['cursorBlink', 'cursorVisible', 'bracketedPaste', 'applicationCursor'].include? key
  end
end
reconnect(ws) click to toggle source
# File lib/abalone/terminal.rb, line 83
def reconnect(ws)
  if @ttl # stop the countdown
    warn "Stopping timeout"
    @ttl.terminate rescue nil
    @ttl.join rescue nil
    @ttl = nil
  end
  @ws.close_connection if @ws
  @ws = ws

  sleep 0.25 # allow the terminal to finish initialization before we blast it.

  if @modes
    @ws.send({
      'event' => 'modes',
      'data'  => @modes
    }.to_json)
  end
  @ws.send({'data' => @buffer.replay}.to_json)
  @writer.write "\cl" # ctrl-l forces a screen redraw
end
resize(rows, cols) click to toggle source
# File lib/abalone/terminal.rb, line 141
def resize(rows, cols)
  @writer.winsize = [rows, cols]
end
stop!() click to toggle source
# File lib/abalone/terminal.rb, line 116
def stop!
  if @settings.respond_to? :ttl
    @ws  = @buffer
    @ttl = Thread.new do
      warn "Providing a shutdown grace period of #{@settings.ttl} seconds."
      sleep @settings.ttl
      terminate!
    end
  else
    terminate!
  end
end
terminate!() click to toggle source
# File lib/abalone/terminal.rb, line 129
def terminate!
  warn "Terminating session."
  Process.kill('TERM', @pid) rescue nil
  sleep 1
  Process.kill('KILL', @pid) rescue nil

  [@ttl, @timer, @term].each do |thread|
    thread.terminate rescue nil
    thread.join rescue nil
  end
end
write(message) click to toggle source
# File lib/abalone/terminal.rb, line 105
def write(message)
  @writer.write message
end

Private Instance Methods

shell_command() click to toggle source
# File lib/abalone/terminal.rb, line 146
def shell_command
  if @settings.respond_to? :command
    return @settings.command unless @settings.respond_to? :params

    command = @settings.command
    command = command.split if command.class == String

    @params.each do |param, value|
      if @settings.params.is_a? Array
        command << "--#{param}" << value
      else
        config = @settings.params[param]
        case config
        when nil
          command << "--#{param}" << value
        when Hash
          command << (config[:map] || "--#{param}")
          command << value
        end
      end
    end

    return command
  end

  if @settings.respond_to? :ssh
    config = @settings.ssh.dup
    config[:user] ||= @params['user'] # if not in the config file, it must come from the user

    if config[:user].nil?
      warn "SSH configuration must include the user"
      return ['echo', 'no username provided']
    end

    command = ['ssh', config[:host] ]
    command << '-l' << config[:user] if config.include? :user
    command << '-p' << config[:port] if config.include? :port
    command << '-i' << config[:cert] if config.include? :cert

    return command
  end

  # default just to running login
  'login'
end