em-ssh 0.4.3 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ 0.5.0
2
+ - Shell an Connection instances can have their own Loggers
3
+ - [#18](https://github.com/simulacre/em-ssh/pull/18) - Target devices and options for specs can be configured through environment variables [@freakhill](https://github.com/freakhill)
4
+ - [#19](https://github.com/simulacre/em-ssh/pull/19) - Decouple interactive behavior from Shell allowing for other channels to be extended with #expect, etc., [@freakhill](https://github.com/freakhill)
5
+
6
+ 0.4.2
7
+ - Connection accepts :nego_timeout (seconds to wait for protocol and algorithm negotiation to finish)
8
+ - If protocol, or algorithm negotiation fail #errback will be called
9
+ - Shell#disconnect! forcefully terminates the connection
10
+ - Shell#disconnect takes a timeout
11
+ - EM::Ssh::Session#close overrides Net::SSH::Connection::Session to check for
12
+ transport before trying to close channels
13
+ - Fixes: dangling references to EM::Timers are removed from EM::Ssh::Connection
14
+ - Fixes: various dangling references to EM::Ssh::Connection and EM::Ssh::Session are properly removed
15
+
16
+ 0.4.1
17
+ - Connections terminated before version negotiation wil fail with EM::Ssh::ConnectionTermianted
18
+
19
+ 0.3.0
20
+ - Provides Shell#expect which can be used to wait for a string, or send a command and then wait
21
+ - Connection errors are provided as Deferreds and don't halt the entire reactor
22
+ - Shell timeouts are for inactivity and not total duration
23
+ - Shell buffer is maintained until wait_for matches; results from send_cmd without a corresponding wait_for will be retained in the buffer.
24
+
25
+ 0.1.0
26
+ - Connection#initialize will fire :error Net::SSH::AuthenticationError when authentication fails rather than raising an error
27
+ - Shell#open will catch :error fired by Connection#initialize and raise it as a ConnectionError
28
+ - Shell no longer accepts :halt_on_timeout
29
+ - Shell#wait_for will fire :error if timeout is reached
30
+ - Key exchange exceptions will be caught and propagated through fire :error
31
+
32
+ 0.0.3
33
+ - Removes CPU pegging via recursive EM.next_tick
34
+ - Adds an auto reconnect option to shell
data/README.md CHANGED
@@ -8,10 +8,12 @@ Em-ssh is not associated with the Jamis Buck's [net-ssh](http://net-ssh.github.c
8
8
  ##Synopsis
9
9
 
10
10
  ```ruby
11
+ require "em-ssh"
11
12
  EM.run do
12
13
  EM::Ssh.start(host, user, :password => password) do |connection|
13
14
  connection.errback do |err|
14
15
  $stderr.puts "#{err} (#{err.class})"
16
+ EM.stop
15
17
  end
16
18
  connection.callback do |ssh|
17
19
  # capture all stderr and stdout output from a remote process
@@ -70,14 +72,13 @@ require 'em-ssh/shell'
70
72
  EM.run do
71
73
  EM::Ssh::Shell.new(host, ENV['USER'], "") do |shell|
72
74
  shell.callback do
73
- shell.expect('~]$ ')
74
- shell.expect('~]$ ','uname -a')
75
- shell.expect('~]$ ')
76
- shell.expect('~]$ ', '/sbin/ifconfig -a')
75
+ shell.expect(Regexp.escape('~]$ '))
76
+ $stderr.puts shell.expect(Regexp.escape('~]$ '),'uname -a')
77
+ $stderr.puts shell.expect(Regexp.escape('~]$ '), '/sbin/ifconfig -a')
77
78
  EM.stop
78
79
  end
79
- shell.errback do
80
- puts "error: #{err} (#{err.class})"
80
+ shell.errback do |err|
81
+ $stderr.puts "error: #{err} (#{err.class})"
81
82
  EM.stop
82
83
  end
83
84
  end
@@ -88,34 +89,90 @@ end
88
89
 
89
90
  ```ruby
90
91
  require 'em-ssh/shell'
92
+
93
+ waitstr = Regexp.escape('~]$ ')
94
+ commands = ["uname -a", "uptime", "ifconfig"]
91
95
  EM.run do
92
- EM::Ssh::Shell.new(host, ENV['USER'], '') do |shell|
96
+ EM::Ssh::Shell.new(host, user, "") do |shell|
93
97
  shell.errback do |err|
94
- puts "error: #{err} (#{err.class})"
98
+ $stderr.puts "error: #{err} (#{err.class})"
95
99
  EM.stop
96
- end
100
+ end
97
101
 
98
- shell.callback do
102
+ shell.callback do
99
103
  commands.clone.each do |command|
100
104
  mys = shell.split # provides a second session over the same connection
105
+
101
106
  mys.on(:closed) do
102
107
  commands.delete(command)
103
108
  EM.stop if commands.empty?
104
109
  end
105
110
 
106
- puts("waiting for: #{waitstr.inspect}")
107
- # When given a block, Shell#expect does not 'block'
108
- mys.expect(waitstr) do
109
- puts "sending #{command.inspect} and waiting for #{waitstr.inspect}"
110
- mys.expect(waitstr, command) do |result|
111
- puts "#{mys} result: '#{result}'"
112
- mys.close
113
- end
114
- end
115
- end
116
- end
117
- end
118
- end
111
+ mys.callback do
112
+ $stderr.puts("waiting for: #{waitstr.inspect}")
113
+ # When given a block, Shell#expect does not 'block'
114
+ mys.expect(waitstr) do
115
+ $stderr.puts "sending #{command.inspect} and waiting for #{waitstr.inspect}"
116
+ mys.expect(waitstr, command) do |result|
117
+ $stderr.puts "#{mys} result: '#{result}'"
118
+ mys.close
119
+ end
120
+ end
121
+ end
122
+
123
+ mys.errback do |err|
124
+ $stderr.puts "subshell error: #{err} (#{err.class})"
125
+ mys.close
126
+ end
127
+
128
+ end
129
+ end
130
+ end
131
+ end
132
+ ```
133
+
134
+
135
+ ```ruby
136
+ waitstr = Regexp.escape('~]$ ')
137
+ commands = ["uname -a", "uptime", "ifconfig"]
138
+
139
+ require 'em-ssh/shell'
140
+ EM.run do
141
+ EM::Ssh::Shell.new(host, user, "") do |shell|
142
+ shell.errback do |err|
143
+ $stderr.puts "error: #{err} (#{err.class})"
144
+ $stderr.puts err.backtrace
145
+ EM.stop
146
+ end
147
+
148
+ shell.callback do
149
+ commands.clone.each do |command|
150
+ Fiber.new {
151
+ # When given a block Shell#split will close the Shell after
152
+ # the block returns. If a block is given it must be called
153
+ # within a Fiber.
154
+ sresult = shell.split do |mys|
155
+ mys.on(:closed) do
156
+ commands.delete(command)
157
+ EM.stop if commands.empty?
158
+ end
159
+ mys.errback do |err|
160
+ $stderr.puts "subshell error: #{err} (#{err.class})"
161
+ mys.close
162
+ end
163
+
164
+ mys.expect(waitstr)
165
+ result = mys.expect(waitstr, command)
166
+ $stderr.puts "#{mys} result: '#{result.inspect}'"
167
+ result
168
+ end
169
+ $stderr.puts "split result: #{sresult.inspect} +++"
170
+ }.resume
171
+
172
+ end
173
+ end
174
+ end
175
+ end
119
176
  ```
120
177
 
121
178
  ## Other Examples
@@ -0,0 +1,168 @@
1
+ require 'em-ssh/connection/channel/null-logger'
2
+
3
+ module EventMachine
4
+ class Ssh
5
+ class Connection
6
+ class Channel
7
+
8
+ # This module adds functionality to any channel it extends.
9
+ # It mainly provides functionality to help interactive behaviour on said channel.
10
+ # * #send_data (improved) that can append 'line terminators'.
11
+ # * #wait_for - waits for the shell to send data containing the given string.
12
+ # * #send_and_wait - sends a string and waits for a response containing a specified pattern.
13
+ # * #expect - waits for a number of seconds until a pattern is matched by the channel output.
14
+ # @example
15
+ # ch = get_some_ssh_channel_from_somewhere
16
+ # ch.extend(Interactive)
17
+ # ch.send_and_wait("ls -a", PROMPT)
18
+ # # > we get the result of the `ls -a` command through the channel (hopefully)
19
+ module Interactive
20
+
21
+ include Callbacks
22
+
23
+ DEFAULT_TIMEOUT = 15
24
+
25
+ attr_accessor :buffer
26
+ private :buffer, :buffer=
27
+
28
+ # @return[String] a string (\r\n) to append to every command
29
+ attr_accessor :line_terminator
30
+
31
+ def self.extended(channel)
32
+ channel.init_interactive_module
33
+ end
34
+
35
+ # When this module extends an object this method is automatically called (via self#extended).
36
+ # In other cases (include, prepend?), you need to call this method manually before use of the channel.
37
+ def init_interactive_module
38
+ @buffer = ''
39
+ @line_terminator = "\n"
40
+ on_data do |ch, data|
41
+ @buffer += data
42
+ fire(:data, data)
43
+ end
44
+ end
45
+
46
+ # @returns[#to_s] Returns a #to_s object describing the dump of the content of the buffers used by
47
+ # the methods of the interactive module mixed in the host object.
48
+ def dump_buffers
49
+ @buffer.dump
50
+ end
51
+
52
+ # Wait for a number of seconds until a specified string or regexp is matched by the
53
+ # data returned from the ssh channel. Optionally send a given string first.
54
+ #
55
+ # If a block is not provided the current Fiber will yield until strregex matches or
56
+ # :timeout is reached.
57
+ #
58
+ # If a block is provided expect will return.
59
+ #
60
+ # @param [String, Regexp] strregex to match against
61
+ # @param [String] send_str the data to send before waiting
62
+ # @param [Hash] opts
63
+ # @option opts [Fixnum] :timeout (@timeout) number of seconds to wait when there is no activity
64
+ # @return [Shell, String] all data received up to an including strregex if a block is not provided.
65
+ # the Shell if a block is provided
66
+ # @example expect a prompt
67
+ # expect(' ~]$ ')
68
+ # @example send a command and wait for a prompt
69
+ # expect(' ~]$ ', '/sbin/ifconfig')
70
+ # @example expect a prompt and within 5 seconds
71
+ # expect(' ~]$ ', :timeout => 5)
72
+ # @example send a command and wait up to 10 seconds for a prompt
73
+ # expect(' ~]$ ', '/etc/sysconfig/openvpn restart', :timeout => 10)
74
+ def expect(strregex, send_str = nil, opts = {})
75
+ send_str, opts = nil, send_str if send_str.is_a?(Hash)
76
+ if block_given?
77
+ Fiber.new {
78
+ yield send_str ? send_and_wait(send_str, strregex, opts) : wait_for(strregex, opts)
79
+ }.resume
80
+ self
81
+ else
82
+ send_str ? send_and_wait(send_str, strregex, opts) : wait_for(strregex, opts)
83
+ end
84
+ end
85
+
86
+ # Send a string to the server and wait for a response containing a specified String or Regex.
87
+ # @param [String] send_str
88
+ # @return [String] all data in the buffer including the wait_str if it was found
89
+ def send_and_wait(send_str, wait_str = nil, opts = {})
90
+ send_data(send_str, true)
91
+ return wait_for(wait_str, opts)
92
+ end
93
+
94
+ # Send data to the ssh server shell.
95
+ # You generally don't need to call this.
96
+ # @see #send_and_wait
97
+ # @param [String] d the data to send encoded as a string
98
+ # @param [Boolean] send_newline appends a newline terminator to the data (defaults: false).
99
+ def send_data(d, send_newline=false)
100
+ if send_newline
101
+ super("#{d}#{@line_terminator}")
102
+ else
103
+ super("#{d}")
104
+ end
105
+ end
106
+
107
+ # Wait for the shell to send data containing the given string.
108
+ # @param [String, Regexp] strregex a string or regex to match the console output against.
109
+ # @param [Hash] opts
110
+ # @option opts [Fixnum] :timeout (Session::TIMEOUT) the maximum number of seconds to wait
111
+ # @return [String] the contents of the buffer or a TimeoutError
112
+ # @raise Disconnected
113
+ # @raise ClosedChannel
114
+ # @raise TimeoutError
115
+ def wait_for(strregex, opts = { })
116
+ ###
117
+ log = opts[:log] || NullLogger.new
118
+ timeout = opts[:timeout].is_a?(Fixnum) ? opts[:timeout] : DEFAULT_TIMEOUT
119
+ ###
120
+ log.debug("wait_for(#{strregex.inspect}, :timeout => #{opts[:timeout] || @timeout})")
121
+ opts = { :timeout => @timeout }.merge(opts)
122
+ found = nil
123
+ f = Fiber.current
124
+ trace = caller
125
+ timer = nil
126
+ data_callback = nil
127
+ matched = false
128
+ started = Time.new
129
+
130
+ timeout = proc do
131
+ data_callback && data_callback.cancel
132
+ f.resume(TimeoutError.new("#{connection.host}: inactivity timeout (#{opts[:timeout]}) while waiting for #{strregex.inspect}; received: #{buffer.inspect}; waited total: #{Time.new - started}"))
133
+ end
134
+
135
+ data_callback = on(:data) do
136
+ timer && timer.cancel
137
+ if matched
138
+ log.warn("data_callback invoked when already matched")
139
+ next
140
+ end
141
+ if (matched = buffer.match(strregex))
142
+ log.debug("matched #{strregex.inspect} on #{buffer.inspect}")
143
+ data_callback.cancel
144
+ @buffer = matched.post_match
145
+ f.resume(matched.pre_match + matched.to_s)
146
+ else
147
+ timer = EM::Timer.new(opts[:timeout], &timeout)
148
+ end
149
+ end
150
+
151
+ # Check against current buffer
152
+ EM::next_tick { data_callback.call() if buffer.length > 0 }
153
+
154
+ timer = EM::Timer.new(opts[:timeout], &timeout)
155
+ Fiber.yield.tap do |res|
156
+ if res.is_a?(Exception)
157
+ res.set_backtrace(Array(res.backtrace) + trace)
158
+ raise res
159
+ end
160
+ yield(res) if block_given?
161
+ end
162
+ end
163
+
164
+ end # module Interactive
165
+ end # class Channel
166
+ end # class Connection
167
+ end # class Ssh
168
+ end # module EventChannel
@@ -0,0 +1,17 @@
1
+ require 'logger'
2
+
3
+ module EventMachine
4
+ class Ssh
5
+ class Connection
6
+ class Channel
7
+ class NullLogger < ::Logger
8
+
9
+ def add(*params, &block)
10
+ nil
11
+ end
12
+
13
+ end # class NullLogger
14
+ end # class Channel
15
+ end # class Connection
16
+ end # class Ssh
17
+ end # module EventMachine
data/lib/em-ssh/log.rb CHANGED
@@ -3,7 +3,7 @@ module EventMachine
3
3
  module Log
4
4
  # @return [Logger] the default logger
5
5
  def log
6
- EventMachine::Ssh.logger
6
+ @logger || EventMachine::Ssh.logger
7
7
  end
8
8
 
9
9
  def debug(msg = nil, &blk)
data/lib/em-ssh/shell.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'em-ssh'
2
+ require 'em-ssh/connection/channel/interactive'
2
3
 
3
4
  module EventMachine
4
5
  class Ssh
@@ -50,12 +51,17 @@ module EventMachine
50
51
  attr_reader :children
51
52
  # @return [Shell] the parent of this shell
52
53
  attr_reader :parent
54
+
53
55
  # @return [String] a string (\r\n) to append to every command
54
56
  def line_terminator
55
- @line_terminator ||= "\n"
57
+ shell ? shell.line_terminator : "\n"
58
+ end
59
+
60
+ # @param[String] lt a string (\r\n) to append to every command
61
+ def line_terminator=(lt)
62
+ @line_terminator = lt
63
+ shell.line_terminator = lt if shell
56
64
  end
57
- # [String]
58
- attr_writer :line_terminator
59
65
 
60
66
  # Connect to an ssh server then start a user shell.
61
67
  # @param [String] address
@@ -71,12 +77,12 @@ module EventMachine
71
77
  @user = user
72
78
  @pass = pass
73
79
  @options = opts
74
- @connect_opts = {:password => pass, :port => 22, :auth_methods => ['publickey', 'password']}.merge(opts[:net_ssh] || {})
80
+ @logger = opts[:logger] if opts[:logger]
81
+ @connect_opts = {:password => pass, :port => 22, :auth_methods => ['publickey', 'password'], :logger => log}.merge(opts[:net_ssh] || {})
75
82
  @session = opts[:session]
76
83
  @parent = opts[:parent]
77
84
  @children = []
78
85
  @reconnect = opts[:reconnect]
79
- @buffer = ''
80
86
 
81
87
  # TODO make all methods other than #callback and #errback inaccessible until connected? == true
82
88
  yield self if block_given?
@@ -99,6 +105,7 @@ module EventMachine
99
105
  end
100
106
  close
101
107
  @session && @session.close
108
+ @shell = nil
102
109
  disconnect!
103
110
  end
104
111
 
@@ -115,6 +122,7 @@ module EventMachine
115
122
  session && !session.closed?
116
123
  end
117
124
 
125
+
118
126
  # Close this shell and all children.
119
127
  # Even when a shell is closed it is still connected to the server.
120
128
  # Fires :closed event.
@@ -131,117 +139,31 @@ module EventMachine
131
139
  @closed == true
132
140
  end
133
141
 
134
- # Wait for a number of seconds until a specified string or regexp is matched by the
135
- # data returned from the ssh connection. Optionally send a given string first.
136
- #
137
- # If a block is not provided the current Fiber will yield until strregex matches or
138
- # :timeout # is reached.
139
- #
140
- # If a block is provided expect will return.
141
- #
142
- # @param [String, Regexp] strregex to match against
143
- # @param [String] send_str the data to send before waiting
144
- # @param [Hash] opts
145
- # @option opts [Fixnum] :timeout (@timeout) number of seconds to wait when there is no activity
146
- # @return [Shell, String] all data received up to an including strregex if a block is not provided.
147
- # the Shell if a block is provided
148
- # @example expect a prompt
149
- # expect(' ~]$ ')
150
- # @example send a command and wait for a prompt
151
- # expect(' ~]$ ', '/sbin/ifconfig')
152
- # @example expect a prompt and within 5 seconds
153
- # expect(' ~]$ ', :timeout => 5)
154
- # @example send a command and wait up to 10 seconds for a prompt
155
- # expect(' ~]$ ', '/etc/sysconfig/openvpn restart', :timeout => 10)
156
- def expect(strregex, send_str = nil, opts = {})
157
- send_str, opts = nil, send_str if send_str.is_a?(Hash)
158
- if block_given?
159
- Fiber.new {
160
- yield send_str ? send_and_wait(send_str, strregex, opts) : wait_for(strregex, opts)
161
- }.resume
162
- self
163
- else
164
- send_str ? send_and_wait(send_str, strregex, opts) : wait_for(strregex, opts)
165
- end
166
- end
167
142
 
168
- # Send a string to the server and wait for a response containing a specified String or Regex.
169
- # @param [String] send_str
170
- # @return [String] all data in the buffer including the wait_str if it was found
171
- def send_and_wait(send_str, wait_str = nil, opts = {})
172
- reconnect? ? open : raise(Disconnected) if !connected?
173
- raise ClosedChannel if closed?
174
- debug("send_and_wait(#{send_str.inspect}, #{wait_str.inspect}, #{opts})")
175
- send_data(send_str)
176
- return wait_for(wait_str, opts)
143
+ # @return [Boolean] Is the shell open?
144
+ def open?
145
+ !closed? && @shell
177
146
  end
178
147
 
179
- # Wait for the shell to send data containing the given string.
180
- # @param [String, Regexp] strregex a string or regex to match the console output against.
181
- # @param [Hash] opts
182
- # @option opts [Fixnum] :timeout (Session::TIMEOUT) the maximum number of seconds to wait
183
- # @return [String] the contents of the buffer or a TimeoutError
184
- # @raise Disconnected
185
- # @raise ClosedChannel
186
- # @raise TimeoutError
187
- def wait_for(strregex, opts = { })
188
- reconnect? ? open : raise(Disconnected) unless connected?
189
- raise ClosedChannel if closed?
190
- debug("wait_for(#{strregex.inspect}, #{opts})")
191
- opts = { :timeout => @timeout }.merge(opts)
192
- found = nil
193
- f = Fiber.current
194
- trace = caller
195
- timer = nil
196
- data_callback = nil
197
- matched = false
198
- started = Time.new
199
-
200
- timeout = proc do
201
- data_callback && data_callback.cancel
202
- f.resume(TimeoutError.new("#{host}: inactivity timeout (#{opts[:timeout]}) while waiting for #{strregex.inspect}; received: #{@buffer.inspect}; waited total: #{Time.new - started}"))
203
- end
204
-
205
- data_callback = on(:data) do
206
- timer && timer.cancel
207
- if matched
208
- warn("data_callback invoked when already matched")
209
- next
210
- end
211
- if (matched = @buffer.match(strregex))
212
- data_callback.cancel
213
- @buffer=matched.post_match
214
- f.resume(matched.pre_match + matched.to_s)
215
- else
216
- timer = EM::Timer.new(opts[:timeout], &timeout)
217
- end
218
- end
219
-
220
- # Check against current buffer
221
- EM::next_tick {
222
- data_callback.call() if @buffer.length>0
223
- }
224
-
225
- timer = EM::Timer.new(opts[:timeout], &timeout)
226
- debug("set timer: #{timer} for #{opts[:timeout]}")
227
- Fiber.yield.tap do |res|
228
- if res.is_a?(Exception)
229
- res.set_backtrace(Array(res.backtrace) + trace)
230
- raise res
231
- end
232
- yield(res) if block_given?
233
- end
234
- end
235
-
236
-
237
148
  # Open a shell on the server.
238
149
  # You generally don't need to call this.
239
150
  # @return [self, Exception]
240
151
  def open(&blk)
241
- debug("open(#{blk})")
242
- f = Fiber.current
243
- trace = caller
152
+ f = Fiber.current
153
+ trace = caller
154
+ on_open do |s|
155
+ Fiber.new { yield(self) if block_given? }.resume
156
+ f.resume(self)
157
+ end
158
+ open!
159
+ return Fiber.yield.tap { |r| raise r if r.is_a?(Exception) }
160
+ end
161
+
162
+ private
244
163
 
164
+ def open!
165
+ return if @opening
166
+ @opening = true
245
167
  begin
246
168
  connect
247
169
  session.open_channel do |channel|
@@ -250,32 +172,56 @@ module EventMachine
250
172
  debug "***** pty open: #{pty}; suc: #{suc}"
251
173
  pty.send_channel_request("shell") do |shell,success|
252
174
  if !success
253
- f.resume(ConnectionError.new("Failed to create shell").tap{|e| e.set_backtrace(caller) })
175
+ set_open_status(ConnectionError.new("Failed to create shell").tap{|e| e.set_backtrace(caller) })
254
176
  else
255
177
  debug "***** shell open: #{shell}"
256
178
  @closed = false
257
179
  @shell = shell
258
- @shell.on_data do |ch,data|
259
- @buffer += data
260
- debug("data: #{@buffer.dump}")
261
- fire(:data, data)
262
- end
263
- Fiber.new { yield(self) if block_given? }.resume
264
- f.resume(self)
180
+ shell.extend(EventMachine::Ssh::Connection::Channel::Interactive)
181
+ shell.line_terminator = @line_terminator if @line_terminator
182
+ shell.on(:data) { |data| debug("#{shell.dump_buffers}") }
183
+ set_open_status(self)
265
184
  end
185
+ @opening = false
266
186
  end # |shell,success|
267
187
  end # |pty,suc|
268
188
  end # |channel|
269
189
  rescue => e
190
+ @opening = false
270
191
  raise ConnectionError.new("failed to create shell for #{host}: #{e} (#{e.class})")
271
192
  end
193
+ end
272
194
 
273
- return Fiber.yield.tap { |r| raise r if r.is_a?(Exception) }
195
+ def on_open(&cb)
196
+ if open?
197
+ EM.next_tick { cb.call(@open_status) }
198
+ else
199
+ open_callbacks << cb
200
+ end
274
201
  end
275
202
 
276
- # Create a new shell using the same ssh connection.
277
- # A connection will be established if this shell is not connected.
278
- # If a block is provided the child will be closed after yielding.
203
+ def open_callbacks
204
+ @open_callbacks ||= []
205
+ end
206
+
207
+ def set_open_status(status)
208
+ @open_status = status
209
+ open_callbacks.clone.each do |cb|
210
+ open_callbacks.delete(cb)
211
+ cb.call(status)
212
+ end
213
+ @open_status
214
+ end
215
+
216
+ public
217
+
218
+ # Create a new shell using the same ssh connection. A connection will be
219
+ # established if this shell is not connected.
220
+ #
221
+ # If a block is provided the call to split must be inside of a Fiber. The
222
+ # child will be closed after yielding. The block will not be yielded
223
+ # until the remote PTY has been opened.
224
+ #
279
225
  # @yield [Shell] child
280
226
  # @return [Shell] child
281
227
  def split
@@ -288,7 +234,13 @@ module EventMachine
288
234
  fire(:childless).tap{ info("fired :childless") } if children.empty?
289
235
  end
290
236
  fire(:split, child)
291
- block_given? ? yield(child).tap { child.close } : child
237
+ if block_given?
238
+ # requires that the caller be in a Fiber
239
+ child.open rescue child.fail($!)
240
+ yield(child).tap { child.close }
241
+ else
242
+ child
243
+ end
292
244
  end
293
245
 
294
246
  # Connect to the server.
@@ -301,30 +253,84 @@ module EventMachine
301
253
  ::EM::Ssh.start(host, user, connect_opts) do |connection|
302
254
  @connection = connection
303
255
  connection.callback do |ssh|
304
- f.resume(@session = ssh)
256
+ f.resume(@session = ssh) if f.alive?
305
257
  end
306
258
  connection.errback do |e|
307
259
  e.set_backtrace(trace + Array(e.backtrace))
308
- f.resume(e)
260
+ f.resume(e) if f.alive?
309
261
  end
310
262
  end
311
263
  return Fiber.yield.tap { |r| raise r if r.is_a?(Exception) }
312
264
  end
313
265
 
266
+ # Ensure the channel is open of fail.
267
+ def assert_channel!
268
+ reconnect? ? open : raise(Disconnected) unless connected? && @shell
269
+ raise ClosedChannel if closed?
270
+ end
271
+ private :assert_channel!
272
+
273
+ # Wait for a number of seconds until a specified string or regexp is matched by the
274
+ # data returned from the ssh connection. Optionally send a given string first.
275
+ #
276
+ # If a block is not provided the current Fiber will yield until strregex matches or
277
+ # :timeout # is reached.
278
+ #
279
+ # If a block is provided expect will return immediately.
280
+ #
281
+ # @param [String, Regexp] strregex to match against
282
+ # @param [String] send_str the data to send before waiting
283
+ # @param [Hash] opts
284
+ # @option opts [Fixnum] :timeout (@timeout) number of seconds to wait when there is no activity
285
+ # @return [Shell, String] all data received up to an including strregex if a block is not provided.
286
+ # the Shell if a block is provided
287
+ # @example expect a prompt
288
+ # expect(' ~]$ ')
289
+ # @example send a command and wait for a prompt
290
+ # expect(' ~]$ ', '/sbin/ifconfig')
291
+ # @example expect a prompt and within 5 seconds
292
+ # expect(' ~]$ ', :timeout => 5)
293
+ # @example send a command and wait up to 10 seconds for a prompt
294
+ # expect(' ~]$ ', '/etc/sysconfig/openvpn restart', :timeout => 10)
295
+ def expect(strregex, send_str = nil, opts = {}, &blk)
296
+ assert_channel!
297
+ shell.expect(strregex,
298
+ send_str,
299
+ {:timeout => @timeout, :log => self }.merge(opts),
300
+ &blk)
301
+ end
302
+
303
+ # Send a string to the server and wait for a response containing a specified String or Regex.
304
+ # @param [String] send_str
305
+ # @return [String] all data in the buffer including the wait_str if it was found
306
+ def send_and_wait(send_str, wait_str = nil, opts = {})
307
+ assert_channel!
308
+ shell.send_and_wait(send_str,
309
+ wait_str,
310
+ {:timeout => @timeout, :log => self }.merge(opts))
311
+ end
314
312
 
315
313
  # Send data to the ssh server shell.
316
314
  # You generally don't need to call this.
317
315
  # @see #send_and_wait
318
316
  # @param [String] d the data to send encoded as a string
319
317
  def send_data(d, send_newline=true)
320
- return unless shell
321
- if send_newline
322
- shell.send_data("#{d}#{line_terminator}")
323
- else
324
- shell.send_data("#{d}")
325
- end
318
+ assert_channel!
319
+ shell.send_data(d, send_newline)
326
320
  end
327
321
 
322
+ # Wait for the shell to send data containing the given string.
323
+ # @param [String, Regexp] strregex a string or regex to match the console output against.
324
+ # @param [Hash] opts
325
+ # @option opts [Fixnum] :timeout (Session::TIMEOUT) the maximum number of seconds to wait
326
+ # @return [String] the contents of the buffer or a TimeoutError
327
+ # @raise Disconnected
328
+ # @raise ClosedChannel
329
+ # @raise TimeoutError
330
+ def wait_for(strregex, opts = { })
331
+ assert_channel!
332
+ shell.wait_for(strregex, {:timeout => @timeout, :log => self }.merge(opts))
333
+ end
328
334
 
329
335
  def debug(msg = nil, &blk)
330
336
  super("#{host} #{msg}", &blk)
@@ -346,7 +352,6 @@ module EventMachine
346
352
  super("#{host} #{msg}", &blk)
347
353
  end
348
354
 
349
-
350
355
  private
351
356
  # TODO move private stuff down to private
352
357
  # e.g., #open, #connect,
@@ -1,5 +1,5 @@
1
1
  module EventMachine
2
2
  class Ssh
3
- VERSION='0.4.3'
3
+ VERSION='0.5.0'
4
4
  end # class::Ssh
5
5
  end # module::EventMachine
data/lib/em-ssh.rb CHANGED
@@ -52,7 +52,7 @@ module EventMachine
52
52
  # log.debug "**** channel: #{channel}"
53
53
  # channel.request_pty(options[:pty] || {}) do |pty,suc|
54
54
  def connect(host, user, opts = {}, &blk)
55
- logger.debug("#{self}.connect(#{host}, #{user}, #{opts})")
55
+ opts[:logger] || logger.debug("#{self}.connect(#{host}, #{user}, #{opts})")
56
56
  options = { :host => host, :user => user, :port => DEFAULT_PORT }.merge(opts)
57
57
  EM.connect(options[:host], options[:port], Connection, options, &blk)
58
58
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: em-ssh
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.5.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-09-23 00:00:00.000000000 Z
12
+ date: 2013-03-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: eventmachine
@@ -86,6 +86,8 @@ extra_rdoc_files: []
86
86
  files:
87
87
  - lib/em-ssh/authentication-session.rb
88
88
  - lib/em-ssh/callbacks.rb
89
+ - lib/em-ssh/connection/channel/interactive.rb
90
+ - lib/em-ssh/connection/channel/null-logger.rb
89
91
  - lib/em-ssh/connection.rb
90
92
  - lib/em-ssh/ext/net/ssh/connection/channel.rb
91
93
  - lib/em-ssh/log.rb
@@ -97,6 +99,7 @@ files:
97
99
  - lib/em-ssh.rb
98
100
  - bin/em-ssh
99
101
  - bin/em-ssh-shell
102
+ - CHANGELOG.md
100
103
  - README.md
101
104
  homepage: http://github.com/simulacre/em-ssh
102
105
  licenses: