Merged #2486, enhancements to WAMP client : auto reconnecting, wamp conf checking and saving, additional attributes for exposed calls, etc nevow_service_rework
authorEdouard Tisserant
Wed, 04 Jul 2018 14:17:00 +0200
branchnevow_service_rework
changeset 2211 46447d99e5f9
parent 2210 81949104291d (current diff)
parent 2207 c27b820cb96b (diff)
child 2212 cf1718962567
Merged #2486, enhancements to WAMP client : auto reconnecting, wamp conf checking and saving, additional attributes for exposed calls, etc
Beremiz_service.py
tests/wamp/.crossbar/config.json
tests/wamp/README
tests/wamp/py_ext_0@py_ext/baseconfnode.xml
tests/wamp/py_ext_0@py_ext/pyfile.xml
--- 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:
--- 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
--- 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"
                     ]
                 }
             ]
--- 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
--- 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 @@
 <?xml version='1.0' encoding='utf-8'?>
-<BeremizRoot xmlns:xsd="http://www.w3.org/2001/XMLSchema" URI_location="WAMP://127.0.0.1:8888#Automation#wamptest">
+<BeremizRoot xmlns:xsd="http://www.w3.org/2001/XMLSchema" URI_location="WAMP://127.0.0.1:8888#Automation#WampID">
   <TargetType/>
 </BeremizRoot>
--- 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"
 }