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
@@ -0,0 +1,270 @@
|
|
1
|
+
module Shells
|
2
|
+
|
3
|
+
##
|
4
|
+
# A wrapper class around a base shell to execute pfSense PHP commands within.
|
5
|
+
class PfShellWrapper
|
6
|
+
|
7
|
+
##
|
8
|
+
# The pfSense shell itself.
|
9
|
+
PF_SHELL = '/usr/local/sbin/pfSsh.php'
|
10
|
+
|
11
|
+
##
|
12
|
+
# The prompt in the pfSense shell.
|
13
|
+
PF_PROMPT = 'pfSense shell:'
|
14
|
+
|
15
|
+
|
16
|
+
attr_accessor :config_parsed
|
17
|
+
private :config_parsed, :config_parsed=
|
18
|
+
|
19
|
+
attr_accessor :shell
|
20
|
+
private :shell, :shell=
|
21
|
+
|
22
|
+
##
|
23
|
+
# Gets the output from the pfSense PHP shell session.
|
24
|
+
attr_accessor :output
|
25
|
+
|
26
|
+
##
|
27
|
+
# Creates the wrapper, executing the pfSense shell.
|
28
|
+
#
|
29
|
+
# The provided code block is yielded this wrapper for execution.
|
30
|
+
def initialize(base_shell, &block)
|
31
|
+
raise ArgumentError, 'a code block is required' unless block_given?
|
32
|
+
raise ArgumentError, 'the base shell must be a valid shell' unless base_shell.is_a?(::Shells::ShellBase)
|
33
|
+
|
34
|
+
self.shell = base_shell
|
35
|
+
|
36
|
+
wrapper = self
|
37
|
+
code_block = block
|
38
|
+
self.output = ''
|
39
|
+
self.config_parsed = false
|
40
|
+
|
41
|
+
shell.instance_eval do
|
42
|
+
merge_local_buffer do
|
43
|
+
begin
|
44
|
+
temporary_prompt(PF_PROMPT) do
|
45
|
+
debug 'Initializing the pfSense PHP shell...'
|
46
|
+
queue_input PF_SHELL + line_ending
|
47
|
+
wait_for_prompt 999, 10, true
|
48
|
+
|
49
|
+
debug ' > initialized'
|
50
|
+
begin
|
51
|
+
code_block.call wrapper
|
52
|
+
ensure
|
53
|
+
debug 'Exiting the pfSense PHP shell...'
|
54
|
+
if wait_for_prompt(5, 5, false)
|
55
|
+
# only queue the exit command if we are still in the pfSense shell.
|
56
|
+
queue_input 'exit' + line_ending
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
ensure
|
61
|
+
# wait for the normal shell to return.
|
62
|
+
wait_for_prompt 10, 10, true
|
63
|
+
debug ' > exited'
|
64
|
+
wrapper.output = output
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
##
|
71
|
+
# Executes a series of commands on the pfSense shell.
|
72
|
+
def exec(*commands)
|
73
|
+
ret = ''
|
74
|
+
commands.each { |cmd| ret += shell.exec(cmd) }
|
75
|
+
ret + shell.exec('exec')
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Reloads the pfSense configuration on the device.
|
80
|
+
def parse_config
|
81
|
+
exec 'parse_config(true);'
|
82
|
+
self.config_parsed = true
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# Determines if the configuration has been parsed during this session.
|
87
|
+
def config_parsed?
|
88
|
+
config_parsed
|
89
|
+
end
|
90
|
+
|
91
|
+
##
|
92
|
+
# Gets a configuration section from the pfSense device.
|
93
|
+
def get_config_section(section_name)
|
94
|
+
parse_config unless config_parsed?
|
95
|
+
JSON.parse exec("echo json_encode($config[#{section_name.to_s.inspect}]);").strip
|
96
|
+
end
|
97
|
+
|
98
|
+
##
|
99
|
+
# Sets a configuration section to the pfSense device.
|
100
|
+
#
|
101
|
+
# Returns the number of changes made to the configuration.
|
102
|
+
def set_config_section(section_name, values, message = '')
|
103
|
+
current_values = get_config_section(section_name)
|
104
|
+
changes = generate_config_changes("$config[#{section_name.to_s.inspect}]", current_values, values)
|
105
|
+
if changes&.any?
|
106
|
+
if message.to_s.strip == ''
|
107
|
+
message = "Updating #{section_name} section."
|
108
|
+
end
|
109
|
+
changes << "write_config(#{message.inspect});"
|
110
|
+
|
111
|
+
exec(*changes)
|
112
|
+
|
113
|
+
(changes.size - 1)
|
114
|
+
else
|
115
|
+
0
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
##
|
120
|
+
# Apply the firewall configuration.
|
121
|
+
#
|
122
|
+
# You need to apply the firewall configuration after you make changes to aliases, NAT rules, or filter rules.
|
123
|
+
def apply_filter_config
|
124
|
+
exec(
|
125
|
+
'require_once("shaper.inc");',
|
126
|
+
'require_once("filter.inc");',
|
127
|
+
'filter_configure_sync();'
|
128
|
+
)
|
129
|
+
end
|
130
|
+
|
131
|
+
##
|
132
|
+
# Applies the user configuration for the specified user.
|
133
|
+
def apply_user_config(user_id)
|
134
|
+
user_id = user_id.to_i
|
135
|
+
exec(
|
136
|
+
'require_once("auth.inc");',
|
137
|
+
"$user_entry = $config[\"system\"][\"user\"][#{user_id}];",
|
138
|
+
'$user_groups = array();',
|
139
|
+
'foreach ($config["system"]["group"] as $gidx => $group) {',
|
140
|
+
' if (is_array($group["member"])) {',
|
141
|
+
" if (in_array(#{user_id}, $group[\"member\"])) { $user_groups[] = $group[\"name\"]; }",
|
142
|
+
' }',
|
143
|
+
'}',
|
144
|
+
# Intentionally run set_groups before and after to ensure group membership gets fully applied.
|
145
|
+
'local_user_set_groups($user_entry, $user_groups);',
|
146
|
+
'local_user_set($user_entry);',
|
147
|
+
'local_user_set_groups($user_entry, $user_groups);'
|
148
|
+
)
|
149
|
+
end
|
150
|
+
|
151
|
+
##
|
152
|
+
# Enabled public key authentication for the current pfSense user.
|
153
|
+
#
|
154
|
+
# Once this has been done you should be able to connect without using a password.
|
155
|
+
def enable_cert_auth(public_key = '~/.ssh/id_rsa.pub')
|
156
|
+
cert_regex = /^ssh-[rd]sa (?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)? \S*$/m
|
157
|
+
|
158
|
+
# get our cert unless the user provided a full cert for us.
|
159
|
+
unless public_key =~ cert_regex
|
160
|
+
public_key = File.expand_path(public_key)
|
161
|
+
if File.exist?(public_key)
|
162
|
+
public_key = File.read(public_key).to_s.strip
|
163
|
+
else
|
164
|
+
raise Shells::PfSenseCommon::PublicKeyNotFound
|
165
|
+
end
|
166
|
+
raise Shells::PfSenseCommon::PublicKeyInvalid unless public_key =~ cert_regex
|
167
|
+
end
|
168
|
+
|
169
|
+
cfg = get_config_section 'system'
|
170
|
+
user_id = nil
|
171
|
+
user_name = options[:user].downcase
|
172
|
+
cfg['user'].each_with_index do |user,index|
|
173
|
+
if user['name'].downcase == user_name
|
174
|
+
user_id = index
|
175
|
+
|
176
|
+
authkeys = Base64.decode64(user['authorizedkeys'].to_s).gsub("\r\n", "\n").strip
|
177
|
+
unless authkeys == '' || authkeys =~ cert_regex
|
178
|
+
warn "Existing authorized keys for user #{options[:user]} are invalid and are being reset."
|
179
|
+
authkeys = ''
|
180
|
+
end
|
181
|
+
|
182
|
+
if authkeys == ''
|
183
|
+
user['authorizedkeys'] = Base64.strict_encode64(public_key)
|
184
|
+
else
|
185
|
+
authkeys = authkeys.split("\n")
|
186
|
+
unless authkeys.include?(public_key)
|
187
|
+
authkeys << public_key unless authkeys.include?(public_key)
|
188
|
+
user['authorizedkeys'] = Base64.strict_encode64(authkeys.join("\n"))
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
break
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
raise Shells::PfSenseCommon::UserNotFound unless user_id
|
198
|
+
|
199
|
+
set_config_section 'system', cfg, "Enable certificate authentication for #{options[:user]}."
|
200
|
+
|
201
|
+
apply_user_config user_id
|
202
|
+
end
|
203
|
+
|
204
|
+
|
205
|
+
##
|
206
|
+
# Exits the shell session immediately and requests a reboot of the pfSense device.
|
207
|
+
def reboot
|
208
|
+
raise Shells::NotRunning unless running?
|
209
|
+
raise Shells::PfSenseCommon::RestartNow
|
210
|
+
end
|
211
|
+
|
212
|
+
##
|
213
|
+
# Exits the shell session immediately.
|
214
|
+
def quit
|
215
|
+
raise Shells::NotRunning unless running?
|
216
|
+
raise Shells::ShellBase::QuitNow
|
217
|
+
end
|
218
|
+
|
219
|
+
private
|
220
|
+
|
221
|
+
def generate_config_changes(prefix, old_value, new_value)
|
222
|
+
old_value = fix_config_arrays(old_value)
|
223
|
+
new_value = fix_config_arrays(new_value)
|
224
|
+
|
225
|
+
if new_value.is_a?(Hash)
|
226
|
+
changes = []
|
227
|
+
|
228
|
+
unless old_value.is_a?(Hash)
|
229
|
+
# make sure the value is an array now.
|
230
|
+
changes << "#{prefix} = array();"
|
231
|
+
# and change the old_value to be an empty hash so we can work with it.
|
232
|
+
old_value = {}
|
233
|
+
end
|
234
|
+
|
235
|
+
# now iterate the hashes and process the child elements.
|
236
|
+
new_value.each do |k, new_v|
|
237
|
+
old_v = old_value[k]
|
238
|
+
changes += generate_config_changes("#{prefix}[#{k.inspect}]", old_v, new_v)
|
239
|
+
end
|
240
|
+
|
241
|
+
changes
|
242
|
+
else
|
243
|
+
if new_value != old_value
|
244
|
+
if new_value.nil?
|
245
|
+
[ "unset #{prefix};" ]
|
246
|
+
else
|
247
|
+
[ "#{prefix} = #{new_value.inspect};" ]
|
248
|
+
end
|
249
|
+
else
|
250
|
+
[ ]
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def fix_config_arrays(value)
|
256
|
+
if value.is_a?(Array)
|
257
|
+
value.each_with_index
|
258
|
+
.map{|v,i| [i,v]}.to_h # convert to hash
|
259
|
+
.inject({}){ |m,(k,v)| m[k.to_s] = v; m } # stringify keys
|
260
|
+
elsif value.is_a?(Hash)
|
261
|
+
value.inject({}) { |m,(k,v)| m[k.to_s] = v; m } # stringify keys
|
262
|
+
else
|
263
|
+
value
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
end
|
268
|
+
|
269
|
+
|
270
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'shells/serial_shell'
|
2
|
+
require 'shells/bash_common'
|
3
|
+
|
4
|
+
module Shells
|
5
|
+
##
|
6
|
+
# Executes a serial session with a device.
|
7
|
+
#
|
8
|
+
# The default setup of this class should work well with any bash-like shell.
|
9
|
+
#
|
10
|
+
# Valid options:
|
11
|
+
# +path+::
|
12
|
+
# The path to the serial device (e.g. - COM3 or /dev/tty2)
|
13
|
+
# This is a required option.
|
14
|
+
# +speed+::
|
15
|
+
# The bitrate for the connection.
|
16
|
+
# The default is 115200.
|
17
|
+
# +data_bits+::
|
18
|
+
# The number of data bits for the connection.
|
19
|
+
# The default is 8.
|
20
|
+
# +parity+::
|
21
|
+
# The parity for the connection.
|
22
|
+
# The default is :none.
|
23
|
+
# +prompt+::
|
24
|
+
# The prompt used to determine when processes finish execution.
|
25
|
+
# +quit+::
|
26
|
+
# If set, this defines the command to execute when quitting the session.
|
27
|
+
# The default is "exit" which will probably work most of the time.
|
28
|
+
# +retrieve_exit_code+::
|
29
|
+
# If set to a non-false value, then the default behavior will be to retrieve the exit code from the shell after
|
30
|
+
# executing a command. If set to a false or nil value, the default behavior will be to ignore the exit code
|
31
|
+
# from the shell. When retrieved, the exit code is stored in the +last_exit_code+ property.
|
32
|
+
# This option can be overridden by providing an alternate value to the +exec+ method on a case-by-case basis.
|
33
|
+
# +on_non_zero_exit_code+::
|
34
|
+
# If set to :ignore (the default) then non-zero exit codes will not cause errors. You will still be able to check
|
35
|
+
# the +last_exit_code+ property to determine if the command was successful.
|
36
|
+
# If set to :raise then non-zero exit codes will cause a Shells::NonZeroExitCode to be raised when a command exits
|
37
|
+
# with a non-zero return value.
|
38
|
+
# This option only comes into play when +retrieve_exit_code+ is set to a non-false value.
|
39
|
+
# This option can be overridden by providing an alternate value to the +exec+ method on a case-by-case basis.
|
40
|
+
# +silence_timeout+::
|
41
|
+
# When a command is executing, this is the maximum amount of time to wait for any feedback from the shell.
|
42
|
+
# If set to 0 (or less) there is no timeout.
|
43
|
+
# Unlike +command_timeout+ this value resets every time we receive feedback.
|
44
|
+
# This option can be overridden by providing an alternate value to the +exec+ method on a case-by-case basis.
|
45
|
+
# +command_timeout+::
|
46
|
+
# When a command is executing, this is the maximum amount of time to wait for the command to finish.
|
47
|
+
# If set to 0 (or less) there is no timeout.
|
48
|
+
# Unlike +silence_timeout+ this value does not reset when we receive feedback.
|
49
|
+
# This option can be overridden by providing an alternate value to the +exec+ method on a case-by-case basis.
|
50
|
+
#
|
51
|
+
# Shells::SerialBashShell.new(
|
52
|
+
# path: '/dev/ttyusb3',
|
53
|
+
# speed: 9600
|
54
|
+
# ) do |shell|
|
55
|
+
# shell.exec('cd /usr/local/bin')
|
56
|
+
# user_bin_files = shell.exec('ls -A1').split("\n")
|
57
|
+
# @app_is_installed = user_bin_files.include?('my_app')
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
class SerialBashShell < SerialShell
|
61
|
+
|
62
|
+
include Shells::BashCommon
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'shells/
|
1
|
+
require 'shells/serial_bash_shell'
|
2
2
|
require 'shells/pf_sense_common'
|
3
3
|
|
4
4
|
module Shells
|
@@ -30,23 +30,25 @@ module Shells
|
|
30
30
|
# Unlike +silence_timeout+ this value does not reset when we receive feedback.
|
31
31
|
# This option can be overridden by providing an alternate value to the +exec+ method on a case-by-case basis.
|
32
32
|
#
|
33
|
-
# Shells::
|
33
|
+
# Shells::SerialPfSenseShell.new(
|
34
34
|
# path: 'COM3'
|
35
35
|
# ) do |shell|
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
36
|
+
# shell.pf_shell do |shell|
|
37
|
+
# cfg = shell.get_config_section("aliases")
|
38
|
+
# cfg["alias"] ||= []
|
39
|
+
# cfg["alias"] << {
|
40
|
+
# :name => "MY_NETWORK",
|
41
|
+
# :type => "network",
|
42
|
+
# :address => "192.168.1.0/24",
|
43
|
+
# :descr => "My home network",
|
44
|
+
# :details => "Created #{Time.now.to_s}"
|
45
|
+
# }
|
46
|
+
# shell.set_config_section("aliases", cfg, "Add home network")
|
47
|
+
# shell.apply_filter_config
|
48
|
+
# end
|
47
49
|
# end
|
48
50
|
#
|
49
|
-
class
|
51
|
+
class SerialPfSenseShell < SerialShell
|
50
52
|
|
51
53
|
include PfSenseCommon
|
52
54
|
|
@@ -1,18 +1,10 @@
|
|
1
1
|
require 'rubyserial'
|
2
2
|
require 'shells/shell_base'
|
3
|
-
require 'shells/bash_common'
|
4
3
|
|
5
4
|
module Shells
|
6
5
|
##
|
7
6
|
# Executes a serial session with a device.
|
8
7
|
#
|
9
|
-
# The default setup of this class should work well with any bash-like shell.
|
10
|
-
# In particular, the +exec_prompt+ method sets the "PS1" environment variable, which should set the prompt the shell
|
11
|
-
# uses, and the +get_exit_code+ methods retrieves the value of the "$?" variable which should contain the exit code
|
12
|
-
# from the last action. Because there is a possibility that your shell does not utilize those methods, the
|
13
|
-
# +override_set_prompt+ and +override_get_exit_code+ options are available to change the behavior.
|
14
|
-
#
|
15
|
-
#
|
16
8
|
# Valid options:
|
17
9
|
# +path+::
|
18
10
|
# The path to the serial device (e.g. - COM3 or /dev/tty2)
|
@@ -28,11 +20,6 @@ module Shells
|
|
28
20
|
# The default is :none.
|
29
21
|
# +prompt+::
|
30
22
|
# The prompt used to determine when processes finish execution.
|
31
|
-
# Defaults to '~~#', but if that doesn't work for some reason because it is valid output from one or more
|
32
|
-
# commands, you can change it to something else. It must be unique and cannot contain certain characters.
|
33
|
-
# The characters you should avoid are !, $, \, /, ", and ' because no attempt is made to escape them and the
|
34
|
-
# resulting prompt can very easily become something else entirely. If they are provided, they will be
|
35
|
-
# replaced to protect the shell from getting stuck.
|
36
23
|
# +quit+::
|
37
24
|
# If set, this defines the command to execute when quitting the session.
|
38
25
|
# The default is "exit" which will probably work most of the time.
|
@@ -58,21 +45,8 @@ module Shells
|
|
58
45
|
# If set to 0 (or less) there is no timeout.
|
59
46
|
# Unlike +silence_timeout+ this value does not reset when we receive feedback.
|
60
47
|
# This option can be overridden by providing an alternate value to the +exec+ method on a case-by-case basis.
|
61
|
-
# +override_set_prompt+::
|
62
|
-
# If provided, this must be set to either a command string that will set the prompt, or a Proc that accepts
|
63
|
-
# the shell as an argument.
|
64
|
-
# If set to a string, the string is sent to the shell and we wait up to two seconds for the prompt to appear.
|
65
|
-
# If that fails, we resend the string and wait one more time before failing.
|
66
|
-
# If set to a Proc, the Proc is called. If the Proc returns a false value, we fail. If the Proc returns
|
67
|
-
# a non-false value, we consider it successful.
|
68
|
-
# +override_get_exit_code+::
|
69
|
-
# If provided, this must be set to either a command string that will retrieve the exit code, or a Proc that
|
70
|
-
# accepts the shell as an argument.
|
71
|
-
# If set to a string, the string is sent to the shell and the output is parsed as an integer and used as the exit
|
72
|
-
# code.
|
73
|
-
# If set to a Proc, the Proc is called and the return value of the proc is used as the exit code.
|
74
48
|
#
|
75
|
-
# Shells::
|
49
|
+
# Shells::SerialShell.new(
|
76
50
|
# path: '/dev/ttyusb3',
|
77
51
|
# speed: 9600
|
78
52
|
# ) do |shell|
|
@@ -81,9 +55,30 @@ module Shells
|
|
81
55
|
# @app_is_installed = user_bin_files.include?('my_app')
|
82
56
|
# end
|
83
57
|
#
|
84
|
-
class
|
58
|
+
class SerialShell < Shells::ShellBase
|
59
|
+
|
60
|
+
attr_accessor :serport
|
61
|
+
private :serport, :serport=
|
62
|
+
|
63
|
+
attr_accessor :ser_stdout_recv
|
64
|
+
private :ser_stdout_recv, :ser_stdout_recv=
|
65
|
+
|
66
|
+
attr_accessor :output_reader
|
67
|
+
private :output_reader, :output_reader=
|
68
|
+
|
69
|
+
add_hook :on_before_run do |sh|
|
70
|
+
sh.instance_eval do
|
71
|
+
self.serport = nil
|
72
|
+
self.output_reader = nil
|
73
|
+
end
|
74
|
+
end
|
85
75
|
|
86
|
-
|
76
|
+
add_hook :on_after_run do |sh|
|
77
|
+
sh.instance_eval do
|
78
|
+
self.serport = nil
|
79
|
+
self.output_reader = nil
|
80
|
+
end
|
81
|
+
end
|
87
82
|
|
88
83
|
##
|
89
84
|
# Sets the line ending for the instance.
|
@@ -109,75 +104,68 @@ module Shells
|
|
109
104
|
raise InvalidOption, 'Missing path.' if options[:path].to_s.strip == ''
|
110
105
|
end
|
111
106
|
|
112
|
-
def
|
113
|
-
|
107
|
+
def connect #:nodoc:
|
114
108
|
debug 'Opening serial port...'
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
109
|
+
self.serport = Serial.new(options[:path], options[:speed], options[:data_bits], options[:parity])
|
110
|
+
debug 'Starting output reading thread...'
|
111
|
+
self.output_reader = Thread.start(self) do |shell|
|
112
|
+
while true
|
113
|
+
shell.instance_eval do
|
114
|
+
data = ''
|
115
|
+
while (byte = serport&.getbyte)
|
116
|
+
data << byte.chr
|
117
|
+
end
|
118
|
+
if data != ''
|
119
|
+
# add to the output buffer.
|
120
|
+
debug "Received: (#{data.size} bytes) #{(data.size > 32 ? (data[0..30] + '...') : data).inspect}"
|
121
|
+
ser_stdout_recv&.call data
|
122
|
+
end
|
123
|
+
end
|
124
|
+
Thread.pass
|
125
|
+
end
|
126
|
+
end
|
123
127
|
|
124
|
-
|
125
|
-
# send the quit message.
|
126
|
-
send_data options[:quit] + line_ending
|
128
|
+
end
|
127
129
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
end
|
130
|
+
def disconnect #:nodoc:
|
131
|
+
output_reader&.exit
|
132
|
+
serport.close
|
132
133
|
end
|
133
134
|
|
134
|
-
def
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
else
|
139
|
-
# set the prompt, wait up to 2 seconds for a response, then try one more time.
|
140
|
-
begin
|
141
|
-
exec cmd, command_timeout: 2, retrieve_exit_code: false, command_is_echoed: false
|
142
|
-
rescue Shells::CommandTimeout
|
143
|
-
begin
|
144
|
-
exec cmd, command_timeout: 2, retrieve_exit_code: false, command_is_echoed: false
|
145
|
-
rescue Shells::CommandTimeout
|
146
|
-
raise Shells::FailedToSetPrompt
|
147
|
-
end
|
148
|
-
end
|
149
|
-
end
|
135
|
+
def setup
|
136
|
+
# send a newline to the shell to (hopefully) redraw a menu.
|
137
|
+
debug 'Refreshing...'
|
138
|
+
queue_input line_ending
|
150
139
|
|
151
|
-
|
152
|
-
|
140
|
+
debug 'Calling setup_prompt...'
|
141
|
+
setup_prompt
|
142
|
+
debug ' > prompt setup'
|
153
143
|
end
|
154
144
|
|
155
145
|
def send_data(data) #:nodoc:
|
156
|
-
|
146
|
+
serport.write data
|
157
147
|
debug "Sent: (#{data.size} bytes) #{(data.size > 32 ? (data[0..30] + '...') : data).inspect}"
|
158
148
|
end
|
159
149
|
|
160
|
-
def
|
150
|
+
def active?
|
151
|
+
return false if serport.nil?
|
152
|
+
return false if serport.closed?
|
153
|
+
true
|
154
|
+
end
|
155
|
+
|
156
|
+
def io_loop(&block) #:nodoc:
|
161
157
|
while true
|
162
|
-
|
163
|
-
|
164
|
-
while (byte = @serport.getbyte)
|
165
|
-
data << byte.chr
|
166
|
-
end
|
167
|
-
break if data == ""
|
168
|
-
debug "Received: (#{data.size} bytes) #{(data.size > 32 ? (data[0..30] + '...') : data).inspect}"
|
169
|
-
@_stdout_recv.call data
|
170
|
-
end
|
171
|
-
break unless block&.call
|
158
|
+
break unless block.call
|
159
|
+
Thread.pass
|
172
160
|
end
|
173
161
|
end
|
174
162
|
|
175
163
|
def stdout_received(&block) #:nodoc:
|
176
|
-
|
164
|
+
sync { self.ser_stdout_recv = block }
|
177
165
|
end
|
178
166
|
|
179
167
|
def stderr_received(&block) #:nodoc:
|
180
|
-
|
168
|
+
nil # no stderr to report.
|
181
169
|
end
|
182
170
|
|
183
171
|
end
|