# HG changeset patch # User Edouard Tisserant # Date 1530706620 -7200 # Node ID 46447d99e5f96e46f044ead406b373fbb1b45298 # Parent 81949104291d026ae5a02c38c900a0338888af61# Parent c27b820cb96bb98fd4af3022af83f0b122155045 Merged #2486, enhancements to WAMP client : auto reconnecting, wamp conf checking and saving, additional attributes for exposed calls, etc diff -r 81949104291d -r 46447d99e5f9 Beremiz_service.py --- a/Beremiz_service.py Wed Jul 04 14:10:03 2018 +0200 +++ b/Beremiz_service.py Wed Jul 04 14:17:00 2018 +0200 @@ -623,14 +623,13 @@ if wampconf is not None: try: - _wampconf = WC.LoadWampClientConf(wampconf) + WC.SetServer(pyroserver, wampconf, wampsecret) + _wampconf = WC.GetConfiguration() if _wampconf: - if _wampconf["url"]: # TODO : test more ? - WC.RegisterWampClient(wampconf, wampsecret) - pyruntimevars["wampsession"] = WC.GetSession - WC.SetServer(pyroserver) + if _wampconf.get("url", False) and _wampconf.get("active", False): # TODO : test more ? + WC.RegisterWampClient() else: - raise Exception(_("WAMP config is incomplete.")) + raise Exception(_("WAMP config is incomplete or active is false.")) else: raise Exception(_("WAMP config is missing.")) except Exception: diff -r 81949104291d -r 46447d99e5f9 runtime/WampClient.py --- a/runtime/WampClient.py Wed Jul 04 14:10:03 2018 +0200 +++ b/runtime/WampClient.py Wed Jul 04 14:17:00 2018 +0200 @@ -26,6 +26,8 @@ from __future__ import print_function import time import json +import os +import re from autobahn.twisted import wamp from autobahn.twisted.websocket import WampWebSocketClientFactory, connectWS from autobahn.wamp import types, auth @@ -34,21 +36,24 @@ from twisted.internet.protocol import ReconnectingClientFactory +_transportFactory = None _WampSession = None _PySrv = None +_WampConf = None +_WampSecret = None ExposedCalls = [ - "StartPLC", - "StopPLC", - "ForceReload", - "GetPLCstatus", - "NewPLC", - "MatchMD5", - "SetTraceVariablesList", - "GetTraceVariables", - "RemoteExec", - "GetLogMessage", - "ResetLogCount", + ("StartPLC", {}), + ("StopPLC", {}), + ("ForceReload", {}), + ("GetPLCstatus", {}), + ("NewPLC", {}), + ("MatchMD5", {}), + ("SetTraceVariablesList", {}), + ("GetTraceVariables", {}), + ("RemoteExec", {}), + ("GetLogMessage", {}), + ("ResetLogCount", {}) ] # Those two lists are meant to be filled by customized runtime @@ -69,11 +74,20 @@ obj = getattr(obj, names.pop(0)) return obj +def getValidOptins(options, arguments): + validOptions = {} + for key in options: + if key in arguments: + validOptions[key] = options[key] + if len(validOptions) > 0: + return validOptions + else: + return None class WampSession(wamp.ApplicationSession): def onConnect(self): if "secret" in self.config.extra: - user = self.config.extra["ID"].encode('utf8') + user = self.config.extra["ID"] self.join(u"Automation", [u"wampcra"], user) else: self.join(u"Automation") @@ -91,10 +105,15 @@ global _WampSession _WampSession = self ID = self.config.extra["ID"] - print('WAMP session joined by :', ID) - for name in ExposedCalls: - regoption = types.RegisterOptions(u'exact', u'last') - yield self.register(GetCallee(name), u'.'.join((ID, name)), regoption) + + for name, kwargs in ExposedCalls: + try: + registerOptions = types.RegisterOptions(**kwargs) + except TypeError as e: + registerOptions = None + print(_("TypeError register option: {}".format(e))) + + yield self.register(GetCallee(name), u'.'.join((ID, name)), registerOptions) for name in SubscribedEvents: yield self.subscribe(GetCallee(name), unicode(name)) @@ -102,32 +121,96 @@ for func in DoOnJoin: yield func(self) + print(_('WAMP session joined (%s) by:' % time.ctime()), ID) + def onLeave(self, details): - global _WampSession + global _WampSession, _transportFactory + super(WampSession, self).onLeave(details) _WampSession = None + _transportFactory = None print(_('WAMP session left')) class ReconnectingWampWebSocketClientFactory(WampWebSocketClientFactory, ReconnectingClientFactory): + def __init__(self, config, *args, **kwargs): + global _transportFactory + WampWebSocketClientFactory.__init__(self, *args, **kwargs) + + try: + protocolOptions = config.extra.get('protocolOptions', None) + if protocolOptions: + self.setProtocolOptions(**protocolOptions) + _transportFactory = self + except Exception, e: + print(_("Custom protocol options failed :"), e) + _transportFactory = None + + def buildProtocol(self, addr): + self.resetDelay() + return ReconnectingClientFactory.buildProtocol(self, addr) + def clientConnectionFailed(self, connector, reason): - print(_("WAMP Client connection failed (%s) .. retrying .." % time.ctime())) - ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) + if self.continueTrying: + print(_("WAMP Client connection failed (%s) .. retrying .." % time.ctime())) + super(ReconnectingWampWebSocketClientFactory, self).clientConnectionFailed(connector, reason) + else: + del connector def clientConnectionLost(self, connector, reason): - print(_("WAMP Client connection lost (%s) .. retrying .." % time.ctime())) - ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) - - -def LoadWampClientConf(wampconf): + if self.continueTrying: + print(_("WAMP Client connection lost (%s) .. retrying .." % time.ctime())) + super(ReconnectingWampWebSocketClientFactory, self).clientConnectionFailed(connector, reason) + else: + del connector + + +def GetConfiguration(items=None): try: - WSClientConf = json.load(open(wampconf)) + WSClientConf = json.load(open(_WampConf)) + if items and isinstance(items, list): + WSClientConfItems = {} + for item in items: + wampconf_value = WSClientConf.get(item, None) + if wampconf_value is not None: + WSClientConfItems[item] = wampconf_value + if WSClientConfItems: + return WSClientConfItems return WSClientConf except ValueError, ve: print(_("WAMP load error: "), ve) return None - except Exception: - return None - + except Exception, e: + print(_("WAMP load error: "), e) + return None + +def SetConfiguration(items): + try: + WSClientConf = json.load(open(_WampConf)) + saveChanges = False + if items: + for itemKey in items.keys(): + wampconf_value = WSClientConf.get(itemKey, None) + if (wampconf_value is not None) and (items[itemKey] is not None) and (wampconf_value != items[itemKey]): + WSClientConf[itemKey] = items[itemKey] + saveChanges = True + + if saveChanges: + with open(os.path.realpath(_WampConf), 'w') as f: + json.dump(WSClientConf, f, sort_keys=True, indent=4) + if 'active' in WSClientConf and WSClientConf['active']: + if _transportFactory and _WampSession: + StopReconnectWampClient() + StartReconnectWampClient() + else: + StopReconnectWampClient() + + return WSClientConf + except ValueError, ve: + print(_("WAMP save error: "), ve) + return None + except Exception, e: + print(_("WAMP save error: "), e) + return None def LoadWampSecret(secretfname): try: @@ -140,15 +223,33 @@ return None -def RegisterWampClient(wampconf, secretfname): - - WSClientConf = LoadWampClientConf(wampconf) +def IsCorrectUri(uri): + if re.match(r'w{1}s{1,2}:{1}/{2}.+:{1}[0-9]+/{1}.+', uri): + return True + else: + return False + + +def RegisterWampClient(wampconf=None, secretfname=None): + global _WampConf + if wampconf: + _WampConf = wampconf + WSClientConf = GetConfiguration() + else: + WSClientConf = GetConfiguration() if not WSClientConf: print(_("WAMP client connection not established!")) - return - - WampSecret = LoadWampSecret(secretfname) + return False + + if not IsCorrectUri(WSClientConf["url"]): + print(_("WAMP url {} is not correct!".format(WSClientConf["url"]))) + return False + + if secretfname: + WampSecret = LoadWampSecret(secretfname) + else: + WampSecret = LoadWampSecret(_WampSecret) if WampSecret is not None: WSClientConf["secret"] = WampSecret @@ -162,21 +263,48 @@ session_factory.session = WampSession # create a WAMP-over-WebSocket transport client factory - transport_factory = ReconnectingWampWebSocketClientFactory( + ReconnectingWampWebSocketClientFactory( + component_config, session_factory, url=WSClientConf["url"], serializers=[MsgPackSerializer()]) # start the client from a Twisted endpoint - conn = connectWS(transport_factory) - print(_("WAMP client connecting to :"), WSClientConf["url"]) - return conn + if _transportFactory: + conn = connectWS(_transportFactory) + print(_("WAMP client connecting to :"), WSClientConf["url"]) + return True + else: + print(_("WAMP client can not connect to :"), WSClientConf["url"]) + return False + + +def StopReconnectWampClient(): + _transportFactory.stopTrying() + return _WampSession.leave() + + +def StartReconnectWampClient(): + if _WampSession: + # do reconnect + _WampSession.disconnect() + return True + else: + # do connect + RegisterWampClient() + return True def GetSession(): return _WampSession -def SetServer(pysrv): - global _PySrv +def StatusWampClient(): + return _WampSession and _WampSession.is_attached() + + +def SetServer(pysrv, wampconf=None, wampsecret=None): + global _PySrv, _WampConf, _WampSecret _PySrv = pysrv + _WampConf = wampconf + _WampSecret = wampsecret diff -r 81949104291d -r 46447d99e5f9 tests/wamp/.crossbar/config.json --- a/tests/wamp/.crossbar/config.json Wed Jul 04 14:10:03 2018 +0200 +++ b/tests/wamp/.crossbar/config.json Wed Jul 04 14:17:00 2018 +0200 @@ -39,13 +39,15 @@ "transports": [ { "type": "websocket", + "debug": true, "endpoint": { "type": "tcp", "port": 8888 }, "url": "ws://127.0.0.1:8888/", "serializers": [ - "msgpack" + "msgpack", + "json" ] } ] diff -r 81949104291d -r 46447d99e5f9 tests/wamp/README --- a/tests/wamp/README Wed Jul 04 14:10:03 2018 +0200 +++ b/tests/wamp/README Wed Jul 04 14:17:00 2018 +0200 @@ -1,25 +1,26 @@ -Crossbar test router configuration is available in .crossbar directory. - -Starting command: -crossbar start - -This project contains wamp client config to be loaded at runtime startup. - -project_files/wampconf.json +/* This project contains wamp client config to be loaded at runtime startup. */ +./project_files/wampconf.json wampconf.json is in "Project Files", so it is copied to runtime's working directory, and then loaded after program transfer + runtime restart. Otherwise, wamp config file path can be forced : ./Beremiz_service.py -c /path/to/my/wampconf.json /working/dir -Otherwise, path for CRA secret can be forced : -./Beremiz_service.py -s /path/to/my/secret /working/dir +/* Crossbar install */ +#sudo apt-get update +#sudo apt-get -y dist-upgrade +sudo apt-get -y install build-essential libssl-dev libffi-dev libreadline-dev libbz2-dev libsqlite3-dev libncurses5-dev +sudo python -m pip install -U pip +sudo pip install crossbar +crossbar version +/* Start Crossbar command: */ +crossbar start + +/* Crossbar test router configuration is available in .crossbar directory. */ Tested on version: - Crossbar.io : 17.12.1 (Crossbar.io COMMUNITY) - Autobahn : 17.10.1 (with JSON, MessagePack, CBOR, UBJSON) + Crossbar.io : 18.3.1 (Crossbar.io COMMUNITY) + Autobahn : 18.3.1 (with JSON, MessagePack, CBOR, UBJSON) Twisted : 17.9.0-EPollReactor LMDB : 0.93/lmdb-0.9.18 - Python : 2.7.12/CPython - - + Python : 2.7.12/CPython \ No newline at end of file diff -r 81949104291d -r 46447d99e5f9 tests/wamp/beremiz.xml --- a/tests/wamp/beremiz.xml Wed Jul 04 14:10:03 2018 +0200 +++ b/tests/wamp/beremiz.xml Wed Jul 04 14:17:00 2018 +0200 @@ -1,4 +1,4 @@ - + diff -r 81949104291d -r 46447d99e5f9 tests/wamp/project_files/wampconf.json --- a/tests/wamp/project_files/wampconf.json Wed Jul 04 14:10:03 2018 +0200 +++ b/tests/wamp/project_files/wampconf.json Wed Jul 04 14:17:00 2018 +0200 @@ -1,7 +1,12 @@ { - "url":"ws://127.0.0.1:8888", - "realm":"Automation", - "ID":"wamptest", - "password":"1234567890", - "key":"ABCDEFGHIJ" + "ID": "wamptest", + "active": true, + "key": "ABCDEFGHIJ", + "password": "1234567890", + "protocolOptions": { + "autoPingInterval": 60, + "autoPingTimeout": 20 + }, + "realm": "Automation", + "url": "ws://127.0.0.1:8888" }