stomp 1.1.3 → 1.1.4
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 +15 -0
- data/Rakefile +7 -0
- data/lib/stomp.rb +5 -6
- data/lib/stomp/client.rb +31 -17
- data/lib/stomp/connection.rb +204 -147
- data/lib/stomp/errors.rb +27 -0
- data/lib/stomp/ext/hash.rb +24 -0
- data/lib/stomp/message.rb +36 -4
- data/test/test_client.rb +54 -40
- data/test/test_connection.rb +66 -28
- data/test/test_helper.rb +20 -1
- metadata +6 -2
data/CHANGELOG
CHANGED
@@ -1,3 +1,18 @@
|
|
1
|
+
== 1.1.4 2010-21-01
|
2
|
+
|
3
|
+
* Added unreceive message method that sends the message back to its queue or to the
|
4
|
+
dead letter queue, depending on the :max_redeliveries option, similar to a13m one.
|
5
|
+
* Added environment variable option for running 'rake test' on any stomp server, using any port with any user.
|
6
|
+
* Added suppress_content_length header option for ActiveMQ knowing it is a text message (see:
|
7
|
+
http://juretta.com/log/2009/05/24/activemq-jms-stomp/)
|
8
|
+
* Fixed some bugs with Ruby 1.9 (concatenate string + exception)
|
9
|
+
* Major changes on message parsing feature
|
10
|
+
* Fixed bug with old socket not being closed when using failover
|
11
|
+
* Fixed broken poll method on Connection
|
12
|
+
* Fixed broken close method on Client
|
13
|
+
* Added connection_frame accessor
|
14
|
+
* Added disconnect receipt
|
15
|
+
|
1
16
|
== 1.1.3 2009-24-11
|
2
17
|
|
3
18
|
* Failover support
|
data/Rakefile
CHANGED
@@ -44,6 +44,13 @@ Rake::RDocTask.new do |rd|
|
|
44
44
|
rd.options = spec.rdoc_options
|
45
45
|
end
|
46
46
|
|
47
|
+
desc "Rspec : run all with RCov"
|
48
|
+
Spec::Rake::SpecTask.new('spec:rcov') do |t|
|
49
|
+
t.spec_files = FileList['spec/**/*.rb']
|
50
|
+
t.rcov = true
|
51
|
+
t.rcov_opts = ['--exclude', 'gems', '--exclude', 'spec']
|
52
|
+
end
|
53
|
+
|
47
54
|
desc "RSpec : run all"
|
48
55
|
Spec::Rake::SpecTask.new('spec') do |t|
|
49
56
|
t.spec_files = FileList['spec/**/*.rb']
|
data/lib/stomp.rb
CHANGED
@@ -13,12 +13,11 @@
|
|
13
13
|
# See the License for the specific language governing permissions and
|
14
14
|
# limitations under the License.
|
15
15
|
|
16
|
-
require '
|
17
|
-
require '
|
18
|
-
require '
|
19
|
-
require 'stomp
|
20
|
-
require 'stomp
|
21
|
-
require 'stomp/message'
|
16
|
+
require File.join(File.dirname(__FILE__), 'stomp', 'ext', 'hash')
|
17
|
+
require File.join(File.dirname(__FILE__), 'stomp', 'connection')
|
18
|
+
require File.join(File.dirname(__FILE__), 'stomp', 'client')
|
19
|
+
require File.join(File.dirname(__FILE__), 'stomp', 'message')
|
20
|
+
require File.join(File.dirname(__FILE__), 'stomp', 'errors')
|
22
21
|
|
23
22
|
module Stomp
|
24
23
|
end
|
data/lib/stomp/client.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
1
3
|
module Stomp
|
2
4
|
|
3
5
|
# Typical Stomp client class. Uses a listener thread to receive frames
|
@@ -7,7 +9,7 @@ module Stomp
|
|
7
9
|
# in that thread if you have much message volume.
|
8
10
|
class Client
|
9
11
|
|
10
|
-
attr_reader :login, :passcode, :host, :port, :reliable, :
|
12
|
+
attr_reader :login, :passcode, :host, :port, :reliable, :parameters
|
11
13
|
alias :obj_send :send
|
12
14
|
|
13
15
|
# A new Client object can be initialized using two forms:
|
@@ -40,7 +42,7 @@ module Stomp
|
|
40
42
|
@login = first_host[:login]
|
41
43
|
@passcode = first_host[:passcode]
|
42
44
|
@host = first_host[:host]
|
43
|
-
@port = first_host[:port] || default_port(first_host[:ssl])
|
45
|
+
@port = first_host[:port] || Connection::default_port(first_host[:ssl])
|
44
46
|
|
45
47
|
@reliable = true
|
46
48
|
|
@@ -57,7 +59,7 @@ module Stomp
|
|
57
59
|
@login = first_host[:login] = $4 || ""
|
58
60
|
@passcode = first_host[:passcode] = $5 || ""
|
59
61
|
@host = first_host[:host] = $6
|
60
|
-
@port = first_host[:port] = $7.to_i || default_port(first_host[:ssl])
|
62
|
+
@port = first_host[:port] = $7.to_i || Connection::default_port(first_host[:ssl])
|
61
63
|
|
62
64
|
options = $16 || ""
|
63
65
|
parts = options.split(/&|=/)
|
@@ -168,7 +170,13 @@ module Stomp
|
|
168
170
|
end
|
169
171
|
@connection.ack message.headers['message-id'], headers
|
170
172
|
end
|
171
|
-
|
173
|
+
|
174
|
+
# Unreceive a message, sending it back to its queue or to the DLQ
|
175
|
+
# client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
|
176
|
+
#
|
177
|
+
def unreceive(message)
|
178
|
+
@connection.unreceive message
|
179
|
+
end
|
172
180
|
# Send message to destination
|
173
181
|
#
|
174
182
|
# If a block is given a receipt will be requested and passed to the
|
@@ -181,6 +189,14 @@ module Stomp
|
|
181
189
|
end
|
182
190
|
@connection.send(destination, message, headers)
|
183
191
|
end
|
192
|
+
|
193
|
+
def connection_frame
|
194
|
+
@connection.connection_frame
|
195
|
+
end
|
196
|
+
|
197
|
+
def disconnect_receipt
|
198
|
+
@connection.disconnect_receipt
|
199
|
+
end
|
184
200
|
|
185
201
|
# Is this client open?
|
186
202
|
def open?
|
@@ -193,9 +209,14 @@ module Stomp
|
|
193
209
|
end
|
194
210
|
|
195
211
|
# Close out resources in use by this client
|
196
|
-
def close
|
197
|
-
@
|
198
|
-
@
|
212
|
+
def close headers={}
|
213
|
+
@listener_thread.exit
|
214
|
+
@connection.disconnect headers
|
215
|
+
end
|
216
|
+
|
217
|
+
# Check if the thread was created and isn't dead
|
218
|
+
def running
|
219
|
+
@listener_thread && !!@listener_thread.status
|
199
220
|
end
|
200
221
|
|
201
222
|
private
|
@@ -210,12 +231,6 @@ module Stomp
|
|
210
231
|
id
|
211
232
|
end
|
212
233
|
|
213
|
-
def default_port(ssl)
|
214
|
-
return 61612 if ssl
|
215
|
-
|
216
|
-
61613
|
217
|
-
end
|
218
|
-
|
219
234
|
def parse_hosts(url)
|
220
235
|
hosts = []
|
221
236
|
|
@@ -257,15 +272,14 @@ module Stomp
|
|
257
272
|
def start_listeners
|
258
273
|
@listeners = {}
|
259
274
|
@receipt_listeners = {}
|
260
|
-
@running = true
|
261
275
|
@replay_messages_by_txn = {}
|
262
276
|
|
263
277
|
@listener_thread = Thread.start do
|
264
|
-
while
|
265
|
-
message = @connection.
|
278
|
+
while true
|
279
|
+
message = @connection.poll
|
266
280
|
case
|
267
281
|
when message.nil?
|
268
|
-
|
282
|
+
sleep 0.1
|
269
283
|
when message.command == 'MESSAGE'
|
270
284
|
if listener = @listeners[message.headers['destination']]
|
271
285
|
listener.call(message)
|
data/lib/stomp/connection.rb
CHANGED
@@ -1,10 +1,20 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'monitor'
|
3
|
+
require 'timeout'
|
4
|
+
|
1
5
|
module Stomp
|
2
6
|
|
3
7
|
# Low level connection which maps commands and supports
|
4
8
|
# synchronous receives
|
5
9
|
class Connection
|
6
|
-
|
10
|
+
attr_reader :connection_frame
|
11
|
+
attr_reader :disconnect_receipt
|
7
12
|
alias :obj_send :send
|
13
|
+
|
14
|
+
def self.default_port(ssl)
|
15
|
+
ssl ? 61612 : 61613
|
16
|
+
end
|
17
|
+
|
8
18
|
# A new Connection object accepts the following parameters:
|
9
19
|
#
|
10
20
|
# login (String, default : '')
|
@@ -45,6 +55,8 @@ module Stomp
|
|
45
55
|
# stomp://user:pass@host.domain.tld:port
|
46
56
|
#
|
47
57
|
def initialize(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false, reconnect_delay = 5, connect_headers = {})
|
58
|
+
@received_messages = []
|
59
|
+
|
48
60
|
if login.is_a?(Hash)
|
49
61
|
hashed_initialize(login)
|
50
62
|
else
|
@@ -60,8 +72,7 @@ module Stomp
|
|
60
72
|
end
|
61
73
|
|
62
74
|
@transmit_semaphore = Mutex.new
|
63
|
-
@read_semaphore =
|
64
|
-
@socket_semaphore = Mutex.new
|
75
|
+
@read_semaphore = Monitor.new
|
65
76
|
|
66
77
|
@subscriptions = {}
|
67
78
|
@failure = nil
|
@@ -87,37 +98,28 @@ module Stomp
|
|
87
98
|
end
|
88
99
|
|
89
100
|
def socket
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
s = @socket;
|
94
|
-
|
95
|
-
s = nil unless connected?
|
101
|
+
@read_semaphore.synchronize do
|
102
|
+
used_socket = @socket
|
103
|
+
used_socket = nil if closed?
|
96
104
|
|
97
|
-
while
|
105
|
+
while used_socket.nil? || !@failure.nil?
|
98
106
|
@failure = nil
|
99
107
|
begin
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
headers[:login] = @login
|
105
|
-
headers[:passcode] = @passcode
|
106
|
-
_transmit(s, "CONNECT", headers)
|
107
|
-
@connect = _receive(s)
|
108
|
-
# replay any subscriptions.
|
109
|
-
@subscriptions.each { |k,v| _transmit(s, "SUBSCRIBE", v) }
|
108
|
+
used_socket = open_socket
|
109
|
+
# Open complete
|
110
|
+
|
111
|
+
connect(used_socket)
|
110
112
|
|
111
113
|
@connection_attempts = 0
|
112
114
|
rescue
|
113
|
-
@failure =
|
114
|
-
|
115
|
+
@failure = $!
|
116
|
+
used_socket = nil
|
115
117
|
raise unless @reliable
|
116
|
-
$stderr.print "connect to #{@host} failed:
|
118
|
+
$stderr.print "connect to #{@host} failed: #{$!} will retry(##{@connection_attempts}) in #{@reconnect_delay}\n"
|
117
119
|
|
118
|
-
raise
|
120
|
+
raise Stomp::Error::MaxReconnectAttempts if max_reconnect_attempts?
|
119
121
|
|
120
|
-
sleep(@reconnect_delay)
|
122
|
+
sleep(@reconnect_delay)
|
121
123
|
|
122
124
|
@connection_attempts += 1
|
123
125
|
|
@@ -127,86 +129,28 @@ module Stomp
|
|
127
129
|
end
|
128
130
|
end
|
129
131
|
end
|
130
|
-
@socket =
|
131
|
-
return s;
|
132
|
-
#end
|
133
|
-
end
|
134
|
-
|
135
|
-
def connected?
|
136
|
-
begin
|
137
|
-
test_socket = TCPSocket.open @host, @port
|
138
|
-
test_socket.close
|
139
|
-
open?
|
140
|
-
rescue
|
141
|
-
false
|
132
|
+
@socket = used_socket
|
142
133
|
end
|
143
134
|
end
|
144
|
-
|
145
|
-
def close_socket
|
146
|
-
begin
|
147
|
-
@socket.close
|
148
|
-
rescue
|
149
|
-
#Ignoring if already closed
|
150
|
-
end
|
151
|
-
|
152
|
-
@closed = true
|
153
|
-
end
|
154
|
-
|
155
|
-
def open_socket
|
156
|
-
return TCPSocket.open @host, @port unless @ssl
|
157
|
-
|
158
|
-
ssl_socket
|
159
|
-
end
|
160
|
-
|
161
|
-
def ssl_socket
|
162
|
-
require 'openssl' unless defined?(OpenSSL)
|
163
|
-
|
164
|
-
ctx = OpenSSL::SSL::SSLContext.new
|
165
|
-
|
166
|
-
# For client certificate authentication:
|
167
|
-
# key_path = ENV["STOMP_KEY_PATH"] || "~/stomp_keys"
|
168
|
-
# ctx.cert = OpenSSL::X509::Certificate.new("#{key_path}/client.cer")
|
169
|
-
# ctx.key = OpenSSL::PKey::RSA.new("#{key_path}/client.keystore")
|
170
|
-
|
171
|
-
# For server certificate authentication:
|
172
|
-
# truststores = OpenSSL::X509::Store.new
|
173
|
-
# truststores.add_file("#{key_path}/client.ts")
|
174
|
-
# ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
175
|
-
# ctx.cert_store = truststores
|
176
|
-
|
177
|
-
ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
178
|
-
|
179
|
-
tcp_socket = TCPSocket.new @host, @port
|
180
|
-
ssl = OpenSSL::SSL::SSLSocket.new(tcp_socket, ctx)
|
181
|
-
ssl.connect
|
182
|
-
ssl
|
183
|
-
end
|
184
135
|
|
185
136
|
def refine_params(params)
|
186
|
-
params =
|
137
|
+
params = params.uncamelize_and_symbolize_keys
|
187
138
|
|
188
|
-
{
|
139
|
+
default_params = {
|
140
|
+
:connect_headers => {},
|
141
|
+
# Failover parameters
|
189
142
|
:initial_reconnect_delay => 0.01,
|
190
143
|
:max_reconnect_delay => 30.0,
|
191
144
|
:use_exponential_back_off => true,
|
192
145
|
:back_off_multiplier => 2,
|
193
146
|
:max_reconnect_attempts => 0,
|
194
147
|
:randomize => false,
|
195
|
-
:connect_headers => {},
|
196
148
|
:backup => false,
|
197
149
|
:timeout => -1
|
198
|
-
}
|
199
|
-
|
200
|
-
end
|
201
|
-
|
202
|
-
def uncamelized_sym_keys(params)
|
203
|
-
uncamelized = {}
|
204
|
-
params.each_pair do |key, value|
|
205
|
-
key = key.to_s.split(/(?=[A-Z])/).join('_').downcase.to_sym
|
206
|
-
uncamelized[key] = value
|
207
|
-
end
|
150
|
+
}
|
208
151
|
|
209
|
-
|
152
|
+
default_params.merge(params)
|
153
|
+
|
210
154
|
end
|
211
155
|
|
212
156
|
def change_host
|
@@ -218,18 +162,12 @@ module Stomp
|
|
218
162
|
|
219
163
|
@ssl = current_host[:ssl]
|
220
164
|
@host = current_host[:host]
|
221
|
-
@port = current_host[:port] || default_port(@ssl)
|
165
|
+
@port = current_host[:port] || Connection::default_port(@ssl)
|
222
166
|
@login = current_host[:login] || ""
|
223
167
|
@passcode = current_host[:passcode] || ""
|
224
168
|
|
225
169
|
end
|
226
170
|
|
227
|
-
def default_port(ssl)
|
228
|
-
return 61612 if ssl
|
229
|
-
|
230
|
-
61613
|
231
|
-
end
|
232
|
-
|
233
171
|
def max_reconnect_attempts?
|
234
172
|
!(@parameters.nil? || @parameters[:max_reconnect_attempts].nil?) && @parameters[:max_reconnect_attempts] != 0 && @connection_attempts > @parameters[:max_reconnect_attempts]
|
235
173
|
end
|
@@ -303,16 +241,56 @@ module Stomp
|
|
303
241
|
|
304
242
|
# Send message to destination
|
305
243
|
#
|
244
|
+
# To disable content length header ( :suppress_content_length => true )
|
306
245
|
# Accepts a transaction header ( :transaction => 'some_transaction_id' )
|
307
246
|
def send(destination, message, headers = {})
|
308
247
|
headers[:destination] = destination
|
309
248
|
transmit("SEND", headers, message)
|
310
249
|
end
|
250
|
+
|
251
|
+
# Send a message back to the source or to the dead letter queue
|
252
|
+
#
|
253
|
+
# Accepts a dead letter queue option ( :dead_letter_queue => "/queue/DLQ" )
|
254
|
+
# Accepts a limit number of redeliveries option ( :max_redeliveries => 6 )
|
255
|
+
def unreceive(message, options = {})
|
256
|
+
options = { :dead_letter_queue => "/queue/DLQ", :max_redeliveries => 6 }.merge options
|
257
|
+
# Lets make sure all keys are symbols
|
258
|
+
message.headers = message.headers.symbolize_keys
|
259
|
+
|
260
|
+
retry_count = message.headers[:retry_count].to_i || 0
|
261
|
+
message.headers[:retry_count] = retry_count + 1
|
262
|
+
transaction_id = "transaction-#{message.headers[:'message-id']}-#{retry_count}"
|
263
|
+
|
264
|
+
begin
|
265
|
+
self.begin transaction_id
|
266
|
+
|
267
|
+
if client_ack?(message)
|
268
|
+
self.ack(message.headers[:'message-id'], :transaction => transaction_id)
|
269
|
+
end
|
270
|
+
|
271
|
+
if retry_count <= options[:max_redeliveries]
|
272
|
+
self.send(message.headers[:destination], message.body, message.headers.merge(:transaction => transaction_id))
|
273
|
+
else
|
274
|
+
# Poison ack, sending the message to the DLQ
|
275
|
+
self.send(options[:dead_letter_queue], message.body, message.headers.merge(:transaction => transaction_id, :persistent => true))
|
276
|
+
end
|
277
|
+
self.commit transaction_id
|
278
|
+
rescue Exception => exception
|
279
|
+
self.abort transaction_id
|
280
|
+
raise exception
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def client_ack?(message)
|
285
|
+
headers = @subscriptions[message.headers[:destination]]
|
286
|
+
!headers.nil? && headers[:ack] == "client"
|
287
|
+
end
|
311
288
|
|
312
289
|
# Close this connection
|
313
290
|
def disconnect(headers = {})
|
314
291
|
transmit("DISCONNECT", headers)
|
315
|
-
|
292
|
+
headers = headers.symbolize_keys
|
293
|
+
@disconnect_receipt = receive if headers[:receipt]
|
316
294
|
close_socket
|
317
295
|
end
|
318
296
|
|
@@ -321,7 +299,7 @@ module Stomp
|
|
321
299
|
def poll
|
322
300
|
@read_semaphore.synchronize do
|
323
301
|
return nil if @socket.nil? || !@socket.ready?
|
324
|
-
|
302
|
+
receive
|
325
303
|
end
|
326
304
|
end
|
327
305
|
|
@@ -330,93 +308,172 @@ module Stomp
|
|
330
308
|
# The recive my fail so we may need to retry.
|
331
309
|
while TRUE
|
332
310
|
begin
|
333
|
-
|
334
|
-
return _receive(
|
311
|
+
used_socket = socket
|
312
|
+
return _receive(used_socket)
|
335
313
|
rescue
|
336
|
-
@failure =
|
314
|
+
@failure = $!
|
337
315
|
raise unless @reliable
|
338
|
-
$stderr.print "receive failed: "
|
316
|
+
$stderr.print "receive failed: #{$!}"
|
339
317
|
end
|
340
318
|
end
|
341
319
|
end
|
342
320
|
|
343
321
|
def receive
|
344
|
-
super_result = __old_receive
|
322
|
+
super_result = __old_receive
|
345
323
|
if super_result.nil? && @reliable
|
346
324
|
$stderr.print "connection.receive returning EOF as nil - resetting connection.\n"
|
347
325
|
@socket = nil
|
348
|
-
super_result = __old_receive
|
326
|
+
super_result = __old_receive
|
349
327
|
end
|
350
328
|
return super_result
|
351
329
|
end
|
352
330
|
|
353
331
|
private
|
354
332
|
|
355
|
-
def _receive(
|
356
|
-
line = ' '
|
333
|
+
def _receive( read_socket )
|
357
334
|
@read_semaphore.synchronize do
|
358
|
-
line =
|
335
|
+
line = read_socket.gets
|
359
336
|
return nil if line.nil?
|
360
337
|
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
end
|
338
|
+
# If the reading hangs for more than 5 seconds, abort the parsing process
|
339
|
+
Timeout::timeout(5, Stomp::Error::PacketParsingTimeout) do
|
340
|
+
# Reads the beginning of the message until it runs into a empty line
|
341
|
+
message_header = ''
|
342
|
+
begin
|
343
|
+
message_header += line
|
344
|
+
line = read_socket.gets
|
345
|
+
end until line =~ /^\s?\n$/
|
346
|
+
|
347
|
+
# Checks if it includes content_length header
|
348
|
+
content_length = message_header.match /content-length\s?:\s?(\d+)\s?\n/
|
349
|
+
message_body = ''
|
369
350
|
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
351
|
+
# If it does, reads the specified amount of bytes
|
352
|
+
char = ''
|
353
|
+
if content_length
|
354
|
+
message_body = read_socket.read content_length[1].to_i
|
355
|
+
raise Stomp::Error::InvalidMessageLength unless parse_char(read_socket.getc) == "\0"
|
356
|
+
# Else reads, the rest of the message until the first \0
|
374
357
|
else
|
375
|
-
|
376
|
-
if RUBY_VERSION > '1.9'
|
377
|
-
until (c = s.getc.ord) == 0
|
378
|
-
m.body << c.chr
|
379
|
-
end
|
380
|
-
else
|
381
|
-
until (c = s.getc) == 0
|
382
|
-
m.body << c.chr
|
383
|
-
end
|
384
|
-
end
|
358
|
+
message_body += char while read_socket.ready? && (char = parse_char(read_socket.getc)) != "\0"
|
385
359
|
end
|
386
|
-
#c = s.getc
|
387
|
-
#raise "Invalid frame termination received" unless c == 10
|
388
|
-
end # message
|
389
|
-
return message
|
390
360
|
|
361
|
+
# If the buffer isn't empty, reads the next char and returns it to the buffer
|
362
|
+
# unless it's a \n
|
363
|
+
if read_socket.ready?
|
364
|
+
last_char = read_socket.getc
|
365
|
+
read_socket.ungetc(last_char) if parse_char(last_char) != "\n"
|
366
|
+
end
|
367
|
+
|
368
|
+
# Adds the excluded \n and \0 and tries to create a new message with it
|
369
|
+
Message.new(message_header + "\n" + message_body + "\0")
|
370
|
+
end
|
391
371
|
end
|
392
372
|
end
|
393
373
|
|
374
|
+
def parse_char(char)
|
375
|
+
RUBY_VERSION > '1.9' ? char : char.chr
|
376
|
+
end
|
377
|
+
|
394
378
|
def transmit(command, headers = {}, body = '')
|
395
379
|
# The transmit may fail so we may need to retry.
|
396
380
|
while TRUE
|
397
381
|
begin
|
398
|
-
|
399
|
-
_transmit(
|
382
|
+
used_socket = socket
|
383
|
+
_transmit(used_socket, command, headers, body)
|
400
384
|
return
|
401
385
|
rescue
|
402
|
-
@failure =
|
386
|
+
@failure = $!
|
403
387
|
raise unless @reliable
|
404
|
-
$stderr.print "transmit to #{@host} failed:
|
388
|
+
$stderr.print "transmit to #{@host} failed: #{$!}\n"
|
405
389
|
end
|
406
390
|
end
|
407
391
|
end
|
408
392
|
|
409
|
-
def _transmit(
|
393
|
+
def _transmit(used_socket, command, headers = {}, body = '')
|
410
394
|
@transmit_semaphore.synchronize do
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
395
|
+
# ActiveMQ interprets every message as a BinaryMessage
|
396
|
+
# if content_length header is included.
|
397
|
+
# Using :suppress_content_length => true will suppress this behaviour
|
398
|
+
# and ActiveMQ will interpret the message as a TextMessage.
|
399
|
+
# For more information refer to http://juretta.com/log/2009/05/24/activemq-jms-stomp/
|
400
|
+
suppress_content_length = headers.delete :suppress_content_length
|
401
|
+
headers['content-length'] = "#{body.length}" unless suppress_content_length
|
402
|
+
|
403
|
+
used_socket.puts command
|
404
|
+
headers.each {|k,v| used_socket.puts "#{k}:#{v}" }
|
405
|
+
used_socket.puts "content-type: text/plain; charset=UTF-8"
|
406
|
+
used_socket.puts
|
407
|
+
used_socket.write body
|
408
|
+
used_socket.write "\0"
|
418
409
|
end
|
419
410
|
end
|
411
|
+
|
412
|
+
def open_tcp_socket
|
413
|
+
tcp_socket = TCPSocket.open @host, @port
|
414
|
+
def tcp_socket.ready?
|
415
|
+
r,w,e = IO.select([self],nil,nil,0)
|
416
|
+
! r.nil?
|
417
|
+
end
|
418
|
+
|
419
|
+
tcp_socket
|
420
|
+
end
|
421
|
+
|
422
|
+
def open_ssl_socket
|
423
|
+
require 'openssl' unless defined?(OpenSSL)
|
424
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
425
|
+
|
426
|
+
# For client certificate authentication:
|
427
|
+
# key_path = ENV["STOMP_KEY_PATH"] || "~/stomp_keys"
|
428
|
+
# ctx.cert = OpenSSL::X509::Certificate.new("#{key_path}/client.cer")
|
429
|
+
# ctx.key = OpenSSL::PKey::RSA.new("#{key_path}/client.keystore")
|
430
|
+
|
431
|
+
# For server certificate authentication:
|
432
|
+
# truststores = OpenSSL::X509::Store.new
|
433
|
+
# truststores.add_file("#{key_path}/client.ts")
|
434
|
+
# ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
435
|
+
# ctx.cert_store = truststores
|
436
|
+
|
437
|
+
ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
438
|
+
|
439
|
+
ssl = OpenSSL::SSL::SSLSocket.new(open_tcp_socket, ctx)
|
440
|
+
def ssl.ready?
|
441
|
+
! @rbuffer.empty? || @io.ready?
|
442
|
+
end
|
443
|
+
ssl.connect
|
444
|
+
ssl
|
445
|
+
end
|
446
|
+
|
447
|
+
def close_socket
|
448
|
+
begin
|
449
|
+
@socket.close
|
450
|
+
rescue
|
451
|
+
#Ignoring if already closed
|
452
|
+
end
|
453
|
+
|
454
|
+
@closed = true
|
455
|
+
end
|
456
|
+
|
457
|
+
def open_socket
|
458
|
+
used_socket = @ssl ? open_ssl_socket : open_tcp_socket
|
459
|
+
# try to close the old connection if any
|
460
|
+
close_socket
|
461
|
+
|
462
|
+
@closed = false
|
463
|
+
|
464
|
+
used_socket
|
465
|
+
end
|
466
|
+
|
467
|
+
def connect(used_socket)
|
468
|
+
headers = @connect_headers.clone
|
469
|
+
headers[:login] = @login
|
470
|
+
headers[:passcode] = @passcode
|
471
|
+
_transmit(used_socket, "CONNECT", headers)
|
472
|
+
@connection_frame = _receive(used_socket)
|
473
|
+
@disconnect_receipt = nil
|
474
|
+
# replay any subscriptions.
|
475
|
+
@subscriptions.each { |k,v| _transmit(used_socket, "SUBSCRIBE", v) }
|
476
|
+
end
|
420
477
|
|
421
478
|
end
|
422
479
|
|
data/lib/stomp/errors.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Stomp
|
2
|
+
module Error
|
3
|
+
class InvalidFormat < RuntimeError
|
4
|
+
def message
|
5
|
+
"Invalid message - invalid format"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class InvalidMessageLength < RuntimeError
|
10
|
+
def message
|
11
|
+
"Invalid content length received"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class PacketParsingTimeout < RuntimeError
|
16
|
+
def message
|
17
|
+
"Packet parsing timeout"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class MaxReconnectAttempts < RuntimeError
|
22
|
+
def message
|
23
|
+
"Maximum number of reconnection attempts reached"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class ::Hash
|
2
|
+
def uncamelize_and_symbolize_keys
|
3
|
+
self.uncamelize_and_stringify_keys.symbolize_keys
|
4
|
+
end
|
5
|
+
|
6
|
+
def uncamelize_and_stringify_keys
|
7
|
+
uncamelized = {}
|
8
|
+
self.each_pair do |key, value|
|
9
|
+
new_key = key.to_s.split(/(?=[A-Z])/).join('_').downcase
|
10
|
+
uncamelized[new_key] = value
|
11
|
+
end
|
12
|
+
|
13
|
+
uncamelized
|
14
|
+
end
|
15
|
+
|
16
|
+
def symbolize_keys
|
17
|
+
symbolized = {}
|
18
|
+
self.each_pair do |key, value|
|
19
|
+
symbolized[key.to_sym] = value
|
20
|
+
end
|
21
|
+
|
22
|
+
symbolized
|
23
|
+
end unless self.respond_to? :symbolize_keys
|
24
|
+
end
|
data/lib/stomp/message.rb
CHANGED
@@ -2,16 +2,48 @@ module Stomp
|
|
2
2
|
|
3
3
|
# Container class for frames, misnamed technically
|
4
4
|
class Message
|
5
|
-
attr_accessor :headers, :body, :
|
5
|
+
attr_accessor :command, :headers, :body, :original
|
6
6
|
|
7
|
-
def initialize
|
8
|
-
|
7
|
+
def initialize(message)
|
8
|
+
# Set default empty values
|
9
|
+
self.command = ''
|
10
|
+
self.headers = {}
|
11
|
+
self.body = ''
|
12
|
+
self.original = message
|
13
|
+
return self if is_blank?(message)
|
14
|
+
|
15
|
+
# Parse the format of the received stomp message
|
16
|
+
parse = message.match /^(CONNECTED|MESSAGE|RECEIPT|ERROR)\n(.*?)\n\n(.*)\0\n?$/m
|
17
|
+
raise Stomp::Error::InvalidFormat if parse.nil?
|
18
|
+
|
19
|
+
# Set the message values
|
20
|
+
self.command = parse[1]
|
21
|
+
self.headers = {}
|
22
|
+
parse[2].split("\n").map do |value|
|
23
|
+
parsed_value = value.match /^([\w|-]*):(.*)$/
|
24
|
+
self.headers[parsed_value[1].strip] = parsed_value[2].strip if parsed_value
|
25
|
+
end
|
26
|
+
|
27
|
+
body_length = -1
|
28
|
+
if self.headers['content-length']
|
29
|
+
body_length = self.headers['content-length'].to_i
|
30
|
+
raise Stomp::Error::InvalidMessageLength if parse[3].length != body_length
|
31
|
+
end
|
32
|
+
self.body = parse[3][0..body_length]
|
9
33
|
end
|
10
34
|
|
11
35
|
def to_s
|
12
36
|
"<Stomp::Message headers=#{headers.inspect} body='#{body}' command='#{command}' >"
|
13
37
|
end
|
38
|
+
|
39
|
+
def empty?
|
40
|
+
is_blank?(command) && is_blank?(headers) && is_blank?(body)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def is_blank?(value)
|
45
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
46
|
+
end
|
14
47
|
end
|
15
48
|
|
16
49
|
end
|
17
|
-
|
data/test/test_client.rb
CHANGED
@@ -1,27 +1,28 @@
|
|
1
1
|
require File.join(File.dirname(__FILE__), 'test_helper.rb')
|
2
2
|
|
3
3
|
class TestClient < Test::Unit::TestCase
|
4
|
-
|
4
|
+
include TestBase
|
5
|
+
|
5
6
|
def setup
|
6
|
-
@client = Stomp::Client.new(
|
7
|
+
@client = Stomp::Client.new(user, passcode, host, port)
|
7
8
|
end
|
8
9
|
|
9
10
|
def teardown
|
10
|
-
@client.close
|
11
|
+
@client.close if @client # allow tests to close
|
11
12
|
end
|
12
13
|
|
13
|
-
def
|
14
|
-
|
15
|
-
end
|
14
|
+
def test_ack_api_works
|
15
|
+
@client.send destination, message_text, {:suppress_content_length => true}
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
received = nil
|
18
|
+
@client.subscribe(destination, {:ack => 'client'}) {|msg| received = msg}
|
19
|
+
sleep 0.01 until received
|
20
|
+
assert_equal message_text, received.body
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
receipt = nil
|
23
|
+
@client.acknowledge(received) {|r| receipt = r}
|
24
|
+
sleep 0.01 until receipt
|
25
|
+
assert_not_nil receipt.headers['receipt-id']
|
25
26
|
end
|
26
27
|
|
27
28
|
def test_asynch_subscribe
|
@@ -33,20 +34,6 @@ class TestClient < Test::Unit::TestCase
|
|
33
34
|
assert_equal message_text, received.body
|
34
35
|
end
|
35
36
|
|
36
|
-
def test_ack_api_works
|
37
|
-
@client.send destination, message_text
|
38
|
-
|
39
|
-
received = nil
|
40
|
-
@client.subscribe(destination, :ack => 'client') {|msg| received = msg}
|
41
|
-
sleep 0.01 until received
|
42
|
-
assert_equal message_text, received.body
|
43
|
-
|
44
|
-
receipt = nil
|
45
|
-
@client.acknowledge(received) {|r| receipt = r}
|
46
|
-
sleep 0.01 until receipt
|
47
|
-
assert_not_nil receipt.headers['receipt-id']
|
48
|
-
end
|
49
|
-
|
50
37
|
# BROKEN
|
51
38
|
def test_noack
|
52
39
|
@client.send destination, message_text
|
@@ -59,7 +46,7 @@ class TestClient < Test::Unit::TestCase
|
|
59
46
|
|
60
47
|
# was never acked so should be resent to next client
|
61
48
|
|
62
|
-
@client = Stomp::Client.new(
|
49
|
+
@client = Stomp::Client.new(user, passcode, host, port)
|
63
50
|
received = nil
|
64
51
|
@client.subscribe(destination) {|msg| received = msg}
|
65
52
|
sleep 0.01 until received
|
@@ -78,6 +65,16 @@ class TestClient < Test::Unit::TestCase
|
|
78
65
|
assert_equal message_text, message.body
|
79
66
|
end
|
80
67
|
|
68
|
+
def test_disconnect_receipt
|
69
|
+
@client.close :receipt => "xyz789"
|
70
|
+
assert_nothing_raised {
|
71
|
+
assert_not_nil(@client.disconnect_receipt, "should have a receipt")
|
72
|
+
assert_equal(@client.disconnect_receipt.headers['receipt-id'],
|
73
|
+
"xyz789", "receipt sent and received should match")
|
74
|
+
}
|
75
|
+
@client = nil
|
76
|
+
end
|
77
|
+
|
81
78
|
def test_send_then_sub
|
82
79
|
@client.send destination, message_text
|
83
80
|
message = nil
|
@@ -87,6 +84,12 @@ class TestClient < Test::Unit::TestCase
|
|
87
84
|
assert_equal message_text, message.body
|
88
85
|
end
|
89
86
|
|
87
|
+
def test_subscribe_requires_block
|
88
|
+
assert_raise(RuntimeError) do
|
89
|
+
@client.subscribe destination
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
90
93
|
def test_transactional_send
|
91
94
|
@client.begin 'tx1'
|
92
95
|
@client.send destination, message_text, :transaction => 'tx1'
|
@@ -142,18 +145,6 @@ class TestClient < Test::Unit::TestCase
|
|
142
145
|
@client.commit 'tx2'
|
143
146
|
end
|
144
147
|
|
145
|
-
def test_unsubscribe
|
146
|
-
message = nil
|
147
|
-
client = Stomp::Client.new("test", "user", "localhost", 61613, true)
|
148
|
-
client.subscribe(destination, :ack => 'client') { |m| message = m }
|
149
|
-
@client.send destination, message_text
|
150
|
-
Timeout::timeout(4) do
|
151
|
-
sleep 0.01 until message
|
152
|
-
end
|
153
|
-
client.unsubscribe destination # was throwing exception on unsub at one point
|
154
|
-
|
155
|
-
end
|
156
|
-
|
157
148
|
def test_transaction_with_client_side_redelivery
|
158
149
|
@client.send destination, message_text
|
159
150
|
|
@@ -177,6 +168,29 @@ class TestClient < Test::Unit::TestCase
|
|
177
168
|
@client.acknowledge message, :transaction => 'tx2'
|
178
169
|
@client.commit 'tx2'
|
179
170
|
end
|
171
|
+
|
172
|
+
def test_connection_frame
|
173
|
+
assert_not_nil @client.connection_frame
|
174
|
+
end
|
180
175
|
|
176
|
+
def test_unsubscribe
|
177
|
+
message = nil
|
178
|
+
client = Stomp::Client.new(user, passcode, host, port, true)
|
179
|
+
client.subscribe(destination, :ack => 'client') { |m| message = m }
|
180
|
+
@client.send destination, message_text
|
181
|
+
Timeout::timeout(4) do
|
182
|
+
sleep 0.01 until message
|
183
|
+
end
|
184
|
+
client.unsubscribe destination # was throwing exception on unsub at one point
|
185
|
+
|
186
|
+
end
|
181
187
|
|
188
|
+
private
|
189
|
+
def message_text
|
190
|
+
"test_client#" + name
|
191
|
+
end
|
192
|
+
|
193
|
+
def destination
|
194
|
+
"/queue/test/ruby/client/" + name
|
195
|
+
end
|
182
196
|
end
|
data/test/test_connection.rb
CHANGED
@@ -1,38 +1,14 @@
|
|
1
1
|
require File.join(File.dirname(__FILE__), 'test_helper.rb')
|
2
2
|
|
3
3
|
class TestStomp < Test::Unit::TestCase
|
4
|
-
|
4
|
+
include TestBase
|
5
|
+
|
5
6
|
def setup
|
6
|
-
@conn = Stomp::Connection.open(
|
7
|
+
@conn = Stomp::Connection.open(user, passcode, host, port)
|
7
8
|
end
|
8
9
|
|
9
10
|
def teardown
|
10
|
-
@conn.disconnect
|
11
|
-
end
|
12
|
-
|
13
|
-
def make_destination
|
14
|
-
"/queue/test/ruby/stomp/" + name()
|
15
|
-
end
|
16
|
-
|
17
|
-
def _test_transaction
|
18
|
-
@conn.subscribe make_destination
|
19
|
-
|
20
|
-
# Drain the destination.
|
21
|
-
sleep 0.01 while
|
22
|
-
sleep 0.01 while @conn.poll!=nil
|
23
|
-
|
24
|
-
@conn.begin "tx1"
|
25
|
-
@conn.send make_destination, "txn message", 'transaction' => "tx1"
|
26
|
-
|
27
|
-
@conn.send make_destination, "first message"
|
28
|
-
|
29
|
-
sleep 0.01
|
30
|
-
msg = @conn.receive
|
31
|
-
assert_equal "first message", msg.body
|
32
|
-
|
33
|
-
@conn.commit "tx1"
|
34
|
-
msg = @conn.receive
|
35
|
-
assert_equal "txn message", msg.body
|
11
|
+
@conn.disconnect if @conn # allow tests to disconnect
|
36
12
|
end
|
37
13
|
|
38
14
|
def test_connection_exists
|
@@ -52,6 +28,16 @@ class TestStomp < Test::Unit::TestCase
|
|
52
28
|
assert_equal "abc", msg.headers['receipt-id']
|
53
29
|
end
|
54
30
|
|
31
|
+
def test_disconnect_receipt
|
32
|
+
@conn.disconnect :receipt => "abc123"
|
33
|
+
assert_nothing_raised {
|
34
|
+
assert_not_nil(@conn.disconnect_receipt, "should have a receipt")
|
35
|
+
assert_equal(@conn.disconnect_receipt.headers['receipt-id'],
|
36
|
+
"abc123", "receipt sent and received should match")
|
37
|
+
}
|
38
|
+
@conn = nil
|
39
|
+
end
|
40
|
+
|
55
41
|
def test_client_ack_with_symbol
|
56
42
|
@conn.subscribe make_destination, :ack => :client
|
57
43
|
@conn.send make_destination, "test_stomp#test_client_ack_with_symbol"
|
@@ -91,5 +77,57 @@ class TestStomp < Test::Unit::TestCase
|
|
91
77
|
msg = @conn.receive
|
92
78
|
assert_match /^<Stomp::Message headers=/ , msg.to_s
|
93
79
|
end
|
80
|
+
|
81
|
+
def test_connection_frame
|
82
|
+
assert_not_nil @conn.connection_frame
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_messages_with_multipleLine_ends
|
86
|
+
@conn.subscribe make_destination
|
87
|
+
@conn.send make_destination, "a\n\n"
|
88
|
+
@conn.send make_destination, "b\n\na\n\n"
|
89
|
+
|
90
|
+
msg_a = @conn.receive
|
91
|
+
msg_b = @conn.receive
|
92
|
+
|
93
|
+
assert_equal "a\n\n", msg_a.body
|
94
|
+
assert_equal "b\n\na\n\n", msg_b.body
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_send_two_messages
|
98
|
+
@conn.subscribe make_destination
|
99
|
+
@conn.send make_destination, "a\0"
|
100
|
+
@conn.send make_destination, "b\0"
|
101
|
+
msg_a = @conn.receive
|
102
|
+
msg_b = @conn.receive
|
103
|
+
|
104
|
+
assert_equal "a\0", msg_a.body
|
105
|
+
assert_equal "b\0", msg_b.body
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
def make_destination
|
110
|
+
"/queue/test/ruby/stomp/" + name
|
111
|
+
end
|
112
|
+
|
113
|
+
def _test_transaction
|
114
|
+
@conn.subscribe make_destination
|
115
|
+
|
116
|
+
# Drain the destination.
|
117
|
+
sleep 0.01 while
|
118
|
+
sleep 0.01 while @conn.poll!=nil
|
119
|
+
|
120
|
+
@conn.begin "tx1"
|
121
|
+
@conn.send make_destination, "txn message", 'transaction' => "tx1"
|
122
|
+
|
123
|
+
@conn.send make_destination, "first message"
|
124
|
+
|
125
|
+
sleep 0.01
|
126
|
+
msg = @conn.receive
|
127
|
+
assert_equal "first message", msg.body
|
94
128
|
|
129
|
+
@conn.commit "tx1"
|
130
|
+
msg = @conn.receive
|
131
|
+
assert_equal "txn message", msg.body
|
132
|
+
end
|
95
133
|
end
|
data/test/test_helper.rb
CHANGED
@@ -1,5 +1,24 @@
|
|
1
|
+
$:.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
|
2
|
+
|
1
3
|
require 'test/unit'
|
2
4
|
require 'timeout'
|
3
5
|
require 'stomp'
|
4
|
-
|
6
|
+
|
7
|
+
# Helper routines
|
8
|
+
module TestBase
|
9
|
+
def user
|
10
|
+
ENV['STOMP_USER'] || "test"
|
11
|
+
end
|
12
|
+
def passcode
|
13
|
+
ENV['STOMP_PASSCODE'] || "user"
|
14
|
+
end
|
15
|
+
# Get host
|
16
|
+
def host
|
17
|
+
ENV['STOMP_HOST'] || "localhost"
|
18
|
+
end
|
19
|
+
# Get port
|
20
|
+
def port
|
21
|
+
(ENV['STOMP_PORT'] || 61613).to_i
|
22
|
+
end
|
23
|
+
end
|
5
24
|
|
metadata
CHANGED
@@ -1,16 +1,17 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stomp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brian McCallister
|
8
8
|
- Marius Mathiesen
|
9
|
+
- Thiago Morello
|
9
10
|
autorequire:
|
10
11
|
bindir: bin
|
11
12
|
cert_chain: []
|
12
13
|
|
13
|
-
date:
|
14
|
+
date: 2010-02-09 00:00:00 -02:00
|
14
15
|
default_executable:
|
15
16
|
dependencies: []
|
16
17
|
|
@@ -18,6 +19,7 @@ description: Ruby client for the Stomp messaging protocol
|
|
18
19
|
email:
|
19
20
|
- brianm@apache.org
|
20
21
|
- marius@stones.com
|
22
|
+
- morellon@gmail.com
|
21
23
|
executables:
|
22
24
|
- catstomp
|
23
25
|
- stompcat
|
@@ -36,6 +38,8 @@ files:
|
|
36
38
|
- lib/stomp/client.rb
|
37
39
|
- lib/stomp/connection.rb
|
38
40
|
- lib/stomp/message.rb
|
41
|
+
- lib/stomp/errors.rb
|
42
|
+
- lib/stomp/ext/hash.rb
|
39
43
|
- test/test_client.rb
|
40
44
|
- test/test_connection.rb
|
41
45
|
- test/test_helper.rb
|