shells 0.1.23 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +8 -10
- data/bin/console +2 -4
- data/bin/test-client +202 -0
- data/lib/shells.rb +6 -10
- data/lib/shells/bash_common.rb +17 -7
- data/lib/shells/errors.rb +25 -4
- data/lib/shells/pf_sense_common.rb +85 -273
- data/lib/shells/pf_shell_wrapper.rb +270 -0
- data/lib/shells/serial_bash_shell.rb +65 -0
- data/lib/shells/{pf_sense_serial_session.rb → serial_pf_sense_shell.rb} +16 -14
- data/lib/shells/{serial_session.rb → serial_shell.rb} +66 -78
- data/lib/shells/shell_base.rb +17 -867
- data/lib/shells/shell_base/debug.rb +37 -0
- data/lib/shells/shell_base/exec.rb +175 -0
- data/lib/shells/shell_base/hooks.rb +83 -0
- data/lib/shells/shell_base/input.rb +50 -0
- data/lib/shells/shell_base/interface.rb +149 -0
- data/lib/shells/shell_base/options.rb +111 -0
- data/lib/shells/shell_base/output.rb +217 -0
- data/lib/shells/shell_base/prompt.rb +141 -0
- data/lib/shells/shell_base/regex_escape.rb +23 -0
- data/lib/shells/shell_base/run.rb +188 -0
- data/lib/shells/shell_base/sync.rb +24 -0
- data/lib/shells/ssh_bash_shell.rb +71 -0
- data/lib/shells/{pf_sense_ssh_session.rb → ssh_pf_sense_shell.rb} +16 -14
- data/lib/shells/ssh_shell.rb +215 -0
- data/lib/shells/version.rb +1 -1
- data/shells.gemspec +1 -0
- metadata +35 -6
- data/lib/shells/ssh_session.rb +0 -249
@@ -0,0 +1,37 @@
|
|
1
|
+
module Shells
|
2
|
+
class ShellBase
|
3
|
+
|
4
|
+
##
|
5
|
+
# Sets the code to be run when debug messages are processed.
|
6
|
+
#
|
7
|
+
# The code will receive the debug message as an argument.
|
8
|
+
#
|
9
|
+
# on_debug do |msg|
|
10
|
+
# puts msg
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
def self.on_debug(proc = nil, &block)
|
14
|
+
add_hook :on_debug, proc, &block
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
##
|
20
|
+
# Processes a debug message.
|
21
|
+
def self.debug(msg) #:doc:
|
22
|
+
run_static_hook :on_debug, msg
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# Processes a debug message for an instance.
|
27
|
+
#
|
28
|
+
# This is processed synchronously.
|
29
|
+
def debug(msg) #:doc:
|
30
|
+
if have_hook?(:on_debug)
|
31
|
+
sync { self.class.debug msg }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
module Shells
|
2
|
+
class ShellBase
|
3
|
+
|
4
|
+
##
|
5
|
+
# Gets the exit code from the last command if it was retrieved.
|
6
|
+
attr_accessor :last_exit_code
|
7
|
+
private :last_exit_code=
|
8
|
+
|
9
|
+
add_hook :on_before_run do |sh|
|
10
|
+
sh.instance_eval do
|
11
|
+
self.last_exit_code = nil
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
add_hook :on_after_run do |sh|
|
16
|
+
sh.instance_eval do
|
17
|
+
self.last_exit_code = nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# Executes a command during the shell session.
|
23
|
+
#
|
24
|
+
# If called outside of the +new+ block, this will raise an error.
|
25
|
+
#
|
26
|
+
# The +command+ is the command to execute in the shell.
|
27
|
+
#
|
28
|
+
# The +options+ can be used to override the exit code behavior.
|
29
|
+
# In all cases, the :default option is the same as not providing the option and will cause +exec+
|
30
|
+
# to inherit the option from the shell's options.
|
31
|
+
#
|
32
|
+
# +retrieve_exit_code+::
|
33
|
+
# This can be one of :default, true, or false.
|
34
|
+
# +on_non_zero_exit_code+::
|
35
|
+
# This can be on ot :default, :ignore, or :raise.
|
36
|
+
# +silence_timeout+::
|
37
|
+
# This can be :default or the number of seconds to wait in silence before timing out.
|
38
|
+
# +command_timeout+::
|
39
|
+
# This can be :default or the maximum number of seconds to wait for a command to finish before timing out.
|
40
|
+
#
|
41
|
+
# If provided, the +block+ is a chunk of code that will be processed every time the
|
42
|
+
# shell receives output from the program. If the block returns a string, the string
|
43
|
+
# will be sent to the shell. This can be used to monitor processes or monitor and
|
44
|
+
# interact with processes. The +block+ is optional.
|
45
|
+
#
|
46
|
+
# shell.exec('sudo -p "password:" nginx restart') do |data,type|
|
47
|
+
# return 'super-secret' if /password:$/.match(data)
|
48
|
+
# nil
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
def exec(command, options = {}, &block)
|
52
|
+
raise Shells::NotRunning unless running?
|
53
|
+
|
54
|
+
options ||= {}
|
55
|
+
options = { timeout_error: true, get_output: true }.merge(options)
|
56
|
+
options = self.options.merge(options.inject({}) { |m,(k,v)| m[k.to_sym] = v; m })
|
57
|
+
options[:retrieve_exit_code] = self.options[:retrieve_exit_code] if options[:retrieve_exit_code] == :default
|
58
|
+
options[:on_non_zero_exit_code] = self.options[:on_non_zero_exit_code] unless [:raise, :ignore].include?(options[:on_non_zero_exit_code])
|
59
|
+
options[:silence_timeout] = self.options[:silence_timeout] if options[:silence_timeout] == :default
|
60
|
+
options[:command_timeout] = self.options[:command_timeout] if options[:command_timeout] == :default
|
61
|
+
options[:command_is_echoed] = true if options[:command_is_echoed].nil?
|
62
|
+
ret = ''
|
63
|
+
|
64
|
+
merge_local_buffer do
|
65
|
+
begin
|
66
|
+
# buffer while also passing data to the supplied block.
|
67
|
+
if block_given?
|
68
|
+
buffer_output(&block)
|
69
|
+
end
|
70
|
+
|
71
|
+
command = command.to_s
|
72
|
+
|
73
|
+
# send the command and wait for the prompt to return.
|
74
|
+
debug 'Queueing command: ' + command
|
75
|
+
queue_input command + line_ending
|
76
|
+
if wait_for_prompt(options[:silence_timeout], options[:command_timeout], options[:timeout_error])
|
77
|
+
# get the output of the command, minus the trailing prompt.
|
78
|
+
ret =
|
79
|
+
if options[:get_output]
|
80
|
+
debug 'Reading output of command...'
|
81
|
+
command_output command, options[:command_is_echoed]
|
82
|
+
else
|
83
|
+
''
|
84
|
+
end
|
85
|
+
|
86
|
+
if options[:retrieve_exit_code]
|
87
|
+
self.last_exit_code = get_exit_code
|
88
|
+
if options[:on_non_zero_exit_code] == :raise
|
89
|
+
raise NonZeroExitCode.new(last_exit_code) unless last_exit_code == 0 || last_exit_code == :undefined
|
90
|
+
end
|
91
|
+
else
|
92
|
+
self.last_exit_code = nil
|
93
|
+
end
|
94
|
+
else
|
95
|
+
# A timeout occurred and timeout_error was set to false.
|
96
|
+
self.last_exit_code = :timeout
|
97
|
+
ret = output
|
98
|
+
end
|
99
|
+
|
100
|
+
ensure
|
101
|
+
# return buffering to normal.
|
102
|
+
if block_given?
|
103
|
+
buffer_output
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
ret
|
110
|
+
end
|
111
|
+
|
112
|
+
##
|
113
|
+
# Executes a command specifically for the exit code.
|
114
|
+
#
|
115
|
+
# Does not return the output of the command, only the exit code.
|
116
|
+
def exec_for_code(command, options = {}, &block)
|
117
|
+
options = (options || {}).merge(retrieve_exit_code: true, on_non_zero_exit_code: :ignore)
|
118
|
+
exec command, options, &block
|
119
|
+
last_exit_code
|
120
|
+
end
|
121
|
+
|
122
|
+
##
|
123
|
+
# Executes a command ignoring any exit code.
|
124
|
+
#
|
125
|
+
# Returns the output of the command and does not even retrieve the exit code.
|
126
|
+
def exec_ignore_code(command, options = {}, &block)
|
127
|
+
options = (options || {}).merge(retrieve_exit_code: false, on_non_zero_exit_code: :ignore)
|
128
|
+
exec command, options, &block
|
129
|
+
end
|
130
|
+
|
131
|
+
protected
|
132
|
+
|
133
|
+
##
|
134
|
+
# Gets the output from a command.
|
135
|
+
def command_output(command, expect_command = true) #:doc:
|
136
|
+
# get everything except for the ending prompt.
|
137
|
+
ret =
|
138
|
+
if (prompt_pos = (output =~ prompt_match))
|
139
|
+
output[0...prompt_pos]
|
140
|
+
else
|
141
|
+
output
|
142
|
+
end
|
143
|
+
|
144
|
+
if expect_command
|
145
|
+
command_regex = command_match(command)
|
146
|
+
|
147
|
+
# Go until we run out of data or we find one of the possible command starts.
|
148
|
+
# Note that we EXPECT the command to the first line of the output from the command because we expect the
|
149
|
+
# shell to echo it back to us.
|
150
|
+
result_cmd,_,result_data = ret.partition("\n")
|
151
|
+
until result_data.to_s.strip == '' || result_cmd.strip =~ command_regex
|
152
|
+
result_cmd,_,result_data = result_data.partition("\n")
|
153
|
+
end
|
154
|
+
|
155
|
+
if result_cmd.nil? || !(result_cmd =~ command_regex)
|
156
|
+
STDERR.puts "SHELL WARNING: Failed to match #{command_regex.inspect}."
|
157
|
+
end
|
158
|
+
|
159
|
+
result_data
|
160
|
+
else
|
161
|
+
ret
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
def command_match(command)
|
168
|
+
p = regex_escape options[:prompt]
|
169
|
+
c = regex_escape command
|
170
|
+
/\A(?:#{p}\s*)?#{c}[ \t]*\z/
|
171
|
+
end
|
172
|
+
|
173
|
+
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Shells
|
2
|
+
class ShellBase
|
3
|
+
|
4
|
+
private
|
5
|
+
|
6
|
+
def self.hooks
|
7
|
+
@hooks ||= {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.parent_hooks(name)
|
11
|
+
superclass.respond_to?(:all_hooks, true) ? superclass.send(:all_hooks, name) : []
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.all_hooks(name)
|
15
|
+
parent_hooks(name) + (hooks[name] || [])
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
##
|
21
|
+
# Adds a hook method to the class.
|
22
|
+
#
|
23
|
+
# A hook method should return :break if it wants to cancel executing any other hook methods in the chain.
|
24
|
+
def self.add_hook(hook_name, proc = nil, &block) #:doc:
|
25
|
+
hooks[hook_name] ||= []
|
26
|
+
|
27
|
+
if proc.respond_to?(:call)
|
28
|
+
hooks[hook_name] << proc
|
29
|
+
elsif proc.is_a?(Symbol) || proc.is_a?(String)
|
30
|
+
if self.respond_to?(proc, true)
|
31
|
+
hooks[hook_name] << method(proc.to_sym)
|
32
|
+
end
|
33
|
+
elsif proc
|
34
|
+
raise ArgumentError, 'proc must respond to :call method or be the name of a static method in this class'
|
35
|
+
end
|
36
|
+
|
37
|
+
if block
|
38
|
+
hooks[hook_name] << block
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
|
45
|
+
##
|
46
|
+
# Runs a hook statically.
|
47
|
+
#
|
48
|
+
# The arguments supplied are passed to the hook methods directly.
|
49
|
+
#
|
50
|
+
# Return false unless the hook was executed. Returns :break if one of the hook methods returns :break.
|
51
|
+
def self.run_static_hook(hook_name, *args)
|
52
|
+
list = all_hooks(hook_name)
|
53
|
+
list.each do |hook|
|
54
|
+
result = hook.call(*args)
|
55
|
+
return :break if result == :break
|
56
|
+
end
|
57
|
+
list.any?
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# Runs a hook in the current shell instance.
|
62
|
+
#
|
63
|
+
# The hook method is passed the shell as the first argument then the arguments passed to this method.
|
64
|
+
#
|
65
|
+
# Return false unless the hook was executed. Returns :break if one of the hook methods returns :break.
|
66
|
+
def run_hook(hook_name, *args)
|
67
|
+
list = self.class.all_hooks(hook_name)
|
68
|
+
shell = self
|
69
|
+
list.each do |hook|
|
70
|
+
result = hook.call(shell, *args)
|
71
|
+
return :break if result == :break
|
72
|
+
end
|
73
|
+
list.any?
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# Returns true if there are any hooks to run.
|
78
|
+
def have_hook?(hook_name)
|
79
|
+
self.class.all_hooks(hook_name).any?
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Shells
|
2
|
+
class ShellBase
|
3
|
+
|
4
|
+
attr_accessor :input_fifo
|
5
|
+
private :input_fifo, :input_fifo
|
6
|
+
|
7
|
+
add_hook :on_before_run do |sh|
|
8
|
+
sh.instance_eval do
|
9
|
+
self.input_fifo = []
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
add_hook :on_after_run do |sh|
|
14
|
+
sh.instance_eval do
|
15
|
+
self.input_fifo = nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Defines the line ending used to terminate commands sent to the shell.
|
21
|
+
#
|
22
|
+
# The default is "\n". If you need "\r\n", "\r", or some other value, simply override this function.
|
23
|
+
def line_ending
|
24
|
+
"\n"
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
##
|
30
|
+
# Adds input to be sent to the shell.
|
31
|
+
def queue_input(data) #:doc:
|
32
|
+
sync do
|
33
|
+
if options[:unbuffered_input]
|
34
|
+
data = data.chars
|
35
|
+
input_fifo.push *data
|
36
|
+
else
|
37
|
+
input_fifo.push data
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def next_input
|
45
|
+
sync { input_fifo.shift }
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module Shells
|
2
|
+
class ShellBase
|
3
|
+
|
4
|
+
protected
|
5
|
+
|
6
|
+
##
|
7
|
+
# Sets up the shell session.
|
8
|
+
#
|
9
|
+
# This method is called after connecting the shell before the session block is run.
|
10
|
+
#
|
11
|
+
# By default this method will wait for the prompt to appear in the output.
|
12
|
+
#
|
13
|
+
# If you need to set the prompt, you would want to do it here.
|
14
|
+
def setup #:doc:
|
15
|
+
setup_prompt
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# Sets up the prompt for the shell session.
|
20
|
+
#
|
21
|
+
# By default this method will wait for the prompt to appear in the output.
|
22
|
+
#
|
23
|
+
# If you need to set the prompt, you would want to do it here.
|
24
|
+
def setup_prompt #:doc:
|
25
|
+
wait_for_prompt 30, 30, true
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
##
|
30
|
+
# Tears down the shell session.
|
31
|
+
#
|
32
|
+
# This method is called after the session block is run before disconnecting the shell.
|
33
|
+
#
|
34
|
+
# The default implementation simply sends the quit command to the shell and waits up to 1 second for a result.
|
35
|
+
#
|
36
|
+
# This method will be called even if an exception is raised during the session.
|
37
|
+
def teardown #:doc:
|
38
|
+
unless options[:quit].to_s.strip == ''
|
39
|
+
self.ignore_io_error = true
|
40
|
+
exec_ignore_code options[:quit], command_timeout: 1, timeout_error: false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Connects to the shell.
|
46
|
+
#
|
47
|
+
# You must define this method in your subclass.
|
48
|
+
def connect #:doc:
|
49
|
+
raise ::NotImplementedError
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Disconnects from the shell.
|
54
|
+
#
|
55
|
+
# You must define this method in your subclass.
|
56
|
+
# This method will always be called, even if an exception occurs during the session.
|
57
|
+
def disconnect #:doc:
|
58
|
+
raise ::NotImplementedError
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# Determines if the shell is currently active.
|
63
|
+
#
|
64
|
+
# You must define this method in your subclass.
|
65
|
+
def active? #:doc:
|
66
|
+
raise ::NotImplementedError
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Runs the IO loop on the shell while the block returns true.
|
71
|
+
#
|
72
|
+
# You must define this method in your subclass.
|
73
|
+
# It should block for as little time as necessary before yielding to the block.
|
74
|
+
def io_loop(&block) #:doc:
|
75
|
+
raise ::NotImplementedError
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Sends data to the shell.
|
80
|
+
#
|
81
|
+
# You must define this method in your subclass.
|
82
|
+
#
|
83
|
+
# It is important that this method not be called directly outside of the +run+ method.
|
84
|
+
# Use +queue_input+ to send data to the shell so that it can be handled in a synchronous manner.
|
85
|
+
def send_data(data) #:doc:
|
86
|
+
raise ::NotImplementedError
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
# Register a callback to run when stdout data is received.
|
91
|
+
#
|
92
|
+
# The block will be passed the data received.
|
93
|
+
#
|
94
|
+
# You must define this method in your subclass and it should set a hook to be called when data is received.
|
95
|
+
#
|
96
|
+
# def stdout_received
|
97
|
+
# @conn.on_stdout do |data|
|
98
|
+
# yield data
|
99
|
+
# end
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
def stdout_received(&block) #:doc:
|
103
|
+
raise ::NotImplementedError
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# Register a callback to run when stderr data is received.
|
108
|
+
#
|
109
|
+
# The block will be passed the data received.
|
110
|
+
#
|
111
|
+
# You must define this method in your subclass and it should set a hook to be called when data is received.
|
112
|
+
#
|
113
|
+
# def stderr_received
|
114
|
+
# @conn.on_stderr do |data|
|
115
|
+
# yield data
|
116
|
+
# end
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
def stderr_received(&block) #:doc:
|
120
|
+
raise ::NotImplementedError
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
# Gets the exit code from the last command.
|
125
|
+
#
|
126
|
+
# You must define this method in your subclass to utilize exit codes.
|
127
|
+
def get_exit_code #:doc:
|
128
|
+
self.last_exit_code = :undefined
|
129
|
+
end
|
130
|
+
|
131
|
+
public
|
132
|
+
|
133
|
+
##
|
134
|
+
# Reads from a file on the device.
|
135
|
+
def read_file(path)
|
136
|
+
nil
|
137
|
+
end
|
138
|
+
|
139
|
+
##
|
140
|
+
# Writes to a file on the device.
|
141
|
+
def write_file(path, data)
|
142
|
+
false
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
|
147
|
+
|
148
|
+
end
|
149
|
+
end
|