Class: Shells::ShellBase

Inherits:
Object
  • Object
show all
Defined in:
lib/shells/shell_base.rb

Overview

Provides a base interface for all shells to build on.

Instantiating this class will raise an error. All shell sessions should inherit this class.

Direct Known Subclasses

SerialSession, SshSession

Defined Under Namespace

Classes: QuitNow

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}, &block) ⇒ ShellBase

Initializes the shell with the supplied options.

These options are common to all shells.

prompt

Defaults to “~~#”. Most special characters will be stripped.

retrieve_exit_code

Defaults to false. Can also be true.

on_non_zero_exit_code

Defaults to :ignore. Can also be :raise.

silence_timeout

Defaults to 0. If greater than zero, will raise an error after waiting this many seconds for a prompt.

command_timeout

Defaults to 0. If greater than zero, will raise an error after a command runs for this long without finishing.

Please check the documentation for each session class for specific shell options.

Once the shell is initialized, the shell is yielded to the provided code block which can then interact with the shell. Once the code block completes, the shell is closed and the session object is returned.

After completion the session object can only be used to review the stdout, stderr, and combined_output properties. The exec method and any other methods that interact with the shell will no longer be functional.

Raises:

  • (NotImplementedError)


50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/shells/shell_base.rb', line 50

def initialize(options = {}, &block)

  # cannot instantiate a ShellBase
  raise NotImplementedError if self.class == Shells::ShellBase

  raise ArgumentError, 'A code block is required.' unless block_given?
  raise ArgumentError, '\'options\' must be a hash.' unless options.is_a?(Hash)

  @options = {
      prompt: '~~#',
      retrieve_exit_code: false,
      on_non_zero_exit_code: :ignore,
      silence_timeout: 0,
      command_timeout: 0
  }.merge( options.inject({}){ |m,(k,v)|  m[k.to_sym] = v; m } )

  @options[:prompt] = @options[:prompt]
                          .to_s.strip
                          .gsub('!', '#')
                          .gsub('$', '#')
                          .gsub('\\', '.')
                          .gsub('/', '.')
                          .gsub('"', '-')
                          .gsub('\'', '-')

  @options[:prompt] = '~~#' if @options[:prompt] == ''

  raise Shells::InvalidOption, ':on_non_zero_exit_code must be :ignore, :raise, or nil.' unless [:ignore, :raise].include?(@options[:on_non_zero_exit_code])

  validate_options
  @options.freeze   # no more changes to options now.

  @session_complete = false
  @last_input = Time.now
  debug 'Calling "exec_shell"...'
  exec_shell do
    begin
      debug 'Running "before_init" hooks...'
      run_hook :before_init
      debug 'Calling "exec_prompt"...'
      exec_prompt do
        begin
          debug 'Running "after_init" hooks...'
          run_hook :after_init
          debug 'Executing code block...'
          block.call self
        ensure
          debug 'Running "before_term" hooks...'
          run_hook :before_term
        end
      end
    rescue QuitNow
      debug 'Received QuitNow signal.'
      nil
    rescue Exception => ex
      unless run_hook(:on_exception, ex)
        raise
      end
    ensure
      debug 'Running "after_term" hooks...'
      run_hook :after_term
    end
  end

  @session_complete = true
end

Instance Attribute Details

#last_exit_codeObject

Gets the exit code from the last command if it was retrieved.



24
25
26
# File 'lib/shells/shell_base.rb', line 24

def last_exit_code
  @last_exit_code
end

#optionsObject (readonly)

The options provided to this shell.



20
21
22
# File 'lib/shells/shell_base.rb', line 20

def options
  @options
end

Class Method Details

.after_init(proc = nil, &block) ⇒ Object

Adds code to be run after the shell is fully initialized but before the session code executes.

after_init do |shell|
  ...
end

You can also pass the name of a static method.

def self.some_init_function(shell)
  ...
end

after_init :some_init_function


176
177
178
# File 'lib/shells/shell_base.rb', line 176

def self.after_init(proc = nil, &block)
  add_hook :after_init, proc, &block
end

.after_term(proc = nil, &block) ⇒ Object

Adds code to be run after the shell session is terminated but before closing the shell session.

This code might also be used to navigate a menu or clean up an environment. This method allows you to define that behavior without rewriting the connection code.

This code is guaranteed to be called if the shell connects correctly. That means if an error is raised in the session code or shell initialization code, this will still fire before closing the shell session.

after_term do |shell|
  ...
end

You can also pass the name of a static method.

def self.some_term_function(shell)
  ...
end

after_term :some_term_function


228
229
230
# File 'lib/shells/shell_base.rb', line 228

def self.after_term(proc = nil, &block)
  add_hook :after_term, proc, &block
end

.before_init(proc = nil, &block) ⇒ Object

Adds code to be run before the shell is fully initialized.

This code would normally be used to navigate a menu or setup an environment. This method allows you to define that behavior without rewriting the connection code.

before_init do |shell|
  ...
end

You can also pass the name of a static method.

def self.some_init_function(shell)
  ...
end

before_init :some_init_function


157
158
159
# File 'lib/shells/shell_base.rb', line 157

def self.before_init(proc = nil, &block)
  add_hook :before_init, proc, &block
end

.before_term(proc = nil, &block) ⇒ Object

Adds code to be run before the shell is terminated immediately after executing the session code.

This code might also be used to navigate a menu or clean up an environment. This method allows you to define that behavior without rewriting the connection code.

This code is guaranteed to be called if the shell initializes correctly. That means if an error is raised in the session code, this will still fire before handling the error.

before_term do |shell|
  ...
end

You can also pass the name of a static method.

def self.some_term_function(shell)
  ...
end

before_term :some_term_function


202
203
204
# File 'lib/shells/shell_base.rb', line 202

def self.before_term(proc = nil, &block)
  add_hook :before_term, proc, &block
end

.on_debug(proc = nil, &block) ⇒ Object

Sets the code to be run when debug messages are processed.

The code will receive the debug message as an argument.

on_debug do |msg|
  puts msg
end


126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/shells/shell_base.rb', line 126

def self.on_debug(proc = nil, &block)
  @on_debug =
      if proc.respond_to?(:call)
        proc
      elsif proc && respond_to?(proc.to_s, true)
        method(proc.to_s.to_sym)
      elsif block
        block
      else
        nil
      end
end

.on_exception(proc = nil, &block) ⇒ Object

Adds code to be run when an exception occurs.

This code will receive the shell as the first argument and the exception as the second. If it handles the exception it should return true, otherwise nil or false.

on_exception do |shell, ex|
  if ex.is_a?(MyExceptionType)
    ...
    true
  else
    false
  end
end

You can also pass the name of a static method.

def self.some_exception_handler(shell, ex)
  ...
end

on_exception :some_exception_handler


256
257
258
# File 'lib/shells/shell_base.rb', line 256

def self.on_exception(proc = nil, &block)
  add_hook :on_exception, proc, &block
end

Instance Method Details

#combined_outputObject

Gets both the standard output and error output from the session.

The prompts will be included in the combined output. There is no attempt to differentiate error output from standard output.

This is essentially the definitive log for the session.

All line endings are converted to LF characters, so you will not encounter or need to search for CRLF or CR sequences.



308
309
310
# File 'lib/shells/shell_base.rb', line 308

def combined_output
  @stdcomb ||= ''
end

#exec(command, options = {}, &block) ⇒ Object

Executes a command during the shell session.

If called outside of the new block, this will raise an error.

The command is the command to execute in the shell.

The options can be used to override the exit code behavior. In all cases, the :default option is the same as not providing the option and will cause exec to inherit the option from the shell’s options.

retrieve_exit_code

This can be one of :default, true, or false.

on_non_zero_exit_code

This can be on ot :default, :ignore, or :raise.

silence_timeout

This can be :default or the number of seconds to wait in silence before timing out.

command_timeout

This can be :default or the maximum number of seconds to wait for a command to finish before timing out.

If provided, the block is a chunk of code that will be processed every time the shell receives output from the program. If the block returns a string, the string will be sent to the shell. This can be used to monitor processes or monitor and interact with processes. The block is optional.

shell.exec('sudo -p "password:" nginx restart') do |data,type|
  return 'super-secret' if /password:$/.match(data)
  nil
end


342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/shells/shell_base.rb', line 342

def exec(command, options = {}, &block)
  raise Shells::SessionCompleted if session_complete?

  options ||= {}
  options = { timeout_error: true }.merge(options)
  options = self.options.merge(options.inject({}) { |m,(k,v)| m[k.to_sym] = v; m })
  options[:retrieve_exit_code] = self.options[:retrieve_exit_code] if options[:retrieve_exit_code] == :default
  options[:on_non_zero_exit_code] = self.options[:on_non_zero_exit_code] unless [:raise, :ignore].include?(options[:on_non_zero_exit_code])
  options[:silence_timeout] = self.options[:silence_timeout] if options[:silence_timeout] == :default
  options[:command_timeout] = self.options[:command_timeout] if options[:command_timeout] == :default
  options[:command_is_echoed] = true if options[:command_is_echoed].nil?
  ret = ''

  begin
    push_buffer # store the current buffer and start a fresh buffer

    # buffer while also passing data to the supplied block.
    if block_given?
      buffer_input(&block)
    end

    # send the command and wait for the prompt to return.
    debug 'Sending command: ' + command
    send_data command + line_ending
    if wait_for_prompt(options[:silence_timeout], options[:command_timeout], options[:timeout_error])
      # get the output of the command, minus the trailing prompt.
      debug 'Reading output of command...'
      ret = command_output command, options[:command_is_echoed]

      if options[:retrieve_exit_code]
        self.last_exit_code = get_exit_code
        if options[:on_non_zero_exit_code] == :raise
          raise NonZeroExitCode.new(last_exit_code) unless last_exit_code == 0
        end
      else
        self.last_exit_code = nil
      end
    else
      # A timeout occurred and timeout_error was set to false.
      debug 'Command timed out...'
      self.last_exit_code = :timeout
      ret = combined_output
    end

  ensure
    # return buffering to normal.
    if block_given?
      buffer_input
    end

    # restore the original buffer and merge the output from the command.
    pop_merge_buffer
  end
  ret
end

#exec_for_code(command, options = {}, &block) ⇒ Object

Executes a command specifically for the exit code.

Does not return the output of the command, only the exit code.



402
403
404
405
406
# File 'lib/shells/shell_base.rb', line 402

def exec_for_code(command, options = {}, &block)
  options = (options || {}).merge(retrieve_exit_code: true, on_non_zero_exit_code: :ignore)
  exec command, options, &block
  last_exit_code
end

#exec_ignore_code(command, options = {}, &block) ⇒ Object

Executes a command ignoring any exit code.

Returns the output of the command and does not even retrieve the exit code.



412
413
414
415
# File 'lib/shells/shell_base.rb', line 412

def exec_ignore_code(command, options = {}, &block)
  options = (options || {}).merge(retrieve_exit_code: false, on_non_zero_exit_code: :ignore)
  exec command, options, &block
end

#line_endingObject

Defines the line ending used to terminate commands sent to the shell.

The default is “n”. If you need “rn”, “r”, or some other value, simply override this function.



264
265
266
# File 'lib/shells/shell_base.rb', line 264

def line_ending
  "\n"
end

#read_file(path) ⇒ Object

Reads from a file on the device.

Raises:

  • (::NotImplementedError)


419
420
421
# File 'lib/shells/shell_base.rb', line 419

def read_file(path)
  raise ::NotImplementedError
end

#session_complete?Boolean

Has the session been completed?

Returns:

  • (Boolean)


270
271
272
# File 'lib/shells/shell_base.rb', line 270

def session_complete?
  @session_complete
end

#stderrObject

Gets the error output from the session.

All line endings are converted to LF characters, so you will not encounter or need to search for CRLF or CR sequences.



293
294
295
# File 'lib/shells/shell_base.rb', line 293

def stderr
  @stderr ||= ''
end

#stdoutObject

Gets the standard output from the session.

The prompts are stripped from the standard ouput as they are encountered. So this will be a list of commands with their output.

All line endings are converted to LF characters, so you will not encounter or need to search for CRLF or CR sequences.



283
284
285
# File 'lib/shells/shell_base.rb', line 283

def stdout
  @stdout ||= ''
end

#write_file(path, data) ⇒ Object

Writes to a file on the device.

Raises:

  • (::NotImplementedError)


425
426
427
# File 'lib/shells/shell_base.rb', line 425

def write_file(path, data)
  raise ::NotImplementedError
end