Class: Net::SSH::Connection::Session
- Inherits:
-
Object
- Object
- Net::SSH::Connection::Session
- Defined in:
- lib/net/ssh/connection/session.rb
Overview
A session class representing the connection service running on top of the SSH transport layer. It manages the creation of channels (see #open_channel), and the dispatching of messages to the various channels. It also encapsulates the SSH event loop (via #loop and #process), 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
Defined Under Namespace
Classes: NilChannel, StringWithExitstatus
Constant Summary collapse
- DEFAULT_IO_SELECT_TIMEOUT =
Default IO.select timeout threshold
300
Constants included from Constants
Constants::CHANNEL_CLOSE, Constants::CHANNEL_DATA, Constants::CHANNEL_EOF, Constants::CHANNEL_EXTENDED_DATA, Constants::CHANNEL_FAILURE, Constants::CHANNEL_OPEN, Constants::CHANNEL_OPEN_CONFIRMATION, Constants::CHANNEL_OPEN_FAILURE, Constants::CHANNEL_REQUEST, Constants::CHANNEL_SUCCESS, Constants::CHANNEL_WINDOW_ADJUST, Constants::GLOBAL_REQUEST, Constants::REQUEST_FAILURE, Constants::REQUEST_SUCCESS
Instance Attribute Summary collapse
-
#channel_open_handlers ⇒ Object
readonly
The map of specialized handlers for opening specific channel types.
-
#channels ⇒ Object
readonly
The map of channels, each key being the local-id for the channel.
-
#listeners ⇒ Object
readonly
The map of listeners that the event loop knows about.
-
#options ⇒ Object
readonly
The map of options that were used to initialize this instance.
-
#pending_requests ⇒ Object
readonly
The list of callbacks for pending requests.
-
#properties ⇒ Object
readonly
The collection of custom properties for this instance.
-
#transport ⇒ Object
readonly
The underlying transport layer abstraction (see Net::SSH::Transport::Session).
Attributes included from Loggable
Instance Method Summary collapse
-
#[](key) ⇒ Object
Retrieves a custom property from this instance.
-
#[]=(key, value) ⇒ Object
Sets a custom property for this instance.
-
#busy?(include_invisible = false) ⇒ Boolean
Returns
true
if there are any channels currently active on this session. - #cleanup_channel(channel) ⇒ Object
-
#close ⇒ Object
Closes the session gracefully, blocking until all channels have successfully closed, and then closes the underlying transport layer connection.
-
#closed? ⇒ Boolean
Returns true if the underlying transport has been closed.
-
#ev_do_calculate_rw_wait(wait) ⇒ Object
Returns the file descriptors the event loop should wait for read/write events, we also return the max wait.
-
#ev_do_handle_events(readers, writers) ⇒ Object
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.
-
#ev_do_postprocess(was_events) ⇒ Object
calls Net::SSH::Transport::Session#rekey_as_needed to allow the transport layer to rekey.
-
#ev_preprocess(&block) ⇒ Object
Called by event loop to process available data before going to event multiplexing.
-
#exec(command, status: nil, &block) ⇒ Object
A convenience method for executing a command and interacting with it.
-
#exec!(command, status: nil, &block) ⇒ Object
Same as #exec, except this will block until the command finishes.
-
#forward ⇒ Object
Returns a reference to the Net::SSH::Service::Forward service, which can be used for forwarding ports over SSH.
-
#host ⇒ Object
Returns the name of the host that was given to the transport layer to connect to.
-
#initialize(transport, options = {}) ⇒ Session
constructor
Create a new connection service instance atop the given transport layer.
-
#listen_to(io, &callback) ⇒ Object
Adds an IO object for the event loop to listen to.
-
#loop(wait = nil, &block) ⇒ Object
The main event loop.
-
#loop_forever ⇒ Object
preserve a reference to Kernel#loop.
-
#max_select_wait_time ⇒ Object
If the #preprocess and #postprocess callbacks for this session need to run periodically, this method returns the maximum number of seconds which may pass between callbacks.
-
#on_global_request(type, &block) ⇒ Object
Registers a handler to be invoked when the server sends a global request of the given type.
-
#on_open_channel(type, &block) ⇒ Object
Registers a handler to be invoked when the server wants to open a channel on the client.
-
#open_channel(type = "session", *extra, &on_confirm) ⇒ Object
Requests that a new channel be opened.
-
#postprocess(readers, writers) ⇒ Object
This is called internally as part of #process.
-
#preprocess(&block) ⇒ Object
This is called internally as part of #process.
-
#process(wait = nil, &block) ⇒ Object
The core of the event loop.
-
#send_global_request(type, *extra, &callback) ⇒ Object
Send a global request of the given type.
-
#send_message(message) ⇒ Object
Enqueues a message to be sent to the server as soon as the socket is available for writing.
-
#shutdown! ⇒ Object
Performs a “hard” shutdown of the connection.
-
#stop_listening_to(io) ⇒ Object
Removes the given io object from the listeners collection, so that the event loop will no longer monitor it.
Methods included from Loggable
#debug, #error, #fatal, #info, #lwarn
Constructor Details
#initialize(transport, options = {}) ⇒ Session
Create a new connection service instance atop the given transport layer. Initializes the listeners to be only the underlying socket object.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/net/ssh/connection/session.rb', line 68 def initialize(transport, = {}) self.logger = transport.logger @transport = transport @options = @channel_id_counter = -1 @channels = Hash.new(NilChannel.new(self)) @listeners = { transport.socket => nil } @pending_requests = [] @channel_open_handlers = {} @on_global_request = {} @properties = ([:properties] || {}).dup @max_pkt_size = (.key?(:max_pkt_size) ? [:max_pkt_size] : 0x8000) @max_win_size = (.key?(:max_win_size) ? [:max_win_size] : 0x20000) @keepalive = Keepalive.new(self) @event_loop = [:event_loop] || SingleSessionEventLoop.new @event_loop.register(self) end |
Instance Attribute Details
#channel_open_handlers ⇒ Object (readonly)
The map of specialized handlers for opening specific channel types. See #on_open_channel.
51 52 53 |
# File 'lib/net/ssh/connection/session.rb', line 51 def channel_open_handlers @channel_open_handlers end |
#channels ⇒ Object (readonly)
The map of channels, each key being the local-id for the channel.
44 45 46 |
# File 'lib/net/ssh/connection/session.rb', line 44 def channels @channels end |
#listeners ⇒ Object (readonly)
The map of listeners that the event loop knows about. See #listen_to.
47 48 49 |
# File 'lib/net/ssh/connection/session.rb', line 47 def listeners @listeners end |
#options ⇒ Object (readonly)
The map of options that were used to initialize this instance.
38 39 40 |
# File 'lib/net/ssh/connection/session.rb', line 38 def @options end |
#pending_requests ⇒ Object (readonly)
The list of callbacks for pending requests. See #send_global_request.
54 55 56 |
# File 'lib/net/ssh/connection/session.rb', line 54 def pending_requests @pending_requests end |
#properties ⇒ Object (readonly)
The collection of custom properties for this instance. (See #[] and #[]=).
41 42 43 |
# File 'lib/net/ssh/connection/session.rb', line 41 def properties @properties end |
#transport ⇒ Object (readonly)
The underlying transport layer abstraction (see Net::SSH::Transport::Session).
35 36 37 |
# File 'lib/net/ssh/connection/session.rb', line 35 def transport @transport end |
Instance Method Details
#[](key) ⇒ Object
Retrieves a custom property from this instance. This can be used to store additional state in applications that must manage multiple SSH connections.
94 95 96 |
# File 'lib/net/ssh/connection/session.rb', line 94 def [](key) @properties[key] end |
#[]=(key, value) ⇒ Object
Sets a custom property for this instance.
99 100 101 |
# File 'lib/net/ssh/connection/session.rb', line 99 def []=(key, value) @properties[key] = value end |
#busy?(include_invisible = false) ⇒ Boolean
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? }
152 153 154 155 156 157 158 |
# File 'lib/net/ssh/connection/session.rb', line 152 def busy?(include_invisible = false) if include_invisible channels.any? else channels.any? { |id, ch| !ch[:invisible] } end end |
#cleanup_channel(channel) ⇒ Object
522 523 524 525 526 527 |
# File 'lib/net/ssh/connection/session.rb', line 522 def cleanup_channel(channel) if channel.local_closed? and channel.remote_closed? info { "#{host} delete channel #{channel.local_id} which closed locally and remotely" } channels.delete(channel.local_id) end end |
#close ⇒ Object
Closes the session gracefully, blocking until all channels have successfully closed, and then closes the underlying transport layer connection.
121 122 123 124 125 126 127 128 129 130 |
# File 'lib/net/ssh/connection/session.rb', line 121 def close info { "closing remaining channels (#{channels.length} open)" } channels.each { |id, channel| channel.close } begin loop(0.1) { channels.any? } rescue Net::SSH::Disconnect raise unless channels.empty? end transport.close end |
#closed? ⇒ Boolean
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.
114 115 116 |
# File 'lib/net/ssh/connection/session.rb', line 114 def closed? transport.closed? end |
#ev_do_calculate_rw_wait(wait) ⇒ Object
Returns the file descriptors the event loop should wait for read/write events, we also return the max wait
255 256 257 258 259 |
# File 'lib/net/ssh/connection/session.rb', line 255 def ev_do_calculate_rw_wait(wait) r = listeners.keys w = r.select { |w2| w2.respond_to?(:pending_write?) && w2.pending_write? } [r, w, io_select_wait(wait)] end |
#ev_do_handle_events(readers, writers) ⇒ Object
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.
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 |
# File 'lib/net/ssh/connection/session.rb', line 270 def ev_do_handle_events(readers, writers) Array(readers).each do |reader| if listeners[reader] listeners[reader].call(reader) else if reader.fill.zero? reader.close stop_listening_to(reader) end end end Array(writers).each do |writer| writer.send_pending end end |
#ev_do_postprocess(was_events) ⇒ Object
calls Net::SSH::Transport::Session#rekey_as_needed to allow the transport layer to rekey
289 290 291 292 293 |
# File 'lib/net/ssh/connection/session.rb', line 289 def ev_do_postprocess(was_events) @keepalive.send_as_needed(was_events) transport.rekey_as_needed true end |
#ev_preprocess(&block) ⇒ Object
Called by event loop to process available data before going to event multiplexing
248 249 250 251 |
# File 'lib/net/ssh/connection/session.rb', line 248 def ev_preprocess(&block) dispatch_incoming_packets(raise_disconnect_errors: false) each_channel { |id, channel| channel.process unless channel.local_closed? } end |
#exec(command, status: nil, &block) ⇒ Object
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 #open_channel, 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
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 |
# File 'lib/net/ssh/connection/session.rb', line 379 def exec(command, status: nil, &block) open_channel do |channel| channel.exec(command) do |ch, success| raise "could not execute command: #{command.inspect}" unless success if status channel.on_request("exit-status") do |ch2, data| status[:exit_code] = data.read_long end channel.on_request("exit-signal") do |ch2, data| status[:exit_signal] = data.read_long end end channel.on_data do |ch2, data| if block block.call(ch2, :stdout, data) else $stdout.print(data) end end channel.on_extended_data do |ch2, type, data| if block block.call(ch2, :stderr, data) else $stderr.print(data) end end end end end |
#exec!(command, status: nil, &block) ⇒ Object
Same as #exec, except this will block until the command finishes. Also, if no block is given, this will return all output (stdout and stderr) as a single string.
matches = ssh.exec!("grep something /some/files")
the returned string has an exitstatus method to query its exit status
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 |
# File 'lib/net/ssh/connection/session.rb', line 420 def exec!(command, status: nil, &block) block_or_concat = block || Proc.new do |ch, type, data| ch[:result] ||= String.new ch[:result] << data end status ||= {} channel = exec(command, status: status, &block_or_concat) channel.wait channel[:result] ||= String.new unless block channel[:result] &&= channel[:result].force_encoding("UTF-8") unless block StringWithExitstatus.new(channel[:result], status[:exit_code]) if channel[:result] end |
#forward ⇒ Object
Returns a reference to the Net::SSH::Service::Forward service, which can be used for forwarding ports over SSH.
493 494 495 |
# File 'lib/net/ssh/connection/session.rb', line 493 def forward @forward ||= Service::Forward.new(self) end |
#host ⇒ Object
Returns the name of the host that was given to the transport layer to connect to.
105 106 107 |
# File 'lib/net/ssh/connection/session.rb', line 105 def host transport.host end |
#listen_to(io, &callback) ⇒ Object
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 #fill method invoked.
Any io
value passed to this method must have mixed into it the Net::SSH::BufferedIo functionality, typically by calling #extend 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
481 482 483 |
# File 'lib/net/ssh/connection/session.rb', line 481 def listen_to(io, &callback) listeners[io] = callback end |
#loop(wait = nil, &block) ⇒ Object
The main event loop. Calls #process until #process returns false. If a block is given, it is passed to #process, otherwise a default proc is used that just returns true if there are any channels active (see #busy?). The # wait
parameter is also passed through to #process (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 }
177 178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/net/ssh/connection/session.rb', line 177 def loop(wait = nil, &block) running = block || Proc.new { busy? } loop_forever { break unless process(wait, &running) } begin process(0) rescue IOError => e if e. =~ /closed/ debug { "stream was closed after loop => shallowing exception so it will be re-raised in next loop" } else raise end end end |
#loop_forever ⇒ Object
preserve a reference to Kernel#loop
141 |
# File 'lib/net/ssh/connection/session.rb', line 141 alias :loop_forever :loop |
#max_select_wait_time ⇒ Object
If the #preprocess and #postprocess callbacks for this session need to run periodically, this method returns the maximum number of seconds which may pass between callbacks.
532 533 534 |
# File 'lib/net/ssh/connection/session.rb', line 532 def max_select_wait_time @keepalive.interval if @keepalive.enabled? end |
#on_global_request(type, &block) ⇒ Object
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.
517 518 519 520 |
# File 'lib/net/ssh/connection/session.rb', line 517 def on_global_request(type, &block) old, @on_global_request[type] = @on_global_request[type], block old end |
#on_open_channel(type, &block) ⇒ Object
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.
507 508 509 |
# File 'lib/net/ssh/connection/session.rb', line 507 def on_open_channel(type, &block) channel_open_handlers[type] = block end |
#open_channel(type = "session", *extra, &on_confirm) ⇒ Object
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 #open_channel 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
338 339 340 341 342 343 344 345 346 347 348 |
# File 'lib/net/ssh/connection/session.rb', line 338 def open_channel(type = "session", *extra, &on_confirm) local_id = get_next_channel_id channel = Channel.new(self, type, local_id, @max_pkt_size, @max_win_size, &on_confirm) msg = Buffer.from(:byte, CHANNEL_OPEN, :string, type, :long, local_id, :long, channel.local_maximum_window_size, :long, channel.local_maximum_packet_size, *extra) (msg) channels[local_id] = channel end |
#postprocess(readers, writers) ⇒ Object
This is called internally as part of #process.
262 263 264 |
# File 'lib/net/ssh/connection/session.rb', line 262 def postprocess(readers, writers) ev_do_handle_events(readers, writers) end |
#preprocess(&block) ⇒ Object
This is called internally as part of #process. 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.
237 238 239 240 241 242 243 244 |
# File 'lib/net/ssh/connection/session.rb', line 237 def preprocess(&block) return false if block_given? && !yield(self) ev_preprocess(&block) return false if block_given? && !yield(self) return true end |
#process(wait = nil, &block) ⇒ Object
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 #process to return false. Otherwise, #process 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).
TODO revise example
# 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
225 226 227 228 229 230 |
# File 'lib/net/ssh/connection/session.rb', line 225 def process(wait = nil, &block) @event_loop.process(wait, &block) rescue StandardError force_channel_cleanup_on_close if closed? raise end |
#send_global_request(type, *extra, &callback) ⇒ Object
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("[email protected]")
311 312 313 314 315 316 317 |
# File 'lib/net/ssh/connection/session.rb', line 311 def send_global_request(type, *extra, &callback) info { "sending global request #{type}" } msg = Buffer.from(:byte, GLOBAL_REQUEST, :string, type.to_s, :bool, !callback.nil?, *extra) (msg) pending_requests << callback if callback self end |
#send_message(message) ⇒ Object
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.(Buffer.from(:byte, REQUEST_SUCCESS).to_s)
443 444 445 |
# File 'lib/net/ssh/connection/session.rb', line 443 def () transport.() end |
#shutdown! ⇒ Object
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).
136 137 138 |
# File 'lib/net/ssh/connection/session.rb', line 136 def shutdown! transport.shutdown! end |
#stop_listening_to(io) ⇒ Object
Removes the given io object from the listeners collection, so that the event loop will no longer monitor it.
487 488 489 |
# File 'lib/net/ssh/connection/session.rb', line 487 def stop_listening_to(io) listeners.delete(io) end |