shells 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +78 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/shells/bash_common.rb +170 -0
- data/lib/shells/errors.rb +57 -0
- data/lib/shells/pf_sense_common.rb +400 -0
- data/lib/shells/pf_sense_serial_session.rb +55 -0
- data/lib/shells/pf_sense_ssh_session.rb +56 -0
- data/lib/shells/serial_session.rb +184 -0
- data/lib/shells/shell_base.rb +846 -0
- data/lib/shells/ssh_session.rb +232 -0
- data/lib/shells/version.rb +5 -0
- data/lib/shells.rb +37 -0
- data/shells.gemspec +32 -0
- metadata +160 -0
@@ -0,0 +1,846 @@
|
|
1
|
+
require 'shells/errors'
|
2
|
+
|
3
|
+
module Shells
|
4
|
+
|
5
|
+
##
|
6
|
+
# Provides a base interface for all shells to build on.
|
7
|
+
#
|
8
|
+
# Instantiating this class will raise an error.
|
9
|
+
# All shell sessions should inherit this class.
|
10
|
+
class ShellBase
|
11
|
+
|
12
|
+
##
|
13
|
+
# Raise a QuitNow to tell the shell to stop processing and exit.
|
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
|
178
|
+
end
|
179
|
+
|
180
|
+
|
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 options[:on_non_zero_exit_code] == :default
|
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
|
+
ret = ''
|
353
|
+
|
354
|
+
begin
|
355
|
+
push_buffer # store the current buffer and start a fresh buffer
|
356
|
+
|
357
|
+
# buffer while also passing data to the supplied block.
|
358
|
+
if block_given?
|
359
|
+
buffer_input(&block)
|
360
|
+
end
|
361
|
+
|
362
|
+
# send the command and wait for the prompt to return.
|
363
|
+
debug 'Sending command: ' + command
|
364
|
+
send_data command + line_ending
|
365
|
+
if wait_for_prompt(options[:silence_timeout], options[:command_timeout], options[:timeout_error])
|
366
|
+
# get the output of the command, minus the trailing prompt.
|
367
|
+
debug 'Reading output of command...'
|
368
|
+
ret = command_output command
|
369
|
+
|
370
|
+
if options[:retrieve_exit_code]
|
371
|
+
self.last_exit_code = get_exit_code
|
372
|
+
if options[:on_non_zero_exit_code] == :raise
|
373
|
+
raise NonZeroExitCode.new(last_exit_code) unless last_exit_code == 0
|
374
|
+
end
|
375
|
+
else
|
376
|
+
self.last_exit_code = nil
|
377
|
+
end
|
378
|
+
else
|
379
|
+
# A timeout occurred and timeout_error was set to false.
|
380
|
+
debug 'Command timed out...'
|
381
|
+
self.last_exit_code = :timeout
|
382
|
+
ret = combined_output
|
383
|
+
end
|
384
|
+
|
385
|
+
ensure
|
386
|
+
# return buffering to normal.
|
387
|
+
if block_given?
|
388
|
+
buffer_input
|
389
|
+
end
|
390
|
+
|
391
|
+
# restore the original buffer and merge the output from the command.
|
392
|
+
pop_merge_buffer
|
393
|
+
end
|
394
|
+
ret
|
395
|
+
end
|
396
|
+
|
397
|
+
##
|
398
|
+
# Executes a command specifically for the exit code.
|
399
|
+
#
|
400
|
+
# Does not return the output of the command, only the exit code.
|
401
|
+
def exec_for_code(command, options = {}, &block)
|
402
|
+
options = (options || {}).merge(retrieve_exit_code: true, on_non_zero_exit_code: :ignore)
|
403
|
+
exec command, options, &block
|
404
|
+
last_exit_code
|
405
|
+
end
|
406
|
+
|
407
|
+
##
|
408
|
+
# Reads from a file on the device.
|
409
|
+
def read_file(path)
|
410
|
+
raise ::NotImplementedError
|
411
|
+
end
|
412
|
+
|
413
|
+
##
|
414
|
+
# Writes to a file on the device.
|
415
|
+
def write_file(path, data)
|
416
|
+
raise ::NotImplementedError
|
417
|
+
end
|
418
|
+
|
419
|
+
|
420
|
+
protected
|
421
|
+
|
422
|
+
|
423
|
+
|
424
|
+
##
|
425
|
+
# Validates the options provided to the class.
|
426
|
+
#
|
427
|
+
# You should define this method in your subclass.
|
428
|
+
def validate_options #:doc:
|
429
|
+
warn "The validate_options() method is not defined on the #{self.class} class."
|
430
|
+
end
|
431
|
+
|
432
|
+
##
|
433
|
+
# Executes a shell session.
|
434
|
+
#
|
435
|
+
# This method should connect to the shell and then yield.
|
436
|
+
# It should not initialize the prompt.
|
437
|
+
# When the yielded block returns this method should then disconnect from the shell.
|
438
|
+
#
|
439
|
+
# You must define this method in your subclass.
|
440
|
+
def exec_shell(&block) #:doc:
|
441
|
+
raise ::NotImplementedError
|
442
|
+
end
|
443
|
+
|
444
|
+
##
|
445
|
+
# Runs all prompted commands.
|
446
|
+
#
|
447
|
+
# This method should initialize the shell prompt and then yield.
|
448
|
+
#
|
449
|
+
# You must define this method in your subclass.
|
450
|
+
def exec_prompt(&block) #:doc:
|
451
|
+
raise ::NotImplementedError
|
452
|
+
end
|
453
|
+
|
454
|
+
##
|
455
|
+
# Sends data to the shell.
|
456
|
+
#
|
457
|
+
# You must define this method in your subclass.
|
458
|
+
def send_data(data) #:doc:
|
459
|
+
raise ::NotImplementedError
|
460
|
+
end
|
461
|
+
|
462
|
+
##
|
463
|
+
# Loops while the block returns any true value.
|
464
|
+
#
|
465
|
+
# Inside the loop you should check for data being received from the shell and dispatch it to the appropriate
|
466
|
+
# hook method (see stdout_received() and stderr_received()). Once input has been cleared you should execute
|
467
|
+
# the block and exit unless the block returns a true value.
|
468
|
+
#
|
469
|
+
# You must define this method in your subclass.
|
470
|
+
def loop(&block) #:doc:
|
471
|
+
raise ::NotImplementedError
|
472
|
+
end
|
473
|
+
|
474
|
+
##
|
475
|
+
# Register a callback to run when stdout data is received.
|
476
|
+
#
|
477
|
+
# The block will be passed the data received.
|
478
|
+
#
|
479
|
+
# You must define this method in your subclass and it should set a hook to be called when data is received.
|
480
|
+
def stdout_received(&block) #:doc:
|
481
|
+
raise ::NotImplementedError
|
482
|
+
end
|
483
|
+
|
484
|
+
##
|
485
|
+
# Register a callback to run when stderr data is received.
|
486
|
+
#
|
487
|
+
# The block will be passed the data received.
|
488
|
+
#
|
489
|
+
# You must define this method in your subclass and it should set a hook to be called when data is received.
|
490
|
+
def stderr_received(&block) #:doc:
|
491
|
+
raise ::NotImplementedError
|
492
|
+
end
|
493
|
+
|
494
|
+
##
|
495
|
+
# Gets the exit code from the last command.
|
496
|
+
#
|
497
|
+
# You must define this method in your subclass to utilize exit codes.
|
498
|
+
def get_exit_code #:doc:
|
499
|
+
raise ::NotImplementedError
|
500
|
+
end
|
501
|
+
|
502
|
+
|
503
|
+
|
504
|
+
|
505
|
+
##
|
506
|
+
# Waits for the prompt to appear at the end of the output.
|
507
|
+
#
|
508
|
+
# Once the prompt appears, new input can be sent to the shell.
|
509
|
+
# This is automatically called in +exec+ so you would only need
|
510
|
+
# to call it directly if you were sending data manually to the
|
511
|
+
# shell.
|
512
|
+
#
|
513
|
+
# This method is used internally in the +exec+ method, but there may be legitimate use cases
|
514
|
+
# outside of that method as well.
|
515
|
+
def wait_for_prompt(silence_timeout = nil, command_timeout = nil, timeout_error = true) #:doc:
|
516
|
+
raise Shells::SessionCompleted if session_complete?
|
517
|
+
|
518
|
+
silence_timeout ||= options[:silence_timeout]
|
519
|
+
command_timeout ||= options[:command_timeout]
|
520
|
+
|
521
|
+
sent_nl_at = nil
|
522
|
+
sent_nl_times = 0
|
523
|
+
silence_timeout = silence_timeout.to_s.to_f unless silence_timeout.is_a?(Numeric)
|
524
|
+
nudge_timeout =
|
525
|
+
if silence_timeout > 0
|
526
|
+
(silence_timeout / 3) # we want to nudge twice before officially timing out.
|
527
|
+
else
|
528
|
+
0
|
529
|
+
end
|
530
|
+
|
531
|
+
command_timeout = command_timeout.to_s.to_f unless command_timeout.is_a?(Numeric)
|
532
|
+
timeout =
|
533
|
+
if command_timeout > 0
|
534
|
+
Time.now + command_timeout
|
535
|
+
else
|
536
|
+
nil
|
537
|
+
end
|
538
|
+
|
539
|
+
loop do
|
540
|
+
last_input = @last_input
|
541
|
+
|
542
|
+
# Do we need to nudge the shell?
|
543
|
+
if nudge_timeout > 0 && (Time.now - last_input) > nudge_timeout
|
544
|
+
|
545
|
+
# Have we previously nudged the shell?
|
546
|
+
if sent_nl_times > 2
|
547
|
+
raise Shells::SilenceTimeout if timeout_error
|
548
|
+
return false
|
549
|
+
else
|
550
|
+
sent_nl_times = (sent_nl_at.nil? || sent_nl_at < last_input) ? 1 : (sent_nl_times + 1)
|
551
|
+
sent_nl_at = Time.now
|
552
|
+
|
553
|
+
send_data line_ending
|
554
|
+
|
555
|
+
# wait a bit longer...
|
556
|
+
@last_input = sent_nl_at
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
if timeout && Time.now > timeout
|
561
|
+
raise Shells::CommandTimeout if timeout_error
|
562
|
+
return false
|
563
|
+
end
|
564
|
+
|
565
|
+
!(combined_output =~ prompt_match)
|
566
|
+
end
|
567
|
+
|
568
|
+
pos = combined_output =~ prompt_match
|
569
|
+
if combined_output[pos - 1] != "\n"
|
570
|
+
# no newline before prompt, fix that.
|
571
|
+
self.combined_output = combined_output[0...pos] + "\n" + combined_output[pos..-1]
|
572
|
+
end
|
573
|
+
if stdout[-1] != "\n"
|
574
|
+
# no newline at end, fix that.
|
575
|
+
self.stdout <<= "\n"
|
576
|
+
end
|
577
|
+
|
578
|
+
true
|
579
|
+
end
|
580
|
+
|
581
|
+
##
|
582
|
+
# Sets the block to call when data is received.
|
583
|
+
#
|
584
|
+
# If no block is provided, then the shell will simply log all output from the program.
|
585
|
+
# If a block is provided, it will be passed the data as it is received. If the block
|
586
|
+
# returns a string, then that string will be sent to the shell.
|
587
|
+
#
|
588
|
+
# This method is called internally in the +exec+ method, but there may be legitimate use
|
589
|
+
# cases outside of that method as well.
|
590
|
+
def buffer_input(&block) #:doc:
|
591
|
+
raise Shells::SessionCompleted if session_complete?
|
592
|
+
block ||= Proc.new { }
|
593
|
+
stdout_received do |data|
|
594
|
+
@last_input = Time.now
|
595
|
+
append_stdout strip_ansi_escape(data), &block
|
596
|
+
end
|
597
|
+
stderr_received do |data|
|
598
|
+
@last_input = Time.now
|
599
|
+
append_stderr strip_ansi_escap(data), &block
|
600
|
+
end
|
601
|
+
end
|
602
|
+
|
603
|
+
##
|
604
|
+
# Pushes the buffers for output capture.
|
605
|
+
#
|
606
|
+
# This method is called internally in the +exec+ method, but there may be legitimate use
|
607
|
+
# cases outside of that method as well.
|
608
|
+
def push_buffer #:doc:
|
609
|
+
raise Shells::SessionCompleted if session_complete?
|
610
|
+
# push the buffer so we can get the output of a command.
|
611
|
+
debug 'Pushing buffer >>'
|
612
|
+
stdout_hist.push stdout
|
613
|
+
stderr_hist.push stderr
|
614
|
+
stdcomb_hist.push combined_output
|
615
|
+
self.stdout = ''
|
616
|
+
self.stderr = ''
|
617
|
+
self.combined_output = ''
|
618
|
+
end
|
619
|
+
|
620
|
+
##
|
621
|
+
# Pops the buffers and merges the captured output.
|
622
|
+
#
|
623
|
+
# This method is called internally in the +exec+ method, but there may be legitimate use
|
624
|
+
# cases outside of that method as well.
|
625
|
+
def pop_merge_buffer #:doc:
|
626
|
+
raise Shells::SessionCompleted if session_complete?
|
627
|
+
# almost a standard pop, however we want to merge history with current.
|
628
|
+
debug 'Merging buffer <<'
|
629
|
+
if (hist = stdout_hist.pop)
|
630
|
+
self.stdout = hist + stdout
|
631
|
+
end
|
632
|
+
if (hist = stderr_hist.pop)
|
633
|
+
self.stderr = hist + stderr
|
634
|
+
end
|
635
|
+
if (hist = stdcomb_hist.pop)
|
636
|
+
self.combined_output = hist + combined_output
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
##
|
641
|
+
# Pops the buffers and discards the captured output.
|
642
|
+
#
|
643
|
+
# This method is used internally in the +get_exit_code+ method, but there may be legitimate use
|
644
|
+
# cases outside of that method as well.
|
645
|
+
def pop_discard_buffer #:doc:
|
646
|
+
raise Shells::SessionCompleted if session_complete?
|
647
|
+
# a standard pop discarding current data and retrieving the history.
|
648
|
+
debug 'Discarding buffer <<'
|
649
|
+
if (hist = stdout_hist.pop)
|
650
|
+
@stdout = hist
|
651
|
+
end
|
652
|
+
if (hist = stderr_hist.pop)
|
653
|
+
@stderr = hist
|
654
|
+
end
|
655
|
+
if (hist = stdcomb_hist.pop)
|
656
|
+
@stdcomb = hist
|
657
|
+
end
|
658
|
+
end
|
659
|
+
|
660
|
+
##
|
661
|
+
# Processes a debug message.
|
662
|
+
def self.debug(msg) #:doc:
|
663
|
+
@debug_proc ||= instance_variable_defined?(:@on_debug) ? (instance_variable_get(:@on_debug) || ->(_) { }) : ->(_){ }
|
664
|
+
@debug_proc.call(msg)
|
665
|
+
end
|
666
|
+
|
667
|
+
##
|
668
|
+
# Processes a debug message for an instance.
|
669
|
+
def debug(msg) #:nodoc:
|
670
|
+
self.class.debug msg
|
671
|
+
end
|
672
|
+
|
673
|
+
##
|
674
|
+
# Sets the prompt to the value temporarily for execution of the code block.
|
675
|
+
#
|
676
|
+
# The prompt is automatically reset after completion or failure of the code block.
|
677
|
+
#
|
678
|
+
# If no code block is provided this essentially only resets the prompt.
|
679
|
+
def temporary_prompt(prompt)
|
680
|
+
raise Shells::SessionCompleted if session_complete?
|
681
|
+
begin
|
682
|
+
@prompt_match = prompt.is_a?(Regexp) ? prompt : /#{prompt}[ \t]*$/
|
683
|
+
|
684
|
+
yield if block_given?
|
685
|
+
ensure
|
686
|
+
@prompt_match = nil
|
687
|
+
end
|
688
|
+
end
|
689
|
+
|
690
|
+
##
|
691
|
+
# Allows you to change the :quit option inside of a session.
|
692
|
+
#
|
693
|
+
# This is useful if you need to change the quit command for some reason.
|
694
|
+
# e.g. - Changing the command to "reboot".
|
695
|
+
def change_quit(quit_command)
|
696
|
+
raise Shells::SessionCompleted if session_complete?
|
697
|
+
opts = options.dup
|
698
|
+
opts[:quit] = quit_command
|
699
|
+
opts.freeze
|
700
|
+
@options = opts
|
701
|
+
end
|
702
|
+
|
703
|
+
|
704
|
+
private
|
705
|
+
|
706
|
+
|
707
|
+
def self.add_hook(hook_name, proc, &block)
|
708
|
+
hooks[hook_name] ||= []
|
709
|
+
|
710
|
+
if proc.respond_to?(:call)
|
711
|
+
hooks[hook_name] << proc
|
712
|
+
elsif proc.is_a?(Symbol) || proc.is_a?(String)
|
713
|
+
if self.respond_to?(proc, true)
|
714
|
+
hooks[hook_name] << method(proc.to_sym)
|
715
|
+
end
|
716
|
+
elsif proc
|
717
|
+
raise ArgumentError, 'proc must respond to :call method or be the name of a static method in this class'
|
718
|
+
end
|
719
|
+
|
720
|
+
if block
|
721
|
+
hooks[hook_name] << block
|
722
|
+
end
|
723
|
+
|
724
|
+
end
|
725
|
+
|
726
|
+
def run_hook(hook_name, *args)
|
727
|
+
(self.class.hooks[hook_name] || []).each do |hook|
|
728
|
+
result = hook.call(self, *args)
|
729
|
+
return true if result.is_a?(TrueClass)
|
730
|
+
end
|
731
|
+
false
|
732
|
+
end
|
733
|
+
|
734
|
+
def self.hooks
|
735
|
+
@hooks ||= {}
|
736
|
+
end
|
737
|
+
|
738
|
+
|
739
|
+
def stdout=(value)
|
740
|
+
@stdout = value
|
741
|
+
end
|
742
|
+
|
743
|
+
def stderr=(value)
|
744
|
+
@stderr = value
|
745
|
+
end
|
746
|
+
|
747
|
+
def combined_output=(value)
|
748
|
+
@stdcomb = value
|
749
|
+
end
|
750
|
+
|
751
|
+
def append_stdout(data, &block)
|
752
|
+
# Combined output gets the prompts,
|
753
|
+
# but stdout will be without prompts.
|
754
|
+
data = reduce_newlines data
|
755
|
+
for_stdout = if (pos = (data =~ prompt_match))
|
756
|
+
data[0...pos]
|
757
|
+
else
|
758
|
+
data
|
759
|
+
end
|
760
|
+
|
761
|
+
self.stdout <<= for_stdout
|
762
|
+
self.combined_output <<= data
|
763
|
+
|
764
|
+
if block_given?
|
765
|
+
result = block.call(for_stdout, :stdout)
|
766
|
+
if result && result.is_a?(String)
|
767
|
+
send_data(result + line_ending)
|
768
|
+
end
|
769
|
+
end
|
770
|
+
end
|
771
|
+
|
772
|
+
def append_stderr(data, &block)
|
773
|
+
data = reduce_newlines data
|
774
|
+
|
775
|
+
self.stderr <<= data
|
776
|
+
self.combined_output <<= data
|
777
|
+
|
778
|
+
if block_given?
|
779
|
+
result = block.call(data, :stderr)
|
780
|
+
if result && result.is_a?(String)
|
781
|
+
send_data(result + line_ending)
|
782
|
+
end
|
783
|
+
end
|
784
|
+
end
|
785
|
+
|
786
|
+
def reduce_newlines(data)
|
787
|
+
data.gsub("\r\n", "\n").gsub(" \r", "").gsub("\r", "")
|
788
|
+
end
|
789
|
+
|
790
|
+
def command_output(command)
|
791
|
+
# get everything except for the ending prompt.
|
792
|
+
ret =
|
793
|
+
if (prompt_pos = combined_output =~ prompt_match)
|
794
|
+
combined_output[0...prompt_pos]
|
795
|
+
else
|
796
|
+
combined_output
|
797
|
+
end
|
798
|
+
|
799
|
+
possible_starts = [
|
800
|
+
command,
|
801
|
+
options[:prompt] + command,
|
802
|
+
options[:prompt] + ' ' + command
|
803
|
+
]
|
804
|
+
|
805
|
+
# Go until we run out of data or we find one of the possible command starts.
|
806
|
+
# Note that we EXPECT the command to the first line of the output from the command because we expect the
|
807
|
+
# shell to echo it back to us.
|
808
|
+
result_cmd,_,result_data = ret.partition("\n")
|
809
|
+
until result_data.to_s.strip == '' || possible_starts.include?(result_cmd)
|
810
|
+
result_cmd,_,result_data = result_data.partition("\n")
|
811
|
+
end
|
812
|
+
|
813
|
+
result_data
|
814
|
+
end
|
815
|
+
|
816
|
+
def strip_ansi_escape(data)
|
817
|
+
data
|
818
|
+
.gsub(/\e\[(\d+;?)*[ABCDEFGHfu]/, "\n") # any of the "set cursor position" CSI commands.
|
819
|
+
.gsub(/\e\[=?(\d+;?)*[A-Za-z]/,'') # \e[#;#;#A or \e[=#;#;#A basically all the CSI commands except ...
|
820
|
+
.gsub(/\e\[(\d+;"[^"]+";?)+p/, '') # \e[#;"A"p
|
821
|
+
.gsub(/\e[NOc]./,'?') # any of the alternate character set commands.
|
822
|
+
.gsub(/\e[P_\]^X][^\e\a]*(\a|(\e\\))/,'') # any string command
|
823
|
+
.gsub(/[\x00\x08\x0B\x0C\x0E-\x1F]/, '') # any non-printable characters (notice \x0A (LF) and \x0D (CR) are left as is).
|
824
|
+
.gsub("\t", ' ') # turn tabs into spaces.
|
825
|
+
end
|
826
|
+
|
827
|
+
def stdout_hist
|
828
|
+
@stdout_hist ||= []
|
829
|
+
end
|
830
|
+
|
831
|
+
def stderr_hist
|
832
|
+
@stderr_hist ||= []
|
833
|
+
end
|
834
|
+
|
835
|
+
def stdcomb_hist
|
836
|
+
@stdcomb_hist ||= []
|
837
|
+
end
|
838
|
+
|
839
|
+
def prompt_match
|
840
|
+
# allow for trailing spaces or tabs, but no other whitespace.
|
841
|
+
@prompt_match ||= /#{@options[:prompt]}[ \t]*$/
|
842
|
+
end
|
843
|
+
|
844
|
+
end
|
845
|
+
|
846
|
+
end
|