net-ssh-cli 1.3.0 → 1.7.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: 2e89dcb750ca04e0f861e75a15f7b43f5c18a9409857bd1c44e1458227d2e153
4
- data.tar.gz: f9e6ac90efa410f49401c04a4f66fa26e5977739005feba233b200d3336a41cf
3
+ metadata.gz: 6dd5a361663ac8c45079e1833688679c699f0022c1aa222f5e315992b2457fa3
4
+ data.tar.gz: d10d53f383ea82e100ff38ca46978fb27fbbaec1f49a8e6c4cd0e8cbca7abf9d
5
5
  SHA512:
6
- metadata.gz: d71c9a4d883426849d939b49505c84d336fdcf26a3bce7ef1cae364f903881d6de2654091da2061d0ebb50e806ab56fea71b090b591c0924388976944b0aff27
7
- data.tar.gz: a25df68ad93832c42767a88e6e14800160301e2ce36e4e938efb65954dbf2b8c5fdcd92a1cabab57b2ec06482cdf27a14936f101fa120914ae656fe828f0bd3c
6
+ metadata.gz: 3ab1b89f32f27604f5c04ed43b262f69860884be324077acc5802b5d0b03ebb544818ea53e36a8c04077a00a47b8e320aa7ae664653fd6eddeb3f6d53f0e30b9
7
+ data.tar.gz: 99bd97c5b61ea4853a3150047f8b88d6165e00a8e343b1a3ca746fd8648915555edcdc9bcbe0277568cf8d822e7622bdaa3a457604a7956bf9797d73a32eeef4
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.6
1
+ 3.0
data/README.md CHANGED
@@ -79,12 +79,22 @@ It's the same as #cmd but for multiple commands.
79
79
  ```
80
80
 
81
81
  ### #dialog
82
+
83
+ Use this method to specify a differnt 'prompt' for once. This is perfect for interactive commands.
84
+
82
85
  ```ruby
83
86
  cli.dialog "echo 'are you sure?' && read -p 'yes|no>'", /\nyes|no>/
84
87
  # => "echo 'are you sure?' && read -p 'yes|no>'\nyes|no>"
85
88
  cli.cmd "yes"
86
89
  ```
87
90
 
91
+ ```ruby
92
+ cli.dialog "passwd", /Current Password:/i
93
+ cli.dialog "Old Password", /New Password:/i
94
+ cli.dialog "New Password", /Repeat Password:/i
95
+ cli.cmd "New Password"
96
+ ```
97
+
88
98
  ### #impact
89
99
 
90
100
  The very same as `#cmd` but it respects a flag whether to run commands with 'impact'.
@@ -148,10 +158,12 @@ Nearly everything can be configured.
148
158
  ### Callbacks
149
159
 
150
160
  The following callbacks are available
151
- - #before_open_channel
152
- - #after_open_channel
153
- - #before_on_data
154
- - #before_on_data
161
+ - before_open_channel
162
+ - after_open_channel
163
+ - before_on_stdout
164
+ - after_on_stdout
165
+ - before_on_stdin
166
+ - after_on_stdin
155
167
 
156
168
  ```ruby
157
169
  cli.before_open_channel do
@@ -166,6 +178,41 @@ cli.after_open_channel do
166
178
  end
167
179
  ```
168
180
 
181
+ Using the callbacks you can define a debugger which shows the `stdout` buffer content each time new data is received.
182
+
183
+ ```ruby
184
+ cli.after_on_stdout do
185
+ warn stdout
186
+ end
187
+ ```
188
+
189
+ ```ruby
190
+ cli.after_on_stdout do
191
+ puts "the following new data arrived on stdout #{new_data.inspect} from #{hostname}"
192
+ end
193
+ ```
194
+
195
+ or convert new lines between different OS
196
+ ```ruby
197
+ cli.after_on_stdout do
198
+ stdout.gsub!("\r\n", "\n")
199
+ end
200
+ ```
201
+
202
+ or hide passwords
203
+ ```ruby
204
+ cli.after_on_stdout do
205
+ stdout.gsub!(/password:\S+/, "<HIDDEN>")
206
+ end
207
+ ```
208
+
209
+ or change the stdin before sending it
210
+ ```ruby
211
+ cli.before_on_stdin do
212
+ content.gsub("\n\n", "\n")
213
+ end
214
+ ```
215
+
169
216
  ### #hostname #host #to_s
170
217
 
171
218
  ```ruby
data/lib/net/ssh/cli.rb CHANGED
@@ -17,6 +17,7 @@ module Net
17
17
  class OpenChannelTimeout < Error; end
18
18
  class ReadTillTimeout < Error; end
19
19
  class PromptDetection < Error; end
20
+ class CMD < Error; end
20
21
  end
21
22
 
22
23
  # Example
@@ -42,17 +43,26 @@ module Net
42
43
  default_prompt: /\n?^(\S+@.*)\z/, # the default prompt to search for
43
44
  cmd_rm_prompt: false, # whether the prompt should be removed in the output of #cmd
44
45
  cmd_rm_command: false, # whether the given command should be removed in the output of #cmd
46
+ cmd_rm_command_tail: "\n", # which format does the end of line return after a command has been submitted. Could be something like "ls\n" "ls\r\n" or "ls \n" (extra spaces)
45
47
  run_impact: false, # whether to run #impact commands. This might align with testing|development|production. example #impact("reboot")
46
48
  read_till_timeout: nil, # timeout for #read_till to find the match
49
+ read_till_hard_timeout: nil, # hard timeout for #read_till to find the match using Timeout.timeout(hard_timeout) {}. Might creates unpredicted sideffects
50
+ read_till_hard_timeout_factor: 1.2, # hard timeout factor in case read_till_hard_timeout is true
47
51
  named_prompts: ActiveSupport::HashWithIndifferentAccess.new, # you can used named prompts for #with_prompt {}
52
+ before_cmd_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call before #cmd
53
+ after_cmd_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call after #cmd
48
54
  before_on_stdout_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call before data arrives from the underlying connection
49
55
  after_on_stdout_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call after data arrives from the underlying connection
56
+ before_on_stdin_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call before data is sent to the underlying channel
57
+ after_on_stdin_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call after data is sent to the underlying channel
50
58
  before_open_channel_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call before opening a channel
51
59
  after_open_channel_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call after opening a channel, for example you could call #detect_prompt or #read_till
52
60
  open_channel_timeout: nil, # timeout to open the channel
53
61
  net_ssh_options: ActiveSupport::HashWithIndifferentAccess.new, # a wrapper for options to pass to Net::SSH.start in case net_ssh is undefined
54
62
  process_time: 0.00001, # how long #process is processing net_ssh#process or sleeping (waiting for something)
55
63
  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
64
+ on_stdout_processing: 100, # whether to optimize the on_stdout performance by calling #process #optimize_on_stdout-times in case more data arrives
65
+ sleep_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call instead of Kernel.sleep(), perfect for async hooks
56
66
  )
57
67
 
58
68
  def options
@@ -107,19 +117,19 @@ module Net
107
117
  before_on_stdout_procs.each { |_name, a_proc| instance_eval(&a_proc) }
108
118
  stdout << new_data
109
119
  after_on_stdout_procs.each { |_name, a_proc| instance_eval(&a_proc) }
110
- self.process_count += 1
111
- 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
112
- self.process_count -= 1
120
+ optimise_stdout_processing
113
121
  stdout
114
122
  end
115
123
 
116
- def write(content = String.new)
124
+ def stdin(content = String.new)
117
125
  logger.debug { "#write #{content.inspect}" }
126
+ before_on_stdin_procs.each { |_name, a_proc| instance_eval(&a_proc) }
118
127
  channel.send_data content
119
128
  process
129
+ after_on_stdin_procs.each { |_name, a_proc| instance_eval(&a_proc) }
120
130
  content
121
131
  end
122
- alias stdin write
132
+ alias write stdin
123
133
 
124
134
  def write_n(content = String.new)
125
135
  write content + "\n"
@@ -128,7 +138,7 @@ module Net
128
138
  def read
129
139
  process
130
140
  var = stdout!
131
- logger.debug("#read: \n#{var}")
141
+ logger.debug { "#read: \n#{var}" }
132
142
  var
133
143
  end
134
144
 
@@ -139,6 +149,16 @@ module Net
139
149
  with_prompts[-1] || default_prompt
140
150
  end
141
151
 
152
+ # run something with a different named prompt
153
+ #
154
+ # named_prompts["root"] = /(?<prompt>\nroot)\z/
155
+ #
156
+ # with_named_prompt("root") do
157
+ # cmd("sudo -i")
158
+ # cmd("cat /etc/passwd")
159
+ # end
160
+ # cmd("exit")
161
+ #
142
162
  def with_named_prompt(name)
143
163
  raise Error::UndefinedMatch, "unknown named_prompt #{name}" unless named_prompts[name]
144
164
 
@@ -147,20 +167,25 @@ module Net
147
167
  end
148
168
  end
149
169
 
170
+ # tries to detect the prompt
171
+ # sends a "\n", waits for a X seconds, and uses the last line as prompt
172
+ # this won't work reliable if the prompt changes during the session
150
173
  def detect_prompt(seconds: 3)
151
174
  write_n
152
- future = Time.now + seconds
153
- while future > Time.now
154
- process
155
- sleep 0.1
156
- end
175
+ process(seconds)
157
176
  self.default_prompt = read[/\n?^.*\z/]
158
177
  raise Error::PromptDetection, "couldn't detect a prompt" unless default_prompt.present?
159
178
 
160
179
  default_prompt
161
180
  end
162
181
 
163
- # prove a block where the default prompt changes
182
+ # run something with a different prompt
183
+ #
184
+ # with_prompt(/(?<prompt>\nroot)\z/) do
185
+ # cmd("sudo -i")
186
+ # cmd("cat /etc/passwd")
187
+ # end
188
+ # cmd("exit")
164
189
  def with_prompt(prompt)
165
190
  logger.debug { "#with_prompt: #{current_prompt.inspect} => #{prompt.inspect}" }
166
191
  with_prompts << prompt
@@ -171,22 +196,48 @@ module Net
171
196
  logger.debug { "#with_prompt: => #{current_prompt.inspect}" }
172
197
  end
173
198
 
174
- def read_till(prompt: current_prompt, timeout: read_till_timeout, **_opts)
199
+ # continues to process the ssh connection till #stdout matches the given prompt.
200
+ # might raise a timeout error if a soft/hard timeout is given
201
+ # be carefull when using the hard_timeout, this is using the dangerous Timeout.timeout
202
+ # this gets really slow on large outputs, since the prompt will be searched in the whole output. Use \z in the regex if possible
203
+ #
204
+ # Optional named arguments:
205
+ # - prompt: expected to be a regex
206
+ # - timeout: nil or a number
207
+ # - hard_timeout: nil, true, or a number
208
+ # - hard_timeout_factor: nil, true, or a number
209
+ # - when hard_timeout == true, this will set the hard_timeout as (read_till_hard_timeout_factor * read_till_timeout), defaults to 1.2 = +20%
210
+ 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)
175
211
  raise Error::UndefinedMatch, 'no prompt given or default_prompt defined' unless prompt
176
-
177
- ::Timeout.timeout(timeout + 0.5, Error::ReadTillTimeout) do
178
- with_prompt(prompt) do
179
- soft_timeout = Time.now + timeout
180
- until stdout[current_prompt] do
181
- raise Error::ReadTillTimeout, "Timeout after #{timeout}s, #{current_prompt.inspect} never matched #{stdout.inspect}" if soft_timeout < Time.now
212
+ hard_timeout = (read_till_hard_timeout_factor * timeout) if timeout and hard_timeout == true
213
+ hard_timeout = nil if hard_timeout == true
214
+
215
+ with_prompt(prompt) do
216
+ ::Timeout.timeout(hard_timeout, Error::ReadTillTimeout, "#{current_prompt.inspect} didn't match on #{stdout.inspect} within #{hard_timeout}s") do
217
+ soft_timeout = Time.now + timeout if timeout
218
+ until prompt_in_stdout? do
219
+ if timeout and soft_timeout < Time.now
220
+ raise Error::ReadTillTimeout, "#{current_prompt.inspect} didn't match on #{stdout.inspect} within #{timeout}s"
221
+ end
182
222
  process
183
- sleep 0.1
223
+ sleep 0.01 # don't race for CPU
184
224
  end
185
225
  end
186
226
  end
187
227
  read
188
228
  end
189
229
 
230
+ def prompt_in_stdout?
231
+ case current_prompt
232
+ when Regexp
233
+ !!stdout[current_prompt]
234
+ when String
235
+ stdout.include?(current_prompt)
236
+ else
237
+ raise Net::SSH::CLI::Error, "prompt/current_prompt is not a String/Regex #{current_prompt.inspect}"
238
+ end
239
+ end
240
+
190
241
  def read_for(seconds:)
191
242
  process(seconds)
192
243
  read
@@ -194,40 +245,65 @@ module Net
194
245
 
195
246
  def dialog(command, prompt, **opts)
196
247
  opts = opts.clone.merge(prompt: prompt)
197
- cmd(command, opts)
248
+ cmd(command, **opts)
198
249
  end
199
250
 
200
- # 'read' first on purpuse as a feature. once you cmd you ignore what happend before. otherwise use read|write directly.
201
- # this should avoid many horrible state issues where the prompt is not the last prompt
251
+ # send a command and get the output as return value
252
+ # 1. sends the given command to the ssh connection channel
253
+ # 2. continues to process the ssh connection until the prompt is found in the stdout
254
+ # 3. prepares the output using your callbacks
255
+ # 4. returns the output of your command
256
+ # Hint: 'read' first on purpuse as a feature. once you cmd you ignore what happend before. otherwise use read|write directly.
257
+ # this should avoid many horrible state issues where the prompt is not the last prompt
202
258
  def cmd(command, pre_read: true, rm_prompt: cmd_rm_prompt, rm_command: cmd_rm_command, prompt: current_prompt, **opts)
203
259
  opts = opts.clone.merge(pre_read: pre_read, rm_prompt: rm_prompt, rm_command: rm_command, prompt: prompt)
204
260
  if pre_read
205
261
  pre_read_data = read
206
262
  logger.debug { "#cmd ignoring pre-command output: #{pre_read_data.inspect}" } if pre_read_data.present?
207
263
  end
264
+ before_cmd_procs.each { |_name, a_proc| instance_eval(&a_proc) }
208
265
  write_n command
209
- output = read_till(opts)
210
- rm_prompt!(output, opts)
211
- rm_command!(output, command, opts)
266
+ output = read_till(**opts)
267
+ rm_prompt!(output, **opts)
268
+ rm_command!(output, command, **opts)
269
+ after_cmd_procs.each { |_name, a_proc| instance_eval(&a_proc) }
212
270
  output
271
+ rescue Error::ReadTillTimeout => error
272
+ raise Error::CMD, "#{error.message} after cmd #{command.inspect} was sent"
213
273
  end
214
274
  alias command cmd
215
275
  alias exec cmd
216
276
 
277
+ # Execute multiple cmds, see #cmd
217
278
  def cmds(*commands, **opts)
218
279
  commands.flatten.map { |command| [command, cmd(command, **opts)] }
219
280
  end
220
281
  alias commands cmds
221
282
 
222
283
  def rm_command!(output, command, **opts)
223
- output[command + "\n"] = '' if rm_command?(opts) && output[command + "\n"]
284
+ output[command + cmd_rm_command_tail] = '' if rm_command?(**opts) && output[command + cmd_rm_command_tail]
224
285
  end
225
286
 
226
- def rm_prompt!(output, **opts)
227
- if rm_prompt?(opts)
228
- prompt = opts[:prompt] || current_prompt
287
+ # removes the prompt from the given output
288
+ # prompt should contain a named match 'prompt' /(?<prompt>.*something.*)\z/
289
+ # for backwards compatibility it also tries to replace the first match of the prompt /(something)\z/
290
+ # it removes the whole match if no matches are given /something\z/
291
+ def rm_prompt!(output, prompt: current_prompt, **opts)
292
+ if rm_prompt?(**opts)
229
293
  if output[prompt]
230
- prompt.is_a?(Regexp) ? output[prompt, 1] = '' : output[prompt] = ''
294
+ case prompt
295
+ when String then output[prompt] = ''
296
+ when Regexp
297
+ if prompt.names.include?("prompt")
298
+ output[prompt, "prompt"] = ''
299
+ else
300
+ begin
301
+ output[prompt, 1] = ''
302
+ rescue IndexError
303
+ output[prompt] = ''
304
+ end
305
+ end
306
+ end
231
307
  end
232
308
  end
233
309
  end
@@ -254,6 +330,19 @@ module Net
254
330
  alias hostname host
255
331
  alias to_s host
256
332
 
333
+ # if #sleep_procs are set, they will be called instead of Kernel.sleep
334
+ # great for async
335
+ # .sleep_procs["async"] = proc do |duration| async_reactor.sleep(duration) end
336
+ #
337
+ # cli.sleep(1)
338
+ def sleep(duration)
339
+ if sleep_procs.any?
340
+ sleep_procs.each { |_name, a_proc| instance_exec(duration, &a_proc) }
341
+ else
342
+ Kernel.sleep(duration)
343
+ end
344
+ end
345
+
257
346
  ## NET::SSH
258
347
  #
259
348
 
@@ -326,6 +415,17 @@ module Net
326
415
  def rm_command?(**opts)
327
416
  opts[:rm_cmd].nil? ? cmd_rm_command : opts[:rm_cmd]
328
417
  end
418
+
419
+ # when new data is beeing received, likely more data will arrive - this improves the performance by a large factor
420
+ # but on a lot of data, this leads to a stack level too deep
421
+ # therefore it is limited to max #on_stdout_processing
422
+ # the bigger on_stdout_processing, the closer we get to a stack level too deep
423
+ def optimise_stdout_processing
424
+ self.process_count += 1
425
+ process unless process_count > on_stdout_processing
426
+ ensure
427
+ self.process_count -= 1
428
+ end
329
429
  end
330
430
  end
331
431
  end
@@ -3,7 +3,7 @@
3
3
  module Net
4
4
  module SSH
5
5
  module CLI
6
- VERSION = '1.3.0'
6
+ VERSION = '1.7.0'
7
7
  end
8
8
  end
9
9
  end
data/net-ssh-cli.gemspec CHANGED
@@ -39,7 +39,7 @@ Gem::Specification.new do |spec|
39
39
 
40
40
  spec.add_development_dependency 'bundler'
41
41
  spec.add_development_dependency 'bump'
42
- spec.add_development_dependency 'rake', '~> 10.0'
42
+ spec.add_development_dependency 'rake', '~> 13.0'
43
43
  spec.add_development_dependency 'rspec', '~> 3.0'
44
44
  spec.add_dependency 'activesupport', '>= 4.0'
45
45
  spec.add_dependency 'net-ssh', '>= 4.0'
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.3.0
4
+ version: 1.7.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: 2019-07-15 00:00:00.000000000 Z
11
+ date: 2021-02-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '10.0'
47
+ version: '13.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '10.0'
54
+ version: '13.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rspec
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -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.4
143
+ rubygems_version: 3.2.3
144
144
  signing_key:
145
145
  specification_version: 4
146
146
  summary: 'Net::SSH::CLI: A library to handle CLI Sessions'