def test_immediate_failure(framework): exception = RuntimeError("it failed") try: raise exception except: f0 = txaio.create_future_error() fail = txaio.create_failure() errors = [] results = [] f1 = txaio.create_future_error(fail) def cb(x): results.append(x) def errback(f): errors.append(f) txaio.add_callbacks(f0, cb, errback) txaio.add_callbacks(f1, cb, errback) run_once() run_once() run_once() assert len(results) == 0 assert len(errors) == 2 assert isinstance(errors[0], txaio.IFailedFuture) assert isinstance(errors[1], txaio.IFailedFuture) assert errors[0].value == exception assert errors[1].value == exception # should be distinct FailedPromise instances assert id(errors[0]) != id(errors[1])
def test_errback_without_except(framework): ''' Create a failure without an except block ''' f = txaio.create_future() exception = RuntimeError("it failed") errors = [] def err(f): errors.append(f) txaio.add_callbacks(f, None, err) fail = txaio.create_failure(exception) txaio.reject(f, fail) run_once() assert len(errors) == 1 assert isinstance(errors[0], txaio.IFailedFuture) tb = txaio.failure_format_traceback(errors[0]) assert 'RuntimeError' in tb assert 'it failed' in tb assert txaio.failure_message(errors[0]) == 'RuntimeError: it failed' assert 'it failed' in str(errors[0])
def test_errback(framework): f = txaio.create_future() exception = RuntimeError("it failed") errors = [] def err(f): errors.append(f) txaio.add_callbacks(f, None, err) try: raise exception except: fail = txaio.create_failure() txaio.reject(f, fail) run_once() assert len(errors) == 1 assert isinstance(errors[0], txaio.IFailedFuture) assert exception == errors[0].value assert txaio.failure_traceback(errors[0]) is not None tb = txaio.failure_format_traceback(errors[0]) assert 'RuntimeError' in tb assert 'it failed' in tb assert txaio.failure_message(errors[0]) == 'RuntimeError: it failed' assert 'it failed' in str(errors[0])
def render_POST(self, request): """ A client sends a message via WAMP-over-Longpoll by HTTP/POSTing to this Web resource. The body of the POST should contain a batch of WAMP messages which are serialized according to the selected serializer, and delimited by a single ``\0`` byte in between two WAMP messages in the batch. """ payload = request.content.read() self.log.debug( "WampLongPoll: receiving data for transport '{tid}'\n{octets}", tid=self._parent._transport_id, octets=_LazyHexFormatter(payload), ) try: # process (batch of) WAMP message(s) self._parent.onMessage(payload, None) except Exception: f = create_failure() self.log.error( "Could not unserialize WAMP message: {msg}", msg=failure_message(f), ) self.log.debug("{tb}", tb=failure_format_traceback(f)) return self._parent._parent._fail_request( request, b"could not unserialize WAMP message.") else: request.setResponseCode(http.NO_CONTENT) self._parent._parent._set_standard_headers(request) self._parent._isalive = True return b""
def send(self, msg): """ Implements :func:`autobahn.wamp.interfaces.ITransport.send` """ if self.isOpen(): try: self.log.debug( "WampLongPoll: TX {octets}", octets=_LazyHexFormatter(msg), ) payload, isBinary = self._serializer.serialize(msg) except Exception as e: # all exceptions raised from above should be serialization errors .. f = create_failure() self.log.error( "Unable to serialize WAMP application payload: {msg}", msg=failure_message(f), ) self.log.debug("{tb}", tb=failure_format_traceback(f)) raise SerializationError( "unable to serialize WAMP application payload ({0})". format(e)) else: self._receive.queue(payload) else: raise TransportLost()
def test_loseConnection(self): """ If we lose our connection before openHandshakeTimeout fires, it is cleaned up """ # so, I guess a little cheezy, but we depend on the asyncio or # twisted class to call _connectionLost at some point; faking # that here self.protocol._connectionLost(txaio.create_failure(RuntimeError("testing"))) self.assertTrue(self.protocol.openHandshakeTimeoutCall is None)
def _exception_from_message(self, msg): """ Create a user (or generic) exception from a WAMP error message. :param msg: A WAMP error message. :type msg: instance of :class:`autobahn.wamp.message.Error` """ # FIXME: # 1. map to ecls based on error URI wildcard/prefix # 2. extract additional args/kwargs from error URI exc = None if msg.error in self._uri_to_ecls: ecls = self._uri_to_ecls[msg.error] try: # the following might fail, eg. TypeError when # signature of exception constructor is incompatible # with args/kwargs or when the exception constructor raises if msg.kwargs: if msg.args: exc = ecls(*msg.args, **msg.kwargs) else: exc = ecls(**msg.kwargs) else: if msg.args: exc = ecls(*msg.args) else: exc = ecls() except Exception: try: self.onUserError( txaio.create_failure(), "While re-constructing exception", ) except: pass if not exc: # the following ctor never fails .. if msg.kwargs: if msg.args: exc = exception.ApplicationError(msg.error, *msg.args, **msg.kwargs) else: exc = exception.ApplicationError(msg.error, **msg.kwargs) else: if msg.args: exc = exception.ApplicationError(msg.error, *msg.args) else: exc = exception.ApplicationError(msg.error) return exc
def test_loseConnection(self): """ If we lose our connection before openHandshakeTimeout fires, it is cleaned up """ # so, I guess a little cheezy, but we depend on the asyncio or # twisted class to call _connectionLost at some point; faking # that here self.protocol._connectionLost( txaio.create_failure(RuntimeError("testing"))) self.assertTrue(self.protocol.openHandshakeTimeoutCall is None)
def on_leave(session, details): self.log.info( "session leaving '{details.reason}'", details=details, ) if not txaio.is_called(done): if details.reason in [u"wamp.close.normal"]: txaio.resolve(done, None) else: f = txaio.create_failure( ApplicationError(details.reason)) txaio.reject(done, f)
def on_leave(session, details): self.log.info( "session leaving '{details.reason}'", details=details, ) if not txaio.is_called(done): if details.reason in [u"wamp.close.normal"]: txaio.resolve(done, None) else: f = txaio.create_failure( ApplicationError(details.reason) ) txaio.reject(done, f)
def on_leave(session, details): self.log.info( "session leaving '{details.reason}'", details=details, ) if not txaio.is_called(done): if details.reason in [u"wamp.error.no_auth_method"]: txaio.resolve(done, txaio.create_failure( ApplicationError( u"wamp.error.no_auth_method" ) )) else: txaio.resolve(done, None)
def render_POST(self, request): """ A client sends a message via WAMP-over-Longpoll by HTTP/POSTing to this Web resource. The body of the POST should contain a batch of WAMP messages which are serialized according to the selected serializer, and delimited by a single ``\0`` byte in between two WAMP messages in the batch. """ payload = request.content.read() self.log.debug( "WampLongPoll: receiving data for transport '{tid}'\n{octets}", tid=self._parent._transport_id, octets=_LazyHexFormatter(payload), ) try: # process (batch of) WAMP message(s) self._parent.onMessage(payload, None) except Exception: f = create_failure() self.log.error( "Could not unserialize WAMP message: {msg}", msg=failure_message(f), ) self.log.debug("{tb}", tb=failure_format_traceback(f)) return self._parent._parent._fail_request( request, b"could not unserialize WAMP message." ) else: request.setResponseCode(http.NO_CONTENT) self._parent._parent._set_standard_headers(request) self._parent._isalive = True return b""
def send(self, msg): """ Implements :func:`autobahn.wamp.interfaces.ITransport.send` """ if self.isOpen(): try: self.log.debug( "WampLongPoll: TX {octets}", octets=_LazyHexFormatter(msg), ) payload, isBinary = self._serializer.serialize(msg) except Exception as e: # all exceptions raised from above should be serialization errors .. f = create_failure() self.log.error( "Unable to serialize WAMP application payload: {msg}", msg=failure_message(f), ) self.log.debug("{tb}", tb=failure_format_traceback(f)) raise SerializationError("unable to serialize WAMP application payload ({0})".format(e)) else: self._receive.queue(payload) else: raise TransportLost()
def onMessage(self, msg): """ Implements :func:`autobahn.wamp.interfaces.ITransportHandler.onMessage` """ if self._session_id is None: # the first message must be WELCOME, ABORT or CHALLENGE .. if isinstance(msg, message.Welcome): self._session_id = msg.session details = SessionDetails(self._realm, self._session_id, msg.authid, msg.authrole, msg.authmethod) d = txaio.as_future(self.onJoin, details) def _error(e): return self._swallow_error(e, "While firing onJoin") txaio.add_callbacks(d, None, _error) elif isinstance(msg, message.Abort): # fire callback and close the transport details = types.CloseDetails(msg.reason, msg.message) d = txaio.as_future(self.onLeave, details) def _error(e): return self._swallow_error(e, "While firing onLeave") txaio.add_callbacks(d, None, _error) elif isinstance(msg, message.Challenge): challenge = types.Challenge(msg.method, msg.extra) d = txaio.as_future(self.onChallenge, challenge) def success(signature): if not isinstance(signature, six.text_type): raise Exception('signature must be unicode') reply = message.Authenticate(signature) self._transport.send(reply) def error(err): self.onUserError(err, "Authentication failed") reply = message.Abort(u"wamp.error.cannot_authenticate", u"{0}".format(err.value)) self._transport.send(reply) # fire callback and close the transport details = types.CloseDetails(reply.reason, reply.message) d = txaio.as_future(self.onLeave, details) def _error(e): return self._swallow_error(e, "While firing onLeave") txaio.add_callbacks(d, None, _error) # switching to the callback chain, effectively # cancelling error (which we've now handled) return d txaio.add_callbacks(d, success, error) else: raise ProtocolError( "Received {0} message, and session is not yet established". format(msg.__class__)) else: # self._session_id != None (aka "session established") if isinstance(msg, message.Goodbye): if not self._goodbye_sent: # the peer wants to close: send GOODBYE reply reply = message.Goodbye() self._transport.send(reply) self._session_id = None # fire callback and close the transport details = types.CloseDetails(msg.reason, msg.message) d = txaio.as_future(self.onLeave, details) def _error(e): errmsg = 'While firing onLeave for reason "{0}" and message "{1}"'.format( msg.reason, msg.message) return self._swallow_error(e, errmsg) txaio.add_callbacks(d, None, _error) elif isinstance(msg, message.Event): if msg.subscription in self._subscriptions: # fire all event handlers on subscription .. for subscription in self._subscriptions[msg.subscription]: handler = subscription.handler invoke_args = ( handler.obj, ) if handler.obj else tuple() if msg.args: invoke_args = invoke_args + tuple(msg.args) invoke_kwargs = msg.kwargs if msg.kwargs else dict() if handler.details_arg: invoke_kwargs[ handler.details_arg] = types.EventDetails( publication=msg.publication, publisher=msg.publisher, topic=msg.topic or subscription.topic) def _error(e): errmsg = 'While firing {0} subscribed under {1}.'.format( handler.fn, msg.subscription) return self._swallow_error(e, errmsg) future = txaio.as_future(handler.fn, *invoke_args, **invoke_kwargs) txaio.add_callbacks(future, None, _error) else: raise ProtocolError( "EVENT received for non-subscribed subscription ID {0}" .format(msg.subscription)) elif isinstance(msg, message.Published): if msg.request in self._publish_reqs: # get and pop outstanding publish request publish_request = self._publish_reqs.pop(msg.request) # create a new publication object publication = Publication(msg.publication) # resolve deferred/future for publishing successfully txaio.resolve(publish_request.on_reply, publication) else: raise ProtocolError( "PUBLISHED received for non-pending request ID {0}". format(msg.request)) elif isinstance(msg, message.Subscribed): if msg.request in self._subscribe_reqs: # get and pop outstanding subscribe request request = self._subscribe_reqs.pop(msg.request) # create new handler subscription list for subscription ID if not yet tracked if msg.subscription not in self._subscriptions: self._subscriptions[msg.subscription] = [] subscription = Subscription(msg.subscription, request.topic, self, request.handler) # add handler to existing subscription self._subscriptions[msg.subscription].append(subscription) # resolve deferred/future for subscribing successfully txaio.resolve(request.on_reply, subscription) else: raise ProtocolError( "SUBSCRIBED received for non-pending request ID {0}". format(msg.request)) elif isinstance(msg, message.Unsubscribed): if msg.request in self._unsubscribe_reqs: # get and pop outstanding subscribe request request = self._unsubscribe_reqs.pop(msg.request) # if the subscription still exists, mark as inactive and remove .. if request.subscription_id in self._subscriptions: for subscription in self._subscriptions[ request.subscription_id]: subscription.active = False del self._subscriptions[request.subscription_id] # resolve deferred/future for unsubscribing successfully txaio.resolve(request.on_reply, 0) else: raise ProtocolError( "UNSUBSCRIBED received for non-pending request ID {0}". format(msg.request)) elif isinstance(msg, message.Result): if msg.request in self._call_reqs: if msg.progress: # progressive result call_request = self._call_reqs[msg.request] if call_request.options.on_progress: kw = msg.kwargs or dict() args = msg.args or tuple() try: # XXX what if on_progress returns a Deferred/Future? call_request.options.on_progress(*args, **kw) except Exception: try: self.onUserError( txaio.create_failure(), "While firing on_progress", ) except: pass else: # silently ignore progressive results pass else: # final result # call_request = self._call_reqs.pop(msg.request) on_reply = call_request.on_reply if msg.kwargs: if msg.args: res = types.CallResult(*msg.args, **msg.kwargs) else: res = types.CallResult(**msg.kwargs) txaio.resolve(on_reply, res) else: if msg.args: if len(msg.args) > 1: res = types.CallResult(*msg.args) txaio.resolve(on_reply, res) else: txaio.resolve(on_reply, msg.args[0]) else: txaio.resolve(on_reply, None) else: raise ProtocolError( "RESULT received for non-pending request ID {0}". format(msg.request)) elif isinstance(msg, message.Invocation): if msg.request in self._invocations: raise ProtocolError( "INVOCATION received for request ID {0} already invoked" .format(msg.request)) else: if msg.registration not in self._registrations: raise ProtocolError( "INVOCATION received for non-registered registration ID {0}" .format(msg.registration)) else: registration = self._registrations[msg.registration] endpoint = registration.endpoint if endpoint.obj is not None: invoke_args = (endpoint.obj, ) else: invoke_args = tuple() if msg.args: invoke_args = invoke_args + tuple(msg.args) invoke_kwargs = msg.kwargs if msg.kwargs else dict() if endpoint.details_arg: if msg.receive_progress: def progress(*args, **kwargs): progress_msg = message.Yield(msg.request, args=args, kwargs=kwargs, progress=True) self._transport.send(progress_msg) else: progress = None invoke_kwargs[ endpoint.details_arg] = types.CallDetails( progress, caller=msg.caller, procedure=msg.procedure) on_reply = txaio.as_future(endpoint.fn, *invoke_args, **invoke_kwargs) def success(res): del self._invocations[msg.request] if isinstance(res, types.CallResult): reply = message.Yield(msg.request, args=res.results, kwargs=res.kwresults) else: reply = message.Yield(msg.request, args=[res]) try: self._transport.send(reply) except SerializationError as e: # the application-level payload returned from the invoked procedure can't be serialized reply = message.Error( message.Invocation.MESSAGE_TYPE, msg.request, ApplicationError.INVALID_PAYLOAD, args=[ u'success return value from invoked procedure "{0}" could not be serialized: {1}' .format(registration.procedure, e) ]) self._transport.send(reply) def error(err): errmsg = txaio.failure_message(err) try: self.onUserError(err, errmsg) except: pass formatted_tb = None if self.traceback_app: formatted_tb = txaio.failure_format_traceback( err) del self._invocations[msg.request] reply = self._message_from_exception( message.Invocation.MESSAGE_TYPE, msg.request, err.value, formatted_tb, ) try: self._transport.send(reply) except SerializationError as e: # the application-level payload returned from the invoked procedure can't be serialized reply = message.Error( message.Invocation.MESSAGE_TYPE, msg.request, ApplicationError.INVALID_PAYLOAD, args=[ u'error return value from invoked procedure "{0}" could not be serialized: {1}' .format(registration.procedure, e) ]) self._transport.send(reply) # we have handled the error, so we eat it return None self._invocations[msg.request] = InvocationRequest( msg.request, on_reply) txaio.add_callbacks(on_reply, success, error) elif isinstance(msg, message.Interrupt): if msg.request not in self._invocations: raise ProtocolError( "INTERRUPT received for non-pending invocation {0}". format(msg.request)) else: # noinspection PyBroadException try: self._invocations[msg.request].cancel() except Exception: # XXX can .cancel() return a Deferred/Future? try: self.onUserError( txaio.create_failure(), "While cancelling call.", ) except: pass finally: del self._invocations[msg.request] elif isinstance(msg, message.Registered): if msg.request in self._register_reqs: # get and pop outstanding register request request = self._register_reqs.pop(msg.request) # create new registration if not yet tracked if msg.registration not in self._registrations: registration = Registration(self, msg.registration, request.procedure, request.endpoint) self._registrations[msg.registration] = registration else: raise ProtocolError( "REGISTERED received for already existing registration ID {0}" .format(msg.registration)) txaio.resolve(request.on_reply, registration) else: raise ProtocolError( "REGISTERED received for non-pending request ID {0}". format(msg.request)) elif isinstance(msg, message.Unregistered): if msg.request in self._unregister_reqs: # get and pop outstanding subscribe request request = self._unregister_reqs.pop(msg.request) # if the registration still exists, mark as inactive and remove .. if request.registration_id in self._registrations: self._registrations[ request.registration_id].active = False del self._registrations[request.registration_id] # resolve deferred/future for unregistering successfully txaio.resolve(request.on_reply) else: raise ProtocolError( "UNREGISTERED received for non-pending request ID {0}". format(msg.request)) elif isinstance(msg, message.Error): # remove outstanding request and get the reply deferred/future on_reply = None # ERROR reply to CALL if msg.request_type == message.Call.MESSAGE_TYPE and msg.request in self._call_reqs: on_reply = self._call_reqs.pop(msg.request).on_reply # ERROR reply to PUBLISH elif msg.request_type == message.Publish.MESSAGE_TYPE and msg.request in self._publish_reqs: on_reply = self._publish_reqs.pop(msg.request).on_reply # ERROR reply to SUBSCRIBE elif msg.request_type == message.Subscribe.MESSAGE_TYPE and msg.request in self._subscribe_reqs: on_reply = self._subscribe_reqs.pop(msg.request).on_reply # ERROR reply to UNSUBSCRIBE elif msg.request_type == message.Unsubscribe.MESSAGE_TYPE and msg.request in self._unsubscribe_reqs: on_reply = self._unsubscribe_reqs.pop(msg.request).on_reply # ERROR reply to REGISTER elif msg.request_type == message.Register.MESSAGE_TYPE and msg.request in self._register_reqs: on_reply = self._register_reqs.pop(msg.request).on_reply # ERROR reply to UNREGISTER elif msg.request_type == message.Unregister.MESSAGE_TYPE and msg.request in self._unregister_reqs: on_reply = self._unregister_reqs.pop(msg.request).on_reply if on_reply: txaio.reject(on_reply, self._exception_from_message(msg)) else: raise ProtocolError( "WampAppSession.onMessage(): ERROR received for non-pending request_type {0} and request ID {1}" .format(msg.request_type, msg.request)) else: raise ProtocolError("Unexpected message {0}".format( msg.__class__))
def create_session(): cfg = ComponentConfig(self._realm, self._extra) try: self._session = session = self.session_factory(cfg) for auth_name, auth_config in self._authentication.items(): if isinstance(auth_config, IAuthenticator): session.add_authenticator(auth_config) else: authenticator = create_authenticator( auth_name, **auth_config) session.add_authenticator(authenticator) except Exception as e: # couldn't instantiate session calls, which is fatal. # let the reconnection logic deal with that f = txaio.create_failure(e) txaio.reject(done, f) raise else: # hook up the listener to the parent so we can bubble # up events happning on the session onto the # connection. This lets you do component.on('join', # cb) which will work just as if you called # session.on('join', cb) for every session created. session._parent = self # listen on leave events; if we get errors # (e.g. no_such_realm), an on_leave can happen without # an on_join before def on_leave(session, details): self.log.info( "session leaving '{details.reason}'", details=details, ) if not txaio.is_called(done): if details.reason in [u"wamp.close.normal"]: txaio.resolve(done, None) else: f = txaio.create_failure( ApplicationError(details.reason)) txaio.reject(done, f) session.on('leave', on_leave) # if we were given a "main" procedure, we run through # it completely (i.e. until its Deferred fires) and # then disconnect this session def on_join(session, details): transport.connect_sucesses += 1 self.log.debug("session on_join: {details}", details=details) d = txaio.as_future(self._entry, reactor, session) def main_success(_): self.log.debug("main_success") def leave(): try: session.leave() except SessionNotReady: # someone may have already called # leave() pass txaio.call_later(0, leave) def main_error(err): self.log.debug("main_error: {err}", err=err) txaio.reject(done, err) session.disconnect() txaio.add_callbacks(d, main_success, main_error) if self._entry is not None: session.on('join', on_join) # listen on disconnect events. Note that in case we # had a "main" procedure, we could have already # resolve()'d our "done" future def on_disconnect(session, was_clean): self.log.debug( "session on_disconnect: was_clean={was_clean}", was_clean=was_clean, ) if not txaio.is_called(done): if not was_clean: self.log.warn(u"Session disconnected uncleanly") else: # eg the session has left the realm, and the transport was properly # shut down. successfully finish the connection txaio.resolve(done, None) session.on('disconnect', on_disconnect) # return the fresh session object return session
def start(self, reactor=None): """ This starts the Component, which means it will start connecting (and re-connecting) to its configured transports. A Component runs until it is "done", which means one of: - There was a "main" function defined, and it completed successfully; - Something called ``.leave()`` on our session, and we left successfully; - ``.stop()`` was called, and completed successfully; - none of our transports were able to connect successfully (failure); :returns: a Deferred that fires (with ``None``) when we are "done" or with a Failure if something went wrong. """ if reactor is None: self.log.warn("Using default reactor") from twisted.internet import reactor yield self.fire('start', reactor, self) # transports to try again and again .. transport_gen = itertools.cycle(self._transports) reconnect = True self.log.debug('Entering re-connect loop') while reconnect: # cycle through all transports forever .. transport = next(transport_gen) # only actually try to connect using the transport, # if the transport hasn't reached max. connect count if transport.can_reconnect(): delay = transport.next_delay() self.log.debug( 'trying transport {transport_idx} using connect delay {transport_delay}', transport_idx=transport.idx, transport_delay=delay, ) yield sleep(delay) try: transport.connect_attempts += 1 yield self._connect_once(reactor, transport) transport.connect_sucesses += 1 except Exception as e: transport.connect_failures += 1 f = txaio.create_failure() self.log.error(u'component failed: {error}', error=txaio.failure_message(f)) self.log.debug(u'{tb}', tb=txaio.failure_format_traceback(f)) # If this is a "fatal error" that will never work, # we bail out now if isinstance(e, ApplicationError): if e.error in [u'wamp.error.no_such_realm']: reconnect = False self.log.error(u"Fatal error, not reconnecting") # The thinking here is that we really do # want to 'raise' (and thereby fail the # entire "start" / reconnect loop) because # if the realm isn't valid, we're "never" # going to succeed... raise self.log.error(u"{msg}", msg=e.error_message()) elif _is_ssl_error(e): # Quoting pyOpenSSL docs: "Whenever # [SSL.Error] is raised directly, it has a # list of error messages from the OpenSSL # error queue, where each item is a tuple # (lib, function, reason). Here lib, function # and reason are all strings, describing where # and what the problem is. See err(3) for more # information." for (lib, fn, reason) in e.args[0]: self.log.error(u"TLS failure: {reason}", reason=reason) self.log.error(u"Marking this transport as failed") transport.failed() else: f = txaio.create_failure() self.log.error( u'Connection failed: {error}', error=txaio.failure_message(f), ) # some types of errors should probably have # stacktraces logged immediately at error # level, e.g. SyntaxError? self.log.debug(u'{tb}', tb=txaio.failure_format_traceback(f)) else: self.log.debug(u"Not reconnecting") reconnect = False else: # check if there is any transport left we can use # to connect if not self._can_reconnect(): self.log.info("No remaining transports to try") reconnect = False
def render_POST(self, request): """ Request to create a new WAMP session. """ self.log.debug("WampLongPoll: creating new session ..") payload = request.content.read().decode('utf8') try: options = json.loads(payload) except Exception: f = create_failure() self.log.error( "Couldn't parse WAMP request body: {msg}", msg=failure_message(f), ) self.log.debug("{msg}", msg=failure_format_traceback(f)) return self._parent._fail_request( request, b"could not parse WAMP session open request body") if not isinstance(options, dict): return self._parent._fail_request( request, b"invalid type for WAMP session open request") if 'protocols' not in options: return self._parent._fail_request( request, "missing attribute 'protocols' in WAMP session open request") # determine the protocol to speak # protocol = None serializer = None for p in options['protocols']: version, serializerId = parseSubprotocolIdentifier(p) if version == 2 and serializerId in self._parent._serializers.keys( ): serializer = self._parent._serializers[serializerId] protocol = p break if protocol is None: return self._fail_request( request, b"no common protocol to speak (I speak: {0})".format([ "wamp.2.{0}".format(s) for s in self._parent._serializers.keys() ])) # make up new transport ID # if self._parent._debug_transport_id: # use fixed transport ID for debugging purposes transport = self._parent._debug_transport_id else: transport = generate_token(1, 12) # this doesn't contain all the info (when a header key appears multiple times) # http_headers_received = request.getAllHeaders() http_headers_received = {} for key, values in request.requestHeaders.getAllRawHeaders(): if key not in http_headers_received: http_headers_received[key] = [] http_headers_received[key].extend(values) transport_details = { 'transport': transport, 'serializer': serializer, 'protocol': protocol, 'peer': request.getClientIP(), 'http_headers_received': http_headers_received, 'http_headers_sent': None } # create instance of WampLongPollResourceSession or subclass thereof .. # self._parent._transports[transport] = self._parent.protocol( self._parent, transport_details) # create response # self._parent._set_standard_headers(request) request.setHeader(b'content-type', b'application/json; charset=utf-8') result = {'transport': transport, 'protocol': protocol} self.log.debug("WampLongPoll: new session created on transport" " '{transport}'".format(transport=transport, )) payload = json.dumps(result) return payload.encode()
def create_session(): cfg = ComponentConfig(self._realm, self._extra) try: self._session = session = self.session_factory(cfg) for auth_name, auth_config in self._authentication.items(): authenticator = create_authenticator(auth_name, **auth_config) session.add_authenticator(authenticator) except Exception as e: # couldn't instantiate session calls, which is fatal. # let the reconnection logic deal with that f = txaio.create_failure(e) txaio.reject(done, f) raise else: # hook up the listener to the parent so we can bubble # up events happning on the session onto the # connection. This lets you do component.on('join', # cb) which will work just as if you called # session.on('join', cb) for every session created. session._parent = self # listen on leave events; if we get errors # (e.g. no_such_realm), an on_leave can happen without # an on_join before def on_leave(session, details): self.log.info( "session leaving '{details.reason}'", details=details, ) if not txaio.is_called(done): if details.reason in [u"wamp.error.no_auth_method"]: txaio.resolve(done, txaio.create_failure( ApplicationError( u"wamp.error.no_auth_method" ) )) else: txaio.resolve(done, None) session.on('leave', on_leave) # if we were given a "main" procedure, we run through # it completely (i.e. until its Deferred fires) and # then disconnect this session def on_join(session, details): transport.connect_sucesses += 1 self.log.debug("session on_join: {details}", details=details) d = txaio.as_future(self._entry, reactor, session) def main_success(_): self.log.debug("main_success") def leave(): try: session.leave() except SessionNotReady: # someone may have already called # leave() pass txaio.call_later(0, leave) def main_error(err): self.log.debug("main_error: {err}", err=err) txaio.reject(done, err) session.disconnect() txaio.add_callbacks(d, main_success, main_error) if self._entry is not None: session.on('join', on_join) # listen on disconnect events. Note that in case we # had a "main" procedure, we could have already # resolve()'d our "done" future def on_disconnect(session, was_clean): self.log.debug( "session on_disconnect: was_clean={was_clean}", was_clean=was_clean, ) if not txaio.is_called(done): if not was_clean: self.log.warn( u"Session disconnected uncleanly" ) # eg the session has left the realm, and the transport was properly # shut down. successfully finish the connection txaio.resolve(done, None) session.on('disconnect', on_disconnect) # return the fresh session object return session
def create_session(): cfg = ComponentConfig(self._realm, self._extra) try: session = self.session_factory(cfg) except Exception as e: # couldn't instantiate session calls, which is fatal. # let the reconnection logic deal with that f = txaio.create_failure(e) txaio.reject(done, f) raise else: # hook up the listener to the parent so we can bubble # up events happning on the session onto the # connection. This lets you do component.on('join', # cb) which will work just as if you called # session.on('join', cb) for every session created. session._parent = self # the only difference bewteen MAIN and SETUP-type # entry-points is that we want to shut down the # component when a MAIN-type entrypoint's Deferred is # done. if self._entry_type == Component.TYPE_MAIN: def on_join(session, details): self.log.debug("session on_join: {details}", details=details) transport.connect_sucesses += 1 self.log.info( 'Successfully connected to transport #{transport_idx}: url={url}', transport_idx=transport.idx, url=transport.config['url'], ) d = txaio.as_future(self._entry, reactor, session) def main_success(_): self.log.debug("main_success") session.leave() def main_error(err): self.log.debug("main_error: {err}", err=err) txaio.reject(done, err) # I guess .leave() here too...? txaio.add_callbacks(d, main_success, main_error) session.on('join', on_join) elif self._entry_type == Component.TYPE_SETUP: def on_join(session, details): self.log.debug("session on_join: {details}", details=details) self.log.info( 'Successfully connected to transport #{transport_idx}: url={url}', transport_idx=transport.idx, url=transport.config['url'], ) d = txaio.as_future(self._entry, reactor, session) def setup_success(_): self.log.debug("setup_success") def setup_error(err): self.log.debug("setup_error: {err}", err=err) txaio.reject(done, err) txaio.add_callbacks(d, setup_success, setup_error) session.on('join', on_join) else: assert (False), 'logic error' # listen on leave events def on_leave(session, details): self.log.debug("session on_leave: {details}", details=details) # this could be a "leave" that's expected e.g. our # main() exited, or it could be an error if not txaio.is_called(done): if details.reason.startswith('wamp.error.'): txaio.reject( done, ApplicationError(details.reason, details.message)) else: txaio.resolve(done, None) session.on('leave', on_leave) # listen on disconnect events def on_disconnect(session, was_clean): self.log.debug("session on_disconnect: {was_clean}", was_clean=was_clean) if was_clean: # eg the session has left the realm, and the transport was properly # shut down. successfully finish the connection txaio.resolve(done, None) else: txaio.reject( done, RuntimeError('transport closed uncleanly')) session.on('disconnect', on_disconnect) # return the fresh session object return session
def render_POST(self, request): """ Request to create a new WAMP session. """ self.log.debug("WampLongPoll: creating new session ..") payload = request.content.read().decode('utf8') try: options = json.loads(payload) except Exception: f = create_failure() self.log.error( "Couldn't parse WAMP request body: {msg}", msg=failure_message(f), ) self.log.debug("{msg}", msg=failure_format_traceback(f)) return self._parent._fail_request(request, b"could not parse WAMP session open request body") if type(options) != dict: return self._parent._fail_request(request, b"invalid type for WAMP session open request") if u'protocols' not in options: return self._parent._fail_request(request, "missing attribute 'protocols' in WAMP session open request") # determine the protocol to speak # protocol = None serializer = None for p in options[u'protocols']: version, serializerId = parseSubprotocolIdentifier(p) if version == 2 and serializerId in self._parent._serializers.keys(): serializer = self._parent._serializers[serializerId] protocol = p break if protocol is None: return self._fail_request( request, b"no common protocol to speak (I speak: {0})".format( ["wamp.2.{0}".format(s) for s in self._parent._serializers.keys()]) ) # make up new transport ID # if self._parent._debug_transport_id: # use fixed transport ID for debugging purposes transport = self._parent._debug_transport_id else: transport = generate_token(1, 12) # this doesn't contain all the info (when a header key appears multiple times) # http_headers_received = request.getAllHeaders() http_headers_received = {} for key, values in request.requestHeaders.getAllRawHeaders(): if key not in http_headers_received: http_headers_received[key] = [] http_headers_received[key].extend(values) transport_details = { u'transport': transport, u'serializer': serializer, u'protocol': protocol, u'peer': request.getClientIP(), u'http_headers_received': http_headers_received, u'http_headers_sent': None } # create instance of WampLongPollResourceSession or subclass thereof .. # self._parent._transports[transport] = self._parent.protocol(self._parent, transport_details) # create response # self._parent._set_standard_headers(request) request.setHeader(b'content-type', b'application/json; charset=utf-8') result = { u'transport': transport, u'protocol': protocol } self.log.debug( "WampLongPoll: new session created on transport" " '{transport}'".format( transport=transport, ) ) payload = json.dumps(result) return payload.encode()
def start(self, reactor=None): """ This starts the Component, which means it will start connecting (and re-connecting) to its configured transports. A Component runs until it is "done", which means one of: - There was a "main" function defined, and it completed successfully; - Something called ``.leave()`` on our session, and we left successfully; - ``.stop()`` was called, and completed successfully; - none of our transports were able to connect successfully (failure); :returns: a Deferred that fires (with ``None``) when we are "done" or with a Failure if something went wrong. """ if reactor is None: self.log.warn("Using default reactor") from twisted.internet import reactor yield self.fire('start', reactor, self) # transports to try again and again .. transport_gen = itertools.cycle(self._transports) reconnect = True last_failure = None self.log.debug('Entering re-connect loop') while reconnect: # cycle through all transports forever .. transport = next(transport_gen) # only actually try to connect using the transport, # if the transport hasn't reached max. connect count if transport.can_reconnect(): delay = transport.next_delay() self.log.debug( 'trying transport {transport_idx} using connect delay {transport_delay}', transport_idx=transport.idx, transport_delay=delay, ) yield sleep(delay) try: yield self._connect_once(reactor, transport) except Exception as e: f = txaio.create_failure() last_failure = f self.log.error(u'component failed: {error}', error=txaio.failure_message(f)) self.log.debug(u'{tb}', tb=txaio.failure_format_traceback(f)) # If this is a "fatal error" that will never work, # we bail out now if isinstance(e, ApplicationError): if e.error in [u'wamp.error.no_such_realm', u'wamp.error.no_auth_method']: reconnect = False self.log.error(u"Fatal error, not reconnecting") # The thinking here is that we really do # want to 'raise' (and thereby fail the # entire "start" / reconnect loop) because # if the realm isn't valid, we're "never" # going to succeed... raise self.log.error(u"{msg}", msg=e.error_message()) elif _is_ssl_error(e): # Quoting pyOpenSSL docs: "Whenever # [SSL.Error] is raised directly, it has a # list of error messages from the OpenSSL # error queue, where each item is a tuple # (lib, function, reason). Here lib, function # and reason are all strings, describing where # and what the problem is. See err(3) for more # information." for (lib, fn, reason) in e.args[0]: self.log.error(u"TLS failure: {reason}", reason=reason) self.log.error(u"Marking this transport as failed") transport.failed() else: f = txaio.create_failure() self.log.error( u'Connection failed: {error}', error=txaio.failure_message(f), ) # some types of errors should probably have # stacktraces logged immediately at error # level, e.g. SyntaxError? self.log.debug(u'{tb}', tb=txaio.failure_format_traceback(f)) else: self.log.debug(u"Not reconnecting") reconnect = False else: # check if there is any transport left we can use # to connect if not self._can_reconnect(): self.log.info("No remaining transports to try") reconnect = False if last_failure is not None: last_failure.raiseException()
def onMessage(self, msg): """ Implements :func:`autobahn.wamp.interfaces.ITransportHandler.onMessage` """ if self._session_id is None: # the first message must be WELCOME, ABORT or CHALLENGE .. if isinstance(msg, message.Welcome): self._session_id = msg.session details = SessionDetails(self._realm, self._session_id, msg.authid, msg.authrole, msg.authmethod) d = txaio.as_future(self.onJoin, details) def _error(e): return self._swallow_error(e, "While firing onJoin") txaio.add_callbacks(d, None, _error) elif isinstance(msg, message.Abort): # fire callback and close the transport details = types.CloseDetails(msg.reason, msg.message) d = txaio.as_future(self.onLeave, details) def _error(e): return self._swallow_error(e, "While firing onLeave") txaio.add_callbacks(d, None, _error) elif isinstance(msg, message.Challenge): challenge = types.Challenge(msg.method, msg.extra) d = txaio.as_future(self.onChallenge, challenge) def success(signature): if not isinstance(signature, six.text_type): raise Exception('signature must be unicode') reply = message.Authenticate(signature) self._transport.send(reply) def error(err): self.onUserError(err, "Authentication failed") reply = message.Abort(u"wamp.error.cannot_authenticate", u"{0}".format(err.value)) self._transport.send(reply) # fire callback and close the transport details = types.CloseDetails(reply.reason, reply.message) d = txaio.as_future(self.onLeave, details) def _error(e): return self._swallow_error(e, "While firing onLeave") txaio.add_callbacks(d, None, _error) # switching to the callback chain, effectively # cancelling error (which we've now handled) return d txaio.add_callbacks(d, success, error) else: raise ProtocolError("Received {0} message, and session is not yet established".format(msg.__class__)) else: # self._session_id != None (aka "session established") if isinstance(msg, message.Goodbye): if not self._goodbye_sent: # the peer wants to close: send GOODBYE reply reply = message.Goodbye() self._transport.send(reply) self._session_id = None # fire callback and close the transport details = types.CloseDetails(msg.reason, msg.message) d = txaio.as_future(self.onLeave, details) def _error(e): errmsg = 'While firing onLeave for reason "{0}" and message "{1}"'.format(msg.reason, msg.message) return self._swallow_error(e, errmsg) txaio.add_callbacks(d, None, _error) elif isinstance(msg, message.Event): if msg.subscription in self._subscriptions: # fire all event handlers on subscription .. for subscription in self._subscriptions[msg.subscription]: handler = subscription.handler invoke_args = (handler.obj,) if handler.obj else tuple() if msg.args: invoke_args = invoke_args + tuple(msg.args) invoke_kwargs = msg.kwargs if msg.kwargs else dict() if handler.details_arg: invoke_kwargs[handler.details_arg] = types.EventDetails(publication=msg.publication, publisher=msg.publisher, topic=msg.topic or subscription.topic) def _error(e): errmsg = 'While firing {0} subscribed under {1}.'.format( handler.fn, msg.subscription) return self._swallow_error(e, errmsg) future = txaio.as_future(handler.fn, *invoke_args, **invoke_kwargs) txaio.add_callbacks(future, None, _error) else: raise ProtocolError("EVENT received for non-subscribed subscription ID {0}".format(msg.subscription)) elif isinstance(msg, message.Published): if msg.request in self._publish_reqs: # get and pop outstanding publish request publish_request = self._publish_reqs.pop(msg.request) # create a new publication object publication = Publication(msg.publication) # resolve deferred/future for publishing successfully txaio.resolve(publish_request.on_reply, publication) else: raise ProtocolError("PUBLISHED received for non-pending request ID {0}".format(msg.request)) elif isinstance(msg, message.Subscribed): if msg.request in self._subscribe_reqs: # get and pop outstanding subscribe request request = self._subscribe_reqs.pop(msg.request) # create new handler subscription list for subscription ID if not yet tracked if msg.subscription not in self._subscriptions: self._subscriptions[msg.subscription] = [] subscription = Subscription(msg.subscription, request.topic, self, request.handler) # add handler to existing subscription self._subscriptions[msg.subscription].append(subscription) # resolve deferred/future for subscribing successfully txaio.resolve(request.on_reply, subscription) else: raise ProtocolError("SUBSCRIBED received for non-pending request ID {0}".format(msg.request)) elif isinstance(msg, message.Unsubscribed): if msg.request in self._unsubscribe_reqs: # get and pop outstanding subscribe request request = self._unsubscribe_reqs.pop(msg.request) # if the subscription still exists, mark as inactive and remove .. if request.subscription_id in self._subscriptions: for subscription in self._subscriptions[request.subscription_id]: subscription.active = False del self._subscriptions[request.subscription_id] # resolve deferred/future for unsubscribing successfully txaio.resolve(request.on_reply, 0) else: raise ProtocolError("UNSUBSCRIBED received for non-pending request ID {0}".format(msg.request)) elif isinstance(msg, message.Result): if msg.request in self._call_reqs: if msg.progress: # progressive result call_request = self._call_reqs[msg.request] if call_request.options.on_progress: kw = msg.kwargs or dict() args = msg.args or tuple() try: # XXX what if on_progress returns a Deferred/Future? call_request.options.on_progress(*args, **kw) except Exception: try: self.onUserError( txaio.create_failure(), "While firing on_progress", ) except: pass else: # silently ignore progressive results pass else: # final result # call_request = self._call_reqs.pop(msg.request) on_reply = call_request.on_reply if msg.kwargs: if msg.args: res = types.CallResult(*msg.args, **msg.kwargs) else: res = types.CallResult(**msg.kwargs) txaio.resolve(on_reply, res) else: if msg.args: if len(msg.args) > 1: res = types.CallResult(*msg.args) txaio.resolve(on_reply, res) else: txaio.resolve(on_reply, msg.args[0]) else: txaio.resolve(on_reply, None) else: raise ProtocolError("RESULT received for non-pending request ID {0}".format(msg.request)) elif isinstance(msg, message.Invocation): if msg.request in self._invocations: raise ProtocolError("INVOCATION received for request ID {0} already invoked".format(msg.request)) else: if msg.registration not in self._registrations: raise ProtocolError("INVOCATION received for non-registered registration ID {0}".format(msg.registration)) else: registration = self._registrations[msg.registration] endpoint = registration.endpoint if endpoint.obj is not None: invoke_args = (endpoint.obj,) else: invoke_args = tuple() if msg.args: invoke_args = invoke_args + tuple(msg.args) invoke_kwargs = msg.kwargs if msg.kwargs else dict() if endpoint.details_arg: if msg.receive_progress: def progress(*args, **kwargs): progress_msg = message.Yield(msg.request, args=args, kwargs=kwargs, progress=True) self._transport.send(progress_msg) else: progress = None invoke_kwargs[endpoint.details_arg] = types.CallDetails(progress, caller=msg.caller, procedure=msg.procedure) on_reply = txaio.as_future(endpoint.fn, *invoke_args, **invoke_kwargs) def success(res): del self._invocations[msg.request] if isinstance(res, types.CallResult): reply = message.Yield(msg.request, args=res.results, kwargs=res.kwresults) else: reply = message.Yield(msg.request, args=[res]) try: self._transport.send(reply) except SerializationError as e: # the application-level payload returned from the invoked procedure can't be serialized reply = message.Error(message.Invocation.MESSAGE_TYPE, msg.request, ApplicationError.INVALID_PAYLOAD, args=[u'success return value from invoked procedure "{0}" could not be serialized: {1}'.format(registration.procedure, e)]) self._transport.send(reply) def error(err): errmsg = txaio.failure_message(err) try: self.onUserError(err, errmsg) except: pass formatted_tb = None if self.traceback_app: formatted_tb = txaio.failure_format_traceback(err) del self._invocations[msg.request] reply = self._message_from_exception( message.Invocation.MESSAGE_TYPE, msg.request, err.value, formatted_tb, ) try: self._transport.send(reply) except SerializationError as e: # the application-level payload returned from the invoked procedure can't be serialized reply = message.Error(message.Invocation.MESSAGE_TYPE, msg.request, ApplicationError.INVALID_PAYLOAD, args=[u'error return value from invoked procedure "{0}" could not be serialized: {1}'.format(registration.procedure, e)]) self._transport.send(reply) # we have handled the error, so we eat it return None self._invocations[msg.request] = InvocationRequest(msg.request, on_reply) txaio.add_callbacks(on_reply, success, error) elif isinstance(msg, message.Interrupt): if msg.request not in self._invocations: raise ProtocolError("INTERRUPT received for non-pending invocation {0}".format(msg.request)) else: # noinspection PyBroadException try: self._invocations[msg.request].cancel() except Exception: # XXX can .cancel() return a Deferred/Future? try: self.onUserError( txaio.create_failure(), "While cancelling call.", ) except: pass finally: del self._invocations[msg.request] elif isinstance(msg, message.Registered): if msg.request in self._register_reqs: # get and pop outstanding register request request = self._register_reqs.pop(msg.request) # create new registration if not yet tracked if msg.registration not in self._registrations: registration = Registration(self, msg.registration, request.procedure, request.endpoint) self._registrations[msg.registration] = registration else: raise ProtocolError("REGISTERED received for already existing registration ID {0}".format(msg.registration)) txaio.resolve(request.on_reply, registration) else: raise ProtocolError("REGISTERED received for non-pending request ID {0}".format(msg.request)) elif isinstance(msg, message.Unregistered): if msg.request in self._unregister_reqs: # get and pop outstanding subscribe request request = self._unregister_reqs.pop(msg.request) # if the registration still exists, mark as inactive and remove .. if request.registration_id in self._registrations: self._registrations[request.registration_id].active = False del self._registrations[request.registration_id] # resolve deferred/future for unregistering successfully txaio.resolve(request.on_reply) else: raise ProtocolError("UNREGISTERED received for non-pending request ID {0}".format(msg.request)) elif isinstance(msg, message.Error): # remove outstanding request and get the reply deferred/future on_reply = None # ERROR reply to CALL if msg.request_type == message.Call.MESSAGE_TYPE and msg.request in self._call_reqs: on_reply = self._call_reqs.pop(msg.request).on_reply # ERROR reply to PUBLISH elif msg.request_type == message.Publish.MESSAGE_TYPE and msg.request in self._publish_reqs: on_reply = self._publish_reqs.pop(msg.request).on_reply # ERROR reply to SUBSCRIBE elif msg.request_type == message.Subscribe.MESSAGE_TYPE and msg.request in self._subscribe_reqs: on_reply = self._subscribe_reqs.pop(msg.request).on_reply # ERROR reply to UNSUBSCRIBE elif msg.request_type == message.Unsubscribe.MESSAGE_TYPE and msg.request in self._unsubscribe_reqs: on_reply = self._unsubscribe_reqs.pop(msg.request).on_reply # ERROR reply to REGISTER elif msg.request_type == message.Register.MESSAGE_TYPE and msg.request in self._register_reqs: on_reply = self._register_reqs.pop(msg.request).on_reply # ERROR reply to UNREGISTER elif msg.request_type == message.Unregister.MESSAGE_TYPE and msg.request in self._unregister_reqs: on_reply = self._unregister_reqs.pop(msg.request).on_reply if on_reply: txaio.reject(on_reply, self._exception_from_message(msg)) else: raise ProtocolError("WampAppSession.onMessage(): ERROR received for non-pending request_type {0} and request ID {1}".format(msg.request_type, msg.request)) else: raise ProtocolError("Unexpected message {0}".format(msg.__class__))
def create_session(): cfg = ComponentConfig(self._realm, self._extra) try: session = self.session_factory(cfg) except Exception as e: # couldn't instantiate session calls, which is fatal. # let the reconnection logic deal with that f = txaio.create_failure(e) txaio.reject(done, f) raise else: # hook up the listener to the parent so we can bubble # up events happning on the session onto the # connection. This lets you do component.on('join', # cb) which will work just as if you called # session.on('join', cb) for every session created. session._parent = self # the only difference bewteen MAIN and SETUP-type # entry-points is that we want to shut down the # component when a MAIN-type entrypoint's Deferred is # done. if self._entry_type == Component.TYPE_MAIN: def on_join(session, details): self.log.debug("session on_join: {details}", details=details) transport.connect_sucesses += 1 self.log.info( 'Successfully connected to transport #{transport_idx}: url={url}', transport_idx=transport.idx, url=transport.config['url'], ) d = txaio.as_future(self._entry, reactor, session) def main_success(_): self.log.debug("main_success") session.leave() def main_error(err): self.log.debug("main_error: {err}", err=err) txaio.reject(done, err) # I guess .leave() here too...? txaio.add_callbacks(d, main_success, main_error) session.on('join', on_join) elif self._entry_type == Component.TYPE_SETUP: def on_join(session, details): self.log.debug("session on_join: {details}", details=details) self.log.info( 'Successfully connected to transport #{transport_idx}: url={url}', transport_idx=transport.idx, url=transport.config['url'], ) d = txaio.as_future(self._entry, reactor, session) def setup_success(_): self.log.debug("setup_success") def setup_error(err): self.log.debug("setup_error: {err}", err=err) txaio.reject(done, err) txaio.add_callbacks(d, setup_success, setup_error) session.on('join', on_join) else: assert(False), 'logic error' # listen on leave events def on_leave(session, details): self.log.debug("session on_leave: {details}", details=details) # this could be a "leave" that's expected e.g. our # main() exited, or it could be an error if not txaio.is_called(done): if details.reason.startswith('wamp.error.'): txaio.reject(done, ApplicationError(details.reason, details.message)) else: txaio.resolve(done, None) session.on('leave', on_leave) # listen on disconnect events def on_disconnect(session, was_clean): self.log.debug("session on_disconnect: {was_clean}", was_clean=was_clean) if was_clean: # eg the session has left the realm, and the transport was properly # shut down. successfully finish the connection txaio.resolve(done, None) else: txaio.reject(done, RuntimeError('transport closed uncleanly')) session.on('disconnect', on_disconnect) # return the fresh session object return session