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 +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:
|