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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c603e7ea0fcbda5d3e982acf28cd42b1f8eaa5aa7d553f8b4e6e0e783ccf3ec
4
- data.tar.gz: 0c70002f557ad71cee9a12f32998c808fe6d984d24c82a1a11c9f7f0a04655e4
3
+ metadata.gz: 479372e0f9e6a333d880bdf4ff6e1956070292b4983074b37cc51eaba300b59a
4
+ data.tar.gz: 16809adcd8badca90511c7ba97d4c081ae52cf20e37e812891af313b0bf6592a
5
5
  SHA512:
6
- metadata.gz: 4f806eb1c4a035b586ce2b2e3783db4a8ba77d4143e7b916686fe95d54a0f5f748139245265b7cca18a52cdf7cbdb28de3f54c71bcbbb8cad15e03eeeb7599b6
7
- data.tar.gz: ff6953edf2520f5732ee7b896a30e6fa184506093addb7e39b0c8cb69eb5735f928291047f989a1ba100f2d5a317cda5608cea1c97624eaac6b284c83e34b923
6
+ metadata.gz: 7b768213f3ccbc060411c96f0051b01b8a87339122f65fabc4c1898d1850fe06d7ffbcee437e5ddb627838a70f861cb3790fe2ddeaf1ee341545b8168cbfd63b
7
+ data.tar.gz: adb4c879d5f74e14924d9ab041a83512ca4ae551fc634d482a6ef5bd787eb8267ee4c54ff97f59a312af195fbdb1bed1d109c4b2d6fc0d59e6e132db91258640
@@ -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
- self.process_count += 1
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 write(content = String.new)
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 write
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("#read: \n#{var}")
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
- future = Time.now + seconds
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
- # prove a block where the default prompt changes
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
- def read_till(prompt: current_prompt, timeout: read_till_timeout, **_opts)
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
- hard_timeout = timeout
179
- hard_timeout += 0.5 if timeout
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 stdout[current_prompt] do
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.1
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
- # 'read' first on purpuse as a feature. once you cmd you ignore what happend before. otherwise use read|write directly.
206
- # this should avoid many horrible state issues where the prompt is not the last prompt
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
- def rm_prompt!(output, **opts)
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
- prompt.is_a?(Regexp) ? output[prompt, 1] = '' : output[prompt] = ''
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
@@ -3,7 +3,7 @@
3
3
  module Net
4
4
  module SSH
5
5
  module CLI
6
- VERSION = '1.5.0'
6
+ VERSION = '1.6.0'
7
7
  end
8
8
  end
9
9
  end
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.5.0
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-03-13 00:00:00.000000000 Z
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.0.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'