net-ssh-cli 1.5.0 → 1.6.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/lib/net/ssh/cli.rb +116 -24
- data/lib/net/ssh/cli/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 479372e0f9e6a333d880bdf4ff6e1956070292b4983074b37cc51eaba300b59a
|
|
4
|
+
data.tar.gz: 16809adcd8badca90511c7ba97d4c081ae52cf20e37e812891af313b0bf6592a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7b768213f3ccbc060411c96f0051b01b8a87339122f65fabc4c1898d1850fe06d7ffbcee437e5ddb627838a70f861cb3790fe2ddeaf1ee341545b8168cbfd63b
|
|
7
|
+
data.tar.gz: adb4c879d5f74e14924d9ab041a83512ca4ae551fc634d482a6ef5bd787eb8267ee4c54ff97f59a312af195fbdb1bed1d109c4b2d6fc0d59e6e132db91258640
|
data/lib/net/ssh/cli.rb
CHANGED
|
@@ -45,15 +45,23 @@ module Net
|
|
|
45
45
|
cmd_rm_command: false, # whether the given command should be removed in the output of #cmd
|
|
46
46
|
run_impact: false, # whether to run #impact commands. This might align with testing|development|production. example #impact("reboot")
|
|
47
47
|
read_till_timeout: nil, # timeout for #read_till to find the match
|
|
48
|
+
read_till_hard_timeout: nil, # hard timeout for #read_till to find the match using Timeout.timeout(hard_timeout) {}. Might creates unpredicted sideffects
|
|
49
|
+
read_till_hard_timeout_factor: 1.2, # hard timeout factor in case read_till_hard_timeout is true
|
|
48
50
|
named_prompts: ActiveSupport::HashWithIndifferentAccess.new, # you can used named prompts for #with_prompt {}
|
|
51
|
+
before_cmd_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call before #cmd
|
|
52
|
+
after_cmd_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call after #cmd
|
|
49
53
|
before_on_stdout_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call before data arrives from the underlying connection
|
|
50
54
|
after_on_stdout_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call after data arrives from the underlying connection
|
|
55
|
+
before_on_stdin_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call before data is sent to the underlying channel
|
|
56
|
+
after_on_stdin_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call after data is sent to the underlying channel
|
|
51
57
|
before_open_channel_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call before opening a channel
|
|
52
58
|
after_open_channel_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call after opening a channel, for example you could call #detect_prompt or #read_till
|
|
53
59
|
open_channel_timeout: nil, # timeout to open the channel
|
|
54
60
|
net_ssh_options: ActiveSupport::HashWithIndifferentAccess.new, # a wrapper for options to pass to Net::SSH.start in case net_ssh is undefined
|
|
55
61
|
process_time: 0.00001, # how long #process is processing net_ssh#process or sleeping (waiting for something)
|
|
56
62
|
background_processing: false, # default false, whether the process method maps to the underlying net_ssh#process or the net_ssh#process happens in a separate loop
|
|
63
|
+
on_stdout_processing: 100, # whether to optimize the on_stdout performance by calling #process #optimize_on_stdout-times in case more data arrives
|
|
64
|
+
sleep_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call instead of Kernel.sleep(), perfect for async hooks
|
|
57
65
|
)
|
|
58
66
|
|
|
59
67
|
def options
|
|
@@ -108,19 +116,19 @@ module Net
|
|
|
108
116
|
before_on_stdout_procs.each { |_name, a_proc| instance_eval(&a_proc) }
|
|
109
117
|
stdout << new_data
|
|
110
118
|
after_on_stdout_procs.each { |_name, a_proc| instance_eval(&a_proc) }
|
|
111
|
-
|
|
112
|
-
process unless process_count > 100 # if we receive data, we probably receive more - improves performance - but on a lot of data, this leads to a stack level too deep
|
|
113
|
-
self.process_count -= 1
|
|
119
|
+
optimise_stdout_processing
|
|
114
120
|
stdout
|
|
115
121
|
end
|
|
116
122
|
|
|
117
|
-
def
|
|
123
|
+
def stdin(content = String.new)
|
|
118
124
|
logger.debug { "#write #{content.inspect}" }
|
|
125
|
+
before_on_stdin_procs.each { |_name, a_proc| instance_eval(&a_proc) }
|
|
119
126
|
channel.send_data content
|
|
120
127
|
process
|
|
128
|
+
after_on_stdin_procs.each { |_name, a_proc| instance_eval(&a_proc) }
|
|
121
129
|
content
|
|
122
130
|
end
|
|
123
|
-
alias stdin
|
|
131
|
+
alias write stdin
|
|
124
132
|
|
|
125
133
|
def write_n(content = String.new)
|
|
126
134
|
write content + "\n"
|
|
@@ -129,7 +137,7 @@ module Net
|
|
|
129
137
|
def read
|
|
130
138
|
process
|
|
131
139
|
var = stdout!
|
|
132
|
-
logger.debug
|
|
140
|
+
logger.debug { "#read: \n#{var}" }
|
|
133
141
|
var
|
|
134
142
|
end
|
|
135
143
|
|
|
@@ -140,6 +148,16 @@ module Net
|
|
|
140
148
|
with_prompts[-1] || default_prompt
|
|
141
149
|
end
|
|
142
150
|
|
|
151
|
+
# run something with a different named prompt
|
|
152
|
+
#
|
|
153
|
+
# named_prompts["root"] = /(?<prompt>\nroot)\z/
|
|
154
|
+
#
|
|
155
|
+
# with_named_prompt("root") do
|
|
156
|
+
# cmd("sudo -i")
|
|
157
|
+
# cmd("cat /etc/passwd")
|
|
158
|
+
# end
|
|
159
|
+
# cmd("exit")
|
|
160
|
+
#
|
|
143
161
|
def with_named_prompt(name)
|
|
144
162
|
raise Error::UndefinedMatch, "unknown named_prompt #{name}" unless named_prompts[name]
|
|
145
163
|
|
|
@@ -148,20 +166,25 @@ module Net
|
|
|
148
166
|
end
|
|
149
167
|
end
|
|
150
168
|
|
|
169
|
+
# tries to detect the prompt
|
|
170
|
+
# sends a "\n", waits for a X seconds, and uses the last line as prompt
|
|
171
|
+
# this won't work reliable if the prompt changes during the session
|
|
151
172
|
def detect_prompt(seconds: 3)
|
|
152
173
|
write_n
|
|
153
|
-
|
|
154
|
-
while future > Time.now
|
|
155
|
-
process
|
|
156
|
-
sleep 0.1
|
|
157
|
-
end
|
|
174
|
+
process(seconds)
|
|
158
175
|
self.default_prompt = read[/\n?^.*\z/]
|
|
159
176
|
raise Error::PromptDetection, "couldn't detect a prompt" unless default_prompt.present?
|
|
160
177
|
|
|
161
178
|
default_prompt
|
|
162
179
|
end
|
|
163
180
|
|
|
164
|
-
#
|
|
181
|
+
# run something with a different prompt
|
|
182
|
+
#
|
|
183
|
+
# with_prompt(/(?<prompt>\nroot)\z/) do
|
|
184
|
+
# cmd("sudo -i")
|
|
185
|
+
# cmd("cat /etc/passwd")
|
|
186
|
+
# end
|
|
187
|
+
# cmd("exit")
|
|
165
188
|
def with_prompt(prompt)
|
|
166
189
|
logger.debug { "#with_prompt: #{current_prompt.inspect} => #{prompt.inspect}" }
|
|
167
190
|
with_prompts << prompt
|
|
@@ -172,26 +195,48 @@ module Net
|
|
|
172
195
|
logger.debug { "#with_prompt: => #{current_prompt.inspect}" }
|
|
173
196
|
end
|
|
174
197
|
|
|
175
|
-
|
|
198
|
+
# continues to process the ssh connection till #stdout matches the given prompt.
|
|
199
|
+
# might raise a timeout error if a soft/hard timeout is given
|
|
200
|
+
# be carefull when using the hard_timeout, this is using the dangerous Timeout.timeout
|
|
201
|
+
# this gets really slow on large outputs, since the prompt will be searched in the whole output. Use \z in the regex if possible
|
|
202
|
+
#
|
|
203
|
+
# Optional named arguments:
|
|
204
|
+
# - prompt: expected to be a regex
|
|
205
|
+
# - timeout: nil or a number
|
|
206
|
+
# - hard_timeout: nil, true, or a number
|
|
207
|
+
# - hard_timeout_factor: nil, true, or a number
|
|
208
|
+
# - when hard_timeout == true, this will set the hard_timeout as (read_till_hard_timeout_factor * read_till_timeout), defaults to 1.2 = +20%
|
|
209
|
+
def read_till(prompt: current_prompt, timeout: read_till_timeout, hard_timeout: read_till_hard_timeout, hard_timeout_factor: read_till_hard_timeout_factor, **_opts)
|
|
176
210
|
raise Error::UndefinedMatch, 'no prompt given or default_prompt defined' unless prompt
|
|
211
|
+
hard_timeout = (read_till_hard_timeout_factor * timeout) if timeout and hard_timeout == true
|
|
212
|
+
hard_timeout = nil if hard_timeout == true
|
|
177
213
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
::Timeout.timeout(hard_timeout, Error::ReadTillTimeout, "#{current_prompt.inspect} didn't match on #{stdout.inspect} within #{timeout}s") do
|
|
181
|
-
with_prompt(prompt) do
|
|
214
|
+
with_prompt(prompt) do
|
|
215
|
+
::Timeout.timeout(hard_timeout, Error::ReadTillTimeout, "#{current_prompt.inspect} didn't match on #{stdout.inspect} within #{hard_timeout}s") do
|
|
182
216
|
soft_timeout = Time.now + timeout if timeout
|
|
183
|
-
until
|
|
217
|
+
until prompt_in_stdout? do
|
|
184
218
|
if timeout and soft_timeout < Time.now
|
|
185
219
|
raise Error::ReadTillTimeout, "#{current_prompt.inspect} didn't match on #{stdout.inspect} within #{timeout}s"
|
|
186
220
|
end
|
|
187
221
|
process
|
|
188
|
-
sleep 0.
|
|
222
|
+
sleep 0.01 # don't race for CPU
|
|
189
223
|
end
|
|
190
224
|
end
|
|
191
225
|
end
|
|
192
226
|
read
|
|
193
227
|
end
|
|
194
228
|
|
|
229
|
+
def prompt_in_stdout?
|
|
230
|
+
case current_prompt
|
|
231
|
+
when Regexp
|
|
232
|
+
!!stdout[current_prompt]
|
|
233
|
+
when String
|
|
234
|
+
stdout.include?(current_prompt)
|
|
235
|
+
else
|
|
236
|
+
raise Net::SSH::CLI::Error, "prompt/current_prompt is not a String/Regex #{current_prompt.inspect}"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
195
240
|
def read_for(seconds:)
|
|
196
241
|
process(seconds)
|
|
197
242
|
read
|
|
@@ -202,18 +247,25 @@ module Net
|
|
|
202
247
|
cmd(command, **opts)
|
|
203
248
|
end
|
|
204
249
|
|
|
205
|
-
#
|
|
206
|
-
#
|
|
250
|
+
# send a command and get the output as return value
|
|
251
|
+
# 1. sends the given command to the ssh connection channel
|
|
252
|
+
# 2. continues to process the ssh connection until the prompt is found in the stdout
|
|
253
|
+
# 3. prepares the output using your callbacks
|
|
254
|
+
# 4. returns the output of your command
|
|
255
|
+
# Hint: 'read' first on purpuse as a feature. once you cmd you ignore what happend before. otherwise use read|write directly.
|
|
256
|
+
# this should avoid many horrible state issues where the prompt is not the last prompt
|
|
207
257
|
def cmd(command, pre_read: true, rm_prompt: cmd_rm_prompt, rm_command: cmd_rm_command, prompt: current_prompt, **opts)
|
|
208
258
|
opts = opts.clone.merge(pre_read: pre_read, rm_prompt: rm_prompt, rm_command: rm_command, prompt: prompt)
|
|
209
259
|
if pre_read
|
|
210
260
|
pre_read_data = read
|
|
211
261
|
logger.debug { "#cmd ignoring pre-command output: #{pre_read_data.inspect}" } if pre_read_data.present?
|
|
212
262
|
end
|
|
263
|
+
before_cmd_procs.each { |_name, a_proc| instance_eval(&a_proc) }
|
|
213
264
|
write_n command
|
|
214
265
|
output = read_till(**opts)
|
|
215
266
|
rm_prompt!(output, **opts)
|
|
216
267
|
rm_command!(output, command, **opts)
|
|
268
|
+
after_cmd_procs.each { |_name, a_proc| instance_eval(&a_proc) }
|
|
217
269
|
output
|
|
218
270
|
rescue Error::ReadTillTimeout => error
|
|
219
271
|
raise Error::CMD, "#{error.message} after cmd #{command.inspect} was sent"
|
|
@@ -221,6 +273,7 @@ module Net
|
|
|
221
273
|
alias command cmd
|
|
222
274
|
alias exec cmd
|
|
223
275
|
|
|
276
|
+
# Execute multiple cmds, see #cmd
|
|
224
277
|
def cmds(*commands, **opts)
|
|
225
278
|
commands.flatten.map { |command| [command, cmd(command, **opts)] }
|
|
226
279
|
end
|
|
@@ -230,11 +283,26 @@ module Net
|
|
|
230
283
|
output[command + "\n"] = '' if rm_command?(**opts) && output[command + "\n"]
|
|
231
284
|
end
|
|
232
285
|
|
|
233
|
-
|
|
286
|
+
# removes the prompt from the given output
|
|
287
|
+
# prompt should contain a named match 'prompt' /(?<prompt>.*something.*)\z/
|
|
288
|
+
# for backwards compatibility it also tries to replace the first match of the prompt /(something)\z/
|
|
289
|
+
# it removes the whole match if no matches are given /something\z/
|
|
290
|
+
def rm_prompt!(output, prompt: current_prompt, **opts)
|
|
234
291
|
if rm_prompt?(**opts)
|
|
235
|
-
prompt = opts[:prompt] || current_prompt
|
|
236
292
|
if output[prompt]
|
|
237
|
-
|
|
293
|
+
case prompt
|
|
294
|
+
when String then output[prompt] = ''
|
|
295
|
+
when Regexp
|
|
296
|
+
if prompt.names.include?("prompt")
|
|
297
|
+
output[prompt, "prompt"] = ''
|
|
298
|
+
else
|
|
299
|
+
begin
|
|
300
|
+
output[prompt, 1] = ''
|
|
301
|
+
rescue IndexError
|
|
302
|
+
output[prompt] = ''
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
238
306
|
end
|
|
239
307
|
end
|
|
240
308
|
end
|
|
@@ -261,6 +329,19 @@ module Net
|
|
|
261
329
|
alias hostname host
|
|
262
330
|
alias to_s host
|
|
263
331
|
|
|
332
|
+
# if #sleep_procs are set, they will be called instead of Kernel.sleep
|
|
333
|
+
# great for async
|
|
334
|
+
# .sleep_procs["async"] = proc do |duration| async_reactor.sleep(duration) end
|
|
335
|
+
#
|
|
336
|
+
# cli.sleep(1)
|
|
337
|
+
def sleep(duration)
|
|
338
|
+
if sleep_procs.any?
|
|
339
|
+
sleep_procs.each { |_name, a_proc| instance_exec(duration, &a_proc) }
|
|
340
|
+
else
|
|
341
|
+
Kernel.sleep(duration)
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
264
345
|
## NET::SSH
|
|
265
346
|
#
|
|
266
347
|
|
|
@@ -333,6 +414,17 @@ module Net
|
|
|
333
414
|
def rm_command?(**opts)
|
|
334
415
|
opts[:rm_cmd].nil? ? cmd_rm_command : opts[:rm_cmd]
|
|
335
416
|
end
|
|
417
|
+
|
|
418
|
+
# when new data is beeing received, likely more data will arrive - this improves the performance by a large factor
|
|
419
|
+
# but on a lot of data, this leads to a stack level too deep
|
|
420
|
+
# therefore it is limited to max #on_stdout_processing
|
|
421
|
+
# the bigger on_stdout_processing, the closer we get to a stack level too deep
|
|
422
|
+
def optimise_stdout_processing
|
|
423
|
+
self.process_count += 1
|
|
424
|
+
process unless process_count > on_stdout_processing
|
|
425
|
+
ensure
|
|
426
|
+
self.process_count -= 1
|
|
427
|
+
end
|
|
336
428
|
end
|
|
337
429
|
end
|
|
338
430
|
end
|
data/lib/net/ssh/cli/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: net-ssh-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Fabian Stillhart
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2020-
|
|
11
|
+
date: 2020-05-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -140,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
140
140
|
- !ruby/object:Gem::Version
|
|
141
141
|
version: '0'
|
|
142
142
|
requirements: []
|
|
143
|
-
rubygems_version: 3.
|
|
143
|
+
rubygems_version: 3.1.2
|
|
144
144
|
signing_key:
|
|
145
145
|
specification_version: 4
|
|
146
146
|
summary: 'Net::SSH::CLI: A library to handle CLI Sessions'
|