stomp 1.1.3 → 1.1.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|