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 modulename, exportedname = specifier, None
if ':' in specifier: if ':' in specifier:
modulename, exportedname = specifier.split(':', 1) 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) 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: if exportedname is not None:
pname = moduleobj.accessiblename2attr.get(exportedname, True) pname = moduleobj.accessiblename2attr.get(exportedname, True)
if pname and pname not in moduleobj.accessibles: if pname and pname not in moduleobj.accessibles:
@ -281,7 +281,7 @@ class Dispatcher:
else: else:
# activate all modules # activate all modules
self._active_connections.add(conn) 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. # send updates for all subscribed values.
# note: The initial poll already happend before the server is active # note: The initial poll already happend before the server is active

View File

@ -44,8 +44,6 @@ class SecNode:
self.nodeprops = {} self.nodeprops = {}
# map ALL modulename -> moduleobj # map ALL modulename -> moduleobj
self.modules = {} self.modules = {}
# list of EXPORTED modules
self.export = []
self.log = logger self.log = logger
self.srv = srv self.srv = srv
# set of modules that failed creation # set of modules that failed creation
@ -193,60 +191,62 @@ class SecNode:
modname, len(pinata_modules)) modname, len(pinata_modules))
todos.extend(pinata_modules) todos.extend(pinata_modules)
def export_accessibles(self, modulename): def export_accessibles(self, modobj):
self.log.debug('export_accessibles(%r)', modulename) self.log.debug('export_accessibles(%r)', modobj.name)
if modulename in self.export: # omit export=False params!
# omit export=False params! res = OrderedDict()
res = OrderedDict() for aobj in modobj.accessibles.values():
for aobj in self.get_module(modulename).accessibles.values(): if aobj.export:
if aobj.export: res[aobj.export] = aobj.for_export()
res[aobj.export] = aobj.for_export() self.log.debug('list accessibles for module %s -> %r',
self.log.debug('list accessibles for module %s -> %r', modobj.name, res)
modulename, res) return res
return res
self.log.debug('-> module is not to be exported!') def build_descriptive_data(self):
return OrderedDict() 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): def get_descriptive_data(self, specifier):
"""returns a python object which upon serialisation results in the """returns a python object which upon serialisation results in the
descriptive data""" descriptive data"""
specifier = specifier or '' 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(':') modname, _, pname = specifier.partition(':')
modules = self.descriptive_data['modules']
if modname in modules: # extension to SECoP standard: description of a single module if modname in modules: # extension to SECoP standard: description of a single module
result = modules[modname] result = modules[modname]
if pname in result['accessibles']: # extension to SECoP standard: description of a single accessible if pname in result['accessibles']: # extension to SECoP standard: description of a single accessible
# command is also accepted # command is also accepted
result = result['accessibles'][pname] return result['accessibles'][pname]
elif pname: if pname:
raise NoSuchParameterError(f'Module {modname!r} ' raise NoSuchParameterError(f'Module {modname!r} '
f'has no parameter {pname!r}') f'has no parameter {pname!r}')
elif not modname or modname == '.': return result
result['equipment_id'] = self.equipment_id if not modname or modname == '.':
result['firmware'] = 'FRAPPY ' + get_version() return self.descriptive_data
result['description'] = self.nodeprops['description'] raise NoSuchModuleError(f'Module {modname!r} does not exist')
for prop, propvalue in self.nodeprops.items():
if prop.startswith('_'): def get_exported_modules(self):
result[prop] = propvalue return [m for m, o in self.modules.items() if o.export]
else:
raise NoSuchModuleError(f'Module {modname!r} does not exist')
return result
def add_module(self, module, modulename): def add_module(self, module, modulename):
"""Adds a named module object to this SecNode.""" """Adds a named module object to this SecNode."""
self.modules[modulename] = module self.modules[modulename] = module
if module.export:
self.export.append(modulename)
# def remove_module(self, modulename_or_obj): # def remove_module(self, modulename_or_obj):
# moduleobj = self.get_module(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 If there are errors that occur, they will be collected and emitted
together in the end. together in the end.
""" """
errors = []
opts = dict(self.node_cfg) opts = dict(self.node_cfg)
cls = get_class(opts.pop('cls')) cls = get_class(opts.pop('cls'))
self.secnode = SecNode(self.name, self.log.getChild('secnode'), opts, self) 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.add_secnode_property(k, opts.pop(k))
self.secnode.create_modules() self.secnode.create_modules()
# initialize all modules by getting them with Dispatcher.get_module, # initialize modules by calling self.secnode.get_module for all of them
# which is done in the get_descriptive data # this is done in build_descriptive_data even for unexported modules
# TODO: caching, to not make this extra work self.secnode.build_descriptive_data()
self.secnode.get_descriptive_data('')
# =========== All modules are initialized =========== # =========== All modules are initialized ===========
# all errors from initialization process # all errors from initialization process