em-ssh 0.3.0.pre1 → 0.3.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.
data/README.md CHANGED
@@ -3,7 +3,7 @@ Em-ssh is a net-ssh adapter for EventMachine. For the most part you can take any
3
3
 
4
4
  Em-ssh is not associated with the Jamis Buck's [net-ssh](http://net-ssh.github.com/) library. Please report any bugs with em-ssh to [https://github.com/simulacre/em-ssh/issues](https://github.com/simulacre/em-ssh/issues)
5
5
  ##Installation
6
- gem install em-ssh
6
+ gem install em-ssh
7
7
 
8
8
  ##Synopsis
9
9
  EM.run do
@@ -63,33 +63,51 @@ Em-ssh provides an expect-like shell abstraction layer on top of net-ssh in EM::
63
63
  ### Example
64
64
  require 'em-ssh/shell'
65
65
  EM.run {
66
- EM::Ssh::Shell.new(host, 'caleb', "") do |shell|
67
- shell.should be_a(EventMachine::Ssh::Shell)
68
- shell.wait_for(']$')
69
- shell.send_and_wait('uname -a', ']$')
70
- shell.wait_for(']$')
71
- shell.send_and_wait('/sbin/ifconfig -a', ']$')
72
- timer.cancel
73
- EM.stop
66
+ EM::Ssh::Shell.new(host, ENV['USER'], "") do |shell|
67
+ shell.callback do
68
+ shell.expect('~]$ ')
69
+ shell.expect('~]$ ','uname -a')
70
+ shell.expect('~]$ ')
71
+ shell.expect('~]$ ', '/sbin/ifconfig -a')
72
+ EM.stop
73
+ end
74
+ shell.errback do
75
+ puts "error: #{err} (#{err.class})"
76
+ EM.stop
77
+ end
74
78
  end
75
79
  }
76
80
 
77
- #### Synchrony Example
78
- require 'em-ssh/shell'
79
- EM.run {
80
- Fiber.new {
81
- shell = EM::Ssh::Shell.new(host, 'caleb', '')
82
- shell.wait_for(']$')
83
- shell.send_and_wait('sudo su -', 'password for caleb: ')
84
- shell.send_and_wait('password', ']$')
85
- output = shell.send_and_wait('/etc/init.d/openvpn restart', ']$')
86
- # ...
87
- shell.send_and_wait('exit', ']$')
88
- shell.send_data('exit')
89
- }.resume
90
- }
91
-
81
+ ### Run Multiple Commands in Parallel
82
+ require 'em-ssh/shell'
83
+ EM.run do
84
+ EM::Ssh::Shell.new(host, ENV['USER'], '') do |shell|
85
+ shell.errback do |err|
86
+ puts "error: #{err} (#{err.class})"
87
+ EM.stop
88
+ end
89
+
90
+ shell.callback do
91
+ commands.clone.each do |command|
92
+ mys = shell.split # provides a second session over the same connection
93
+ mys.on(:closed) do
94
+ commands.delete(command)
95
+ EM.stop if commands.empty?
96
+ end
92
97
 
98
+ puts("waiting for: #{waitstr.inspect}")
99
+ # When given a block, Shell#expect does not 'block'
100
+ mys.expect(waitstr) do
101
+ puts "sending #{command.inspect} and waiting for #{waitstr.inspect}"
102
+ mys.expect(waitstr, command) do |result|
103
+ puts "#{mys} result: '#{result}'"
104
+ mys.close
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
93
111
 
94
112
  ## Other Examples
95
113
  See bin/em-ssh for an example of a basic replacement for system ssh.
data/bin/em-ssh CHANGED
@@ -43,8 +43,10 @@ end # host.nil?
43
43
  abort("a host is required") if host.nil?
44
44
 
45
45
  options[:user], host = *host.split('@') if host.include?('@')
46
- options[:user], options[:password] = *options[:user].split(':') if options[:user].include?(':')
46
+ options[:user], options[:password] = *options[:user].split(':') if options[:user] && options[:user].include?(':')
47
47
  host, options[:port] = *host.split(':') if host.include?(':')
48
+ options[:user] = ENV['USER'] unless options[:user]
49
+ options[:password] = HighLine.new.ask("#{options[:user]}'s password: "){|q| q.echo = "*" } unless options[:password]
48
50
  connected = false
49
51
 
50
52
 
@@ -68,7 +70,7 @@ end
68
70
  EM.run do
69
71
  EM::Ssh.start(host, options[:user], options) do |ssh|
70
72
  ssh.errback do |err|
71
- puts "#{err} (#{err.class})"
73
+ puts "error: #{err} (#{err.class})"
72
74
  EM.stop
73
75
  end
74
76
 
data/bin/em-ssh-shell CHANGED
@@ -15,14 +15,18 @@ def abort(msg)
15
15
  Process.exit
16
16
  end # abort(msg)
17
17
 
18
- options = {}
19
- opts = OptionParser.new
18
+ options = {:auth_methods => ['publickey', 'password'], :port => 22}
19
+ opts = OptionParser.new
20
20
  opts.banner += " [user:[password]@]host[:port] wait_string command [command command ...]"
21
- options[:port] = 22
21
+
22
22
  opts.on('-u', '--user String', String) { |u| options[:user] = u }
23
23
  opts.on('-p', '--password [String]', String) do |p|
24
24
  options[:password] = p.nil? ? HighLine.new.ask("password: "){|q| q.echo = "*" } : p
25
25
  end
26
+ opts.on('-t', '--timeout Integer', Integer) { |t| options[:timeout] = t }
27
+ opts.on('--[no-]publickey', "don't attempt public key auth") do |pk|
28
+ options[:auth_methods] = ['password'] unless pk
29
+ end
26
30
  opts.on('-v', '--verbose') do
27
31
  EM::Ssh.logger.level = EM::Ssh.logger.level - 1 unless EM::Ssh.logger.level == 0
28
32
  options[:verbose] = EM::Ssh.logger.level
@@ -36,36 +40,43 @@ end # host.nil?
36
40
  abort("a host is required") if host.nil?
37
41
 
38
42
  options[:user], host = *host.split('@') if host.include?('@')
39
- options[:user], options[:password] = *options[:user].split(':') if options[:user].include?(':')
43
+ options[:user], options[:password] = *options[:user].split(':') if options[:user] && options[:user].include?(':')
40
44
  host, options[:port] = *host.split(':') if host.include?(':')
45
+ options[:user] = ENV['USER'] unless options[:user]
46
+ options[:password] = HighLine.new.ask("#{options[:user]}'s password: "){|q| q.echo = "*" } unless options[:password]
41
47
 
42
48
 
43
- waitstr = ARGV.shift
49
+ waitstr = ARGV.shift
44
50
  commands = ARGV
45
51
  abort("wait_string is required") if waitstr.nil?
46
52
  abort("command is required") if commands.empty?
53
+ waitstr = Regexp.escape(waitstr)
47
54
 
48
55
 
49
56
  EM.run do
50
- EM::Ssh::Shell.new(host, options[:user], options[:password], :net_ssh => options) do |shell|
51
- commands.each do |command|
52
- mys = shell.split
53
- mys.on(:closed) { info("#{mys} has closed") }
54
-
55
- EM.next_tick do
56
- Fiber.new {
57
- debug("#{mys} waited for: '#{mys.wait_for(waitstr)}'")
58
- debug("#{mys} send: #{command.inspect}")
59
- puts "#{mys} result: '#{mys.send_and_wait(command, waitstr)}'"
60
- mys.close
61
- }.resume
62
- end
63
- end # |command|
64
-
65
- shell.on(:childless) do
66
- info("#{shell}'s children all closed")
67
- shell.close
57
+ EM::Ssh::Shell.new(host, options[:user], options[:password], :timeout => options[:timeout], :net_ssh => options) do |shell|
58
+ shell.errback do |err|
59
+ puts "error: #{err} (#{err.class})"
68
60
  EM.stop
69
- end # :childless
70
- end # |shell|
71
- end # EM.run
61
+ end
62
+
63
+ shell.callback do
64
+ commands.clone.each do |command|
65
+ mys = shell.split
66
+ mys.on(:closed) do
67
+ commands.delete(command)
68
+ EM.stop if commands.empty?
69
+ end
70
+
71
+ puts("waiting for: #{waitstr.inspect}")
72
+ mys.expect(waitstr) do
73
+ puts "sending #{command.inspect} and waiting for #{waitstr.inspect}"
74
+ mys.expect(waitstr, command) do |result|
75
+ puts "#{mys} result: '#{result}'"
76
+ mys.close
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -261,7 +261,7 @@ module EventMachine
261
261
  end # begin
262
262
  end.resume
263
263
  else
264
- @queue.push(packet)
264
+ @queue.push(packet) unless packet.type >= CHANNEL_OPEN
265
265
  if algorithms.allow?(packet)
266
266
  fire(:packet, packet)
267
267
  fire(:session_packet, packet) if packet.type >= GLOBAL_REQUEST
data/lib/em-ssh/shell.rb CHANGED
@@ -6,8 +6,8 @@ module EventMachine
6
6
  # @example Retrieve the output of ifconfig -a on a server
7
7
  # EM.run{
8
8
  # shell = EM::Ssh::Shell.new(host, user, password)
9
- # shell.wait_for('@icaleb ~]$')
10
- # interfaces = send_and_wait('/sbin/ifconfig -a', '@icaleb ~]$')
9
+ # shell.expect('~]$ ')
10
+ # interfaces = expect('~]$ ', '/sbin/ifconfig -a')
11
11
  #
12
12
  # Shells can be easily and quickly duplicated (#split) without the need to establish another connection.
13
13
  # Shells provide :closed, :childless, and :split callbacks.
@@ -21,10 +21,11 @@ module EventMachine
21
21
  #
22
22
  # admin_shell = shell.split
23
23
  # admin_shell.on(:closed) { warn("admin shell has closed") }
24
- # admin_shell.send_and_wait('sudo su -', ']$')
24
+ # admin_shell.expect(']$', 'sudo su -')
25
25
  class Shell
26
26
  include Log
27
27
  include Callbacks
28
+ include EM::Deferrable
28
29
 
29
30
  # Global timeout for wait operations; can be overriden by :timeout option to new
30
31
  TIMEOUT = 15
@@ -75,11 +76,12 @@ module EventMachine
75
76
  @reconnect = opts[:reconnect]
76
77
  @buffer = ''
77
78
 
78
- if block_given?
79
- Fiber.new { open(&blk).tap{|r| raise r if r.is_a?(Exception) } }.resume
80
- else
81
- open.tap{|r| raise r if r.is_a?(Exception) }
82
- end # block_given?
79
+ # TODO make all methods other than #callback and #errback inaccessible until connected? == true
80
+ yield self if block_given?
81
+ Fiber.new {
82
+ open rescue fail($!)
83
+ succeed(self) if connected? && !closed?
84
+ }.resume
83
85
  end
84
86
 
85
87
  # @return [Boolean] true if the connection should be automatically re-established; default: false
@@ -91,7 +93,7 @@ module EventMachine
91
93
  # Disconnected shells cannot be split.
92
94
  def disconnect
93
95
  close
94
- connection.close
96
+ connection && connection.close
95
97
  end
96
98
 
97
99
  # @return [Boolean] true if the connection is still alive
@@ -115,6 +117,40 @@ module EventMachine
115
117
  @closed == true
116
118
  end
117
119
 
120
+ # Wait for a number of seconds until a specified string or regexp is matched by the
121
+ # data returned from the ssh connection. Optionally send a given string first.
122
+ #
123
+ # If a block is not provided the current Fiber will yield until strregex matches or
124
+ # :timeout # is reached.
125
+ #
126
+ # If a block is provided expect will return.
127
+ #
128
+ # @param [String, Regexp] strregex to match against
129
+ # @param [String] send_str the data to send before waiting
130
+ # @param [Hash] opts
131
+ # @option opts [Fixnum] :timeout (@timeout) number of seconds to wait when there is no activity
132
+ # @return [Shell, String] all data received up to an including strregex if a block is not provided.
133
+ # the Shell if a block is provided
134
+ # @example expect a prompt
135
+ # expect(' ~]$ ')
136
+ # @example send a command and wait for a prompt
137
+ # expect(' ~]$ ', '/sbin/ifconfig')
138
+ # @example expect a prompt and within 5 seconds
139
+ # expect(' ~]$ ', :timeout => 5)
140
+ # @example send a command and wait up to 10 seconds for a prompt
141
+ # expect(' ~]$ ', '/etc/sysconfig/openvpn restart', :timeout => 10)
142
+ def expect(strregex, send_str = nil, opts = {})
143
+ send_str, opts = nil, send_str if send_str.is_a?(Hash)
144
+ if block_given?
145
+ Fiber.new {
146
+ yield send_str ? send_and_wait(send_str, strregex, opts) : wait_for(strregex, opts)
147
+ }.resume
148
+ self
149
+ else
150
+ send_str ? send_and_wait(send_str, strregex, opts) : wait_for(strregex, opts)
151
+ end
152
+ end
153
+
118
154
  # Send a string to the server and wait for a response containing a specified String or Regex.
119
155
  # @param [String] send_str
120
156
  # @return [String] all data in the buffer including the wait_str if it was found
@@ -155,12 +191,10 @@ module EventMachine
155
191
  data_callback = on(:data) do
156
192
  timer && timer.cancel
157
193
  if matched
158
- debug("data_callback invoked when already matched")
194
+ warn("data_callback invoked when already matched")
159
195
  next
160
196
  end
161
- matched = @buffer.match(strregex)
162
- if matched
163
- debug("data matched")
197
+ if (matched = @buffer.match(strregex))
164
198
  data_callback.cancel
165
199
  @buffer=matched.post_match
166
200
  f.resume(matched.pre_match + matched.to_s)
@@ -176,10 +210,13 @@ module EventMachine
176
210
 
177
211
  timer = EM::Timer.new(opts[:timeout], &timeout)
178
212
  debug("set timer: #{timer} for #{opts[:timeout]}")
179
- res = Fiber.yield
180
- raise(res, res.message, Array(res.backtrace) + trace) if res.is_a?(Exception)
181
- yield(res) if block_given?
182
- res
213
+ Fiber.yield.tap do |res|
214
+ if res.is_a?(Exception)
215
+ res.set_backtrace(Array(res.backtrace) + trace)
216
+ raise res
217
+ end
218
+ yield(res) if block_given?
219
+ end
183
220
  end
184
221
 
185
222
 
@@ -191,45 +228,35 @@ module EventMachine
191
228
  f = Fiber.current
192
229
  trace = caller
193
230
 
194
- conerr = nil
195
- unless connected?
196
- conerr = on(:error) do |e|
197
- error("#{e} (#{e.class})")
198
- e.set_backtrace(trace + Array(e.backtrace))
199
- debug(e.backtrace)
200
- conerr = e
201
- f.resume(e)
202
- end # |e|
231
+ begin
203
232
  connect
204
- end # connected?
205
-
206
- connection || raise(ConnectionError, "failed to create shell for #{host}: #{conerr} (#{conerr.class})")
207
-
208
- connection.open_channel do |channel|
209
- debug "**** channel open: #{channel}"
210
- channel.request_pty(options[:pty] || {}) do |pty,suc|
211
- debug "***** pty open: #{pty}; suc: #{suc}"
212
- pty.send_channel_request("shell") do |shell,success|
213
- unless success
214
- f.resume(ConnectionError.new("Failed to create shell").tap{|e| e.set_backtrace(caller) })
215
- end
216
- conerr && conerr.cancel
217
- debug "***** shell open: #{shell}"
218
- @closed = false
219
- @shell = shell
220
- @shell.on_data do |ch,data|
221
- debug("data: #{@buffer.dump}")
222
- @buffer += data
223
- fire(:data)
224
- end
225
-
226
- Fiber.new { yield(self) if block_given? }.resume
227
- f.resume(self)
228
- end # |shell,success|
229
- end # |pty,suc|
230
- end # |channel|
231
-
232
- return Fiber.yield
233
+ connection.open_channel do |channel|
234
+ debug "**** channel open: #{channel}"
235
+ channel.request_pty(options[:pty] || {}) do |pty,suc|
236
+ debug "***** pty open: #{pty}; suc: #{suc}"
237
+ pty.send_channel_request("shell") do |shell,success|
238
+ if !success
239
+ f.resume(ConnectionError.new("Failed to create shell").tap{|e| e.set_backtrace(caller) })
240
+ else
241
+ debug "***** shell open: #{shell}"
242
+ @closed = false
243
+ @shell = shell
244
+ @shell.on_data do |ch,data|
245
+ @buffer += data
246
+ debug("data: #{@buffer.dump}")
247
+ fire(:data)
248
+ end
249
+ Fiber.new { yield(self) if block_given? }.resume
250
+ f.resume(self)
251
+ end
252
+ end # |shell,success|
253
+ end # |pty,suc|
254
+ end # |channel|
255
+ rescue => e
256
+ raise ConnectionError.new("failed to create shell for #{host}: #{e} (#{e.class})")
257
+ end
258
+
259
+ return Fiber.yield.tap { |r| raise r if r.is_a?(Exception) }
233
260
  end
234
261
 
235
262
  # Create a new shell using the same ssh connection.
@@ -254,20 +281,19 @@ module EventMachine
254
281
  # Does not open the shell; use #open or #split
255
282
  # You generally won't need to call this on your own.
256
283
  def connect
257
- return if connected?
284
+ return @connection if connected?
258
285
  trace = caller
259
286
  f = Fiber.current
260
287
  ::EM::Ssh.start(host, user, connect_opts) do |connection|
261
288
  connection.callback do |ssh|
262
289
  f.resume(@connection = ssh)
263
- end # ssh
290
+ end
264
291
  connection.errback do |e|
265
292
  e.set_backtrace(trace + Array(e.backtrace))
266
- fire(:error, e)
267
293
  f.resume(e)
268
- end # err
294
+ end
269
295
  end
270
- return Fiber.yield
296
+ return Fiber.yield.tap { |r| raise r if r.is_a?(Exception) }
271
297
  end
272
298
 
273
299
 
@@ -1,5 +1,5 @@
1
1
  module EventMachine
2
2
  class Ssh
3
- VERSION='0.3.0.pre1'
3
+ VERSION='0.3.0'
4
4
  end # class::Ssh
5
5
  end # module::EventMachine
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: em-ssh
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0.pre1
5
- prerelease: 6
4
+ version: 0.3.0
5
+ prerelease:
6
6
  platform: ruby
7
7
  authors:
8
8
  - Caleb Crane
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-06-04 00:00:00.000000000 Z
12
+ date: 2012-07-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: eventmachine
@@ -123,3 +123,4 @@ signing_key:
123
123
  specification_version: 3
124
124
  summary: An EventMachine compatible net-ssh
125
125
  test_files: []
126
+ has_rdoc: