various fixes

- nodestatechange callback must appear after the online attribute
  is changed
- IntRange: move range validation outside of try except
- fixes on some error names
- well defined error message 'no such class' in secop.lib.get_class
- try again 4 times when starting Tcp Server on EADDRINUSE
- fix error handling

Change-Id: I4eee9b79ea8173936b9f5193b87e006ac8fca827
Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/26171
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
zolliker 2021-06-08 18:02:52 +02:00
parent 026e657799
commit 9a60de9c1c
9 changed files with 69 additions and 50 deletions

View File

@ -493,14 +493,12 @@ class SecopClient(ProxyClient):
self.log.warning('unhandled message: %s %s %r' % (action, ident, data)) self.log.warning('unhandled message: %s %s %r' % (action, ident, data))
def _set_state(self, online, state=None): def _set_state(self, online, state=None):
# treat reconnecting as online! # remark: reconnecting is treated as online
state = state or self.state
self.callback(None, 'nodeStateChange', online, state)
for mname in self.modules:
self.callback(mname, 'nodeStateChange', online, state)
# set online attribute after callbacks -> callback may check for old state
self.online = online self.online = online
self.state = state self.state = state or self.state
self.callback(None, 'nodeStateChange', self.online, self.state)
for mname in self.modules:
self.callback(mname, 'nodeStateChange', self.online, self.state)
def queue_request(self, action, ident=None, data=None): def queue_request(self, action, ident=None, data=None):
"""make a request""" """make a request"""

View File

@ -102,7 +102,7 @@ class DataType(HasProperties):
self.setProperty(k, v) self.setProperty(k, v)
self.checkProperties() self.checkProperties()
except Exception as e: except Exception as e:
raise ProgrammingError(str(e)) raise ProgrammingError(str(e)) from None
def get_info(self, **kwds): def get_info(self, **kwds):
"""prepare dict for export or repr """prepare dict for export or repr
@ -194,7 +194,7 @@ class FloatRange(DataType):
try: try:
value = float(value) value = float(value)
except Exception: except Exception:
raise BadValueError('Can not __call__ %r to float' % value) raise BadValueError('Can not __call__ %r to float' % value) from None
# map +/-infty to +/-max possible number # map +/-infty to +/-max possible number
value = clamp(-sys.float_info.max, value, sys.float_info.max) value = clamp(-sys.float_info.max, value, sys.float_info.max)
@ -267,12 +267,12 @@ class IntRange(DataType):
try: try:
fvalue = float(value) fvalue = float(value)
value = int(value) value = int(value)
if not self.min <= value <= self.max or round(fvalue) != fvalue:
raise BadValueError('%r should be an int between %d and %d' %
(value, self.min, self.max))
return value
except Exception: except Exception:
raise BadValueError('Can not convert %r to int' % value) raise BadValueError('Can not convert %r to int' % value) from None
if not self.min <= value <= self.max or round(fvalue) != fvalue:
raise BadValueError('%r should be an int between %d and %d' %
(value, self.min, self.max))
return value
def __repr__(self): def __repr__(self):
args = (self.min, self.max) args = (self.min, self.max)
@ -371,7 +371,7 @@ class ScaledInteger(DataType):
try: try:
value = float(value) value = float(value)
except Exception: except Exception:
raise BadValueError('Can not convert %r to float' % value) raise BadValueError('Can not convert %r to float' % value) from None
prec = max(self.scale, abs(value * self.relative_resolution), prec = max(self.scale, abs(value * self.relative_resolution),
self.absolute_resolution) self.absolute_resolution)
if self.min - prec <= value <= self.max + prec: if self.min - prec <= value <= self.max + prec:
@ -454,7 +454,7 @@ class EnumType(DataType):
try: try:
return self._enum[value] return self._enum[value]
except (KeyError, TypeError): # TypeError will be raised when value is not hashable except (KeyError, TypeError): # TypeError will be raised when value is not hashable
raise BadValueError('%r is not a member of enum %r' % (value, self._enum)) raise BadValueError('%r is not a member of enum %r' % (value, self._enum)) from None
def from_string(self, text): def from_string(self, text):
return self(text) return self(text)
@ -533,7 +533,7 @@ class BLOBType(DataType):
if self.minbytes < other.minbytes or self.maxbytes > other.maxbytes: if self.minbytes < other.minbytes or self.maxbytes > other.maxbytes:
raise BadValueError('incompatible datatypes') raise BadValueError('incompatible datatypes')
except AttributeError: except AttributeError:
raise BadValueError('incompatible datatypes') raise BadValueError('incompatible datatypes') from None
class StringType(DataType): class StringType(DataType):
@ -572,7 +572,7 @@ class StringType(DataType):
try: try:
value.encode('ascii') value.encode('ascii')
except UnicodeEncodeError: except UnicodeEncodeError:
raise BadValueError('%r contains non-ascii character!' % value) raise BadValueError('%r contains non-ascii character!' % value) from None
size = len(value) size = len(value)
if size < self.minchars: if size < self.minchars:
raise BadValueError( raise BadValueError(
@ -606,7 +606,7 @@ class StringType(DataType):
self.isUTF8 > other.isUTF8: self.isUTF8 > other.isUTF8:
raise BadValueError('incompatible datatypes') raise BadValueError('incompatible datatypes')
except AttributeError: except AttributeError:
raise BadValueError('incompatible datatypes') raise BadValueError('incompatible datatypes') from None
# TextType is a special StringType intended for longer texts (i.e. embedding \n), # TextType is a special StringType intended for longer texts (i.e. embedding \n),
@ -618,7 +618,7 @@ class TextType(StringType):
def __init__(self, maxchars=None): def __init__(self, maxchars=None):
if maxchars is None: if maxchars is None:
maxchars = UNLIMITED maxchars = UNLIMITED
super(TextType, self).__init__(0, maxchars) super().__init__(0, maxchars)
def __repr__(self): def __repr__(self):
if self.maxchars == UNLIMITED: if self.maxchars == UNLIMITED:
@ -771,7 +771,7 @@ class ArrayOf(DataType):
raise BadValueError('incompatible datatypes') raise BadValueError('incompatible datatypes')
self.members.compatible(other.members) self.members.compatible(other.members)
except AttributeError: except AttributeError:
raise BadValueError('incompatible datatypes') raise BadValueError('incompatible datatypes') from None
class TupleOf(DataType): class TupleOf(DataType):
@ -811,7 +811,7 @@ class TupleOf(DataType):
return tuple(sub(elem) return tuple(sub(elem)
for sub, elem in zip(self.members, value)) for sub, elem in zip(self.members, value))
except Exception as exc: except Exception as exc:
raise BadValueError('Can not validate:', str(exc)) raise BadValueError('Can not validate:', str(exc)) from None
def export_value(self, value): def export_value(self, value):
"""returns a python object fit for serialisation""" """returns a python object fit for serialisation"""
@ -894,7 +894,7 @@ class StructOf(DataType):
return ImmutableDict((str(k), self.members[k](v)) return ImmutableDict((str(k), self.members[k](v))
for k, v in list(value.items())) for k, v in list(value.items()))
except Exception as exc: except Exception as exc:
raise BadValueError('Can not validate %s: %s' % (repr(value), str(exc))) raise BadValueError('Can not validate %s: %s' % (repr(value), str(exc))) from None
def export_value(self, value): def export_value(self, value):
"""returns a python object fit for serialisation""" """returns a python object fit for serialisation"""
@ -924,7 +924,7 @@ class StructOf(DataType):
if mandatory: if mandatory:
raise BadValueError('incompatible datatypes') raise BadValueError('incompatible datatypes')
except (AttributeError, TypeError, KeyError): except (AttributeError, TypeError, KeyError):
raise BadValueError('incompatible datatypes') raise BadValueError('incompatible datatypes') from None
class CommandType(DataType): class CommandType(DataType):
@ -987,7 +987,7 @@ class CommandType(DataType):
if self.result != other.result: # not both are None if self.result != other.result: # not both are None
other.result.compatible(self.result) other.result.compatible(self.result)
except AttributeError: except AttributeError:
raise BadValueError('incompatible datatypes') raise BadValueError('incompatible datatypes') from None
# internally used datatypes (i.e. only for programming the SEC-node) # internally used datatypes (i.e. only for programming the SEC-node)
@ -1164,9 +1164,9 @@ def get_datatype(json, pname=''):
kwargs = json.copy() kwargs = json.copy()
base = kwargs.pop('type') base = kwargs.pop('type')
except (TypeError, KeyError, AttributeError): except (TypeError, KeyError, AttributeError):
raise BadValueError('a data descriptor must be a dict containing a "type" key, not %r' % json) raise BadValueError('a data descriptor must be a dict containing a "type" key, not %r' % json) from None
try: try:
return DATATYPES[base](pname=pname, **kwargs) return DATATYPES[base](pname=pname, **kwargs)
except Exception as e: except Exception as e:
raise BadValueError('invalid data descriptor: %r (%s)' % (json, str(e))) raise BadValueError('invalid data descriptor: %r (%s)' % (json, str(e))) from None

View File

@ -143,7 +143,7 @@ EXCEPTIONS = dict(
NoSuchCommand=NoSuchCommandError, NoSuchCommand=NoSuchCommandError,
CommandFailed=CommandFailedError, CommandFailed=CommandFailedError,
CommandRunning=CommandRunningError, CommandRunning=CommandRunningError,
Readonly=ReadOnlyError, ReadOnly=ReadOnlyError,
BadValue=BadValueError, BadValue=BadValueError,
CommunicationFailed=CommunicationFailedError, CommunicationFailed=CommunicationFailedError,
HardwareError=HardwareError, HardwareError=HardwareError,
@ -151,7 +151,8 @@ EXCEPTIONS = dict(
IsError=IsErrorError, IsError=IsErrorError,
Disabled=DisabledError, Disabled=DisabledError,
SyntaxError=ProtocolError, SyntaxError=ProtocolError,
NotImplementedError=NotImplementedError, NotImplemented=NotImplementedError,
ProtocolError=ProtocolError,
InternalError=InternalError, InternalError=InternalError,
# internal short versions (candidates for spec) # internal short versions (candidates for spec)
Protocol=ProtocolError, Protocol=ProtocolError,

View File

@ -105,7 +105,10 @@ def get_class(spec):
else: else:
# rarely needed by now.... # rarely needed by now....
module = importlib.import_module('secop.' + modname) module = importlib.import_module('secop.' + modname)
return getattr(module, classname) try:
return getattr(module, classname)
except AttributeError:
raise AttributeError('no such class') from None
def mkthread(func, *args, **kwds): def mkthread(func, *args, **kwds):

View File

@ -99,10 +99,13 @@ class HasAccessibles(HasProperties):
rfunc_handler = pobj.handler.get_read_func(cls, pname) if pobj.handler else None rfunc_handler = pobj.handler.get_read_func(cls, pname) if pobj.handler else None
wrapped = hasattr(rfunc, '__wrapped__') wrapped = hasattr(rfunc, '__wrapped__')
if rfunc_handler: if rfunc_handler:
if rfunc and not wrapped: if 'read_' + pname in cls.__dict__:
raise ProgrammingError("parameter '%s' can not have a handler " if pname in cls.__dict__:
"and read_%s" % (pname, pname)) raise ProgrammingError("parameter '%s' can not have a handler "
rfunc = rfunc_handler "and read_%s" % (pname, pname))
# read_<pname> overwrites inherited handler
else:
rfunc = rfunc_handler
wrapped = False wrapped = False
# create wrapper except when read function is already wrapped # create wrapper except when read function is already wrapped
@ -388,7 +391,7 @@ class Module(HasAccessibles):
if k in self.parameters or k in self.propertyDict: if k in self.parameters or k in self.propertyDict:
setattr(self, k, v) setattr(self, k, v)
cfgdict.pop(k) cfgdict.pop(k)
except (ValueError, TypeError): except (ValueError, TypeError) as e:
# self.log.exception(formatExtendedStack()) # self.log.exception(formatExtendedStack())
errors.append('module %s, parameter %s: %s' % (self.name, k, e)) errors.append('module %s, parameter %s: %s' % (self.name, k, e))

View File

@ -39,10 +39,6 @@ class Accessible(HasProperties):
kwds = None # is a dict if it might be used as Override kwds = None # is a dict if it might be used as Override
def __init__(self, **kwds):
super().__init__()
self.init(kwds)
def init(self, kwds): def init(self, kwds):
# do not use self.propertyValues.update here, as no invalid values should be # do not use self.propertyValues.update here, as no invalid values should be
# assigned to properties, even not before checkProperties # assigned to properties, even not before checkProperties
@ -101,10 +97,10 @@ class Parameter(Accessible):
description = Property( description = Property(
'mandatory description of the parameter', TextType(), 'mandatory description of the parameter', TextType(),
extname='description', mandatory=True) extname='description', mandatory=True, export='always')
datatype = Property( datatype = Property(
'datatype of the Parameter (SECoP datainfo)', DataTypeType(), 'datatype of the Parameter (SECoP datainfo)', DataTypeType(),
extname='datainfo', mandatory=True) extname='datainfo', mandatory=True, export='always')
readonly = Property( readonly = Property(
'not changeable via SECoP (default True)', BoolType(), 'not changeable via SECoP (default True)', BoolType(),
extname='readonly', default=True, export='always') extname='readonly', default=True, export='always')
@ -165,7 +161,7 @@ class Parameter(Accessible):
readerror = None readerror = None
def __init__(self, description=None, datatype=None, inherit=True, *, unit=None, constant=None, **kwds): def __init__(self, description=None, datatype=None, inherit=True, *, unit=None, constant=None, **kwds):
super().__init__(**kwds) super().__init__()
if datatype is not None: if datatype is not None:
if not isinstance(datatype, DataType): if not isinstance(datatype, DataType):
if isinstance(datatype, type) and issubclass(datatype, DataType): if isinstance(datatype, type) and issubclass(datatype, DataType):
@ -178,6 +174,8 @@ class Parameter(Accessible):
if 'default' in kwds: if 'default' in kwds:
self.default = datatype(kwds['default']) self.default = datatype(kwds['default'])
self.init(kwds) # datatype must be defined before we can treat dataset properties like fmtstr or unit
if description is not None: if description is not None:
self.description = inspect.cleandoc(description) self.description = inspect.cleandoc(description)
@ -187,6 +185,7 @@ class Parameter(Accessible):
self._constant = constant self._constant = constant
def __get__(self, instance, owner): def __get__(self, instance, owner):
# not used yet
if instance is None: if instance is None:
return self return self
return instance.parameters[self.name].value return instance.parameters[self.name].value
@ -282,7 +281,7 @@ class Command(Accessible):
description = Property( description = Property(
'description of the Command', TextType(), 'description of the Command', TextType(),
extname='description', export=True, mandatory=True) extname='description', export='always', mandatory=True)
group = Property( group = Property(
'optional command group of the command.', StringType(), 'optional command group of the command.', StringType(),
extname='group', export=True, default='') extname='group', export=True, default='')
@ -312,7 +311,8 @@ class Command(Accessible):
func = None func = None
def __init__(self, argument=False, *, result=None, inherit=True, **kwds): def __init__(self, argument=False, *, result=None, inherit=True, **kwds):
super().__init__(**kwds) super().__init__()
self.init(kwds)
if result or kwds or isinstance(argument, DataType) or not callable(argument): if result or kwds or isinstance(argument, DataType) or not callable(argument):
# normal case # normal case
if argument is False and result: if argument is False and result:

View File

@ -202,7 +202,7 @@ class Poller(PollerBase):
module.pollOneParam(pname) module.pollOneParam(pname)
done = True done = True
lastdue = due lastdue = due
due = max(lastdue + mininterval, now + min(self.maxwait, mininterval * 0.5)) due = max(lastdue + mininterval, now + min(self.maxwait, mininterval * 0.5))
# replace due, lastdue with new values and sort in # replace due, lastdue with new values and sort in
heapreplace(queue, (due, lastdue, pollitem)) heapreplace(queue, (due, lastdue, pollitem))
return 0 return 0
@ -227,9 +227,10 @@ class Poller(PollerBase):
(where n is the number of regular parameters). (where n is the number of regular parameters).
""" """
if not self: if not self:
# nothing to do (else we might call time.sleep(float('inf')) below # nothing to do (else time.sleep(float('inf')) might be called below
started_callback() started_callback()
return return
# if writeInitParams is not yet done, we do it here
for module in self.modules: for module in self.modules:
module.writeInitParams() module.writeInitParams()
# do all polls once and, at the same time, insert due info # do all polls once and, at the same time, insert due info

View File

@ -25,6 +25,8 @@ import socket
import socketserver import socketserver
import sys import sys
import threading import threading
import time
import errno
from secop.datatypes import BoolType, StringType from secop.datatypes import BoolType, StringType
from secop.errors import SECoPError from secop.errors import SECoPError
@ -184,8 +186,19 @@ class TCPServer(socketserver.ThreadingTCPServer):
port = int(options.pop('uri').split('://', 1)[-1]) port = int(options.pop('uri').split('://', 1)[-1])
self.detailed_errors = options.pop('detailed_errors', False) self.detailed_errors = options.pop('detailed_errors', False)
self.allow_reuse_address = True
self.log.info("TCPServer %s binding to port %d" % (name, port)) self.log.info("TCPServer %s binding to port %d" % (name, port))
socketserver.ThreadingTCPServer.__init__( for ntry in range(5):
self, ('0.0.0.0', port), TCPRequestHandler, bind_and_activate=True) try:
socketserver.ThreadingTCPServer.__init__(
self, ('0.0.0.0', port), TCPRequestHandler, bind_and_activate=True)
break
except OSError as e:
if e.args[0] == errno.EADDRINUSE: # address already in use
# this may happen despite of allow_reuse_address
time.sleep(0.3 * (1 << ntry)) # max accumulated sleep time: 0.3 * 31 = 9.3 sec
else:
self.log.error('could not initialize TCP Server: %r' % e)
raise
if ntry:
self.log.warning('tried again %d times after "Address already in use"' % ntry)
self.log.info("TCPServer initiated") self.log.info("TCPServer initiated")

View File

@ -256,7 +256,7 @@ class Server:
self.modules[modname] = modobj self.modules[modname] = modobj
except ConfigError as e: except ConfigError as e:
errors.append('error creating module %s:' % modname) errors.append('error creating module %s:' % modname)
for errtxt in e.args[0]: for errtxt in e.args[0] if isinstance(e.args[0], list) else [e.args[0]]:
errors.append(' ' + errtxt) errors.append(' ' + errtxt)
except Exception: except Exception:
failure_traceback = traceback.format_exc() failure_traceback = traceback.format_exc()