net-ssh-multi 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +13 -0
- data/Manifest +23 -0
- data/README.rdoc +87 -0
- data/Rakefile +28 -0
- data/lib/net/ssh/multi.rb +71 -0
- data/lib/net/ssh/multi/channel.rb +216 -0
- data/lib/net/ssh/multi/channel_proxy.rb +50 -0
- data/lib/net/ssh/multi/dynamic_server.rb +71 -0
- data/lib/net/ssh/multi/pending_connection.rb +112 -0
- data/lib/net/ssh/multi/server.rb +229 -0
- data/lib/net/ssh/multi/server_list.rb +80 -0
- data/lib/net/ssh/multi/session.rb +546 -0
- data/lib/net/ssh/multi/session_actions.rb +153 -0
- data/lib/net/ssh/multi/subsession.rb +48 -0
- data/lib/net/ssh/multi/version.rb +21 -0
- data/net-ssh-multi.gemspec +59 -0
- data/setup.rb +1585 -0
- data/test/channel_test.rb +152 -0
- data/test/common.rb +2 -0
- data/test/multi_test.rb +20 -0
- data/test/server_test.rb +256 -0
- data/test/session_actions_test.rb +128 -0
- data/test/session_test.rb +201 -0
- data/test/test_all.rb +3 -0
- metadata +93 -0
data/CHANGELOG.rdoc
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
=== 1.0.0 / 1 May 2008
|
2
|
+
|
3
|
+
* (no changes since the last preview release)
|
4
|
+
|
5
|
+
|
6
|
+
=== 1.0 Preview Release 2 (0.99.1) / 19 Apr 2008
|
7
|
+
|
8
|
+
* Don't try to select on closed IO streams [Jamis Buck]
|
9
|
+
|
10
|
+
|
11
|
+
=== 1.0 Preview Release 1 (0.99.0) / 10 Apr 2008
|
12
|
+
|
13
|
+
* First release of Net::SSH::Multi
|
data/Manifest
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
CHANGELOG.rdoc
|
2
|
+
lib/net/ssh/multi/channel.rb
|
3
|
+
lib/net/ssh/multi/channel_proxy.rb
|
4
|
+
lib/net/ssh/multi/dynamic_server.rb
|
5
|
+
lib/net/ssh/multi/pending_connection.rb
|
6
|
+
lib/net/ssh/multi/server.rb
|
7
|
+
lib/net/ssh/multi/server_list.rb
|
8
|
+
lib/net/ssh/multi/session.rb
|
9
|
+
lib/net/ssh/multi/session_actions.rb
|
10
|
+
lib/net/ssh/multi/subsession.rb
|
11
|
+
lib/net/ssh/multi/version.rb
|
12
|
+
lib/net/ssh/multi.rb
|
13
|
+
Manifest
|
14
|
+
Rakefile
|
15
|
+
README.rdoc
|
16
|
+
setup.rb
|
17
|
+
test/channel_test.rb
|
18
|
+
test/common.rb
|
19
|
+
test/multi_test.rb
|
20
|
+
test/server_test.rb
|
21
|
+
test/session_actions_test.rb
|
22
|
+
test/session_test.rb
|
23
|
+
test/test_all.rb
|
data/README.rdoc
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
= Net::SSH::Multi
|
2
|
+
|
3
|
+
* http://net-ssh.rubyforge.org/multi
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
Net::SSH::Multi is a library for controlling multiple Net::SSH connections via a single interface. It exposes an API similar to that of Net::SSH::Connection::Session and Net::SSH::Connection::Channel, making it simpler to adapt programs designed for single connections to be used with multiple connections.
|
8
|
+
|
9
|
+
This library is particularly useful for automating repetitive tasks that must be performed on multiple machines. It executes the commands in parallel, and allows commands to be executed on subsets of servers (defined by groups).
|
10
|
+
|
11
|
+
== FEATURES:
|
12
|
+
|
13
|
+
* Easily manage multiple connections
|
14
|
+
* Open channels, spawn processes, etc. on multiple connections in parallel
|
15
|
+
* Transparently limit concurrent connections when dealing with large numbers of servers (Net::SSH::Multi::Session#concurrent_connections)
|
16
|
+
* Specify a default gateway machine through which connections should be tunneled, or even specify a different gateway machine for each server
|
17
|
+
|
18
|
+
== SYNOPSIS:
|
19
|
+
|
20
|
+
In a nutshell:
|
21
|
+
|
22
|
+
require 'net/ssh/multi'
|
23
|
+
|
24
|
+
Net::SSH::Multi.start do |session|
|
25
|
+
# access servers via a gateway
|
26
|
+
session.via 'gateway', 'gateway-user'
|
27
|
+
|
28
|
+
# define the servers we want to use
|
29
|
+
session.use 'user1@host1'
|
30
|
+
session.use 'user2@host2'
|
31
|
+
|
32
|
+
# define servers in groups for more granular access
|
33
|
+
session.group :app do
|
34
|
+
session.use 'user@app1'
|
35
|
+
session.use 'user@app2'
|
36
|
+
end
|
37
|
+
|
38
|
+
# execute commands on all servers
|
39
|
+
session.exec "uptime"
|
40
|
+
|
41
|
+
# execute commands on a subset of servers
|
42
|
+
session.with(:app).exec "hostname"
|
43
|
+
|
44
|
+
# run the aggregated event loop
|
45
|
+
session.loop
|
46
|
+
end
|
47
|
+
|
48
|
+
See Net::SSH::Multi::Session for more documentation.
|
49
|
+
|
50
|
+
== REQUIREMENTS:
|
51
|
+
|
52
|
+
* net-ssh (version 2)
|
53
|
+
* net-ssh-gateway
|
54
|
+
|
55
|
+
If you want to run the tests or use any of the Rake tasks, you'll need:
|
56
|
+
|
57
|
+
* Echoe (for the Rakefile)
|
58
|
+
* Mocha (for the tests)
|
59
|
+
|
60
|
+
== INSTALL:
|
61
|
+
|
62
|
+
* gem install net-ssh-multi (might need sudo privileges)
|
63
|
+
|
64
|
+
== LICENSE:
|
65
|
+
|
66
|
+
(The MIT License)
|
67
|
+
|
68
|
+
Copyright (c) 2008 Jamis Buck <jamis@37signals.com>
|
69
|
+
|
70
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
71
|
+
a copy of this software and associated documentation files (the
|
72
|
+
'Software'), to deal in the Software without restriction, including
|
73
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
74
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
75
|
+
permit persons to whom the Software is furnished to do so, subject to
|
76
|
+
the following conditions:
|
77
|
+
|
78
|
+
The above copyright notice and this permission notice shall be
|
79
|
+
included in all copies or substantial portions of the Software.
|
80
|
+
|
81
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
82
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
83
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
84
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
85
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
86
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
87
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
begin
|
2
|
+
require 'echoe'
|
3
|
+
rescue LoadError
|
4
|
+
abort "You'll need to have `echoe' installed to use Net::SSH::Multi's Rakefile"
|
5
|
+
end
|
6
|
+
|
7
|
+
require './lib/net/ssh/multi/version'
|
8
|
+
|
9
|
+
version = Net::SSH::Multi::Version::STRING.dup
|
10
|
+
if ENV['SNAPSHOT'].to_i == 1
|
11
|
+
version << "." << Time.now.utc.strftime("%Y%m%d%H%M%S")
|
12
|
+
end
|
13
|
+
|
14
|
+
Echoe.new('net-ssh-multi', version) do |p|
|
15
|
+
p.changelog = "CHANGELOG.rdoc"
|
16
|
+
|
17
|
+
p.author = "Jamis Buck"
|
18
|
+
p.email = "jamis@jamisbuck.org"
|
19
|
+
p.summary = "Control multiple Net::SSH connections via a single interface"
|
20
|
+
p.url = "http://net-ssh.rubyforge.org/multi"
|
21
|
+
|
22
|
+
p.dependencies = ["net-ssh >=1.99.2", "net-ssh-gateway >=0.99.0"]
|
23
|
+
|
24
|
+
p.need_zip = true
|
25
|
+
p.include_rakefile = true
|
26
|
+
|
27
|
+
p.rdoc_pattern = /^(lib|README.rdoc|CHANGELOG.rdoc)/
|
28
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'net/ssh/multi/session'
|
2
|
+
|
3
|
+
module Net; module SSH
|
4
|
+
# Net::SSH::Multi is a library for controlling multiple Net::SSH
|
5
|
+
# connections via a single interface. It exposes an API similar to that of
|
6
|
+
# Net::SSH::Connection::Session and Net::SSH::Connection::Channel, making it
|
7
|
+
# simpler to adapt programs designed for single connections to be used with
|
8
|
+
# multiple connections.
|
9
|
+
#
|
10
|
+
# This library is particularly useful for automating repetitive tasks that
|
11
|
+
# must be performed on multiple machines. It executes the commands in
|
12
|
+
# parallel, and allows commands to be executed on subsets of servers
|
13
|
+
# (defined by groups).
|
14
|
+
#
|
15
|
+
# require 'net/ssh/multi'
|
16
|
+
#
|
17
|
+
# Net::SSH::Multi.start do |session|
|
18
|
+
# # access servers via a gateway
|
19
|
+
# session.via 'gateway', 'gateway-user'
|
20
|
+
#
|
21
|
+
# # define the servers we want to use
|
22
|
+
# session.use 'user1@host1'
|
23
|
+
# session.use 'user2@host2'
|
24
|
+
#
|
25
|
+
# # define servers in groups for more granular access
|
26
|
+
# session.group :app do
|
27
|
+
# session.use 'user@app1'
|
28
|
+
# session.use 'user@app2'
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# # execute commands on all servers
|
32
|
+
# session.exec "uptime"
|
33
|
+
#
|
34
|
+
# # execute commands on a subset of servers
|
35
|
+
# session.with(:app).exec "hostname"
|
36
|
+
#
|
37
|
+
# # run the aggregated event loop
|
38
|
+
# session.loop
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# See Net::SSH::Multi::Session for more documentation.
|
42
|
+
module Multi
|
43
|
+
# This is a convenience method for instantiating a new
|
44
|
+
# Net::SSH::Multi::Session. If a block is given, the session will be
|
45
|
+
# yielded to the block automatically closed (see Net::SSH::Multi::Session#close)
|
46
|
+
# when the block finishes. Otherwise, the new session will be returned.
|
47
|
+
#
|
48
|
+
# Net::SSH::Multi.start do |session|
|
49
|
+
# # ...
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# session = Net::SSH::Multi.start
|
53
|
+
# # ...
|
54
|
+
# session.close
|
55
|
+
#
|
56
|
+
# Any options are passed directly to Net::SSH::Multi::Session.new (q.v.).
|
57
|
+
def self.start(options={})
|
58
|
+
session = Session.new(options)
|
59
|
+
|
60
|
+
if block_given?
|
61
|
+
begin
|
62
|
+
yield session
|
63
|
+
session.loop
|
64
|
+
session.close
|
65
|
+
end
|
66
|
+
else
|
67
|
+
return session
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end; end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
module Net; module SSH; module Multi
|
2
|
+
# Net::SSH::Multi::Channel encapsulates a collection of Net::SSH::Connection::Channel
|
3
|
+
# instances from multiple different connections. It allows for operations to
|
4
|
+
# be performed on all contained channels, simultaneously, using an interface
|
5
|
+
# mostly identical to Net::SSH::Connection::Channel itself.
|
6
|
+
#
|
7
|
+
# You typically obtain a Net::SSH::Multi::Channel instance via
|
8
|
+
# Net::SSH::Multi::Session#open_channel or Net::SSH::Multi::Session#exec,
|
9
|
+
# though there is nothing stopping you from instantiating one yourself with
|
10
|
+
# a handful of Net::SSH::Connection::Channel objects (though they should be
|
11
|
+
# associated with connections managed by a Net::SSH::Multi::Session object
|
12
|
+
# for consistent behavior).
|
13
|
+
#
|
14
|
+
# channel = session.open_channel do |ch|
|
15
|
+
# # ...
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# channel.wait
|
19
|
+
class Channel
|
20
|
+
include Enumerable
|
21
|
+
|
22
|
+
# The Net::SSH::Multi::Session instance that controls this channel collection.
|
23
|
+
attr_reader :connection
|
24
|
+
|
25
|
+
# The collection of Net::SSH::Connection::Channel instances that this multi-channel aggregates.
|
26
|
+
attr_reader :channels
|
27
|
+
|
28
|
+
# A Hash of custom properties that may be set and queried on this object.
|
29
|
+
attr_reader :properties
|
30
|
+
|
31
|
+
# Instantiate a new Net::SSH::Multi::Channel instance, controlled by the
|
32
|
+
# given +connection+ (a Net::SSH::Multi::Session object) and wrapping the
|
33
|
+
# given +channels+ (Net::SSH::Connection::Channel instances).
|
34
|
+
#
|
35
|
+
# You will typically never call this directly; rather, you'll get your
|
36
|
+
# multi-channel references via Net::SSH::Multi::Session#open_channel and
|
37
|
+
# friends.
|
38
|
+
def initialize(connection, channels)
|
39
|
+
@connection = connection
|
40
|
+
@channels = channels
|
41
|
+
@properties = {}
|
42
|
+
end
|
43
|
+
|
44
|
+
# Iterate over each component channel object, yielding each in order to the
|
45
|
+
# associated block.
|
46
|
+
def each
|
47
|
+
@channels.each { |channel| yield channel }
|
48
|
+
end
|
49
|
+
|
50
|
+
# Retrieve the property (see #properties) with the given +key+.
|
51
|
+
#
|
52
|
+
# host = channel[:host]
|
53
|
+
def [](key)
|
54
|
+
@properties[key]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Set the property (see #properties) with the given +key+ to the given
|
58
|
+
# +value+.
|
59
|
+
#
|
60
|
+
# channel[:visited] = true
|
61
|
+
def []=(key, value)
|
62
|
+
@properties[key] = value
|
63
|
+
end
|
64
|
+
|
65
|
+
# Perform an +exec+ command on all component channels. The block, if given,
|
66
|
+
# is passed to each component channel, so it will (potentially) be invoked
|
67
|
+
# once for every channel in the collection. The block will receive two
|
68
|
+
# parameters: the specific channel object being operated on, and a boolean
|
69
|
+
# indicating whether the exec succeeded or not.
|
70
|
+
#
|
71
|
+
# channel.exec "ls -l" do |ch, success|
|
72
|
+
# # ...
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# See the documentation in Net::SSH for Net::SSH::Connection::Channel#exec
|
76
|
+
# for more information on how to work with the callback.
|
77
|
+
def exec(command, &block)
|
78
|
+
channels.each { |channel| channel.exec(command, &block) }
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
# Perform a +request_pty+ command on all component channels. The block, if
|
83
|
+
# given, is passed to each component channel, so it will (potentially) be
|
84
|
+
# invoked once for every channel in the collection. The block will
|
85
|
+
# receive two parameters: the specific channel object being operated on,
|
86
|
+
# and a boolean indicating whether the pty request succeeded or not.
|
87
|
+
#
|
88
|
+
# channel.request_pty do |ch, success|
|
89
|
+
# # ...
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# See the documentation in Net::SSH for
|
93
|
+
# Net::SSH::Connection::Channel#request_pty for more information on how to
|
94
|
+
# work with the callback.
|
95
|
+
def request_pty(opts={}, &block)
|
96
|
+
channels.each { |channel| channel.request_pty(opts, &block) }
|
97
|
+
self
|
98
|
+
end
|
99
|
+
|
100
|
+
# Send the given +data+ to each component channel. It will be sent to the
|
101
|
+
# remote process, typically being received on the process' +stdin+ stream.
|
102
|
+
#
|
103
|
+
# channel.send_data "password\n"
|
104
|
+
def send_data(data)
|
105
|
+
channels.each { |channel| channel.send_data(data) }
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns true as long as any of the component channels are active.
|
110
|
+
#
|
111
|
+
# connection.loop { channel.active? }
|
112
|
+
def active?
|
113
|
+
channels.any? { |channel| channel.active? }
|
114
|
+
end
|
115
|
+
|
116
|
+
# Runs the connection's event loop until the channel is no longer active
|
117
|
+
# (see #active?).
|
118
|
+
#
|
119
|
+
# channel.exec "something"
|
120
|
+
# channel.wait
|
121
|
+
def wait
|
122
|
+
connection.loop { active? }
|
123
|
+
self
|
124
|
+
end
|
125
|
+
|
126
|
+
# Closes all component channels.
|
127
|
+
def close
|
128
|
+
channels.each { |channel| channel.close }
|
129
|
+
self
|
130
|
+
end
|
131
|
+
|
132
|
+
# Tells the remote process for each component channel not to expect any
|
133
|
+
# further data from this end of the channel.
|
134
|
+
def eof!
|
135
|
+
channels.each { |channel| channel.eof! }
|
136
|
+
self
|
137
|
+
end
|
138
|
+
|
139
|
+
# Registers a callback on all component channels, to be invoked when the
|
140
|
+
# remote process emits data (usually on its +stdout+ stream). The block
|
141
|
+
# will be invoked with two arguments: the specific channel object, and the
|
142
|
+
# data that was received.
|
143
|
+
#
|
144
|
+
# channel.on_data do |ch, data|
|
145
|
+
# puts "got data: #{data}"
|
146
|
+
# end
|
147
|
+
def on_data(&block)
|
148
|
+
channels.each { |channel| channel.on_data(&block) }
|
149
|
+
self
|
150
|
+
end
|
151
|
+
|
152
|
+
# Registers a callback on all component channels, to be invoked when the
|
153
|
+
# remote process emits "extended" data (typically on its +stderr+ stream).
|
154
|
+
# The block will be invoked with three arguments: the specific channel
|
155
|
+
# object, an integer describing the data type (usually a 1 for +stderr+)
|
156
|
+
# and the data that was received.
|
157
|
+
#
|
158
|
+
# channel.on_extended_data do |ch, type, data|
|
159
|
+
# puts "got extended data: #{data}"
|
160
|
+
# end
|
161
|
+
def on_extended_data(&block)
|
162
|
+
channels.each { |channel| channel.on_extended_data(&block) }
|
163
|
+
self
|
164
|
+
end
|
165
|
+
|
166
|
+
# Registers a callback on all component channels, to be invoked during the
|
167
|
+
# idle portion of the connection event loop. The callback will be invoked
|
168
|
+
# with one argument: the specific channel object being processed.
|
169
|
+
#
|
170
|
+
# channel.on_process do |ch|
|
171
|
+
# # ...
|
172
|
+
# end
|
173
|
+
def on_process(&block)
|
174
|
+
channels.each { |channel| channel.on_process(&block) }
|
175
|
+
self
|
176
|
+
end
|
177
|
+
|
178
|
+
# Registers a callback on all component channels, to be invoked when the
|
179
|
+
# remote server terminates the channel. The callback will be invoked
|
180
|
+
# with one argument: the specific channel object being closed.
|
181
|
+
#
|
182
|
+
# channel.on_close do |ch|
|
183
|
+
# # ...
|
184
|
+
# end
|
185
|
+
def on_close(&block)
|
186
|
+
channels.each { |channel| channel.on_close(&block) }
|
187
|
+
self
|
188
|
+
end
|
189
|
+
|
190
|
+
# Registers a callback on all component channels, to be invoked when the
|
191
|
+
# remote server has no further data to send. The callback will be invoked
|
192
|
+
# with one argument: the specific channel object being marked EOF.
|
193
|
+
#
|
194
|
+
# channel.on_eof do |ch|
|
195
|
+
# # ...
|
196
|
+
# end
|
197
|
+
def on_eof(&block)
|
198
|
+
channels.each { |channel| channel.on_eof(&block) }
|
199
|
+
self
|
200
|
+
end
|
201
|
+
|
202
|
+
# Registers a callback on all component channels, to be invoked when the
|
203
|
+
# remote server sends a channel request of the given +type+. The callback
|
204
|
+
# will be invoked with two arguments: the specific channel object receiving
|
205
|
+
# the request, and a Net::SSH::Buffer instance containing the request-specific
|
206
|
+
# data.
|
207
|
+
#
|
208
|
+
# channel.on_request("exit-status") do |ch, data|
|
209
|
+
# puts "exited with #{data.read_long}"
|
210
|
+
# end
|
211
|
+
def on_request(type, &block)
|
212
|
+
channels.each { |channel| channel.on_request(type, &block) }
|
213
|
+
self
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end; end; end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Net; module SSH; module Multi
|
2
|
+
|
3
|
+
# The ChannelProxy is a delegate class that represents a channel that has
|
4
|
+
# not yet been opened. It is only used when Net::SSH::Multi is running with
|
5
|
+
# with a concurrent connections limit (see Net::SSH::Multi::Session#concurrent_connections).
|
6
|
+
#
|
7
|
+
# You'll never need to instantiate one of these directly, and will probably
|
8
|
+
# (if all goes well!) never even notice when one of these is in use. Essentially,
|
9
|
+
# it is spawned by a Net::SSH::Multi::PendingConnection when the pending
|
10
|
+
# connection is asked to open a channel. Any actions performed on the
|
11
|
+
# channel proxy will then be recorded, until a real channel is set as the
|
12
|
+
# delegate (see #delegate_to). At that point, all recorded actions will be
|
13
|
+
# replayed on the channel, and any subsequent actions will be immediately
|
14
|
+
# delegated to the channel.
|
15
|
+
class ChannelProxy
|
16
|
+
# This is the "on confirm" callback that gets called when the real channel
|
17
|
+
# is opened.
|
18
|
+
attr_reader :on_confirm
|
19
|
+
|
20
|
+
# Instantiates a new channel proxy with the given +on_confirm+ callback.
|
21
|
+
def initialize(&on_confirm)
|
22
|
+
@on_confirm = on_confirm
|
23
|
+
@recordings = []
|
24
|
+
@channel = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
# Instructs the proxy to delegate all further actions to the given +channel+
|
28
|
+
# (which must be an instance of Net::SSH::Connection::Channel). All recorded
|
29
|
+
# actions are immediately replayed, in order, against the delegate channel.
|
30
|
+
def delegate_to(channel)
|
31
|
+
@channel = channel
|
32
|
+
@recordings.each do |sym, args, block|
|
33
|
+
@channel.__send__(sym, *args, &block)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# If a channel delegate has been specified (see #delegate_to), the method
|
38
|
+
# will be immediately sent to the delegate. Otherwise, the call is added
|
39
|
+
# to the list of recorded method calls, to be played back when a delegate
|
40
|
+
# is specified.
|
41
|
+
def method_missing(sym, *args, &block)
|
42
|
+
if @channel
|
43
|
+
@channel.__send__(sym, *args, &block)
|
44
|
+
else
|
45
|
+
@recordings << [sym, args, block]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end; end; end
|