Class: Shells::ShellBase

Inherits:
Object
  • Object
show all
Defined in:
lib/shells/shell_base.rb,
lib/shells/shell_base/run.rb,
lib/shells/shell_base/exec.rb,
lib/shells/shell_base/sync.rb,
lib/shells/shell_base/debug.rb,
lib/shells/shell_base/hooks.rb,
lib/shells/shell_base/input.rb,
lib/shells/shell_base/output.rb,
lib/shells/shell_base/prompt.rb,
lib/shells/shell_base/options.rb,
lib/shells/shell_base/interface.rb,
lib/shells/shell_base/regex_escape.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 and override the necessary interface methods.

Direct Known Subclasses

SerialShell, SshShell

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.

unbuffered_input

Defaults to false. If non-false, then input is sent one character at a time, otherwise input is sent in whole strings. If set to :echo, then input is sent one character at a time and the character must be echoed back from the shell before the next character will be sent.

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

Raises:

  • (NotImplementedError)


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
# File 'lib/shells/shell_base/options.rb', line 53

def initialize(options = {}, &block)

  # cannot instantiate a ShellBase

  raise NotImplementedError if self.class == Shells::ShellBase

  raise ArgumentError, '\'options\' must be a hash.' unless options.is_a?(Hash)

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

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

  self.options[:prompt] = '~~#' if self.options[:prompt] == ''

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

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

  self.orig_options = self.options  # sort of, we might provide helpers (like +change_quit+)


  run_hook :on_init

  # allow for backwards compatibility.

  if block_given?
    run &block
  end

end

Instance Attribute Details

#last_exit_codeObject

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



6
7
8
# File 'lib/shells/shell_base/exec.rb', line 6

def last_exit_code
  @last_exit_code
end

#optionsObject

The options provided to this shell.

This hash is read-only.



8
9
10
# File 'lib/shells/shell_base/options.rb', line 8

def options
  @options
end

#outputObject

Gets all of the output contents from the session.



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

def output
  @output
end

#stderrObject

Gets the STDERR contents from the session.



15
16
17
# File 'lib/shells/shell_base/output.rb', line 15

def stderr
  @stderr
end

#stdoutObject

Gets the STDOUT contents from the session.



10
11
12
# File 'lib/shells/shell_base/output.rb', line 10

def stdout
  @stdout
end

Class Method Details

.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


13
14
15
# File 'lib/shells/shell_base/debug.rb', line 13

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

Instance Method Details

#change_quit(quit_command) ⇒ Object

Allows you to change the :quit option inside of a session.

This is useful if you need to change the quit command for some reason. e.g. - Changing the command to “reboot”.

Returns the shell instance.

Raises:



103
104
105
106
107
# File 'lib/shells/shell_base/options.rb', line 103

def change_quit(quit_command)
  raise Shells::NotRunning unless running?
  self.options = options.dup.merge( quit: quit_command ).freeze
  self
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

Raises:



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
# File 'lib/shells/shell_base/exec.rb', line 51

def exec(command, options = {}, &block)
  raise Shells::NotRunning unless running?

  options ||= {}
  options = { timeout_error: true, get_output: 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 = ''

  merge_local_buffer do
    begin
      # buffer while also passing data to the supplied block.

      if block_given?
        buffer_output(&block)
      end

      command = command.to_s

      # send the command and wait for the prompt to return.

      debug 'Queueing command: ' + command
      queue_input 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.

        ret =
            if options[:get_output]
              debug 'Reading output of command...'
              command_output command, options[:command_is_echoed]
            else
              ''
            end

        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 || last_exit_code == :undefined
          end
        else
          self.last_exit_code = nil
        end
      else
        # A timeout occurred and timeout_error was set to false.

        self.last_exit_code = :timeout
        ret = output
      end

    ensure
      # return buffering to normal.

      if block_given?
        buffer_output
      end

    end
  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.



116
117
118
119
120
# File 'lib/shells/shell_base/exec.rb', line 116

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.



126
127
128
129
# File 'lib/shells/shell_base/exec.rb', line 126

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

#inspectObject



13
14
15
# File 'lib/shells/shell_base.rb', line 13

def inspect
  "#<#{self.class}:0x#{object_id.to_s(16).rjust(12,'0')} #{options.reject{|k,v| k == :password}.inspect}>"
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.



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

def line_ending
  "\n"
end

#read_file(path) ⇒ Object

Reads from a file on the device.



135
136
137
# File 'lib/shells/shell_base/interface.rb', line 135

def read_file(path)
  nil
end

#run(&block) ⇒ Object

Runs a shell session.

The block provided will be run asynchronously with the shell.

Returns the shell instance.



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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/shells/shell_base/run.rb', line 55

def run(&block)
  sync do
    raise Shells::AlreadyRunning if running?
    self.run_flag = true
  end

  begin
    run_hook :on_before_run

    debug 'Connecting...'
    connect

    debug 'Starting output buffering...'
    buffer_output

    debug 'Starting session thread...'
    self.session_thread = Thread.start(self) do |sh|
      begin
        begin
          debug 'Executing setup...'
          sh.instance_eval { setup }
          debug 'Executing block...'
          block.call sh
        ensure
          debug 'Executing teardown...'
          sh.instance_eval { teardown }
        end
      rescue Shells::QuitNow
        # just exit the session.

      rescue =>e
        # if the exception is handled by the hook no further processing is required, otherwise we store the exception

        # to propagate it in the main thread.

        unless sh.run_hook(:on_exception, e) == :break
          sh.sync { sh.instance_eval { self.session_exception = e } }
        end
      end
    end

    # process the input buffer while the thread is alive and the shell is active.

    debug 'Entering IO loop...'
    io_loop do
      if active?
        begin
          if session_thread.status    # not dead

            # process input from the session.

            unless wait_for_output
              inp = next_input
              if inp
                send_data inp
                self.wait_for_output = (options[:unbuffered_input] == :echo)
              end
            end

            # continue running the IO loop

            true
          elsif session_exception
            # propagate the exception.

            raise session_exception.class, session_exception.message, session_exception.backtrace
          else
            # the thread has exited, but no exception exists.

            # regardless, the IO loop should now exit.

            false
          end
        rescue IOError
          if ignore_io_error
            # we were (sort of) expecting the IO error, so just tell the IO loop to exit.

            false
          else
            raise
          end
        end
      else
        # the shell session is no longer active, tell the IO loop to exit.

        false
      end
    end
  rescue
    # when an error occurs, try to disconnect, but ignore any further errors.

    begin
      debug 'Disconnecting...'
      disconnect
    rescue
      # ignore

    end
    raise
  else
    # when no error occurs, try to disconnect and propagate any errors (unless we are ignoring IO errors).

    begin
      debug 'Disconnecting...'
      disconnect
    rescue IOError
      raise unless ignore_io_error
    end
  ensure
    # cleanup

    run_hook :on_after_run
    self.run_flag = false
  end

  self
end

#running?Boolean

Is the shell currently running?



15
16
17
# File 'lib/shells/shell_base/run.rb', line 15

def running?
  run_flag
end

#write_file(path, data) ⇒ Object

Writes to a file on the device.



141
142
143
# File 'lib/shells/shell_base/interface.rb', line 141

def write_file(path, data)
  false
end