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>
141 lines
4.1 KiB
Python
141 lines
4.1 KiB
Python
# -*- 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>
|
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
|
#
|
|
# *****************************************************************************
|
|
|
|
"""asynchonous connections
|
|
|
|
generic class for byte oriented communication
|
|
includes implementation for TCP connections
|
|
"""
|
|
|
|
import socket
|
|
import time
|
|
|
|
from secop.lib import parseHostPort, tcpSocket, closeSocket
|
|
|
|
|
|
class ConnectionClosed(ConnectionError):
|
|
pass
|
|
|
|
|
|
class AsynConn:
|
|
timeout = 1 # inter byte timeout
|
|
SCHEME_MAP = {}
|
|
connection = None # is not None, if connected
|
|
defaultport = None
|
|
|
|
def __new__(cls, uri):
|
|
scheme = uri.split('://')[0]
|
|
iocls = cls.SCHEME_MAP.get(scheme, None)
|
|
if not iocls:
|
|
# try tcp, if scheme not given
|
|
try:
|
|
host_port = parseHostPort(uri, cls.defaultport)
|
|
except (ValueError, TypeError, AssertionError):
|
|
raise ValueError('invalid uri: %s' % uri)
|
|
iocls = cls.SCHEME_MAP['tcp']
|
|
uri = 'tcp://%s:%d' % host_port
|
|
return object.__new__(iocls)
|
|
|
|
def __init__(self, *args):
|
|
self._rxbuffer = b''
|
|
|
|
def __del__(self):
|
|
self.disconnect()
|
|
|
|
@classmethod
|
|
def register_scheme(cls, scheme):
|
|
cls.SCHEME_MAP[scheme] = cls
|
|
|
|
def disconnect(self):
|
|
raise NotImplementedError
|
|
|
|
def send(self, data):
|
|
"""send data (bytes!)
|
|
|
|
tries to send all data"""
|
|
raise NotImplementedError
|
|
|
|
def recv(self):
|
|
"""return bytes received within timeout
|
|
|
|
in contrast to socket.recv:
|
|
- returns b'' on timeout
|
|
- raises ConnectionClosed if the other end has disconnected
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def readline(self, timeout=None):
|
|
"""read one line
|
|
|
|
return either a complete line or None in case of timeout
|
|
the timeout argument may increase, but not decrease the default timeout
|
|
"""
|
|
if timeout:
|
|
end = time.time() + timeout
|
|
while b'\n' not in self._rxbuffer:
|
|
data = self.recv()
|
|
if not data:
|
|
if timeout:
|
|
if time.time() < end:
|
|
continue
|
|
raise TimeoutError('timeout in readline')
|
|
return None
|
|
self._rxbuffer += data
|
|
line, self._rxbuffer = self._rxbuffer.split(b'\n', 1)
|
|
return line
|
|
|
|
def writeline(self, line):
|
|
self.send(line + b'\n')
|
|
|
|
|
|
class AsynTcp(AsynConn):
|
|
def __init__(self, uri):
|
|
super().__init__()
|
|
self.uri = uri
|
|
if uri.startswith('tcp://'):
|
|
# should be the case always
|
|
uri = uri[6:]
|
|
self.connection = tcpSocket(uri, self.defaultport, self.timeout)
|
|
|
|
def disconnect(self):
|
|
if self.connection:
|
|
closeSocket(self.connection)
|
|
self.connection = None
|
|
|
|
def send(self, data):
|
|
"""send data (bytes!)"""
|
|
self.connection.sendall(data)
|
|
|
|
def recv(self):
|
|
"""return bytes received within 1 sec"""
|
|
try:
|
|
data = self.connection.recv(8192)
|
|
if data:
|
|
return data
|
|
except socket.timeout:
|
|
# timeout while waiting
|
|
return b''
|
|
raise ConnectionClosed() # marks end of connection
|
|
|
|
AsynTcp.register_scheme('tcp')
|