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 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'