shells 0.1.23 → 0.2.0

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