Class | Jabber::Stream |
In: |
lib/xmpp4r/stream.rb
|
Parent: | Object |
The stream class manages a connection stream (a file descriptor using which XML messages are read and sent)
You may register callbacks for the three Jabber stanzas (message, presence and iq) and use the send and send_with_id methods.
To ensure the order of received stanzas, callback blocks are launched in the parser thread. If further blocking operations are intended in those callbacks, run your own thread there.
DISCONNECTED | = | 1 |
CONNECTED | = | 2 |
fd | [R] | file descriptor used |
status | [R] | connection status |
Initialize a new stream
# File lib/xmpp4r/stream.rb, line 40 40: def initialize 41: @fd = nil 42: @status = DISCONNECTED 43: @xmlcbs = CallbackList.new 44: @stanzacbs = CallbackList.new 45: @messagecbs = CallbackList.new 46: @iqcbs = CallbackList.new 47: @presencecbs = CallbackList.new 48: @send_lock = Mutex.new 49: @last_send = Time.now 50: @exception_block = nil 51: @threadblocks = [] 52: @wakeup_thread = nil 53: @streamid = nil 54: @streamns = 'jabber:client' 55: @features_sem = Semaphore.new 56: @parser_thread = nil 57: end
Adds a callback block to process received Iqs
priority: | [Integer] The callback‘s priority, the higher, the sooner |
ref: | [String] The callback‘s reference |
&block: | [Block] The optional block |
# File lib/xmpp4r/stream.rb, line 473 473: def add_iq_callback(priority = 0, ref = nil, &block) 474: @iqcbs.add(priority, ref, block) 475: end
Adds a callback block to process received Messages
priority: | [Integer] The callback‘s priority, the higher, the sooner |
ref: | [String] The callback‘s reference |
&block: | [Block] The optional block |
# File lib/xmpp4r/stream.rb, line 419 419: def add_message_callback(priority = 0, ref = nil, &block) 420: @messagecbs.add(priority, ref, block) 421: end
Adds a callback block to process received Presences
priority: | [Integer] The callback‘s priority, the higher, the sooner |
ref: | [String] The callback‘s reference |
&block: | [Block] The optional block |
# File lib/xmpp4r/stream.rb, line 455 455: def add_presence_callback(priority = 0, ref = nil, &block) 456: @presencecbs.add(priority, ref, block) 457: end
Adds a callback block to process received Stanzas
priority: | [Integer] The callback‘s priority, the higher, the sooner |
ref: | [String] The callback‘s reference |
&block: | [Block] The optional block |
# File lib/xmpp4r/stream.rb, line 437 437: def add_stanza_callback(priority = 0, ref = nil, &block) 438: @stanzacbs.add(priority, ref, block) 439: end
Adds a callback block to process received XML messages
priority: | [Integer] The callback‘s priority, the higher, the sooner |
ref: | [String] The callback‘s reference |
&block: | [Block] The optional block |
# File lib/xmpp4r/stream.rb, line 401 401: def add_xml_callback(priority = 0, ref = nil, &block) 402: @xmlcbs.add(priority, ref, block) 403: end
# File lib/xmpp4r/stream.rb, line 491 491: def close! 492: @parser_thread.kill if @parser_thread 493: @fd.close if @fd and !@fd.closed? 494: @status = DISCONNECTED 495: end
Delete a Stanza callback
ref: | [String] The reference of the callback to delete |
# File lib/xmpp4r/stream.rb, line 445 445: def delete_stanza_callback(ref) 446: @stanzacbs.delete(ref) 447: end
Delete an XML-messages callback
ref: | [String] The reference of the callback to delete |
# File lib/xmpp4r/stream.rb, line 409 409: def delete_xml_callback(ref) 410: @xmlcbs.delete(ref) 411: end
Returns if this connection is connected to a Jabber service
return: | [Boolean] Connection status |
# File lib/xmpp4r/stream.rb, line 155 155: def is_connected? 156: return @status == CONNECTED 157: end
Returns if this connection is NOT connected to a Jabber service
return: | [Boolean] Connection status |
# File lib/xmpp4r/stream.rb, line 163 163: def is_disconnected? 164: return @status == DISCONNECTED 165: end
Mounts a block to handle exceptions if they occur during the poll send. This will likely be the first indication that the socket dropped in a Jabber Session.
The block has to take three arguments:
# File lib/xmpp4r/stream.rb, line 112 112: def on_exception(&block) 113: @exception_block = block 114: end
This method is called by the parser when a failure occurs
# File lib/xmpp4r/stream.rb, line 118 118: def parse_failure(e) 119: Jabber::debuglog("EXCEPTION:\n#{e.class}\n#{e.message}\n#{e.backtrace.join("\n")}") 120: 121: # A new thread has to be created because close will cause the thread 122: # to commit suicide(???) 123: if @exception_block 124: # New thread, because close will kill the current thread 125: Thread.new do 126: Thread.current.abort_on_exception = true 127: close 128: @exception_block.call(e, self, :parser) 129: end 130: else 131: Jabber::debuglog "Stream#parse_failure was called by XML parser. Dumping " + 132: "backtrace...\n" + e.exception + "\n#{e.backtrace.join("\n")}" 133: close 134: raise 135: end 136: end
This method is called by the parser upon receiving </stream:stream>
# File lib/xmpp4r/stream.rb, line 140 140: def parser_end 141: if @exception_block 142: Thread.new do 143: Thread.current.abort_on_exception = true 144: close 145: @exception_block.call(nil, self, :close) 146: end 147: else 148: close 149: end 150: end
Processes a received REXML::Element and executes registered thread blocks and filters against it.
element: | [REXML::Element] The received element |
# File lib/xmpp4r/stream.rb, line 172 172: def receive(element) 173: Jabber::debuglog("RECEIVED:\n#{element.to_s}") 174: 175: if element.namespace('').to_s == '' # REXML namespaces are always strings 176: element.add_namespace(@streamns) 177: end 178: 179: case element.prefix 180: when 'stream' 181: case element.name 182: when 'stream' 183: stanza = element 184: @streamid = element.attributes['id'] 185: @streamns = element.namespace('') if element.namespace('') 186: 187: # Hack: component streams are basically client streams. 188: # Someday we may want to create special stanza classes 189: # for components/s2s deriving from normal stanzas but 190: # posessing these namespaces 191: @streamns = 'jabber:client' if @streamns == 'jabber:component:accept' 192: 193: unless element.attributes['version'] # isn't XMPP compliant, so 194: Jabber::debuglog("FEATURES: server not XMPP compliant, will not wait for features") 195: @features_sem.run # don't wait for <stream:features/> 196: end 197: when 'features' 198: stanza = element 199: element.each { |e| 200: if e.name == 'mechanisms' and e.namespace == 'urn:ietf:params:xml:ns:xmpp-sasl' 201: e.each_element('mechanism') { |mech| 202: @stream_mechanisms.push(mech.text) 203: } 204: else 205: @stream_features[e.name] = e.namespace 206: end 207: } 208: Jabber::debuglog("FEATURES: received") 209: @features_sem.run 210: else 211: stanza = element 212: end 213: else 214: # Any stanza, classes are registered by XMPPElement::name_xmlns 215: begin 216: stanza = XMPPStanza::import(element) 217: rescue NoNameXmlnsRegistered 218: stanza = element 219: end 220: end 221: 222: # Iterate through blocked threads (= waiting for an answer) 223: # 224: # We're dup'ping the @threadblocks here, so that we won't end up in an 225: # endless loop if Stream#send is being nested. That means, the nested 226: # threadblock won't receive the stanza currently processed, but the next 227: # one. 228: threadblocks = @threadblocks.dup 229: threadblocks.each { |threadblock| 230: exception = nil 231: r = false 232: begin 233: r = threadblock.call(stanza) 234: rescue Exception => e 235: exception = e 236: end 237: 238: if r == true 239: @threadblocks.delete(threadblock) 240: threadblock.wakeup 241: return 242: elsif exception 243: @threadblocks.delete(threadblock) 244: threadblock.raise(exception) 245: end 246: } 247: 248: Jabber::debuglog("PROCESSING:\n#{stanza.to_s} (#{stanza.class})") 249: return true if @xmlcbs.process(stanza) 250: return true if @stanzacbs.process(stanza) 251: case stanza 252: when Message 253: return true if @messagecbs.process(stanza) 254: when Iq 255: return true if @iqcbs.process(stanza) 256: when Presence 257: return true if @presencecbs.process(stanza) 258: end 259: end
Sends XML data to the socket and (optionally) waits to process received data.
Do not invoke this in a callback but in a seperate thread because we may not suspend the parser-thread (in whose context callbacks are executed).
xml: | [String] The xml data to send |
&block: | [Block] The optional block |
# File lib/xmpp4r/stream.rb, line 306 306: def send(xml, &block) 307: Jabber::debuglog("SENDING:\n#{xml}") 308: @threadblocks.unshift(threadblock = ThreadBlock.new(block)) if block 309: begin 310: # Temporarily remove stanza's namespace to 311: # reduce bandwidth consumption 312: if xml.kind_of? XMPPStanza and xml.namespace == 'jabber:client' 313: xml.delete_namespace 314: send_data(xml.to_s) 315: xml.add_namespace(@streamns) 316: else 317: send_data(xml.to_s) 318: end 319: rescue Exception => e 320: Jabber::debuglog("EXCEPTION:\n#{e.class}\n#{e.message}\n#{e.backtrace.join("\n")}") 321: 322: if @exception_block 323: Thread.new do 324: Thread.current.abort_on_exception = true 325: close! 326: @exception_block.call(e, self, :sending) 327: end 328: else 329: Jabber::debuglog "Exception caught while sending! (#{e.class})\n#{e.backtrace.join("\n")}" 330: close! 331: raise 332: end 333: end 334: # The parser thread might be running this (think of a callback running send()) 335: # If this is the case, we mustn't stop (or we would cause a deadlock) 336: if block and Thread.current != @parser_thread 337: threadblock.wait 338: elsif block 339: Jabber::debuglog("WARNING:\nCannot stop current thread in Jabber::Stream#send because it is the parser thread!") 340: end 341: end
# File lib/xmpp4r/stream.rb, line 288 288: def send_data(data) 289: @send_lock.synchronize do 290: @last_send = Time.now 291: @fd << data 292: @fd.flush 293: end 294: end
Send an XMMP stanza with an Jabber::XMPPStanza#id. The id will be generated by Jabber::IdGenerator if not already set.
The block will be called once: when receiving a stanza with the same Jabber::XMPPStanza#id. There is no need to return true to complete this! Instead the return value of the block will be returned. This is a direct result of unique request/response stanza identification via the id attribute.
The block may be omitted. Then, the result will be the response stanza.
Be aware that if a stanza with type=‘error‘ is received the function does not yield but raises an ServerError with the corresponding error element.
Please see Stream#send for some implementational details.
Please read the note about nesting at Stream#send
xml: | [XMPPStanza] |
# File lib/xmpp4r/stream.rb, line 364 364: def send_with_id(xml, &block) 365: if xml.id.nil? 366: xml.id = Jabber::IdGenerator.instance.generate_id 367: end 368: 369: res = nil 370: error = nil 371: send(xml) do |received| 372: if received.kind_of? XMPPStanza and received.id == xml.id 373: if received.type == :error 374: error = (received.error ? received.error : ErrorResponse.new) 375: true 376: elsif block_given? 377: res = yield(received) 378: true 379: else 380: res = received 381: true 382: end 383: else 384: false 385: end 386: end 387: 388: unless error.nil? 389: raise ServerError.new(error) 390: end 391: 392: res 393: end
Start the XML parser on the fd
# File lib/xmpp4r/stream.rb, line 61 61: def start(fd) 62: @stream_mechanisms = [] 63: @stream_features = {} 64: 65: @fd = fd 66: @parser = StreamParser.new(@fd, self) 67: @parser_thread = Thread.new do 68: Thread.current.abort_on_exception = true 69: begin 70: @parser.parse 71: Jabber::debuglog("DISCONNECTED\n") 72: 73: if @exception_block 74: Thread.new { close!; @exception_block.call(nil, self, :disconnected) } 75: else 76: close! 77: end 78: rescue Exception => e 79: Jabber::debuglog("EXCEPTION:\n#{e.class}\n#{e.message}\n#{e.backtrace.join("\n")}") 80: 81: if @exception_block 82: Thread.new do 83: Thread.current.abort_on_exception = true 84: close 85: @exception_block.call(e, self, :start) 86: end 87: else 88: Jabber::debuglog "Exception caught in Parser thread! (#{e.class})\n#{e.backtrace.join("\n")}" 89: close! 90: raise 91: end 92: end 93: end 94: 95: @status = CONNECTED 96: end