Class: Net::SSH::Connection::Session

Inherits:
Object
  • Object
show all
Includes:
Constants, Loggable
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

Attributes included from Loggable

#logger

Instance Method Summary collapse

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.


69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/net/ssh/connection/session.rb', line 69

def initialize(transport, options={})
  self.logger = transport.logger
    
  @transport = transport
  @options = options
    
  @channel_id_counter = -1
  @channels = Hash.new(NilChannel.new(self))
  @listeners = { transport.socket => nil }
  @pending_requests = []
  @channel_open_handlers = {}
  @on_global_request = {}
  @properties = (options[:properties] || {}).dup
    
  @max_pkt_size = (options.key?(:max_pkt_size) ? options[:max_pkt_size] : 0x8000)
  @max_win_size = (options.key?(:max_win_size) ? options[:max_win_size] : 0x20000)
    
  @keepalive = Keepalive.new(self)
    
  @event_loop = options[:event_loop] || SingleSessionEventLoop.new
  @event_loop.register(self)
end

Instance Attribute Details

#channel_open_handlersObject (readonly)

The map of specialized handlers for opening specific channel types. See #on_open_channel.


52
53
54
# File 'lib/net/ssh/connection/session.rb', line 52

def channel_open_handlers
  @channel_open_handlers
end

#channelsObject (readonly)

The map of channels, each key being the local-id for the channel.


45
46
47
# File 'lib/net/ssh/connection/session.rb', line 45

def channels
  @channels
end

#listenersObject (readonly)

The map of listeners that the event loop knows about. See #listen_to.


48
49
50
# File 'lib/net/ssh/connection/session.rb', line 48

def listeners
  @listeners
end

#optionsObject (readonly)

The map of options that were used to initialize this instance.


39
40
41
# File 'lib/net/ssh/connection/session.rb', line 39

def options
  @options
end

#pending_requestsObject (readonly)

The list of callbacks for pending requests. See #send_global_request.


55
56
57
# File 'lib/net/ssh/connection/session.rb', line 55

def pending_requests
  @pending_requests
end

#propertiesObject (readonly)

The collection of custom properties for this instance. (See #[] and #[]=).


42
43
44
# File 'lib/net/ssh/connection/session.rb', line 42

def properties
  @properties
end

#transportObject (readonly)

The underlying transport layer abstraction (see Net::SSH::Transport::Session).


36
37
38
# File 'lib/net/ssh/connection/session.rb', line 36

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.


95
96
97
# File 'lib/net/ssh/connection/session.rb', line 95

def [](key)
  @properties[key]
end

#[]=(key, value) ⇒ Object

Sets a custom property for this instance.


100
101
102
# File 'lib/net/ssh/connection/session.rb', line 100

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? }

Returns:

  • (Boolean)

153
154
155
156
157
158
159
# File 'lib/net/ssh/connection/session.rb', line 153

def busy?(include_invisible=false)
  if include_invisible
    channels.any?
  else
    channels.any? { |id, ch| !ch[:invisible] }
  end
end

#cleanup_channel(channel) ⇒ Object


521
522
523
524
525
526
# File 'lib/net/ssh/connection/session.rb', line 521

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

#closeObject

Closes the session gracefully, blocking until all channels have successfully closed, and then closes the underlying transport layer connection.


122
123
124
125
126
127
128
129
130
131
# File 'lib/net/ssh/connection/session.rb', line 122

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.

Returns:

  • (Boolean)

115
116
117
# File 'lib/net/ssh/connection/session.rb', line 115

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


254
255
256
257
258
# File 'lib/net/ssh/connection/session.rb', line 254

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.


269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/net/ssh/connection/session.rb', line 269

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


288
289
290
291
292
# File 'lib/net/ssh/connection/session.rb', line 288

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


247
248
249
250
# File 'lib/net/ssh/connection/session.rb', line 247

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

378
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
# File 'lib/net/ssh/connection/session.rb', line 378

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 it's exit satus


419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/net/ssh/connection/session.rb', line 419

def exec!(command, status: nil, &block)
  block_or_concat = block || Proc.new do |ch, type, data|
    ch[:result] ||= ""
    ch[:result] << data
  end
    
  status ||= {}
  channel = exec(command, status: status, &block_or_concat)
  channel.wait
    
  channel[:result] ||= "" unless block
  channel[:result] &&= channel[:result].force_encoding("UTF-8") unless block
    
  StringWithExitstatus.new(channel[:result], status[:exit_code]) if channel[:result]
end

#forwardObject

Returns a reference to the Net::SSH::Service::Forward service, which can be used for forwarding ports over SSH.


492
493
494
# File 'lib/net/ssh/connection/session.rb', line 492

def forward
  @forward ||= Service::Forward.new(self)
end

#hostObject

Returns the name of the host that was given to the transport layer to connect to.


106
107
108
# File 'lib/net/ssh/connection/session.rb', line 106

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

480
481
482
# File 'lib/net/ssh/connection/session.rb', line 480

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 }

178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/net/ssh/connection/session.rb', line 178

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.message =~ /closed/
      debug { "stream was closed after loop => shallowing exception so it will be re-raised in next loop" }
    else
      raise
    end
  end
end

#loop_foreverObject

preserve a reference to Kernel#loop


142
# File 'lib/net/ssh/connection/session.rb', line 142

alias :loop_forever :loop

#max_select_wait_timeObject

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.


531
532
533
# File 'lib/net/ssh/connection/session.rb', line 531

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.


516
517
518
519
# File 'lib/net/ssh/connection/session.rb', line 516

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.


506
507
508
# File 'lib/net/ssh/connection/session.rb', line 506

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

337
338
339
340
341
342
343
344
345
346
347
# File 'lib/net/ssh/connection/session.rb', line 337

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)
  send_message(msg)
    
  channels[local_id] = channel
end

#postprocess(readers, writers) ⇒ Object

This is called internally as part of #process.


261
262
263
# File 'lib/net/ssh/connection/session.rb', line 261

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.


238
239
240
241
242
243
# File 'lib/net/ssh/connection/session.rb', line 238

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

226
227
228
229
230
231
# File 'lib/net/ssh/connection/session.rb', line 226

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]")

310
311
312
313
314
315
316
# File 'lib/net/ssh/connection/session.rb', line 310

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)
  send_message(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.send_message(Buffer.from(:byte, REQUEST_SUCCESS).to_s)

442
443
444
# File 'lib/net/ssh/connection/session.rb', line 442

def send_message(message)
  transport.enqueue_message(message)
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).


137
138
139
# File 'lib/net/ssh/connection/session.rb', line 137

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.


486
487
488
# File 'lib/net/ssh/connection/session.rb', line 486

def stop_listening_to(io)
  listeners.delete(io)
end