Object
A session class representing the connection service running on top of the SSH transport layer. It manages the creation of channels (see #), and the dispatching of messages to the various channels. It also encapsulates the SSH event loop (via # and #), and serves as a central point-of-reference for all SSH-related services (e.g. port forwarding, SFTP, SCP, etc.).
You will rarely (if ever) need to instantiate this class directly; rather, you’ll almost always use Net::SSH.start to initialize a new network connection, authenticate a user, and return a new connection session, all in one call.
Net::SSH.start("localhost", "user") do |ssh| # 'ssh' is an instance of Net::SSH::Connection::Session ssh.exec! "/etc/init.d/some_process start" end
Create a new connection service instance atop the given transport layer. Initializes the listeners to be only the underlying socket object.
# File lib/net/ssh/connection/session.rb, line 62 62: def initialize(transport, options={}) 63: self.logger = transport.logger 64: 65: @transport = transport 66: @options = options 67: 68: @channel_id_counter = 1 69: @channels = Hash.new(NilChannel.new(self)) 70: @listeners = { transport.socket => nil } 71: @pending_requests = [] 72: @channel_open_handlers = {} 73: @on_global_request = {} 74: @properties = (options[:properties] || {}).dup 75: end
Retrieves a custom property from this instance. This can be used to store additional state in applications that must manage multiple SSH connections.
# File lib/net/ssh/connection/session.rb, line 80 80: def [](key) 81: @properties[key] 82: end
Sets a custom property for this instance.
# File lib/net/ssh/connection/session.rb, line 85 85: def []=(key, value) 86: @properties[key] = value 87: end
Returns true if there are any channels currently active on this session. By default, this will not include “invisible” channels (such as those created by forwarding ports and such), but if you pass a true value for include_invisible, then those will be counted.
This can be useful for determining whether the event loop should continue to be run.
ssh.loop { ssh.busy? }
# File lib/net/ssh/connection/session.rb, line 134 134: def busy?(include_invisible=false) 135: if include_invisible 136: channels.any? 137: else 138: channels.any? { |id, ch| !ch[:invisible] } 139: end 140: end
Closes the session gracefully, blocking until all channels have successfully closed, and then closes the underlying transport layer connection.
# File lib/net/ssh/connection/session.rb, line 107 107: def close 108: info { "closing remaining channels (#{channels.length} open)" } 109: channels.each { |id, channel| channel.close } 110: loop { channels.any? } 111: transport.close 112: end
Returns true if the underlying transport has been closed. Note that this can be a little misleading, since if the remote server has closed the connection, the local end will still think it is open until the next operation on the socket. Nevertheless, this method can be useful if you just want to know if you have closed the connection.
# File lib/net/ssh/connection/session.rb, line 100 100: def closed? 101: transport.closed? 102: end
A convenience method for executing a command and interacting with it. If no block is given, all output is printed via $stdout and $stderr. Otherwise, the block is called for each data and extended data packet, with three arguments: the channel object, a symbol indicating the data type (:stdout or :stderr), and the data (as a string).
Note that this method returns immediately, and requires an event loop (see Session#loop) in order for the command to actually execute.
This is effectively identical to calling #, and then Net::SSH::Connection::Channel#exec, and then setting up the channel callbacks. However, for most uses, this will be sufficient.
ssh.exec "grep something /some/files" do |ch, stream, data| if stream == :stderr puts "ERROR: #{data}" else puts data end end
# File lib/net/ssh/connection/session.rb, line 319 319: def exec(command, &block) 320: open_channel do |channel| 321: channel.exec(command) do |ch, success| 322: raise "could not execute command: #{command.inspect}" unless success 323: 324: channel.on_data do |ch2, data| 325: if block 326: block.call(ch2, :stdout, data) 327: else 328: $stdout.print(data) 329: end 330: end 331: 332: channel.on_extended_data do |ch2, type, data| 333: if block 334: block.call(ch2, :stderr, data) 335: else 336: $stderr.print(data) 337: end 338: end 339: end 340: end 341: end
Same as #, except this will block until the command finishes. Also, if a block is not given, this will return all output (stdout and stderr) as a single string.
matches = ssh.exec!("grep something /some/files")
# File lib/net/ssh/connection/session.rb, line 348 348: def exec!(command, &block) 349: block ||= Proc.new do |ch, type, data| 350: ch[:result] ||= "" 351: ch[:result] << data 352: end 353: 354: channel = exec(command, &block) 355: channel.wait 356: 357: return channel[:result] 358: end
Returns a reference to the Net::SSH::Service::Forward service, which can be used for forwarding ports over SSH.
# File lib/net/ssh/connection/session.rb, line 417 417: def forward 418: @forward ||= Service::Forward.new(self) 419: end
Returns the name of the host that was given to the transport layer to connect to.
# File lib/net/ssh/connection/session.rb, line 91 91: def host 92: transport.host 93: end
Adds an IO object for the event loop to listen to. If a callback is given, it will be invoked when the io is ready to be read, otherwise, the io will merely have its # method invoked.
Any io value passed to this method must have mixed into it the Net::SSH::BufferedIo functionality, typically by calling # on the object.
The following example executes a process on the remote server, opens a socket to somewhere, and then pipes data from that socket to the remote process’ stdin stream:
channel = ssh.open_channel do |ch| ch.exec "/some/process/that/wants/input" do |ch, success| abort "can't execute!" unless success io = TCPSocket.new(somewhere, port) io.extend(Net::SSH::BufferedIo) ssh.listen_to(io) ch.on_process do if io.available > 0 ch.send_data(io.read_available) end end ch.on_close do ssh.stop_listening_to(io) io.close end end end channel.wait
# File lib/net/ssh/connection/session.rb, line 405 405: def listen_to(io, &callback) 406: listeners[io] = callback 407: end
The main event loop. Calls # until # returns false. If a block is given, it is passed to #, otherwise a default proc is used that just returns true if there are any channels active (see #). The # wait parameter is also passed through to # (where it is interpreted as the maximum number of seconds to wait for IO.select to return).
# loop for as long as there are any channels active ssh.loop # loop for as long as there are any channels active, but make sure # the event loop runs at least once per 0.1 second ssh.loop(0.1) # loop until ctrl-C is pressed int_pressed = false trap("INT") { int_pressed = true } ssh.loop(0.1) { not int_pressed }
# File lib/net/ssh/connection/session.rb, line 159 159: def loop(wait=nil, &block) 160: running = block || Proc.new { busy? } 161: loop_forever { break unless process(wait, &running) } 162: end
preserve a reference to Kernel#loop
Registers a handler to be invoked when the server sends a global request of the given type. The callback receives the request data as the first parameter, and true/false as the second (indicating whether a response is required). If the callback sends the response, it should return :sent. Otherwise, if it returns true, REQUEST_SUCCESS will be sent, and if it returns false, REQUEST_FAILURE will be sent.
# File lib/net/ssh/connection/session.rb, line 441 441: def on_global_request(type, &block) 442: old, @on_global_request[type] = @on_global_request[type], block 443: old 444: end
Registers a handler to be invoked when the server wants to open a channel on the client. The callback receives the connection object, the new channel object, and the packet itself as arguments, and should raise ChannelOpenFailed if it is unable to open the channel for some reason. Otherwise, the channel will be opened and a confirmation message sent to the server.
This is used by the Net::SSH::Service::Forward service to open a channel when a remote forwarded port receives a connection. However, you are welcome to register handlers for other channel types, as needed.
# File lib/net/ssh/connection/session.rb, line 431 431: def on_open_channel(type, &block) 432: channel_open_handlers[type] = block 433: end
Requests that a new channel be opened. By default, the channel will be of type “session”, but if you know what you’re doing you can select any of the channel types supported by the SSH protocol. The extra parameters must be even in number and conform to the same format as described for Net::SSH::Buffer.from. If a callback is given, it will be invoked when the server confirms that the channel opened successfully. The sole parameter for the callback is the channel object itself.
In general, you’ll use # without any arguments; the only time you’d want to set the channel type or pass additional initialization data is if you were implementing an SSH extension.
channel = ssh.open_channel do |ch| ch.exec "grep something /some/files" do |ch, success| ... end end channel.wait
# File lib/net/ssh/connection/session.rb, line 287 287: def open_channel(type="session", *extra, &on_confirm) 288: local_id = get_next_channel_id 289: channel = Channel.new(self, type, local_id, &on_confirm) 290: 291: msg = Buffer.from(:byte, CHANNEL_OPEN, :string, type, :long, local_id, 292: :long, channel.local_maximum_window_size, 293: :long, channel.local_maximum_packet_size, *extra) 294: send_message(msg) 295: 296: channels[local_id] = channel 297: end
This is called internally as part of #. It loops over the given arrays of reader IO’s and writer IO’s, processing them as needed, and then calls Net::SSH::Transport::Session#rekey_as_needed to allow the transport layer to rekey. Then returns true.
# File lib/net/ssh/connection/session.rb, line 223 223: def postprocess(readers, writers) 224: Array(readers).each do |reader| 225: if listeners[reader] 226: listeners[reader].call(reader) 227: else 228: if reader.fill.zero? 229: reader.close 230: stop_listening_to(reader) 231: end 232: end 233: end 234: 235: Array(writers).each do |writer| 236: writer.send_pending 237: end 238: 239: transport.rekey_as_needed 240: 241: return true 242: end
This is called internally as part of #. It dispatches any available incoming packets, and then runs Net::SSH::Connection::Channel#process for any active channels. If a block is given, it is invoked at the start of the method and again at the end, and if the block ever returns false, this method returns false. Otherwise, it returns true.
# File lib/net/ssh/connection/session.rb, line 211 211: def preprocess 212: return false if block_given? && !yield(self) 213: dispatch_incoming_packets 214: channels.each { |id, channel| channel.process unless channel.closing? } 215: return false if block_given? && !yield(self) 216: return true 217: end
The core of the event loop. It processes a single iteration of the event loop. If a block is given, it should return false when the processing should abort, which causes # to return false. Otherwise, # returns true. The session itself is yielded to the block as its only argument.
If wait is nil (the default), this method will block until any of the monitored IO objects are ready to be read from or written to. If you want it to not block, you can pass 0, or you can pass any other numeric value to indicate that it should block for no more than that many seconds. Passing 0 is a good way to poll the connection, but if you do it too frequently it can make your CPU quite busy!
This will also cause all active channels to be processed once each (see Net::SSH::Connection::Channel#on_process).
# process multiple Net::SSH connections in parallel connections = [ Net::SSH.start("host1", ...), Net::SSH.start("host2", ...) ] connections.each do |ssh| ssh.exec "grep something /in/some/files" end condition = Proc.new { |s| s.busy? } loop do connections.delete_if { |ssh| !ssh.process(0.1, &condition) } break if connections.empty? end
# File lib/net/ssh/connection/session.rb, line 196 196: def process(wait=nil, &block) 197: return false unless preprocess(&block) 198: 199: r = listeners.keys 200: w = r.select { |w2| w2.respond_to?(:pending_write?) && w2.pending_write? } 201: readers, writers, = Net::SSH::Compat.io_select(r, w, nil, wait) 202: 203: postprocess(readers, writers) 204: end
Send a global request of the given type. The extra parameters must be even in number, and conform to the same format as described for Net::SSH::Buffer.from. If a callback is not specified, the request will not require a response from the server, otherwise the server is required to respond and indicate whether the request was successful or not. This success or failure is indicated by the callback being invoked, with the first parameter being true or false (success, or failure), and the second being the packet itself.
Generally, Net::SSH will manage global requests that need to be sent (e.g. port forward requests and such are handled in the Net::SSH::Service::Forward class, for instance). However, there may be times when you need to send a global request that isn’t explicitly handled by Net::SSH, and so this method is available to you.
ssh.send_global_request("keep-alive@openssh.com")
# File lib/net/ssh/connection/session.rb, line 260 260: def send_global_request(type, *extra, &callback) 261: info { "sending global request #{type}" } 262: msg = Buffer.from(:byte, GLOBAL_REQUEST, :string, type.to_s, :bool, !callback.nil?, *extra) 263: send_message(msg) 264: pending_requests << callback if callback 265: self 266: end
Enqueues a message to be sent to the server as soon as the socket is available for writing. Most programs will never need to call this, but if you are implementing an extension to the SSH protocol, or if you need to send a packet that Net::SSH does not directly support, you can use this to send it.
ssh.send_message(Buffer.from(:byte, REQUEST_SUCCESS).to_s)
# File lib/net/ssh/connection/session.rb, line 367 367: def send_message(message) 368: transport.enqueue_message(message) 369: end
Performs a “hard” shutdown of the connection. In general, this should never be done, but it might be necessary (in a rescue clause, for instance, when the connection needs to close but you don’t know the status of the underlying protocol’s state).
# File lib/net/ssh/connection/session.rb, line 118 118: def shutdown! 119: transport.shutdown! 120: end
# File lib/net/ssh/connection/session.rb, line 569 569: def channel_close(packet) 570: info { "channel_close: #{packet[:local_id]}" } 571: 572: channel = channels[packet[:local_id]] 573: channel.close 574: 575: channels.delete(packet[:local_id]) 576: channel.do_close 577: end
# File lib/net/ssh/connection/session.rb, line 554 554: def channel_data(packet) 555: info { "channel_data: #{packet[:local_id]} #{packet[:data].length}b" } 556: channels[packet[:local_id]].do_data(packet[:data]) 557: end
# File lib/net/ssh/connection/session.rb, line 564 564: def channel_eof(packet) 565: info { "channel_eof: #{packet[:local_id]}" } 566: channels[packet[:local_id]].do_eof 567: end
# File lib/net/ssh/connection/session.rb, line 559 559: def channel_extended_data(packet) 560: info { "channel_extended_data: #{packet[:local_id]} #{packet[:data_type]} #{packet[:data].length}b" } 561: channels[packet[:local_id]].do_extended_data(packet[:data_type], packet[:data]) 562: end
# File lib/net/ssh/connection/session.rb, line 584 584: def channel_failure(packet) 585: info { "channel_failure: #{packet[:local_id]}" } 586: channels[packet[:local_id]].do_failure 587: end
Called when the server wants to open a channel. If no registered channel handler exists for the given channel type, CHANNEL_OPEN_FAILURE is returned, otherwise the callback is invoked and everything proceeds accordingly.
# File lib/net/ssh/connection/session.rb, line 502 502: def channel_open(packet) 503: info { "channel open #{packet[:channel_type]}" } 504: 505: local_id = get_next_channel_id 506: channel = Channel.new(self, packet[:channel_type], local_id) 507: channel.do_open_confirmation(packet[:remote_id], packet[:window_size], packet[:packet_size]) 508: 509: callback = channel_open_handlers[packet[:channel_type]] 510: 511: if callback 512: begin 513: callback[self, channel, packet] 514: rescue ChannelOpenFailed => err 515: failure = [err.code, err.reason] 516: else 517: channels[local_id] = channel 518: msg = Buffer.from(:byte, CHANNEL_OPEN_CONFIRMATION, :long, channel.remote_id, :long, channel.local_id, :long, channel.local_maximum_window_size, :long, channel.local_maximum_packet_size) 519: end 520: else 521: failure = [3, "unknown channel type #{channel.type}"] 522: end 523: 524: if failure 525: error { failure.inspect } 526: msg = Buffer.from(:byte, CHANNEL_OPEN_FAILURE, :long, channel.remote_id, :long, failure[0], :string, failure[1], :string, "") 527: end 528: 529: send_message(msg) 530: end
# File lib/net/ssh/connection/session.rb, line 532 532: def channel_open_confirmation(packet) 533: info { "channel_open_confirmation: #{packet[:local_id]} #{packet[:remote_id]} #{packet[:window_size]} #{packet[:packet_size]}" } 534: channel = channels[packet[:local_id]] 535: channel.do_open_confirmation(packet[:remote_id], packet[:window_size], packet[:packet_size]) 536: end
# File lib/net/ssh/connection/session.rb, line 538 538: def channel_open_failure(packet) 539: error { "channel_open_failed: #{packet[:local_id]} #{packet[:reason_code]} #{packet[:description]}" } 540: channel = channels.delete(packet[:local_id]) 541: channel.do_open_failed(packet[:reason_code], packet[:description]) 542: end
# File lib/net/ssh/connection/session.rb, line 549 549: def channel_request(packet) 550: info { "channel_request: #{packet[:local_id]} #{packet[:request]} #{packet[:want_reply]}" } 551: channels[packet[:local_id]].do_request(packet[:request], packet[:want_reply], packet[:request_data]) 552: end
# File lib/net/ssh/connection/session.rb, line 579 579: def channel_success(packet) 580: info { "channel_success: #{packet[:local_id]}" } 581: channels[packet[:local_id]].do_success 582: end
# File lib/net/ssh/connection/session.rb, line 544 544: def channel_window_adjust(packet) 545: info { "channel_window_adjust: #{packet[:local_id]} +#{packet[:extra_bytes]}" } 546: channels[packet[:local_id]].do_window_adjust(packet[:extra_bytes]) 547: end
Read all pending packets from the connection and dispatch them as appropriate. Returns as soon as there are no more pending packets.
# File lib/net/ssh/connection/session.rb, line 450 450: def dispatch_incoming_packets 451: while packet = transport.poll_message 452: unless MAP.key?(packet.type) 453: raise Net::SSH::Exception, "unexpected response #{packet.type} (#{packet.inspect})" 454: end 455: 456: send(MAP[packet.type], packet) 457: end 458: end
Returns the next available channel id to be assigned, and increments the counter.
# File lib/net/ssh/connection/session.rb, line 462 462: def get_next_channel_id 463: @channel_id_counter += 1 464: end
Invoked when a global request is received. The registered global request callback will be invoked, if one exists, and the necessary reply returned.
# File lib/net/ssh/connection/session.rb, line 469 469: def global_request(packet) 470: info { "global request received: #{packet[:request_type]} #{packet[:want_reply]}" } 471: callback = @on_global_request[packet[:request_type]] 472: result = callback ? callback.call(packet[:request_data], packet[:want_reply]) : false 473: 474: if result != :sent && result != true && result != false 475: raise "expected global request handler for `#{packet[:request_type]}' to return true, false, or :sent, but got #{result.inspect}" 476: end 477: 478: if packet[:want_reply] && result != :sent 479: msg = Buffer.from(:byte, result ? REQUEST_SUCCESS : REQUEST_FAILURE) 480: send_message(msg) 481: end 482: end
Invokes the next pending request callback with false.
# File lib/net/ssh/connection/session.rb, line 492 492: def request_failure(packet) 493: info { "global request failure" } 494: callback = pending_requests.shift 495: callback.call(false, packet) if callback 496: end
Invokes the next pending request callback with true.
# File lib/net/ssh/connection/session.rb, line 485 485: def request_success(packet) 486: info { "global request success" } 487: callback = pending_requests.shift 488: callback.call(true, packet) if callback 489: end
Disabled; run with --debug to generate this.
Generated with the Darkfish Rdoc Generator 1.1.6.