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 self.get_module(modulename).accessibles.values(): for aobj in modobj.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',
modulename, res) modobj.name, res)
return res return res
self.log.debug('-> module is not to be exported!')
return OrderedDict()
def get_descriptive_data(self, specifier): def build_descriptive_data(self):
"""returns a python object which upon serialisation results in the
descriptive data"""
specifier = specifier or ''
modules = {} modules = {}
result = {'modules': modules} result = {'modules': modules}
for modulename in self.export: for modulename in self.modules:
module = self.get_module(modulename) modobj = self.get_module(modulename)
if not module.export: if not modobj.export:
continue continue
# some of these need rework ! # some of these need rework !
mod_desc = {'accessibles': self.export_accessibles(modulename)} mod_desc = {'accessibles': self.export_accessibles(modobj)}
mod_desc.update(module.exportProperties()) mod_desc.update(modobj.exportProperties())
mod_desc.pop('export', False) mod_desc.pop('export', None)
modules[modulename] = mod_desc modules[modulename] = mod_desc
modname, _, pname = specifier.partition(':')
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:
raise NoSuchParameterError(f'Module {modname!r} '
f'has no parameter {pname!r}')
elif not modname or modname == '.':
result['equipment_id'] = self.equipment_id result['equipment_id'] = self.equipment_id
result['firmware'] = 'FRAPPY ' + get_version() result['firmware'] = 'FRAPPY ' + get_version()
result['description'] = self.nodeprops['description'] result['description'] = self.nodeprops['description']
for prop, propvalue in self.nodeprops.items(): for prop, propvalue in self.nodeprops.items():
if prop.startswith('_'): if prop.startswith('_'):
result[prop] = propvalue result[prop] = propvalue
else: self.descriptive_data = result
raise NoSuchModuleError(f'Module {modname!r} does not exist')
def get_descriptive_data(self, specifier):
"""returns a python object which upon serialisation results in the
descriptive data"""
specifier = specifier or ''
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
return result['accessibles'][pname]
if pname:
raise NoSuchParameterError(f'Module {modname!r} '
f'has no parameter {pname!r}')
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): 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