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,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
|