class EventMachine::Ssh::Shell

EM::Ssh::Shell encapsulates interaction with a user shell on an SSH server. @example Retrieve the output of ifconfig -a on a server

EM.run{
  shell = EM::Ssh::Shell.new(host, user, password)
  shell.expect('~]$ ')
  interfaces = expect('~]$ ', '/sbin/ifconfig -a')

Shells can be easily and quickly duplicated (split) without the need to establish another connection. Shells provide :closed, :childless, and :split callbacks.

@example Start another shell using the same connection

shell.on(:childless) do
  info("#{shell}'s children all closed")
  shell.disconnect
  EM.stop
end

admin_shell = shell.split
admin_shell.on(:closed) { warn("admin shell has closed") }
admin_shell.expect(']$', 'sudo su -')

Constants

TIMEOUT

Global timeout for wait operations; can be overriden by :timeout option to new

Attributes

children[R]

@return [Array] all shells that have been split off from this one.

connect_opts[R]

@return [Hash] the options to pass to connect automatically. They will be extracted from the opptions on initialization

connection[R]

@return [EM::Ssh::Connection]

host[R]

@return [String] the host to login to

options[R]

@return [Hash] the options passed to initialize

parent[R]

@return [Shell] the parent of this shell

pass[R]

@return [String] the password to authenticate with - can be nil

session[R]

@return [EM::Ssh::Session]

shell[R]

@return [Net::SSH::Connection::Channel] The shell to which we can send_data

user[R]

@return [String] The user to authenticate as

Public Class Methods

new(address, user, pass, opts = {}) { |self| ... } click to toggle source

Connect to an ssh server then start a user shell. @param [String] address @param [String] user @param [String, nil] pass by default publickey and password auth will be attempted @param [Hash] opts @option opts [Hash] :net_ssh options to pass to Net::SSH; see Net::SSH.start @option opts [Fixnum] :timeout (TIMEOUT) default timeout for all wait_for and send_wait calls @option opts [Boolean] :reconnect when disconnected reconnect

# File lib/em-ssh/shell.rb, line 74
def initialize(address, user, pass, opts = {}, &blk)
  @timeout         = opts[:timeout].is_a?(Fixnum) ? opts[:timeout] : TIMEOUT
  @host            = address
  @user            = user
  @pass            = pass
  @options         = opts
  @logger          = opts[:logger] if opts[:logger]
  @connect_opts    = {
    password: pass,
    port: opts[:port] || 22,
    auth_methods: ['publickey', 'password'],
    logger: log
  }.merge(opts[:net_ssh] || {})
  @session         = opts[:session]
  @parent          = opts[:parent]
  @children        = []
  @reconnect       = opts[:reconnect]

  # TODO make all methods other than #callback and #errback inaccessible until connected? == true
  yield self if block_given?
  Fiber.new {
    begin
      open
      succeed(self) if connected? && !closed?
    rescue => e
      fail(e)
    end
  }.resume
end

Public Instance Methods

clear_buffer!() click to toggle source

Remove any data in the buffer.

# File lib/em-ssh/shell.rb, line 351
def clear_buffer!
  shell.clear_buffer!
end
close() click to toggle source

Close this shell and all children. Even when a shell is closed it is still connected to the server. Fires :closed event. @see Callbacks

# File lib/em-ssh/shell.rb, line 139
def close
  shell.close.tap{ debug("closing") } if shell && shell.active?
  @closed = true
  children.each { |c| c.close }
  fire(:closed)
end
closed?() click to toggle source

@return [Boolean] Has this shell been closed.

# File lib/em-ssh/shell.rb, line 147
def closed?
  @closed == true
end
connect() click to toggle source

Connect to the server. Does not open the shell; use open or split You generally won't need to call this on your own.

# File lib/em-ssh/shell.rb, line 264
def connect
  return @session if connected?
  trace = caller
  f = Fiber.current
  ::EM::Ssh.start(host, user, connect_opts) do |connection|
    @connection = connection
    connection.callback do |ssh|
      f.resume(@session = ssh) if f.alive?
    end
    connection.errback do |e|
      e.set_backtrace(trace + Array(e.backtrace))
      f.resume(e) if f.alive?
    end
  end
  return Fiber.yield.tap { |r| raise r if r.is_a?(Exception) }
end
connected?() click to toggle source

@return [Boolean] true if the session is still alive

# File lib/em-ssh/shell.rb, line 130
def connected?
  session && !session.closed?
end
debug(msg = nil, &blk) click to toggle source
Calls superclass method EventMachine::Ssh::Log#debug
# File lib/em-ssh/shell.rb, line 355
def debug(msg = nil, &blk)
  super("#{host} #{msg}", &blk)
end
disconnect(timeout = nil) click to toggle source

Close the connection to the server and all child shells. Disconnected shells cannot be split.

# File lib/em-ssh/shell.rb, line 111
def disconnect(timeout = nil)
  if timeout
    EM::Timer.new(timeout) { disconnect! }
  end
  close
  @session && @session.close
  @shell = nil
  disconnect!
end
disconnect!() click to toggle source
# File lib/em-ssh/shell.rb, line 121
def disconnect!
  @session = nil
  if @connection
    @connection.close_connection
    @connection = nil
  end
end
error(msg = nil, &blk) click to toggle source
Calls superclass method EventMachine::Ssh::Log#error
# File lib/em-ssh/shell.rb, line 371
def error(msg = nil, &blk)
  super("#{host} #{msg}", &blk)
end
expect(strregex, send_str = nil, opts = {}, &blk) click to toggle source

Wait for a number of seconds until a specified string or regexp is matched by the data returned from the ssh connection. Optionally send a given string first.

If a block is not provided the current Fiber will yield until strregex matches or :timeout # is reached.

If a block is provided expect will return immediately.

@param [String, Regexp] strregex to match against @param [String] send_str the data to send before waiting @param [Hash] opts @option opts [Fixnum] :timeout (@timeout) number of seconds to wait when there is no activity @return [Shell, String] all data received up to an including strregex if a block is not provided.

the Shell if a block is provided

@example expect a prompt

expect(' ~]$ ')

@example send a command and wait for a prompt

expect(' ~]$ ', '/sbin/ifconfig')

@example expect a prompt and within 5 seconds

expect(' ~]$ ', :timeout => 5)

@example send a command and wait up to 10 seconds for a prompt

expect(' ~]$ ', '/etc/sysconfig/openvpn restart', :timeout => 10)
# File lib/em-ssh/shell.rb, line 310
def expect(strregex, send_str = nil, opts = {}, &blk)
  assert_channel!
  shell.expect(strregex,
               send_str,
               {:timeout => @timeout, :log => self }.merge(opts),
              &blk)
end
fatal(msg = nil, &blk) click to toggle source
Calls superclass method EventMachine::Ssh::Log#fatal
# File lib/em-ssh/shell.rb, line 363
def fatal(msg = nil, &blk)
  super("#{host} #{msg}", &blk)
end
info(msg = nil, &blk) click to toggle source
Calls superclass method EventMachine::Ssh::Log#info
# File lib/em-ssh/shell.rb, line 359
def info(msg = nil, &blk)
  super("#{host} #{msg}", &blk)
end
line_terminator() click to toggle source

@return [String] a string (rn) to append to every command

# File lib/em-ssh/shell.rb, line 56
def line_terminator
  shell ? shell.line_terminator : "\n"
end
line_terminator=(lt) click to toggle source

@param lt a string (rn) to append to every command

# File lib/em-ssh/shell.rb, line 61
def line_terminator=(lt)
  @line_terminator = lt
  shell.line_terminator = lt if shell
end
open() { |self| ... } click to toggle source

Open a shell on the server. You generally don't need to call this. @return [self, Exception]

# File lib/em-ssh/shell.rb, line 160
def open(&blk)
  f       = Fiber.current
  trace   = caller
  on_open do |s|
    Fiber.new { yield(self) if block_given? }.resume
    f.resume(self)
  end
  @on_open_err = proc { |e| f.resume(e) }
  open!
  return Fiber.yield.tap { |r| raise r if r.is_a?(Exception) }
end
open?() click to toggle source

@return [Boolean] Is the shell open?

# File lib/em-ssh/shell.rb, line 153
def open?
  !closed? && @shell
end
reconnect?() click to toggle source

@return [Boolean] true if the connection should be automatically re-established; default: false

# File lib/em-ssh/shell.rb, line 105
def reconnect?
  @reconnect == true
end
send_and_wait(send_str, wait_str = nil, opts = {}) click to toggle source

Send a string to the server and wait for a response containing a specified String or Regex. @param [String] send_str @return [String] all data in the buffer including the wait_str if it was found

# File lib/em-ssh/shell.rb, line 321
def send_and_wait(send_str, wait_str = nil, opts = {})
  assert_channel!
  shell.send_and_wait(send_str,
                      wait_str,
                      {:timeout => @timeout, :log => self }.merge(opts))
end
send_data(d, send_newline=true) click to toggle source

Send data to the ssh server shell. You generally don't need to call this. @see send_and_wait @param [String] d the data to send encoded as a string

# File lib/em-ssh/shell.rb, line 332
def send_data(d, send_newline=true)
  assert_channel!
  shell.send_data(d, send_newline)
end
split() { |child| ... } click to toggle source

Create a new shell using the same ssh connection. A connection will be established if this shell is not connected.

If a block is provided the call to split must be inside of a Fiber. The child will be closed after yielding. The block will not be yielded until the remote PTY has been opened.

@yield [Shell] child @return [Shell] child

# File lib/em-ssh/shell.rb, line 242
def split
  connect unless connected?
  child = self.class.new(host, user, pass, {:session => session, :parent => self}.merge(options))
  child.line_terminator = line_terminator
  children.push(child)
  child.on(:closed) do
    children.delete(child)
    fire(:childless).tap{ info("fired :childless") } if children.empty?
  end
  fire(:split, child)
  if block_given?
    # requires that the caller be in a Fiber
    child.open
    yield(child).tap { child.close }
  else
    child
  end
end
wait_for(strregex, opts = { }) click to toggle source

Wait for the shell to send data containing the given string. @param [String, Regexp] strregex a string or regex to match the console output against. @param [Hash] opts @option opts [Fixnum] :timeout (Session::TIMEOUT) the maximum number of seconds to wait @return [String] the contents of the buffer or a TimeoutError @raise Disconnected @raise ClosedChannel @raise TimeoutError

# File lib/em-ssh/shell.rb, line 345
def wait_for(strregex, opts = { })
  assert_channel!
  shell.wait_for(strregex, {:timeout => @timeout, :log => self }.merge(opts))
end
warn(msg = nil, &blk) click to toggle source
Calls superclass method EventMachine::Ssh::Log#warn
# File lib/em-ssh/shell.rb, line 367
def warn(msg = nil, &blk)
  super("#{host} #{msg}", &blk)
end

Private Instance Methods

assert_channel!() click to toggle source

Ensure the channel is open of fail.

# File lib/em-ssh/shell.rb, line 282
def assert_channel!
  reconnect? ? open : raise(Disconnected) unless connected? && @shell
  raise ClosedChannel if closed?
end
on_open(&cb) click to toggle source
# File lib/em-ssh/shell.rb, line 210
def on_open(&cb)
  if open?
    EM.next_tick { cb.call(@open_status) }
  else
    open_callbacks << cb
  end
end
open!() click to toggle source
# File lib/em-ssh/shell.rb, line 174
def open!
  return if @opening
  @opening = true
  begin
    connect
    chan = session.open_channel do |channel|
      debug "**** channel open: #{channel}"
      channel.request_pty(options[:pty] || {}) do |pty,suc|
        debug "***** pty open: #{pty}; suc: #{suc}"
        pty.send_channel_request("shell") do |shell,success|
          if !success
            set_open_status(ConnectionError.new("Failed to create shell").tap{|e| e.set_backtrace(caller) })
          else
            debug "***** shell open: #{shell}"
            @closed = false
            @shell  = shell
            shell.extend(EventMachine::Ssh::Connection::Channel::Interactive)
            # Share callbacks with shell
            shell.callbacks       = callbacks
            shell.line_terminator = @line_terminator if @line_terminator
            shell.on(:data) { |data| debug("#{shell.dump_buffer}") }
            set_open_status(self)
          end
          @opening     = false
        end # |shell,success|
      end # |pty,suc|
    end # |channel|
    chan.on_open_failed do |chan, code, desc|
      @on_open_err && @on_open_err[ChannelOpenFailed.from_code(code, desc)]
    end
  rescue => e
    @opening = false
    raise ConnectionError.new("failed to create shell for #{host}: #{e} (#{e.class})")
  end
end
open_callbacks() click to toggle source
# File lib/em-ssh/shell.rb, line 218
def open_callbacks
  @open_callbacks ||= []
end
set_open_status(status) click to toggle source
# File lib/em-ssh/shell.rb, line 222
def set_open_status(status)
  @open_status = status
  open_callbacks.clone.each do |cb|
    open_callbacks.delete(cb)
    cb.call(status)
  end
  @open_status
end