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
data/lib/shells/shell_base.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'shells/errors'
|
2
|
+
require 'thread'
|
2
3
|
|
3
4
|
module Shells
|
4
5
|
|
@@ -6,878 +7,27 @@ module Shells
|
|
6
7
|
# Provides a base interface for all shells to build on.
|
7
8
|
#
|
8
9
|
# Instantiating this class will raise an error.
|
9
|
-
# All shell sessions should inherit this class.
|
10
|
+
# All shell sessions should inherit this class and override the necessary interface methods.
|
10
11
|
class ShellBase
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
class QuitNow < Exception
|
15
|
-
|
16
|
-
end
|
17
|
-
|
18
|
-
##
|
19
|
-
# The options provided to this shell.
|
20
|
-
attr_reader :options
|
21
|
-
|
22
|
-
##
|
23
|
-
# Gets the exit code from the last command if it was retrieved.
|
24
|
-
attr_accessor :last_exit_code
|
25
|
-
|
26
|
-
##
|
27
|
-
# Initializes the shell with the supplied options.
|
28
|
-
#
|
29
|
-
# These options are common to all shells.
|
30
|
-
# +prompt+::
|
31
|
-
# Defaults to "~~#". Most special characters will be stripped.
|
32
|
-
# +retrieve_exit_code+::
|
33
|
-
# Defaults to false. Can also be true.
|
34
|
-
# +on_non_zero_exit_code+::
|
35
|
-
# Defaults to :ignore. Can also be :raise.
|
36
|
-
# +silence_timeout+::
|
37
|
-
# Defaults to 0.
|
38
|
-
# If greater than zero, will raise an error after waiting this many seconds for a prompt.
|
39
|
-
# +command_timeout+::
|
40
|
-
# Defaults to 0.
|
41
|
-
# If greater than zero, will raise an error after a command runs for this long without finishing.
|
42
|
-
#
|
43
|
-
# Please check the documentation for each session class for specific shell options.
|
44
|
-
#
|
45
|
-
# Once the shell is initialized, the shell is yielded to the provided code block which can then interact with
|
46
|
-
# the shell. Once the code block completes, the shell is closed and the session object is returned.
|
47
|
-
#
|
48
|
-
# After completion the session object can only be used to review the +stdout+, +stderr+, and +combined_output+
|
49
|
-
# properties. The +exec+ method and any other methods that interact with the shell will no longer be functional.
|
50
|
-
def initialize(options = {}, &block)
|
51
|
-
|
52
|
-
# cannot instantiate a ShellBase
|
53
|
-
raise NotImplementedError if self.class == Shells::ShellBase
|
54
|
-
|
55
|
-
raise ArgumentError, 'A code block is required.' unless block_given?
|
56
|
-
raise ArgumentError, '\'options\' must be a hash.' unless options.is_a?(Hash)
|
57
|
-
|
58
|
-
@options = {
|
59
|
-
prompt: '~~#',
|
60
|
-
retrieve_exit_code: false,
|
61
|
-
on_non_zero_exit_code: :ignore,
|
62
|
-
silence_timeout: 0,
|
63
|
-
command_timeout: 0
|
64
|
-
}.merge( options.inject({}){ |m,(k,v)| m[k.to_sym] = v; m } )
|
65
|
-
|
66
|
-
@options[:prompt] = @options[:prompt]
|
67
|
-
.to_s.strip
|
68
|
-
.gsub('!', '#')
|
69
|
-
.gsub('$', '#')
|
70
|
-
.gsub('\\', '.')
|
71
|
-
.gsub('/', '.')
|
72
|
-
.gsub('"', '-')
|
73
|
-
.gsub('\'', '-')
|
74
|
-
|
75
|
-
@options[:prompt] = '~~#' if @options[:prompt] == ''
|
76
|
-
|
77
|
-
raise Shells::InvalidOption, ':on_non_zero_exit_code must be :ignore, :raise, or nil.' unless [:ignore, :raise].include?(@options[:on_non_zero_exit_code])
|
78
|
-
|
79
|
-
validate_options
|
80
|
-
@options.freeze # no more changes to options now.
|
81
|
-
|
82
|
-
@session_complete = false
|
83
|
-
@last_input = Time.now
|
84
|
-
debug 'Calling "exec_shell"...'
|
85
|
-
exec_shell do
|
86
|
-
begin
|
87
|
-
debug 'Running "before_init" hooks...'
|
88
|
-
run_hook :before_init
|
89
|
-
debug 'Calling "exec_prompt"...'
|
90
|
-
exec_prompt do
|
91
|
-
begin
|
92
|
-
debug 'Running "after_init" hooks...'
|
93
|
-
run_hook :after_init
|
94
|
-
debug 'Executing code block...'
|
95
|
-
block.call self
|
96
|
-
ensure
|
97
|
-
debug 'Running "before_term" hooks...'
|
98
|
-
run_hook :before_term
|
99
|
-
end
|
100
|
-
end
|
101
|
-
rescue QuitNow
|
102
|
-
debug 'Received QuitNow signal.'
|
103
|
-
nil
|
104
|
-
rescue Exception => ex
|
105
|
-
unless run_hook(:on_exception, ex)
|
106
|
-
raise
|
107
|
-
end
|
108
|
-
ensure
|
109
|
-
debug 'Running "after_term" hooks...'
|
110
|
-
run_hook :after_term
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
@session_complete = true
|
115
|
-
end
|
116
|
-
|
117
|
-
##
|
118
|
-
# Sets the code to be run when debug messages are processed.
|
119
|
-
#
|
120
|
-
# The code will receive the debug message as an argument.
|
121
|
-
#
|
122
|
-
# on_debug do |msg|
|
123
|
-
# puts msg
|
124
|
-
# end
|
125
|
-
#
|
126
|
-
def self.on_debug(proc = nil, &block)
|
127
|
-
@on_debug =
|
128
|
-
if proc.respond_to?(:call)
|
129
|
-
proc
|
130
|
-
elsif proc && respond_to?(proc.to_s, true)
|
131
|
-
method(proc.to_s.to_sym)
|
132
|
-
elsif block
|
133
|
-
block
|
134
|
-
else
|
135
|
-
nil
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
|
-
##
|
140
|
-
# Adds code to be run before the shell is fully initialized.
|
141
|
-
#
|
142
|
-
# This code would normally be used to navigate a menu or setup an environment.
|
143
|
-
# This method allows you to define that behavior without rewriting the connection code.
|
144
|
-
#
|
145
|
-
# before_init do |shell|
|
146
|
-
# ...
|
147
|
-
# end
|
148
|
-
#
|
149
|
-
# You can also pass the name of a static method.
|
150
|
-
#
|
151
|
-
# def self.some_init_function(shell)
|
152
|
-
# ...
|
153
|
-
# end
|
154
|
-
#
|
155
|
-
# before_init :some_init_function
|
156
|
-
#
|
157
|
-
def self.before_init(proc = nil, &block)
|
158
|
-
add_hook :before_init, proc, &block
|
159
|
-
end
|
160
|
-
|
161
|
-
##
|
162
|
-
# Adds code to be run after the shell is fully initialized but before the session code executes.
|
163
|
-
#
|
164
|
-
# after_init do |shell|
|
165
|
-
# ...
|
166
|
-
# end
|
167
|
-
#
|
168
|
-
# You can also pass the name of a static method.
|
169
|
-
#
|
170
|
-
# def self.some_init_function(shell)
|
171
|
-
# ...
|
172
|
-
# end
|
173
|
-
#
|
174
|
-
# after_init :some_init_function
|
175
|
-
#
|
176
|
-
def self.after_init(proc = nil, &block)
|
177
|
-
add_hook :after_init, proc, &block
|
13
|
+
def inspect
|
14
|
+
"#<#{self.class}:0x#{object_id.to_s(16).rjust(12,'0')} #{options.reject{|k,v| k == :password}.inspect}>"
|
178
15
|
end
|
179
16
|
|
17
|
+
end
|
18
|
+
end
|
180
19
|
|
181
|
-
##
|
182
|
-
# Adds code to be run before the shell is terminated immediately after executing the session code.
|
183
|
-
#
|
184
|
-
# This code might also be used to navigate a menu or clean up an environment.
|
185
|
-
# This method allows you to define that behavior without rewriting the connection code.
|
186
|
-
#
|
187
|
-
# This code is guaranteed to be called if the shell initializes correctly.
|
188
|
-
# That means if an error is raised in the session code, this will still fire before handling the error.
|
189
|
-
#
|
190
|
-
# before_term do |shell|
|
191
|
-
# ...
|
192
|
-
# end
|
193
|
-
#
|
194
|
-
# You can also pass the name of a static method.
|
195
|
-
#
|
196
|
-
# def self.some_term_function(shell)
|
197
|
-
# ...
|
198
|
-
# end
|
199
|
-
#
|
200
|
-
# before_term :some_term_function
|
201
|
-
#
|
202
|
-
def self.before_term(proc = nil, &block)
|
203
|
-
add_hook :before_term, proc, &block
|
204
|
-
end
|
205
|
-
|
206
|
-
##
|
207
|
-
# Adds code to be run after the shell session is terminated but before closing the shell session.
|
208
|
-
#
|
209
|
-
# This code might also be used to navigate a menu or clean up an environment.
|
210
|
-
# This method allows you to define that behavior without rewriting the connection code.
|
211
|
-
#
|
212
|
-
# This code is guaranteed to be called if the shell connects correctly.
|
213
|
-
# That means if an error is raised in the session code or shell initialization code, this will still fire before
|
214
|
-
# closing the shell session.
|
215
|
-
#
|
216
|
-
# after_term do |shell|
|
217
|
-
# ...
|
218
|
-
# end
|
219
|
-
#
|
220
|
-
# You can also pass the name of a static method.
|
221
|
-
#
|
222
|
-
# def self.some_term_function(shell)
|
223
|
-
# ...
|
224
|
-
# end
|
225
|
-
#
|
226
|
-
# after_term :some_term_function
|
227
|
-
#
|
228
|
-
def self.after_term(proc = nil, &block)
|
229
|
-
add_hook :after_term, proc, &block
|
230
|
-
end
|
231
|
-
|
232
|
-
|
233
|
-
##
|
234
|
-
# Adds code to be run when an exception occurs.
|
235
|
-
#
|
236
|
-
# This code will receive the shell as the first argument and the exception as the second.
|
237
|
-
# If it handles the exception it should return true, otherwise nil or false.
|
238
|
-
#
|
239
|
-
# on_exception do |shell, ex|
|
240
|
-
# if ex.is_a?(MyExceptionType)
|
241
|
-
# ...
|
242
|
-
# true
|
243
|
-
# else
|
244
|
-
# false
|
245
|
-
# end
|
246
|
-
# end
|
247
|
-
#
|
248
|
-
# You can also pass the name of a static method.
|
249
|
-
#
|
250
|
-
# def self.some_exception_handler(shell, ex)
|
251
|
-
# ...
|
252
|
-
# end
|
253
|
-
#
|
254
|
-
# on_exception :some_exception_handler
|
255
|
-
#
|
256
|
-
def self.on_exception(proc = nil, &block)
|
257
|
-
add_hook :on_exception, proc, &block
|
258
|
-
end
|
259
|
-
|
260
|
-
##
|
261
|
-
# Defines the line ending used to terminate commands sent to the shell.
|
262
|
-
#
|
263
|
-
# The default is "\n". If you need "\r\n", "\r", or some other value, simply override this function.
|
264
|
-
def line_ending
|
265
|
-
"\n"
|
266
|
-
end
|
267
|
-
|
268
|
-
##
|
269
|
-
# Has the session been completed?
|
270
|
-
def session_complete?
|
271
|
-
@session_complete
|
272
|
-
end
|
273
|
-
|
274
|
-
##
|
275
|
-
# Gets the standard output from the session.
|
276
|
-
#
|
277
|
-
# The prompts are stripped from the standard ouput as they are encountered.
|
278
|
-
# So this will be a list of commands with their output.
|
279
|
-
#
|
280
|
-
# All line endings are converted to LF characters, so you will not
|
281
|
-
# encounter or need to search for CRLF or CR sequences.
|
282
|
-
#
|
283
|
-
def stdout
|
284
|
-
@stdout ||= ''
|
285
|
-
end
|
286
|
-
|
287
|
-
##
|
288
|
-
# Gets the error output from the session.
|
289
|
-
#
|
290
|
-
# All line endings are converted to LF characters, so you will not
|
291
|
-
# encounter or need to search for CRLF or CR sequences.
|
292
|
-
#
|
293
|
-
def stderr
|
294
|
-
@stderr ||= ''
|
295
|
-
end
|
296
|
-
|
297
|
-
##
|
298
|
-
# Gets both the standard output and error output from the session.
|
299
|
-
#
|
300
|
-
# The prompts will be included in the combined output.
|
301
|
-
# There is no attempt to differentiate error output from standard output.
|
302
|
-
#
|
303
|
-
# This is essentially the definitive log for the session.
|
304
|
-
#
|
305
|
-
# All line endings are converted to LF characters, so you will not
|
306
|
-
# encounter or need to search for CRLF or CR sequences.
|
307
|
-
#
|
308
|
-
def combined_output
|
309
|
-
@stdcomb ||= ''
|
310
|
-
end
|
311
|
-
|
312
|
-
##
|
313
|
-
# Executes a command during the shell session.
|
314
|
-
#
|
315
|
-
# If called outside of the +new+ block, this will raise an error.
|
316
|
-
#
|
317
|
-
# The +command+ is the command to execute in the shell.
|
318
|
-
#
|
319
|
-
# The +options+ can be used to override the exit code behavior.
|
320
|
-
# In all cases, the :default option is the same as not providing the option and will cause +exec+
|
321
|
-
# to inherit the option from the shell's options.
|
322
|
-
#
|
323
|
-
# +retrieve_exit_code+::
|
324
|
-
# This can be one of :default, true, or false.
|
325
|
-
# +on_non_zero_exit_code+::
|
326
|
-
# This can be on ot :default, :ignore, or :raise.
|
327
|
-
# +silence_timeout+::
|
328
|
-
# This can be :default or the number of seconds to wait in silence before timing out.
|
329
|
-
# +command_timeout+::
|
330
|
-
# This can be :default or the maximum number of seconds to wait for a command to finish before timing out.
|
331
|
-
#
|
332
|
-
# If provided, the +block+ is a chunk of code that will be processed every time the
|
333
|
-
# shell receives output from the program. If the block returns a string, the string
|
334
|
-
# will be sent to the shell. This can be used to monitor processes or monitor and
|
335
|
-
# interact with processes. The +block+ is optional.
|
336
|
-
#
|
337
|
-
# shell.exec('sudo -p "password:" nginx restart') do |data,type|
|
338
|
-
# return 'super-secret' if /password:$/.match(data)
|
339
|
-
# nil
|
340
|
-
# end
|
341
|
-
#
|
342
|
-
def exec(command, options = {}, &block)
|
343
|
-
raise Shells::SessionCompleted if session_complete?
|
344
|
-
|
345
|
-
options ||= {}
|
346
|
-
options = { timeout_error: true }.merge(options)
|
347
|
-
options = self.options.merge(options.inject({}) { |m,(k,v)| m[k.to_sym] = v; m })
|
348
|
-
options[:retrieve_exit_code] = self.options[:retrieve_exit_code] if options[:retrieve_exit_code] == :default
|
349
|
-
options[:on_non_zero_exit_code] = self.options[:on_non_zero_exit_code] unless [:raise, :ignore].include?(options[:on_non_zero_exit_code])
|
350
|
-
options[:silence_timeout] = self.options[:silence_timeout] if options[:silence_timeout] == :default
|
351
|
-
options[:command_timeout] = self.options[:command_timeout] if options[:command_timeout] == :default
|
352
|
-
options[:command_is_echoed] = true if options[:command_is_echoed].nil?
|
353
|
-
ret = ''
|
354
|
-
|
355
|
-
begin
|
356
|
-
push_buffer # store the current buffer and start a fresh buffer
|
357
|
-
|
358
|
-
# buffer while also passing data to the supplied block.
|
359
|
-
if block_given?
|
360
|
-
buffer_input(&block)
|
361
|
-
end
|
362
|
-
|
363
|
-
# send the command and wait for the prompt to return.
|
364
|
-
debug 'Sending command: ' + command
|
365
|
-
send_data command + line_ending
|
366
|
-
if wait_for_prompt(options[:silence_timeout], options[:command_timeout], options[:timeout_error])
|
367
|
-
# get the output of the command, minus the trailing prompt.
|
368
|
-
debug 'Reading output of command...'
|
369
|
-
ret = command_output command, options[:command_is_echoed]
|
370
|
-
|
371
|
-
if options[:retrieve_exit_code]
|
372
|
-
self.last_exit_code = get_exit_code
|
373
|
-
if options[:on_non_zero_exit_code] == :raise
|
374
|
-
raise NonZeroExitCode.new(last_exit_code) unless last_exit_code == 0
|
375
|
-
end
|
376
|
-
else
|
377
|
-
self.last_exit_code = nil
|
378
|
-
end
|
379
|
-
else
|
380
|
-
# A timeout occurred and timeout_error was set to false.
|
381
|
-
debug 'Command timed out...'
|
382
|
-
self.last_exit_code = :timeout
|
383
|
-
ret = combined_output
|
384
|
-
end
|
385
|
-
|
386
|
-
ensure
|
387
|
-
# return buffering to normal.
|
388
|
-
if block_given?
|
389
|
-
buffer_input
|
390
|
-
end
|
391
|
-
|
392
|
-
# restore the original buffer and merge the output from the command.
|
393
|
-
pop_merge_buffer
|
394
|
-
end
|
395
|
-
ret
|
396
|
-
end
|
397
|
-
|
398
|
-
##
|
399
|
-
# Executes a command specifically for the exit code.
|
400
|
-
#
|
401
|
-
# Does not return the output of the command, only the exit code.
|
402
|
-
def exec_for_code(command, options = {}, &block)
|
403
|
-
options = (options || {}).merge(retrieve_exit_code: true, on_non_zero_exit_code: :ignore)
|
404
|
-
exec command, options, &block
|
405
|
-
last_exit_code
|
406
|
-
end
|
407
|
-
|
408
|
-
##
|
409
|
-
# Executes a command ignoring any exit code.
|
410
|
-
#
|
411
|
-
# Returns the output of the command and does not even retrieve the exit code.
|
412
|
-
def exec_ignore_code(command, options = {}, &block)
|
413
|
-
options = (options || {}).merge(retrieve_exit_code: false, on_non_zero_exit_code: :ignore)
|
414
|
-
exec command, options, &block
|
415
|
-
end
|
416
|
-
|
417
|
-
##
|
418
|
-
# Reads from a file on the device.
|
419
|
-
def read_file(path)
|
420
|
-
raise ::NotImplementedError
|
421
|
-
end
|
422
|
-
|
423
|
-
##
|
424
|
-
# Writes to a file on the device.
|
425
|
-
def write_file(path, data)
|
426
|
-
raise ::NotImplementedError
|
427
|
-
end
|
428
|
-
|
429
|
-
|
430
|
-
protected
|
431
|
-
|
432
|
-
##
|
433
|
-
# Validates the options provided to the class.
|
434
|
-
#
|
435
|
-
# You should define this method in your subclass.
|
436
|
-
def validate_options #:doc:
|
437
|
-
warn "The validate_options() method is not defined on the #{self.class} class."
|
438
|
-
end
|
439
|
-
|
440
|
-
##
|
441
|
-
# Executes a shell session.
|
442
|
-
#
|
443
|
-
# This method should connect to the shell and then yield.
|
444
|
-
# It should not initialize the prompt.
|
445
|
-
# When the yielded block returns this method should then disconnect from the shell.
|
446
|
-
#
|
447
|
-
# You must define this method in your subclass.
|
448
|
-
def exec_shell(&block) #:doc:
|
449
|
-
raise ::NotImplementedError
|
450
|
-
end
|
451
|
-
|
452
|
-
##
|
453
|
-
# Runs all prompted commands.
|
454
|
-
#
|
455
|
-
# This method should initialize the shell prompt and then yield.
|
456
|
-
#
|
457
|
-
# You must define this method in your subclass.
|
458
|
-
def exec_prompt(&block) #:doc:
|
459
|
-
raise ::NotImplementedError
|
460
|
-
end
|
461
|
-
|
462
|
-
##
|
463
|
-
# Sends data to the shell.
|
464
|
-
#
|
465
|
-
# You must define this method in your subclass.
|
466
|
-
def send_data(data) #:doc:
|
467
|
-
raise ::NotImplementedError
|
468
|
-
end
|
469
|
-
|
470
|
-
##
|
471
|
-
# Loops while the block returns any true value.
|
472
|
-
#
|
473
|
-
# Inside the loop you should check for data being received from the shell and dispatch it to the appropriate
|
474
|
-
# hook method (see stdout_received() and stderr_received()). Once input has been cleared you should execute
|
475
|
-
# the block and exit unless the block returns a true value.
|
476
|
-
#
|
477
|
-
# You must define this method in your subclass.
|
478
|
-
def loop(&block) #:doc:
|
479
|
-
raise ::NotImplementedError
|
480
|
-
end
|
481
|
-
|
482
|
-
##
|
483
|
-
# Register a callback to run when stdout data is received.
|
484
|
-
#
|
485
|
-
# The block will be passed the data received.
|
486
|
-
#
|
487
|
-
# You must define this method in your subclass and it should set a hook to be called when data is received.
|
488
|
-
def stdout_received(&block) #:doc:
|
489
|
-
raise ::NotImplementedError
|
490
|
-
end
|
491
|
-
|
492
|
-
##
|
493
|
-
# Register a callback to run when stderr data is received.
|
494
|
-
#
|
495
|
-
# The block will be passed the data received.
|
496
|
-
#
|
497
|
-
# You must define this method in your subclass and it should set a hook to be called when data is received.
|
498
|
-
def stderr_received(&block) #:doc:
|
499
|
-
raise ::NotImplementedError
|
500
|
-
end
|
501
|
-
|
502
|
-
##
|
503
|
-
# Gets the exit code from the last command.
|
504
|
-
#
|
505
|
-
# You must define this method in your subclass to utilize exit codes.
|
506
|
-
def get_exit_code #:doc:
|
507
|
-
raise ::NotImplementedError
|
508
|
-
end
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
##
|
514
|
-
# Waits for the prompt to appear at the end of the output.
|
515
|
-
#
|
516
|
-
# Once the prompt appears, new input can be sent to the shell.
|
517
|
-
# This is automatically called in +exec+ so you would only need
|
518
|
-
# to call it directly if you were sending data manually to the
|
519
|
-
# shell.
|
520
|
-
#
|
521
|
-
# This method is used internally in the +exec+ method, but there may be legitimate use cases
|
522
|
-
# outside of that method as well.
|
523
|
-
def wait_for_prompt(silence_timeout = nil, command_timeout = nil, timeout_error = true) #:doc:
|
524
|
-
raise Shells::SessionCompleted if session_complete?
|
525
|
-
|
526
|
-
silence_timeout ||= options[:silence_timeout]
|
527
|
-
command_timeout ||= options[:command_timeout]
|
528
|
-
|
529
|
-
sent_nl_at = nil
|
530
|
-
sent_nl_times = 0
|
531
|
-
silence_timeout = silence_timeout.to_s.to_f unless silence_timeout.is_a?(Numeric)
|
532
|
-
nudge_timeout =
|
533
|
-
if silence_timeout > 0
|
534
|
-
(silence_timeout / 3) # we want to nudge twice before officially timing out.
|
535
|
-
else
|
536
|
-
0
|
537
|
-
end
|
538
|
-
|
539
|
-
command_timeout = command_timeout.to_s.to_f unless command_timeout.is_a?(Numeric)
|
540
|
-
timeout =
|
541
|
-
if command_timeout > 0
|
542
|
-
Time.now + command_timeout
|
543
|
-
else
|
544
|
-
nil
|
545
|
-
end
|
546
|
-
|
547
|
-
loop do
|
548
|
-
Thread.pass
|
549
|
-
|
550
|
-
last_input = @last_input
|
551
|
-
|
552
|
-
# Do we need to nudge the shell?
|
553
|
-
if nudge_timeout > 0 && (Time.now - last_input) > nudge_timeout
|
554
|
-
|
555
|
-
# Have we previously nudged the shell?
|
556
|
-
if sent_nl_times > 2
|
557
|
-
raise Shells::SilenceTimeout if timeout_error
|
558
|
-
return false
|
559
|
-
else
|
560
|
-
sent_nl_times = (sent_nl_at.nil? || sent_nl_at < last_input) ? 1 : (sent_nl_times + 1)
|
561
|
-
sent_nl_at = Time.now
|
562
|
-
|
563
|
-
send_data line_ending
|
564
|
-
|
565
|
-
# wait a bit longer...
|
566
|
-
@last_input = sent_nl_at
|
567
|
-
end
|
568
|
-
end
|
569
|
-
|
570
|
-
if timeout && Time.now > timeout
|
571
|
-
raise Shells::CommandTimeout if timeout_error
|
572
|
-
return false
|
573
|
-
end
|
574
|
-
|
575
|
-
!(combined_output =~ prompt_match)
|
576
|
-
end
|
577
|
-
|
578
|
-
pos = (combined_output =~ prompt_match)
|
579
|
-
if combined_output[pos - 1] != "\n"
|
580
|
-
# no newline before prompt, fix that.
|
581
|
-
self.combined_output = combined_output[0...pos] + "\n" + combined_output[pos..-1]
|
582
|
-
end
|
583
|
-
if stdout[-1] != "\n"
|
584
|
-
# no newline at end, fix that.
|
585
|
-
self.stdout <<= "\n"
|
586
|
-
end
|
587
|
-
|
588
|
-
true
|
589
|
-
end
|
590
|
-
|
591
|
-
##
|
592
|
-
# Sets the block to call when data is received.
|
593
|
-
#
|
594
|
-
# If no block is provided, then the shell will simply log all output from the program.
|
595
|
-
# If a block is provided, it will be passed the data as it is received. If the block
|
596
|
-
# returns a string, then that string will be sent to the shell.
|
597
|
-
#
|
598
|
-
# This method is called internally in the +exec+ method, but there may be legitimate use
|
599
|
-
# cases outside of that method as well.
|
600
|
-
def buffer_input(&block) #:doc:
|
601
|
-
raise Shells::SessionCompleted if session_complete?
|
602
|
-
block ||= Proc.new { }
|
603
|
-
stdout_received do |data|
|
604
|
-
@last_input = Time.now
|
605
|
-
append_stdout strip_ansi_escape(data), &block
|
606
|
-
end
|
607
|
-
stderr_received do |data|
|
608
|
-
@last_input = Time.now
|
609
|
-
append_stderr strip_ansi_escape(data), &block
|
610
|
-
end
|
611
|
-
end
|
612
|
-
|
613
|
-
##
|
614
|
-
# Pushes the buffers for output capture.
|
615
|
-
#
|
616
|
-
# This method is called internally in the +exec+ method, but there may be legitimate use
|
617
|
-
# cases outside of that method as well.
|
618
|
-
def push_buffer #:doc:
|
619
|
-
raise Shells::SessionCompleted if session_complete?
|
620
|
-
# push the buffer so we can get the output of a command.
|
621
|
-
debug 'Pushing buffer >>'
|
622
|
-
stdout_hist.push stdout
|
623
|
-
stderr_hist.push stderr
|
624
|
-
stdcomb_hist.push combined_output
|
625
|
-
self.stdout = ''
|
626
|
-
self.stderr = ''
|
627
|
-
self.combined_output = ''
|
628
|
-
end
|
629
|
-
|
630
|
-
##
|
631
|
-
# Pops the buffers and merges the captured output.
|
632
|
-
#
|
633
|
-
# This method is called internally in the +exec+ method, but there may be legitimate use
|
634
|
-
# cases outside of that method as well.
|
635
|
-
def pop_merge_buffer #:doc:
|
636
|
-
raise Shells::SessionCompleted if session_complete?
|
637
|
-
# almost a standard pop, however we want to merge history with current.
|
638
|
-
debug 'Merging buffer <<'
|
639
|
-
if (hist = stdout_hist.pop)
|
640
|
-
self.stdout = hist + stdout
|
641
|
-
end
|
642
|
-
if (hist = stderr_hist.pop)
|
643
|
-
self.stderr = hist + stderr
|
644
|
-
end
|
645
|
-
if (hist = stdcomb_hist.pop)
|
646
|
-
self.combined_output = hist + combined_output
|
647
|
-
end
|
648
|
-
end
|
649
|
-
|
650
|
-
##
|
651
|
-
# Pops the buffers and discards the captured output.
|
652
|
-
#
|
653
|
-
# This method is used internally in the +get_exit_code+ method, but there may be legitimate use
|
654
|
-
# cases outside of that method as well.
|
655
|
-
def pop_discard_buffer #:doc:
|
656
|
-
raise Shells::SessionCompleted if session_complete?
|
657
|
-
# a standard pop discarding current data and retrieving the history.
|
658
|
-
debug 'Discarding buffer <<'
|
659
|
-
if (hist = stdout_hist.pop)
|
660
|
-
@stdout = hist
|
661
|
-
end
|
662
|
-
if (hist = stderr_hist.pop)
|
663
|
-
@stderr = hist
|
664
|
-
end
|
665
|
-
if (hist = stdcomb_hist.pop)
|
666
|
-
@stdcomb = hist
|
667
|
-
end
|
668
|
-
end
|
669
|
-
|
670
|
-
##
|
671
|
-
# Processes a debug message.
|
672
|
-
def self.debug(msg) #:doc:'
|
673
|
-
@on_debug&.call(msg)
|
674
|
-
end
|
675
|
-
|
676
|
-
##
|
677
|
-
# Processes a debug message for an instance.
|
678
|
-
def debug(msg) #:nodoc:
|
679
|
-
self.class.debug msg
|
680
|
-
end
|
681
|
-
|
682
|
-
##
|
683
|
-
# Sets the prompt to the value temporarily for execution of the code block.
|
684
|
-
#
|
685
|
-
# The prompt is automatically reset after completion or failure of the code block.
|
686
|
-
#
|
687
|
-
# If no code block is provided this essentially only resets the prompt.
|
688
|
-
def temporary_prompt(prompt)
|
689
|
-
raise Shells::SessionCompleted if session_complete?
|
690
|
-
begin
|
691
|
-
@prompt_match = prompt.is_a?(Regexp) ? prompt : /#{prompt}[ \t]*$/
|
692
|
-
|
693
|
-
yield if block_given?
|
694
|
-
ensure
|
695
|
-
@prompt_match = nil
|
696
|
-
end
|
697
|
-
end
|
698
|
-
|
699
|
-
##
|
700
|
-
# Allows you to change the :quit option inside of a session.
|
701
|
-
#
|
702
|
-
# This is useful if you need to change the quit command for some reason.
|
703
|
-
# e.g. - Changing the command to "reboot".
|
704
|
-
def change_quit(quit_command)
|
705
|
-
raise Shells::SessionCompleted if session_complete?
|
706
|
-
opts = options.dup
|
707
|
-
opts[:quit] = quit_command
|
708
|
-
opts.freeze
|
709
|
-
@options = opts
|
710
|
-
end
|
711
|
-
|
712
|
-
|
713
|
-
private
|
714
|
-
|
715
|
-
|
716
|
-
def self.add_hook(hook_name, proc, &block)
|
717
|
-
hooks[hook_name] ||= []
|
718
|
-
|
719
|
-
if proc.respond_to?(:call)
|
720
|
-
hooks[hook_name] << proc
|
721
|
-
elsif proc.is_a?(Symbol) || proc.is_a?(String)
|
722
|
-
if self.respond_to?(proc, true)
|
723
|
-
hooks[hook_name] << method(proc.to_sym)
|
724
|
-
end
|
725
|
-
elsif proc
|
726
|
-
raise ArgumentError, 'proc must respond to :call method or be the name of a static method in this class'
|
727
|
-
end
|
728
|
-
|
729
|
-
if block
|
730
|
-
hooks[hook_name] << block
|
731
|
-
end
|
732
|
-
|
733
|
-
end
|
734
|
-
|
735
|
-
def run_hook(hook_name, *args)
|
736
|
-
(self.class.hooks[hook_name] || []).each do |hook|
|
737
|
-
result = hook.call(self, *args)
|
738
|
-
return true if result.is_a?(TrueClass)
|
739
|
-
end
|
740
|
-
false
|
741
|
-
end
|
742
|
-
|
743
|
-
def self.hooks
|
744
|
-
@hooks ||= {}
|
745
|
-
end
|
746
|
-
|
747
|
-
|
748
|
-
def stdout=(value)
|
749
|
-
@stdout = value
|
750
|
-
end
|
751
|
-
|
752
|
-
def stderr=(value)
|
753
|
-
@stderr = value
|
754
|
-
end
|
755
|
-
|
756
|
-
def combined_output=(value)
|
757
|
-
@stdcomb = value
|
758
|
-
end
|
759
|
-
|
760
|
-
def append_stdout(data, &block)
|
761
|
-
# Combined output gets the prompts,
|
762
|
-
# but stdout will be without prompts.
|
763
|
-
data = reduce_newlines data
|
764
|
-
for_stdout = if (pos = (data =~ prompt_match))
|
765
|
-
data[0...pos]
|
766
|
-
else
|
767
|
-
data
|
768
|
-
end
|
769
|
-
|
770
|
-
self.stdout <<= for_stdout
|
771
|
-
self.combined_output <<= data
|
772
|
-
|
773
|
-
if block_given?
|
774
|
-
result = block.call(for_stdout, :stdout)
|
775
|
-
if result && result.is_a?(String)
|
776
|
-
send_data(result + line_ending)
|
777
|
-
end
|
778
|
-
end
|
779
|
-
end
|
780
|
-
|
781
|
-
def append_stderr(data, &block)
|
782
|
-
data = reduce_newlines data
|
783
|
-
|
784
|
-
self.stderr <<= data
|
785
|
-
self.combined_output <<= data
|
786
|
-
|
787
|
-
if block_given?
|
788
|
-
result = block.call(data, :stderr)
|
789
|
-
if result && result.is_a?(String)
|
790
|
-
send_data(result + line_ending)
|
791
|
-
end
|
792
|
-
end
|
793
|
-
end
|
794
|
-
|
795
|
-
def reduce_newlines(data)
|
796
|
-
data.gsub("\r\n", "\n").gsub(" \r", "").gsub("\r", "")
|
797
|
-
end
|
798
|
-
|
799
|
-
def command_output(command, expect_command = true)
|
800
|
-
# get everything except for the ending prompt.
|
801
|
-
ret =
|
802
|
-
if (prompt_pos = (combined_output =~ prompt_match))
|
803
|
-
combined_output[0...prompt_pos]
|
804
|
-
else
|
805
|
-
combined_output
|
806
|
-
end
|
807
|
-
|
808
|
-
|
809
|
-
if expect_command
|
810
|
-
command_regex = command_match(command)
|
811
|
-
|
812
|
-
# Go until we run out of data or we find one of the possible command starts.
|
813
|
-
# Note that we EXPECT the command to the first line of the output from the command because we expect the
|
814
|
-
# shell to echo it back to us.
|
815
|
-
result_cmd,_,result_data = ret.partition("\n")
|
816
|
-
until result_data.to_s.strip == '' || result_cmd.strip =~ command_regex
|
817
|
-
result_cmd,_,result_data = result_data.partition("\n")
|
818
|
-
end
|
819
|
-
|
820
|
-
if result_cmd.nil? || !(result_cmd =~ command_regex)
|
821
|
-
STDERR.puts "SHELL WARNING: Failed to match #{command_regex.inspect}."
|
822
|
-
end
|
823
|
-
|
824
|
-
result_data
|
825
|
-
else
|
826
|
-
ret
|
827
|
-
end
|
828
|
-
end
|
829
|
-
|
830
|
-
def strip_ansi_escape(data)
|
831
|
-
data
|
832
|
-
.gsub(/\e\[(\d+;?)*[ABCDEFGHfu]/, "\n") # any of the "set cursor position" CSI commands.
|
833
|
-
.gsub(/\e\[=?(\d+;?)*[A-Za-z]/,'') # \e[#;#;#A or \e[=#;#;#A basically all the CSI commands except ...
|
834
|
-
.gsub(/\e\[(\d+;"[^"]+";?)+p/, '') # \e[#;"A"p
|
835
|
-
.gsub(/\e[NOc]./,'?') # any of the alternate character set commands.
|
836
|
-
.gsub(/\e[P_\]^X][^\e\a]*(\a|(\e\\))/,'') # any string command
|
837
|
-
.gsub(/[\x00\x08\x0B\x0C\x0E-\x1F]/, '') # any non-printable characters (notice \x0A (LF) and \x0D (CR) are left as is).
|
838
|
-
.gsub("\t", ' ') # turn tabs into spaces.
|
839
|
-
end
|
840
|
-
|
841
|
-
def stdout_hist
|
842
|
-
@stdout_hist ||= []
|
843
|
-
end
|
844
|
-
|
845
|
-
def stderr_hist
|
846
|
-
@stderr_hist ||= []
|
847
|
-
end
|
848
|
-
|
849
|
-
def stdcomb_hist
|
850
|
-
@stdcomb_hist ||= []
|
851
|
-
end
|
852
|
-
|
853
|
-
def regex_escape(text)
|
854
|
-
text
|
855
|
-
.gsub('\\', '\\\\')
|
856
|
-
.gsub('[', '\\[')
|
857
|
-
.gsub(']', '\\]')
|
858
|
-
.gsub('(', '\\(')
|
859
|
-
.gsub(')', '\\)')
|
860
|
-
.gsub('.', '\\.')
|
861
|
-
.gsub('*', '\\*')
|
862
|
-
.gsub('+', '\\+')
|
863
|
-
.gsub('?', '\\?')
|
864
|
-
.gsub('{', '\\{')
|
865
|
-
.gsub('}', '\\}')
|
866
|
-
.gsub('$', '\\$')
|
867
|
-
.gsub('^', '\\^')
|
868
|
-
end
|
869
|
-
|
870
|
-
def command_match(command)
|
871
|
-
p = regex_escape @options[:prompt]
|
872
|
-
c = regex_escape command
|
873
|
-
/\A(?:#{p}\s*)?#{c}[ \t]*\z/
|
874
|
-
end
|
875
20
|
|
876
|
-
|
877
|
-
# allow for trailing spaces or tabs, but no other whitespace.
|
878
|
-
@prompt_match ||= /#{regex_escape @options[:prompt]}[ \t]*$/
|
879
|
-
end
|
21
|
+
require 'shells/shell_base/hooks'
|
880
22
|
|
881
|
-
|
23
|
+
require 'shells/shell_base/sync'
|
24
|
+
require 'shells/shell_base/debug'
|
25
|
+
require 'shells/shell_base/options'
|
26
|
+
require 'shells/shell_base/interface' # methods to override in derived classes.
|
27
|
+
require 'shells/shell_base/input'
|
28
|
+
require 'shells/shell_base/output'
|
29
|
+
require 'shells/shell_base/regex_escape'
|
30
|
+
require 'shells/shell_base/prompt'
|
31
|
+
require 'shells/shell_base/exec'
|
32
|
+
require 'shells/shell_base/run'
|
882
33
|
|
883
|
-
end
|