From 47c1793b6147ea624da5ef71bc76a886ed52e488 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Wed, 13 Sep 2023 17:20:59 +0200 Subject: [PATCH] inform user about possible device change taking into account cfg retrieved from frappy and sea service manager --- commands.py | 206 ++++++------------------------------------------ devices.py | 221 +++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 216 insertions(+), 211 deletions(-) diff --git a/commands.py b/commands.py index 75e778c..af53158 100644 --- a/commands.py +++ b/commands.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # ***************************************************************************** # # This program is free software; you can redistribute it and/or modify it under @@ -20,124 +19,15 @@ # # ***************************************************************************** -import sys -from os.path import expanduser, basename, join -from configparser import ConfigParser - from nicos import session, config -from nicos.core import status from nicos.utils import printTable from nicos.commands import helparglist, usercommand -from nicos.commands.basic import AddSetup, CreateAllDevices, CreateDevice -from nicos.devices.secop.devices import get_attaching_devices -from nicos_sinq.frappy_sinq.devices import applyAliasConfig, FrappyNode - - -home = expanduser('~') -if home not in sys.path: - # the first Frappy installations have /home/nicos in the PYTHONPATH in nicos.conf - # for newer Frappy installations this should be home (= /home/) - # the following line fixes this in case nicos.conf is not yet updated - sys.path.append(home) from servicemanager import FrappyManager -def cleanup_defunct(): - for devname, setupname in list(session.dynamic_devices.items()): - dev = session.devices.get(devname) - if dev and dev._defunct: - devnames = [d.name for d, _ in get_attaching_devices(dev)] - if devnames: - session.log.warning('can not remove device %r due to dependencies on %s' - % (devname, ', '.join(devnames))) - else: - session.destroyDevice(devname) - session.dynamic_devices.pop(devname, None) - - SERVICES = FrappyManager.services -def all_info(all_cfg): - info = [] - addkwd = False - for srv in SERVICES: - cfginfo = all_cfg.get(srv) - if cfginfo is None: - addkwd = True - elif addkwd: - info.append('%s=%r' % (srv, cfginfo)) - else: - info.append(repr(cfginfo)) - result = 'frappy(%s)' % ', '.join(info) - if '?' in result: - result += ' (?: device from sea has no frappy cfg file)' - return result - - -def frappy_start(**services): - """start/stop frappy servers - - for example: frappy_start(main='xy', stick='') - - restart main server with cfg='xy' - - stop stick server - - do not touch addons server - - in addition, if a newly given cfg is already used on a running server, - this cfg is removed from the server (remark: cfg might be a comma separated list) - """ - frappy_config = session.devices.get('frappy_config') - for service in SERVICES: - if services.get(service) == '': - seaconn = session.devices.get(f'se_sea_{service}') - if seaconn and seaconn._attached_secnode: - seaconn.communicate('frappy_remove %s' % service) - used_cfg = {} - all_cfg = {} - new_cfg = [] - remove_cfg = [] - for service in reversed(SERVICES): - secnode = session.devices.get('se_' + service) - cfginfo = services.get(service) - if cfginfo is not None: - if cfginfo: - new_cfg.append((service, secnode, cfginfo)) - else: - remove_cfg.append(secnode) - if secnode: - secnode('') - if secnode: - all_cfg[service] = secnode.get_info() - - # check cfg is not used twice - for cfg in (cfginfo or '').split(','): - cfg = cfg.strip() - if cfg: - prev = used_cfg.get(cfg) - if prev: - raise ValueError('%r can not be used in both %s and %s' % (cfg, prev, service)) - used_cfg[cfg] = service - - if new_cfg: - for service, secnode, cfginfo in new_cfg: - nodename = 'se_' + service - if not secnode: - AddSetup('frappy_' + service) - secnode = session.devices[nodename] - secnode(cfginfo) - all_cfg[service] = secnode.get_info() - CreateDevice(nodename) - cleanup_defunct() - CreateAllDevices() - if frappy_config: - frappy_config.set_envlist() - else: - applyAliasConfig() - for secnode in remove_cfg: - secnode.disable() - return all_cfg - - @usercommand def set_se_list(): frappy_config = session.devices['frappy_config'] @@ -146,34 +36,37 @@ def set_se_list(): @usercommand @helparglist('main [, stick [, addons]]') -def frappy(*args, main=None, stick=None, addons=None): +def frappy(*args, main=None, stick=None, addons=None, force=False): """(re)start frappy server(s) with given configs and load setup if needed - without argument: list running frappy servers, restart failed frappy servers - frappy(''): if available, the standard stick is added too - frappy(''): the stick is removed too - addons are not changed when not given - - frappy(main='') # main cfg is changed, but stick is kept + - frappy(main='') # main cfg is changed, but stick is kept + - frappy('restart') # restart all frappy servers + - frappy(stick='restart') # restart stick frappy server """ - seacfg = FrappyManager().cfg_from_sea(config.instrument) + confirmed = FrappyManager().cfg_from_sea(config.instrument).get('confirmed') if args: if main is not None: raise TypeError('got multiple values for main') main = args[0] if len(args) == 1: # special case: main given as single argument - if stick is None: # auto stick + if main == 'restart': + stick = 'restart' + addons = 'restart' + elif stick is None: # auto stick if main == '': stick = '' # remove stick with main else: allsticks = FrappyManager().all_cfg(config.instrument, 'stick') - if seacfg.get('main') != main: - # main sea device has changed - stickcfg = main + 'stick' - if stickcfg in allsticks: - # if a default stick is available, start this also - stick = stickcfg - else: - stick = '' # remove stick when main has changed + stickcfg = main + 'stick' + if stickcfg in allsticks: + # if a default stick is available, start this also + stick = stickcfg + else: + stick = '' # remove stick when main has changed else: if stick is not None: raise TypeError('got multiple values for stick') @@ -182,63 +75,12 @@ def frappy(*args, main=None, stick=None, addons=None): if addons is not None: raise TypeError('got multiple values for addons') addons = ','.join(alist) - allcfg = frappy_start(main=main, stick=stick, addons=addons) - session.log.info('currently configured %s', all_info(allcfg)) - else: - allcfg = frappy_start(main=main, stick=stick, addons=addons) - session.log.info('currently configured %s', all_info(allcfg)) - guess1 = {} - guess2 = {} - for s in SERVICES: - info = allcfg.get(s, '') - prev = info.split(' ', 1)[0] - if prev != info: - guess1[s] = prev - guess2[s] = prev - fromsea = seacfg.get(s) - if fromsea and fromsea != prev: - guess2[s] = fromsea - if guess1 and guess2: - session.log.info('please consider to call one of:') - elif guess1 or guess2: - session.log.info('please consider to call:') - if guess1: - session.log.info('from frappy / cache: %s', all_info(guess1)) - if guess2: - session.log.info('including info from sea: %s', all_info(guess2)) - - -@usercommand -@helparglist('cfg') -def frappy_main(cfg=None): - """(re)start frappy_main server with given cfg and load setup if needed - - - without argument: list running frappy servers - - cfg = "": stop frappy_main server - """ - session.log.info('currently configured %s', all_info(frappy_start(main=cfg))) - - -@usercommand -@helparglist('cfg') -def frappy_stick(cfg=None): - """(re)start frappy_stick server with given cfg and load setup if needed - - - without argument: list running frappy servers - - cfg = "": stop frappy_stick server - """ - session.log.info('currently configured %s', all_info(frappy_start(stick=cfg))) - - -@usercommand -@helparglist('cfg,...') -def frappy_addons(cfg=None): - """(re)start frappy_addons server with given cfg and load setup if needed - - - without argument: list running frappy servers - - cfg = "": stop frappy_addons server - """ - session.log.info('currently configured %s', all_info(frappy_start(addons=cfg))) + if confirmed and confirmed != main and not force: + session.log.warning('%r is plugged to the cryostat control rack', confirmed) + session.log.warning('if you are sure, use frappy(..., force=True)', confirmed) + raise TypeError('refuse to override plugged device') + frappy_config = session.devices['frappy_config'] + frappy_config.show_config(*frappy_config.check_or_start(main, stick, addons)) @usercommand @@ -250,8 +92,6 @@ def frappy_list(service=None): def prt(line): content.append(line) - # TODO: remove next line - bases = list(dict.fromkeys(expanduser(p) for p in FrappyNode.config_dirs(config.instrument, service or 'main'))) if service is None: prt('Available configuration files') prt('') @@ -265,3 +105,7 @@ def frappy_list(service=None): FrappyManager().do_listcfg(config.instrument, service or 'main', prt) session.log.info('\n%s', '\n'.join(content)) + +@usercommand +def frappy_changed(): + session.devices['frappy_config'].changed() diff --git a/devices.py b/devices.py index f8f6014..39c295a 100644 --- a/devices.py +++ b/devices.py @@ -27,16 +27,21 @@ SEC Node with added functionality for starting and stopping frappy servers connected to a SEC node """ -import os -from os.path import expanduser - +import time +import threading from nicos import config, session -from nicos.core import Override, Param, Moveable, status, POLLER, SIMULATION, DeviceAlias +from nicos.core import Override, Param, Moveable, status, POLLER, SIMULATION, DeviceAlias, \ + Device, anytype, listof from nicos.devices.secop.devices import SecNodeDevice -from nicos.core import Device, anytype, listof +from nicos.core.utils import USER, User, createThread +from nicos.services.daemon.script import RequestError, ScriptRequest from nicos.utils.comparestrings import compare +from nicos.devices.secop.devices import get_attaching_devices +from nicos.commands.basic import AddSetup, CreateAllDevices, CreateDevice from servicemanager import FrappyManager +SERVICES = FrappyManager.services + def suggest(poi, allowed_keys): comp = {} @@ -67,6 +72,48 @@ def applyAliasConfig(): break +def cleanup_defunct(): + for devname, setupname in list(session.dynamic_devices.items()): + dev = session.devices.get(devname) + if dev and dev._defunct: + devnames = [d.name for d, _ in get_attaching_devices(dev)] + if devnames: + session.log.warning('can not remove device %r due to dependencies on %s' + % (devname, ', '.join(devnames))) + else: + session.destroyDevice(devname) + session.dynamic_devices.pop(devname, None) + + +def all_info(all_cfg, prefix='currently configured: '): + info = [] + addkwd = False + for srv in SERVICES: + cfginfo = all_cfg.get(srv) + if cfginfo is None: + addkwd = True + elif addkwd: + info.append('%s=%r' % (srv, cfginfo)) + else: + info.append(repr(cfginfo)) + return f"{prefix}frappy({', '.join(info)})" + + +def get_guess(allcfg): + guess = {} + seacfg = FrappyManager().cfg_from_sea(config.instrument) + guess.pop('confirmed', '') + for s in ('main', 'stick', 'addons'): + info = allcfg.get(s, '') + prev = info.split(' ', 1)[0] + if prev != info: + guess[s] = prev + fromsea = seacfg.get(s) + if fromsea and fromsea != prev: + guess[s] = fromsea + return guess + + class FrappyConfig(Device): # respect the order: e.g. temperature_regulation must be after temperature # because it will not be added to envlist when temperature is the same device @@ -92,6 +139,121 @@ class FrappyConfig(Device): meanings = list(parameters) meanings.remove('nodes') + _trigger_change = None + _previous_shown = None + + def doInit(self, mode): + if mode != SIMULATION and session.sessiontype != POLLER: + self._trigger_change = threading.Event() + createThread('frappy change notification', self.handle_notifications) + + def handle_notifications(self): + try: + controller = session.daemon_device._controller + while True: + self._trigger_change.wait() + self._trigger_change.clear() + time.sleep(2) + if self._trigger_change.is_set(): + continue + try: + current = self.check_or_start() + if current != self._previous_shown: + cmd = 'frappy_changed()' + controller.new_request(ScriptRequest(cmd, None, User('guest', USER))) + except RequestError as e: + session.log.error(f'can not queue request {e!r}') + except Exception as e: + print(e) + + def check_or_start(self, main=None, stick=None, addons=None): + """start/stop frappy servers + + for example: check_or_start(main='xy', stick='') + - restart main server with cfg='xy' + - stop stick server + - do not touch addons server + + in addition, if a newly given cfg is already used on a running server, + this cfg is removed from the server (remark: cfg might be a comma separated list) + """ + services = {'main': main, 'stick': stick, 'addons': addons} + for service, cfg in services.items(): + if cfg == '': + seaconn = session.devices.get(f'se_sea_{service}') + if seaconn and seaconn._attached_secnode: + seaconn.communicate('frappy_remove %s' % service) + used_cfg = {} + all_cfg = {} + new_cfg = {} + secnodes = {} + remove_cfg = [] + for service, cfginfo in services.items(): + secnodes[service] = secnode = session.devices.get('se_' + service) + chkinfo = '' + if secnode: + all_cfg[service] = chkinfo = secnode.get_info() + if cfginfo is not None: + new_cfg[service] = chkinfo = cfginfo + + # check cfg is not used twice + for cfg in chkinfo.split(','): + cfg = cfg.strip() + if cfg and cfg != 'restart': + prev = used_cfg.get(cfg) + if prev: + raise ValueError('%r can not be used in both %s and %s' % (cfg, prev, service)) + used_cfg[cfg] = service + + for service, cfginfo in reversed(list(new_cfg.items())): + if cfginfo != all_cfg.get(service, ''): + secnode = secnodes[service] + if secnode: + secnode('') # stop previous frappy server + + if new_cfg: + for service, cfginfo in new_cfg.items(): + nodename = 'se_' + service + secnode = secnodes[service] + prev = all_cfg.get(service) + if cfginfo != prev: + if cfginfo == 'restart': + cfginfo = prev + if not cfginfo: + continue + if not secnode: + if not cfginfo: + continue + AddSetup('frappy_' + service) + secnode = session.devices[nodename] + secnode(cfginfo) + all_cfg[service] = secnode.get_info() + CreateDevice(nodename) + cleanup_defunct() + CreateAllDevices() + self.set_envlist() + for secnode in remove_cfg: + secnode.disable() + return all_cfg, get_guess(all_cfg) + + def show_config(self, allcfg, guess): + # remove 'frappy_changed()' commands in script queue + controller = session.daemon_device._controller + controller.block_requests(r['reqid'] for r in controller.get_queue() if r['script'] == 'frappy_changed()') + self._previous_shown = allcfg, guess + session.log.info(all_info(allcfg)) + if guess: + info = all_info(guess, '') + session.log.warning('please consider to call:') + session.log.info(info) + if '?' in info: + session.log.warning("but create cfg files first for items marked with '?'") + + def changed(self): + current = self.check_or_start() + #if current == self._previous_shown: + # return + self.show_config(*current) def remove_aliases(self): for meaning in self.meanings: @@ -135,7 +297,10 @@ class FrappyConfig(Device): continue for devname, (_, desc) in nodedev.setup_info.items(): secop_module = desc['secop_module'] - meaning = secnode.modules[secop_module]['properties'].get('meaning') + try: + meaning = secnode.modules[secop_module]['properties'].get('meaning') + except KeyError: + meaning = None if meaning: meaning_name, importance = meaning sample_devices.setdefault(meaning_name, []).append((importance, devname)) @@ -230,6 +395,7 @@ class FrappyNode(SecNodeDevice, Moveable): } _cfgvalue = None + _lastcfg = None def doStart(self, value): if value == 'None': @@ -241,17 +407,18 @@ class FrappyNode(SecNodeDevice, Moveable): def doInit(self, mode): if mode != SIMULATION and session.sessiontype != POLLER: - cfg = self.doRead() - self.restart(cfg, False) # do not restart when not changed + self._lastcfg = self.doRead() + self.restart(self._lastcfg, False) # do not restart when not changed super().doInit(mode) def doRead(self, maxage=0): try: - return self.secnode.descriptive_data['_frappy_config'] + if self._secnode.online: + return self._secnode.descriptive_data['_frappy_config'] except (KeyError, AttributeError): pass if self._cfgvalue is None: - sea_cfg = FrappyManager().cfg_from_sea(config.instrument).get(self.service) + sea_cfg = FrappyManager().cfg_from_sea(config.instrument).get(self.service) if sea_cfg: return sea_cfg if self._cache: @@ -259,7 +426,10 @@ class FrappyNode(SecNodeDevice, Moveable): return self._cfgvalue def createDevices(self): + cfg = self.read() super().createDevices() + if cfg != self._lastcfg: + session.devices.get('frappy_config')._trigger_change.set() if self.param_category: for devname, (_, devcfg) in self.setup_info.items(): params_cfg = devcfg['params_cfg'] @@ -269,23 +439,14 @@ class FrappyNode(SecNodeDevice, Moveable): if not pinfo.category: pinfo.category = self.param_category - @classmethod - def config_dirs(cls, ins, service): - # TODO: no more needed after allowing ~cfg in FrappyManager.do_start - return FrappyManager().config_dirs(ins, service) - - @classmethod - def available_cfg(cls, service): - # TODO: no more needed after allowing ~cfg in FrappyManager.do_start - ins = config.instrument - available_cfg = set() - for d in cls.config_dirs(ins, service): - try: - # available_cfg |= set(c[:-4] for c in os.listdir(expanduser(d)) if c.endswith('.cfg')) - available_cfg |= set(c[:-7] for c in os.listdir(expanduser(d)) if c.endswith('_cfg.py')) - except FileNotFoundError: # ignore missing directories - pass - return available_cfg + def nodeStateChange(self, online, state): + super().nodeStateChange(online, state) + if not online: + self._cfgvalue = None + cfg = self.read() + if self._lastcfg != cfg: + self._lastcfg = cfg + session.devices.get('frappy_config')._trigger_change.set() def disable(self): seaconn = session.devices.get('sea_%s' % self.service) @@ -315,12 +476,12 @@ class FrappyNode(SecNodeDevice, Moveable): fm.get_procs(cfginfo=cfginfo) running_cfg = cfginfo.get((ins, self.service), '') if not forced: - sea_cfg = fm.cfg_from_sea(ins).get(self.service) + sea_cfg = fm.cfg_from_sea(ins).get(self.service, '') if '?' in sea_cfg: if sea_cfg == '?': self.log.warning('undefined sea device') else: - self.log.warning(f"missing frappy cfg file for {sea_cfg.replace('?', ''))}") + self.log.warning(f"missing frappy cfg file for {sea_cfg.replace('?', '')}") cfg = '' # stop server elif sea_cfg: cfg = sea_cfg @@ -335,7 +496,7 @@ class FrappyNode(SecNodeDevice, Moveable): fm.do_stop(ins, self.service) is_cfg = cfg and ':' not in cfg if is_cfg: - available_cfg = self.available_cfg(self.service) + available_cfg = FrappyManager().all_cfg(config.instrument, self.service) failed = False for cfgitem in cfg.split(','): if cfgitem not in available_cfg: