em-ssh 0.4.3 → 0.5.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/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: