shells 0.1.23 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|