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 +34 -0
- data/README.md +80 -23
- data/lib/em-ssh/connection/channel/interactive.rb +168 -0
- data/lib/em-ssh/connection/channel/null-logger.rb +17 -0
- data/lib/em-ssh/log.rb +1 -1
- data/lib/em-ssh/shell.rb +135 -130
- data/lib/em-ssh/version.rb +1 -1
- data/lib/em-ssh.rb +1 -1
- metadata +5 -2
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,
|
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
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
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
|
-
|
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
|
-
@
|
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
|
-
#
|
169
|
-
|
170
|
-
|
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
|
-
|
242
|
-
|
243
|
-
|
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
|
-
|
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
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
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
|
-
|
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
|
-
|
277
|
-
|
278
|
-
|
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?
|
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
|
-
|
321
|
-
|
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,
|
data/lib/em-ssh/version.rb
CHANGED
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
|
+
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:
|
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:
|