make sure unexported modules are initialized

take the opportunity for a small redesign:

- create a new method build_descriptive_data which
  calls secnode.get_modules also on unexported modules.

+ cache descriptive data

Change-Id: I4a0b8ac96108463dc0c800bb11a404206c26b092
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/36089
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
This commit is contained in:
zolliker 2025-04-22 12:48:23 +02:00
parent 1fead8b2c6
commit 9545cb4188
3 changed files with 46 additions and 48 deletions

View File

@ -265,9 +265,9 @@ class Dispatcher:
modulename, exportedname = specifier, None
if ':' in specifier:
modulename, exportedname = specifier.split(':', 1)
if modulename not in self.secnode.export:
raise NoSuchModuleError(f'Module {modulename!r} does not exist')
moduleobj = self.secnode.get_module(modulename)
if moduleobj is None or not moduleobj.export:
raise NoSuchModuleError(f'Module {modulename!r} does not exist')
if exportedname is not None:
pname = moduleobj.accessiblename2attr.get(exportedname, True)
if pname and pname not in moduleobj.accessibles:
@ -281,7 +281,7 @@ class Dispatcher:
else:
# activate all modules
self._active_connections.add(conn)
modules = [(m, None) for m in self.secnode.export]
modules = [(m, None) for m in self.secnode.get_exported_modules()]
# send updates for all subscribed values.
# note: The initial poll already happend before the server is active

View File

@ -44,8 +44,6 @@ class SecNode:
self.nodeprops = {}
# map ALL modulename -> moduleobj
self.modules = {}
# list of EXPORTED modules
self.export = []
self.log = logger
self.srv = srv
# set of modules that failed creation
@ -193,60 +191,62 @@ class SecNode:
modname, len(pinata_modules))
todos.extend(pinata_modules)
def export_accessibles(self, modulename):
self.log.debug('export_accessibles(%r)', modulename)
if modulename in self.export:
# omit export=False params!
res = OrderedDict()
for aobj in self.get_module(modulename).accessibles.values():
if aobj.export:
res[aobj.export] = aobj.for_export()
self.log.debug('list accessibles for module %s -> %r',
modulename, res)
return res
self.log.debug('-> module is not to be exported!')
return OrderedDict()
def export_accessibles(self, modobj):
self.log.debug('export_accessibles(%r)', modobj.name)
# omit export=False params!
res = OrderedDict()
for aobj in modobj.accessibles.values():
if aobj.export:
res[aobj.export] = aobj.for_export()
self.log.debug('list accessibles for module %s -> %r',
modobj.name, res)
return res
def build_descriptive_data(self):
modules = {}
result = {'modules': modules}
for modulename in self.modules:
modobj = self.get_module(modulename)
if not modobj.export:
continue
# some of these need rework !
mod_desc = {'accessibles': self.export_accessibles(modobj)}
mod_desc.update(modobj.exportProperties())
mod_desc.pop('export', None)
modules[modulename] = mod_desc
result['equipment_id'] = self.equipment_id
result['firmware'] = 'FRAPPY ' + get_version()
result['description'] = self.nodeprops['description']
for prop, propvalue in self.nodeprops.items():
if prop.startswith('_'):
result[prop] = propvalue
self.descriptive_data = result
def get_descriptive_data(self, specifier):
"""returns a python object which upon serialisation results in the
descriptive data"""
specifier = specifier or ''
modules = {}
result = {'modules': modules}
for modulename in self.export:
module = self.get_module(modulename)
if not module.export:
continue
# some of these need rework !
mod_desc = {'accessibles': self.export_accessibles(modulename)}
mod_desc.update(module.exportProperties())
mod_desc.pop('export', False)
modules[modulename] = mod_desc
modname, _, pname = specifier.partition(':')
modules = self.descriptive_data['modules']
if modname in modules: # extension to SECoP standard: description of a single module
result = modules[modname]
if pname in result['accessibles']: # extension to SECoP standard: description of a single accessible
# command is also accepted
result = result['accessibles'][pname]
elif pname:
return result['accessibles'][pname]
if pname:
raise NoSuchParameterError(f'Module {modname!r} '
f'has no parameter {pname!r}')
elif not modname or modname == '.':
result['equipment_id'] = self.equipment_id
result['firmware'] = 'FRAPPY ' + get_version()
result['description'] = self.nodeprops['description']
for prop, propvalue in self.nodeprops.items():
if prop.startswith('_'):
result[prop] = propvalue
else:
raise NoSuchModuleError(f'Module {modname!r} does not exist')
return result
return result
if not modname or modname == '.':
return self.descriptive_data
raise NoSuchModuleError(f'Module {modname!r} does not exist')
def get_exported_modules(self):
return [m for m, o in self.modules.items() if o.export]
def add_module(self, module, modulename):
"""Adds a named module object to this SecNode."""
self.modules[modulename] = module
if module.export:
self.export.append(modulename)
# def remove_module(self, modulename_or_obj):
# moduleobj = self.get_module(modulename_or_obj)

View File

@ -289,7 +289,6 @@ class Server:
If there are errors that occur, they will be collected and emitted
together in the end.
"""
errors = []
opts = dict(self.node_cfg)
cls = get_class(opts.pop('cls'))
self.secnode = SecNode(self.name, self.log.getChild('secnode'), opts, self)
@ -301,10 +300,9 @@ class Server:
self.secnode.add_secnode_property(k, opts.pop(k))
self.secnode.create_modules()
# initialize all modules by getting them with Dispatcher.get_module,
# which is done in the get_descriptive data
# TODO: caching, to not make this extra work
self.secnode.get_descriptive_data('')
# initialize modules by calling self.secnode.get_module for all of them
# this is done in build_descriptive_data even for unexported modules
self.secnode.build_descriptive_data()
# =========== All modules are initialized ===========
# all errors from initialization process