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,111 @@
|
|
1
|
+
module Shells
|
2
|
+
class ShellBase
|
3
|
+
|
4
|
+
##
|
5
|
+
# The options provided to this shell.
|
6
|
+
#
|
7
|
+
# This hash is read-only.
|
8
|
+
attr_accessor :options
|
9
|
+
private :options=
|
10
|
+
|
11
|
+
attr_accessor :orig_options
|
12
|
+
private :orig_options, :orig_options=
|
13
|
+
|
14
|
+
add_hook :on_after_run do |sh|
|
15
|
+
sh.instance_eval do
|
16
|
+
self.options = orig_options
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Validates the options provided to the class.
|
22
|
+
#
|
23
|
+
# You should define this method in your subclass.
|
24
|
+
def validate_options #:doc:
|
25
|
+
warn "The validate_options() method is not defined on the #{self.class} class."
|
26
|
+
end
|
27
|
+
protected :validate_options
|
28
|
+
|
29
|
+
|
30
|
+
##
|
31
|
+
# Initializes the shell with the supplied options.
|
32
|
+
#
|
33
|
+
# These options are common to all shells.
|
34
|
+
# +prompt+::
|
35
|
+
# Defaults to "~~#". Most special characters will be stripped.
|
36
|
+
# +retrieve_exit_code+::
|
37
|
+
# Defaults to false. Can also be true.
|
38
|
+
# +on_non_zero_exit_code+::
|
39
|
+
# Defaults to :ignore. Can also be :raise.
|
40
|
+
# +silence_timeout+::
|
41
|
+
# Defaults to 0.
|
42
|
+
# If greater than zero, will raise an error after waiting this many seconds for a prompt.
|
43
|
+
# +command_timeout+::
|
44
|
+
# Defaults to 0.
|
45
|
+
# If greater than zero, will raise an error after a command runs for this long without finishing.
|
46
|
+
# +unbuffered_input+::
|
47
|
+
# Defaults to false.
|
48
|
+
# If non-false, then input is sent one character at a time, otherwise input is sent in whole strings.
|
49
|
+
# If set to :echo, then input is sent one character at a time and the character must be echoed back
|
50
|
+
# from the shell before the next character will be sent.
|
51
|
+
#
|
52
|
+
# Please check the documentation for each shell class for specific shell options.
|
53
|
+
def initialize(options = {}, &block)
|
54
|
+
|
55
|
+
# cannot instantiate a ShellBase
|
56
|
+
raise NotImplementedError if self.class == Shells::ShellBase
|
57
|
+
|
58
|
+
raise ArgumentError, '\'options\' must be a hash.' unless options.is_a?(Hash)
|
59
|
+
|
60
|
+
self.options = {
|
61
|
+
prompt: '~~#',
|
62
|
+
retrieve_exit_code: false,
|
63
|
+
on_non_zero_exit_code: :ignore,
|
64
|
+
silence_timeout: 0,
|
65
|
+
command_timeout: 0,
|
66
|
+
unbuffered_input: false
|
67
|
+
}.merge( options.inject({}){ |m,(k,v)| m[k.to_sym] = v; m } )
|
68
|
+
|
69
|
+
self.options[:prompt] = self.options[:prompt]
|
70
|
+
.to_s.strip
|
71
|
+
.gsub('!', '#')
|
72
|
+
.gsub('$', '#')
|
73
|
+
.gsub('\\', '.')
|
74
|
+
.gsub('/', '.')
|
75
|
+
.gsub('"', '-')
|
76
|
+
.gsub('\'', '-')
|
77
|
+
|
78
|
+
self.options[:prompt] = '~~#' if self.options[:prompt] == ''
|
79
|
+
|
80
|
+
raise Shells::InvalidOption, ':on_non_zero_exit_code must be :ignore or :raise.' unless [:ignore, :raise].include?(self.options[:on_non_zero_exit_code])
|
81
|
+
|
82
|
+
validate_options
|
83
|
+
self.options.freeze # no more changes to options now.
|
84
|
+
self.orig_options = self.options # sort of, we might provide helpers (like +change_quit+)
|
85
|
+
|
86
|
+
run_hook :on_init
|
87
|
+
|
88
|
+
# allow for backwards compatibility.
|
89
|
+
if block_given?
|
90
|
+
run &block
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
##
|
97
|
+
# Allows you to change the :quit option inside of a session.
|
98
|
+
#
|
99
|
+
# This is useful if you need to change the quit command for some reason.
|
100
|
+
# e.g. - Changing the command to "reboot".
|
101
|
+
#
|
102
|
+
# Returns the shell instance.
|
103
|
+
def change_quit(quit_command)
|
104
|
+
raise Shells::NotRunning unless running?
|
105
|
+
self.options = options.dup.merge( quit: quit_command ).freeze
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
module Shells
|
2
|
+
class ShellBase
|
3
|
+
|
4
|
+
# used when the buffer is pushed/popped.
|
5
|
+
attr_accessor :output_stack
|
6
|
+
private :output_stack, :output_stack=
|
7
|
+
|
8
|
+
##
|
9
|
+
# Gets the STDOUT contents from the session.
|
10
|
+
attr_accessor :stdout
|
11
|
+
private :stdout=
|
12
|
+
|
13
|
+
##
|
14
|
+
# Gets the STDERR contents from the session.
|
15
|
+
attr_accessor :stderr
|
16
|
+
private :stderr=
|
17
|
+
|
18
|
+
##
|
19
|
+
# Gets all of the output contents from the session.
|
20
|
+
attr_accessor :output
|
21
|
+
private :output=
|
22
|
+
|
23
|
+
##
|
24
|
+
# Gets the last time output was received from the shell.
|
25
|
+
attr_accessor :last_output
|
26
|
+
protected :last_output
|
27
|
+
private :last_output=
|
28
|
+
|
29
|
+
##
|
30
|
+
# The character string we are expecting to be echoed back from the shell.
|
31
|
+
attr_accessor :wait_for_output
|
32
|
+
private :wait_for_output, :wait_for_output
|
33
|
+
|
34
|
+
add_hook :on_before_run do |sh|
|
35
|
+
sh.instance_eval do
|
36
|
+
self.output_stack = []
|
37
|
+
self.stdout = ''
|
38
|
+
self.stderr = ''
|
39
|
+
self.output = ''
|
40
|
+
self.last_output = Time.now
|
41
|
+
self.wait_for_output = false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
add_hook :on_after_run do |sh|
|
46
|
+
sh.instance_eval do
|
47
|
+
self.output_stack = nil
|
48
|
+
self.last_output = nil
|
49
|
+
self.wait_for_output = false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def strip_ansi_escape(data)
|
56
|
+
data
|
57
|
+
.gsub(/\e\[(\d+;?)*[ABCDEFGHfu]/, "\n") # any of the "set cursor position" CSI commands.
|
58
|
+
.gsub(/\e\[=?(\d+;?)*[A-Za-z]/,'') # \e[#;#;#A or \e[=#;#;#A basically all the CSI commands except ...
|
59
|
+
.gsub(/\e\[(\d+;"[^"]+";?)+p/, '') # \e[#;"A"p
|
60
|
+
.gsub(/\e[NOc]./,'?') # any of the alternate character set commands.
|
61
|
+
.gsub(/\e[P_\]^X][^\e\a]*(\a|(\e\\))/,'') # any string command
|
62
|
+
.gsub(/[\x00\x08\x0B\x0C\x0E-\x1F]/, '') # any non-printable characters (notice \x0A (LF) and \x0D (CR) are left as is).
|
63
|
+
.gsub("\t", ' ') # turn tabs into spaces.
|
64
|
+
end
|
65
|
+
|
66
|
+
def reduce_newlines(data)
|
67
|
+
data.gsub("\r\n", "\n").gsub(" \r", "").gsub("\r", "")
|
68
|
+
end
|
69
|
+
|
70
|
+
def append_stdout(data, &block)
|
71
|
+
# Combined output gets the prompts,
|
72
|
+
# but stdout will be without prompts.
|
73
|
+
data = reduce_newlines data
|
74
|
+
for_stdout = if (pos = (data =~ prompt_match))
|
75
|
+
data[0...pos]
|
76
|
+
else
|
77
|
+
data
|
78
|
+
end
|
79
|
+
|
80
|
+
sync do
|
81
|
+
self.stdout += for_stdout
|
82
|
+
self.output += data
|
83
|
+
self.wait_for_output = false
|
84
|
+
end
|
85
|
+
|
86
|
+
if block_given?
|
87
|
+
result = block.call(for_stdout, :stdout)
|
88
|
+
if result && result.is_a?(String)
|
89
|
+
queue_input(result + line_ending)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def append_stderr(data, &block)
|
95
|
+
data = reduce_newlines data
|
96
|
+
|
97
|
+
sync do
|
98
|
+
self.stderr += data
|
99
|
+
self.output += data
|
100
|
+
end
|
101
|
+
|
102
|
+
if block_given?
|
103
|
+
result = block.call(data, :stderr)
|
104
|
+
if result && result.is_a?(String)
|
105
|
+
queue_input(result + line_ending)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
protected
|
112
|
+
|
113
|
+
##
|
114
|
+
# Sets the block to call when data is received.
|
115
|
+
#
|
116
|
+
# If no block is provided, then the shell will simply log all output from the program.
|
117
|
+
# If a block is provided, it will be passed the data as it is received. If the block
|
118
|
+
# returns a string, then that string will be sent to the shell.
|
119
|
+
#
|
120
|
+
# This method is called internally in the +exec+ method, but there may be legitimate use
|
121
|
+
# cases outside of that method as well.
|
122
|
+
def buffer_output(&block) #:doc:
|
123
|
+
raise Shells::NotRunning unless running?
|
124
|
+
block ||= Proc.new { }
|
125
|
+
stdout_received do |data|
|
126
|
+
self.last_output = Time.now
|
127
|
+
append_stdout strip_ansi_escape(data), &block
|
128
|
+
end
|
129
|
+
stderr_received do |data|
|
130
|
+
self.last_output = Time.now
|
131
|
+
append_stderr strip_ansi_escape(data), &block
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Executes the code block with a local output buffer, merging the local buffer into the parent buffer upon completion.
|
137
|
+
def merge_local_buffer(&block) #:doc:
|
138
|
+
push_buffer
|
139
|
+
begin
|
140
|
+
yield
|
141
|
+
ensure
|
142
|
+
pop_merge_buffer
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
##
|
147
|
+
# Executes the code block with a local output buffer, discarding the local buffer upon completion.
|
148
|
+
def discard_local_buffer(&block) #:doc:
|
149
|
+
push_buffer
|
150
|
+
begin
|
151
|
+
yield
|
152
|
+
ensure
|
153
|
+
pop_discard_buffer
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
##
|
160
|
+
# Pushes the buffers for output capture.
|
161
|
+
#
|
162
|
+
# This method is called internally in the +exec+ method, but there may be legitimate use
|
163
|
+
# cases outside of that method as well.
|
164
|
+
def push_buffer
|
165
|
+
raise Shells::NotRunning unless running?
|
166
|
+
# push the buffer so we can get the output of a command.
|
167
|
+
debug 'Pushing buffer >>'
|
168
|
+
sync do
|
169
|
+
output_stack.push [ stdout, stderr, output ]
|
170
|
+
self.stdout = ''
|
171
|
+
self.stderr = ''
|
172
|
+
self.output = ''
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
##
|
177
|
+
# Pops the buffers and merges the captured output.
|
178
|
+
#
|
179
|
+
# This method is called internally in the +exec+ method, but there may be legitimate use
|
180
|
+
# cases outside of that method as well.
|
181
|
+
def pop_merge_buffer
|
182
|
+
raise Shells::NotRunning unless running?
|
183
|
+
# almost a standard pop, however we want to merge history with current.
|
184
|
+
debug 'Merging buffer <<'
|
185
|
+
sync do
|
186
|
+
hist_stdout, hist_stderr, hist_output = (output_stack.pop || [])
|
187
|
+
if hist_stdout
|
188
|
+
self.stdout = hist_stdout + stdout
|
189
|
+
end
|
190
|
+
if hist_stderr
|
191
|
+
self.stderr = hist_stderr + stderr
|
192
|
+
end
|
193
|
+
if hist_output
|
194
|
+
self.output = hist_output + output
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
##
|
200
|
+
# Pops the buffers and discards the captured output.
|
201
|
+
#
|
202
|
+
# This method is used internally in the +get_exit_code+ method, but there may be legitimate use
|
203
|
+
# cases outside of that method as well.
|
204
|
+
def pop_discard_buffer
|
205
|
+
raise Shells::NotRunning unless running?
|
206
|
+
# a standard pop discarding current data and retrieving the history.
|
207
|
+
debug 'Discarding buffer <<'
|
208
|
+
sync do
|
209
|
+
hist_stdout, hist_stderr, hist_output = (output_stack.pop || [])
|
210
|
+
self.stdout = hist_stdout || ''
|
211
|
+
self.stderr = hist_stderr || ''
|
212
|
+
self.output = hist_output || ''
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|
217
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module Shells
|
2
|
+
class ShellBase
|
3
|
+
|
4
|
+
private
|
5
|
+
|
6
|
+
def prompt_match
|
7
|
+
@prompt_match
|
8
|
+
end
|
9
|
+
|
10
|
+
def prompt_match=(value)
|
11
|
+
# allow for trailing spaces or tabs, but no other whitespace.
|
12
|
+
@prompt_match =
|
13
|
+
if value.nil?
|
14
|
+
nil
|
15
|
+
elsif value.is_a?(::Regexp)
|
16
|
+
value
|
17
|
+
else
|
18
|
+
/#{regex_escape value.to_s}[ \t]*$/
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
add_hook :on_before_run do |sh|
|
23
|
+
sh.instance_eval do
|
24
|
+
self.prompt_match = options[:prompt]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
add_hook :on_after_run do |sh|
|
29
|
+
sh.instance_eval do
|
30
|
+
self.prompt_match = nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
##
|
37
|
+
# Waits for the prompt to appear at the end of the output.
|
38
|
+
#
|
39
|
+
# Once the prompt appears, new input can be sent to the shell.
|
40
|
+
# This is automatically called in +exec+ so you would only need
|
41
|
+
# to call it directly if you were sending data manually to the
|
42
|
+
# shell.
|
43
|
+
#
|
44
|
+
# This method is used internally in the +exec+ method, but there may be legitimate use cases
|
45
|
+
# outside of that method as well.
|
46
|
+
def wait_for_prompt(silence_timeout = nil, command_timeout = nil, timeout_error = true) #:doc:
|
47
|
+
raise Shells::NotRunning unless running?
|
48
|
+
|
49
|
+
silence_timeout ||= options[:silence_timeout]
|
50
|
+
command_timeout ||= options[:command_timeout]
|
51
|
+
|
52
|
+
# when did we send a NL and how many have we sent while waiting for output?
|
53
|
+
nudged_at = nil
|
54
|
+
nudge_count = 0
|
55
|
+
|
56
|
+
silence_timeout = silence_timeout.to_s.to_f unless silence_timeout.is_a?(Numeric)
|
57
|
+
nudge_seconds =
|
58
|
+
if silence_timeout > 0
|
59
|
+
(silence_timeout / 3.0) # we want to nudge twice before officially timing out.
|
60
|
+
else
|
61
|
+
0
|
62
|
+
end
|
63
|
+
|
64
|
+
# if there is a limit for the command timeout, then set the absolute timeout for the loop.
|
65
|
+
command_timeout = command_timeout.to_s.to_f unless command_timeout.is_a?(Numeric)
|
66
|
+
timeout =
|
67
|
+
if command_timeout > 0
|
68
|
+
Time.now + command_timeout
|
69
|
+
else
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
|
73
|
+
# loop until the output matches the prompt regex.
|
74
|
+
# if something gets output async server side, the silence timeout will be handy in getting the shell to reappear.
|
75
|
+
# a match while waiting for output is invalid, so by requiring that flag to be false this should work with
|
76
|
+
# unbuffered input as well.
|
77
|
+
until output =~ prompt_match && !wait_for_output
|
78
|
+
# hint that we need to let another thread run.
|
79
|
+
Thread.pass
|
80
|
+
|
81
|
+
last_response = last_output
|
82
|
+
|
83
|
+
# Do we need to nudge the shell?
|
84
|
+
if nudge_seconds > 0 && (Time.now - last_response) > nudge_seconds
|
85
|
+
nudge_count = (nudged_at.nil? || nudged_at < last_response) ? 1 : (nudge_count + 1)
|
86
|
+
|
87
|
+
# Have we previously nudged the shell?
|
88
|
+
if nudge_count > 2 # we timeout on the third nudge.
|
89
|
+
raise Shells::SilenceTimeout if timeout_error
|
90
|
+
debug ' > silence timeout'
|
91
|
+
return false
|
92
|
+
else
|
93
|
+
nudged_at = Time.now
|
94
|
+
|
95
|
+
queue_input line_ending
|
96
|
+
|
97
|
+
# wait a bit longer...
|
98
|
+
self.last_output = nudged_at
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# honor the absolute timeout.
|
103
|
+
if timeout && Time.now > timeout
|
104
|
+
raise Shells::CommandTimeout if timeout_error
|
105
|
+
debug ' > command timeout'
|
106
|
+
return false
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# make sure there is a newline before the prompt, just to keep everything clean.
|
111
|
+
pos = (output =~ prompt_match)
|
112
|
+
if output[pos - 1] != "\n"
|
113
|
+
# no newline before prompt, fix that.
|
114
|
+
self.output = output[0...pos] + "\n" + output[pos..-1]
|
115
|
+
end
|
116
|
+
|
117
|
+
# make sure there is a newline at the end of STDOUT content buffer.
|
118
|
+
if stdout[-1] != "\n"
|
119
|
+
# no newline at end, fix that.
|
120
|
+
self.stdout += "\n"
|
121
|
+
end
|
122
|
+
|
123
|
+
true
|
124
|
+
end
|
125
|
+
|
126
|
+
##
|
127
|
+
# Sets the prompt to the value temporarily for execution of the code block.
|
128
|
+
def temporary_prompt(prompt) #:doc:
|
129
|
+
raise Shells::NotRunning unless running?
|
130
|
+
old_prompt = prompt_match
|
131
|
+
begin
|
132
|
+
self.prompt_match = prompt
|
133
|
+
yield if block_given?
|
134
|
+
ensure
|
135
|
+
self.prompt_match = old_prompt
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
end
|
141
|
+
end
|