Class: Shells::ShellBase
- Inherits:
-
Object
- Object
- Shells::ShellBase
- 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
Instance Attribute Summary collapse
-
#last_exit_code ⇒ Object
readonly
Gets the exit code from the last command if it was retrieved.
-
#options ⇒ Object
readonly
The options provided to this shell.
-
#output ⇒ Object
readonly
Gets all of the output contents from the session.
-
#stderr ⇒ Object
readonly
Gets the STDERR contents from the session.
-
#stdout ⇒ Object
readonly
Gets the STDOUT contents from the session.
Class Method Summary collapse
-
.on_debug(proc = nil, &block) ⇒ Object
Sets the code to be run when debug messages are processed.
Instance Method Summary collapse
-
#change_quit(quit_command) ⇒ Object
Allows you to change the :quit option inside of a session.
-
#exec(command, options = {}, &block) ⇒ Object
Executes a command during the shell session.
-
#exec_for_code(command, options = {}, &block) ⇒ Object
Executes a command specifically for the exit code.
-
#exec_ignore_code(command, options = {}, &block) ⇒ Object
Executes a command ignoring any exit code.
-
#initialize(options = {}, &block) ⇒ ShellBase
constructor
Initializes the shell with the supplied options.
- #inspect ⇒ Object
-
#line_ending ⇒ Object
Defines the line ending used to terminate commands sent to the shell.
-
#read_file(path) ⇒ Object
Reads from a file on the device.
-
#run(&block) ⇒ Object
Runs a shell session.
-
#running? ⇒ Boolean
Is the shell currently running?.
-
#write_file(path, data) ⇒ Object
Writes to a file on the device.
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.
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( = {}, &block) # cannot instantiate a ShellBase raise NotImplementedError if self.class == Shells::ShellBase raise ArgumentError, '\'options\' must be a hash.' unless .is_a?(Hash) self. = { prompt: '~~#', retrieve_exit_code: false, on_non_zero_exit_code: :ignore, silence_timeout: 0, command_timeout: 0, unbuffered_input: false }.merge( .inject({}){ |m,(k,v)| m[k.to_sym] = v; m } ) self.[:prompt] = self.[:prompt] .to_s.strip .gsub('!', '#') .gsub('$', '#') .gsub('\\', '.') .gsub('/', '.') .gsub('"', '-') .gsub('\'', '-') self.[:prompt] = '~~#' if self.[:prompt] == '' raise Shells::InvalidOption, ':on_non_zero_exit_code must be :ignore or :raise.' unless [:ignore, :raise].include?(self.[:on_non_zero_exit_code]) self..freeze # no more changes to options now. self. = self. # 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_code ⇒ Object
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 |
#options ⇒ Object
The options provided to this shell.
This hash is read-only.
8 9 10 |
# File 'lib/shells/shell_base/options.rb', line 8 def end |
#output ⇒ Object
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 |
#stderr ⇒ Object
Gets the STDERR contents from the session.
15 16 17 |
# File 'lib/shells/shell_base/output.rb', line 15 def stderr @stderr end |
#stdout ⇒ Object
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.
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. = .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
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, = {}, &block) raise Shells::NotRunning unless running? ||= {} = { timeout_error: true, get_output: true }.merge() = self..merge(.inject({}) { |m,(k,v)| m[k.to_sym] = v; m }) [:retrieve_exit_code] = self.[:retrieve_exit_code] if [:retrieve_exit_code] == :default [:on_non_zero_exit_code] = self.[:on_non_zero_exit_code] unless [:raise, :ignore].include?([:on_non_zero_exit_code]) [:silence_timeout] = self.[:silence_timeout] if [:silence_timeout] == :default [:command_timeout] = self.[:command_timeout] if [:command_timeout] == :default [:command_is_echoed] = true if [: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([:silence_timeout], [:command_timeout], [:timeout_error]) # get the output of the command, minus the trailing prompt. ret = if [:get_output] debug 'Reading output of command...' command_output command, [:command_is_echoed] else '' end if [:retrieve_exit_code] self.last_exit_code = get_exit_code if [: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, = {}, &block) = ( || {}).merge(retrieve_exit_code: true, on_non_zero_exit_code: :ignore) exec command, , &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, = {}, &block) = ( || {}).merge(retrieve_exit_code: false, on_non_zero_exit_code: :ignore) exec command, , &block end |
#inspect ⇒ Object
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_ending ⇒ Object
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 = ([:unbuffered_input] == :echo) end end # continue running the IO loop true elsif session_exception # propagate the exception. raise session_exception.class, session_exception., 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 |