net-ssh-cli 1.5.0 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|