Module: Net::SSH::CLI

Included in:
Session
Defined in:
lib/net/ssh/cli.rb,
lib/net/ssh/cli/version.rb

Defined Under Namespace

Classes: Error, Session

Constant Summary collapse

OPTIONS =
ActiveSupport::HashWithIndifferentAccess.new(
  default_prompt:            /\n?^(\S+@.*)\z/,                             # the default prompt to search for
  cmd_rm_prompt:             false,                                        # whether the prompt should be removed in the output of #cmd
  cmd_rm_command:            false,                                        # whether the given command should be removed in the output of #cmd
  read_till_timeout:         nil,                                          # timeout for #read_till to find the match
  named_prompts:             ActiveSupport::HashWithIndifferentAccess.new, # you can used named prompts for #with_prompt {} 
  before_on_stdout_procs:    ActiveSupport::HashWithIndifferentAccess.new, # procs to call before data arrives from the underlying connection 
  after_on_stdout_procs:     ActiveSupport::HashWithIndifferentAccess.new, # procs to call after  data arrives from the underlying connection
  before_open_channel_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call before opening a channel 
  after_open_channel_procs:  ActiveSupport::HashWithIndifferentAccess.new, # procs to call after  opening a channel, for example you could call #detect_prompt or #read_till
  open_channel_timeout:      nil,                                          # timeout to open the channel
  net_ssh_options:           ActiveSupport::HashWithIndifferentAccess.new, # a wrapper for options to pass to Net::SSH.start in case net_ssh is undefined
  process_time:              0.00001,                                      # how long #process is processing net_ssh#process or sleeping (waiting for something)
  background_processing:     false,                                        # default false, whether the process method maps to the underlying net_ssh#process or the net_ssh#process happens in a separate loop
)
VERSION =
'1.0.0'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#channelObject

Returns the value of attribute channel.



37
38
39
# File 'lib/net/ssh/cli.rb', line 37

def channel
  @channel
end

#loggerObject

Returns the value of attribute logger.



37
38
39
# File 'lib/net/ssh/cli.rb', line 37

def logger
  @logger
end

#net_sshObject Also known as: proxy

NET::SSH



236
237
238
# File 'lib/net/ssh/cli.rb', line 236

def net_ssh
  @net_ssh
end

#stdoutObject

Returns the value of attribute stdout.



37
38
39
# File 'lib/net/ssh/cli.rb', line 37

def stdout
  @stdout
end

Class Method Details

.start(**opts) ⇒ Object

Example net_ssh = Net::SSH.start(“localhost”) net_ssh_cli = Net::SSH::CLI.start(net_ssh: net_ssh) net_ssh_cli.cmd “cat /etc/passwd”

> “root:x:0:0:root:/root:/bin/bashn…”



27
28
29
# File 'lib/net/ssh/cli.rb', line 27

def self.start(**opts)
  Net::SSH::CLI::Session.new(**opts)
end

Instance Method Details

#close_channelObject



283
284
285
286
# File 'lib/net/ssh/cli.rb', line 283

def close_channel
  net_ssh&.cleanup_channel(channel) if channel
  self.channel = nil
end

#cmd(command, pre_read: true, rm_prompt: cmd_rm_prompt, rm_command: cmd_rm_command, prompt: current_prompt, **opts) ⇒ Object Also known as: command, exec

‘read’ first on purpuse as a feature. once you cmd you ignore what happend before. otherwise use read|write directly. this should avoid many horrible state issues where the prompt is not the last prompt



194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/net/ssh/cli.rb', line 194

def cmd(command, pre_read: true, rm_prompt: cmd_rm_prompt, rm_command: cmd_rm_command, prompt: current_prompt, **opts)
  opts = opts.clone.merge(pre_read: pre_read, rm_prompt: rm_prompt, rm_command: rm_command, prompt: prompt)
  if pre_read
    pre_read_data = read
    logger.debug { "#cmd ignoring pre-command output: #{pre_read_data.inspect}" } if pre_read_data.present?
  end
  write_n command
  output = read_till(opts)
  rm_prompt!(output, opts)
  rm_command!(output, command, opts)
  output
end

#cmds(*commands, **opts) ⇒ Object Also known as: commands



209
210
211
# File 'lib/net/ssh/cli.rb', line 209

def cmds(*commands, **opts)
  commands.flatten.map { |command| [command, cmd(command, **opts)] }
end

#current_promptObject

fancy prompt|prompt handling methods



132
133
134
# File 'lib/net/ssh/cli.rb', line 132

def current_prompt
  with_prompts[-1] || default_prompt
end

#detect_prompt(seconds: 3) ⇒ Object



144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/net/ssh/cli.rb', line 144

def detect_prompt(seconds: 3)
  write_n
  future = Time.now + seconds
  while future > Time.now
    process
    sleep 0.1
  end
  self.default_prompt = read[/\n?^.*\z/]
  raise Error::PromptDetection, "couldn't detect a prompt" unless default_prompt.present?

  default_prompt
end

#dialog(command, prompt, **opts) ⇒ Object



187
188
189
190
# File 'lib/net/ssh/cli.rb', line 187

def dialog(command, prompt, **opts)
  opts = opts.clone.merge(prompt: prompt)
  cmd(command, opts)
end

#hostObject Also known as: hostname, to_s



227
228
229
# File 'lib/net/ssh/cli.rb', line 227

def host
  @net_ssh&.host
end

#initialize(**opts) ⇒ Object



31
32
33
34
35
# File 'lib/net/ssh/cli.rb', line 31

def initialize(**opts)
  options.merge!(opts)
  self.net_ssh = options.delete(:net_ssh)
  self.logger = options.delete(:logger) || Logger.new(STDOUT, level: Logger::WARN)
end

#on_stdout(data) ⇒ Object



102
103
104
105
106
107
108
# File 'lib/net/ssh/cli.rb', line 102

def on_stdout(data)
  before_on_stdout_procs.each { |_name, a_proc| instance_eval(&a_proc) }
  stdout << data
  after_on_stdout_procs.each { |_name, a_proc| instance_eval(&a_proc) }
  process # if we receive data, we probably receive more - improves performance
  stdout
end

#open_channelObject

cli_channel



257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/net/ssh/cli.rb', line 257

def open_channel # cli_channel
  before_open_channel_procs.each { |_name, a_proc| instance_eval(&a_proc) }
  ::Timeout.timeout(open_channel_timeout, Error::OpenChannelTimeout) do
    net_ssh.open_channel do |new_channel|
      logger.debug 'channel is open'
      self.channel = new_channel
      new_channel.request_pty do |_ch, success|
        raise Error::Pty, "#{host || ip} Failed to open ssh pty" unless success
      end
      new_channel.send_channel_request('shell') do |_ch, success|
        raise Error::RequestShell, 'Failed to open ssh shell' unless success
      end
      new_channel.on_data do |_ch, data|
        on_stdout(data)
      end
      # new_channel.on_extended_data do |_ch, type, data| end
      # new_channel.on_close do end
    end
    until channel do process end
  end
  logger.debug 'channel is ready, running callbacks now'
  after_open_channel_procs.each { |_name, a_proc| instance_eval(&a_proc) }
  process
  self
end

#optionsObject



55
56
57
58
59
60
61
62
63
# File 'lib/net/ssh/cli.rb', line 55

def options
  @options ||= begin
    opts = OPTIONS.clone
    opts.each do |key, value|
      opts[key] = value.clone if value.is_a?(Hash)
    end
    opts
  end
end

#options!(**opts) ⇒ Object

don’t even think about nesting hashes here



66
67
68
# File 'lib/net/ssh/cli.rb', line 66

def options!(**opts)
  options.merge!(opts)
end

#options=(opts) ⇒ Object



70
71
72
# File 'lib/net/ssh/cli.rb', line 70

def options=(opts)
  @options = ActiveSupport::HashWithIndifferentAccess.new(opts)
end

#process(time = process_time) ⇒ Object

have a deep look at the source of Net::SSH session#process github.com/net-ssh/net-ssh/blob/dd13dd44d68b7fa82d4ca9a3bbe18e30c855f1d2/lib/net/ssh/connection/session.rb#L227 session#loop github.com/net-ssh/net-ssh/blob/dd13dd44d68b7fa82d4ca9a3bbe18e30c855f1d2/lib/net/ssh/connection/session.rb#L179 because the (cli) channel stays open, we always need to ensure that the ssh layer gets “processed” further. This can be done inside here automatically or outside in a separate event loop for the net_ssh connection.



251
252
253
254
255
# File 'lib/net/ssh/cli.rb', line 251

def process(time = process_time)
  background_processing? ? sleep(time) : net_ssh.process(time)
rescue IOError => error
  raise Error, error.message
end

#readObject



122
123
124
125
126
127
# File 'lib/net/ssh/cli.rb', line 122

def read
  process
  var = stdout!
  logger.debug("#read: \n#{var}")
  var
end

#read_for(seconds:) ⇒ Object



182
183
184
185
# File 'lib/net/ssh/cli.rb', line 182

def read_for(seconds:)
  process(seconds)
  read
end

#read_till(prompt: current_prompt, timeout: read_till_timeout, **_opts) ⇒ Object



168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/net/ssh/cli.rb', line 168

def read_till(prompt: current_prompt, timeout: read_till_timeout, **_opts)
  raise Error::UndefinedMatch, 'no prompt given or default_prompt defined' unless prompt

  ::Timeout.timeout(timeout, Error::ReadTillTimeout.new("output did not prompt #{prompt.inspect} within #{timeout}")) do
    with_prompt(prompt) do
      until stdout[current_prompt] do
        process
        sleep 0.1
      end
    end
  end
  read
end

#rm_command!(output, command, **opts) ⇒ Object



214
215
216
# File 'lib/net/ssh/cli.rb', line 214

def rm_command!(output, command, **opts)
  output[command + "\n"] = '' if rm_command?(opts) && output[command + "\n"]
end

#rm_prompt!(output, **opts) ⇒ Object



218
219
220
221
222
223
224
225
# File 'lib/net/ssh/cli.rb', line 218

def rm_prompt!(output, **opts)
  if rm_prompt?(opts)
    prompt = opts[:prompt] || current_prompt
    if output[prompt]
      prompt.is_a?(Regexp) ? output[prompt, 1] = '' : output[prompt] = ''
    end
  end
end

#stdout!Object



96
97
98
99
100
# File 'lib/net/ssh/cli.rb', line 96

def stdout!
  var = stdout
  self.stdout = String.new
  var
end

#with_named_prompt(name) ⇒ Object



136
137
138
139
140
141
142
# File 'lib/net/ssh/cli.rb', line 136

def with_named_prompt(name)
  raise Error::UndefinedMatch, "unknown named_prompt #{name}" unless named_prompts[name]

  with_prompt(named_prompts[name]) do
    yield
  end
end

#with_prompt(prompt) ⇒ Object

prove a block where the default prompt changes



158
159
160
161
162
163
164
165
166
# File 'lib/net/ssh/cli.rb', line 158

def with_prompt(prompt)
  logger.debug { "#with_prompt: #{current_prompt.inspect} => #{prompt.inspect}" }
  with_prompts << prompt
  yield
  prompt
ensure
  with_prompts.delete_at(-1)
  logger.debug { "#with_prompt: => #{current_prompt.inspect}" }
end

#write(content = String.new) ⇒ Object Also known as: stdin



110
111
112
113
114
115
# File 'lib/net/ssh/cli.rb', line 110

def write(content = String.new)
  logger.debug { "#write #{content.inspect}" }
  channel.send_data content
  process
  content
end

#write_n(content = String.new) ⇒ Object



118
119
120
# File 'lib/net/ssh/cli.rb', line 118

def write_n(content = String.new)
  write content + "\n"
end