new generic secop client
new client intended as base class for all clients - based on Ennos secop client in nicos ([WIP] provide secop client) - self healing connection singletons - extension for other than TCP is foreseen (by extending new uri schemes) - extensible name mangling - seperate rx and tx threads supporting events - internal cache - extensible error handling - callback for unhandled messages - callback for descriptive data change - callback for node stat change (connected, disconnected) - a short close down and reconnect without change in descriptive data does not disturb the client side works with secop-gui (change follows), planned to be used for Frappy internal secop proxy and as a replacement for secop.client.baseclient.Client in the nicos secop device. -> secop/client/baseclient.py to be removed after planned changes moved secop/client/__init__.py to secop/client/console.py because secop.client would be the natural place to put the new base class. Change-Id: I1a7b1f1ded2221a8f9fcdd52f9cc7414e8fbe035 Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/22218 Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de> Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
193
secop/client/console.py
Normal file
193
secop/client/console.py
Normal file
@ -0,0 +1,193 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""console client"""
|
||||
|
||||
# this needs to be reworked or removed
|
||||
|
||||
|
||||
import code
|
||||
import socket
|
||||
import threading
|
||||
from collections import deque
|
||||
from os import path
|
||||
import configparser
|
||||
import mlzlog
|
||||
|
||||
from secop.protocol.interface import decode_msg, encode_msg_frame, get_msg
|
||||
from secop.protocol.messages import EVENTREPLY
|
||||
|
||||
|
||||
|
||||
class NameSpace(dict):
|
||||
|
||||
def __init__(self):
|
||||
dict.__init__(self)
|
||||
self.__const = set()
|
||||
|
||||
def setconst(self, name, value):
|
||||
dict.__setitem__(self, name, value)
|
||||
self.__const.add(name)
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
if name in self.__const:
|
||||
raise RuntimeError('%s cannot be assigned' % name)
|
||||
dict.__setitem__(self, name, value)
|
||||
|
||||
def __delitem__(self, name):
|
||||
if name in self.__const:
|
||||
raise RuntimeError('%s cannot be deleted' % name)
|
||||
dict.__delitem__(self, name)
|
||||
|
||||
|
||||
|
||||
def getClientOpts(cfgfile):
|
||||
parser = configparser.SafeConfigParser()
|
||||
if not parser.read([cfgfile + '.cfg']):
|
||||
print("Error reading cfg file %r" % cfgfile)
|
||||
return {}
|
||||
if not parser.has_section('client'):
|
||||
print("No Server section found!")
|
||||
return dict(item for item in parser.items('client'))
|
||||
|
||||
|
||||
class ClientConsole:
|
||||
|
||||
def __init__(self, cfgname, basepath):
|
||||
self.namespace = NameSpace()
|
||||
self.namespace.setconst('help', self.helpCmd)
|
||||
|
||||
cfgfile = path.join(basepath, 'etc', cfgname)
|
||||
cfg = getClientOpts(cfgfile)
|
||||
self.client = Client(cfg)
|
||||
self.client.populateNamespace(self.namespace)
|
||||
|
||||
def run(self):
|
||||
console = code.InteractiveConsole(self.namespace)
|
||||
console.interact("Welcome to the SECoP console")
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def helpCmd(self, arg=Ellipsis):
|
||||
if arg is Ellipsis:
|
||||
print("No help available yet")
|
||||
else:
|
||||
help(arg)
|
||||
|
||||
|
||||
class TCPConnection:
|
||||
|
||||
def __init__(self, connect, port, **kwds):
|
||||
self.log = mlzlog.log.getChild('connection', False)
|
||||
port = int(port)
|
||||
self.connection = socket.create_connection((connect, port), 3)
|
||||
self.queue = deque()
|
||||
self._rcvdata = ''
|
||||
self.callbacks = set()
|
||||
self._thread = threading.Thread(target=self.thread)
|
||||
self._thread.daemonize = True
|
||||
self._thread.start()
|
||||
|
||||
def send(self, msg):
|
||||
self.log.debug("Sending msg %r" % msg)
|
||||
data = encode_msg_frame(*msg.serialize())
|
||||
self.log.debug("raw data: %r" % data)
|
||||
self.connection.sendall(data)
|
||||
|
||||
def thread(self):
|
||||
while True:
|
||||
try:
|
||||
self.thread_step()
|
||||
except Exception as e:
|
||||
self.log.exception("Exception in RCV thread: %r" % e)
|
||||
|
||||
def thread_step(self):
|
||||
data = b''
|
||||
while True:
|
||||
newdata = self.connection.recv(1024)
|
||||
self.log.debug("RCV: got raw data %r" % newdata)
|
||||
data = data + newdata
|
||||
while True:
|
||||
origin, data = get_msg(data)
|
||||
if origin is None:
|
||||
break # no more messages to process
|
||||
if not origin: # empty string
|
||||
continue # ???
|
||||
_ = decode_msg(origin)
|
||||
# construct msgObj from msg
|
||||
try:
|
||||
#msgObj = Message(*msg)
|
||||
#msgObj.origin = origin.decode('latin-1')
|
||||
#self.handle(msgObj)
|
||||
pass
|
||||
except Exception:
|
||||
# ??? what to do here?
|
||||
pass
|
||||
|
||||
def handle(self, msg):
|
||||
if msg.action == EVENTREPLY:
|
||||
self.log.info("got Async: %r" % msg)
|
||||
for cb in self.callbacks:
|
||||
try:
|
||||
cb(msg)
|
||||
except Exception as e:
|
||||
self.log.debug(
|
||||
"handle_async: got exception %r" % e, exception=True)
|
||||
else:
|
||||
self.queue.append(msg)
|
||||
|
||||
def read(self):
|
||||
while not self.queue:
|
||||
pass # XXX: remove BUSY polling
|
||||
return self.queue.popleft()
|
||||
|
||||
def register_callback(self, callback):
|
||||
"""registers callback for async data"""
|
||||
self.callbacks.add(callback)
|
||||
|
||||
def unregister_callback(self, callback):
|
||||
"""unregisters callback for async data"""
|
||||
self.callbacks.discard(callback)
|
||||
|
||||
|
||||
class Client:
|
||||
|
||||
def __init__(self, opts):
|
||||
self.log = mlzlog.log.getChild('client', True)
|
||||
self._cache = dict()
|
||||
self.connection = TCPConnection(**opts)
|
||||
self.connection.register_callback(self.handle_async)
|
||||
|
||||
def handle_async(self, msg):
|
||||
self.log.info("Got async update %r" % msg)
|
||||
module = msg.module
|
||||
param = msg.param
|
||||
value = msg.value
|
||||
self._cache.getdefault(module, {})[param] = value
|
||||
# XXX: further notification-callbacks needed ???
|
||||
|
||||
def populateNamespace(self, namespace):
|
||||
#self.connection.send(Message(DESCRIPTIONREQUEST))
|
||||
# reply = self.connection.read()
|
||||
# self.log.info("found modules %r" % reply)
|
||||
# create proxies, populate cache....
|
||||
namespace.setconst('connection', self.connection)
|
Reference in New Issue
Block a user