Edouard@1439: #!/usr/bin/env python Edouard@1439: # -*- coding: utf-8 -*- Edouard@1439: andrej@1667: # This file is part of Beremiz runtime. andrej@1511: # andrej@1511: # Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD andrej@1511: # andrej@1667: # See COPYING.Runtime file for copyrights details. andrej@1511: # andrej@1667: # This library is free software; you can redistribute it and/or andrej@1667: # modify it under the terms of the GNU Lesser General Public andrej@1667: # License as published by the Free Software Foundation; either andrej@1667: # version 2.1 of the License, or (at your option) any later version. andrej@1667: andrej@1667: # This library is distributed in the hope that it will be useful, andrej@1511: # but WITHOUT ANY WARRANTY; without even the implied warranty of andrej@1667: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU andrej@1667: # Lesser General Public License for more details. andrej@1667: andrej@1667: # You should have received a copy of the GNU Lesser General Public andrej@1667: # License along with this library; if not, write to the Free Software andrej@1667: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA andrej@1511: andrej@1826: andrej@1881: from __future__ import absolute_import andrej@1826: from __future__ import print_function Edouard@1899: import time Edouard@1893: import json denis@2203: import os denis@2195: import inspect denis@2201: import re Edouard@1439: from autobahn.twisted import wamp Edouard@1439: from autobahn.twisted.websocket import WampWebSocketClientFactory, connectWS Edouard@1893: from autobahn.wamp import types, auth Edouard@1440: from autobahn.wamp.serializer import MsgPackSerializer andrej@1834: from twisted.internet.defer import inlineCallbacks Edouard@1441: from twisted.internet.protocol import ReconnectingClientFactory andrej@1832: Edouard@1439: denis@2201: _transportFactory = None Edouard@1439: _WampSession = None Edouard@1439: _PySrv = None denis@2202: _WampConf = None denis@2202: _WampSecret = None Edouard@1439: andrej@1878: ExposedCalls = [ andrej@1878: "StartPLC", andrej@1878: "StopPLC", andrej@1878: "ForceReload", andrej@1878: "GetPLCstatus", andrej@1878: "NewPLC", andrej@1878: "MatchMD5", andrej@1878: "SetTraceVariablesList", andrej@1878: "GetTraceVariables", andrej@1878: "RemoteExec", andrej@1878: "GetLogMessage", andrej@1878: "ResetLogCount", andrej@1878: ] Edouard@1440: denis@2198: ExposedProgressCalls = [] denis@2198: Edouard@1898: # Those two lists are meant to be filled by customized runtime Edouard@1901: # or User python code. Edouard@1898: Edouard@1901: """ crossbar Events to register to """ Edouard@1446: SubscribedEvents = [] Edouard@1446: Edouard@1901: """ things to do on join (callables) """ Edouard@1446: DoOnJoin = [] Edouard@1446: andrej@1736: Edouard@1445: def GetCallee(name): Edouard@1446: """ Get Callee or Subscriber corresponding to '.' spearated object path """ Edouard@1445: names = name.split('.') Edouard@1445: obj = _PySrv.plcobj andrej@1756: while names: andrej@1756: obj = getattr(obj, names.pop(0)) Edouard@1445: return obj Edouard@1440: denis@2196: def getValidOptins(options, arguments): denis@2196: validOptions = {} denis@2196: for key in options: denis@2196: if key in arguments: denis@2196: validOptions[key] = options[key] denis@2196: if len(validOptions) > 0: denis@2196: return validOptions denis@2196: else: denis@2196: return None andrej@1736: Edouard@1439: class WampSession(wamp.ApplicationSession): Edouard@1893: def onConnect(self): Edouard@1901: if "secret" in self.config.extra: denis@2194: user = self.config.extra["ID"] Edouard@1893: self.join(u"Automation", [u"wampcra"], user) Edouard@1893: else: Edouard@1893: self.join(u"Automation") Edouard@1893: Edouard@1893: def onChallenge(self, challenge): Edouard@1893: if challenge.method == u"wampcra": Edouard@1893: secret = self.config.extra["secret"].encode('utf8') Edouard@1893: signature = auth.compute_wcs(secret, challenge.extra['challenge'].encode('utf8')) Edouard@1893: return signature.decode("ascii") Edouard@1893: else: Edouard@1893: raise Exception("don't know how to handle authmethod {}".format(challenge.method)) Edouard@1440: Edouard@1440: @inlineCallbacks Edouard@1439: def onJoin(self, details): Edouard@1439: global _WampSession Edouard@1439: _WampSession = self Edouard@1901: ID = self.config.extra["ID"] denis@2199: validRegisterOptions = {} denis@2196: denis@2196: registerOptions = self.config.extra.get('registerOptions', None) denis@2199: if registerOptions: denis@2199: arguments = inspect.getargspec(types.RegisterOptions.__init__).args denis@2199: validRegisterOptions = getValidOptins(registerOptions, arguments) denis@2199: if validRegisterOptions: denis@2199: registerOptions = types.RegisterOptions(**validRegisterOptions) denis@2199: #print(_("Added custom register options")) denis@2196: Edouard@1440: for name in ExposedCalls: denis@2197: yield self.register(GetCallee(name), u'.'.join((ID, name)), registerOptions) Edouard@1439: denis@2198: if ExposedProgressCalls: denis@2198: validRegisterOptions["details_arg"] = 'details' denis@2198: registerOptions = types.RegisterOptions(**validRegisterOptions) denis@2198: # using progress, details argument must be added denis@2198: for name in ExposedProgressCalls: denis@2198: yield self.register(GetCallee(name), u'.'.join((ID, name)), registerOptions) denis@2198: Edouard@1446: for name in SubscribedEvents: Edouard@1898: yield self.subscribe(GetCallee(name), unicode(name)) Edouard@1446: Edouard@1446: for func in DoOnJoin: Edouard@1446: yield func(self) Edouard@1446: denis@2194: print(_('WAMP session joined (%s) by:' % time.ctime()), ID) denis@2194: Edouard@1439: def onLeave(self, details): denis@2201: global _WampSession, _transportFactory denis@2201: super(WampSession, self).onLeave(details) Edouard@1439: _WampSession = None denis@2201: _transportFactory = None Edouard@1890: print(_('WAMP session left')) Edouard@1439: andrej@1736: Edouard@1441: class ReconnectingWampWebSocketClientFactory(WampWebSocketClientFactory, ReconnectingClientFactory): denis@2195: def __init__(self, config, *args, **kwargs): denis@2201: global _transportFactory denis@2195: WampWebSocketClientFactory.__init__(self, *args, **kwargs) denis@2195: denis@2196: protocolOptions = config.extra.get('protocolOptions', None) denis@2199: if protocolOptions: denis@2199: arguments = inspect.getargspec(self.setProtocolOptions).args denis@2199: validProtocolOptions = getValidOptins(protocolOptions, arguments) denis@2199: if validProtocolOptions: denis@2199: self.setProtocolOptions(**validProtocolOptions) denis@2199: #print(_("Added custom protocol options")) denis@2201: _transportFactory = self denis@2195: denis@2193: def buildProtocol(self, addr): denis@2193: self.resetDelay() denis@2193: return ReconnectingClientFactory.buildProtocol(self, addr) denis@2193: Edouard@1441: def clientConnectionFailed(self, connector, reason): denis@2201: if self.continueTrying: denis@2201: print(_("WAMP Client connection failed (%s) .. retrying .." % time.ctime())) denis@2201: super(ReconnectingWampWebSocketClientFactory, self).clientConnectionFailed(connector, reason) denis@2201: else: denis@2201: del connector andrej@1751: Edouard@1441: def clientConnectionLost(self, connector, reason): denis@2201: if self.continueTrying: denis@2201: print(_("WAMP Client connection lost (%s) .. retrying .." % time.ctime())) denis@2201: super(ReconnectingWampWebSocketClientFactory, self).clientConnectionFailed(connector, reason) denis@2201: else: denis@2201: del connector Edouard@1441: andrej@1736: denis@2204: def LoadWampClientConf(items=None): Edouard@1893: try: denis@2204: WSClientConf = json.load(open(_WampConf)) denis@2204: if items and isinstance(items, list): denis@2204: WSClientConfItems = {} denis@2204: for item in items: denis@2204: wampconf_value = WSClientConf.get(item, None) denis@2204: if wampconf_value is not None: denis@2204: WSClientConfItems[item] = wampconf_value denis@2204: if WSClientConfItems: denis@2204: return WSClientConfItems Edouard@1893: return WSClientConf Edouard@1893: except ValueError, ve: Edouard@1893: print(_("WAMP load error: "), ve) Edouard@1893: return None denis@2204: except Exception, e: denis@2204: print(_("WAMP load error: "), e) denis@2204: return None denis@2204: denis@2204: def SaveWampClientConf(items): denis@2202: try: denis@2204: WSClientConf = LoadWampClientConf() denis@2204: saveChanges = False denis@2204: if items: denis@2204: for itemKey in items.keys(): denis@2204: wampconf_value = WSClientConf.get(itemKey, None) denis@2204: if (wampconf_value is not None) and (items[itemKey] is not None) and (wampconf_value != items[itemKey]): denis@2204: WSClientConf[itemKey] = items[itemKey] denis@2204: saveChanges = True denis@2204: denis@2204: if saveChanges: denis@2204: with open(os.path.realpath(_WampConf), 'w') as f: denis@2204: json.dump(WSClientConf, f, sort_keys=True, indent=4) denis@2204: if 'active' in WSClientConf and WSClientConf['active']: denis@2204: StartReconnectWampClient() denis@2204: else: denis@2204: StopReconnectWampClient() denis@2202: denis@2202: return WSClientConf denis@2202: except ValueError, ve: denis@2203: print(_("WAMP save error: "), ve) denis@2203: return None denis@2203: except Exception, e: denis@2203: print(_("WAMP save error: "), e) denis@2202: return None Edouard@1901: Edouard@1893: def LoadWampSecret(secretfname): Edouard@1893: try: Edouard@1893: WSClientWampSecret = open(secretfname, 'rb').read() Edouard@1893: return WSClientWampSecret Edouard@1893: except ValueError, ve: Edouard@1893: print(_("Wamp secret load error:"), ve) Edouard@1893: return None Edouard@1893: except Exception: Edouard@1893: return None Edouard@1446: andrej@1736: denis@2201: def IsCorrectUri(uri): denis@2201: if re.match(r'w{1}s{1,2}:{1}/{2}.+:{1}[0-9]+/{1}.+', uri): denis@2201: return True denis@2201: else: denis@2201: return False denis@2201: denis@2201: denis@2204: def RegisterWampClient(wampconf=None, secretfname=None): denis@2204: global _WampConf denis@2202: if wampconf: denis@2204: _WampConf = wampconf denis@2204: WSClientConf = LoadWampClientConf() denis@2204: else: denis@2204: WSClientConf = LoadWampClientConf() Edouard@1439: Edouard@1893: if not WSClientConf: Edouard@1894: print(_("WAMP client connection not established!")) denis@2201: return False denis@2201: denis@2201: if not IsCorrectUri(WSClientConf["url"]): denis@2201: print(_("WAMP url {} is not correct!".format(WSClientConf["url"]))) denis@2201: return False Edouard@1893: denis@2202: if secretfname: denis@2202: WampSecret = LoadWampSecret(secretfname) denis@2202: else: denis@2202: WampSecret = LoadWampSecret(_WampSecret) Edouard@1439: Edouard@1900: if WampSecret is not None: Edouard@1900: WSClientConf["secret"] = WampSecret Edouard@1900: Edouard@1440: # create a WAMP application session factory Edouard@1439: component_config = types.ComponentConfig( andrej@1744: realm=WSClientConf["realm"], Edouard@1901: extra=WSClientConf) Edouard@1439: session_factory = wamp.ApplicationSessionFactory( andrej@1744: config=component_config) Edouard@1439: session_factory.session = WampSession Edouard@1439: Edouard@1440: # create a WAMP-over-WebSocket transport client factory Edouard@1441: transport_factory = ReconnectingWampWebSocketClientFactory( denis@2195: component_config, Edouard@1439: session_factory, andrej@1744: url=WSClientConf["url"], Edouard@1893: serializers=[MsgPackSerializer()]) Edouard@1439: Edouard@1440: # start the client from a Twisted endpoint Edouard@1439: conn = connectWS(transport_factory) Edouard@1894: print(_("WAMP client connecting to :"), WSClientConf["url"]) denis@2204: return True denis@2204: denis@2201: denis@2203: def StopReconnectWampClient(): denis@2203: _transportFactory.stopTrying() denis@2203: return _WampSession.leave() denis@2203: denis@2204: denis@2203: def StartReconnectWampClient(): denis@2203: if _WampSession: denis@2203: # do reconnect denis@2202: _WampSession.disconnect() denis@2202: return True denis@2204: else: denis@2203: # do connect denis@2202: RegisterWampClient() denis@2202: return True denis@2203: andrej@1736: Edouard@1439: def GetSession(): Edouard@1439: return _WampSession Edouard@1439: denis@2204: denis@2202: def StatusWampClient(): denis@2202: return _WampSession and _WampSession.is_attached() denis@2202: denis@2204: denis@2204: def SetServer(pysrv, wampconf=None, wampsecret=None): denis@2202: global _PySrv, _WampConf, _WampSecret Edouard@1439: _PySrv = pysrv denis@2202: _WampConf = wampconf denis@2202: _WampSecret = wampsecret