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