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