rework frappy to be a device instead of a command

+ improve proposed config mechanism
This commit is contained in:
2023-10-17 15:18:47 +02:00
parent 807b9eb4e7
commit 5678530e6e
3 changed files with 121 additions and 200 deletions

View File

@ -27,18 +27,17 @@ SEC Node with added functionality for starting and stopping frappy servers
connected to a SEC node
"""
import time
import threading
from nicos import config, session
from nicos.core import Override, Param, Moveable, status, POLLER, SIMULATION, DeviceAlias, \
Device, anytype, listof
from nicos.devices.secop.devices import SecNodeDevice
from nicos.devices.secop.devices import SecNodeDevice, NicosSecopClient
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
from servicemanager import FrappyManager, SeaManager
SERVICES = FrappyManager.services
@ -107,9 +106,9 @@ def all_info(all_cfg, prefix='currently configured: '):
def get_frappy_config():
try:
return session.devices['frappy_config']
return session.devices['frappy']
except KeyError:
session.log.error("'frappy_config' is not available - 'frappy' setup is not loaded")
session.log.error("the frappy device is not available - 'frappy' setup is not loaded")
return None
@ -155,16 +154,17 @@ class FrappyConfig(Device):
def handle_notifications(self):
controller = session.daemon_device._controller
while True:
self._trigger_change.wait()
# we do not wait for ever here, because there might be changes
# on an unconnected service
self._trigger_change.wait(15)
self._trigger_change.clear()
time.sleep(2)
if self._trigger_change.is_set():
continue
while self._trigger_change.wait(2): # triggered again within 2 sec
self._trigger_change.clear()
try:
cfgs = self.check_services()
guess_info = self.to_consider(cfgs)
if (cfgs, guess_info) != self._previous_shown and (guess_info or not self._servers_loaded):
cmd = 'frappy_changed()'
changes, state, remarks = self.to_consider(cfgs)
if state != self._previous_shown and changes:
cmd = 'frappy.has_changed() # inserted automatically when frappy or sea servers changed'
controller.new_request(ScriptRequest(cmd, None, User('guest', USER)))
except RequestError as e:
session.log.error(f'can not queue request {e!r}')
@ -174,16 +174,18 @@ class FrappyConfig(Device):
for a potential "please consider calling frappy(...)" message
"""
guess_info = {}
for service, guess in FrappyManager().guess_cfgs(config.instrument, cfgs).items():
proposed = guess.get('proposed')
if proposed:
guess_info[service] = proposed
else:
missing = guess.get('missing')
if missing:
guess_info[service] = [m + '?' for m in missing]
return guess_info
error, proposed, state, remarks = FrappyManager().get_server_state(config.instrument, cfgs)
changes = dict(proposed)
for service, guess in proposed.items():
if guess is True:
changes.pop(service)
disconnected = set()
for service, info in cfgs.items():
if info == '<disconnected>':
disconnected.add(service)
if not changes.get(service):
changes[service] = ''
return changes, (proposed,) + state, remarks
def check_services(self):
cfgs = {}
@ -209,7 +211,10 @@ class FrappyConfig(Device):
if cfg == '':
seaconn = session.devices.get(f'se_sea_{service}')
if seaconn and seaconn._attached_secnode:
seaconn.communicate('frappy_remove %s' % service)
try:
seaconn.communicate('frappy_remove %s' % service)
except Exception:
pass
used_cfg = {}
all_cfg = {}
new_cfg = {}
@ -264,21 +269,89 @@ class FrappyConfig(Device):
self._cache.put(self, 'config', all_cfg)
return all_cfg
def show_config(self, allcfg):
guess_info = self.to_consider(allcfg)
# remove 'frappy_changed()' commands in script queue
def __call__(self, *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('<cfg>'): if available, the standard stick is added too
- frappy(''): the stick is removed too
- addons are not changed when not given
- frappy(main='<cfg>') # main cfg is changed, but stick is kept
- frappy('restart') # restart all frappy servers
- frappy(stick='restart') # restart stick frappy server
"""
stickarg = stick
confirmed = SeaManager().get_cfg(config.instrument, 'sea', True).split('/', 1)[0]
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 main == 'restart':
stick = 'restart'
addons = 'restart'
elif stick is None: # auto stick
if main == '':
stick = '' # remove stick with main
else:
stickcfg = main + 'stick'
if FrappyManager().is_cfg(config.instrument, 'stick', stickcfg):
# 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')
stick, *alist = args[1:]
if alist:
if addons is not None:
raise TypeError('got multiple values for addons')
addons = ','.join(alist)
elif main is None and stick is None and addons is None: # bare frappy() command
self.show_config(self.check_services(), True)
return
if confirmed and confirmed != main and main not in (None, 'restart') and not force:
session.log.warning('%r is plugged to the cryostat control rack', confirmed)
cmd = all_info({'main': main, 'stick': stickarg, 'addons': addons}, '')[:-1] + ', force=True)'
session.log.warning(f'if you are sure, use: %s', cmd)
raise TypeError('refuse to override plugged device')
self.show_config(self.start_services(main, stick, addons))
def show_config(self, allcfg, show_server_state=False):
changes, state, remarks = self.to_consider(allcfg)
if show_server_state == 'auto':
show_server_state = state != self._previous_shown
if show_server_state:
proposed, frappycfgs, seacfgs = state
rows = [['server', 'frappy', 'sea', '']]
for key, remark in remarks.items():
rows.append([key if key in ('main', 'stick') else 'addons',
frappycfgs.get(key, ''), seacfgs.get(key, ''), remark])
wid = [max(len(v) for v in column) for column in zip(*rows)]
# insert title underlines
rows.insert(1, ['-' * w for w in wid[:-1]] + [''])
for row in rows:
session.log.info('%s', ' '.join(v.ljust(w) for w, v in zip(wid, row)))
session.log.info('')
# remove 'frappy.has_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_info
controller.block_requests(r['reqid'] for r in controller.get_queue()
if r['script'].startswith('frappy.has_changed()'))
self._previous_shown = state
session.log.info(all_info(allcfg))
if guess_info:
info = all_info(guess_info, '')
session.log.warning('please consider to call:')
if changes:
info = all_info(changes, 'proposed cfg changes: ')
session.log.info(info)
session.log.warning('please consider to call: frappy.update() for doing above changes')
if '?' in info:
session.log.warning("but create cfg files first for items marked with '?'")
def update(self):
changes = self.to_consider(self.check_services())[0]
self.show_config(self.start_services(**changes))
def initial_restart_cfg(self, service):
"""get cfg for (re)start of the service
@ -290,7 +363,7 @@ class FrappyConfig(Device):
if self._servers_loaded:
return None
if self._initial_config is None:
success = True
# we do this only once for all services
fm = FrappyManager()
running = fm.get_cfg(config.instrument, None)
cache = self._getCache()
@ -309,30 +382,15 @@ class FrappyConfig(Device):
cfgs['stick'] = running_stick
else:
cfgs.pop('stick', None)
for serv, guess in fm.guess_cfgs(config.instrument, cfgs).items():
ok = guess.get('ok', [])
for proposed in guess.get('proposed', []):
if isinstance(proposed, str): # start only when ambiguous
ok.append(proposed)
else:
success = False
if ok:
cfgs[serv] = ','.join(ok)
if not success:
cfgs = {}
result = {}
for serv, cfg in cfgs.items():
runcfg = running.get(serv)
if runcfg != cfg:
result[serv] = cfg
elif runcfg:
result[serv] = True # restart not needed as cfg has not changed
self._initial_config = result
error, proposed, state, remarks = fm.get_server_state(config.instrument, cfgs)
self._initial_config = proposed
if not error:
self._previous_shown = state # otherwise the server state will be shown on startup
return self._initial_config.get(service)
def changed(self):
def has_changed(self, show_server_state='auto'):
self._servers_loaded = True
self.show_config(self.check_services())
self.show_config(self.check_services(), show_server_state)
def remove_aliases(self):
for meaning in self.meanings:
@ -485,15 +543,13 @@ class FrappyNode(SecNodeDevice, Moveable):
if mode == SIMULATION or session.sessiontype == POLLER:
super().doInit(mode)
else:
fc = session.devices.get('frappy_config')
fc = session.devices.get('frappy')
if fc:
cfg = fc.initial_restart_cfg(self.service)
if isinstance(cfg, str): # may also be None or True
self.restart(cfg)
if cfg is None: # None means: server is not running, and does not need to be restarted
# connect in background, as the server might be started later
createThread('connect', self._connect)
# TODO: check if it is not better to add a try_period argument to SecNode._connect()
self._disconnect()
return
try:
self._connect()
@ -594,9 +650,15 @@ class FrappyNode(SecNodeDevice, Moveable):
self._cache.put(self, 'value', cfg)
self._setROParam('target', cfg)
def _disconnect(self):
super()._disconnect()
self._setROParam('target', '')
def get_info(self):
result = self.doRead() or ''
code, text = self.status()
if not result and self.target:
return '<disconnected>'
if code == status.OK or result == '':
return result
if (code, text) == (status.ERROR, 'reconnecting'):