Merge remote-tracking branch 'upstream/master' into more-twisted-tox-versions

This commit is contained in:
HawkOwl 2015-08-14 11:30:52 +08:00
commit 8f253822d6
328 changed files with 3943 additions and 6556 deletions

57
.gitignore vendored
View File

@ -1,28 +1,29 @@
*.pyc *.pyc
*.pyo *.pyo
*.dat *.dat
build build
dist dist
autobahn.egg-info autobahn.egg-info
*.sublime-workspace *.sublime-workspace
*.sublime-project *.sublime-project
__pycache__ __pycache__
dropin.cache dropin.cache
*.egg *.egg
*.tar.gz *.tar.gz
_trial_temp _trial_temp
_trial_temp* _trial_temp*
.idea .idea
.sconsign.dblite .sconsign.dblite
_html _html
_upload _upload
build build
egg-info egg-info
egg\-info egg\-info
egg egg
.egg* .egg*
.tox .tox
htmlcov/ htmlcov/
.coverage .coverage
*~ *~
*.log *.log
*.pid

View File

@ -55,6 +55,12 @@ test_twisted_coverage:
coverage html coverage html
coverage report --show-missing coverage report --show-missing
test_coverage:
-rm .coverage
tox -e py27twisted,py27asyncio,py34asyncio
coverage html
coverage report --show-missing
# test under asyncio # test under asyncio
test_asyncio: test_asyncio:
USE_ASYNCIO=1 python -m pytest -rsx USE_ASYNCIO=1 python -m pytest -rsx

View File

@ -19,13 +19,19 @@ WAMP provides asynchronous **Remote Procedure Calls** and **Publish & Subscribe*
It is ideal for distributed, multi-client and server applications, such as multi-user database-drive business applications, sensor networks (IoT), instant messaging or MMOGs (massively multi-player online games) . It is ideal for distributed, multi-client and server applications, such as multi-user database-drive business applications, sensor networks (IoT), instant messaging or MMOGs (massively multi-player online games) .
WAMP enables application architectures with application code distributed freely across processes and devices according to functional aspects. Since WAMP implementations exist for multiple languages, WAMP applications can be polyglott. Application components can be implemented in a language and run on a device which best fit the particular use case. WAMP enables application architectures with application code distributed freely across processes and devices according to functional aspects. Since WAMP implementations exist for multiple languages, WAMP applications can be polyglot. Application components can be implemented in a language and run on a device which best fit the particular use case.
**Note** that WAMP is a *routed* protocol, so you need to run something that plays the Broker and Dealer roles from the [WAMP Specification](http://wamp.ws/spec/). We provide [Crossbar.io](http://crossbar.io) but there are [other options](http://wamp.ws/implementations/#routers) as well.
## Show me some code ## Show me some code
A simple WebSocket echo server: A simple WebSocket echo server:
```python ```python
from autobahn.twisted.websocket import WebSocketServerProtocol
# or: from autobahn.asyncio.websocket import WebSocketServerProtocol
class MyServerProtocol(WebSocketServerProtocol): class MyServerProtocol(WebSocketServerProtocol):
def onConnect(self, request): def onConnect(self, request):
@ -50,6 +56,8 @@ class MyServerProtocol(WebSocketServerProtocol):
... and a sample WAMP application component: ... and a sample WAMP application component:
```python ```python
from autobahn.twisted.wamp import ApplicationSession
# or: from autobahn.asyncio.wamp import ApplicationSession
class MyComponent(ApplicationSession): class MyComponent(ApplicationSession):

View File

@ -24,5 +24,5 @@
# #
############################################################################### ###############################################################################
__version__ = "0.10.2" __version__ = "0.10.5-2"
version = __version__ # backward compat. version = __version__ # backward compat.

View File

@ -25,6 +25,7 @@
############################################################################### ###############################################################################
from __future__ import absolute_import from __future__ import absolute_import
import signal
from autobahn.wamp import protocol from autobahn.wamp import protocol
from autobahn.wamp.types import ComponentConfig from autobahn.wamp.types import ComponentConfig
@ -33,94 +34,28 @@ from autobahn.asyncio.websocket import WampWebSocketClientFactory
try: try:
import asyncio import asyncio
from asyncio import iscoroutine
from asyncio import Future
except ImportError: except ImportError:
# Trollius >= 0.3 was renamed # Trollius >= 0.3 was renamed to asyncio
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
import trollius as asyncio import trollius as asyncio
from trollius import iscoroutine
from trollius import Future import txaio
txaio.use_asyncio()
__all__ = ( __all__ = (
'FutureMixin',
'ApplicationSession', 'ApplicationSession',
'ApplicationSessionFactory', 'ApplicationSessionFactory',
'ApplicationRunner' 'ApplicationRunner'
) )
class FutureMixin(object): class ApplicationSession(protocol.ApplicationSession):
"""
Mixin for Asyncio style Futures.
"""
@staticmethod
def _create_future():
return Future()
@staticmethod
def _create_future_success(result=None):
f = Future()
f.set_result(result)
return f
@staticmethod
def _create_future_error(error=None):
f = Future()
f.set_exception(error)
return f
@staticmethod
def _as_future(fun, *args, **kwargs):
try:
res = fun(*args, **kwargs)
except Exception as e:
f = Future()
f.set_exception(e)
return f
else:
if isinstance(res, Future):
return res
elif iscoroutine(res):
return asyncio.Task(res)
else:
f = Future()
f.set_result(res)
return f
@staticmethod
def _resolve_future(future, result=None):
future.set_result(result)
@staticmethod
def _reject_future(future, error):
future.set_exception(error)
@staticmethod
def _add_future_callbacks(future, callback, errback):
def done(f):
try:
res = f.result()
if callback:
callback(res)
except Exception as e:
if errback:
errback(e)
return future.add_done_callback(done)
@staticmethod
def _gather_futures(futures, consume_exceptions=True):
return asyncio.gather(*futures, return_exceptions=consume_exceptions)
class ApplicationSession(FutureMixin, protocol.ApplicationSession):
""" """
WAMP application session for asyncio-based applications. WAMP application session for asyncio-based applications.
""" """
class ApplicationSessionFactory(FutureMixin, protocol.ApplicationSessionFactory): class ApplicationSessionFactory(protocol.ApplicationSessionFactory):
""" """
WAMP application session factory for asyncio-based applications. WAMP application session factory for asyncio-based applications.
""" """
@ -141,24 +76,36 @@ class ApplicationRunner(object):
""" """
def __init__(self, url, realm, extra=None, serializers=None, def __init__(self, url, realm, extra=None, serializers=None,
debug=False, debug_wamp=False, debug_app=False): debug=False, debug_wamp=False, debug_app=False,
ssl=None):
""" """
:param url: The WebSocket URL of the WAMP router to connect to (e.g. `ws://somehost.com:8090/somepath`) :param url: The WebSocket URL of the WAMP router to connect to (e.g. `ws://somehost.com:8090/somepath`)
:type url: unicode :type url: unicode
:param realm: The WAMP realm to join the application session to. :param realm: The WAMP realm to join the application session to.
:type realm: unicode :type realm: unicode
:param extra: Optional extra configuration to forward to the application component. :param extra: Optional extra configuration to forward to the application component.
:type extra: dict :type extra: dict
:param serializers: A list of WAMP serializers to use (or None for default serializers). :param serializers: A list of WAMP serializers to use (or None for default serializers).
Serializers must implement :class:`autobahn.wamp.interfaces.ISerializer`. Serializers must implement :class:`autobahn.wamp.interfaces.ISerializer`.
:type serializers: list :type serializers: list
:param debug: Turn on low-level debugging. :param debug: Turn on low-level debugging.
:type debug: bool :type debug: bool
:param debug_wamp: Turn on WAMP-level debugging. :param debug_wamp: Turn on WAMP-level debugging.
:type debug_wamp: bool :type debug_wamp: bool
:param debug_app: Turn on app-level debugging. :param debug_app: Turn on app-level debugging.
:type debug_app: bool :type debug_app: bool
:param ssl: An (optional) SSL context instance or a bool. See
the documentation for the `loop.create_connection` asyncio
method, to which this value is passed as the ``ssl=``
kwarg.
:type ssl: :class:`ssl.SSLContext` or bool
""" """
self.url = url self.url = url
self.realm = realm self.realm = realm
@ -168,6 +115,7 @@ class ApplicationRunner(object):
self.debug_app = debug_app self.debug_app = debug_app
self.make = None self.make = None
self.serializers = serializers self.serializers = serializers
self.ssl = ssl
def run(self, make): def run(self, make):
""" """
@ -192,15 +140,37 @@ class ApplicationRunner(object):
isSecure, host, port, resource, path, params = parseWsUrl(self.url) isSecure, host, port, resource, path, params = parseWsUrl(self.url)
if self.ssl is None:
ssl = isSecure
else:
if self.ssl and not isSecure:
raise RuntimeError(
'ssl argument value passed to %s conflicts with the "ws:" '
'prefix of the url argument. Did you mean to use "wss:"?' %
self.__class__.__name__)
ssl = self.ssl
# 2) create a WAMP-over-WebSocket transport client factory # 2) create a WAMP-over-WebSocket transport client factory
transport_factory = WampWebSocketClientFactory(create, url=self.url, serializers=self.serializers, transport_factory = WampWebSocketClientFactory(create, url=self.url, serializers=self.serializers,
debug=self.debug, debug_wamp=self.debug_wamp) debug=self.debug, debug_wamp=self.debug_wamp)
# 3) start the client # 3) start the client
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
coro = loop.create_connection(transport_factory, host, port, ssl=isSecure) txaio.use_asyncio()
loop.run_until_complete(coro) txaio.config.loop = loop
coro = loop.create_connection(transport_factory, host, port, ssl=ssl)
(transport, protocol) = loop.run_until_complete(coro)
loop.add_signal_handler(signal.SIGTERM, loop.stop)
# 4) now enter the asyncio event loop # 4) now enter the asyncio event loop
loop.run_forever() try:
loop.run_forever()
except KeyboardInterrupt:
# wait until we send Goodbye if user hit ctrl-c
# (done outside this except so SIGTERM gets the same handling)
pass
# give Goodbye message a chance to go through, if we still
# have an active session
if protocol._session:
loop.run_until_complete(protocol._session.leave())
loop.close() loop.close()

View File

@ -41,6 +41,9 @@ except ImportError:
from trollius import iscoroutine from trollius import iscoroutine
from trollius import Future from trollius import Future
from autobahn.logger import make_logger
__all__ = ( __all__ = (
'WebSocketAdapterProtocol', 'WebSocketAdapterProtocol',
'WebSocketServerProtocol', 'WebSocketServerProtocol',
@ -212,12 +215,7 @@ class WebSocketAdapterFactory(object):
""" """
Adapter class for asyncio-based WebSocket client and server factories. Adapter class for asyncio-based WebSocket client and server factories.
""" """
log = make_logger()
def _log(self, msg):
print(msg)
def _callLater(self, delay, fun):
return self.loop.call_later(delay, fun)
def __call__(self): def __call__(self):
proto = self.protocol() proto = self.protocol()

View File

@ -22,21 +22,19 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
# #
############################################################################### ##############################################################################
import binascii from __future__ import absolute_import
from autobahn.wamp.serializer import JsonObjectSerializer, MsgPackObjectSerializer
# ser = JsonObjectSerializer(batched = True)
ser = MsgPackObjectSerializer(batched=True)
o1 = [1, "hello", [1, 2, 3]] def make_logger(logger_type=None):
o2 = [3, {"a": 23, "b": 24}] if logger_type == "twisted":
# If we've been asked for the Twisted logger, try and get the new one
try:
from twisted.logger import Logger
return Logger()
except ImportError:
pass
d1 = ser.serialize(o1) + ser.serialize(o2) + ser.serialize(o1) from logging import getLogger
return getLogger()
m = ser.unserialize(d1)
print m

View File

@ -39,6 +39,10 @@ def install_optimal_reactor(verbose=False):
""" """
import sys import sys
from twisted.python import reflect from twisted.python import reflect
import txaio
txaio.use_twisted() # just to be sure...
# XXX should I configure txaio.config.loop in here too, or just in
# install_reactor()? (I am: see bottom of function)
# determine currently installed reactor, if any # determine currently installed reactor, if any
## ##
@ -110,6 +114,9 @@ def install_optimal_reactor(verbose=False):
except Exception as e: except Exception as e:
print("WARNING: Could not install default Twisted reactor for this platform ({0}).".format(e)) print("WARNING: Could not install default Twisted reactor for this platform ({0}).".format(e))
from twisted.internet import reactor
txaio.config.loop = reactor
def install_reactor(explicitReactor=None, verbose=False): def install_reactor(explicitReactor=None, verbose=False):
""" """
@ -121,6 +128,8 @@ def install_reactor(explicitReactor=None, verbose=False):
:type verbose: bool :type verbose: bool
""" """
import sys import sys
import txaio
txaio.use_twisted() # just to be sure...
if explicitReactor: if explicitReactor:
# install explicitly given reactor # install explicitly given reactor
@ -141,6 +150,7 @@ def install_reactor(explicitReactor=None, verbose=False):
# now the reactor is installed, import it # now the reactor is installed, import it
from twisted.internet import reactor from twisted.internet import reactor
txaio.config.loop = reactor
if verbose: if verbose:
from twisted.python.reflect import qual from twisted.python.reflect import qual

View File

@ -1,4 +1,4 @@
############################################################################### ########################################
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
@ -22,7 +22,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
# #
############################################################################### ########################################
from __future__ import absolute_import from __future__ import absolute_import
@ -76,7 +76,7 @@ class WampLongPollResourceSessionSend(Resource):
""" """
payload = request.content.read() payload = request.content.read()
if self._debug: if self._debug:
log.msg("WampLongPoll: receiving data for transport '{0}'\n{1}".format(self._parent._transportid, binascii.hexlify(payload))) log.msg("WampLongPoll: receiving data for transport '{0}'\n{1}".format(self._parent._transport_id, binascii.hexlify(payload)))
try: try:
# process (batch of) WAMP message(s) # process (batch of) WAMP message(s)
@ -115,7 +115,7 @@ class WampLongPollResourceSessionReceive(Resource):
if self._debug: if self._debug:
def logqueue(): def logqueue():
if not self._killed: if not self._killed:
log.msg("WampLongPoll: transport '{0}' - currently polled {1}, pending messages {2}".format(self._parent._transportid, self._request is not None, len(self._queue))) log.msg("WampLongPoll: transport '{0}' - currently polled {1}, pending messages {2}".format(self._parent._transport_id, self._request is not None, len(self._queue)))
self.reactor.callLater(1, logqueue) self.reactor.callLater(1, logqueue)
logqueue() logqueue()
@ -173,7 +173,7 @@ class WampLongPollResourceSessionReceive(Resource):
def cancel(_): def cancel(_):
if self._debug: if self._debug:
log.msg("WampLongPoll: poll request for transport '{0}' has gone away".format(self._parent._transportid)) log.msg("WampLongPoll: poll request for transport '{0}' has gone away".format(self._parent._transport_id))
self._request = None self._request = None
request.notifyFinish().addErrback(cancel) request.notifyFinish().addErrback(cancel)
@ -205,13 +205,13 @@ class WampLongPollResourceSessionClose(Resource):
by issuing a HTTP/POST with empty body to this resource. by issuing a HTTP/POST with empty body to this resource.
""" """
if self._debug: if self._debug:
log.msg("WampLongPoll: closing transport '{0}'".format(self._parent._transportid)) log.msg("WampLongPoll: closing transport '{0}'".format(self._parent._transport_id))
# now actually close the session # now actually close the session
self._parent.close() self._parent.close()
if self._debug: if self._debug:
log.msg("WampLongPoll: session ended and transport {0} closed".format(self._parent._transportid)) log.msg("WampLongPoll: session ended and transport {0} closed".format(self._parent._transport_id))
request.setResponseCode(http.NO_CONTENT) request.setResponseCode(http.NO_CONTENT)
self._parent._parent._setStandardHeaders(request) self._parent._parent._setStandardHeaders(request)
@ -223,14 +223,14 @@ class WampLongPollResourceSession(Resource):
A Web resource representing an open WAMP session. A Web resource representing an open WAMP session.
""" """
def __init__(self, parent, transportid, serializer): def __init__(self, parent, transport_details):
""" """
Create a new Web resource representing a WAMP session. Create a new Web resource representing a WAMP session.
:param parent: The parent Web resource. :param parent: The parent Web resource.
:type parent: Instance of :class:`autobahn.twisted.longpoll.WampLongPollResource`. :type parent: Instance of :class:`autobahn.twisted.longpoll.WampLongPollResource`.
:param serializer: The WAMP serializer in use for this session. :param transport_details: Details on the WAMP-over-Longpoll transport session.
:type serializer: An object that implements :class:`autobahn.wamp.interfaces.ISerializer`. :type transport_details: dict
""" """
Resource.__init__(self) Resource.__init__(self)
@ -239,15 +239,16 @@ class WampLongPollResourceSession(Resource):
self._debug_wamp = True self._debug_wamp = True
self.reactor = self._parent.reactor self.reactor = self._parent.reactor
self._transportid = transportid self._transport_id = transport_details['transport']
self._serializer = serializer self._serializer = transport_details['serializer']
self._session = None self._session = None
# session authentication information # session authentication information
## #
self._authid = None self._authid = None
self._authrole = None self._authrole = None
self._authmethod = None self._authmethod = None
self._authprovider = None
self._send = WampLongPollResourceSessionSend(self) self._send = WampLongPollResourceSessionSend(self)
self._receive = WampLongPollResourceSessionReceive(self) self._receive = WampLongPollResourceSessionReceive(self)
@ -260,21 +261,21 @@ class WampLongPollResourceSession(Resource):
self._isalive = False self._isalive = False
# kill inactive sessions after this timeout # kill inactive sessions after this timeout
## #
killAfter = self._parent._killAfter killAfter = self._parent._killAfter
if killAfter > 0: if killAfter > 0:
def killIfDead(): def killIfDead():
if not self._isalive: if not self._isalive:
if self._debug: if self._debug:
log.msg("WampLongPoll: killing inactive WAMP session with transport '{0}'".format(self._transportid)) log.msg("WampLongPoll: killing inactive WAMP session with transport '{0}'".format(self._transport_id))
self.onClose(False, 5000, "session inactive") self.onClose(False, 5000, "session inactive")
self._receive._kill() self._receive._kill()
if self._transportid in self._parent._transports: if self._transport_id in self._parent._transports:
del self._parent._transports[self._transportid] del self._parent._transports[self._transport_id]
else: else:
if self._debug: if self._debug:
log.msg("WampLongPoll: transport '{0}' is still alive".format(self._transportid)) log.msg("WampLongPoll: transport '{0}' is still alive".format(self._transport_id))
self._isalive = False self._isalive = False
self.reactor.callLater(killAfter, killIfDead) self.reactor.callLater(killAfter, killIfDead)
@ -282,10 +283,10 @@ class WampLongPollResourceSession(Resource):
self.reactor.callLater(killAfter, killIfDead) self.reactor.callLater(killAfter, killIfDead)
else: else:
if self._debug: if self._debug:
log.msg("WampLongPoll: transport '{0}' automatic killing of inactive session disabled".format(self._transportid)) log.msg("WampLongPoll: transport '{0}' automatic killing of inactive session disabled".format(self._transport_id))
if self._debug: if self._debug:
log.msg("WampLongPoll: session resource for transport '{0}' initialized)".format(self._transportid)) log.msg("WampLongPoll: session resource for transport '{0}' initialized)".format(self._transport_id))
self.onOpen() self.onOpen()
@ -296,7 +297,7 @@ class WampLongPollResourceSession(Resource):
if self.isOpen(): if self.isOpen():
self.onClose(True, 1000, u"session closed") self.onClose(True, 1000, u"session closed")
self._receive._kill() self._receive._kill()
del self._parent._transports[self._transportid] del self._parent._transports[self._transport_id]
else: else:
raise TransportLost() raise TransportLost()
@ -307,7 +308,7 @@ class WampLongPollResourceSession(Resource):
if self.isOpen(): if self.isOpen():
self.onClose(True, 1000, u"session aborted") self.onClose(True, 1000, u"session aborted")
self._receive._kill() self._receive._kill()
del self._parent._transports[self._transportid] del self._parent._transports[self._transport_id]
else: else:
raise TransportLost() raise TransportLost()
@ -405,7 +406,7 @@ class WampLongPollResourceOpen(Resource):
return self._parent._failRequest(request, "missing attribute 'protocols' in WAMP session open request") return self._parent._failRequest(request, "missing attribute 'protocols' in WAMP session open request")
# determine the protocol to speak # determine the protocol to speak
## #
protocol = None protocol = None
serializer = None serializer = None
for p in options['protocols']: for p in options['protocols']:
@ -419,19 +420,36 @@ class WampLongPollResourceOpen(Resource):
return self.__failRequest(request, "no common protocol to speak (I speak: {0})".format(["wamp.2.{0}".format(s) for s in self._parent._serializers.keys()])) return self.__failRequest(request, "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 # make up new transport ID
## #
if self._parent._debug_transport_id: if self._parent._debug_transport_id:
# use fixed transport ID for debugging purposes # use fixed transport ID for debugging purposes
transport = self._parent._debug_transport_id transport = self._parent._debug_transport_id
else: else:
transport = newid() transport = newid()
# 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 .. # create instance of WampLongPollResourceSession or subclass thereof ..
## #
self._parent._transports[transport] = self._parent.protocol(self._parent, transport, serializer) self._parent._transports[transport] = self._parent.protocol(self._parent, transport_details)
# create response # create response
## #
self._parent._setStandardHeaders(request) self._parent._setStandardHeaders(request)
request.setHeader('content-type', 'application/json; charset=utf-8') request.setHeader('content-type', 'application/json; charset=utf-8')
@ -549,7 +567,7 @@ class WampLongPollResource(Resource):
self._transports = {} self._transports = {}
# <Base URL>/open # <Base URL>/open
## #
self.putChild("open", WampLongPollResourceOpen(self)) self.putChild("open", WampLongPollResourceOpen(self))
if self._debug: if self._debug:

View File

@ -51,10 +51,10 @@ class WampRawSocketProtocol(Int32StringReceiver):
def connectionMade(self): def connectionMade(self):
if self.factory.debug: if self.factory.debug:
log.msg("WAMP-over-RawSocket connection made") log.msg("WampRawSocketProtocol: connection made")
# the peer we are connected to # the peer we are connected to
## #
try: try:
peer = self.transport.getPeer() peer = self.transport.getPeer()
except AttributeError: except AttributeError:
@ -63,44 +63,71 @@ class WampRawSocketProtocol(Int32StringReceiver):
else: else:
self.peer = peer2str(peer) self.peer = peer2str(peer)
# this will hold an ApplicationSession object
# once the RawSocket opening handshake has been
# completed
#
self._session = None
# Will hold the negotiated serializer once the opening handshake is complete
#
self._serializer = None
# Will be set to True once the opening handshake is complete
#
self._handshake_complete = False
# Buffer for opening handshake received bytes.
#
self._handshake_bytes = b''
# Clinet requested maximum length of serialized messages.
#
self._max_len_send = None
def _on_handshake_complete(self):
try: try:
self._session = self.factory._factory() self._session = self.factory._factory()
self._session.onOpen(self) self._session.onOpen(self)
except Exception as e: except Exception as e:
# Exceptions raised in onOpen are fatal .. # Exceptions raised in onOpen are fatal ..
if self.factory.debug: if self.factory.debug:
log.msg("ApplicationSession constructor / onOpen raised ({0})".format(e)) log.msg("WampRawSocketProtocol: ApplicationSession constructor / onOpen raised ({0})".format(e))
self.abort() self.abort()
else:
if self.factory.debug:
log.msg("ApplicationSession started.")
def connectionLost(self, reason): def connectionLost(self, reason):
if self.factory.debug: if self.factory.debug:
log.msg("WAMP-over-RawSocket connection lost: reason = '{0}'".format(reason)) log.msg("WampRawSocketProtocol: connection lost: reason = '{0}'".format(reason))
try: try:
wasClean = isinstance(reason.value, ConnectionDone) wasClean = isinstance(reason.value, ConnectionDone)
self._session.onClose(wasClean) self._session.onClose(wasClean)
except Exception as e: except Exception as e:
# silently ignore exceptions raised here .. # silently ignore exceptions raised here ..
if self.factory.debug: if self.factory.debug:
log.msg("ApplicationSession.onClose raised ({0})".format(e)) log.msg("WampRawSocketProtocol: ApplicationSession.onClose raised ({0})".format(e))
self._session = None self._session = None
def stringReceived(self, payload): def stringReceived(self, payload):
if self.factory.debug: if self.factory.debug:
log.msg("RX octets: {0}".format(binascii.hexlify(payload))) log.msg("WampRawSocketProtocol: RX octets: {0}".format(binascii.hexlify(payload)))
try: try:
for msg in self.factory._serializer.unserialize(payload): for msg in self._serializer.unserialize(payload):
if self.factory.debug: if self.factory.debug:
log.msg("RX WAMP message: {0}".format(msg)) log.msg("WampRawSocketProtocol: RX WAMP message: {0}".format(msg))
self._session.onMessage(msg) self._session.onMessage(msg)
except ProtocolError as e: except ProtocolError as e:
log.msg(str(e))
if self.factory.debug: if self.factory.debug:
log.msg("WAMP Protocol Error ({0}) - aborting connection".format(e)) log.msg("WampRawSocketProtocol: WAMP Protocol Error ({0}) - aborting connection".format(e))
self.abort() self.abort()
except Exception as e: except Exception as e:
if self.factory.debug: if self.factory.debug:
log.msg("WAMP Internal Error ({0}) - aborting connection".format(e)) log.msg("WampRawSocketProtocol: WAMP Internal Error ({0}) - aborting connection".format(e))
self.abort() self.abort()
def send(self, msg): def send(self, msg):
@ -109,16 +136,16 @@ class WampRawSocketProtocol(Int32StringReceiver):
""" """
if self.isOpen(): if self.isOpen():
if self.factory.debug: if self.factory.debug:
log.msg("TX WAMP message: {0}".format(msg)) log.msg("WampRawSocketProtocol: TX WAMP message: {0}".format(msg))
try: try:
payload, _ = self.factory._serializer.serialize(msg) payload, _ = self._serializer.serialize(msg)
except Exception as e: except Exception as e:
# all exceptions raised from above should be serialization errors .. # all exceptions raised from above should be serialization errors ..
raise SerializationError("Unable to serialize WAMP application payload ({0})".format(e)) raise SerializationError("WampRawSocketProtocol: unable to serialize WAMP application payload ({0})".format(e))
else: else:
self.sendString(payload) self.sendString(payload)
if self.factory.debug: if self.factory.debug:
log.msg("TX octets: {0}".format(binascii.hexlify(payload))) log.msg("WampRawSocketProtocol: TX octets: {0}".format(binascii.hexlify(payload)))
else: else:
raise TransportLost() raise TransportLost()
@ -156,33 +183,138 @@ class WampRawSocketServerProtocol(WampRawSocketProtocol):
Base class for Twisted-based WAMP-over-RawSocket server protocols. Base class for Twisted-based WAMP-over-RawSocket server protocols.
""" """
def dataReceived(self, data):
if self._handshake_complete:
WampRawSocketProtocol.dataReceived(self, data)
else:
remaining = 4 - len(self._handshake_bytes)
self._handshake_bytes += data[:remaining]
if len(self._handshake_bytes) == 4:
if self.factory.debug:
log.msg("WampRawSocketProtocol: opening handshake received - {0}".format(binascii.b2a_hex(self._handshake_bytes)))
if ord(self._handshake_bytes[0]) != 0x7f:
if self.factory.debug:
log.msg("WampRawSocketProtocol: invalid magic byte (octet 1) in opening handshake: was 0x{0}, but expected 0x7f".format(binascii.b2a_hex(self._handshake_bytes[0])))
self.abort()
# peer requests us to send messages of maximum length 2**max_len_exp
#
self._max_len_send = 2 ** (9 + (ord(self._handshake_bytes[1]) >> 4))
if self.factory.debug:
log.msg("WampRawSocketProtocol: client requests us to send out most {} bytes per message".format(self._max_len_send))
# client wants to speak this serialization format
#
ser_id = ord(self._handshake_bytes[1]) & 0x0F
if ser_id in self.factory._serializers:
self._serializer = self.factory._serializers[ser_id]
if self.factory.debug:
log.msg("WampRawSocketProtocol: client wants to use serializer {}".format(ser_id))
else:
if self.factory.debug:
log.msg("WampRawSocketProtocol: opening handshake - no suitable serializer found (client requested {0}, and we have {1})".format(ser_id, self.factory._serializers.keys()))
self.abort()
# we request the peer to send message of maximum length 2**reply_max_len_exp
#
reply_max_len_exp = 24
# send out handshake reply
#
reply_octet2 = chr(((reply_max_len_exp - 9) << 4) | self._serializer.RAWSOCKET_SERIALIZER_ID)
self.transport.write(b'\x7F') # magic byte
self.transport.write(reply_octet2) # max length / serializer
self.transport.write(b'\x00\x00') # reserved octets
self._handshake_complete = True
self._on_handshake_complete()
if self.factory.debug:
log.msg("WampRawSocketProtocol: opening handshake completed", self._serializer)
# consume any remaining data received already ..
#
data = data[remaining:]
if data:
self.dataReceived(data)
class WampRawSocketClientProtocol(WampRawSocketProtocol): class WampRawSocketClientProtocol(WampRawSocketProtocol):
""" """
Base class for Twisted-based WAMP-over-RawSocket client protocols. Base class for Twisted-based WAMP-over-RawSocket client protocols.
""" """
def connectionMade(self):
WampRawSocketProtocol.connectionMade(self)
self._serializer = self.factory._serializer
# we request the peer to send message of maximum length 2**reply_max_len_exp
#
request_max_len_exp = 24
# send out handshake reply
#
request_octet2 = chr(((request_max_len_exp - 9) << 4) | self._serializer.RAWSOCKET_SERIALIZER_ID)
self.transport.write(b'\x7F') # magic byte
self.transport.write(request_octet2) # max length / serializer
self.transport.write(b'\x00\x00') # reserved octets
def dataReceived(self, data):
if self._handshake_complete:
WampRawSocketProtocol.dataReceived(self, data)
else:
remaining = 4 - len(self._handshake_bytes)
self._handshake_bytes += data[:remaining]
if len(self._handshake_bytes) == 4:
if self.factory.debug:
log.msg("WampRawSocketProtocol: opening handshake received - {0}".format(binascii.b2a_hex(self._handshake_bytes)))
if ord(self._handshake_bytes[0]) != 0x7f:
if self.factory.debug:
log.msg("WampRawSocketProtocol: invalid magic byte (octet 1) in opening handshake: was 0x{0}, but expected 0x7f".format(binascii.b2a_hex(self._handshake_bytes[0])))
self.abort()
# peer requests us to send messages of maximum length 2**max_len_exp
#
self._max_len_send = 2 ** (9 + (ord(self._handshake_bytes[1]) >> 4))
if self.factory.debug:
log.msg("WampRawSocketProtocol: server requests us to send out most {} bytes per message".format(self._max_len_send))
# client wants to speak this serialization format
#
ser_id = ord(self._handshake_bytes[1]) & 0x0F
if ser_id != self._serializer.RAWSOCKET_SERIALIZER_ID:
if self.factory.debug:
log.msg("WampRawSocketProtocol: opening handshake - no suitable serializer found (server replied {0}, and we requested {1})".format(ser_id, self._serializer.RAWSOCKET_SERIALIZER_ID))
self.abort()
self._handshake_complete = True
self._on_handshake_complete()
if self.factory.debug:
log.msg("WampRawSocketProtocol: opening handshake completed", self._serializer)
# consume any remaining data received already ..
#
data = data[remaining:]
if data:
self.dataReceived(data)
class WampRawSocketFactory(Factory): class WampRawSocketFactory(Factory):
""" """
Base class for Twisted-based WAMP-over-RawSocket factories. Base class for Twisted-based WAMP-over-RawSocket factories.
""" """
def __init__(self, factory, serializer, debug=False):
"""
:param factory: A callable that produces instances that implement
:class:`autobahn.wamp.interfaces.ITransportHandler`
:type factory: callable
:param serializer: A WAMP serializer to use. A serializer must implement
:class:`autobahn.wamp.interfaces.ISerializer`.
:type serializer: obj
"""
assert(callable(factory))
self._factory = factory
self._serializer = serializer
self.debug = debug
class WampRawSocketServerFactory(WampRawSocketFactory): class WampRawSocketServerFactory(WampRawSocketFactory):
""" """
@ -190,9 +322,89 @@ class WampRawSocketServerFactory(WampRawSocketFactory):
""" """
protocol = WampRawSocketServerProtocol protocol = WampRawSocketServerProtocol
def __init__(self, factory, serializers=None, debug=False):
"""
:param factory: A callable that produces instances that implement
:class:`autobahn.wamp.interfaces.ITransportHandler`
:type factory: callable
:param serializers: A list of WAMP serializers to use (or None for default
serializers). Serializers must implement
:class:`autobahn.wamp.interfaces.ISerializer`.
:type serializers: list
"""
assert(callable(factory))
self._factory = factory
self.debug = debug
if serializers is None:
serializers = []
# try MsgPack WAMP serializer
try:
from autobahn.wamp.serializer import MsgPackSerializer
serializers.append(MsgPackSerializer(batched=True))
serializers.append(MsgPackSerializer())
except ImportError:
pass
# try JSON WAMP serializer
try:
from autobahn.wamp.serializer import JsonSerializer
serializers.append(JsonSerializer(batched=True))
serializers.append(JsonSerializer())
except ImportError:
pass
if not serializers:
raise Exception("could not import any WAMP serializers")
self._serializers = {}
for ser in serializers:
self._serializers[ser.RAWSOCKET_SERIALIZER_ID] = ser
class WampRawSocketClientFactory(WampRawSocketFactory): class WampRawSocketClientFactory(WampRawSocketFactory):
""" """
Base class for Twisted-based WAMP-over-RawSocket client factories. Base class for Twisted-based WAMP-over-RawSocket client factories.
""" """
protocol = WampRawSocketClientProtocol protocol = WampRawSocketClientProtocol
def __init__(self, factory, serializer=None, debug=False):
"""
:param factory: A callable that produces instances that implement
:class:`autobahn.wamp.interfaces.ITransportHandler`
:type factory: callable
:param serializer: The WAMP serializer to use (or None for default
serializer). Serializers must implement
:class:`autobahn.wamp.interfaces.ISerializer`.
:type serializer: obj
"""
assert(callable(factory))
self._factory = factory
self.debug = debug
if serializer is None:
# try MsgPack WAMP serializer
try:
from autobahn.wamp.serializer import MsgPackSerializer
serializer = MsgPackSerializer()
except ImportError:
pass
if serializer is None:
# try JSON WAMP serializer
try:
from autobahn.wamp.serializer import JsonSerializer
serializer = JsonSerializer()
except ImportError:
pass
if serializer is None:
raise Exception("could not import any WAMP serializer")
self._serializer = serializer

View File

@ -34,13 +34,14 @@ except ImportError:
# starting from Twisted 12.2, NoResource has moved # starting from Twisted 12.2, NoResource has moved
from twisted.web.resource import NoResource from twisted.web.resource import NoResource
from twisted.web.resource import IResource, Resource from twisted.web.resource import IResource, Resource
from six import PY3
# The following imports reactor at module level # The following imports reactor at module level
# See: https://twistedmatrix.com/trac/ticket/6849 # See: https://twistedmatrix.com/trac/ticket/6849
from twisted.web.http import HTTPChannel from twisted.web.http import HTTPChannel
# .. and this also, since it imports t.w.http # .. and this also, since it imports t.w.http
## #
from twisted.web.server import NOT_DONE_YET from twisted.web.server import NOT_DONE_YET
__all__ = ( __all__ = (
@ -139,21 +140,21 @@ class WebSocketResource(object):
and let that do any subsequent communication. and let that do any subsequent communication.
""" """
# Create Autobahn WebSocket protocol. # Create Autobahn WebSocket protocol.
## #
protocol = self._factory.buildProtocol(request.transport.getPeer()) protocol = self._factory.buildProtocol(request.transport.getPeer())
if not protocol: if not protocol:
# If protocol creation fails, we signal "internal server error" # If protocol creation fails, we signal "internal server error"
request.setResponseCode(500) request.setResponseCode(500)
return "" return b""
# Take over the transport from Twisted Web # Take over the transport from Twisted Web
## #
transport, request.transport = request.transport, None transport, request.transport = request.transport, None
# Connect the transport to our protocol. Once #3204 is fixed, there # Connect the transport to our protocol. Once #3204 is fixed, there
# may be a cleaner way of doing this. # may be a cleaner way of doing this.
# http://twistedmatrix.com/trac/ticket/3204 # http://twistedmatrix.com/trac/ticket/3204
## #
if isinstance(transport, ProtocolWrapper): if isinstance(transport, ProtocolWrapper):
# i.e. TLS is a wrapping protocol # i.e. TLS is a wrapping protocol
transport.wrappedProtocol = protocol transport.wrappedProtocol = protocol
@ -165,12 +166,21 @@ class WebSocketResource(object):
# silly (since Twisted Web already did the HTTP request parsing # silly (since Twisted Web already did the HTTP request parsing
# which we will do a 2nd time), but it's totally non-invasive to our # which we will do a 2nd time), but it's totally non-invasive to our
# code. Maybe improve this. # code. Maybe improve this.
## #
data = "%s %s HTTP/1.1\x0d\x0a" % (request.method, request.uri) if PY3:
for h in request.requestHeaders.getAllRawHeaders():
data += "%s: %s\x0d\x0a" % (h[0], ",".join(h[1])) data = request.method + b' ' + request.uri + b' HTTP/1.1\x0d\x0a'
data += "\x0d\x0a" for h in request.requestHeaders.getAllRawHeaders():
data += request.content.read() # we need this for Hixie-76 data += h[0] + b': ' + b",".join(h[1]) + b'\x0d\x0a'
data += b"\x0d\x0a"
data += request.content.read()
else:
data = "%s %s HTTP/1.1\x0d\x0a" % (request.method, request.uri)
for h in request.requestHeaders.getAllRawHeaders():
data += "%s: %s\x0d\x0a" % (h[0], ",".join(h[1]))
data += "\x0d\x0a"
data += request.content.read() # we need this for Hixie-76
protocol.dataReceived(data) protocol.dataReceived(data)
return NOT_DONE_YET return NOT_DONE_YET

View File

@ -52,12 +52,8 @@ if os.environ.get('USE_TWISTED', False):
self.assertRaises(RuntimeError, runner.run, raise_error) self.assertRaises(RuntimeError, runner.run, raise_error)
# both reactor.run and reactor.stop should have been called # both reactor.run and reactor.stop should have been called
run_calls = list(filter(lambda mc: mc[0] == 'run', fakereactor.run.assert_called()
fakereactor.method_calls)) fakereactor.stop.assert_called()
stop_calls = list(filter(lambda mc: mc[0] == 'stop',
fakereactor.method_calls))
self.assertEqual(len(run_calls), 1)
self.assertEqual(len(stop_calls), 1)
@patch('twisted.internet.reactor') @patch('twisted.internet.reactor')
@inlineCallbacks @inlineCallbacks
@ -75,12 +71,8 @@ if os.environ.get('USE_TWISTED', False):
# neither reactor.run() NOR reactor.stop() should have been called # neither reactor.run() NOR reactor.stop() should have been called
# (just connectTCP() will have been called) # (just connectTCP() will have been called)
run_calls = list(filter(lambda mc: mc[0] == 'run', fakereactor.run.assert_not_called()
fakereactor.method_calls)) fakereactor.stop.assert_not_called()
stop_calls = list(filter(lambda mc: mc[0] == 'stop',
fakereactor.method_calls))
self.assertEqual(len(run_calls), 0)
self.assertEqual(len(stop_calls), 0)
@patch('twisted.internet.reactor') @patch('twisted.internet.reactor')
def test_runner_no_run_happypath(self, fakereactor): def test_runner_no_run_happypath(self, fakereactor):
@ -92,18 +84,14 @@ if os.environ.get('USE_TWISTED', False):
# shouldn't have actually connected to anything # shouldn't have actually connected to anything
# successfully, and the run() call shouldn't have inserted # successfully, and the run() call shouldn't have inserted
# any of its own call/errbacks. # any of its own call/errbacks. (except the cleanup handler)
self.assertFalse(d.called) self.assertFalse(d.called)
self.assertEqual(0, len(d.callbacks)) self.assertEqual(1, len(d.callbacks))
# neither reactor.run() NOR reactor.stop() should have been called # neither reactor.run() NOR reactor.stop() should have been called
# (just connectTCP() will have been called) # (just connectTCP() will have been called)
run_calls = list(filter(lambda mc: mc[0] == 'run', fakereactor.run.assert_not_called()
fakereactor.method_calls)) fakereactor.stop.assert_not_called()
stop_calls = list(filter(lambda mc: mc[0] == 'stop',
fakereactor.method_calls))
self.assertEqual(len(run_calls), 0)
self.assertEqual(len(stop_calls), 0)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -30,75 +30,35 @@ import sys
import inspect import inspect
from twisted.python import log from twisted.python import log
from twisted.application import service from twisted.internet.defer import inlineCallbacks
from twisted.internet.defer import Deferred, \
maybeDeferred, \
DeferredList, \
inlineCallbacks, \
succeed, \
fail
from autobahn.wamp import protocol from autobahn.wamp import protocol
from autobahn.wamp.types import ComponentConfig from autobahn.wamp.types import ComponentConfig
from autobahn.websocket.protocol import parseWsUrl from autobahn.websocket.protocol import parseWsUrl
from autobahn.twisted.websocket import WampWebSocketClientFactory from autobahn.twisted.websocket import WampWebSocketClientFactory
__all__ = ( import six
'FutureMixin', import txaio
txaio.use_twisted()
__all__ = [
'ApplicationSession', 'ApplicationSession',
'ApplicationSessionFactory', 'ApplicationSessionFactory',
'ApplicationRunner', 'ApplicationRunner',
'Application', 'Application',
'Service' 'Service'
) ]
try:
from twisted.application import service
except (ImportError, SyntaxError):
# Not on PY3 yet
service = None
__all__.pop(__all__.index('Service'))
class FutureMixin(object): class ApplicationSession(protocol.ApplicationSession):
"""
Mixin for Twisted style Futures ("Deferreds").
"""
@staticmethod
def _create_future():
return Deferred()
@staticmethod
def _create_future_success(result=None):
return succeed(result)
@staticmethod
def _create_future_error(error=None):
return fail(error)
@staticmethod
def _as_future(fun, *args, **kwargs):
return maybeDeferred(fun, *args, **kwargs)
@staticmethod
def _resolve_future(future, result=None):
future.callback(result)
@staticmethod
def _reject_future(future, error):
future.errback(error)
@staticmethod
def _add_future_callbacks(future, callback, errback):
# callback and/or errback may be None
if callback is None:
assert errback is not None
future.addErrback(errback)
return future
else:
future.addCallbacks(callback, errback)
return future
@staticmethod
def _gather_futures(futures, consume_exceptions=True):
return DeferredList(futures, consumeErrors=consume_exceptions)
class ApplicationSession(FutureMixin, protocol.ApplicationSession):
""" """
WAMP application session for Twisted-based applications. WAMP application session for Twisted-based applications.
""" """
@ -114,7 +74,7 @@ class ApplicationSession(FutureMixin, protocol.ApplicationSession):
log.err(msg) log.err(msg)
class ApplicationSessionFactory(FutureMixin, protocol.ApplicationSessionFactory): class ApplicationSessionFactory(protocol.ApplicationSessionFactory):
""" """
WAMP application session factory for Twisted-based applications. WAMP application session factory for Twisted-based applications.
""" """
@ -134,21 +94,34 @@ class ApplicationRunner(object):
connecting to a WAMP router. connecting to a WAMP router.
""" """
def __init__(self, url, realm, extra=None, debug=False, debug_wamp=False, debug_app=False): def __init__(self, url, realm, extra=None, debug=False, debug_wamp=False, debug_app=False, ssl=None):
""" """
:param url: The WebSocket URL of the WAMP router to connect to (e.g. `ws://somehost.com:8090/somepath`) :param url: The WebSocket URL of the WAMP router to connect to (e.g. `ws://somehost.com:8090/somepath`)
:type url: unicode :type url: unicode
:param realm: The WAMP realm to join the application session to. :param realm: The WAMP realm to join the application session to.
:type realm: unicode :type realm: unicode
:param extra: Optional extra configuration to forward to the application component. :param extra: Optional extra configuration to forward to the application component.
:type extra: dict :type extra: dict
:param debug: Turn on low-level debugging. :param debug: Turn on low-level debugging.
:type debug: bool :type debug: bool
:param debug_wamp: Turn on WAMP-level debugging. :param debug_wamp: Turn on WAMP-level debugging.
:type debug_wamp: bool :type debug_wamp: bool
:param debug_app: Turn on app-level debugging. :param debug_app: Turn on app-level debugging.
:type debug_app: bool :type debug_app: bool
:param ssl: (Optional). If specified this should be an
instance suitable to pass as ``sslContextFactory`` to
:class:`twisted.internet.endpoints.SSL4ClientEndpoint`` such
as :class:`twisted.internet.ssl.CertificateOptions`. Leaving
it as ``None`` will use the result of calling Twisted's
:meth:`twisted.internet.ssl.platformTrust` which tries to use
your distribution's CA certificates.
:type ssl: :class:`twisted.internet.ssl.CertificateOptions`
""" """
self.url = url self.url = url
self.realm = realm self.realm = realm
@ -157,6 +130,7 @@ class ApplicationRunner(object):
self.debug_wamp = debug_wamp self.debug_wamp = debug_wamp
self.debug_app = debug_app self.debug_app = debug_app
self.make = None self.make = None
self.ssl = ssl
def run(self, make, start_reactor=True): def run(self, make, start_reactor=True):
""" """
@ -179,6 +153,8 @@ class ApplicationRunner(object):
of :class:`WampWebSocketClientProtocol` of :class:`WampWebSocketClientProtocol`
""" """
from twisted.internet import reactor from twisted.internet import reactor
txaio.use_twisted()
txaio.config.loop = reactor
isSecure, host, port, resource, path, params = parseWsUrl(self.url) isSecure, host, port, resource, path, params = parseWsUrl(self.url)
@ -208,17 +184,40 @@ class ApplicationRunner(object):
transport_factory = WampWebSocketClientFactory(create, url=self.url, transport_factory = WampWebSocketClientFactory(create, url=self.url,
debug=self.debug, debug_wamp=self.debug_wamp) debug=self.debug, debug_wamp=self.debug_wamp)
# start the client from a Twisted endpoint # if user passed ssl= but isn't using isSecure, we'll never
from twisted.internet.endpoints import clientFromString # use the ssl argument which makes no sense.
context_factory = None
if self.ssl is not None:
if not isSecure:
raise RuntimeError(
'ssl= argument value passed to %s conflicts with the "ws:" '
'prefix of the url argument. Did you mean to use "wss:"?' %
self.__class__.__name__)
context_factory = self.ssl
elif isSecure:
from twisted.internet.ssl import optionsForClientTLS
context_factory = optionsForClientTLS(six.u(host))
if isSecure: if isSecure:
endpoint_descriptor = "ssl:{0}:{1}".format(host, port) from twisted.internet.endpoints import SSL4ClientEndpoint
assert context_factory is not None
client = SSL4ClientEndpoint(reactor, host, port, context_factory)
else: else:
endpoint_descriptor = "tcp:{0}:{1}".format(host, port) from twisted.internet.endpoints import TCP4ClientEndpoint
client = TCP4ClientEndpoint(reactor, host, port)
client = clientFromString(reactor, endpoint_descriptor)
d = client.connect(transport_factory) d = client.connect(transport_factory)
# as the reactor shuts down, we wish to wait until we've sent
# out our "Goodbye" message; leave() returns a Deferred that
# fires when the transport gets to STATE_CLOSED
def cleanup(proto):
if hasattr(proto, '_session') and proto._session is not None:
return proto._session.leave()
# if we connect successfully, the arg is a WampWebSocketClientProtocol
d.addCallback(lambda proto: reactor.addSystemEventTrigger(
'before', 'shutdown', cleanup, proto))
# if the user didn't ask us to start the reactor, then they # if the user didn't ask us to start the reactor, then they
# get to deal with any connect errors themselves. # get to deal with any connect errors themselves.
if start_reactor: if start_reactor:
@ -516,78 +515,81 @@ class Application(object):
log.msg("Warning: exception in signal handler swallowed", e) log.msg("Warning: exception in signal handler swallowed", e)
class Service(service.MultiService): if service:
""" # Don't define it if Twisted's service support isn't here
A WAMP application as a twisted service.
The application object provides a simple way of creating, debugging and running WAMP application
components inside a traditional twisted application
This manages application lifecycle of the wamp connection using startService and stopService class Service(service.MultiService):
Using services also allows to create integration tests that properly terminates their connections
It can host a WAMP application component in a WAMP-over-WebSocket client
connecting to a WAMP router.
"""
factory = WampWebSocketClientFactory
def __init__(self, url, realm, make, extra=None,
debug=False, debug_wamp=False, debug_app=False):
""" """
A WAMP application as a twisted service.
The application object provides a simple way of creating, debugging and running WAMP application
components inside a traditional twisted application
:param url: The WebSocket URL of the WAMP router to connect to (e.g. `ws://somehost.com:8090/somepath`) This manages application lifecycle of the wamp connection using startService and stopService
:type url: unicode Using services also allows to create integration tests that properly terminates their connections
:param realm: The WAMP realm to join the application session to.
:type realm: unicode
:param make: A factory that produces instances of :class:`autobahn.asyncio.wamp.ApplicationSession`
when called with an instance of :class:`autobahn.wamp.types.ComponentConfig`.
:type make: callable
:param extra: Optional extra configuration to forward to the application component.
:type extra: dict
:param debug: Turn on low-level debugging.
:type debug: bool
:param debug_wamp: Turn on WAMP-level debugging.
:type debug_wamp: bool
:param debug_app: Turn on app-level debugging.
:type debug_app: bool
You can replace the attribute factory in order to change connectionLost or connectionFailed behaviour. It can host a WAMP application component in a WAMP-over-WebSocket client
The factory attribute must return a WampWebSocketClientFactory object connecting to a WAMP router.
""" """
self.url = url factory = WampWebSocketClientFactory
self.realm = realm
self.extra = extra or dict()
self.debug = debug
self.debug_wamp = debug_wamp
self.debug_app = debug_app
self.make = make
service.MultiService.__init__(self)
self.setupService()
def setupService(self): def __init__(self, url, realm, make, extra=None,
""" debug=False, debug_wamp=False, debug_app=False):
Setup the application component. """
"""
isSecure, host, port, resource, path, params = parseWsUrl(self.url)
# factory for use ApplicationSession :param url: The WebSocket URL of the WAMP router to connect to (e.g. `ws://somehost.com:8090/somepath`)
def create(): :type url: unicode
cfg = ComponentConfig(self.realm, self.extra) :param realm: The WAMP realm to join the application session to.
session = self.make(cfg) :type realm: unicode
session.debug_app = self.debug_app :param make: A factory that produces instances of :class:`autobahn.asyncio.wamp.ApplicationSession`
return session when called with an instance of :class:`autobahn.wamp.types.ComponentConfig`.
:type make: callable
:param extra: Optional extra configuration to forward to the application component.
:type extra: dict
:param debug: Turn on low-level debugging.
:type debug: bool
:param debug_wamp: Turn on WAMP-level debugging.
:type debug_wamp: bool
:param debug_app: Turn on app-level debugging.
:type debug_app: bool
# create a WAMP-over-WebSocket transport client factory You can replace the attribute factory in order to change connectionLost or connectionFailed behaviour.
transport_factory = self.factory(create, url=self.url, The factory attribute must return a WampWebSocketClientFactory object
debug=self.debug, debug_wamp=self.debug_wamp) """
self.url = url
self.realm = realm
self.extra = extra or dict()
self.debug = debug
self.debug_wamp = debug_wamp
self.debug_app = debug_app
self.make = make
service.MultiService.__init__(self)
self.setupService()
# setup the client from a Twisted endpoint def setupService(self):
"""
Setup the application component.
"""
isSecure, host, port, resource, path, params = parseWsUrl(self.url)
if isSecure: # factory for use ApplicationSession
from twisted.application.internet import SSLClient def create():
clientClass = SSLClient cfg = ComponentConfig(self.realm, self.extra)
else: session = self.make(cfg)
from twisted.application.internet import TCPClient session.debug_app = self.debug_app
clientClass = TCPClient return session
client = clientClass(host, port, transport_factory) # create a WAMP-over-WebSocket transport client factory
client.setServiceParent(self) transport_factory = self.factory(create, url=self.url,
debug=self.debug, debug_wamp=self.debug_wamp)
# setup the client from a Twisted endpoint
if isSecure:
from twisted.application.internet import SSLClient
clientClass = SSLClient
else:
from twisted.application.internet import TCPClient
clientClass = TCPClient
client = clientClass(host, port, transport_factory)
client.setServiceParent(self)

View File

@ -32,7 +32,6 @@ from zope.interface import implementer
import twisted.internet.protocol import twisted.internet.protocol
from twisted.internet.defer import maybeDeferred from twisted.internet.defer import maybeDeferred
from twisted.python import log
from twisted.internet.interfaces import ITransport from twisted.internet.interfaces import ITransport
from autobahn.wamp import websocket from autobahn.wamp import websocket
@ -40,11 +39,14 @@ from autobahn.websocket import protocol
from autobahn.websocket import http from autobahn.websocket import http
from autobahn.twisted.util import peer2str from autobahn.twisted.util import peer2str
from autobahn.logger import make_logger
from autobahn.websocket.compress import PerMessageDeflateOffer, \ from autobahn.websocket.compress import PerMessageDeflateOffer, \
PerMessageDeflateOfferAccept, \ PerMessageDeflateOfferAccept, \
PerMessageDeflateResponse, \ PerMessageDeflateResponse, \
PerMessageDeflateResponseAccept PerMessageDeflateResponseAccept
__all__ = ( __all__ = (
'WebSocketAdapterProtocol', 'WebSocketAdapterProtocol',
'WebSocketServerProtocol', 'WebSocketServerProtocol',
@ -189,12 +191,7 @@ class WebSocketAdapterFactory(object):
""" """
Adapter class for Twisted-based WebSocket client and server factories. Adapter class for Twisted-based WebSocket client and server factories.
""" """
log = make_logger("twisted")
def _log(self, msg):
log.msg(msg)
def _callLater(self, delay, fun):
return self.reactor.callLater(delay, fun)
class WebSocketServerFactory(WebSocketAdapterFactory, protocol.WebSocketServerFactory, twisted.internet.protocol.ServerFactory): class WebSocketServerFactory(WebSocketAdapterFactory, protocol.WebSocketServerFactory, twisted.internet.protocol.ServerFactory):

View File

@ -58,7 +58,7 @@ def utcnow():
:rtype: unicode :rtype: unicode
""" """
now = datetime.utcnow() now = datetime.utcnow()
return now.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" return u"{0}Z".format(now.strftime(u"%Y-%m-%dT%H:%M:%S.%f")[:-3])
def utcstr(ts): def utcstr(ts):
@ -72,7 +72,7 @@ def utcstr(ts):
:rtype: unicode :rtype: unicode
""" """
if ts: if ts:
return ts.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" return u"{0}Z".format(ts.strftime(u"%Y-%m-%dT%H:%M:%S.%f")[:-3])
else: else:
return ts return ts
@ -92,11 +92,31 @@ def parseutc(datestr):
:rtype: instance of :py:class:`datetime.datetime` :rtype: instance of :py:class:`datetime.datetime`
""" """
try: try:
return datetime.strptime(datestr, "%Y-%m-%dT%H:%M:%SZ") return datetime.strptime(datestr, u"%Y-%m-%dT%H:%M:%SZ")
except ValueError: except ValueError:
return None return None
class IdGenerator(object):
"""
ID generator for WAMP request IDs.
WAMP request IDs are sequential per WAMP session, starting at 0 and
wrapping around at 2**53 (both value are inclusive [0, 2**53]).
See https://github.com/tavendo/WAMP/blob/master/spec/basic.md#ids
"""
def __init__(self):
self._next = -1
def next(self):
self._next += 1
if self._next > 9007199254740992:
self._next = 0
return self._next
# noinspection PyShadowingBuiltins # noinspection PyShadowingBuiltins
def id(): def id():
""" """
@ -111,8 +131,7 @@ def id():
:returns: A random object ID. :returns: A random object ID.
:rtype: int :rtype: int
""" """
# return random.randint(0, 9007199254740992) # this is what the WAMP spec says return random.randint(0, 9007199254740992)
return random.randint(0, 2147483647) # use a reduced ID space for now (2**31-1)
def newid(length=16): def newid(length=16):

View File

@ -303,6 +303,8 @@ class ISession(object):
:param message: An optional (human readable) closing message, intended for :param message: An optional (human readable) closing message, intended for
logging purposes. logging purposes.
:type message: str :type message: str
:return: may return a Future/Deferred that fires when we've disconnected
""" """
@abc.abstractmethod @abc.abstractmethod
@ -320,6 +322,12 @@ class ISession(object):
Close the underlying transport. Close the underlying transport.
""" """
@abc.abstractmethod
def is_connected(self):
"""
Check if the underlying transport is connected.
"""
@abc.abstractmethod @abc.abstractmethod
def onDisconnect(self): def onDisconnect(self):
""" """

View File

@ -76,6 +76,12 @@ _URI_PAT_STRICT_NON_EMPTY = re.compile(r"^([0-9a-z_]+\.)*([0-9a-z_]+)$")
# loose URI check disallowing empty URI components # loose URI check disallowing empty URI components
_URI_PAT_LOOSE_NON_EMPTY = re.compile(r"^([^\s\.#]+\.)*([^\s\.#]+)$") _URI_PAT_LOOSE_NON_EMPTY = re.compile(r"^([^\s\.#]+\.)*([^\s\.#]+)$")
# strict URI check disallowing empty URI components in all but the last component
_URI_PAT_STRICT_LAST_EMPTY = re.compile(r"^([0-9a-z_]+\.)*([0-9a-z_]*)$")
# loose URI check disallowing empty URI components in all but the last component
_URI_PAT_LOOSE_LAST_EMPTY = re.compile(r"^([^\s\.#]+\.)*([^\s\.#]*)$")
def check_or_raise_uri(value, message=u"WAMP message invalid", strict=False, allowEmptyComponents=False): def check_or_raise_uri(value, message=u"WAMP message invalid", strict=False, allowEmptyComponents=False):
""" """
@ -278,7 +284,6 @@ class Hello(Message):
if u'features' in details_role: if u'features' in details_role:
check_or_raise_extra(details_role[u'features'], "'features' in role '{0}' in 'roles' in 'details' in HELLO".format(role)) check_or_raise_extra(details_role[u'features'], "'features' in role '{0}' in 'roles' in 'details' in HELLO".format(role))
# FIXME: skip unknown attributes
role_features = role_cls(**details_role[u'features']) role_features = role_cls(**details_role[u'features'])
else: else:
@ -431,7 +436,6 @@ class Welcome(Message):
if u'features' in details_role: if u'features' in details_role:
check_or_raise_extra(details_role[u'features'], "'features' in role '{0}' in 'roles' in 'details' in WELCOME".format(role)) check_or_raise_extra(details_role[u'features'], "'features' in role '{0}' in 'roles' in 'details' in WELCOME".format(role))
# FIXME: skip unknown attributes
role_features = role_cls(**details_roles[role][u'features']) role_features = role_cls(**details_roles[role][u'features'])
else: else:

View File

@ -40,7 +40,6 @@ from autobahn.wamp.interfaces import ISession, \
IRegistration, \ IRegistration, \
ITransportHandler ITransportHandler
from autobahn import util
from autobahn import wamp from autobahn import wamp
from autobahn.wamp import uri from autobahn.wamp import uri
from autobahn.wamp import message from autobahn.wamp import message
@ -49,6 +48,9 @@ from autobahn.wamp import role
from autobahn.wamp import exception from autobahn.wamp import exception
from autobahn.wamp.exception import ApplicationError, ProtocolError, SessionNotReady, SerializationError from autobahn.wamp.exception import ApplicationError, ProtocolError, SessionNotReady, SerializationError
from autobahn.wamp.types import SessionDetails from autobahn.wamp.types import SessionDetails
from autobahn.util import IdGenerator
import txaio
def is_method_or_function(f): def is_method_or_function(f):
@ -280,6 +282,9 @@ class BaseSession(object):
self._authmethod = None self._authmethod = None
self._authprovider = None self._authprovider = None
# generator for WAMP request IDs
self._request_id_gen = IdGenerator()
def onConnect(self): def onConnect(self):
""" """
Implements :func:`autobahn.wamp.interfaces.ISession.onConnect` Implements :func:`autobahn.wamp.interfaces.ISession.onConnect`
@ -455,11 +460,11 @@ class ApplicationSession(BaseSession):
Implements :func:`autobahn.wamp.interfaces.ITransportHandler.onOpen` Implements :func:`autobahn.wamp.interfaces.ITransportHandler.onOpen`
""" """
self._transport = transport self._transport = transport
d = self._as_future(self.onConnect) d = txaio.as_future(self.onConnect)
def _error(e): def _error(e):
return self._swallow_error(e, "While firing onConnect") return self._swallow_error(e, "While firing onConnect")
self._add_future_callbacks(d, None, _error) txaio.add_callbacks(d, None, _error)
def onConnect(self): def onConnect(self):
""" """
@ -491,9 +496,12 @@ class ApplicationSession(BaseSession):
""" """
if self._transport: if self._transport:
self._transport.close() self._transport.close()
else:
# XXX or shall we just ignore this? def is_connected(self):
raise RuntimeError("No transport, but disconnect() called.") """
Implements :func:`autobahn.wamp.interfaces.ISession.is_connected`
"""
return self._transport is not None
def onUserError(self, e, msg): def onUserError(self, e, msg):
""" """
@ -544,26 +552,26 @@ class ApplicationSession(BaseSession):
self._session_id = msg.session self._session_id = msg.session
details = SessionDetails(self._realm, self._session_id, msg.authid, msg.authrole, msg.authmethod) details = SessionDetails(self._realm, self._session_id, msg.authid, msg.authrole, msg.authmethod)
d = self._as_future(self.onJoin, details) d = txaio.as_future(self.onJoin, details)
def _error(e): def _error(e):
return self._swallow_error(e, "While firing onJoin") return self._swallow_error(e, "While firing onJoin")
self._add_future_callbacks(d, None, _error) txaio.add_callbacks(d, None, _error)
elif isinstance(msg, message.Abort): elif isinstance(msg, message.Abort):
# fire callback and close the transport # fire callback and close the transport
details = types.CloseDetails(msg.reason, msg.message) details = types.CloseDetails(msg.reason, msg.message)
d = self._as_future(self.onLeave, details) d = txaio.as_future(self.onLeave, details)
def _error(e): def _error(e):
return self._swallow_error(e, "While firing onLeave") return self._swallow_error(e, "While firing onLeave")
self._add_future_callbacks(d, None, _error) txaio.add_callbacks(d, None, _error)
elif isinstance(msg, message.Challenge): elif isinstance(msg, message.Challenge):
challenge = types.Challenge(msg.method, msg.extra) challenge = types.Challenge(msg.method, msg.extra)
d = self._as_future(self.onChallenge, challenge) d = txaio.as_future(self.onChallenge, challenge)
def success(signature): def success(signature):
reply = message.Authenticate(signature) reply = message.Authenticate(signature)
@ -574,16 +582,16 @@ class ApplicationSession(BaseSession):
self._transport.send(reply) self._transport.send(reply)
# fire callback and close the transport # fire callback and close the transport
details = types.CloseDetails(reply.reason, reply.message) details = types.CloseDetails(reply.reason, reply.message)
d = self._as_future(self.onLeave, details) d = txaio.as_future(self.onLeave, details)
def _error(e): def _error(e):
return self._swallow_error(e, "While firing onLeave") return self._swallow_error(e, "While firing onLeave")
self._add_future_callbacks(d, None, _error) txaio.add_callbacks(d, None, _error)
# switching to the callback chain, effectively # switching to the callback chain, effectively
# cancelling error (which we've now handled) # cancelling error (which we've now handled)
return d return d
self._add_future_callbacks(d, success, error) txaio.add_callbacks(d, success, error)
else: else:
raise ProtocolError("Received {0} message, and session is not yet established".format(msg.__class__)) raise ProtocolError("Received {0} message, and session is not yet established".format(msg.__class__))
@ -600,12 +608,12 @@ class ApplicationSession(BaseSession):
# fire callback and close the transport # fire callback and close the transport
details = types.CloseDetails(msg.reason, msg.message) details = types.CloseDetails(msg.reason, msg.message)
d = self._as_future(self.onLeave, details) d = txaio.as_future(self.onLeave, details)
def _error(e): def _error(e):
errmsg = 'While firing onLeave for reason "{0}" and message "{1}"'.format(msg.reason, msg.message) errmsg = 'While firing onLeave for reason "{0}" and message "{1}"'.format(msg.reason, msg.message)
return self._swallow_error(e, errmsg) return self._swallow_error(e, errmsg)
self._add_future_callbacks(d, None, _error) txaio.add_callbacks(d, None, _error)
elif isinstance(msg, message.Event): elif isinstance(msg, message.Event):
@ -624,15 +632,13 @@ class ApplicationSession(BaseSession):
if handler.details_arg: if handler.details_arg:
invoke_kwargs[handler.details_arg] = types.EventDetails(publication=msg.publication, publisher=msg.publisher, topic=msg.topic) invoke_kwargs[handler.details_arg] = types.EventDetails(publication=msg.publication, publisher=msg.publisher, topic=msg.topic)
try: def _error(e):
handler.fn(*invoke_args, **invoke_kwargs) errmsg = 'While firing {0} subscribed under {1}.'.format(
except Exception as e:
msg = 'While firing {0} subscribed under {1}.'.format(
handler.fn, msg.subscription) handler.fn, msg.subscription)
try: return self._swallow_error(e, errmsg)
self.onUserError(e, msg)
except: future = txaio.as_future(handler.fn, *invoke_args, **invoke_kwargs)
pass txaio.add_callbacks(future, None, _error)
else: else:
raise ProtocolError("EVENT received for non-subscribed subscription ID {0}".format(msg.subscription)) raise ProtocolError("EVENT received for non-subscribed subscription ID {0}".format(msg.subscription))
@ -648,7 +654,7 @@ class ApplicationSession(BaseSession):
publication = Publication(msg.publication) publication = Publication(msg.publication)
# resolve deferred/future for publishing successfully # resolve deferred/future for publishing successfully
self._resolve_future(publish_request.on_reply, publication) txaio.resolve(publish_request.on_reply, publication)
else: else:
raise ProtocolError("PUBLISHED received for non-pending request ID {0}".format(msg.request)) raise ProtocolError("PUBLISHED received for non-pending request ID {0}".format(msg.request))
@ -669,7 +675,7 @@ class ApplicationSession(BaseSession):
self._subscriptions[msg.subscription].append(subscription) self._subscriptions[msg.subscription].append(subscription)
# resolve deferred/future for subscribing successfully # resolve deferred/future for subscribing successfully
self._resolve_future(request.on_reply, subscription) txaio.resolve(request.on_reply, subscription)
else: else:
raise ProtocolError("SUBSCRIBED received for non-pending request ID {0}".format(msg.request)) raise ProtocolError("SUBSCRIBED received for non-pending request ID {0}".format(msg.request))
@ -687,7 +693,7 @@ class ApplicationSession(BaseSession):
del self._subscriptions[request.subscription_id] del self._subscriptions[request.subscription_id]
# resolve deferred/future for unsubscribing successfully # resolve deferred/future for unsubscribing successfully
self._resolve_future(request.on_reply, 0) txaio.resolve(request.on_reply, 0)
else: else:
raise ProtocolError("UNSUBSCRIBED received for non-pending request ID {0}".format(msg.request)) raise ProtocolError("UNSUBSCRIBED received for non-pending request ID {0}".format(msg.request))
@ -727,16 +733,16 @@ class ApplicationSession(BaseSession):
res = types.CallResult(*msg.args, **msg.kwargs) res = types.CallResult(*msg.args, **msg.kwargs)
else: else:
res = types.CallResult(**msg.kwargs) res = types.CallResult(**msg.kwargs)
self._resolve_future(on_reply, res) txaio.resolve(on_reply, res)
else: else:
if msg.args: if msg.args:
if len(msg.args) > 1: if len(msg.args) > 1:
res = types.CallResult(*msg.args) res = types.CallResult(*msg.args)
self._resolve_future(on_reply, res) txaio.resolve(on_reply, res)
else: else:
self._resolve_future(on_reply, msg.args[0]) txaio.resolve(on_reply, msg.args[0])
else: else:
self._resolve_future(on_reply, None) txaio.resolve(on_reply, None)
else: else:
raise ProtocolError("RESULT received for non-pending request ID {0}".format(msg.request)) raise ProtocolError("RESULT received for non-pending request ID {0}".format(msg.request))
@ -754,10 +760,9 @@ class ApplicationSession(BaseSession):
else: else:
registration = self._registrations[msg.registration] registration = self._registrations[msg.registration]
endpoint = registration.endpoint endpoint = registration.endpoint
if endpoint.obj: if endpoint.obj is not None:
invoke_args = (endpoint.obj,) invoke_args = (endpoint.obj,)
else: else:
invoke_args = tuple() invoke_args = tuple()
@ -778,7 +783,7 @@ class ApplicationSession(BaseSession):
invoke_kwargs[endpoint.details_arg] = types.CallDetails(progress, caller=msg.caller, procedure=msg.procedure) invoke_kwargs[endpoint.details_arg] = types.CallDetails(progress, caller=msg.caller, procedure=msg.procedure)
on_reply = self._as_future(endpoint.fn, *invoke_args, **invoke_kwargs) on_reply = txaio.as_future(endpoint.fn, *invoke_args, **invoke_kwargs)
def success(res): def success(res):
del self._invocations[msg.request] del self._invocations[msg.request]
@ -831,7 +836,7 @@ class ApplicationSession(BaseSession):
self._invocations[msg.request] = InvocationRequest(msg.request, on_reply) self._invocations[msg.request] = InvocationRequest(msg.request, on_reply)
self._add_future_callbacks(on_reply, success, error) txaio.add_callbacks(on_reply, success, error)
elif isinstance(msg, message.Interrupt): elif isinstance(msg, message.Interrupt):
@ -864,7 +869,7 @@ class ApplicationSession(BaseSession):
else: else:
raise ProtocolError("REGISTERED received for already existing registration ID {0}".format(msg.registration)) raise ProtocolError("REGISTERED received for already existing registration ID {0}".format(msg.registration))
self._resolve_future(request.on_reply, registration) txaio.resolve(request.on_reply, registration)
else: else:
raise ProtocolError("REGISTERED received for non-pending request ID {0}".format(msg.request)) raise ProtocolError("REGISTERED received for non-pending request ID {0}".format(msg.request))
@ -881,7 +886,7 @@ class ApplicationSession(BaseSession):
del self._registrations[request.registration_id] del self._registrations[request.registration_id]
# resolve deferred/future for unregistering successfully # resolve deferred/future for unregistering successfully
self._resolve_future(request.on_reply) txaio.resolve(request.on_reply)
else: else:
raise ProtocolError("UNREGISTERED received for non-pending request ID {0}".format(msg.request)) raise ProtocolError("UNREGISTERED received for non-pending request ID {0}".format(msg.request))
@ -915,7 +920,7 @@ class ApplicationSession(BaseSession):
on_reply = self._unregister_reqs.pop(msg.request).on_reply on_reply = self._unregister_reqs.pop(msg.request).on_reply
if on_reply: if on_reply:
self._reject_future(on_reply, self._exception_from_message(msg)) txaio.reject(on_reply, self._exception_from_message(msg))
else: else:
raise ProtocolError("WampAppSession.onMessage(): ERROR received for non-pending request_type {0} and request ID {1}".format(msg.request_type, msg.request)) raise ProtocolError("WampAppSession.onMessage(): ERROR received for non-pending request_type {0} and request ID {1}".format(msg.request_type, msg.request))
@ -932,19 +937,19 @@ class ApplicationSession(BaseSession):
if self._session_id: if self._session_id:
# fire callback and close the transport # fire callback and close the transport
d = self._as_future(self.onLeave, types.CloseDetails(reason=types.CloseDetails.REASON_TRANSPORT_LOST, message="WAMP transport was lost without closing the session before")) d = txaio.as_future(self.onLeave, types.CloseDetails(reason=types.CloseDetails.REASON_TRANSPORT_LOST, message="WAMP transport was lost without closing the session before"))
def _error(e): def _error(e):
return self._swallow_error(e, "While firing onLeave") return self._swallow_error(e, "While firing onLeave")
self._add_future_callbacks(d, None, _error) txaio.add_callbacks(d, None, _error)
self._session_id = None self._session_id = None
d = self._as_future(self.onDisconnect) d = txaio.as_future(self.onDisconnect)
def _error(e): def _error(e):
return self._swallow_error(e, "While firing onDisconnect") return self._swallow_error(e, "While firing onDisconnect")
self._add_future_callbacks(d, None, _error) txaio.add_callbacks(d, None, _error)
def onChallenge(self, challenge): def onChallenge(self, challenge):
""" """
@ -961,7 +966,9 @@ class ApplicationSession(BaseSession):
""" """
Implements :func:`autobahn.wamp.interfaces.ISession.onLeave` Implements :func:`autobahn.wamp.interfaces.ISession.onLeave`
""" """
self.disconnect() if self._transport:
self.disconnect()
# do we ever call onLeave with a valid transport?
def leave(self, reason=None, log_message=None): def leave(self, reason=None, log_message=None):
""" """
@ -976,6 +983,8 @@ class ApplicationSession(BaseSession):
msg = wamp.message.Goodbye(reason=reason, message=log_message) msg = wamp.message.Goodbye(reason=reason, message=log_message)
self._transport.send(msg) self._transport.send(msg)
self._goodbye_sent = True self._goodbye_sent = True
# deferred that fires when transport actually hits CLOSED
return self._transport.is_closed
else: else:
raise SessionNotReady(u"Already requested to close the session") raise SessionNotReady(u"Already requested to close the session")
@ -990,7 +999,7 @@ class ApplicationSession(BaseSession):
if not self._transport: if not self._transport:
raise exception.TransportLost() raise exception.TransportLost()
request_id = util.id() request_id = self._request_id_gen.next()
if 'options' in kwargs and isinstance(kwargs['options'], types.PublishOptions): if 'options' in kwargs and isinstance(kwargs['options'], types.PublishOptions):
options = kwargs.pop('options') options = kwargs.pop('options')
@ -1001,7 +1010,7 @@ class ApplicationSession(BaseSession):
if options and options.acknowledge: if options and options.acknowledge:
# only acknowledged publications expect a reply .. # only acknowledged publications expect a reply ..
on_reply = self._create_future() on_reply = txaio.create_future()
self._publish_reqs[request_id] = PublishRequest(request_id, on_reply) self._publish_reqs[request_id] = PublishRequest(request_id, on_reply)
else: else:
on_reply = None on_reply = None
@ -1037,8 +1046,8 @@ class ApplicationSession(BaseSession):
raise exception.TransportLost() raise exception.TransportLost()
def _subscribe(obj, fn, topic, options): def _subscribe(obj, fn, topic, options):
request_id = util.id() request_id = self._request_id_gen.next()
on_reply = self._create_future() on_reply = txaio.create_future()
handler_obj = Handler(fn, obj, options.details_arg if options else None) handler_obj = Handler(fn, obj, options.details_arg if options else None)
self._subscribe_reqs[request_id] = SubscribeRequest(request_id, on_reply, handler_obj) self._subscribe_reqs[request_id] = SubscribeRequest(request_id, on_reply, handler_obj)
@ -1051,7 +1060,6 @@ class ApplicationSession(BaseSession):
return on_reply return on_reply
if callable(handler): if callable(handler):
# subscribe a single handler # subscribe a single handler
return _subscribe(None, handler, topic, options) return _subscribe(None, handler, topic, options)
@ -1062,13 +1070,14 @@ class ApplicationSession(BaseSession):
for k in inspect.getmembers(handler.__class__, is_method_or_function): for k in inspect.getmembers(handler.__class__, is_method_or_function):
proc = k[1] proc = k[1]
if "_wampuris" in proc.__dict__: if "_wampuris" in proc.__dict__:
pat = proc.__dict__["_wampuris"][0] for pat in proc.__dict__["_wampuris"]:
if pat.is_handler(): if pat.is_handler():
uri = pat.uri() uri = pat.uri()
subopts = options or pat.subscribe_options() subopts = options or pat.subscribe_options()
on_replies.append(_subscribe(handler, proc, uri, subopts)) on_replies.append(_subscribe(handler, proc, uri, subopts))
return self._gather_futures(on_replies, consume_exceptions=True) # XXX needs coverage
return txaio.gather(on_replies, consume_exceptions=True)
def _unsubscribe(self, subscription): def _unsubscribe(self, subscription):
""" """
@ -1091,9 +1100,9 @@ class ApplicationSession(BaseSession):
if scount == 0: if scount == 0:
# if the last handler was removed, unsubscribe from broker .. # if the last handler was removed, unsubscribe from broker ..
request_id = util.id() request_id = self._request_id_gen.next()
on_reply = self._create_future() on_reply = txaio.create_future()
self._unsubscribe_reqs[request_id] = UnsubscribeRequest(request_id, on_reply, subscription.id) self._unsubscribe_reqs[request_id] = UnsubscribeRequest(request_id, on_reply, subscription.id)
msg = message.Unsubscribe(request_id, subscription.id) msg = message.Unsubscribe(request_id, subscription.id)
@ -1102,7 +1111,7 @@ class ApplicationSession(BaseSession):
return on_reply return on_reply
else: else:
# there are still handlers active on the subscription! # there are still handlers active on the subscription!
return self._create_future_success(scount) return txaio.create_future_success(scount)
def call(self, procedure, *args, **kwargs): def call(self, procedure, *args, **kwargs):
""" """
@ -1115,7 +1124,7 @@ class ApplicationSession(BaseSession):
if not self._transport: if not self._transport:
raise exception.TransportLost() raise exception.TransportLost()
request_id = util.id() request_id = self._request_id_gen.next()
if 'options' in kwargs and isinstance(kwargs['options'], types.CallOptions): if 'options' in kwargs and isinstance(kwargs['options'], types.CallOptions):
options = kwargs.pop('options') options = kwargs.pop('options')
@ -1130,7 +1139,7 @@ class ApplicationSession(BaseSession):
# self._transport.send(cancel_msg) # self._transport.send(cancel_msg)
# d = Deferred(canceller) # d = Deferred(canceller)
on_reply = self._create_future() on_reply = txaio.create_future()
self._call_reqs[request_id] = CallRequest(request_id, on_reply, options) self._call_reqs[request_id] = CallRequest(request_id, on_reply, options)
try: try:
@ -1143,10 +1152,10 @@ class ApplicationSession(BaseSession):
# will immediately lead on an incoming WAMP message in onMessage() # will immediately lead on an incoming WAMP message in onMessage()
# #
self._transport.send(msg) self._transport.send(msg)
except Exception as e: except:
if request_id in self._call_reqs: if request_id in self._call_reqs:
del self._call_reqs[request_id] del self._call_reqs[request_id]
raise e raise
return on_reply return on_reply
@ -1164,8 +1173,8 @@ class ApplicationSession(BaseSession):
raise exception.TransportLost() raise exception.TransportLost()
def _register(obj, fn, procedure, options): def _register(obj, fn, procedure, options):
request_id = util.id() request_id = self._request_id_gen.next()
on_reply = self._create_future() on_reply = txaio.create_future()
endpoint_obj = Endpoint(fn, obj, options.details_arg if options else None) endpoint_obj = Endpoint(fn, obj, options.details_arg if options else None)
self._register_reqs[request_id] = RegisterRequest(request_id, on_reply, procedure, endpoint_obj) self._register_reqs[request_id] = RegisterRequest(request_id, on_reply, procedure, endpoint_obj)
@ -1189,12 +1198,13 @@ class ApplicationSession(BaseSession):
for k in inspect.getmembers(endpoint.__class__, is_method_or_function): for k in inspect.getmembers(endpoint.__class__, is_method_or_function):
proc = k[1] proc = k[1]
if "_wampuris" in proc.__dict__: if "_wampuris" in proc.__dict__:
pat = proc.__dict__["_wampuris"][0] for pat in proc.__dict__["_wampuris"]:
if pat.is_endpoint(): if pat.is_endpoint():
uri = pat.uri() uri = pat.uri()
on_replies.append(_register(endpoint, proc, uri, options)) on_replies.append(_register(endpoint, proc, uri, options))
return self._gather_futures(on_replies, consume_exceptions=True) # XXX neds coverage
return txaio.gather(on_replies, consume_exceptions=True)
def _unregister(self, registration): def _unregister(self, registration):
""" """
@ -1207,9 +1217,9 @@ class ApplicationSession(BaseSession):
if not self._transport: if not self._transport:
raise exception.TransportLost() raise exception.TransportLost()
request_id = util.id() request_id = self._request_id_gen.next()
on_reply = self._create_future() on_reply = txaio.create_future()
self._unregister_reqs[request_id] = UnregisterRequest(request_id, on_reply, registration.id) self._unregister_reqs[request_id] = UnregisterRequest(request_id, on_reply, registration.id)
msg = message.Unregister(request_id, registration.id) msg = message.Unregister(request_id, registration.id)

View File

@ -85,7 +85,8 @@ class RoleBrokerFeatures(RoleFeatures):
subscriber_blackwhite_listing=None, subscriber_blackwhite_listing=None,
publisher_exclusion=None, publisher_exclusion=None,
subscription_revocation=None, subscription_revocation=None,
event_history=None): event_history=None,
**kwargs):
self.publisher_identification = publisher_identification self.publisher_identification = publisher_identification
self.publication_trustlevels = publication_trustlevels self.publication_trustlevels = publication_trustlevels
self.pattern_based_subscription = pattern_based_subscription self.pattern_based_subscription = pattern_based_subscription
@ -110,7 +111,8 @@ class RoleSubscriberFeatures(RoleFeatures):
publication_trustlevels=None, publication_trustlevels=None,
pattern_based_subscription=None, pattern_based_subscription=None,
subscription_revocation=None, subscription_revocation=None,
event_history=None): event_history=None,
**kwargs):
self.publisher_identification = publisher_identification self.publisher_identification = publisher_identification
self.publication_trustlevels = publication_trustlevels self.publication_trustlevels = publication_trustlevels
self.pattern_based_subscription = pattern_based_subscription self.pattern_based_subscription = pattern_based_subscription
@ -130,7 +132,8 @@ class RolePublisherFeatures(RoleFeatures):
def __init__(self, def __init__(self,
publisher_identification=None, publisher_identification=None,
subscriber_blackwhite_listing=None, subscriber_blackwhite_listing=None,
publisher_exclusion=None): publisher_exclusion=None,
**kwargs):
self.publisher_identification = publisher_identification self.publisher_identification = publisher_identification
self.subscriber_blackwhite_listing = subscriber_blackwhite_listing self.subscriber_blackwhite_listing = subscriber_blackwhite_listing
self.publisher_exclusion = publisher_exclusion self.publisher_exclusion = publisher_exclusion
@ -154,7 +157,8 @@ class RoleDealerFeatures(RoleFeatures):
call_timeout=None, call_timeout=None,
call_canceling=None, call_canceling=None,
progressive_call_results=None, progressive_call_results=None,
registration_revocation=None): registration_revocation=None,
**kwargs):
self.caller_identification = caller_identification self.caller_identification = caller_identification
self.call_trustlevels = call_trustlevels self.call_trustlevels = call_trustlevels
self.pattern_based_registration = pattern_based_registration self.pattern_based_registration = pattern_based_registration
@ -179,7 +183,8 @@ class RoleCallerFeatures(RoleFeatures):
caller_identification=None, caller_identification=None,
call_timeout=None, call_timeout=None,
call_canceling=None, call_canceling=None,
progressive_call_results=None): progressive_call_results=None,
**kwargs):
self.caller_identification = caller_identification self.caller_identification = caller_identification
self.call_timeout = call_timeout self.call_timeout = call_timeout
self.call_canceling = call_canceling self.call_canceling = call_canceling
@ -203,7 +208,8 @@ class RoleCalleeFeatures(RoleFeatures):
call_timeout=None, call_timeout=None,
call_canceling=None, call_canceling=None,
progressive_call_results=None, progressive_call_results=None,
registration_revocation=None): registration_revocation=None,
**kwargs):
self.caller_identification = caller_identification self.caller_identification = caller_identification
self.call_trustlevels = call_trustlevels self.call_trustlevels = call_trustlevels
self.pattern_based_registration = pattern_based_registration self.pattern_based_registration = pattern_based_registration

View File

@ -214,7 +214,22 @@ IObjectSerializer.register(JsonObjectSerializer)
class JsonSerializer(Serializer): class JsonSerializer(Serializer):
SERIALIZER_ID = "json" SERIALIZER_ID = "json"
"""
ID used as part of the WebSocket subprotocol name to identify the
serializer with WAMP-over-WebSocket.
"""
RAWSOCKET_SERIALIZER_ID = 1
"""
ID used in lower four bits of second octet in RawSocket opening
handshake identify the serializer with WAMP-over-RawSocket.
"""
MIME_TYPE = "application/json" MIME_TYPE = "application/json"
"""
MIME type announced in HTTP request/response headers when running
WAMP-over-Longpoll HTTP fallback.
"""
def __init__(self, batched=False): def __init__(self, batched=False):
""" """
@ -276,6 +291,23 @@ else:
""" """
Implements :func:`autobahn.wamp.interfaces.IObjectSerializer.unserialize` Implements :func:`autobahn.wamp.interfaces.IObjectSerializer.unserialize`
""" """
def ensure_string_keys(d):
"""
under python 2, with use_bin_type=True, most dict keys end up
getting encoded as bytes (any syntax except {u"key":
u"value"}) so instead of recursively looking through
everything that's getting serialized, we fix them up
on the way out using msgpack's `object_hook` as
there's no corresponding hook for serialization...
"""
for (k, v) in six.iteritems(d):
if not isinstance(k, six.text_type):
newk = six.text_type(k, encoding='utf8')
del d[k]
d[newk] = v
return d
if self._batched: if self._batched:
msgs = [] msgs = []
N = len(payload) N = len(payload)
@ -292,7 +324,13 @@ else:
data = payload[i + 4:i + 4 + l] data = payload[i + 4:i + 4 + l]
# append parsed raw message # append parsed raw message
msgs.append(msgpack.unpackb(data, encoding='utf-8')) msgs.append(
msgpack.unpackb(
data,
encoding='utf-8',
object_hook=ensure_string_keys,
)
)
# advance until everything consumed # advance until everything consumed
i = i + 4 + l i = i + 4 + l
@ -302,7 +340,12 @@ else:
return msgs return msgs
else: else:
return [msgpack.unpackb(payload, encoding='utf-8')] unpacked = msgpack.unpackb(
payload,
encoding='utf-8',
object_hook=ensure_string_keys,
)
return [unpacked]
IObjectSerializer.register(MsgPackObjectSerializer) IObjectSerializer.register(MsgPackObjectSerializer)
@ -311,7 +354,22 @@ else:
class MsgPackSerializer(Serializer): class MsgPackSerializer(Serializer):
SERIALIZER_ID = "msgpack" SERIALIZER_ID = "msgpack"
"""
ID used as part of the WebSocket subprotocol name to identify the
serializer with WAMP-over-WebSocket.
"""
RAWSOCKET_SERIALIZER_ID = 2
"""
ID used in lower four bits of second octet in RawSocket opening
handshake identify the serializer with WAMP-over-RawSocket.
"""
MIME_TYPE = "application/x-msgpack" MIME_TYPE = "application/x-msgpack"
"""
MIME type announced in HTTP request/response headers when running
WAMP-over-Longpoll HTTP fallback.
"""
def __init__(self, batched=False): def __init__(self, batched=False):
""" """

View File

@ -33,8 +33,9 @@ if os.environ.get('USE_TWISTED', False):
from twisted.trial import unittest from twisted.trial import unittest
# import unittest # import unittest
from twisted.internet.defer import inlineCallbacks, Deferred, returnValue, succeed from twisted.internet.defer import inlineCallbacks, Deferred, returnValue, succeed, DeferredList
from twisted.python import log from twisted.python import log
from six import PY3
from autobahn.wamp import message from autobahn.wamp import message
from autobahn.wamp import serializer from autobahn.wamp import serializer
@ -42,9 +43,11 @@ if os.environ.get('USE_TWISTED', False):
from autobahn import util from autobahn import util
from autobahn.wamp.exception import ApplicationError, NotAuthorized, InvalidUri, ProtocolError from autobahn.wamp.exception import ApplicationError, NotAuthorized, InvalidUri, ProtocolError
from autobahn.wamp import types from autobahn.wamp import types
from autobahn.twisted.wamp import ApplicationSession from autobahn.twisted.wamp import ApplicationSession
if PY3:
long = int
class MockTransport(object): class MockTransport(object):
def __init__(self, handler): def __init__(self, handler):
@ -64,6 +67,7 @@ if os.environ.get('USE_TWISTED', False):
msg = message.Welcome(self._my_session_id, roles) msg = message.Welcome(self._my_session_id, roles)
self._handler.onMessage(msg) self._handler.onMessage(msg)
self._fake_router_session = ApplicationSession()
def send(self, msg): def send(self, msg):
if self._log: if self._log:
@ -75,7 +79,7 @@ if os.environ.get('USE_TWISTED', False):
if isinstance(msg, message.Publish): if isinstance(msg, message.Publish):
if msg.topic.startswith(u'com.myapp'): if msg.topic.startswith(u'com.myapp'):
if msg.acknowledge: if msg.acknowledge:
reply = message.Published(msg.request, util.id()) reply = message.Published(msg.request, self._fake_router_session._request_id_gen.next())
elif len(msg.topic) == 0: elif len(msg.topic) == 0:
reply = message.Error(message.Publish.MESSAGE_TYPE, msg.request, u'wamp.error.invalid_uri') reply = message.Error(message.Publish.MESSAGE_TYPE, msg.request, u'wamp.error.invalid_uri')
else: else:
@ -91,7 +95,9 @@ if os.environ.get('USE_TWISTED', False):
elif msg.procedure.startswith(u'com.myapp.myproc'): elif msg.procedure.startswith(u'com.myapp.myproc'):
registration = self._registrations[msg.procedure] registration = self._registrations[msg.procedure]
request = util.id() request = self._fake_router_session._request_id_gen.next()
if request in self._invocations:
raise ProtocolError("duplicate invocation")
self._invocations[request] = msg.request self._invocations[request] = msg.request
reply = message.Invocation( reply = message.Invocation(
request, registration, request, registration,
@ -112,7 +118,7 @@ if os.environ.get('USE_TWISTED', False):
if topic in self._subscription_topics: if topic in self._subscription_topics:
reply_id = self._subscription_topics[topic] reply_id = self._subscription_topics[topic]
else: else:
reply_id = util.id() reply_id = self._fake_router_session._request_id_gen.next()
self._subscription_topics[topic] = reply_id self._subscription_topics[topic] = reply_id
reply = message.Subscribed(msg.request, reply_id) reply = message.Subscribed(msg.request, reply_id)
@ -120,7 +126,7 @@ if os.environ.get('USE_TWISTED', False):
reply = message.Unsubscribed(msg.request) reply = message.Unsubscribed(msg.request)
elif isinstance(msg, message.Register): elif isinstance(msg, message.Register):
registration = util.id() registration = self._fake_router_session._request_id_gen.next()
self._registrations[msg.procedure] = registration self._registrations[msg.procedure] = registration
reply = message.Registered(msg.request, registration) reply = message.Registered(msg.request, registration)
@ -154,6 +160,15 @@ if os.environ.get('USE_TWISTED', False):
def abort(self): def abort(self):
pass pass
class TestClose(unittest.TestCase):
def test_server_abort(self):
handler = ApplicationSession()
MockTransport(handler)
# this should not raise an exception, but did when this
# test-case was written
handler.onClose(False)
class TestPublisher(unittest.TestCase): class TestPublisher(unittest.TestCase):
@inlineCallbacks @inlineCallbacks
@ -532,6 +547,55 @@ if os.environ.get('USE_TWISTED', False):
res = yield handler.call(u'com.myapp.myproc1') res = yield handler.call(u'com.myapp.myproc1')
self.assertEqual(res, 23) self.assertEqual(res, 23)
@inlineCallbacks
def test_invoke_twice(self):
handler = ApplicationSession()
MockTransport(handler)
def myproc1():
return 23
yield handler.register(myproc1, u'com.myapp.myproc1')
d0 = handler.call(u'com.myapp.myproc1')
d1 = handler.call(u'com.myapp.myproc1')
res = yield DeferredList([d0, d1])
self.assertEqual(res, [(True, 23), (True, 23)])
@inlineCallbacks
def test_invoke_request_id_sequences(self):
"""
make sure each session independently generates sequential IDs
"""
handler0 = ApplicationSession()
handler1 = ApplicationSession()
trans0 = MockTransport(handler0)
trans1 = MockTransport(handler1)
# the ID sequences for each session should both start at 0
# (the register) and then increment for the call()
def verify_seq_id(orig, msg):
if isinstance(msg, message.Register):
self.assertEqual(msg.request, 0)
elif isinstance(msg, message.Call):
self.assertEqual(msg.request, 1)
return orig(msg)
orig0 = trans0.send
orig1 = trans1.send
trans0.send = lambda msg: verify_seq_id(orig0, msg)
trans1.send = lambda msg: verify_seq_id(orig1, msg)
def myproc1():
return 23
yield handler0.register(myproc1, u'com.myapp.myproc1')
yield handler1.register(myproc1, u'com.myapp.myproc1')
d0 = handler0.call(u'com.myapp.myproc1')
d1 = handler1.call(u'com.myapp.myproc1')
res = yield DeferredList([d0, d1])
self.assertEqual(res, [(True, 23), (True, 23)])
@inlineCallbacks @inlineCallbacks
def test_invoke_user_raises(self): def test_invoke_user_raises(self):
handler = ApplicationSession() handler = ApplicationSession()
@ -575,7 +639,7 @@ if os.environ.get('USE_TWISTED', False):
yield succeed(i) yield succeed(i)
returnValue(42) returnValue(42)
progressive = map(lambda _: Deferred(), range(10)) progressive = list(map(lambda _: Deferred(), range(10)))
def progress(arg): def progress(arg):
progressive[arg].callback(arg) progressive[arg].callback(arg)

View File

@ -24,60 +24,177 @@
# #
############################################################################### ###############################################################################
from __future__ import absolute_import from __future__ import absolute_import, print_function
import os
try: try:
import unittest2 as unittest import unittest2 as unittest
except ImportError: except ImportError:
import unittest import unittest
from mock import patch
if os.environ.get('USE_TWISTED', False):
from mock import patch
from zope.interface import implementer
from twisted.internet.interfaces import IReactorTime
class FakeReactor(object): @implementer(IReactorTime)
''' class FakeReactor(object):
This just fakes out enough reactor methods so .run() can work.
'''
stop_called = False
def __init__(self, to_raise):
self.stop_called = False
self.to_raise = to_raise
def run(self, *args, **kw):
raise self.to_raise
def stop(self):
self.stop_called = True
def connectTCP(self, *args, **kw):
raise RuntimeError("ConnectTCP shouldn't get called")
class TestWampTwistedRunner(unittest.TestCase):
def test_connect_error(self):
''' '''
Ensure the runner doesn't swallow errors and that it exits the This just fakes out enough reactor methods so .run() can work.
reactor properly if there is one.
''' '''
try: stop_called = False
from autobahn.twisted.wamp import ApplicationRunner
from twisted.internet.error import ConnectionRefusedError
# the 'reactor' member doesn't exist until we import it
from twisted.internet import reactor # noqa: F401
except ImportError:
raise unittest.SkipTest('No twisted')
runner = ApplicationRunner('ws://localhost:1', 'realm') def __init__(self, to_raise):
exception = ConnectionRefusedError("It's a trap!") self.stop_called = False
self.to_raise = to_raise
self.delayed = []
with patch('twisted.internet.reactor', FakeReactor(exception)) as mockreactor: def run(self, *args, **kw):
self.assertRaises( raise self.to_raise
ConnectionRefusedError,
# pass a no-op session-creation method
runner.run, lambda _: None, start_reactor=True
)
self.assertTrue(mockreactor.stop_called)
if __name__ == '__main__': def stop(self):
unittest.main() self.stop_called = True
def callLater(self, delay, func, *args, **kwargs):
self.delayed.append((delay, func, args, kwargs))
def connectTCP(self, *args, **kw):
raise RuntimeError("ConnectTCP shouldn't get called")
class TestWampTwistedRunner(unittest.TestCase):
def test_connect_error(self):
'''
Ensure the runner doesn't swallow errors and that it exits the
reactor properly if there is one.
'''
try:
from autobahn.twisted.wamp import ApplicationRunner
from twisted.internet.error import ConnectionRefusedError
# the 'reactor' member doesn't exist until we import it
from twisted.internet import reactor # noqa: F401
except ImportError:
raise unittest.SkipTest('No twisted')
runner = ApplicationRunner('ws://localhost:1', 'realm')
exception = ConnectionRefusedError("It's a trap!")
with patch('twisted.internet.reactor', FakeReactor(exception)) as mockreactor:
self.assertRaises(
ConnectionRefusedError,
# pass a no-op session-creation method
runner.run, lambda _: None, start_reactor=True
)
self.assertTrue(mockreactor.stop_called)
else:
# Asyncio tests.
try:
import asyncio
from unittest.mock import patch, Mock
except ImportError:
# Trollius >= 0.3 was renamed to asyncio
# noinspection PyUnresolvedReferences
import trollius as asyncio
from mock import patch, Mock
from autobahn.asyncio.wamp import ApplicationRunner
class TestApplicationRunner(unittest.TestCase):
'''
Test the autobahn.asyncio.wamp.ApplicationRunner class.
'''
def _assertRaisesRegex(self, exception, error, *args, **kw):
try:
self.assertRaisesRegex
except AttributeError:
f = self.assertRaisesRegexp
else:
f = self.assertRaisesRegex
f(exception, error, *args, **kw)
def test_explicit_SSLContext(self):
'''
Ensure that loop.create_connection is called with the exact SSL
context object that is passed (as ssl) to the __init__ method of
ApplicationRunner.
'''
loop = Mock()
loop.run_until_complete = Mock(return_value=(Mock(), Mock()))
with patch.object(asyncio, 'get_event_loop', return_value=loop):
ssl = {}
runner = ApplicationRunner('ws://127.0.0.1:8080/ws', 'realm',
ssl=ssl)
runner.run('_unused_')
self.assertIs(ssl, loop.create_connection.call_args[1]['ssl'])
def test_omitted_SSLContext_insecure(self):
'''
Ensure that loop.create_connection is called with ssl=False
if no ssl argument is passed to the __init__ method of
ApplicationRunner and the websocket URL starts with "ws:".
'''
loop = Mock()
loop.run_until_complete = Mock(return_value=(Mock(), Mock()))
with patch.object(asyncio, 'get_event_loop', return_value=loop):
runner = ApplicationRunner('ws://127.0.0.1:8080/ws', 'realm')
runner.run('_unused_')
self.assertIs(False, loop.create_connection.call_args[1]['ssl'])
def test_omitted_SSLContext_secure(self):
'''
Ensure that loop.create_connection is called with ssl=True
if no ssl argument is passed to the __init__ method of
ApplicationRunner and the websocket URL starts with "wss:".
'''
loop = Mock()
loop.run_until_complete = Mock(return_value=(Mock(), Mock()))
with patch.object(asyncio, 'get_event_loop', return_value=loop):
runner = ApplicationRunner('wss://127.0.0.1:8080/wss', 'realm')
runner.run('_unused_')
self.assertIs(True, loop.create_connection.call_args[1]['ssl'])
def test_conflict_SSL_True_with_ws_url(self):
'''
ApplicationRunner must raise an exception if given an ssl value of True
but only a "ws:" URL.
'''
loop = Mock()
loop.run_until_complete = Mock(return_value=(Mock(), Mock()))
with patch.object(asyncio, 'get_event_loop', return_value=loop):
runner = ApplicationRunner('ws://127.0.0.1:8080/wss', 'realm',
ssl=True)
error = ('^ssl argument value passed to ApplicationRunner '
'conflicts with the "ws:" prefix of the url '
'argument\. Did you mean to use "wss:"\?$')
self._assertRaisesRegex(Exception, error, runner.run, '_unused_')
def test_conflict_SSLContext_with_ws_url(self):
'''
ApplicationRunner must raise an exception if given an ssl value that is
an instance of SSLContext, but only a "ws:" URL.
'''
import ssl
try:
# Try to create an SSLContext, to be as rigorous as we can be
# by avoiding making assumptions about the ApplicationRunner
# implementation. If we happen to be on a Python that has no
# SSLContext, we pass ssl=True, which will simply cause this
# test to degenerate to the behavior of
# test_conflict_SSL_True_with_ws_url (above). In fact, at the
# moment (2015-05-10), none of this matters because the
# ApplicationRunner implementation does not check to require
# that its ssl argument is either a bool or an SSLContext. But
# that may change, so we should be careful.
ssl.create_default_context
except AttributeError:
context = True
else:
context = ssl.create_default_context()
loop = Mock()
loop.run_until_complete = Mock(return_value=(Mock(), Mock()))
with patch.object(asyncio, 'get_event_loop', return_value=loop):
runner = ApplicationRunner('ws://127.0.0.1:8080/wss', 'realm',
ssl=context)
error = ('^ssl argument value passed to ApplicationRunner '
'conflicts with the "ws:" prefix of the url '
'argument\. Did you mean to use "wss:"\?$')
self._assertRaisesRegex(Exception, error, runner.run, '_unused_')

View File

@ -28,6 +28,7 @@ from __future__ import absolute_import
# from twisted.trial import unittest # from twisted.trial import unittest
import unittest import unittest
import six
from autobahn.wamp import message from autobahn.wamp import message
from autobahn.wamp import role from autobahn.wamp import role
@ -93,6 +94,50 @@ class TestSerializer(unittest.TestCase):
self.serializers.append(serializer.MsgPackSerializer()) self.serializers.append(serializer.MsgPackSerializer())
self.serializers.append(serializer.MsgPackSerializer(batched=True)) self.serializers.append(serializer.MsgPackSerializer(batched=True))
def test_dict_keys_msgpack(self):
"""
dict keys should always be strings. the data provided is from
calling msgpack encode on a dict in python2 with
`use_bin_type=True` and the following message:
print(ser.serialize(
message.Call(
123456, u"com.myapp.procedure1",
args=(),
kwargs={u'unicode': 23, 'str': 42}
)
))
"""
if not hasattr(serializer, 'MsgPackSerializer'):
self.skipTest("no msgpack")
ser = serializer.MsgPackSerializer()
payload = b'\x960\xce\x00\x01\xe2@\x80\xb4com.myapp.procedure1\x90\x82\xc4\x03str*\xa7unicode\x17'
msg_out = ser.unserialize(payload, True)[0]
for k in msg_out.kwargs.keys():
self.assertEqual(type(k), six.text_type)
self.assertTrue('str' in msg_out.kwargs)
self.assertTrue('unicode' in msg_out.kwargs)
def test_dict_keys_msgpack_batched(self):
"""
dict keys should always be strings. the data provided is from
calling msgpack encode on a dict in python2 with
`use_bin_type=True`
"""
if not hasattr(serializer, 'MsgPackSerializer'):
self.skipTest("no msgpack")
ser = serializer.MsgPackSerializer(batched=True)
payload = b'\x00\x00\x00-\x960\xce\x00\x01\xe2@\x80\xb4com.myapp.procedure1\x90\x82\xa7unicode\x17\xa3str*'
msg_out = ser.unserialize(payload, True)[0]
for k in msg_out.kwargs.keys():
self.assertEqual(type(k), six.text_type)
self.assertTrue('str' in msg_out.kwargs)
self.assertTrue('unicode' in msg_out.kwargs)
def test_roundtrip(self): def test_roundtrip(self):
for msg in generate_test_messages(): for msg in generate_test_messages():
for ser in self.serializers: for ser in self.serializers:

View File

@ -333,10 +333,8 @@ if os.environ.get('USE_TWISTED', False):
# autobahn.wamp.websocket.WampWebSocketProtocol # autobahn.wamp.websocket.WampWebSocketProtocol
session.onClose(False) session.onClose(False)
self.assertEqual(2, len(session.errors)) self.assertEqual(1, len(session.errors))
# might want to re-think this? self.assertEqual(exception, session.errors[0][0])
self.assertEqual("No transport, but disconnect() called.", str(session.errors[0][0]))
self.assertEqual(exception, session.errors[1][0])
def test_on_disconnect_with_session_deferred(self): def test_on_disconnect_with_session_deferred(self):
session = MockApplicationSession() session = MockApplicationSession()
@ -351,10 +349,8 @@ if os.environ.get('USE_TWISTED', False):
# autobahn.wamp.websocket.WampWebSocketProtocol # autobahn.wamp.websocket.WampWebSocketProtocol
session.onClose(False) session.onClose(False)
self.assertEqual(2, len(session.errors)) self.assertEqual(1, len(session.errors))
# might want to re-think this? self.assertEqual(exception, session.errors[0][0])
self.assertEqual("No transport, but disconnect() called.", str(session.errors[0][0]))
self.assertEqual(exception, session.errors[1][0])
def test_on_connect(self): def test_on_connect(self):
session = MockApplicationSession() session = MockApplicationSession()

View File

@ -55,11 +55,13 @@ class ComponentConfig(object):
def __init__(self, realm=None, extra=None): def __init__(self, realm=None, extra=None):
""" """
:param realm: The realm the session should join. :param realm: The realm the session should join.
:type realm: unicode :type realm: unicode
:param extra: Optional dictionary with extra configuration.
:type extra: dict :param extra: Optional user-supplied object with extra
configuration. This can be any object you like, and is
accessible in your `ApplicationSession` subclass via
`self.config.extra`. `dict` is a good default choice.
""" """
if six.PY2 and type(realm) == str: if six.PY2 and type(realm) == str:
realm = six.u(realm) realm = six.u(realm)
@ -330,6 +332,9 @@ class PublishOptions(object):
to subscribers. to subscribers.
:type disclose_me: bool :type disclose_me: bool
""" """
# filter out None entries from exclude list, so it's easier for callers
if type(exclude) == list:
exclude = [x for x in exclude if x is not None]
assert(acknowledge is None or type(acknowledge) == bool) assert(acknowledge is None or type(acknowledge) == bool)
assert(exclude_me is None or type(exclude_me) == bool) assert(exclude_me is None or type(exclude_me) == bool)
assert(exclude is None or (type(exclude) == list and all(type(x) in six.integer_types for x in exclude))) assert(exclude is None or (type(exclude) == list and all(type(x) in six.integer_types for x in exclude)))

View File

@ -24,7 +24,7 @@
# #
############################################################################### ###############################################################################
from __future__ import absolute_import from __future__ import absolute_import, print_function
import traceback import traceback
@ -79,9 +79,8 @@ class WampWebSocketProtocol(object):
print("WAMP-over-WebSocket transport lost: wasClean = {0}, code = {1}, reason = '{2}'".format(wasClean, code, reason)) print("WAMP-over-WebSocket transport lost: wasClean = {0}, code = {1}, reason = '{2}'".format(wasClean, code, reason))
self._session.onClose(wasClean) self._session.onClose(wasClean)
except Exception: except Exception:
# silently ignore exceptions raised here .. print("Error invoking onClose():")
if self.factory.debug_wamp: traceback.print_exc()
traceback.print_exc()
self._session = None self._session = None
def onMessage(self, payload, isBinary): def onMessage(self, payload, isBinary):
@ -95,6 +94,7 @@ class WampWebSocketProtocol(object):
self._session.onMessage(msg) self._session.onMessage(msg)
except ProtocolError as e: except ProtocolError as e:
print(e)
if self.factory.debug_wamp: if self.factory.debug_wamp:
traceback.print_exc() traceback.print_exc()
reason = "WAMP Protocol Error ({0})".format(e) reason = "WAMP Protocol Error ({0})".format(e)

View File

@ -26,8 +26,20 @@
from __future__ import absolute_import from __future__ import absolute_import
from autobahn.websocket.compress_base import * # noqa from autobahn.websocket.compress_base import \
from autobahn.websocket.compress_deflate import * # noqa PerMessageCompressOffer, \
PerMessageCompressOfferAccept, \
PerMessageCompressResponse, \
PerMessageCompressResponseAccept, \
PerMessageCompress
from autobahn.websocket.compress_deflate import \
PerMessageDeflateMixin, \
PerMessageDeflateOffer, \
PerMessageDeflateOfferAccept, \
PerMessageDeflateResponse, \
PerMessageDeflateResponseAccept, \
PerMessageDeflate
# this must be a list (not tuple), since we dynamically # this must be a list (not tuple), since we dynamically
# extend it .. # extend it ..
@ -65,7 +77,13 @@ try:
except ImportError: except ImportError:
bz2 = None bz2 = None
else: else:
from autobahn.websocket.compress_bzip2 import * # noqa from autobahn.websocket.compress_bzip2 import \
PerMessageBzip2Mixin, \
PerMessageBzip2Offer, \
PerMessageBzip2OfferAccept, \
PerMessageBzip2Response, \
PerMessageBzip2ResponseAccept, \
PerMessageBzip2
PMCE = { PMCE = {
'Offer': PerMessageBzip2Offer, 'Offer': PerMessageBzip2Offer,
@ -91,7 +109,13 @@ try:
except ImportError: except ImportError:
snappy = None snappy = None
else: else:
from autobahn.websocket.compress_snappy import * # noqa from autobahn.websocket.compress_snappy import \
PerMessageSnappyMixin, \
PerMessageSnappyOffer, \
PerMessageSnappyOfferAccept, \
PerMessageSnappyResponse, \
PerMessageSnappyResponseAccept, \
PerMessageSnappy
PMCE = { PMCE = {
'Offer': PerMessageSnappyOffer, 'Offer': PerMessageSnappyOffer,

View File

@ -49,10 +49,11 @@ from autobahn.websocket.interfaces import IWebSocketChannel, \
from autobahn.util import Stopwatch, newid, wildcards2patterns from autobahn.util import Stopwatch, newid, wildcards2patterns
from autobahn.websocket.utf8validator import Utf8Validator from autobahn.websocket.utf8validator import Utf8Validator
from autobahn.websocket.xormasker import XorMaskerNull, createXorMasker from autobahn.websocket.xormasker import XorMaskerNull, createXorMasker
from autobahn.websocket.compress import * # noqa from autobahn.websocket.compress import PERMESSAGE_COMPRESSION_EXTENSION
from autobahn.websocket import http from autobahn.websocket import http
from six.moves import urllib from six.moves import urllib
import txaio
if six.PY3: if six.PY3:
# Python 3 # Python 3
@ -659,12 +660,16 @@ class WebSocketProtocol(object):
Configuration attributes specific to clients. Configuration attributes specific to clients.
""" """
def __init__(self):
#: a Future/Deferred that fires when we hit STATE_CLOSED
self.is_closed = txaio.create_future()
def onOpen(self): def onOpen(self):
""" """
Implements :func:`autobahn.websocket.interfaces.IWebSocketChannel.onOpen` Implements :func:`autobahn.websocket.interfaces.IWebSocketChannel.onOpen`
""" """
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("WebSocketProtocol.onOpen") self.factory.log.debug("WebSocketProtocol.onOpen")
def onMessageBegin(self, isBinary): def onMessageBegin(self, isBinary):
""" """
@ -736,14 +741,14 @@ class WebSocketProtocol(object):
Implements :func:`autobahn.websocket.interfaces.IWebSocketChannel.onMessage` Implements :func:`autobahn.websocket.interfaces.IWebSocketChannel.onMessage`
""" """
if self.debug: if self.debug:
self.factory._log("WebSocketProtocol.onMessage") self.factory.log.debug("WebSocketProtocol.onMessage")
def onPing(self, payload): def onPing(self, payload):
""" """
Implements :func:`autobahn.websocket.interfaces.IWebSocketChannel.onPing` Implements :func:`autobahn.websocket.interfaces.IWebSocketChannel.onPing`
""" """
if self.debug: if self.debug:
self.factory._log("WebSocketProtocol.onPing") self.factory.log.debug("WebSocketProtocol.onPing")
if self.state == WebSocketProtocol.STATE_OPEN: if self.state == WebSocketProtocol.STATE_OPEN:
self.sendPong(payload) self.sendPong(payload)
@ -752,7 +757,7 @@ class WebSocketProtocol(object):
Implements :func:`autobahn.websocket.interfaces.IWebSocketChannel.onPong` Implements :func:`autobahn.websocket.interfaces.IWebSocketChannel.onPong`
""" """
if self.debug: if self.debug:
self.factory._log("WebSocketProtocol.onPong") self.factory.log.debug("WebSocketProtocol.onPong")
def onClose(self, wasClean, code, reason): def onClose(self, wasClean, code, reason):
""" """
@ -772,7 +777,7 @@ class WebSocketProtocol(object):
s += "self.localCloseReason=%s\n" % self.localCloseReason s += "self.localCloseReason=%s\n" % self.localCloseReason
s += "self.remoteCloseCode=%s\n" % self.remoteCloseCode s += "self.remoteCloseCode=%s\n" % self.remoteCloseCode
s += "self.remoteCloseReason=%s\n" % self.remoteCloseReason s += "self.remoteCloseReason=%s\n" % self.remoteCloseReason
self.factory._log(s) self.factory.log.debug(s)
def onCloseFrame(self, code, reasonRaw): def onCloseFrame(self, code, reasonRaw):
""" """
@ -789,11 +794,11 @@ class WebSocketProtocol(object):
:param code: Close status code, if there was one (:class:`WebSocketProtocol`.CLOSE_STATUS_CODE_*). :param code: Close status code, if there was one (:class:`WebSocketProtocol`.CLOSE_STATUS_CODE_*).
:type code: int or None :type code: int or None
:param reason: Close reason (when present, a status code MUST have been also be present). :param reasonRaw: Close reason (when present, a status code MUST have been also be present).
:type reason: str or None :type reason: str or None
""" """
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("WebSocketProtocol.onCloseFrame") self.factory.log.debug("WebSocketProtocol.onCloseFrame")
self.remoteCloseCode = code self.remoteCloseCode = code
@ -825,7 +830,7 @@ class WebSocketProtocol(object):
# #
if self.closeHandshakeTimeoutCall is not None: if self.closeHandshakeTimeoutCall is not None:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("closeHandshakeTimeoutCall.cancel") self.factory.log.debug("closeHandshakeTimeoutCall.cancel")
self.closeHandshakeTimeoutCall.cancel() self.closeHandshakeTimeoutCall.cancel()
self.closeHandshakeTimeoutCall = None self.closeHandshakeTimeoutCall = None
@ -838,7 +843,11 @@ class WebSocketProtocol(object):
# When we are a client, the server should drop the TCP # When we are a client, the server should drop the TCP
# If that doesn't happen, we do. And that will set wasClean = False. # If that doesn't happen, we do. And that will set wasClean = False.
if self.serverConnectionDropTimeout > 0: if self.serverConnectionDropTimeout > 0:
self.serverConnectionDropTimeoutCall = self.factory._callLater(self.serverConnectionDropTimeout, self.onServerConnectionDropTimeout) call = txaio.call_later(
self.serverConnectionDropTimeout,
self.onServerConnectionDropTimeout,
)
self.serverConnectionDropTimeoutCall = call
elif self.state == WebSocketProtocol.STATE_OPEN: elif self.state == WebSocketProtocol.STATE_OPEN:
# The peer initiates a closing handshake, so we reply # The peer initiates a closing handshake, so we reply
@ -851,7 +860,7 @@ class WebSocketProtocol(object):
else: else:
# Either reply with same code/reason, or code == NORMAL/reason=None # Either reply with same code/reason, or code == NORMAL/reason=None
if self.echoCloseCodeReason: if self.echoCloseCodeReason:
self.sendCloseFrame(code=code, reasonUtf8=reason.encode("UTF-8"), isReply=True) self.sendCloseFrame(code=code, reasonUtf8=reasonRaw.encode("UTF-8"), isReply=True)
else: else:
self.sendCloseFrame(code=WebSocketProtocol.CLOSE_STATUS_CODE_NORMAL, isReply=True) self.sendCloseFrame(code=WebSocketProtocol.CLOSE_STATUS_CODE_NORMAL, isReply=True)
@ -883,14 +892,14 @@ class WebSocketProtocol(object):
self.serverConnectionDropTimeoutCall = None self.serverConnectionDropTimeoutCall = None
if self.state != WebSocketProtocol.STATE_CLOSED: if self.state != WebSocketProtocol.STATE_CLOSED:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("onServerConnectionDropTimeout") self.factory.log.debug("onServerConnectionDropTimeout")
self.wasClean = False self.wasClean = False
self.wasNotCleanReason = "server did not drop TCP connection (in time)" self.wasNotCleanReason = "server did not drop TCP connection (in time)"
self.wasServerConnectionDropTimeout = True self.wasServerConnectionDropTimeout = True
self.dropConnection(abort=True) self.dropConnection(abort=True)
else: else:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("skipping onServerConnectionDropTimeout since connection is already closed") self.factory.log.debug("skipping onServerConnectionDropTimeout since connection is already closed")
def onOpenHandshakeTimeout(self): def onOpenHandshakeTimeout(self):
""" """
@ -903,20 +912,20 @@ class WebSocketProtocol(object):
self.openHandshakeTimeoutCall = None self.openHandshakeTimeoutCall = None
if self.state in [WebSocketProtocol.STATE_CONNECTING, WebSocketProtocol.STATE_PROXY_CONNECTING]: if self.state in [WebSocketProtocol.STATE_CONNECTING, WebSocketProtocol.STATE_PROXY_CONNECTING]:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("onOpenHandshakeTimeout fired") self.factory.log.debug("onOpenHandshakeTimeout fired")
self.wasClean = False self.wasClean = False
self.wasNotCleanReason = "peer did not finish (in time) the opening handshake" self.wasNotCleanReason = "peer did not finish (in time) the opening handshake"
self.wasOpenHandshakeTimeout = True self.wasOpenHandshakeTimeout = True
self.dropConnection(abort=True) self.dropConnection(abort=True)
elif self.state == WebSocketProtocol.STATE_OPEN: elif self.state == WebSocketProtocol.STATE_OPEN:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("skipping onOpenHandshakeTimeout since WebSocket connection is open (opening handshake already finished)") self.factory.log.debug("skipping onOpenHandshakeTimeout since WebSocket connection is open (opening handshake already finished)")
elif self.state == WebSocketProtocol.STATE_CLOSING: elif self.state == WebSocketProtocol.STATE_CLOSING:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("skipping onOpenHandshakeTimeout since WebSocket connection is closing") self.factory.log.debug("skipping onOpenHandshakeTimeout since WebSocket connection is closing")
elif self.state == WebSocketProtocol.STATE_CLOSED: elif self.state == WebSocketProtocol.STATE_CLOSED:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("skipping onOpenHandshakeTimeout since WebSocket connection already closed") self.factory.log.debug("skipping onOpenHandshakeTimeout since WebSocket connection already closed")
else: else:
# should not arrive here # should not arrive here
raise Exception("logic error") raise Exception("logic error")
@ -932,14 +941,14 @@ class WebSocketProtocol(object):
self.closeHandshakeTimeoutCall = None self.closeHandshakeTimeoutCall = None
if self.state != WebSocketProtocol.STATE_CLOSED: if self.state != WebSocketProtocol.STATE_CLOSED:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("onCloseHandshakeTimeout fired") self.factory.log.debug("onCloseHandshakeTimeout fired")
self.wasClean = False self.wasClean = False
self.wasNotCleanReason = "peer did not respond (in time) in closing handshake" self.wasNotCleanReason = "peer did not respond (in time) in closing handshake"
self.wasCloseHandshakeTimeout = True self.wasCloseHandshakeTimeout = True
self.dropConnection(abort=True) self.dropConnection(abort=True)
else: else:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("skipping onCloseHandshakeTimeout since connection is already closed") self.factory.log.debug("skipping onCloseHandshakeTimeout since connection is already closed")
def onAutoPingTimeout(self): def onAutoPingTimeout(self):
""" """
@ -947,7 +956,7 @@ class WebSocketProtocol(object):
did not reply in time to our ping. We drop the connection. did not reply in time to our ping. We drop the connection.
""" """
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("Auto ping/pong: onAutoPingTimeout fired") self.factory.log.debug("Auto ping/pong: onAutoPingTimeout fired")
self.autoPingTimeoutCall = None self.autoPingTimeoutCall = None
self.dropConnection(abort=True) self.dropConnection(abort=True)
@ -960,14 +969,19 @@ class WebSocketProtocol(object):
""" """
if self.state != WebSocketProtocol.STATE_CLOSED: if self.state != WebSocketProtocol.STATE_CLOSED:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("dropping connection") self.factory.log.debug("dropping connection")
self.droppedByMe = True self.droppedByMe = True
# this code-path will be hit (*without* hitting
# _connectionLost) in some timeout scenarios (unit-tests
# cover these). However, sometimes we hit both.
self.state = WebSocketProtocol.STATE_CLOSED self.state = WebSocketProtocol.STATE_CLOSED
txaio.resolve(self.is_closed, self)
self._closeConnection(abort) self._closeConnection(abort)
else: else:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("skipping dropConnection since connection is already closed") self.factory.log.debug("skipping dropConnection since connection is already closed")
def failConnection(self, code=CLOSE_STATUS_CODE_GOING_AWAY, reason="Going Away"): def failConnection(self, code=CLOSE_STATUS_CODE_GOING_AWAY, reason="Going Away"):
""" """
@ -980,7 +994,7 @@ class WebSocketProtocol(object):
""" """
if self.state != WebSocketProtocol.STATE_CLOSED: if self.state != WebSocketProtocol.STATE_CLOSED:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("Failing connection : %s - %s" % (code, reason)) self.factory.log.debug("Failing connection : %s - %s" % (code, reason))
self.failedByMe = True self.failedByMe = True
@ -1001,7 +1015,7 @@ class WebSocketProtocol(object):
else: else:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("skipping failConnection since connection is already closed") self.factory.log.debug("skipping failConnection since connection is already closed")
def protocolViolation(self, reason): def protocolViolation(self, reason):
""" """
@ -1018,7 +1032,7 @@ class WebSocketProtocol(object):
:returns: bool -- True, when any further processing should be discontinued. :returns: bool -- True, when any further processing should be discontinued.
""" """
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("Protocol violation : %s" % reason) self.factory.log.debug("Protocol violation : %s" % reason)
self.failConnection(WebSocketProtocol.CLOSE_STATUS_CODE_PROTOCOL_ERROR, reason) self.failConnection(WebSocketProtocol.CLOSE_STATUS_CODE_PROTOCOL_ERROR, reason)
if self.failByDrop: if self.failByDrop:
return True return True
@ -1044,7 +1058,7 @@ class WebSocketProtocol(object):
:returns: bool -- True, when any further processing should be discontinued. :returns: bool -- True, when any further processing should be discontinued.
""" """
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("Invalid payload : %s" % reason) self.factory.log.debug("Invalid payload : %s" % reason)
self.failConnection(WebSocketProtocol.CLOSE_STATUS_CODE_INVALID_PAYLOAD, reason) self.failConnection(WebSocketProtocol.CLOSE_STATUS_CODE_INVALID_PAYLOAD, reason)
if self.failByDrop: if self.failByDrop:
return True return True
@ -1089,8 +1103,8 @@ class WebSocketProtocol(object):
configAttrLog.append((configAttr, getattr(self, configAttr), configAttrSource)) configAttrLog.append((configAttr, getattr(self, configAttr), configAttrSource))
if self.debug: if self.debug:
# self.factory._log(configAttrLog) # self.factory.log.debug(configAttrLog)
self.factory._log("\n" + pformat(configAttrLog)) self.factory.log.debug("\n" + pformat(configAttrLog))
# permessage-compress extension # permessage-compress extension
self._perMessageCompress = None self._perMessageCompress = None
@ -1179,7 +1193,7 @@ class WebSocketProtocol(object):
# set opening handshake timeout handler # set opening handshake timeout handler
if self.openHandshakeTimeout > 0: if self.openHandshakeTimeout > 0:
self.openHandshakeTimeoutCall = self.factory._callLater(self.openHandshakeTimeout, self.onOpenHandshakeTimeout) self.openHandshakeTimeoutCall = txaio.call_later(self.openHandshakeTimeout, self.onOpenHandshakeTimeout)
self.autoPingTimeoutCall = None self.autoPingTimeoutCall = None
self.autoPingPending = None self.autoPingPending = None
@ -1195,7 +1209,7 @@ class WebSocketProtocol(object):
# #
if not self.factory.isServer and self.serverConnectionDropTimeoutCall is not None: if not self.factory.isServer and self.serverConnectionDropTimeoutCall is not None:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("serverConnectionDropTimeoutCall.cancel") self.factory.log.debug("serverConnectionDropTimeoutCall.cancel")
self.serverConnectionDropTimeoutCall.cancel() self.serverConnectionDropTimeoutCall.cancel()
self.serverConnectionDropTimeoutCall = None self.serverConnectionDropTimeoutCall = None
@ -1203,20 +1217,25 @@ class WebSocketProtocol(object):
# #
if self.autoPingPendingCall: if self.autoPingPendingCall:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("Auto ping/pong: canceling autoPingPendingCall upon lost connection") self.factory.log.debug("Auto ping/pong: canceling autoPingPendingCall upon lost connection")
self.autoPingPendingCall.cancel() self.autoPingPendingCall.cancel()
self.autoPingPendingCall = None self.autoPingPendingCall = None
if self.autoPingTimeoutCall: if self.autoPingTimeoutCall:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("Auto ping/pong: canceling autoPingTimeoutCall upon lost connection") self.factory.log.debug("Auto ping/pong: canceling autoPingTimeoutCall upon lost connection")
self.autoPingTimeoutCall.cancel() self.autoPingTimeoutCall.cancel()
self.autoPingTimeoutCall = None self.autoPingTimeoutCall = None
self.state = WebSocketProtocol.STATE_CLOSED # check required here because in some scenarios dropConnection
# will already have resolved the Future/Deferred.
if self.state != WebSocketProtocol.STATE_CLOSED:
self.state = WebSocketProtocol.STATE_CLOSED
txaio.resolve(self.is_closed, self)
if self.wasServingFlashSocketPolicyFile: if self.wasServingFlashSocketPolicyFile:
if self.debug: if self.debug:
self.factory._log("connection dropped after serving Flash Socket Policy File") self.factory.log.debug("connection dropped after serving Flash Socket Policy File")
else: else:
if not self.wasClean: if not self.wasClean:
if not self.droppedByMe and self.wasNotCleanReason is None: if not self.droppedByMe and self.wasNotCleanReason is None:
@ -1231,7 +1250,7 @@ class WebSocketProtocol(object):
Modes: Hybi, Hixie Modes: Hybi, Hixie
""" """
self.factory._log("RX Octets from %s : octets = %s" % (self.peer, binascii.b2a_hex(data))) self.factory.log.debug("RX Octets from %s : octets = %s" % (self.peer, binascii.b2a_hex(data)))
def logTxOctets(self, data, sync): def logTxOctets(self, data, sync):
""" """
@ -1239,7 +1258,7 @@ class WebSocketProtocol(object):
Modes: Hybi, Hixie Modes: Hybi, Hixie
""" """
self.factory._log("TX Octets to %s : sync = %s, octets = %s" % (self.peer, sync, binascii.b2a_hex(data))) self.factory.log.debug("TX Octets to %s : sync = %s, octets = %s" % (self.peer, sync, binascii.b2a_hex(data)))
def logRxFrame(self, frameHeader, payload): def logRxFrame(self, frameHeader, payload):
""" """
@ -1256,7 +1275,7 @@ class WebSocketProtocol(object):
frameHeader.length, frameHeader.length,
data if frameHeader.opcode == 1 else binascii.b2a_hex(data)) data if frameHeader.opcode == 1 else binascii.b2a_hex(data))
self.factory._log("RX Frame from %s : fin = %s, rsv = %s, opcode = %s, mask = %s, length = %s, payload = %s" % info) self.factory.log.debug("RX Frame from %s : fin = %s, rsv = %s, opcode = %s, mask = %s, length = %s, payload = %s" % info)
def logTxFrame(self, frameHeader, payload, repeatLength, chopsize, sync): def logTxFrame(self, frameHeader, payload, repeatLength, chopsize, sync):
""" """
@ -1275,7 +1294,7 @@ class WebSocketProtocol(object):
sync, sync,
payload if frameHeader.opcode == 1 else binascii.b2a_hex(payload)) payload if frameHeader.opcode == 1 else binascii.b2a_hex(payload))
self.factory._log("TX Frame to %s : fin = %s, rsv = %s, opcode = %s, mask = %s, length = %s, repeat_length = %s, chopsize = %s, sync = %s, payload = %s" % info) self.factory.log.debug("TX Frame to %s : fin = %s, rsv = %s, opcode = %s, mask = %s, length = %s, repeat_length = %s, chopsize = %s, sync = %s, payload = %s" % info)
def _dataReceived(self, data): def _dataReceived(self, data):
""" """
@ -1332,7 +1351,7 @@ class WebSocketProtocol(object):
# ignore any data received after WS was closed # ignore any data received after WS was closed
# #
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("received data in STATE_CLOSED") self.factory.log.debug("received data in STATE_CLOSED")
# should not arrive here (invalid state) # should not arrive here (invalid state)
# #
@ -1388,13 +1407,13 @@ class WebSocketProtocol(object):
self.logTxOctets(e[0], e[1]) self.logTxOctets(e[0], e[1])
else: else:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("skipped delayed write, since connection is closed") self.factory.log.debug("skipped delayed write, since connection is closed")
# we need to reenter the reactor to make the latter # we need to reenter the reactor to make the latter
# reenter the OS network stack, so that octets # reenter the OS network stack, so that octets
# can get on the wire. Note: this is a "heuristic", # can get on the wire. Note: this is a "heuristic",
# since there is no (easy) way to really force out # since there is no (easy) way to really force out
# octets from the OS network stack to wire. # octets from the OS network stack to wire.
self.factory._callLater(WebSocketProtocol._QUEUED_WRITE_DELAY, self._send) txaio.call_later(WebSocketProtocol._QUEUED_WRITE_DELAY, self._send)
else: else:
self.triggered = False self.triggered = False
@ -1835,7 +1854,7 @@ class WebSocketProtocol(object):
if self._isMessageCompressed: if self._isMessageCompressed:
compressedLen = len(payload) compressedLen = len(payload)
if self.debug: if self.debug:
self.factory._log("RX compressed [%d]: %s" % (compressedLen, binascii.b2a_hex(payload))) self.factory.log.debug("RX compressed [%d]: %s" % (compressedLen, binascii.b2a_hex(payload)))
payload = self._perMessageCompress.decompressMessageData(payload) payload = self._perMessageCompress.decompressMessageData(payload)
uncompressedLen = len(payload) uncompressedLen = len(payload)
@ -1891,7 +1910,7 @@ class WebSocketProtocol(object):
return False return False
# if self.debug: # if self.debug:
# self.factory._log("Traffic statistics:\n" + str(self.trafficStats)) # self.factory.log.debug("Traffic statistics:\n" + str(self.trafficStats))
if self.state == WebSocketProtocol.STATE_OPEN: if self.state == WebSocketProtocol.STATE_OPEN:
self.trafficStats.incomingWebSocketMessages += 1 self.trafficStats.incomingWebSocketMessages += 1
@ -1939,10 +1958,9 @@ class WebSocketProtocol(object):
# #
if self.autoPingPending: if self.autoPingPending:
try: try:
p = payload.decode('utf8') if payload == self.autoPingPending:
if p == self.autoPingPending:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("Auto ping/pong: received pending pong for auto-ping/pong") self.factory.log.debug("Auto ping/pong: received pending pong for auto-ping/pong")
if self.autoPingTimeoutCall: if self.autoPingTimeoutCall:
self.autoPingTimeoutCall.cancel() self.autoPingTimeoutCall.cancel()
@ -1951,13 +1969,13 @@ class WebSocketProtocol(object):
self.autoPingTimeoutCall = None self.autoPingTimeoutCall = None
if self.autoPingInterval: if self.autoPingInterval:
self.autoPingPendingCall = self.factory._callLater(self.autoPingInterval, self._sendAutoPing) self.autoPingPendingCall = txaio.call_later(self.autoPingInterval, self._sendAutoPing)
else: else:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("Auto ping/pong: received non-pending pong") self.factory.log.debug("Auto ping/pong: received non-pending pong")
except: except:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("Auto ping/pong: received non-pending pong") self.factory.log.debug("Auto ping/pong: received non-pending pong")
# fire app-level callback # fire app-level callback
# #
@ -2085,18 +2103,18 @@ class WebSocketProtocol(object):
def _sendAutoPing(self): def _sendAutoPing(self):
# Sends an automatic ping and sets up a timeout. # Sends an automatic ping and sets up a timeout.
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("Auto ping/pong: sending ping auto-ping/pong") self.factory.log.debug("Auto ping/pong: sending ping auto-ping/pong")
self.autoPingPendingCall = None self.autoPingPendingCall = None
self.autoPingPending = newid(self.autoPingSize) self.autoPingPending = newid(self.autoPingSize).encode('utf8')
self.sendPing(self.autoPingPending.encode('utf8')) self.sendPing(self.autoPingPending)
if self.autoPingTimeout: if self.autoPingTimeout:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("Auto ping/pong: expecting ping in {0} seconds for auto-ping/pong".format(self.autoPingTimeout)) self.factory.log.debug("Auto ping/pong: expecting ping in {0} seconds for auto-ping/pong".format(self.autoPingTimeout))
self.autoPingTimeoutCall = self.factory._callLater(self.autoPingTimeout, self.onAutoPingTimeout) self.autoPingTimeoutCall = txaio.call_later(self.autoPingTimeout, self.onAutoPingTimeout)
def sendPong(self, payload=None): def sendPong(self, payload=None):
""" """
@ -2128,11 +2146,11 @@ class WebSocketProtocol(object):
""" """
if self.state == WebSocketProtocol.STATE_CLOSING: if self.state == WebSocketProtocol.STATE_CLOSING:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("ignoring sendCloseFrame since connection is closing") self.factory.log.debug("ignoring sendCloseFrame since connection is closing")
elif self.state == WebSocketProtocol.STATE_CLOSED: elif self.state == WebSocketProtocol.STATE_CLOSED:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("ignoring sendCloseFrame since connection already closed") self.factory.log.debug("ignoring sendCloseFrame since connection already closed")
elif self.state in [WebSocketProtocol.STATE_PROXY_CONNECTING, WebSocketProtocol.STATE_CONNECTING]: elif self.state in [WebSocketProtocol.STATE_PROXY_CONNECTING, WebSocketProtocol.STATE_CONNECTING]:
raise Exception("cannot close a connection not yet connected") raise Exception("cannot close a connection not yet connected")
@ -2160,7 +2178,7 @@ class WebSocketProtocol(object):
# drop connection when timeout on receiving close handshake reply # drop connection when timeout on receiving close handshake reply
if self.closedByMe and self.closeHandshakeTimeout > 0: if self.closedByMe and self.closeHandshakeTimeout > 0:
self.closeHandshakeTimeoutCall = self.factory._callLater(self.closeHandshakeTimeout, self.onCloseHandshakeTimeout) self.closeHandshakeTimeoutCall = txaio.call_later(self.closeHandshakeTimeout, self.onCloseHandshakeTimeout)
else: else:
raise Exception("logic error") raise Exception("logic error")
@ -2516,7 +2534,7 @@ class WebSocketProtocol(object):
i += pfs i += pfs
# if self.debug: # if self.debug:
# self.factory._log("Traffic statistics:\n" + str(self.trafficStats)) # self.factory.log.debug("Traffic statistics:\n" + str(self.trafficStats))
def _parseExtensionsHeader(self, header, removeQuotes=True): def _parseExtensionsHeader(self, header, removeQuotes=True):
""" """
@ -2715,7 +2733,7 @@ class WebSocketServerProtocol(WebSocketProtocol):
WebSocketProtocol._connectionMade(self) WebSocketProtocol._connectionMade(self)
self.factory.countConnections += 1 self.factory.countConnections += 1
if self.debug: if self.debug:
self.factory._log("connection accepted from peer %s" % self.peer) self.factory.log.debug("connection accepted from peer %s" % self.peer)
def _connectionLost(self, reason): def _connectionLost(self, reason):
""" """
@ -2727,7 +2745,7 @@ class WebSocketServerProtocol(WebSocketProtocol):
WebSocketProtocol._connectionLost(self, reason) WebSocketProtocol._connectionLost(self, reason)
self.factory.countConnections -= 1 self.factory.countConnections -= 1
if self.debug: if self.debug:
self.factory._log("connection from %s lost" % self.peer) self.factory.log.debug("connection from %s lost" % self.peer)
def processProxyConnect(self): def processProxyConnect(self):
raise Exception("Autobahn isn't a proxy server") raise Exception("Autobahn isn't a proxy server")
@ -2749,7 +2767,7 @@ class WebSocketServerProtocol(WebSocketProtocol):
self.http_request_data = self.data[:end_of_header + 4] self.http_request_data = self.data[:end_of_header + 4]
if self.debug: if self.debug:
self.factory._log("received HTTP request:\n\n%s\n\n" % self.http_request_data) self.factory.log.debug("received HTTP request:\n\n%s\n\n" % self.http_request_data)
# extract HTTP status line and headers # extract HTTP status line and headers
# #
@ -2758,8 +2776,8 @@ class WebSocketServerProtocol(WebSocketProtocol):
# validate WebSocket opening handshake client request # validate WebSocket opening handshake client request
# #
if self.debug: if self.debug:
self.factory._log("received HTTP status line in opening handshake : %s" % str(self.http_status_line)) self.factory.log.debug("received HTTP status line in opening handshake : %s" % str(self.http_status_line))
self.factory._log("received HTTP headers in opening handshake : %s" % str(self.http_headers)) self.factory.log.debug("received HTTP headers in opening handshake : %s" % str(self.http_headers))
# HTTP Request line : METHOD, VERSION # HTTP Request line : METHOD, VERSION
# #
@ -2818,7 +2836,7 @@ class WebSocketServerProtocol(WebSocketProtocol):
return self.failHandshake("port %d in HTTP Host header '%s' does not match server listening port %s" % (port, str(self.http_request_host), self.factory.externalPort)) return self.failHandshake("port %d in HTTP Host header '%s' does not match server listening port %s" % (port, str(self.http_request_host), self.factory.externalPort))
else: else:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("skipping opening handshake port checking - neither WS URL nor external port set") self.factory.log.debug("skipping opening handshake port checking - neither WS URL nor external port set")
self.http_request_host = h self.http_request_host = h
@ -2829,7 +2847,7 @@ class WebSocketServerProtocol(WebSocketProtocol):
return self.failHandshake("missing port in HTTP Host header '%s' and server runs on non-standard port %d (wss = %s)" % (str(self.http_request_host), self.factory.externalPort, self.factory.isSecure)) return self.failHandshake("missing port in HTTP Host header '%s' and server runs on non-standard port %d (wss = %s)" % (str(self.http_request_host), self.factory.externalPort, self.factory.isSecure))
else: else:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("skipping opening handshake port checking - neither WS URL nor external port set") self.factory.log.debug("skipping opening handshake port checking - neither WS URL nor external port set")
# Upgrade # Upgrade
# #
@ -2857,15 +2875,15 @@ class WebSocketServerProtocol(WebSocketProtocol):
if 'after' in self.http_request_params and len(self.http_request_params['after']) > 0: if 'after' in self.http_request_params and len(self.http_request_params['after']) > 0:
after = int(self.http_request_params['after'][0]) after = int(self.http_request_params['after'][0])
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("HTTP Upgrade header missing : render server status page and meta-refresh-redirecting to %s after %d seconds" % (url, after)) self.factory.log.debug("HTTP Upgrade header missing : render server status page and meta-refresh-redirecting to %s after %d seconds" % (url, after))
self.sendServerStatus(url, after) self.sendServerStatus(url, after)
else: else:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("HTTP Upgrade header missing : 303-redirecting to %s" % url) self.factory.log.debug("HTTP Upgrade header missing : 303-redirecting to %s" % url)
self.sendRedirect(url) self.sendRedirect(url)
else: else:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("HTTP Upgrade header missing : render server status page") self.factory.log.debug("HTTP Upgrade header missing : render server status page")
self.sendServerStatus() self.sendServerStatus()
self.dropConnection(abort=False) self.dropConnection(abort=False)
return return
@ -2895,14 +2913,14 @@ class WebSocketServerProtocol(WebSocketProtocol):
# #
if 'sec-websocket-version' not in self.http_headers: if 'sec-websocket-version' not in self.http_headers:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("Hixie76 protocol detected") self.factory.log.debug("Hixie76 protocol detected")
if self.allowHixie76: if self.allowHixie76:
version = 0 version = 0
else: else:
return self.failHandshake("WebSocket connection denied - Hixie76 protocol mode disabled.") return self.failHandshake("WebSocket connection denied - Hixie76 protocol mode disabled.")
else: else:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("Hybi protocol detected") self.factory.log.debug("Hybi protocol detected")
if http_headers_cnt["sec-websocket-version"] > 1: if http_headers_cnt["sec-websocket-version"] > 1:
return self.failHandshake("HTTP Sec-WebSocket-Version header appears more than once in opening handshake request") return self.failHandshake("HTTP Sec-WebSocket-Version header appears more than once in opening handshake request")
try: try:
@ -3020,7 +3038,7 @@ class WebSocketServerProtocol(WebSocketProtocol):
else: else:
key3 = self.data[end_of_header + 4:end_of_header + 4 + 8] key3 = self.data[end_of_header + 4:end_of_header + 4 + 8]
if self.debug: if self.debug:
self.factory._log("received HTTP request body containing key3 for Hixie-76: %s" % key3) self.factory.log.debug("received HTTP request body containing key3 for Hixie-76: %s" % key3)
# Ok, got complete HS input, remember rest (if any) # Ok, got complete HS input, remember rest (if any)
# #
@ -3067,11 +3085,11 @@ class WebSocketServerProtocol(WebSocketProtocol):
flash_policy_file_request = self.data.find(b"<policy-file-request/>\x00") flash_policy_file_request = self.data.find(b"<policy-file-request/>\x00")
if flash_policy_file_request >= 0: if flash_policy_file_request >= 0:
if self.debug: if self.debug:
self.factory._log("received Flash Socket Policy File request") self.factory.log.debug("received Flash Socket Policy File request")
if self.serveFlashSocketPolicy: if self.serveFlashSocketPolicy:
if self.debug: if self.debug:
self.factory._log("sending Flash Socket Policy File :\n%s" % self.flashSocketPolicy) self.factory.log.debug("sending Flash Socket Policy File :\n%s" % self.flashSocketPolicy)
self.sendData(self.flashSocketPolicy.encode('utf8')) self.sendData(self.flashSocketPolicy.encode('utf8'))
@ -3080,7 +3098,7 @@ class WebSocketServerProtocol(WebSocketProtocol):
self.dropConnection() self.dropConnection()
else: else:
if self.debug: if self.debug:
self.factory._log("No Flash Policy File served. You might want to serve a Flask Socket Policy file on the destination port since you received a request for it. See WebSocketServerFactory.serveFlashSocketPolicy and WebSocketServerFactory.flashSocketPolicy") self.factory.log.debug("No Flash Policy File served. You might want to serve a Flask Socket Policy file on the destination port since you received a request for it. See WebSocketServerFactory.serveFlashSocketPolicy and WebSocketServerFactory.flashSocketPolicy")
def succeedHandshake(self, res): def succeedHandshake(self, res):
""" """
@ -3121,7 +3139,7 @@ class WebSocketServerProtocol(WebSocketProtocol):
for (extension, params) in self.websocket_extensions: for (extension, params) in self.websocket_extensions:
if self.debug: if self.debug:
self.factory._log("parsed WebSocket extension '%s' with params '%s'" % (extension, params)) self.factory.log.debug("parsed WebSocket extension '%s' with params '%s'" % (extension, params))
# process permessage-compress extension # process permessage-compress extension
# #
@ -3137,7 +3155,7 @@ class WebSocketServerProtocol(WebSocketProtocol):
else: else:
if self.debug: if self.debug:
self.factory._log("client requested '%s' extension we don't support or which is not activated" % extension) self.factory.log.debug("client requested '%s' extension we don't support or which is not activated" % extension)
# handle permessage-compress offers by the client # handle permessage-compress offers by the client
# #
@ -3150,7 +3168,7 @@ class WebSocketServerProtocol(WebSocketProtocol):
extensionResponse.append(accept.getExtensionString()) extensionResponse.append(accept.getExtensionString())
else: else:
if self.debug: if self.debug:
self.factory._log("client request permessage-compress extension, but we did not accept any offer [%s]" % pmceOffers) self.factory.log.debug("client request permessage-compress extension, but we did not accept any offer [%s]" % pmceOffers)
# build response to complete WebSocket handshake # build response to complete WebSocket handshake
# #
@ -3190,15 +3208,15 @@ class WebSocketServerProtocol(WebSocketProtocol):
response += "Sec-WebSocket-Origin: %s\x0d\x0a" % str(self.websocket_origin) response += "Sec-WebSocket-Origin: %s\x0d\x0a" % str(self.websocket_origin)
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log('factory isSecure = %s port = %s' % (self.factory.isSecure, self.factory.externalPort)) self.factory.log.debug('factory isSecure = %s port = %s' % (self.factory.isSecure, self.factory.externalPort))
if self.factory.externalPort and ((self.factory.isSecure and self.factory.externalPort != 443) or ((not self.factory.isSecure) and self.factory.externalPort != 80)): if self.factory.externalPort and ((self.factory.isSecure and self.factory.externalPort != 443) or ((not self.factory.isSecure) and self.factory.externalPort != 80)):
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log('factory running on non-default port') self.factory.log.debug('factory running on non-default port')
response_port = ':' + str(self.factory.externalPort) response_port = ':' + str(self.factory.externalPort)
else: else:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log('factory running on default port') self.factory.log.debug('factory running on default port')
response_port = '' response_port = ''
# FIXME: check this! But see below .. # FIXME: check this! But see below ..
@ -3245,12 +3263,12 @@ class WebSocketServerProtocol(WebSocketProtocol):
# send out opening handshake response # send out opening handshake response
# #
if self.debug: if self.debug:
self.factory._log("sending HTTP response:\n\n%s" % response) self.factory.log.debug("sending HTTP response:\n\n%s" % response)
self.sendData(response.encode('utf8')) self.sendData(response.encode('utf8'))
if response_body: if response_body:
if self.debug: if self.debug:
self.factory._log("sending HTTP response body:\n\n%s" % binascii.b2a_hex(response_body)) self.factory.log.debug("sending HTTP response body:\n\n%s" % binascii.b2a_hex(response_body))
self.sendData(response_body) self.sendData(response_body)
# save response for testsuite # save response for testsuite
@ -3265,7 +3283,7 @@ class WebSocketServerProtocol(WebSocketProtocol):
# #
if self.openHandshakeTimeoutCall is not None: if self.openHandshakeTimeoutCall is not None:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("openHandshakeTimeoutCall.cancel") self.factory.log.debug("openHandshakeTimeoutCall.cancel")
self.openHandshakeTimeoutCall.cancel() self.openHandshakeTimeoutCall.cancel()
self.openHandshakeTimeoutCall = None self.openHandshakeTimeoutCall = None
@ -3278,7 +3296,7 @@ class WebSocketServerProtocol(WebSocketProtocol):
# automatic ping/pong # automatic ping/pong
# #
if self.autoPingInterval: if self.autoPingInterval:
self.autoPingPendingCall = self.factory._callLater(self.autoPingInterval, self._sendAutoPing) self.autoPingPendingCall = txaio.call_later(self.autoPingInterval, self._sendAutoPing)
# fire handler on derived class # fire handler on derived class
# #
@ -3297,7 +3315,7 @@ class WebSocketServerProtocol(WebSocketProtocol):
error response and then drop the connection. error response and then drop the connection.
""" """
if self.debug: if self.debug:
self.factory._log("failing WebSocket opening handshake ('%s')" % reason) self.factory.log.debug("failing WebSocket opening handshake ('%s')" % reason)
self.sendHttpErrorResponse(code, reason, responseHeaders) self.sendHttpErrorResponse(code, reason, responseHeaders)
self.dropConnection(abort=False) self.dropConnection(abort=False)
@ -3728,7 +3746,7 @@ class WebSocketClientProtocol(WebSocketProtocol):
""" """
WebSocketProtocol._connectionMade(self) WebSocketProtocol._connectionMade(self)
if self.debug: if self.debug:
self.factory._log("connection to %s established" % self.peer) self.factory.log.debug("connection to %s established" % self.peer)
if not self.factory.isServer and self.factory.proxy is not None: if not self.factory.isServer and self.factory.proxy is not None:
# start by doing a HTTP/CONNECT for explicit proxies # start by doing a HTTP/CONNECT for explicit proxies
@ -3746,7 +3764,7 @@ class WebSocketClientProtocol(WebSocketProtocol):
""" """
WebSocketProtocol._connectionLost(self, reason) WebSocketProtocol._connectionLost(self, reason)
if self.debug: if self.debug:
self.factory._log("connection to %s lost" % self.peer) self.factory.log.debug("connection to %s lost" % self.peer)
def startProxyConnect(self): def startProxyConnect(self):
""" """
@ -3760,7 +3778,7 @@ class WebSocketClientProtocol(WebSocketProtocol):
request += "\x0d\x0a" request += "\x0d\x0a"
if self.debug: if self.debug:
self.factory._log(request) self.factory.log.debug(request)
self.sendData(request) self.sendData(request)
@ -3775,7 +3793,7 @@ class WebSocketClientProtocol(WebSocketProtocol):
http_response_data = self.data[:end_of_header + 4] http_response_data = self.data[:end_of_header + 4]
if self.debug: if self.debug:
self.factory._log("received HTTP response:\n\n%s\n\n" % http_response_data) self.factory.log.debug("received HTTP response:\n\n%s\n\n" % http_response_data)
# extract HTTP status line and headers # extract HTTP status line and headers
# #
@ -3784,8 +3802,8 @@ class WebSocketClientProtocol(WebSocketProtocol):
# validate proxy connect response # validate proxy connect response
# #
if self.debug: if self.debug:
self.factory._log("received HTTP status line for proxy connect request : %s" % str(http_status_line)) self.factory.log.debug("received HTTP status line for proxy connect request : %s" % str(http_status_line))
self.factory._log("received HTTP headers for proxy connect request : %s" % str(http_headers)) self.factory.log.debug("received HTTP headers for proxy connect request : %s" % str(http_headers))
# Response Line # Response Line
# #
@ -3840,7 +3858,7 @@ class WebSocketClientProtocol(WebSocketProtocol):
connection. connection.
""" """
if self.debug: if self.debug:
self.factory._log("failing proxy connect ('%s')" % reason) self.factory.log.debug("failing proxy connect ('%s')" % reason)
self.dropConnection(abort=True) self.dropConnection(abort=True)
def createHixieKey(self): def createHixieKey(self):
@ -3958,7 +3976,7 @@ class WebSocketClientProtocol(WebSocketProtocol):
self.sendData(request_body) self.sendData(request_body)
if self.debug: if self.debug:
self.factory._log(request) self.factory.log.debug(request)
def processHandshake(self): def processHandshake(self):
""" """
@ -3971,7 +3989,7 @@ class WebSocketClientProtocol(WebSocketProtocol):
self.http_response_data = self.data[:end_of_header + 4] self.http_response_data = self.data[:end_of_header + 4]
if self.debug: if self.debug:
self.factory._log("received HTTP response:\n\n%s\n\n" % self.http_response_data) self.factory.log.debug("received HTTP response:\n\n%s\n\n" % self.http_response_data)
# extract HTTP status line and headers # extract HTTP status line and headers
# #
@ -3980,8 +3998,8 @@ class WebSocketClientProtocol(WebSocketProtocol):
# validate WebSocket opening handshake server response # validate WebSocket opening handshake server response
# #
if self.debug: if self.debug:
self.factory._log("received HTTP status line in opening handshake : %s" % str(self.http_status_line)) self.factory.log.debug("received HTTP status line in opening handshake : %s" % str(self.http_status_line))
self.factory._log("received HTTP headers in opening handshake : %s" % str(self.http_headers)) self.factory.log.debug("received HTTP headers in opening handshake : %s" % str(self.http_headers))
# Response Line # Response Line
# #
@ -4072,7 +4090,7 @@ class WebSocketClientProtocol(WebSocketProtocol):
for (extension, params) in websocket_extensions: for (extension, params) in websocket_extensions:
if self.debug: if self.debug:
self.factory._log("parsed WebSocket extension '%s' with params '%s'" % (extension, params)) self.factory.log.debug("parsed WebSocket extension '%s' with params '%s'" % (extension, params))
# process permessage-compress extension # process permessage-compress extension
# #
@ -4142,7 +4160,7 @@ class WebSocketClientProtocol(WebSocketProtocol):
# #
if self.openHandshakeTimeoutCall is not None: if self.openHandshakeTimeoutCall is not None:
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("openHandshakeTimeoutCall.cancel") self.factory.log.debug("openHandshakeTimeoutCall.cancel")
self.openHandshakeTimeoutCall.cancel() self.openHandshakeTimeoutCall.cancel()
self.openHandshakeTimeoutCall = None self.openHandshakeTimeoutCall = None
@ -4187,7 +4205,7 @@ class WebSocketClientProtocol(WebSocketProtocol):
connection. connection.
""" """
if self.debug: if self.debug:
self.factory._log("failing WebSocket opening handshake ('%s')" % reason) self.factory.log.debug("failing WebSocket opening handshake ('%s')" % reason)
self.dropConnection(abort=True) self.dropConnection(abort=True)

View File

@ -0,0 +1,295 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from __future__ import absolute_import, print_function
import os
import struct
if os.environ.get('USE_TWISTED', False):
from twisted.trial import unittest
from twisted.internet.address import IPv4Address
from twisted.internet.task import Clock
from six import PY3
from autobahn.twisted.websocket import WebSocketServerProtocol
from autobahn.twisted.websocket import WebSocketServerFactory
from autobahn.twisted.websocket import WebSocketClientProtocol
from autobahn.twisted.websocket import WebSocketClientFactory
from mock import MagicMock, patch
from txaio.testutil import replace_loop
from base64 import b64decode
@patch('base64.b64encode')
def create_client_frame(b64patch, **kwargs):
"""
Kind-of hack-y; maybe better to re-factor the Protocol to have a
frame-encoder method-call? Anyway, makes a throwaway protocol
encode a frame for us, collects the .sendData call and returns
the data that would have gone out. Accepts all the kwargs that
WebSocketClientProtocol.sendFrame() accepts.
"""
# only real way to inject a "known" secret-key for the headers
# to line up... :/
b64patch.return_value = b'QIatSt9QkZPyS4QQfdufO8TgkL0='
factory = WebSocketClientFactory(protocols=['wamp.2.json'])
factory.protocol = WebSocketClientProtocol
factory.doStart()
proto = factory.buildProtocol(IPv4Address('TCP', '127.0.0.9', 65534))
proto.transport = MagicMock()
proto.connectionMade()
proto.data = mock_handshake_server
proto.processHandshake()
data = []
def collect(d, *args):
data.append(d)
proto.sendData = collect
proto.sendFrame(**kwargs)
return b''.join(data)
# beware the evils of line-endings...
mock_handshake_client = b'GET / HTTP/1.1\r\nUser-Agent: AutobahnPython/0.10.2\r\nHost: localhost:80\r\nUpgrade: WebSocket\r\nConnection: Upgrade\r\nPragma: no-cache\r\nCache-Control: no-cache\r\nSec-WebSocket-Key: 6Jid6RgXpH0RVegaNSs/4g==\r\nSec-WebSocket-Protocol: wamp.2.json\r\nSec-WebSocket-Version: 13\r\n\r\n'
mock_handshake_server = b'HTTP/1.1 101 Switching Protocols\r\nServer: AutobahnPython/0.10.2\r\nX-Powered-By: AutobahnPython/0.10.2\r\nUpgrade: WebSocket\r\nConnection: Upgrade\r\nSec-WebSocket-Protocol: wamp.2.json\r\nSec-WebSocket-Accept: QIatSt9QkZPyS4QQfdufO8TgkL0=\r\n\r\n\x81~\x02\x19[1,"crossbar",{"roles":{"subscriber":{"features":{"publisher_identification":true,"pattern_based_subscription":true,"subscription_revocation":true}},"publisher":{"features":{"publisher_identification":true,"publisher_exclusion":true,"subscriber_blackwhite_listing":true}},"caller":{"features":{"caller_identification":true,"progressive_call_results":true}},"callee":{"features":{"progressive_call_results":true,"pattern_based_registration":true,"registration_revocation":true,"shared_registration":true,"caller_identification":true}}}}]\x18'
class TestClient(unittest.TestCase):
def setUp(self):
self.factory = WebSocketClientFactory(protocols=['wamp.2.json'])
self.factory.protocol = WebSocketClientProtocol
self.factory.doStart()
self.proto = self.factory.buildProtocol(IPv4Address('TCP', '127.0.0.1', 65534))
self.transport = MagicMock()
self.proto.transport = self.transport
self.proto.connectionMade()
def tearDown(self):
self.factory.doStop()
# not really necessary, but ...
del self.factory
del self.proto
def test_unclean_timeout_client(self):
"""
make a delayed call to drop the connection (client-side)
"""
if False:
self.proto.debug = True
self.proto.factory._log = print
# get to STATE_OPEN
self.proto.websocket_key = b64decode('6Jid6RgXpH0RVegaNSs/4g==')
self.proto.data = mock_handshake_server
self.proto.processHandshake()
self.assertEqual(self.proto.state, WebSocketServerProtocol.STATE_OPEN)
self.assertTrue(self.proto.serverConnectionDropTimeout > 0)
with replace_loop(Clock()) as reactor:
# now 'do the test' and transition to CLOSING
self.proto.sendCloseFrame()
self.proto.onCloseFrame(1000, b"raw reason")
# check we scheduled a call
self.assertEqual(len(reactor.calls), 1)
self.assertEqual(reactor.calls[0].func, self.proto.onServerConnectionDropTimeout)
self.assertEqual(reactor.calls[0].getTime(), self.proto.serverConnectionDropTimeout)
# now, advance the clock past the call (and thereby
# execute it)
reactor.advance(self.proto.closeHandshakeTimeout + 1)
# we should have called abortConnection
self.assertEqual("call.abortConnection()", str(self.proto.transport.method_calls[-1]))
self.assertTrue(self.proto.transport.abortConnection.called)
# ...too "internal" for an assert?
self.assertEqual(self.proto.state, WebSocketServerProtocol.STATE_CLOSED)
class TestPing(unittest.TestCase):
def setUp(self):
if False:
# debug leftover reactor events
import twisted.internet.base
twisted.internet.base.DelayedCall.debug = True
self.factory = WebSocketServerFactory(protocols=['wamp.2.json'])
self.factory.protocol = WebSocketServerProtocol
self.factory.doStart()
self.proto = self.factory.buildProtocol(IPv4Address('TCP', '127.0.0.1', 65534))
self.transport = MagicMock()
self.proto.transport = self.transport
self.proto.connectionMade()
def tearDown(self):
self.factory.doStop()
# not really necessary, but ...
del self.factory
del self.proto
def test_unclean_timeout(self):
"""
make a delayed call to drop the connection
"""
# first we have to drive the protocol to STATE_CLOSING
# ... which we achieve by sendCloseFrame after we're in
# STATE_OPEN
# XXX double-check this is the correct code-path to get here
# "normally"?
if False:
self.proto.debug = True
self.proto.factory._log = print
# get to STATE_OPEN
self.proto.data = mock_handshake_client
self.proto.processHandshake()
self.assertTrue(self.proto.state == WebSocketServerProtocol.STATE_OPEN)
with replace_loop(Clock()) as reactor:
# now 'do the test' and transition to CLOSING
self.proto.sendCloseFrame()
# check we scheduled a call
self.assertEqual(len(reactor.calls), 1)
self.assertEqual(reactor.calls[0].func, self.proto.onCloseHandshakeTimeout)
self.assertEqual(reactor.calls[0].getTime(), self.proto.closeHandshakeTimeout)
# now, advance the clock past the call (and thereby
# execute it)
reactor.advance(self.proto.closeHandshakeTimeout + 1)
# we should have called abortConnection
self.assertEqual("call.abortConnection()", str(self.proto.transport.method_calls[-1]))
self.assertTrue(self.proto.transport.abortConnection.called)
# ...too "internal" for an assert?
self.assertEqual(self.proto.state, WebSocketServerProtocol.STATE_CLOSED)
def test_auto_pingpong_timeout(self):
"""
autoping and autoping-timeout timing
"""
if False:
self.proto.debug = True
self.proto.factory._log = print
self.proto.debugCodePaths = True
# options are evaluated in succeedHandshake, called below
self.proto.autoPingInterval = 5
self.proto.autoPingTimeout = 2
with replace_loop(Clock()) as reactor:
# get to STATE_OPEN
self.proto.data = mock_handshake_client
self.proto.processHandshake()
self.assertTrue(self.proto.state == WebSocketServerProtocol.STATE_OPEN)
# we should have scheduled an autoPing
self.assertEqual(1, len(reactor.calls))
self.assertEqual(self.proto._sendAutoPing, reactor.calls[0].func)
# ^^ un-unit-testy to assert on internal method?
# advance past first auto-ping timeout
reactor.advance(5)
# first element from args tuple from transport.write()
# call is our data
self.assertTrue(self.transport.write.called)
data = self.transport.write.call_args[0][0]
if PY3:
_data = bytes([data[0]])
else:
_data = data[0]
# the opcode is the lower 7 bits of the first byte.
(opcode,) = struct.unpack("B", _data)
opcode = opcode & (~0x80)
# ... and should be "9" for ping
self.assertEqual(9, opcode)
# Because we have autoPingTimeout there should be
# another delayed-called created now
self.assertEqual(1, len(reactor.calls))
self.assertEqual(self.proto.onAutoPingTimeout, reactor.calls[0].func)
self.assertNotEqual(self.proto.state, self.proto.STATE_CLOSED)
# ...which we'll now cause to trigger, aborting the connection
reactor.advance(3)
self.assertEqual(self.proto.state, self.proto.STATE_CLOSED)
def test_auto_ping_got_pong(self):
"""
auto-ping with correct reply cancels timeout
"""
if False:
self.proto.debug = True
self.proto.factory._log = print
self.proto.debugCodePaths = True
# options are evaluated in succeedHandshake, called below
self.proto.autoPingInterval = 5
self.proto.autoPingTimeout = 2
with replace_loop(Clock()) as reactor:
# get to STATE_OPEN
self.proto.data = mock_handshake_client
self.proto.processHandshake()
self.assertTrue(self.proto.state == WebSocketServerProtocol.STATE_OPEN)
# we should have scheduled an autoPing
self.assertEqual(1, len(reactor.calls))
self.assertEqual(self.proto._sendAutoPing, reactor.calls[0].func)
# ^^ un-unit-testy to assert on internal method?
# advance past first auto-ping timeout
reactor.advance(5)
# should have an auto-ping timeout scheduled, and we
# save it for later (to check it got cancelled)
self.assertEqual(1, len(reactor.calls))
self.assertEqual(self.proto.onAutoPingTimeout, reactor.calls[0].func)
timeout_call = reactor.calls[0]
# elsewhere we check that we actually send an opcode-9
# message; now we just blindly inject our own reply
# with a PONG frame
frame = create_client_frame(opcode=10, payload=self.proto.autoPingPending)
self.proto.data = frame
# really needed twice; does header first, then rest
self.proto.processData()
self.proto.processData()
# which should have cancelled the call
self.assertTrue(timeout_call.cancelled)

View File

@ -12,10 +12,12 @@ all:
@echo "" @echo ""
build: build:
scons #scons
sphinx-build -A cstatic="//tavendo-common-static.s3-eu-west-1.amazonaws.com" -b html . _build
build_no_network: build_no_network:
scons --no_network #scons --no_network
sphinx-build -A no_network=1 -D no_network=1 -A cstatic="http://127.0.0.1:8888" -b html . _build
test: build test: build
python serve.py --root ./_build --silence python serve.py --root ./_build --silence

View File

@ -10,6 +10,8 @@ You will need to have Python and [SCons](http://www.scons.org/) installed. To in
make install_deps make install_deps
``` ```
**Note:** If you want to use this in a virtualenv, you'll have to install the SCons package for your distribution and use ``virtualenv --system-site-packages`` to build the venv. Then, activate it and install dependencies as above. To run SCons you'll have to do ``python `which scons` `` so that it uses the interpreter from your virtualenv.
Then, to get help on available build targets, just type Then, to get help on available build targets, just type
```sh ```sh

View File

@ -1,19 +1,19 @@
############################################################################### ###############################################################################
# #
# The MIT License (MIT) # The MIT License (MIT)
# #
# Copyright (c) Tavendo GmbH # Copyright (c) Tavendo GmbH
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights # in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is # copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions: # furnished to do so, subject to the following conditions:
# #
# The above copyright notice and this permission notice shall be included in # The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software. # all copies or substantial portions of the Software.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE

BIN
doc/_static/wamp-demos.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

@ -11,30 +11,30 @@ The asynchronous programming approach
|Ab| is written according to a programming paradigm called *asynchronous programming* (or *event driven programming*) and implemented using *non-blocking* execution - and both go hand in hand. |Ab| is written according to a programming paradigm called *asynchronous programming* (or *event driven programming*) and implemented using *non-blocking* execution - and both go hand in hand.
A very good technical introduction to these concepts can be found in `this chapter <http://krondo.com/?p=1209>`__ of an "Introduction to Asynchronous Programming and Twisted". A very good technical introduction to these concepts can be found in `this chapter <http://krondo.com/?p=1209>`_ of an "Introduction to Asynchronous Programming and Twisted".
Here are two more presentations that introduce event-driven programming in Python Here are two more presentations that introduce event-driven programming in Python
* `Alex Martelli - Don't call us, we'll call you: callback patterns and idioms <https://www.youtube.com/watch?v=LCZRJStwkKM>`__ * `Alex Martelli - Don't call us, we'll call you: callback patterns and idioms <https://www.youtube.com/watch?v=LCZRJStwkKM>`_
* `Glyph Lefkowitz - So Easy You Can Even Do It in JavaScript: Event-Driven Architecture for Regular Programmers <http://www.pyvideo.org/video/1681/so-easy-you-can-even-do-it-in-javascript-event-d>`__ * `Glyph Lefkowitz - So Easy You Can Even Do It in JavaScript: Event-Driven Architecture for Regular Programmers <http://www.pyvideo.org/video/1681/so-easy-you-can-even-do-it-in-javascript-event-d>`_
Another highly recommended reading is `The Reactive Manifesto <http://www.reactivemanifesto.org>`__ which describes guiding principles, motivations and connects the dots Another highly recommended reading is `The Reactive Manifesto <http://www.reactivemanifesto.org>`_ which describes guiding principles, motivations and connects the dots
.. epigraph:: .. epigraph::
Non-blocking means the ability to make continuous progress in order to for the application to be responsive at all times, even under failure and burst scenarios. For this all resources needed for a response—for example CPU, memory and network—must not be monopolized. As such it can enable both lower latency, higher throughput and better scalability. Non-blocking means the ability to make continuous progress in order to for the application to be responsive at all times, even under failure and burst scenarios. For this all resources needed for a response—for example CPU, memory and network—must not be monopolized. As such it can enable both lower latency, higher throughput and better scalability.
-- `The Reactive Manifesto <http://www.reactivemanifesto.org>`__ -- `The Reactive Manifesto <http://www.reactivemanifesto.org>`_
The fact that |Ab| is implemented using asynchronous programming and non-blocking execution shouldn't come as a surprise, since both `Twisted <https://twistedmatrix.com/trac/>`__ and `asyncio <https://docs.python.org/3/library/asyncio.html>`__ - the foundations upon which |ab| runs - are *asynchronous network programming frameworks*. The fact that |Ab| is implemented using asynchronous programming and non-blocking execution shouldn't come as a surprise, since both `Twisted <https://twistedmatrix.com/trac/>`__ and `asyncio <https://docs.python.org/3/library/asyncio.html>`__ - the foundations upon which |ab| runs - are *asynchronous network programming frameworks*.
On the other hand, the principles of asynchronous programming are independent of Twisted and asyncio. For example, other frameworks that fall into the same category are: On the other hand, the principles of asynchronous programming are independent of Twisted and asyncio. For example, other frameworks that fall into the same category are:
* `NodeJS <http://nodejs.org/>`__ * `NodeJS <http://nodejs.org/>`_
* `Boost/ASIO <http://think-async.com/>`__ * `Boost/ASIO <http://think-async.com/>`_
* `Netty <http://netty.io/>`__ * `Netty <http://netty.io/>`_
* `Tornado <http://www.tornadoweb.org/>`__ * `Tornado <http://www.tornadoweb.org/>`_
* `React <http://reactphp.org/>`__ * `React <http://reactphp.org/>`_
.. tip:: .. tip::
While getting accustomed to the asynchronous way of thinking takes some time and effort, the knowledge and experience acquired can be translated more or less directly to other frameworks in the asynchronous category. While getting accustomed to the asynchronous way of thinking takes some time and effort, the knowledge and experience acquired can be translated more or less directly to other frameworks in the asynchronous category.
@ -45,27 +45,27 @@ Other forms of Concurrency
Asynchronous programming is not the only approach to concurrency. Other styles of concurrency include Asynchronous programming is not the only approach to concurrency. Other styles of concurrency include
1. `OS Threads <http://en.wikipedia.org/wiki/Thread_%28computing%29>`__ 1. `OS Threads <http://en.wikipedia.org/wiki/Thread_%28computing%29>`_
2. `Green Threads <http://en.wikipedia.org/wiki/Green_threads>`__ 2. `Green Threads <http://en.wikipedia.org/wiki/Green_threads>`_
3. `Actors <http://en.wikipedia.org/wiki/Actor_model>`__ 3. `Actors <http://en.wikipedia.org/wiki/Actor_model>`_
4. `Software Transactional Memory (STM) <http://en.wikipedia.org/wiki/Software_transactional_memory>`__ 4. `Software Transactional Memory (STM) <http://en.wikipedia.org/wiki/Software_transactional_memory>`_
Obviously, we cannot go into much detail with all of above. But here are some pointers for further reading if you want to compare and contrast asynchronous programming with other approaches. Obviously, we cannot go into much detail with all of above. But here are some pointers for further reading if you want to compare and contrast asynchronous programming with other approaches.
With the **Actor model** a system is composed of a set of *actors* which are independently running, executing sequentially and communicate strictly by message passing. There is no shared state at all. This approach is used in systems like With the **Actor model** a system is composed of a set of *actors* which are independently running, executing sequentially and communicate strictly by message passing. There is no shared state at all. This approach is used in systems like
* `Erlang <http://www.erlang.org/>`__ * `Erlang <http://www.erlang.org/>`_
* `Akka <http://akka.io/>`__ * `Akka <http://akka.io/>`_
* `Rust <http://www.rust-lang.org/>`__ * `Rust <http://www.rust-lang.org/>`_
* `C++ Actor Framework <http://actor-framework.org/>`__ * `C++ Actor Framework <http://actor-framework.org/>`_
**Software Transactional Memory (STM)** applies the concept of `Optimistic Concurrency Control <http://en.wikipedia.org/wiki/Optimistic_concurrency_control>`__ from the persistent database world to (transient) program memory. Instead of lettings programs directly modify memory, all operations are first logged (inside a transaction), and then applied atomically - but only if no conflicting transaction has committed in the meantime. Hence, it's "optimistic" in that it assumes to be able to commit "normally", but needs to handle the failing at commit time. **Software Transactional Memory (STM)** applies the concept of `Optimistic Concurrency Control <http://en.wikipedia.org/wiki/Optimistic_concurrency_control>`_ from the persistent database world to (transient) program memory. Instead of lettings programs directly modify memory, all operations are first logged (inside a transaction), and then applied atomically - but only if no conflicting transaction has committed in the meantime. Hence, it's "optimistic" in that it assumes to be able to commit "normally", but needs to handle the failing at commit time.
**Green Threads** is using light-weight, run-time level threads and thread scheduling instead of OS threads. Other than that, systems are implemented similar: green threads still block, and still do share state. Python has multiple efforts in this category: **Green Threads** is using light-weight, run-time level threads and thread scheduling instead of OS threads. Other than that, systems are implemented similar: green threads still block, and still do share state. Python has multiple efforts in this category:
* `Eventlet <http://eventlet.net/>`__ * `Eventlet <http://eventlet.net/>`_
* `Gevent <http://gevent.org/>`__ * `Gevent <http://gevent.org/>`_
* `Stackless <http://www.stackless.com/>`__ * `Stackless <http://www.stackless.com/>`_
Twisted or asyncio? Twisted or asyncio?
@ -89,9 +89,9 @@ Even more so, as the core of Twisted and asyncio is very similar and relies on t
| Protocol Factory | Protocol Factory | responsible for creating protocol instances | | Protocol Factory | Protocol Factory | responsible for creating protocol instances |
+------------------+------------------+-------------------------------------------------------------+ +------------------+------------------+-------------------------------------------------------------+
In fact, I'd say the biggest difference between Twisted and asyncio is Deferred vs Future. Those are similar on surface, but their semantics is different. In fact, I'd say the biggest difference between Twisted and asyncio is ``Deferred`` vs ``Future``. Although similar on surface, their semantics are different. ``Deferred`` supports the concept of chainable callbacks (which can mutate the return values), and separate error-backs (which can cancel errors). ``Future`` has just a callback, that always gets a single argument: the Future.
Also, asyncio is opinionated towards co-routines. Means, idiomatic user code for asyncio is expected to use co-routines, and not plain Futures (which are considered too low-level for application code). Also, asyncio is opinionated towards co-routines. This means idiomatic user code for asyncio is expected to use co-routines, and not plain Futures (which are considered too low-level for application code).
But anyway, with asyncio being part of the language standard library (since Python 3.4), wouldn't you just *always* use asyncio? At least if you don't have a need to support already existing Twisted based code. But anyway, with asyncio being part of the language standard library (since Python 3.4), wouldn't you just *always* use asyncio? At least if you don't have a need to support already existing Twisted based code.
@ -117,14 +117,14 @@ Twisted Resources
We cannot give an introduction to asynchronous programming with Twisted here. And there is no need to, since there is lots of great stuff on the Web. In particular we'd like to recommend the following resources. We cannot give an introduction to asynchronous programming with Twisted here. And there is no need to, since there is lots of great stuff on the Web. In particular we'd like to recommend the following resources.
If you have limited time and nevertheless want to have an in-depth view of Twisted, Jessica McKellar has a great presentation recording with `Architecting an event-driven networking engine: Twisted Python <https://www.youtube.com/watch?v=3R4gP6Egh5M>`__. That's 45 minutes. Highly recommended. If you have limited time and nevertheless want to have an in-depth view of Twisted, Jessica McKellar has a great presentation recording with `Architecting an event-driven networking engine: Twisted Python <https://www.youtube.com/watch?v=3R4gP6Egh5M>`_. That's 45 minutes. Highly recommended.
If you really want to get it, Dave Peticolas has written an awesome `Introduction to Asynchronous Programming and Twisted <http://krondo.com/?page_id=1327>`__. This is a detailed, hands-on tutorial with lots of code examples that will take some time to work through - but you actually *learn* how to program with Twisted. If you really want to get it, Dave Peticolas has written an awesome `Introduction to Asynchronous Programming and Twisted <http://krondo.com/?page_id=1327>`_. This is a detailed, hands-on tutorial with lots of code examples that will take some time to work through - but you actually *learn* how to program with Twisted.
Then of course there is Then of course there is
* `The Twisted Documentation <https://twisted.readthedocs.org/>`__ * `The Twisted Documentation <https://twisted.readthedocs.org/>`_
* `The Twisted API Reference <https://twistedmatrix.com/documents/current/api/>`__ * `The Twisted API Reference <https://twistedmatrix.com/documents/current/api/>`_
and lots and lots of awesome `Twisted talks <http://www.pyvideo.org/search?models=videos.video&q=twisted>`__ on PyVideo. and lots and lots of awesome `Twisted talks <http://www.pyvideo.org/search?models=videos.video&q=twisted>`__ on PyVideo.
@ -134,10 +134,10 @@ Asyncio Resources
asyncio is very new (August 2014). So the amount of material on the Web is still limited. Here are some resources you may find useful: asyncio is very new (August 2014). So the amount of material on the Web is still limited. Here are some resources you may find useful:
* `Guido van Rossum's Keynote at PyCon US 2013 <http://pyvideo.org/video/1667/keynote-1>`__ * `Guido van Rossum's Keynote at PyCon US 2013 <http://pyvideo.org/video/1667/keynote-1>`_
* `Tulip: Async I/O for Python 3 <http://www.youtube.com/watch?v=1coLC-MUCJc>`__ * `Tulip: Async I/O for Python 3 <http://www.youtube.com/watch?v=1coLC-MUCJc>`_
* `Python 3.4 docs - asyncio <http://docs.python.org/3.4/library/asyncio.html>`__ * `Python 3.4 docs - asyncio <http://docs.python.org/3.4/library/asyncio.html>`_
* `PEP-3156 - Asynchronous IO Support Rebooted <http://www.python.org/dev/peps/pep-3156/>`__ * `PEP-3156 - Asynchronous IO Support Rebooted <http://www.python.org/dev/peps/pep-3156/>`_
However, we quickly introduce core asynchronous programming primitives provided by `Twisted <https://twistedmatrix.com/>`__ and `asyncio <https://docs.python.org/3.4/library/asyncio.html>`__: However, we quickly introduce core asynchronous programming primitives provided by `Twisted <https://twistedmatrix.com/>`__ and `asyncio <https://docs.python.org/3.4/library/asyncio.html>`__:
@ -293,11 +293,11 @@ Asyncio Futures and Coroutines
.............................. ..............................
`Asyncio Futures <http://docs.python.org/3.4/library/asyncio-task.html#future>`_ like Twisted Deferreds encapsulate the result of a future computation. At the time of creation, the result is (usually) not yet available, and will only be available eventually. `Asyncio Futures <http://docs.python.org/3.4/library/asyncio-task.html#future>`__ like Twisted Deferreds encapsulate the result of a future computation. At the time of creation, the result is (usually) not yet available, and will only be available eventually.
On the other hand, asyncio futures are quite different from Twisted Deferreds. One difference is that they have no built-in machinery for chaining. On the other hand, asyncio futures are quite different from Twisted Deferreds. One difference is that they have no built-in machinery for chaining.
`Asyncio Coroutines <http://docs.python.org/3.4/library/asyncio-task.html#coroutines>`_ are (on a certain level) quite similar to Twisted inline callbacks. Here is the code corresponding to our example above: `Asyncio Coroutines <http://docs.python.org/3.4/library/asyncio-task.html#coroutines>`__ are (on a certain level) quite similar to Twisted inline callbacks. Here is the code corresponding to our example above:
------- -------

View File

@ -5,6 +5,29 @@
Changelog Changelog
========= =========
0.10.5
------
`Published 2015-08-06 <https://pypi.python.org/pypi/autobahn/0.10.5>`__
* maintenance release with lots of smaller bug fixes
0.10.4
------
`Published 2015-05-08 <https://pypi.python.org/pypi/autobahn/0.10.4>`__
* maintenance release with some smaller bug fixes
0.10.3
------
`Published 2015-04-14 <https://pypi.python.org/pypi/autobahn/0.10.3>`__
* new: using txaio package
* new: revised WAMP-over-RawSocket specification implemented
* fix: ignore unknown attributes in WAMP Options/Details
0.10.2 0.10.2
------ ------

View File

@ -34,7 +34,7 @@ master_doc = 'index'
# General information about the project. # General information about the project.
project = u'AutobahnPython' project = u'AutobahnPython'
copyright = None copyright = u'Tavendo GmbH'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the

View File

@ -17,3 +17,13 @@ This means that you fork the repo, make changes to your fork, and then make a pu
This `article on GitHub <https://help.github.com/articles/using-pull-requests>`_ gives more detailed information on how the process works. This `article on GitHub <https://help.github.com/articles/using-pull-requests>`_ gives more detailed information on how the process works.
Running the Tests
-----------------
In order to run the unit-tests, we use `Tox <http://tox.readthedocs.org/en/latest/>`_ to build the various test-environments. To run them all, simply run ``tox`` from the top-level directory of the clone.
For test-coverage, see the Makefile target ``test_coverage``, which deletes the coverage data and then runs the test suite with various tox test-environments before outputting HTML annotated coverage to ``./htmlcov/index.html`` and a coverage report to the terminal.
There are two environment variables the tests use: ``USE_TWISTED=1`` or ``USE_ASYNCIO=1`` control whether to run unit-tests that are specific to one framework or the other.
See ``tox.ini`` for details on how to run in the different environments

View File

@ -1,7 +1,7 @@
|AbL| |AbL|
===== =====
*Open-source real-time framework for Web, Mobile & Internet of Things.* *Open-source (MIT) real-time framework for Web, Mobile & Internet of Things.*
Latest release: v\ |version| (:ref:`Changelog`) Latest release: v\ |version| (:ref:`Changelog`)
@ -25,13 +25,36 @@ Latest release: v\ |version| (:ref:`Changelog`)
* `The WebSocket Protocol <http://tools.ietf.org/html/rfc6455>`_ * `The WebSocket Protocol <http://tools.ietf.org/html/rfc6455>`_
* `The Web Application Messaging Protocol (WAMP) <http://wamp.ws/>`_ * `The Web Application Messaging Protocol (WAMP) <http://wamp.ws/>`_
in Python 2 and 3, running on `Twisted`_ and `asyncio`_. in Python 2 and 3, running on `Twisted`_ **or** `asyncio`_.
WebSocket allows `bidirectional real-time messaging <http://tavendo.com/blog/post/websocket-why-what-can-i-use-it/>`_ on the Web while `WAMP <http://wamp.ws/>`_ provides applications with `high-level communication abstractions <http://wamp.ws/why/>`_ in an open standard WebSocket based protocol. Documentation Overview
----------------------
|AbL| features See :ref:`site_contents` for a full site-map. Top-level pages available:
* framework for `WebSocket`_ / `WAMP`_ clients .. toctree::
:maxdepth: 1
installation
asynchronous-programming
wamp/programming
wamp/examples
websocket/programming
websocket/examples
reference/autobahn
contribute
changelog
-----
Autobahn Features
-----------------
WebSocket allows `bidirectional real-time messaging <http://tavendo.com/blog/post/websocket-why-what-can-i-use-it/>`_ on the Web while `WAMP <http://wamp.ws/>`_ provides applications with `high-level communication abstractions <http://wamp.ws/why/>`_ (remote procedure calling and publish/subscribe) in an open standard WebSocket-based protocol.
|AbL| features:
* framework for `WebSocket`_ and `WAMP`_ clients
* compatible with Python 2.6, 2.7, 3.3 and 3.4 * compatible with Python 2.6, 2.7, 3.3 and 3.4
* runs on `CPython`_, `PyPy`_ and `Jython`_ * runs on `CPython`_, `PyPy`_ and `Jython`_
* runs under `Twisted`_ and `asyncio`_ * runs under `Twisted`_ and `asyncio`_
@ -41,14 +64,14 @@ WebSocket allows `bidirectional real-time messaging <http://tavendo.com/blog/pos
* supports TLS (secure WebSocket) and proxies * supports TLS (secure WebSocket) and proxies
* Open-source (`MIT license <https://github.com/tavendo/AutobahnPython/blob/master/LICENSE>`_) * Open-source (`MIT license <https://github.com/tavendo/AutobahnPython/blob/master/LICENSE>`_)
and much more. ...and much more.
Further, |AbL| is written with these goals Further, |AbL| is written with these goals:
1. high-performance, fully asynchronous and scalable code 1. high-performance, fully asynchronous and scalable code
2. best-in-class standards conformance and security 2. best-in-class standards conformance and security
We do take those design and implementation goals quite serious. For example, |AbL| has 100% strict passes with `AutobahnTestsuite`_, the quasi industry standard of WebSocket protocol test suites we originally created only to test |AbL|;) We do take those design and implementation goals quite serious. For example, |AbL| has 100% strict passes with `AutobahnTestsuite`_, the quasi industry standard of WebSocket protocol test suites we originally created only to test |AbL| ;)
.. note:: .. note::
In the following, we will just refer to |Ab| instead of the In the following, we will just refer to |Ab| instead of the
@ -56,10 +79,18 @@ We do take those design and implementation goals quite serious. For example, |Ab
ambiguity. ambiguity.
What can I do with this stuff? What can I do with Autobahn?
------------------------------ ----------------------------
WebSocket is great for apps like **chat**, **trading**, **multi-player games** or **real-time charts**. It allows you to **actively push information** to clients as it happens. WebSocket is great for apps like **chat**, **trading**, **multi-player games** or **real-time charts**. It allows you to **actively push information** to clients as it happens. (See also :ref:`run_all_examples`)
.. image:: _static/wamp-demos.png
:alt: ascii-cast of all WAMP demos running
:height: 443
:width: 768
:target: wamp/examples.html#run-all-examples
:scale: 40%
:align: right
Further, WebSocket allows you to real-time enable your Web user interfaces: **always current information** without reloads or polling. UIs no longer need to be a boring, static thing. Looking for the right communication technology for your next-generation Web apps? Enter WebSocket. Further, WebSocket allows you to real-time enable your Web user interfaces: **always current information** without reloads or polling. UIs no longer need to be a boring, static thing. Looking for the right communication technology for your next-generation Web apps? Enter WebSocket.
@ -84,25 +115,28 @@ A sample **WebSocket server**:
.. code-block:: python .. code-block:: python
class MyServerProtocol(WebSocketServerProtocol): from autobahn.twisted.websocket import WebSocketServerProtocol
# or: from autobahn.asyncio.websocket import WebSocketServerProtocol
def onConnect(self, request): class MyServerProtocol(WebSocketServerProtocol):
print("Client connecting: {}".format(request.peer))
def onOpen(self): def onConnect(self, request):
print("WebSocket connection open.") print("Client connecting: {}".format(request.peer))
def onMessage(self, payload, isBinary): def onOpen(self):
if isBinary: print("WebSocket connection open.")
print("Binary message received: {} bytes".format(len(payload)))
else:
print("Text message received: {}".format(payload.decode('utf8')))
## echo back message verbatim def onMessage(self, payload, isBinary):
self.sendMessage(payload, isBinary) if isBinary:
print("Binary message received: {} bytes".format(len(payload)))
else:
print("Text message received: {}".format(payload.decode('utf8')))
def onClose(self, wasClean, code, reason): ## echo back message verbatim
print("WebSocket connection closed: {}".format(reason)) self.sendMessage(payload, isBinary)
def onClose(self, wasClean, code, reason):
print("WebSocket connection closed: {}".format(reason))
Complete example code: Complete example code:
@ -119,34 +153,35 @@ A sample **WAMP application component** implementing all client roles:
.. code-block:: python .. code-block:: python
class MyComponent(ApplicationSession): from autobahn.twisted.wamp import ApplicationSession
# or: from autobahn.asyncio.wamp import ApplicationSession
class MyComponent(ApplicationSession):
@inlineCallbacks @inlineCallbacks
def onJoin(self, details): def onJoin(self, details):
# 1) subscribe to a topic # 1) subscribe to a topic
def onevent(msg): def onevent(msg):
print("Got event: {}".format(msg)) print("Got event: {}".format(msg))
yield self.subscribe(onevent, 'com.myapp.hello')
yield self.subscribe(onevent, 'com.myapp.hello') # 2) publish an event
self.publish('com.myapp.hello', 'Hello, world!')
# 2) publish an event # 3) register a procedure for remoting
self.publish('com.myapp.hello', 'Hello, world!') def add2(x, y):
return x + y
self.register(add2, 'com.myapp.add2');
# 3) register a procedure for remoting # 4) call a remote procedure
def add2(x, y): res = yield self.call('com.myapp.add2', 2, 3)
return x + y print("Got result: {}".format(res))
self.register(add2, 'com.myapp.add2');
# 4) call a remote procedure
res = yield self.call('com.myapp.add2', 2, 3)
print("Got result: {}".format(res))
Complete example code: Complete example code:
* `Twisted <https://github.com/tavendo/AutobahnPython/blob/master/examples/twisted/wamp/beginner/client.py>`__ - * `asyncio <https://github.com/tavendo/AutobahnPython/blob/master/examples/asyncio/wamp/beginner/client.py>`__ * `Twisted Example <https://github.com/tavendo/AutobahnPython/blob/master/examples/twisted/wamp/overview/>`__
* `asyncio Example <https://github.com/tavendo/AutobahnPython/blob/master/examples/asyncio/wamp/overview/>`__
Introduction to WAMP Programming with |ab|: Introduction to WAMP Programming with |ab|:

View File

@ -18,6 +18,7 @@ You will need at least one of those.
For Twisted installation, please see `here <http://twistedmatrix.com/>`__. Asyncio comes bundled with Python 3.4+. For Python 3.3, install it from `here <https://pypi.python.org/pypi/asyncio>`__. For Python 2, `trollius`_ will work. For Twisted installation, please see `here <http://twistedmatrix.com/>`__. Asyncio comes bundled with Python 3.4+. For Python 3.3, install it from `here <https://pypi.python.org/pypi/asyncio>`__. For Python 2, `trollius`_ will work.
Supported Configurations Supported Configurations
........................ ........................
@ -42,6 +43,7 @@ Here are the configurations supported by |ab|:
.. _1: http://twistedmatrix.com/trac/ticket/3413 .. _1: http://twistedmatrix.com/trac/ticket/3413
.. _2: http://twistedmatrix.com/trac/ticket/6746 .. _2: http://twistedmatrix.com/trac/ticket/6746
Performance Note Performance Note
................ ................
@ -53,6 +55,7 @@ Performance Note
To give you an idea of the performance you can expect, here is a `blog post <http://tavendo.com/blog/post/autobahn-pi-benchmark/>`_ benchmarking |ab| running on the `RaspberryPi <http://www.raspberrypi.org/>`_ (a tiny embedded computer) under `PyPy <http://pypy.org/>`_. To give you an idea of the performance you can expect, here is a `blog post <http://tavendo.com/blog/post/autobahn-pi-benchmark/>`_ benchmarking |ab| running on the `RaspberryPi <http://www.raspberrypi.org/>`_ (a tiny embedded computer) under `PyPy <http://pypy.org/>`_.
Installing Autobahn Installing Autobahn
------------------- -------------------
@ -81,13 +84,13 @@ And to install asyncio backports automatically when required
Install from Sources Install from Sources
.................... ....................
To install from sources, clone the repository To install from sources, clone the repository:
.. code-block:: sh .. code-block:: sh
git clone git@github.com:tavendo/AutobahnPython.git git clone git@github.com:tavendo/AutobahnPython.git
checkout a tagged release checkout a tagged release:
.. code-block:: sh .. code-block:: sh
@ -95,16 +98,16 @@ checkout a tagged release
git checkout v0.9.1 git checkout v0.9.1
.. warning:: .. warning::
You should only use *tagged* releases, not *trunk*. The latest code from *trunk* might be broken, unfinished and untested. So you have been warned;) You should only use *tagged* releases, not *master*. The latest code from *master* might be broken, unfinished and untested. So you have been warned ;)
Then do Then do:
.. code-block:: sh .. code-block:: sh
cd autobahn cd autobahn
python setup.py install python setup.py install
You can also use Pip for the last step, which allows to specify install variants (see below) You can also use ``pip`` for the last step, which allows to specify install variants (see below)
.. code-block:: sh .. code-block:: sh

View File

@ -3,31 +3,84 @@
WAMP Examples WAMP Examples
============= =============
**NOTE** that for all examples you will **need to run a router**. We develop `Crossbar.io <http://crossbar.io/docs>`_ and there are `other routers <http://wamp.ws/implementations/#routers>`_ available as well. We include a working `Crossbar.io <http://crossbar.io/docs>`_ configuration in the `examples/router/ subdirectory <https://github.com/tavendo/AutobahnPython/tree/master/examples/router>`_ as well as `instructions on how to run it <https://github.com/tavendo/AutobahnPython/blob/master/examples/running-the-examples.md>`_.
Overview of Examples
++++++++++++++++++++
The examples are organized between `asycio <https://docs.python.org/3.4/library/asyncio.html>`__ and `Twisted <https://www.twistedmatrix.com>`__ at the top-level, with similarly-named examples demonstrating the same functionality with the respective framework.
Each example typically includes four things:
- ``frontend.py``: the Caller or Subscriber, in Python
- ``backend.py``: the Callee or Publisher, in Python
- ``frontend.js``: JavaScript version of the frontend
- ``backend.js``: JavaScript version of the backend
- ``*.html``: boilerplate so a browser can run the JavaScript
So for each example, you start *one* backend and *one* frontend component (your choice). You can usually start multiple frontend components with no problem, but will get errors if you start two backends trying to register at the same procedure URI (for example).
Still, you are encouraged to try playing with mixing and matching the frontend and backend components, starting multiple front-ends, etc. to explore Crossbar and Autobahn's behavior. Often the different examples use similar URIs for procedures and published events, so you can even try mixing between the examples.
The provided `Crossbar.io <http://crossbar.io/docs>`__ configuration will run a Web server that you can visit at `http://localhost:8080` and includes links to the frontend/backend HTML for the javascript versions. Usually these just use ``console.log()`` so you'll have to open up the JavaScript console in your browser to see it working.
.. _run_all_examples:
Automatically Run All Examples
++++++++++++++++++++++++++++++
There is a script (``./examples/run-all-examples.py``) which runs all the WAMP examples for 5 seconds each, `this asciicast
<https://asciinema.org/a/9cnar155zalie80c9725nvyk7>`__ shows you how (see comments for how to run it yourself):
.. raw:: html
<script type="text/javascript" src="https://asciinema.org/a/9cnar155zalie80c9725nvyk7.js" id="asciicast-21588" async></script>
Publish & Subscribe (PubSub) Publish & Subscribe (PubSub)
++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++
* Basic `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/basic/pubsub/basic>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/basic/pubsub/basic>`__ - Demonstrates basic publish and subscribe. * Basic `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/pubsub/basic>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/pubsub/basic>`__ - Demonstrates basic publish and subscribe.
* Complex `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/basic/pubsub/complex>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/basic/pubsub/complex>`__ - Demonstrates publish and subscribe with complex events. * Complex `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/pubsub/complex>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/pubsub/complex>`__ - Demonstrates publish and subscribe with complex events.
* Options `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/basic/pubsub/options>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/basic/pubsub/options>`__ - Using options with PubSub. * Options `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/pubsub/options>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/pubsub/options>`__ - Using options with PubSub.
* Unsubscribe `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/basic/pubsub/unsubscribe>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/basic/pubsub/unsubscribe>`__ - Cancel a subscription to a topic. * Unsubscribe `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/pubsub/unsubscribe>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/pubsub/unsubscribe>`__ - Cancel a subscription to a topic.
Remote Procedure Calls (RPC) Remote Procedure Calls (RPC)
++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++
* Time Service `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/basic/rpc/timeservice>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/basic/rpc/timeservice>`__ - A trivial time service - demonstrates basic remote procedure feature. * Time Service `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/rpc/timeservice>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/rpc/timeservice>`__ - A trivial time service - demonstrates basic remote procedure feature.
* Slow Square `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/basic/rpc/slowsquare>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/basic/rpc/slowsquare>`__ - Demonstrates procedures which return promises and return asynchronously. * Slow Square `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/rpc/slowsquare>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/rpc/slowsquare>`__ - Demonstrates procedures which return promises and return asynchronously.
* Arguments `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/basic/rpc/arguments>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/basic/rpc/arguments>`__ - Demonstrates all variants of call arguments. * Arguments `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/rpc/arguments>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/rpc/arguments>`__ - Demonstrates all variants of call arguments.
* Complex Result `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/basic/rpc/complex>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/basic/rpc/complex>`__ - Demonstrates complex call results (call results with more than one positional or keyword results). * Complex Result `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/rpc/complex>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/rpc/complex>`__ - Demonstrates complex call results (call results with more than one positional or keyword results).
* Errors `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/basic/rpc/errors>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/basic/rpc/errors>`__ - Demonstrates error raising and catching over remote procedures. * Errors `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/rpc/errors>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/rpc/errors>`__ - Demonstrates error raising and catching over remote procedures.
* Progressive Results `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/basic/rpc/progress>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/basic/rpc/progress>`__ - Demonstrates calling remote procedures that produce progressive results. * Progressive Results `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/rpc/progress>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/rpc/progress>`__ - Demonstrates calling remote procedures that produce progressive results.
* Options `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/basic/rpc/options>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/basic/rpc/options>`__ - Using options with RPC. * Options `Twisted <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/wamp/rpc/options>`__ - `asyncio <https://github.com/tavendo/AutobahnPython/tree/master/examples/asyncio/wamp/rpc/options>`__ - Using options with RPC.
I'm Confused, Just Tell Me What To Run
++++++++++++++++++++++++++++++++++++++
If all that is too many options to consider, you want to do this:
1. Open 3 terminals
2. In terminal 1, `setup and run a local Crossbar <https://github.com/tavendo/AutobahnPython/blob/master/examples/running-the-examples.md>`_ in the root of your Autobahn checkout.
3. In terminals 2 and 3, go to the root of your Autobahn checkout and activate the virtualenv from step 2 (``source venv-autobahn/bin/activate``)
4. In terminal 2 run ``python ./examples/twisted/wamp/rpc/arguments/backend.py``
5. In terminal 3 run ``python ./examples/twisted/wamp/rpc/arguments/frontend.py``
The above procedure is gone over in this `this asciicast <https://asciinema.org/a/2vl1eahfaxptoen9bnevd06lq.png)](https://asciinema.org/a/2vl1eahfaxptoen9bnevd06lq>`_:
.. raw:: html
<script type="text/javascript" src="https://asciinema.org/a/2vl1eahfaxptoen9bnevd06lq.js" id="asciicast-21580" async></script>

View File

@ -1,7 +1,8 @@
WAMP Programming ==================
================ WAMP Programming
==================
This guide gives an introduction to programming with `WAMP <http://wamp.ws>`__ in Python using |Ab|. This guide gives an introduction to programming with `WAMP <http://wamp.ws>`__ in Python using |Ab|. (Go straight to :ref:`wamp_examples`)
WAMP provides two communication patterns for application components to talk to each other WAMP provides two communication patterns for application components to talk to each other
@ -15,28 +16,38 @@ and we will cover all four interactions involved in above patterns
3. :ref:`subscribing-to-topics` for receiving events 3. :ref:`subscribing-to-topics` for receiving events
4. :ref:`publishing-events` to topics 4. :ref:`publishing-events` to topics
Note that WAMP is a "routed" protocol, and defines a Dealer and Broker role. Practically speaking, this means that any WAMP client needs a WAMP Router to talk to. We provide an open-source one called `Crossbar <http://crossbar.io>`_ (there are `other routers <http://wamp.ws/implementations/#routers>`_ available). See also `the WAMP specification <http://wamp.ws/spec/>`_ for more details
.. tip:: .. tip::
If you are new to WAMP or want to learn more about the design principles behind WAMP, we have a longer text `here <http://wamp.ws/why/>`__. If you are new to WAMP or want to learn more about the design principles behind WAMP, we have a longer text `here <http://wamp.ws/why/>`__.
------ ------
Application Components Application Components
---------------------- ======================
WAMP is all about creating systems from loosely coupled *application components*. It's application components where your application specific code runs. WAMP is all about creating systems from loosely coupled *application components*. These application components are where your application-specific code runs.
A WAMP based system consists of potentially many application components, which all connect to a WAMP router. The router is *generic*, which means, it does *not* run any application code, but only provides routing of events and calls. A WAMP-based system consists of potentially many application components, which all connect to a WAMP router. The router is *generic*, which means, it does *not* run any application code, but only provides routing of events and calls.
Hence, to create a WAMP application, you These components use either Remote Procedure Calls (RPC) or Publish/Subscribe (PubSub) to communicate. Each component can do any mix of: register, call, subscribe or publish.
For RPC, an application component registers a callable method at a URI ("endpoint"), and other components call it via that endpoint.
In the Publish/Subscribe model, interested components subscribe to an event URI and when a publish to that URI happens, the event payload is routerd to all subscribers:
Hence, to create a WAMP application, you:
1. write application components 1. write application components
2. connect the components to a router 2. connect the components to a router
Note that each component can do any mix of registering, calling, subscribing and publishing -- it is entirely up to you to logically group functionality as suits your problem space.
.. _creating-components: .. _creating-components:
Creating Components Creating Components
................... -------------------
You create an application component by deriving from a base class provided by |ab|. You create an application component by deriving from a base class provided by |ab|.
@ -48,7 +59,6 @@ When using **Twisted**, you derive from :class:`autobahn.twisted.wamp.Applicatio
from autobahn.twisted.wamp import ApplicationSession from autobahn.twisted.wamp import ApplicationSession
class MyComponent(ApplicationSession): class MyComponent(ApplicationSession):
def onJoin(self, details): def onJoin(self, details):
print("session ready") print("session ready")
@ -60,19 +70,18 @@ whereas when you are using **asyncio**, you derive from :class:`autobahn.asyncio
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession
class MyComponent(ApplicationSession): class MyComponent(ApplicationSession):
def onJoin(self, details): def onJoin(self, details):
print("session ready") print("session ready")
As can be seen, the only difference between Twisted and asyncio is the import (line 1). The rest of the code is identical. As can be seen, the only difference between Twisted and asyncio is the import (line 1). The rest of the code is identical.
Also, |ab| will invoke callbacks on your application component when certain events happen. For example, :func:`autobahn.wamp.interfaces.ISession.onJoin` is triggered when the WAMP session has connected to a router and joined a realm. We'll come back to this topic later. Also, |ab| will invoke callbacks on your application component when certain events happen. For example, :func:`ISession.onJoin <autobahn.wamp.interfaces.ISession.onJoin>` is triggered when the WAMP session has connected to a router and joined a realm. We'll come back to this topic later.
.. _running-components: .. _running-components:
Running Components Running Components
.................. ------------------
To actually make use of an application components, the component needs to connect to a WAMP router. To actually make use of an application components, the component needs to connect to a WAMP router.
|Ab| includes a *runner* that does the heavy lifting for you. |Ab| includes a *runner* that does the heavy lifting for you.
@ -84,7 +93,7 @@ Here is how you use :class:`autobahn.twisted.wamp.ApplicationRunner` with **Twis
from autobahn.twisted.wamp import ApplicationRunner from autobahn.twisted.wamp import ApplicationRunner
runner = ApplicationRunner(url = u"ws://localhost:8080/ws", realm = u"realm1") runner = ApplicationRunner(url=u"ws://localhost:8080/ws", realm=u"realm1")
runner.run(MyComponent) runner.run(MyComponent)
and here is how you use :class:`autobahn.asyncio.wamp.ApplicationRunner` with **asyncio** and here is how you use :class:`autobahn.asyncio.wamp.ApplicationRunner` with **asyncio**
@ -94,7 +103,7 @@ and here is how you use :class:`autobahn.asyncio.wamp.ApplicationRunner` with **
from autobahn.asyncio.wamp import ApplicationRunner from autobahn.asyncio.wamp import ApplicationRunner
runner = ApplicationRunner(url = u"ws://localhost:8080/ws", realm = u"realm1") runner = ApplicationRunner(url=u"ws://localhost:8080/ws", realm=u"realm1")
runner.run(MyComponent) runner.run(MyComponent)
As can be seen, the only difference between Twisted and asyncio is the import (line 1). The rest of the code is identical. As can be seen, the only difference between Twisted and asyncio is the import (line 1). The rest of the code is identical.
@ -113,46 +122,52 @@ Here are quick templates for you to copy/paste for creating and running a WAMP c
**Twisted**: **Twisted**:
.. code-block:: python .. code-block:: python
:emphasize-lines: 1 :emphasize-lines: 2
from autobahn.twisted.wamp import ApplicationSession, ApplicationRunner from twisted.internet.defer import inlineCallbacks
from autobahn.twisted.wamp import ApplicationSession, ApplicationRunner
class MyComponent(ApplicationSession): class MyComponent(ApplicationSession):
def onJoin(self, details): @inlineCallbacks
print("session joined") def onJoin(self, details):
print("session joined")
# can do subscribes, registers here e.g.:
# yield self.subscribe(...)
# yield self.register(...)
if __name__ == '__main__':
runner = ApplicationRunner(url=u"ws://localhost:8080/ws", realm=u"realm1")
runner.run(MyComponent)
if __name__ == '__main__':
runner = ApplicationRunner(url = u"ws://localhost:8080/ws", realm = u"realm1")
runner.run(MyComponent)
**asyncio**: **asyncio**:
.. code-block:: python .. code-block:: python
:emphasize-lines: 1 :emphasize-lines: 2
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner from asyncio import coroutine
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class MyComponent(ApplicationSession): class MyComponent(ApplicationSession):
@coroutine
def onJoin(self, details):
print("session joined")
# can do subscribes, registers here e.g.:
# yield from self.subscribe(...)
# yield from self.register(...)
def onJoin(self, details): if __name__ == '__main__':
print("session joined") runner = ApplicationRunner(url=u"ws://localhost:8080/ws", realm=u"realm1")
runner.run(MyComponent)
if __name__ == '__main__':
runner = ApplicationRunner(url = u"ws://localhost:8080/ws", realm = u"realm1")
runner.run(MyComponent)
Running a WAMP Router Running a WAMP Router
--------------------- =====================
The component we've created attempts to connect to a **WAMP router** running locally which accepts connections on port ``8080``, and for a realm ``realm1``. The component we've created attempts to connect to a **WAMP router** running locally which accepts connections on port ``8080``, and for a realm ``realm1``.
Our suggested way is to use `Crossbar.io <http://crossbar.io>`_ as your WAMP router. Our suggested way is to use `Crossbar.io <http://crossbar.io>`_ as your WAMP router. There are `other WAMP routers <http://wamp.ws/implementations#routers>`_ besides Crossbar.io as well.
.. tip::
There are other WAMP routers besides Crossbar.io as well. Please see this `list <http://wamp.ws/implementations#routers>`__.
Once you've `installed Crossbar.io <http://crossbar.io/docs/Quick-Start/>`_, initialize an instance of it with the default settings, which will accept WAMP (over WebSocket) connections on ``ws://<hostname>:8080/ws`` and has a ``realm1`` pre-configured. Once you've `installed Crossbar.io <http://crossbar.io/docs/Quick-Start/>`_, initialize an instance of it with the default settings, which will accept WAMP (over WebSocket) connections on ``ws://<hostname>:8080/ws`` and has a ``realm1`` pre-configured.
@ -162,7 +177,7 @@ To do this, do
crossbar init crossbar init
This will create the default Crossbar.io node configuration ``.crossbar/config.json``. You can then start Crossbar.io by doing This will create the default Crossbar.io node configuration ``./.crossbar/config.json``. You can then start Crossbar.io by doing:
.. code-block:: sh .. code-block:: sh
@ -172,7 +187,7 @@ This will create the default Crossbar.io node configuration ``.crossbar/config.j
.. _remote-procedure-calls: .. _remote-procedure-calls:
Remote Procedure Calls Remote Procedure Calls
---------------------- ======================
**Remote Procedure Call (RPC)** is a messaging pattern involving peers of three roles: **Remote Procedure Call (RPC)** is a messaging pattern involving peers of three roles:
@ -184,40 +199,40 @@ A *Caller* issues calls to remote procedures by providing the procedure URI and
*Callees* register procedures they provide with *Dealers*. *Callers* initiate procedure calls first to *Dealers*. *Dealers* route calls incoming from *Callers* to *Callees* implementing the procedure called, and route call results back from *Callees* to *Callers*. *Callees* register procedures they provide with *Dealers*. *Callers* initiate procedure calls first to *Dealers*. *Dealers* route calls incoming from *Callers* to *Callees* implementing the procedure called, and route call results back from *Callees* to *Callers*.
The *Caller* and *Callee* will usually run application code, while the *Dealer* works as a generic router for remote procedure calls decoupling *Callers* and *Callees*. The *Caller* and *Callee* will usually run application code, while the *Dealer* works as a generic router for remote procedure calls decoupling *Callers* and *Callees*. Thus, the *Caller* can be in a separate process (even a separate implementation language) from the *Callee*.
.. _registering-procedures: .. _registering-procedures:
Registering Procedures
......................
To make a procedure available for remote calling, the procedure needs to be *registered*. Registering a procedure is done by calling :func:`autobahn.wamp.interfaces.ICallee.register` from a session. Registering Procedures
----------------------
To make a procedure available for remote calling, the procedure needs to be *registered*. Registering a procedure is done by calling :meth:`ICallee.register <autobahn.wamp.interfaces.ICallee.register>` from a session.
Here is an example using **Twisted** Here is an example using **Twisted**
.. code-block:: python .. code-block:: python
:linenos: :linenos:
:emphasize-lines: 15 :emphasize-lines: 14
from autobahn.twisted.wamp import ApplicationSession from autobahn.twisted.wamp import ApplicationSession
from twisted.internet.defer import inlineCallbacks from twisted.internet.defer import inlineCallbacks
class MyComponent(ApplicationSession): class MyComponent(ApplicationSession):
@inlineCallbacks
@inlineCallbacks def onJoin(self, details):
def onJoin(self, details): print("session ready")
print("session ready")
def add2(x, y): def add2(x, y):
return x + y return x + y
try: try:
yield self.register(add2, u'com.myapp.add2') yield self.register(add2, u'com.myapp.add2')
print("procedure registered") print("procedure registered")
except Exception as e: except Exception as e:
print("could not register procedure: {0}".format(e)) print("could not register procedure: {0}".format(e))
The procedure ``add2`` is registered (line 14) under the URI ``u"com.myapp.add2"`` immediately in the ``onJoin`` callback which fires when the session has connected to a *Router* and joined a *Realm*. The procedure ``add2`` is registered (line 14) under the URI ``u"com.myapp.add2"`` immediately in the ``onJoin`` callback which fires when the session has connected to a *Router* and joined a *Realm*.
@ -229,29 +244,28 @@ When the registration succeeds, authorized callers will immediately be able to c
A registration may also fail, e.g. when a procedure is already registered under the given URI or when the session is not authorized to register procedures. A registration may also fail, e.g. when a procedure is already registered under the given URI or when the session is not authorized to register procedures.
Using **asyncio**, the example looks like this Using **asyncio**, the example looks like this:
.. code-block:: python .. code-block:: python
:linenos: :linenos:
:emphasize-lines: 14 :emphasize-lines: 13
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession
from asyncio import coroutine from asyncio import coroutine
class MyComponent(ApplicationSession): class MyComponent(ApplicationSession):
@coroutine
def onJoin(self, details):
print("session ready")
@coroutine def add2(x, y):
def onJoin(self, details): return x + y
print("session ready")
def add2(x, y): try:
return x + y yield from self.register(add2, u'com.myapp.add2')
print("procedure registered")
try: except Exception as e:
yield from self.register(add2, u'com.myapp.add2') print("could not register procedure: {0}".format(e))
print("procedure registered")
except Exception as e:
print("could not register procedure: {0}".format(e))
The differences compared with the Twisted variant are: The differences compared with the Twisted variant are:
@ -263,59 +277,57 @@ The differences compared with the Twisted variant are:
.. _calling-procedures: .. _calling-procedures:
Calling Procedures Calling Procedures
.................. ------------------
Calling a procedure (that has been previously registered) is done using :func:`autobahn.wamp.interfaces.ICaller.call`. Calling a procedure (that has been previously registered) is done using :func:`autobahn.wamp.interfaces.ICaller.call`.
Here is how you would call the procedure ``add2`` that we registered in :ref:`registering-procedures` under URI ``com.myapp.add2`` in **Twisted** Here is how you would call the procedure ``add2`` that we registered in :ref:`registering-procedures` under URI ``com.myapp.add2`` in **Twisted**
.. code-block:: python .. code-block:: python
:linenos: :linenos:
:emphasize-lines: 12 :emphasize-lines: 11
from autobahn.twisted.wamp import ApplicationSession from autobahn.twisted.wamp import ApplicationSession
from twisted.internet.defer import inlineCallbacks from twisted.internet.defer import inlineCallbacks
class MyComponent(ApplicationSession): class MyComponent(ApplicationSession):
@inlineCallbacks
def onJoin(self, details):
print("session ready")
@inlineCallbacks try:
def onJoin(self, details): res = yield self.call(u'com.myapp.add2', 2, 3)
print("session ready") print("call result: {}".format(res))
except Exception as e:
try: print("call error: {0}".format(e))
res = yield self.call(u'com.myapp.add2', 2, 3)
print("call result: {}".format(res))
except Exception as e:
print("call error: {0}".format(e))
And here is the same done on **asyncio** And here is the same done on **asyncio**
.. code-block:: python .. code-block:: python
:linenos: :linenos:
:emphasize-lines: 12 :emphasize-lines: 11
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession
from asyncio import coroutine from asyncio import coroutine
class MyComponent(ApplicationSession): class MyComponent(ApplicationSession):
@coroutine
def onJoin(self, details):
print("session ready")
@coroutine try:
def onJoin(self, details): res = yield from self.call(u'com.myapp.add2', 2, 3)
print("session ready") print("call result: {}".format(res))
except Exception as e:
try: print("call error: {0}".format(e))
res = yield from self.call(u'com.myapp.add2', 2, 3)
print("call result: {}".format(res))
except Exception as e:
print("call error: {0}".format(e))
.. _publish-and-subscribe: .. _publish-and-subscribe:
Publish & Subscribe Publish & Subscribe
------------------- ===================
**Publish & Subscribe (PubSub)** is a messaging pattern involving peers of three roles: **Publish & Subscribe (PubSub)** is a messaging pattern involving peers of three roles:
@ -323,46 +335,43 @@ Publish & Subscribe
* *Subscriber* * *Subscriber*
* *Broker* * *Broker*
A *Publishers* publishes events to topics by providing the topic URI and any payload for the event. Subscribers of the topic will receive the event together with the event payload. A *Publisher* publishes events to topics by providing the topic URI and any payload for the event. Subscribers of the topic will receive the event together with the event payload.
*Subscribers* subscribe to topics they are interested in with *Brokers*. *Publishers* initiate publication first at *Brokers*. *Brokers* route events incoming from *Publishers* to *Subscribers* that are subscribed to respective topics. *Subscribers* subscribe to topics they are interested in with *Brokers*. *Publishers* initiate publication first at a *Broker*. *Brokers* route events incoming from *Publishers* to *Subscribers* that are subscribed to respective topics.
The *Publisher* and *Subscriber* will usually run application code, while the *Broker* works as a generic router for events decoupling *Publishers* from *Subscribers*. The *Publisher* and *Subscriber* will usually run application code, while the *Broker* works as a generic router for events thus decoupling *Publishers* from *Subscribers*. That is, there can be many *Subscribers* written in different languages on different machines which can all receive a single event published by an independant *Publisher*.
.. _subscribing-to-topics: .. _subscribing-to-topics:
Subscribing to Topics Subscribing to Topics
..................... ---------------------
To receive events published to a topic, a session needs to first subscribe to the topic. To receive events published to a topic, a session needs to first subscribe to the topic. Subscribing to a topic is done by calling :func:`autobahn.wamp.interfaces.ISubscriber.subscribe`.
Subscribing to a topic is done by calling :func:`autobahn.wamp.interfaces.ISubscriber.subscribe`. Here is a **Twisted** example:
Here is a **Twisted** example
.. code-block:: python .. code-block:: python
:linenos: :linenos:
:emphasize-lines: 15 :emphasize-lines: 14
from autobahn.twisted.wamp import ApplicationSession from autobahn.twisted.wamp import ApplicationSession
from twisted.internet.defer import inlineCallbacks from twisted.internet.defer import inlineCallbacks
class MyComponent(ApplicationSession): class MyComponent(ApplicationSession):
@inlineCallbacks
def onJoin(self, details):
print("session ready")
@inlineCallbacks def oncounter(count):
def onJoin(self, details): print("event received: {0}", count)
print("session ready")
def oncounter(count): try:
print("event received: {0}", count) yield self.subscribe(oncounter, u'com.myapp.oncounter')
print("subscribed to topic")
try: except Exception as e:
yield self.subscribe(oncounter, u'com.myapp.oncounter') print("could not subscribe to topic: {0}".format(e))
print("subscribed to topic")
except Exception as e:
print("could not subscribe to topic: {0}".format(e))
We create an event handler function ``oncounter`` (you can name that as you like) which will get called whenever an event for the topic is received. We create an event handler function ``oncounter`` (you can name that as you like) which will get called whenever an event for the topic is received.
@ -373,27 +382,26 @@ When the subscription succeeds, we will receive any events published to ``u'com.
The corresponding **asyncio** code looks like this The corresponding **asyncio** code looks like this
.. code-block:: python .. code-block:: python
:linenos: :linenos:
:emphasize-lines: 15 :emphasize-lines: 14
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession
from asyncio import coroutine from asyncio import coroutine
class MyComponent(ApplicationSession): class MyComponent(ApplicationSession):
@coroutine
def onJoin(self, details):
print("session ready")
@coroutine def oncounter(count):
def onJoin(self, details): print("event received: {0}", count)
print("session ready")
def oncounter(count): try:
print("event received: {0}", count) yield from self.subscribe(oncounter, u'com.myapp.oncounter')
print("subscribed to topic")
try: except Exception as e:
yield from self.subscribe(oncounter, u'com.myapp.oncounter') print("could not subscribe to topic: {0}".format(e))
print("subscribed to topic")
except Exception as e:
print("could not subscribe to topic: {0}".format(e))
Again, nearly identical to Twisted. Again, nearly identical to Twisted.
@ -401,112 +409,109 @@ Again, nearly identical to Twisted.
.. _publishing-events: .. _publishing-events:
Publishing Events Publishing Events
................. -----------------
Publishing an event to a topic is done by calling :func:`autobahn.wamp.interfaces.IPublisher.publish`. Publishing an event to a topic is done by calling :func:`autobahn.wamp.interfaces.IPublisher.publish`.
Events can carry arbitrary positional and keyword based payload - as long as the payload is serializable in JSON. Events can carry arbitrary positional and keyword based payload -- as long as the payload is serializable in JSON.
Here is a **Twisted** example that will publish an event to topic ``u'com.myapp.oncounter'`` with a single (positional) payload being a counter that is incremented for each publish Here is a **Twisted** example that will publish an event to topic ``u'com.myapp.oncounter'`` with a single (positional) payload being a counter that is incremented for each publish:
.. code-block:: python .. code-block:: python
:linenos: :linenos:
:emphasize-lines: 14 :emphasize-lines: 13
from autobahn.twisted.wamp import ApplicationSession from autobahn.twisted.wamp import ApplicationSession
from autobahn.twisted.util import sleep from autobahn.twisted.util import sleep
from twisted.internet.defer import inlineCallbacks from twisted.internet.defer import inlineCallbacks
class MyComponent(ApplicationSession): class MyComponent(ApplicationSession):
@inlineCallbacks
def onJoin(self, details):
print("session ready")
@inlineCallbacks counter = 0
def onJoin(self, details): while True:
print("session ready") self.publish(u'com.myapp.oncounter', counter)
counter += 1
counter = 0 yield sleep(1)
while True:
self.publish(u'com.myapp.oncounter', counter)
counter += 1
yield sleep(1)
The corresponding **asyncio** code looks like this The corresponding **asyncio** code looks like this
.. code-block:: python .. code-block:: python
:linenos: :linenos:
:emphasize-lines: 14 :emphasize-lines: 13
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession
from asyncio import sleep from asyncio import sleep
from asyncio import coroutine from asyncio import coroutine
class MyComponent(ApplicationSession): class MyComponent(ApplicationSession):
@coroutine
def onJoin(self, details):
print("session ready")
@coroutine counter = 0
def onJoin(self, details): while True:
print("session ready") self.publish(u'com.myapp.oncounter', counter)
counter += 1
counter = 0 yield from sleep(1)
while True:
self.publish(u'com.myapp.oncounter', counter)
counter += 1
yield from sleep(1)
.. tip:: .. tip::
By default, a publisher will not receive an event it publishes even when the publisher is *itself* subscribed to the topic subscribed to. This behavior can be overridden. By default, a publisher will not receive an event it publishes even when the publisher is *itself* subscribed to the topic subscribed to. This behavior can be overridden; see :class:`PublishOptions <autobahn.wamp.types.PublishOptions>` and ``exclude_me=False``.
.. tip:: .. tip::
By default, publications are *unacknowledged*. This means, a ``publish()`` may fail *silently* (like when the session is not authorized to publish to the given topic). This behavior can be overridden. By default, publications are *unacknowledged*. This means, a ``publish()`` may fail *silently* (like when the session is not authorized to publish to the given topic). This behavior can be overridden; see :class:`PublishOptions <autobahn.wamp.types.PublishOptions>` and ``acknowledge=True``.
.. _session_lifecycle: .. _session_lifecycle:
Session Lifecycle Session Lifecycle
----------------- =================
A WAMP application component has this lifecycle: A WAMP application component has this lifecycle:
1. component created 1. component created
2. transport connected 2. transport connected (:meth:`ISession.onConnect <autobahn.wamp.interfaces.ISession.onConnect>` called)
3. authentication challenge received (only for authenticated WAMP sessions) 3. authentication challenge received (only for authenticated WAMP sessions, :meth:`ISession.onChallenge <autobahn.wamp.interfaces.ISession.onChallenge>` called)
4. session established (realm joined) 4. session established (realm joined, :meth:`ISession.onJoin <autobahn.wamp.interfaces.ISession.onJoin>` called)
5. session closed (realm left) 5. session closed (realm left, :meth:`ISession.onLeave <autobahn.wamp.interfaces.ISession.onLeave>` called)
6. transport disconnected 6. transport disconnected (:meth:`ISession.onDisconnect <autobahn.wamp.interfaces.ISession.onDisconnect>` called)
The `ApplicationSession` will fire the following events which you can handle by overriding the respective method (see :class:`autobahn.wamp.interfaces.ISession` for more information): The :class:`ApplicationSession <autobahn.twisted.wamp.ApplicationSession>` will fire the following events which you can handle by overriding the respective method (see :class:`ISession <autobahn.wamp.interfaces.ISession>` for more information):
.. code-block:: python .. code-block:: python
class MyComponent(ApplicationSession): class MyComponent(ApplicationSession):
def __init__(self, config=None):
ApplicationSession.__init__(self, config)
print("component created")
def __init__(self, config = None): def onConnect(self):
ApplicationSession.__init__(self, config) print("transport connected")
print("component created") self.join(self.config.realm)
def onConnect(self): def onChallenge(self, challenge):
print("transport connected") print("authentication challenge received")
self.join(self.config.realm)
def onChallenge(self, challenge): def onJoin(self, details):
print("authentication challenge received") print("session joined")
def onJoin(self, details): def onLeave(self, details):
print("session joined") print("session left")
def onLeave(self, details): def onDisconnect(self):
print("session left") print("transport disconnected")
def onDisconnect(self):
print("transport disconnected")
Upgrading Upgrading
--------- =========
From < 0.8.0 From < 0.8.0
............ ------------
Starting with release 0.8.0, |Ab| now supports WAMP v2, and also support both Twisted and asyncio. This required changing module naming for WAMP v1 (which is Twisted only). Starting with release 0.8.0, |Ab| now supports WAMP v2, and also support both Twisted and asyncio. This required changing module naming for WAMP v1 (which is Twisted only).
@ -526,6 +531,6 @@ should be modified for |ab| **>= 0.8.0** for (using Twisted)
From < 0.9.4 From < 0.9.4
............ ------------
Starting with release 0.9.4, all WAMP router code in |Ab| has been split out and moved to `Crossbar.io <http://crossbar.io>`_. Please see the announcement `here <https://groups.google.com/d/msg/autobahnws/bCj7O2G2sxA/6-pioJZ_S_MJ>`__. Starting with release 0.9.4, all WAMP router code in |Ab| has been split out and moved to `Crossbar.io <http://crossbar.io>`_. Please see the announcement `here <https://groups.google.com/d/msg/autobahnws/bCj7O2G2sxA/6-pioJZ_S_MJ>`__.

View File

@ -14,3 +14,6 @@ flake8:
pylint: pylint:
pylint -d line-too-long,invalid-name . pylint -d line-too-long,invalid-name .
examples:
python run-all-examples.py

View File

@ -1,15 +1,25 @@
# Autobahn|Python Examples # Autobahn|Python Examples
This folder contains complete working code examples that demonstrate various This folder contains complete working code examples that demonstrate various features of **Autobahn**|Python:
features of **Autobahn**|Python:
1. **Twisted**-based Examples 1. **Twisted**-based Examples
* [WebSocket](twisted/websocket) * [WebSocket](twisted/websocket/README.md)
* [WAMP](twisted/wamp) * [WAMP](twisted/wamp/README.md)
2. **asyncio**-based Examples 2. **asyncio**-based Examples
* [WebSocket](asyncio/websocket) * [WebSocket](asyncio/websocket/README.md)
* [WAMP](asyncio/wamp) * [WAMP](asyncio/wamp/README.md)
> Note: old Twisted / WAMP v1 examples are [here](twisted/wamp1) If you are new to Autobahn and WAMP, you should start with the following if you're going to use Twisted:
* twisted/wamp/pubsub/basic/
* twisted/wamp/rpc/arguments/
...whereas if you prefer asyncio:
* asyncio/wamp/pubsub/basic/
* asycnio/wamp/rpc/arguments/
Note that many of the examples use the same URIs for topics or RPC endpoints, so you can mix and match which `backend` or `frontend` script (whether Python or JavaScript) you use. For example, a Web browser tab could load a `backend.html` page that does publishes while you run a Python `frontend.py` that subscribes to those topics.
[Set up locally to run the examples](running-the-examples.md).

View File

@ -0,0 +1,80 @@
#!/usr/bin/env python
# scripted demo for https://asciinema.org/
# to use:
# 1. create virtualenv with autobahn, ansicolors and asciinema installed:
# pip install autobahn asciinema ansicolors
# 2. change to root of fresh AutobahnPython checkout
# 3. a) to record and upload, run:
#
# asciinema -c ./examples/asciinema-autobahn-demo.py rec
#
# 3. b) to just test this (e.g. without recording anything):
#
# python asciinema-autobahn-demo0.py
import os
import sys
import time
import random
import colors
import subprocess
prompt = 'user@machine:~/autobahn-python$ '
def interkey_interval():
"""in milliseconds"""
# return 0 # makes testing faster
return (random.lognormvariate(0.0, 0.5) * 30.0) / 1000.0
return float(random.randrange(10,50)) / 1000.0
def type_it_out(line):
for c in line:
sys.stdout.write(c)
sys.stdout.flush()
time.sleep(interkey_interval())
def do_commands(lines):
for line in lines:
sys.stdout.write(colors.blue(prompt))
type_it_out(line)
time.sleep(0.5)
print
os.system(colors.strip_color(line))
commands = [
"clear",
colors.red('# Welcome! Here we set up and run one basic'),
colors.red('# http://autobahn.ws example'),
colors.red('# (Note there are many other examples to try)'),
colors.red('#'),
colors.red("# I presume you've got a clone of https://github.com/tavendo/AutobahnPython"),
colors.red("# in ~/autobahn-python"),
"sleep 5",
"clear",
colors.red("# first, we create a virtualenv:"),
"virtualenv venv-autobahn",
"./venv-autobahn/bin/" + colors.bold("pip install -q --editable ."),
colors.red("# we also need a WAMP router"),
colors.red("# so we will use http://crossbar.io"),
"./venv-autobahn/bin/" + colors.bold("pip install -q crossbar"),
"clear",
colors.red("# we have installed the AutobahnPython checkout, and crossbar."),
colors.red("# the examples have a suitable crossbar configuration"),
"./venv-autobahn/bin/" + colors.bold("crossbar start --cbdir examples/router/.crossbar &"),
"sleep 2",
colors.red('# now we run a simple "backend" which registers some callable methods'),
"./venv-autobahn/bin/" + colors.bold("python examples/twisted/wamp/rpc/arguments/backend.py &"),
"sleep 2",
colors.red('# ...and a frontend that calls those methods'),
"./venv-autobahn/bin/" + colors.bold("python examples/twisted/wamp/rpc/arguments/frontend.py"),
colors.red('# Thanks for watching!'),
colors.red('# http://autobahn.ws/python/wamp/examples.html'),
"sleep 5",
]
if __name__ == '__main__':
do_commands(commands)

View File

@ -1 +0,0 @@
node_modules

View File

@ -1,150 +0,0 @@
all:
@echo "Targets: s1-s14, f1-f14, b1-b14"
s:
PYTHONPATH=../../../../autobahn python3 server.py
s1:
PYTHONPATH=../../../../autobahn python3 server.py --component "rpc.timeservice.backend.Component"
f1:
PYTHONPATH=../../../../autobahn python3 client.py --component "rpc.timeservice.frontend.Component"
b1:
PYTHONPATH=../../../../autobahn python3 client.py --component "rpc.timeservice.backend.Component"
s2:
PYTHONPATH=../../../../autobahn python3 server.py --component "rpc.slowsquare.backend.Component"
f2:
PYTHONPATH=../../../../autobahn python3 client.py --component "rpc.slowsquare.frontend.Component"
b2:
PYTHONPATH=../../../../autobahn python3 client.py --component "rpc.slowsquare.backend.Component"
s3:
PYTHONPATH=../../../../autobahn python3 server.py --component "rpc.arguments.backend.Component"
f3:
PYTHONPATH=../../../../autobahn python3 client.py --component "rpc.arguments.frontend.Component"
b3:
PYTHONPATH=../../../../autobahn python3 client.py --component "rpc.arguments.backend.Component"
s4:
PYTHONPATH=../../../../autobahn python3 server.py --component "rpc.options.backend.Component"
f4:
PYTHONPATH=../../../../autobahn python3 client.py --component "rpc.options.frontend.Component"
b4:
PYTHONPATH=../../../../autobahn python3 client.py --component "rpc.options.backend.Component"
s5:
PYTHONPATH=../../../../autobahn python3 server.py --component "rpc.errors.backend.Component"
f5:
PYTHONPATH=../../../../autobahn python3 client.py --component "rpc.errors.frontend.Component"
b5:
PYTHONPATH=../../../../autobahn python3 client.py --component "rpc.errors.backend.Component"
s6:
PYTHONPATH=../../../../autobahn python3 server.py --component "rpc.complex.backend.Component"
f6:
PYTHONPATH=../../../../autobahn python3 client.py --component "rpc.complex.frontend.Component"
b6:
PYTHONPATH=../../../../autobahn python3 client.py --component "rpc.complex.backend.Component"
s7:
PYTHONPATH=../../../../autobahn python3 server.py --component "rpc.progress.backend.Component"
f7:
PYTHONPATH=../../../../autobahn python3 client.py --component "rpc.progress.frontend.Component"
b7:
PYTHONPATH=../../../../autobahn python3 client.py --component "rpc.progress.backend.Component"
s8:
PYTHONPATH=../../../../autobahn python3 server.py --component "rpc.decorators.backend.Component"
f8:
PYTHONPATH=../../../../autobahn python3 client.py --component "rpc.decorators.frontend.Component"
b8:
PYTHONPATH=../../../../autobahn python3 client.py --component "rpc.decorators.backend.Component"
s9:
PYTHONPATH=../../../../autobahn python3 server.py --component "pubsub.basic.backend.Component"
f9:
PYTHONPATH=../../../../autobahn python3 client.py --component "pubsub.basic.frontend.Component"
b9:
PYTHONPATH=../../../../autobahn python3 client.py --component "pubsub.basic.backend.Component"
s10:
PYTHONPATH=../../../../autobahn python3 server.py --component "pubsub.complex.backend.Component"
f10:
PYTHONPATH=../../../../autobahn python3 client.py --component "pubsub.complex.frontend.Component"
b10:
PYTHONPATH=../../../../autobahn python3 client.py --component "pubsub.complex.backend.Component"
s11:
PYTHONPATH=../../../../autobahn python3 server.py --component "pubsub.options.backend.Component"
f11:
PYTHONPATH=../../../../autobahn python3 client.py --component "pubsub.options.frontend.Component"
b11:
PYTHONPATH=../../../../autobahn python3 client.py --component "pubsub.options.backend.Component"
s12:
PYTHONPATH=../../../../autobahn python3 server.py --component "pubsub.unsubscribe.backend.Component"
f12:
PYTHONPATH=../../../../autobahn python3 client.py --component "pubsub.unsubscribe.frontend.Component"
b12:
PYTHONPATH=../../../../autobahn python3 client.py --component "pubsub.unsubscribe.backend.Component"
s13:
PYTHONPATH=../../../../autobahn python3 server.py --component "pubsub.decorators.backend.Component"
f13:
PYTHONPATH=../../../../autobahn python3 client.py --component "pubsub.decorators.frontend.Component"
b13:
PYTHONPATH=../../../../autobahn python3 client.py --component "pubsub.decorators.backend.Component"
s14:
PYTHONPATH=../../../../autobahn python3 server.py --component "session.series.backend.Component"
f14:
PYTHONPATH=../../../../autobahn python3 client.py --component "session.series.frontend.Component"
b14:
PYTHONPATH=../../../../autobahn python3 client.py --component "session.series.backend.Component"
client_session_fromoutside_backend:
PYTHONPATH=../../../../autobahn python3 session/fromoutside/backend.py

View File

@ -1,116 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
if __name__ == '__main__':
import sys
import argparse
try:
import asyncio
except ImportError:
# Trollius >= 0.3 was renamed
import trollius as asyncio
# parse command line arguments
##
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--debug", action="store_true",
help="Enable debug output.")
parser.add_argument("-c", "--component", type=str,
help="Start WAMP client with this application component, e.g. 'timeservice.TimeServiceFrontend'")
parser.add_argument("-r", "--realm", type=str, default="realm1",
help="The WAMP realm to start the component in (if any).")
parser.add_argument("--host", type=str, default="127.0.0.1",
help='IP or hostname to connect to.')
parser.add_argument("--port", type=int, default=8080,
help='TCP port to connect to.')
parser.add_argument("--transport", choices=['websocket', 'rawsocket-json', 'rawsocket-msgpack'], default="websocket",
help='WAMP transport type')
parser.add_argument("--url", type=str, default=None,
help='The WebSocket URL to connect to, e.g. ws://127.0.0.1:8080/ws.')
args = parser.parse_args()
# create a WAMP application session factory
##
from autobahn.asyncio.wamp import ApplicationSessionFactory
from autobahn.wamp import types
session_factory = ApplicationSessionFactory(types.ComponentConfig(realm=args.realm))
# dynamically load the application component ..
##
import importlib
c = args.component.split('.')
mod, klass = '.'.join(c[:-1]), c[-1]
app = importlib.import_module(mod)
# .. and set the session class on the factory
##
session_factory.session = getattr(app, klass)
if args.transport == "websocket":
# create a WAMP-over-WebSocket transport client factory
##
from autobahn.asyncio.websocket import WampWebSocketClientFactory
transport_factory = WampWebSocketClientFactory(session_factory, url=args.url, debug_wamp=args.debug)
transport_factory.setProtocolOptions(failByDrop=False)
elif args.transport in ['rawsocket-json', 'rawsocket-msgpack']:
# create a WAMP-over-RawSocket transport client factory
##
if args.transport == 'rawsocket-msgpack':
from autobahn.wamp.serializer import MsgPackSerializer
serializer = MsgPackSerializer()
elif args.transport == 'rawsocket-json':
from autobahn.wamp.serializer import JsonSerializer
serializer = JsonSerializer()
else:
raise Exception("should not arrive here")
from autobahn.asyncio.rawsocket import WampRawSocketClientFactory
transport_factory = WampRawSocketClientFactory(session_factory, serializer, debug=args.debug)
else:
raise Exception("logic error")
# start the client
loop = asyncio.get_event_loop()
coro = loop.create_connection(transport_factory, args.host, args.port)
loop.run_until_complete(coro)
# now enter the asyncio event loop
loop.run_forever()
loop.close()

View File

@ -1,25 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

View File

@ -1,25 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

View File

@ -1,25 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

View File

@ -1,25 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

View File

@ -1,25 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

View File

@ -1,25 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

View File

@ -1,25 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

View File

@ -1,25 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

View File

@ -1,25 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

View File

@ -1,25 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

View File

@ -1,25 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

View File

@ -1,25 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

View File

@ -1,25 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

View File

@ -1,25 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

View File

@ -1,25 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

View File

@ -1,129 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
if __name__ == '__main__':
import sys
import argparse
try:
import asyncio
except ImportError:
# Trollius >= 0.3 was renamed
import trollius as asyncio
# parse command line arguments
##
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--debug", action="store_true",
help="Enable debug output.")
parser.add_argument("-c", "--component", type=str, default=None,
help="Start WAMP server with this application component, e.g. 'timeservice.TimeServiceBackend', or None.")
parser.add_argument("-r", "--realm", type=str, default="realm1",
help="The WAMP realm to start the component in (if any).")
parser.add_argument("--interface", type=str, default="127.0.0.1",
help='IP of interface to listen on.')
parser.add_argument("--port", type=int, default=8080,
help='TCP port to listen on.')
parser.add_argument("--transport", choices=['websocket', 'rawsocket-json', 'rawsocket-msgpack'], default="websocket",
help='WAMP transport type')
args = parser.parse_args()
# create a WAMP router factory
##
from autobahn.asyncio.wamp import RouterFactory
router_factory = RouterFactory()
# create a WAMP router session factory
##
from autobahn.asyncio.wamp import RouterSessionFactory
session_factory = RouterSessionFactory(router_factory)
# if asked to start an embedded application component ..
##
if args.component:
# dynamically load the application component ..
##
import importlib
c = args.component.split('.')
mod, klass = '.'.join(c[:-1]), c[-1]
app = importlib.import_module(mod)
SessionKlass = getattr(app, klass)
# .. and create and add an WAMP application session to
# run next to the router
##
from autobahn.wamp import types
session_factory.add(SessionKlass(types.ComponentConfig(realm=args.realm)))
if args.transport == "websocket":
# create a WAMP-over-WebSocket transport server factory
##
from autobahn.asyncio.websocket import WampWebSocketServerFactory
transport_factory = WampWebSocketServerFactory(session_factory, debug_wamp=args.debug)
transport_factory.setProtocolOptions(failByDrop=False)
elif args.transport in ['rawsocket-json', 'rawsocket-msgpack']:
# create a WAMP-over-RawSocket transport server factory
##
if args.transport == 'rawsocket-msgpack':
from autobahn.wamp.serializer import MsgPackSerializer
serializer = MsgPackSerializer()
elif args.transport == 'rawsocket-json':
from autobahn.wamp.serializer import JsonSerializer
serializer = JsonSerializer()
else:
raise Exception("should not arrive here")
from autobahn.asyncio.rawsocket import WampRawSocketServerFactory
transport_factory = WampRawSocketServerFactory(session_factory, serializer, debug=args.debug)
else:
raise Exception("should not arrive here")
# start the server from an endpoint
##
loop = asyncio.get_event_loop()
coro = loop.create_server(transport_factory, args.interface, args.port)
server = loop.run_until_complete(coro)
try:
# now enter the asyncio event loop
loop.run_forever()
except KeyboardInterrupt:
pass
finally:
server.close()
loop.close()

View File

@ -1,25 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

View File

@ -1,25 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

View File

@ -1,44 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import datetime
from autobahn.asyncio.wamp import ApplicationSession
class Component(ApplicationSession):
"""
A simple time service application component.
"""
def onJoin(self, details):
def utcnow():
now = datetime.datetime.utcnow()
return now.strftime("%Y-%m-%dT%H:%M:%SZ")
self.register(utcnow, 'com.timeservice.now')

View File

@ -1,5 +0,0 @@
test_server:
PYTHONPATH="../../../../autobahn" ~/python34/bin/python3 server.py
test_client:
PYTHONPATH="../../../../autobahn" ~/python34/bin/python3 client.py

View File

@ -1,99 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import sys
try:
import asyncio
except ImportError:
# Trollius >= 0.3 was renamed
import trollius as asyncio
from autobahn.asyncio import wamp, websocket
class MyFrontendComponent(wamp.ApplicationSession):
"""
Application code goes here. This is an example component that calls
a remote procedure on a WAMP peer, subscribes to a topic to receive
events, and then stops the world after some events.
"""
def onConnect(self):
self.join(u"realm1")
@asyncio.coroutine
def onJoin(self, details):
# call a remote procedure
##
try:
now = yield from self.call(u'com.timeservice.now')
except Exception as e:
print("Error: {}".format(e))
else:
print("Current time from time service: {}".format(now))
# subscribe to a topic
##
self.received = 0
def on_event(i):
print("Got event: {}".format(i))
self.received += 1
if self.received > 5:
self.leave()
sub = yield from self.subscribe(on_event, u'com.myapp.topic1')
print("Subscribed with subscription ID {}".format(sub.id))
def onLeave(self, details):
self.disconnect()
def onDisconnect(self):
asyncio.get_event_loop().stop()
if __name__ == '__main__':
# 1) create a WAMP application session factory
session_factory = wamp.ApplicationSessionFactory()
session_factory.session = MyFrontendComponent
# 2) create a WAMP-over-WebSocket transport client factory
transport_factory = websocket.WampWebSocketClientFactory(session_factory,
debug=False,
debug_wamp=False)
# 3) start the client
loop = asyncio.get_event_loop()
coro = loop.create_connection(transport_factory, '127.0.0.1', 8080)
loop.run_until_complete(coro)
# 4) now enter the asyncio event loop
loop.run_forever()
loop.close()

View File

@ -1,101 +0,0 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import sys
import six
import datetime
try:
import asyncio
except ImportError:
# Trollius >= 0.3 was renamed
import trollius as asyncio
from autobahn.asyncio import wamp, websocket
class MyBackendComponent(wamp.ApplicationSession):
"""
Application code goes here. This is an example component that provides
a simple procedure which can be called remotely from any WAMP peer.
It also publishes an event every second to some topic.
"""
def onConnect(self):
self.join(u"realm1")
@asyncio.coroutine
def onJoin(self, details):
# register a procedure for remote calling
##
def utcnow():
print("Someone is calling me;)")
now = datetime.datetime.utcnow()
return six.u(now.strftime("%Y-%m-%dT%H:%M:%SZ"))
reg = yield from self.register(utcnow, u'com.timeservice.now')
print("Registered procedure with ID {}".format(reg.id))
# publish events to a topic
##
counter = 0
while True:
self.publish(u'com.myapp.topic1', counter)
print("Published event.")
counter += 1
yield from asyncio.sleep(1)
if __name__ == '__main__':
# 1) create a WAMP router factory
router_factory = wamp.RouterFactory()
# 2) create a WAMP router session factory
session_factory = wamp.RouterSessionFactory(router_factory)
# 3) Optionally, add embedded WAMP application sessions to the router
session_factory.add(MyBackendComponent())
# 4) create a WAMP-over-WebSocket transport server factory
transport_factory = websocket.WampWebSocketServerFactory(session_factory,
debug=False,
debug_wamp=False)
# 5) start the server
loop = asyncio.get_event_loop()
coro = loop.create_server(transport_factory, '127.0.0.1', 8080)
server = loop.run_until_complete(coro)
try:
# 6) now enter the asyncio event loop
loop.run_forever()
except KeyboardInterrupt:
pass
finally:
server.close()
loop.close()

View File

@ -0,0 +1,29 @@
from os import environ
import asyncio
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class MyComponent(ApplicationSession):
@asyncio.coroutine
def onJoin(self, details):
# a remote procedure; see frontend.py for a Python front-end
# that calls this. Any language with WAMP bindings can now call
# this procedure if its connected to the same router and realm.
def add2(x, y):
return x + y
yield from self.register(add2, 'com.myapp.add2');
# publish an event every second. The event payloads can be
# anything JSON- and msgpack- serializable
while True:
self.publish('com.myapp.hello', 'Hello, world!')
yield from asyncio.sleep(1)
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(MyComponent)

View File

@ -0,0 +1,26 @@
from os import environ
import asyncio
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class MyComponent(ApplicationSession):
@asyncio.coroutine
def onJoin(self, details):
# listening for the corresponding message from the "backend"
# (any session that .publish()es to this topic).
def onevent(msg):
print("Got event: {}".format(msg))
yield from self.subscribe(onevent, 'com.myapp.hello')
# call a remote procedure.
res = yield from self.call('com.myapp.add2', 2, 3)
print("Got result: {}".format(res))
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(MyComponent)

View File

@ -30,11 +30,11 @@ except ImportError:
# Trollius >= 0.3 was renamed # Trollius >= 0.3 was renamed
import trollius as asyncio import trollius as asyncio
from autobahn.asyncio.wamp import ApplicationSession from os import environ
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
An application component that publishes an event every second. An application component that publishes an event every second.
""" """
@ -43,6 +43,17 @@ class Component(ApplicationSession):
def onJoin(self, details): def onJoin(self, details):
counter = 0 counter = 0
while True: while True:
print("publish: com.myapp.topic1", counter)
self.publish('com.myapp.topic1', counter) self.publish('com.myapp.topic1', counter)
counter += 1 counter += 1
yield from asyncio.sleep(1) yield from asyncio.sleep(1)
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -30,14 +30,14 @@ except ImportError:
# Trollius >= 0.3 was renamed # Trollius >= 0.3 was renamed
import trollius as asyncio import trollius as asyncio
from autobahn.asyncio.wamp import ApplicationSession from os import environ
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
An application component that subscribes and receives events, An application component that subscribes and receives events, and
and stop after having received 5 events. stop after having received 5 events.
""" """
@asyncio.coroutine @asyncio.coroutine
@ -55,3 +55,13 @@ class Component(ApplicationSession):
def onDisconnect(self): def onDisconnect(self):
asyncio.get_event_loop().stop() asyncio.get_event_loop().stop()
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -25,6 +25,7 @@
############################################################################### ###############################################################################
import random import random
from os import environ
try: try:
import asyncio import asyncio
@ -33,25 +34,35 @@ except ImportError:
import trollius as asyncio import trollius as asyncio
from autobahn.wamp.types import SubscribeOptions from autobahn.wamp.types import SubscribeOptions
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
An application component that publishes events with no payload An application component that publishes events with no payload and
and with complex payloads every second. with complex payloads every second.
""" """
@asyncio.coroutine @asyncio.coroutine
def onJoin(self, details): def onJoin(self, details):
counter = 0 counter = 0
while True: while True:
print("publish: com.myapp.heartbeat")
self.publish('com.myapp.heartbeat') self.publish('com.myapp.heartbeat')
obj = {'counter': counter, 'foo': [1, 2, 3]} obj = {'counter': counter, 'foo': [1, 2, 3]}
print("publish: com.myapp.topic2")
self.publish('com.myapp.topic2', random.randint(0, 100), 23, c="Hello", d=obj) self.publish('com.myapp.topic2', random.randint(0, 100), 23, c="Hello", d=obj)
counter += 1 counter += 1
yield from asyncio.sleep(1) yield from asyncio.sleep(1)
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -25,6 +25,7 @@
############################################################################### ###############################################################################
import random import random
from os import environ
try: try:
import asyncio import asyncio
@ -33,19 +34,17 @@ except ImportError:
import trollius as asyncio import trollius as asyncio
from autobahn.wamp.types import SubscribeOptions from autobahn.wamp.types import SubscribeOptions
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
An application component that subscribes and receives events An application component that subscribes and receives events of no
of no payload and of complex payload, and stops after 5 seconds. payload and of complex payload, and stops after 5 seconds.
""" """
@asyncio.coroutine @asyncio.coroutine
def onJoin(self, details): def onJoin(self, details):
self.received = 0 self.received = 0
def on_heartbeat(details=None): def on_heartbeat(details=None):
@ -57,8 +56,17 @@ class Component(ApplicationSession):
print("Got event: {} {} {} {}".format(a, b, c, d)) print("Got event: {} {} {} {}".format(a, b, c, d))
yield from self.subscribe(on_topic2, 'com.myapp.topic2') yield from self.subscribe(on_topic2, 'com.myapp.topic2')
asyncio.get_event_loop().call_later(5, self.leave) asyncio.get_event_loop().call_later(5, self.leave)
def onDisconnect(self): def onDisconnect(self):
asyncio.get_event_loop().stop() asyncio.get_event_loop().stop()
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -24,17 +24,18 @@
# #
############################################################################### ###############################################################################
from os import environ
try: try:
import asyncio import asyncio
except ImportError: except ImportError:
# Trollius >= 0.3 was renamed # Trollius >= 0.3 was renamed
import trollius as asyncio import trollius as asyncio
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
An application component that publishes an event every second. An application component that publishes an event every second.
""" """
@ -43,7 +44,20 @@ class Component(ApplicationSession):
def onJoin(self, details): def onJoin(self, details):
counter = 0 counter = 0
while True: while True:
print("publish: com.myapp.topic1", counter)
self.publish('com.myapp.topic1', counter) self.publish('com.myapp.topic1', counter)
print("publish: com.myapp.topic2 'Hello world.'")
self.publish('com.myapp.topic2', "Hello world.") self.publish('com.myapp.topic2', "Hello world.")
counter += 1 counter += 1
yield from asyncio.sleep(1) yield from asyncio.sleep(1)
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -24,6 +24,8 @@
# #
############################################################################### ###############################################################################
from os import environ
try: try:
import asyncio import asyncio
except ImportError: except ImportError:
@ -31,24 +33,21 @@ except ImportError:
import trollius as asyncio import trollius as asyncio
from autobahn import wamp from autobahn import wamp
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
An application component that subscribes and receives events, An application component that subscribes and receives events, and
and stop after having received 5 events. stop after having received 5 events.
""" """
@asyncio.coroutine @asyncio.coroutine
def onJoin(self, details): def onJoin(self, details):
self.received = 0 self.received = 0
# subscribe all methods on this object decorated with "@wamp.subscribe" # subscribe all methods on this object decorated with "@wamp.subscribe"
# as PubSub event handlers # as PubSub event handlers
##
results = yield from self.subscribe(self) results = yield from self.subscribe(self)
for res in results: for res in results:
if isinstance(res, wamp.protocol.Subscription): if isinstance(res, wamp.protocol.Subscription):
@ -71,3 +70,13 @@ class Component(ApplicationSession):
def onDisconnect(self): def onDisconnect(self):
asyncio.get_event_loop().stop() asyncio.get_event_loop().stop()
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -24,6 +24,8 @@
# #
############################################################################### ###############################################################################
from os import environ
try: try:
import asyncio import asyncio
except ImportError: except ImportError:
@ -31,11 +33,10 @@ except ImportError:
import trollius as asyncio import trollius as asyncio
from autobahn.wamp.types import PublishOptions, EventDetails, SubscribeOptions from autobahn.wamp.types import PublishOptions, EventDetails, SubscribeOptions
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
An application component that publishes an event every second. An application component that publishes an event every second.
""" """
@ -45,13 +46,24 @@ class Component(ApplicationSession):
def on_event(i): def on_event(i):
print("Got event: {}".format(i)) print("Got event: {}".format(i))
yield from self.subscribe(on_event, 'com.myapp.topic1') yield from self.subscribe(on_event, 'com.myapp.topic1')
counter = 0 counter = 0
while True: while True:
publication = yield from self.publish('com.myapp.topic1', counter, publication = yield from self.publish(
options=PublishOptions(acknowledge=True, discloseMe=True, excludeMe=False)) 'com.myapp.topic1', counter,
options=PublishOptions(acknowledge=True, disclose_me=True, exclude_me=False)
)
print("Event published with publication ID {}".format(publication.id)) print("Event published with publication ID {}".format(publication.id))
counter += 1 counter += 1
yield from asyncio.sleep(1) yield from asyncio.sleep(1)
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -24,6 +24,8 @@
# #
############################################################################### ###############################################################################
from os import environ
try: try:
import asyncio import asyncio
except ImportError: except ImportError:
@ -31,19 +33,17 @@ except ImportError:
import trollius as asyncio import trollius as asyncio
from autobahn.wamp.types import PublishOptions, EventDetails, SubscribeOptions from autobahn.wamp.types import PublishOptions, EventDetails, SubscribeOptions
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
An application component that subscribes and receives events, An application component that subscribes and receives events, and
and stop after having received 5 events. stop after having received 5 events.
""" """
@asyncio.coroutine @asyncio.coroutine
def onJoin(self, details): def onJoin(self, details):
self.received = 0 self.received = 0
def on_event(i, details=None): def on_event(i, details=None):
@ -57,3 +57,13 @@ class Component(ApplicationSession):
def onDisconnect(self): def onDisconnect(self):
asyncio.get_event_loop().stop() asyncio.get_event_loop().stop()
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -0,0 +1,3 @@
This corresponds to the Twisted version with similar path; see the
README there for how to configure crossbar and create a self-signed
certificate.

View File

@ -0,0 +1,69 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from __future__ import print_function
try:
import asyncio
except ImportError:
# Trollius >= 0.3 was renamed
import trollius as asyncio
from os import environ
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
import ssl
class Component(ApplicationSession):
"""
An application component that publishes an event every second.
"""
@asyncio.coroutine
def onJoin(self, details):
counter = 0
while True:
print("publish: com.myapp.topic1", counter)
self.publish('com.myapp.topic1', counter)
counter += 1
yield from asyncio.sleep(1)
if __name__ == '__main__':
# see README; this way everything accesses same cert-files
cert_path = '../../../../twisted/wamp/pubsub/tls/server.crt'
print(cert_path)
# create an ssl.Context using just our self-signed cert as the CA certificates
options = ssl.create_default_context(cadata=open(cert_path, 'r').read())
# ...which we pass as "ssl=" to ApplicationRunner (passed to loop.create_connection)
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "wss://127.0.0.1:8083/ws"),
u"crossbardemo",
ssl=options, # try removing this, but still use self-signed cert
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -24,17 +24,18 @@
# #
############################################################################### ###############################################################################
from os import environ
try: try:
import asyncio import asyncio
except ImportError: except ImportError:
# Trollius >= 0.3 was renamed # Trollius >= 0.3 was renamed
import trollius as asyncio import trollius as asyncio
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
An application component that subscribes and receives events. An application component that subscribes and receives events.
After receiving 5 events, it unsubscribes, sleeps and then After receiving 5 events, it unsubscribes, sleeps and then
@ -43,10 +44,9 @@ class Component(ApplicationSession):
@asyncio.coroutine @asyncio.coroutine
def test(self): def test(self):
self.received = 0 self.received = 0
# @asyncio.coroutine @asyncio.coroutine
def on_event(i): def on_event(i):
print("Got event: {}".format(i)) print("Got event: {}".format(i))
self.received += 1 self.received += 1
@ -55,21 +55,30 @@ class Component(ApplicationSession):
if self.runs > 1: if self.runs > 1:
self.leave() self.leave()
else: else:
self.subscription.unsubscribe() yield from self.subscription.unsubscribe()
# yield from self.subscription.unsubscribe()
print("Unsubscribed .. continue in 2s ..")
# FIXME print("Unsubscribed .. continue in 5s ..")
asyncio.get_event_loop().call_later(2, self.test) # can't use loop.call_later() with a coroutine for some reason
yield from asyncio.sleep(5)
yield from self.test()
self.subscription = yield from self.subscribe(on_event, 'com.myapp.topic1') self.subscription = yield from self.subscribe(on_event, 'com.myapp.topic1')
print("Subscribed with subscription ID {}".format(self.subscription.id)) print("Subscribed with subscription ID {}".format(self.subscription.id))
@asyncio.coroutine @asyncio.coroutine
def onJoin(self, details): def onJoin(self, details):
self.runs = 0 self.runs = 0
yield from self.test() yield from self.test()
def onDisconnect(self): def onDisconnect(self):
asyncio.get_event_loop().stop() asyncio.get_event_loop().stop()
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -24,15 +24,23 @@
# #
############################################################################### ###############################################################################
from autobahn.asyncio.wamp import ApplicationSession try:
import asyncio
except ImportError:
# Trollius >= 0.3 was renamed
import trollius as asyncio
from os import environ
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
An application component providing procedures with different kinds of arguments. An application component providing procedures with different kinds
of arguments.
""" """
@asyncio.coroutine
def onJoin(self, details): def onJoin(self, details):
def ping(): def ping():
@ -51,8 +59,19 @@ class Component(ApplicationSession):
def arglen(*args, **kwargs): def arglen(*args, **kwargs):
return [len(args), len(kwargs)] return [len(args), len(kwargs)]
self.register(ping, u'com.arguments.ping') yield from self.register(ping, u'com.arguments.ping')
self.register(add2, u'com.arguments.add2') yield from self.register(add2, u'com.arguments.add2')
self.register(stars, u'com.arguments.stars') yield from self.register(stars, u'com.arguments.stars')
self.register(orders, u'com.arguments.orders') yield from self.register(orders, u'com.arguments.orders')
self.register(arglen, u'com.arguments.arglen') yield from self.register(arglen, u'com.arguments.arglen')
print("Registered methods; ready for frontend.")
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -30,11 +30,11 @@ except ImportError:
# Trollius >= 0.3 was renamed # Trollius >= 0.3 was renamed
import trollius as asyncio import trollius as asyncio
from autobahn.asyncio.wamp import ApplicationSession from os import environ
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
An application component calling the different backend procedures. An application component calling the different backend procedures.
""" """
@ -82,3 +82,13 @@ class Component(ApplicationSession):
def onDisconnect(self): def onDisconnect(self):
asyncio.get_event_loop().stop() asyncio.get_event_loop().stop()
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -30,26 +30,37 @@ except ImportError:
# Trollius >= 0.3 was renamed # Trollius >= 0.3 was renamed
import trollius as asyncio import trollius as asyncio
from os import environ
from autobahn.wamp.types import CallResult from autobahn.wamp.types import CallResult
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
Application component that provides procedures which Application component that provides procedures which
return complex results. return complex results.
""" """
@asyncio.coroutine
def onJoin(self, details): def onJoin(self, details):
def add_complex(a, ai, b, bi): def add_complex(a, ai, b, bi):
return CallResult(c=a + b, ci=ai + bi) return CallResult(c=a + b, ci=ai + bi)
self.register(add_complex, 'com.myapp.add_complex') yield from self.register(add_complex, 'com.myapp.add_complex')
def split_name(fullname): def split_name(fullname):
forename, surname = fullname.split() forename, surname = fullname.split()
return CallResult(forename, surname) return CallResult(forename, surname)
self.register(split_name, 'com.myapp.split_name') yield from self.register(split_name, 'com.myapp.split_name')
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -30,12 +30,12 @@ except ImportError:
# Trollius >= 0.3 was renamed # Trollius >= 0.3 was renamed
import trollius as asyncio import trollius as asyncio
from os import environ
from autobahn.wamp.types import CallResult from autobahn.wamp.types import CallResult
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
Application component that calls procedures which Application component that calls procedures which
produce complex results and showing how to access those. produce complex results and showing how to access those.
@ -54,3 +54,13 @@ class Component(ApplicationSession):
def onDisconnect(self): def onDisconnect(self):
asyncio.get_event_loop().stop() asyncio.get_event_loop().stop()
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -24,6 +24,7 @@
# #
############################################################################### ###############################################################################
from os import environ
import datetime import datetime
try: try:
@ -33,11 +34,10 @@ except ImportError:
import trollius as asyncio import trollius as asyncio
from autobahn import wamp from autobahn import wamp
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
An application component registering RPC endpoints using decorators. An application component registering RPC endpoints using decorators.
""" """
@ -71,3 +71,13 @@ class Component(ApplicationSession):
return float(x) / float(y) return float(x) / float(y)
else: else:
return 0 return 0
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -30,11 +30,11 @@ except ImportError:
# Trollius >= 0.3 was renamed # Trollius >= 0.3 was renamed
import trollius as asyncio import trollius as asyncio
from autobahn.asyncio.wamp import ApplicationSession from os import environ
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
An application component calling the different backend procedures. An application component calling the different backend procedures.
""" """
@ -57,3 +57,13 @@ class Component(ApplicationSession):
def onDisconnect(self): def onDisconnect(self):
asyncio.get_event_loop().stop() asyncio.get_event_loop().stop()
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -24,6 +24,7 @@
# #
############################################################################### ###############################################################################
from os import environ
import math import math
try: try:
@ -34,12 +35,11 @@ except ImportError:
from autobahn import wamp from autobahn import wamp
from autobahn.wamp.exception import ApplicationError from autobahn.wamp.exception import ApplicationError
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
@wamp.error("com.myapp.error1") @wamp.error("com.myapp.error1")
class AppError1(Exception): class AppError1(Exception):
""" """
An application specific exception that is decorated with a WAMP URI, An application specific exception that is decorated with a WAMP URI,
and hence can be automapped by Autobahn. and hence can be automapped by Autobahn.
@ -47,11 +47,11 @@ class AppError1(Exception):
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
Example WAMP application backend that raised exceptions. Example WAMP application backend that raised exceptions.
""" """
@asyncio.coroutine
def onJoin(self, details): def onJoin(self, details):
# raising standard exceptions # raising standard exceptions
@ -63,7 +63,7 @@ class Component(ApplicationSession):
# this also will raise, if x < 0 # this also will raise, if x < 0
return math.sqrt(x) return math.sqrt(x)
self.register(sqrt, 'com.myapp.sqrt') yield from self.register(sqrt, 'com.myapp.sqrt')
# raising WAMP application exceptions # raising WAMP application exceptions
## ##
@ -79,7 +79,7 @@ class Component(ApplicationSession):
# forward keyword arguments in exceptions # forward keyword arguments in exceptions
raise ApplicationError("com.myapp.error.invalid_length", min=3, max=10) raise ApplicationError("com.myapp.error.invalid_length", min=3, max=10)
self.register(checkname, 'com.myapp.checkname') yield from self.register(checkname, 'com.myapp.checkname')
# defining and automapping WAMP application exceptions # defining and automapping WAMP application exceptions
## ##
@ -89,4 +89,14 @@ class Component(ApplicationSession):
if a < b: if a < b:
raise AppError1(b - a) raise AppError1(b - a)
self.register(compare, 'com.myapp.compare') yield from self.register(compare, 'com.myapp.compare')
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -24,6 +24,7 @@
# #
############################################################################### ###############################################################################
from os import environ
import math import math
try: try:
@ -34,12 +35,11 @@ except ImportError:
from autobahn import wamp from autobahn import wamp
from autobahn.wamp.exception import ApplicationError from autobahn.wamp.exception import ApplicationError
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
@wamp.error("com.myapp.error1") @wamp.error("com.myapp.error1")
class AppError1(Exception): class AppError1(Exception):
""" """
An application specific exception that is decorated with a WAMP URI, An application specific exception that is decorated with a WAMP URI,
and hence can be automapped by Autobahn. and hence can be automapped by Autobahn.
@ -47,7 +47,6 @@ class AppError1(Exception):
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
Example WAMP application frontend that catches exceptions. Example WAMP application frontend that catches exceptions.
""" """
@ -84,7 +83,17 @@ class Component(ApplicationSession):
except AppError1 as e: except AppError1 as e:
print("Compare Error: {}".format(e)) print("Compare Error: {}".format(e))
self.leave() yield from self.leave()
def onDisconnect(self): def onDisconnect(self):
asyncio.get_event_loop().stop() asyncio.get_event_loop().stop()
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -30,17 +30,18 @@ except ImportError:
# Trollius >= 0.3 was renamed # Trollius >= 0.3 was renamed
import trollius as asyncio import trollius as asyncio
from os import environ
from autobahn.wamp.types import CallOptions, RegisterOptions, PublishOptions from autobahn.wamp.types import CallOptions, RegisterOptions, PublishOptions
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
An application component providing procedures with An application component providing procedures with
different kinds of arguments. different kinds of arguments.
""" """
@asyncio.coroutine
def onJoin(self, details): def onJoin(self, details):
def square(val, details=None): def square(val, details=None):
@ -56,4 +57,14 @@ class Component(ApplicationSession):
self.publish('com.myapp.square_on_nonpositive', val, options=options) self.publish('com.myapp.square_on_nonpositive', val, options=options)
return val * val return val * val
self.register(square, 'com.myapp.square', RegisterOptions(details_arg='details')) yield from self.register(square, 'com.myapp.square', RegisterOptions(details_arg='details'))
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -30,12 +30,12 @@ except ImportError:
# Trollius >= 0.3 was renamed # Trollius >= 0.3 was renamed
import trollius as asyncio import trollius as asyncio
from os import environ
from autobahn.wamp.types import CallOptions, RegisterOptions, PublishOptions from autobahn.wamp.types import CallOptions, RegisterOptions, PublishOptions
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
An application component calling the different backend procedures. An application component calling the different backend procedures.
""" """
@ -49,10 +49,20 @@ class Component(ApplicationSession):
yield from self.subscribe(on_event, 'com.myapp.square_on_nonpositive') yield from self.subscribe(on_event, 'com.myapp.square_on_nonpositive')
for val in [2, 0, -2]: for val in [2, 0, -2]:
res = yield from self.call('com.myapp.square', val, options=CallOptions(discloseMe=True)) res = yield from self.call('com.myapp.square', val, options=CallOptions(disclose_me=True))
print("Squared {} = {}".format(val, res)) print("Squared {} = {}".format(val, res))
self.leave() yield from self.leave()
def onDisconnect(self): def onDisconnect(self):
asyncio.get_event_loop().stop() asyncio.get_event_loop().stop()
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -30,16 +30,17 @@ except ImportError:
# Trollius >= 0.3 was renamed # Trollius >= 0.3 was renamed
import trollius as asyncio import trollius as asyncio
from os import environ
from autobahn.wamp.types import CallOptions, RegisterOptions from autobahn.wamp.types import CallOptions, RegisterOptions
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
Application component that produces progressive results. Application component that produces progressive results.
""" """
@asyncio.coroutine
def onJoin(self, details): def onJoin(self, details):
@asyncio.coroutine @asyncio.coroutine
@ -52,4 +53,14 @@ class Component(ApplicationSession):
yield from asyncio.sleep(1 * n) yield from asyncio.sleep(1 * n)
return n return n
self.register(longop, 'com.myapp.longop', RegisterOptions(details_arg='details')) yield from self.register(longop, 'com.myapp.longop', RegisterOptions(details_arg='details'))
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -30,12 +30,12 @@ except ImportError:
# Trollius >= 0.3 was renamed # Trollius >= 0.3 was renamed
import trollius as asyncio import trollius as asyncio
from os import environ
from autobahn.wamp.types import CallOptions, RegisterOptions from autobahn.wamp.types import CallOptions, RegisterOptions
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
Application component that consumes progressive results. Application component that consumes progressive results.
""" """
@ -54,3 +54,13 @@ class Component(ApplicationSession):
def onDisconnect(self): def onDisconnect(self):
asyncio.get_event_loop().stop() asyncio.get_event_loop().stop()
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -30,25 +30,37 @@ except ImportError:
# Trollius >= 0.3 was renamed # Trollius >= 0.3 was renamed
import trollius as asyncio import trollius as asyncio
from autobahn.asyncio.wamp import ApplicationSession from os import environ
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
A math service application component. A math service application component.
""" """
@asyncio.coroutine
def onJoin(self, details): def onJoin(self, details):
def square(x): def square(x):
return x * x return x * x
self.register(square, 'com.math.square') yield from self.register(square, 'com.math.square')
@asyncio.coroutine @asyncio.coroutine
def slowsquare(x, delay=1): def slowsquare(x, delay=1):
yield from asyncio.sleep(delay) yield from asyncio.sleep(delay)
return x * x return x * x
self.register(slowsquare, 'com.math.slowsquare') yield from self.register(slowsquare, 'com.math.slowsquare')
print("Registered 'com.math.slowsquare'")
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -24,6 +24,7 @@
# #
############################################################################### ###############################################################################
from os import environ
import time import time
try: try:
@ -34,11 +35,10 @@ except ImportError:
from functools import partial from functools import partial
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
An application component using the time service. An application component using the time service.
""" """
@ -65,3 +65,13 @@ class Component(ApplicationSession):
def onDisconnect(self): def onDisconnect(self):
asyncio.get_event_loop().stop() asyncio.get_event_loop().stop()
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

View File

@ -24,30 +24,38 @@
# #
############################################################################### ###############################################################################
try:
import asyncio
except ImportError:
# Trollius >= 0.3 was renamed
import trollius as asyncio
from os import environ
import datetime import datetime
from twisted.internet import reactor from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
from twisted.internet.defer import inlineCallbacks
from autobahn.twisted.wamp import ApplicationSession
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
A simple time service application component. A simple time service application component.
""" """
@asyncio.coroutine
def onJoin(self, details): def onJoin(self, details):
def utcnow(): def utcnow():
now = datetime.datetime.utcnow() now = datetime.datetime.utcnow()
return now.strftime("%Y-%m-%dT%H:%M:%SZ") return now.strftime("%Y-%m-%dT%H:%M:%SZ")
self.register(utcnow, 'com.timeservice.now') yield from self.register(utcnow, 'com.timeservice.now')
if __name__ == '__main__': if __name__ == '__main__':
from autobahn.twisted.wamp import ApplicationRunner runner = ApplicationRunner(
runner = ApplicationRunner("ws://127.0.0.1:8080/ws", "realm1") environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component) runner.run(Component)

View File

@ -24,6 +24,7 @@
# #
############################################################################### ###############################################################################
from os import environ
import datetime import datetime
try: try:
@ -32,11 +33,10 @@ except ImportError:
# Trollius >= 0.3 was renamed # Trollius >= 0.3 was renamed
import trollius as asyncio import trollius as asyncio
from autobahn.asyncio.wamp import ApplicationSession from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
class Component(ApplicationSession): class Component(ApplicationSession):
""" """
An application component using the time service. An application component using the time service.
""" """
@ -54,3 +54,13 @@ class Component(ApplicationSession):
def onDisconnect(self): def onDisconnect(self):
asyncio.get_event_loop().stop() asyncio.get_event_loop().stop()
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", "ws://127.0.0.1:8080/ws"),
u"crossbardemo",
debug_wamp=False, # optional; log many WAMP details
debug=False, # optional; log even more details
)
runner.run(Component)

Some files were not shown because too many files have changed in this diff Show More