diff --git a/new.py b/new.py index 8afd9e6..316eb75 100644 --- a/new.py +++ b/new.py @@ -39,9 +39,12 @@ from frappy_sinq.secop.devices import SecNodeDevice, SecopDevice, DefunctDevice, from nicos.core.utils import createThread from nicos.utils.comparestrings import compare from nicos.devices.secop.devices import get_attaching_devices -from nicos.utils import loggers +from nicos.utils import loggers, printTable +from nicos.services.daemon.script import ScriptRequest +from nicos.commands import helparglist, usercommand from nicos.commands.basic import RemoveSetup, AddSetup from linsetools.frappy import FrappyControl +from linsetools.base import write_content SETUP_TEMPLATE = """description = '%(desc)s' @@ -134,8 +137,7 @@ def shorten(value): return result[:-1] if result.endswith(',') else result -def get_meanings(modname, moddesc): - meaning = moddesc['properties'].get('meaning') +def get_meanings(modname, meaning, hastarget): result = {} if meaning: meaning_name, importance = meaning @@ -146,7 +148,7 @@ def get_meanings(modname, moddesc): if meaning_name == 'temperature_regulation': # add temperature_regulation to temperature list, with very low importance result.setdefault('temperature', []).append((importance - 100, modname)) - elif meaning_name == 'temperature' and moddesc['parameters'].get('target'): + elif meaning_name == 'temperature' and hastarget: result.setdefault('temperature_regulation', []).append((importance, modname)) return result @@ -171,12 +173,13 @@ class FrappyManager(Readable): _box2uri = None _uri2box = None _secnodes = None # a dict like Secnodes instance + _do_cleanup = False def doInit(self, mode): if SETUPDIR not in session._setup_paths: session.log.error('can not use frappy as %r is not in the setup_subdirs', SETUPDIR) expt = session.experiment - if 'persistent_environment' in expt.parameters: + if 'persistent_environment' in expt.parameters and mode != SIMULATION: expt.persistent_environment = [v for k, v in MEANINGS.items() if k not in SKIP_ENV] else: cls = type(expt) @@ -189,6 +192,10 @@ class FrappyManager(Readable): self._secnodes = secnodes_from_string(value) self._value = shorten(value) + def _update_secnodes(self, secnodes): + self._secnodes = secnodes + self.secnodes = secnodes_to_string(self._secnodes) + def doRead(self, maxage=0): return self._value @@ -205,10 +212,6 @@ class FrappyManager(Readable): secnodes = secnodes_from_string(self.secnodes) else: secnodes = {k: v for k, v in self._secnodes.items() if k in self._running or not v.startswith('localhost:')} - for name, uri in self._secnodes: - if name not in self._running and uri.startswith('localhost.'): - continue - secnodes[name] = uri if check_connection: for name, uri in list(secnodes.items()): if not uri.startswith('localhost:'): @@ -219,12 +222,7 @@ class FrappyManager(Readable): secopclient.disconnect() self._box2uri[name] = uri self._uri2box[uri] = name - self._secnodes = secnodes - self.secnodes = secnodes_to_string(secnodes) - self._value = shorten(self.secnodes) - - def doPoll(self, maxage=0): - self.doRead() + self._update_secnodes(secnodes) def doStatus(self, maxage=0): return status.OK, '' @@ -259,87 +257,110 @@ class FrappyManager(Readable): self._check_secnodes(False) fc = self._frappy_control - # determine which servers to stop/start - tostop = [] - tostart = {} + # determine which nodes to add (and start) or remove (and stop) + to_remove = {} + to_add = {} main_item = None stick_item = None - for name, uri in self._secnodes: + for name, uri in self._secnodes.items(): if isinstance(uri, Main): main_item = name elif isinstance(uri, Stick): stick_item = name + elif not isinstance(uri, Addons): + self.log.warning('%r is no service', uri) if main is not None: if main_item: - tostop.append(main_item) - self._secnodes.pop(main_item) + to_remove[main_item] = self._secnodes.pop(main_item) if main: - tostart[main] = Main - # auto stick: - if main_item != main and stick is None: - try: - cfg = f'{main}stick' - service, cfgfile = fc.cfg_file(None, 'stick', cfg) - if service == 'stick': - stick = cfg - except FileNotFoundError: - pass + to_add[main] = Main + if main != main_item: + if main in self._secnodes: + to_remove[main] = self._secnodes.pop(main) + # auto stick: + if stick is None: + try: + cfg = f'{main}stick' + service, cfgfile = fc.cfg_file(None, 'stick', cfg) + if service == 'stick': + stick = cfg + except FileNotFoundError: + pass if stick is not None: if stick_item: - tostop.append(stick_item) - self._secnodes.pop(stick_item) + to_remove[stick_item] = self._secnodes.pop(stick_item) if stick: - tostart[stick] = Stick + to_add[stick] = Stick + if stick in self._secnodes: + to_remove[stick] = self._secnodes.pop(stick) for arglist in (addons,) + extra: if arglist == '': - tostop.extend(self._secnodes) + to_remove.update(self._secnodes) + self._secnodes.clear() elif arglist is not None: # allow legacy ',' for arg in arglist.split(','): if arg: - tostart[arg] = None + to_add[arg] = Addons + prev = self._secnodes.get(arg) + if isinstance(prev, (Main, Stick)): + to_remove[arg] = self._secnodes.pop(arg) - toremove = [] - for cfg in self._running: - if cfg is None: - continue - # TODO: when using nodename for setup, this may not work here ... - setup = self.get_setup_name(cfg) - if setup in session.loaded_setups: - toremove.append(setup) - if ':' in cfg: - continue - if toremove: - RemoveSetup(*toremove) - for cfg in tostop: - fc.stop(cfg) - if cfg not in tostart: - fc.delete_frappy_service(cfg) + # remove_setups = {self.get_setup_name(cfg) for cfg in to_remove} & set(session.loaded_setups) + # if remove_setups: + # RemoveSetup(*remove_setups) + for cfg in to_remove: + secnode_dev = session.devices.get(f'secnode_{cfg}') + if secnode_dev: + secnode_dev.doShutdown() + for cfg, uri in to_remove.items(): + if uri.startswith('localhost:'): + fc.stop(cfg) + fc.delete_frappy_service(cfg) - toadd = [] + add_setups = [] # add and start new servers - for arg, service in tostart.items(): - uri, name = self.get_uri_name(arg, service) + for arg, servicecls in to_add.items(): + uri, name = self.get_uri_name(arg, servicecls) cfginfo = '' - if isinstance(uri, Service): + if uri.startswith('localhost:'): # isinstance(uri, Service): port = fc.get_port(uri.name) - uri = fc.add_frappy_service(uri.name, name, port, session.log) + uri = servicecls(fc.add_frappy_service(servicecls.name, name, port, session.log)) cfginfo = f' ({name})' fc.start(name) - session.log.info('wait for startup of %r%s', uri, cfginfo) + session.log.info('wait for startup of %r%s', uri, cfginfo) + else: + session.log.info('connect to %r%s', uri, cfginfo) secopclient = self.connect_secnode(uri) if secopclient: self.write_setup_file(secopclient, uri, name, arg) secopclient.disconnect() - toadd.append(self.get_setup_name(name)) + add_setups.append(self.get_setup_name(name)) + self._secnodes[name] = uri else: self._secnodes.pop(name, None) session.log.exception('cannot connect to %r%s', uri, cfginfo) + self._update_secnodes(self._secnodes) + remove_setups = {self.get_setup_name(cfg) for cfg in to_remove} & set(session.loaded_setups) + noadd_set = session.loaded_setups - set(remove_setups) + add_setups = [v for v in add_setups if v not in noadd_set] + + if remove_setups: + RemoveSetup(*remove_setups) + if add_setups: + AddSetup(*add_setups) session.readSetups() - if toadd: - AddSetup(*toadd) - self._running = self._frappy_control.running() - return self.read() + unused_setups = {f for f in Path(SETUPDIR).glob('se_*.py') + if f.stem not in session.loaded_setups and f.stem[3:] not in self._secnodes} + if unused_setups: + for setup_file in unused_setups: + content = setup_file.read_text() + newcontent = re.sub("\ngroup = '(optional|plugplay)'", "\ngroup = 'lowlevel'", content) + if newcontent != content: + self.log.info('hide unused setup %s', setup_file.stem) + write_content(setup_file, newcontent, group='+rw') + session.readSetups() + return self.read(0) def get_setup_name(self, cfg): cfg = cfg.replace(':', '_') @@ -359,30 +380,17 @@ class FrappyManager(Readable): depending on their meaning """ modules = secopclient.modules - result = {} # dict of list of (, ) + meaning_info = {} # dict of list of (, ) device_mapping = {} reserved_names = {v.lower() for v in MEANINGS.values()} for modname, moddesc in modules.items(): - if modname.lower() in reserved_names: + if self.prefix == '' and modname.lower() in reserved_names: device_mapping[modname] = {'name': f'{modname}_'} - # meanings = self.get_meanings(modname, moddesc) - - - meaning = moddesc['properties'].get('meaning') - if meaning: - meaning_name, importance = meaning - if meaning_name not in MEANINGS: - self.log.warning('%s: meaning %r is unknown', modname, meaning_name) - continue - result.setdefault(meaning_name, []).append((importance, modname)) - if meaning_name == 'temperature_regulation': - # add temperature_regulation to temperature list, with very low importance - result.setdefault('temperature', []).append((importance - 100, modname)) - elif meaning_name == 'temperature' and moddesc['parameters'].get('target'): - result.setdefault('temperature_regulation', []).append((importance, modname)) + meaning_info.update(get_meanings(modname, moddesc['properties'].get('meaning'), + 'target' in moddesc['parameters'])) envlist = [] alias_config = {} - for meaning_name, info in result.items(): + for meaning_name, info in meaning_info.items(): importance, modname = sorted(info)[-1] devname = self._get_device_name(device_mapping, modname) target = MEANINGS.get(meaning_name) @@ -395,16 +403,15 @@ class FrappyManager(Readable): def write_setup_file(self, secopclient, uri, name, origname): setup = self.get_setup_name(name) - setup_file = Path(SETUPDIR) / f'{setup}.py' if name != origname: + # sanitize name: replace sequences of non-alphanumeric characters by a single '_' + # also prefix '_' when starting with a digit + # and remove trailing .psi.ch name = IDSUB.sub(secopclient.nodename.replace('.psi.ch', ''), '_') self._box2uri[name] = uri envlist, alias_config, devmap = self.node_setup_info(secopclient) nodeargs = f'device_mapping={devmap!r}' if devmap else '' additions = [] - if envlist: - aliassetups = [f'{k}_alias' for k in envlist] - # additions.append(f'includes = {aliassetups!r}') if alias_config: additions.append(f'alias_config = {alias_config!r}') desc = secopclient.properties.get('description') or name @@ -415,18 +422,24 @@ class FrappyManager(Readable): 'uri': repr(uri), 'nodeargs': nodeargs, 'additions': '\n'.join(additions),} - setup_file.write_text(setup_content) - prev_mode = setup_file.stat().st_mode - if not prev_mode & 0x20: - setup_file.chmod(prev_mode | 0x20) + write_content(Path(SETUPDIR) / f'{setup}.py', setup_content, group='+rw') return envlist def adjustEnvironment(self): if self._mode == SIMULATION: return + self._do_cleanup = True + session.readSetups() + # user = type('User', (), {'name': 'ghost'})() + # session.daemon_device._controller.new_request(ScriptRequest('frappy.cleanup_setups()', '', user)) samenvlist = [] for meaning, devname in MEANINGS.items(): dev = session.devices.get(devname) + if dev: + try: + dev.read(0) + except Exception as e: + self.log.info('error %r when reading %s', e, dev) if meaning in SKIP_ENV: continue if dev: @@ -450,6 +463,7 @@ class FrappyManager(Readable): elif devname not in previous: envlist.append(devname) expt.setEnvironment(envlist) + self.read(0) def cleanup_defunct(): @@ -499,13 +513,15 @@ class FrappyNode(SecNodeDevice): def createDevices(self): super().createDevices() self.log.info('--- create devices ---') - secnode = self._attached_secnode for devname, (_, devcfg) in self.setup_info.items(): - meanings = get_meanings(devcfg['secop_module'], devcfg['secop_properties']) + params_cfg = devcfg['params_cfg'] + meanings = get_meanings( + devcfg['secop_module'], + devcfg['secop_properties'].get('meaning'), + 'target' in params_cfg) for meaning in meanings: alias = MEANINGS.get(meaning) if alias and alias not in session.devices: - self.log.info('create alias %r for %s', alias, meaning) # alias is not yet created devcfg = ('nicos.core.device.DeviceAlias', {'descripton': meaning}) session.configured_devices[alias] = devcfg @@ -514,7 +530,6 @@ class FrappyNode(SecNodeDevice): elif not alias: self.log.info('do not know meaning %s', meaning) if self.param_category: - params_cfg = devcfg['params_cfg'] dev = session.devices[devname] for pname, pargs in params_cfg.items(): pinfo = dev.parameters[pname] @@ -582,3 +597,27 @@ class NullDevice(Moveable): def doStart(self, target): self.log.warning('disabled, cannot not move') + + +@usercommand +@helparglist('') +def frappy_list(service=None): + """list available configuration files""" + fc = FrappyControl('this') + content = [] + + def prt(line): + content.append(line) + + if service is None: + prt('Available configuration files') + prt('') + prt('Hint: if no config file can be found which matches your needs exactly') + prt('make a copy of an existing one, and change the description accordingly') + prt('') + prt('Usage (default argument "main"):') + prt('') + printTable(['command'], [['frappy_list(%r)' % s] for s in fc.services], prt) + + fc.listcfg(service or 'main', prt) + session.log.info('\n%s', '\n'.join(content)) diff --git a/secop/devices.py b/secop/devices.py index c527eba..11bf741 100644 --- a/secop/devices.py +++ b/secop/devices.py @@ -82,6 +82,8 @@ except ImportError: class NicosSecopClient(SecopClient): SPECIAL_NAMES = {'target', 'status', 'stop', 'pollinterval', 'reset'} + MANGLED = re.compile(r'(.*)(_*)$') + forbidden_names = set() def internalize_name(self, name): """name mangling @@ -93,22 +95,18 @@ class NicosSecopClient(SecopClient): """ if name in self.SPECIAL_NAMES: return name - name = super().internalize_name(name).lower() - prefix, sep, postfix = name.partition('_') - if prefix not in dir(SecopMoveable): - return name - # mangle names matching NICOS device attributes - # info -> info_, info_ -> info_1, info_1 -> info_2, ... - if not sep: + if not self.forbidden_names: + self.forbidden_names.update(k.lower() for k in dir(SecopMoveable)) + name = super().internalize_name(name) + match = self.MANGLED.match(name) + if match: + basename, postfix = match.groups() + else: + basename, postfix = name, '' + if basename.lower() in self.forbidden_names: + # mangle names matching NICOS device attributes + # info -> info_, info_ -> info__, ... return name + '_' - if not postfix: - return name + '_1' - try: - num = int(postfix) - if str(num) == postfix: - return '%s_%d' % (prefix, num + 1) - except ValueError: - pass return name @@ -330,8 +328,7 @@ class SecNodeDevice(Readable): def get_setup_info(self): if self._mode == SIMULATION: db = session._getSyncDb() - # we might do here in addition: - session.simulationSync(db) + # db = session.simulation_db return db.get('%s/setup_info' % self.name.lower()) return self.setup_info diff --git a/setups/frappy.py b/setups/frappy.py index 8c4de6e..c351cc0 100644 --- a/setups/frappy.py +++ b/setups/frappy.py @@ -2,7 +2,7 @@ description = 'sample environment basics' group = 'optional' display_order = 50.5 -# modules = ['frappy_sinq.commandsnew'] +modules = ['frappy_sinq.new'] devices = dict( frappy = device(