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
@return [Array] all shells that have been split off from this one.
@return [Hash] the options to pass to connect automatically. They will be extracted from the opptions on initialization
@return [EM::Ssh::Connection]
@return [String] the host to login to
@return [Hash] the options passed to initialize
@return [Shell] the parent of this shell
@return [String] the password to authenticate with - can be nil
@return [EM::Ssh::Session]
@return [Net::SSH::Connection::Channel] The shell to which we can send_data
@return [String] The user to authenticate as
Public Class Methods
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
Remove any data in the buffer.
# File lib/em-ssh/shell.rb, line 351 def clear_buffer! shell.clear_buffer! end
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
@return [Boolean] Has this shell been closed.
# File lib/em-ssh/shell.rb, line 147 def closed? @closed == true end
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
@return [Boolean] true if the session is still alive
# File lib/em-ssh/shell.rb, line 130 def connected? session && !session.closed? end
EventMachine::Ssh::Log#debug
# File lib/em-ssh/shell.rb, line 355 def debug(msg = nil, &blk) super("#{host} #{msg}", &blk) end
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
# File lib/em-ssh/shell.rb, line 121 def disconnect! @session = nil if @connection @connection.close_connection @connection = nil end end
EventMachine::Ssh::Log#error
# File lib/em-ssh/shell.rb, line 371 def error(msg = nil, &blk) super("#{host} #{msg}", &blk) end
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
EventMachine::Ssh::Log#fatal
# File lib/em-ssh/shell.rb, line 363 def fatal(msg = nil, &blk) super("#{host} #{msg}", &blk) end
EventMachine::Ssh::Log#info
# File lib/em-ssh/shell.rb, line 359 def info(msg = nil, &blk) super("#{host} #{msg}", &blk) end
@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
@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 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
@return [Boolean] Is the shell open?
# File lib/em-ssh/shell.rb, line 153 def open? !closed? && @shell end
@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 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 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
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 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
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
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
# 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
# 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
# File lib/em-ssh/shell.rb, line 218 def open_callbacks @open_callbacks ||= [] end
# 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