355 lines
14 KiB
Python
355 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
# *****************************************************************************
|
|
# NICOS, the Networked Instrument Control System of the MLZ
|
|
# Copyright (c) 2009-2018 by the NICOS contributors (see AUTHORS)
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify it under
|
|
# the terms of the GNU General Public License as published by the Free Software
|
|
# Foundation; either version 2 of the License, or (at your option) any later
|
|
# version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along with
|
|
# this program; if not, write to the Free Software Foundation, Inc.,
|
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
# Module authors:
|
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
|
#
|
|
# *****************************************************************************
|
|
"""managing SECoP server and connections
|
|
|
|
SEC Node with added functionality for starting and stopping frappy servers
|
|
connected to a SEC node
|
|
"""
|
|
|
|
import os
|
|
from os.path import expanduser
|
|
|
|
from nicos import config, session
|
|
from nicos.core import Override, Param, Moveable, status, POLLER, SIMULATION, DeviceAlias
|
|
from nicos.devices.secop.devices import SecNodeDevice
|
|
from nicos.core import Device, anytype, listof
|
|
from nicos.utils.comparestrings import compare
|
|
from servicemanager import FrappyManager
|
|
|
|
|
|
def suggest(poi, allowed_keys):
|
|
comp = {}
|
|
for key in allowed_keys:
|
|
comp[key] = compare(poi, key)
|
|
comp = sorted(comp.items(), key=lambda t: t[1], reverse=True)
|
|
return [m[0] for m in comp[:3] if m[1] > 2]
|
|
|
|
|
|
def applyAliasConfig():
|
|
"""Apply the desired aliases from session.alias_config.
|
|
|
|
be more quiet than original
|
|
"""
|
|
# reimplemented from Session.applyAliasConfig
|
|
# apply also when target dev name does not change, as the target device might have
|
|
# be exchanged in the mean time
|
|
for aliasname, targets in session.alias_config.items():
|
|
if aliasname not in session.devices:
|
|
continue # silently ignore
|
|
aliasdev = session.getDevice(aliasname)
|
|
for target, _ in sorted(targets, key=lambda t: -t[1]):
|
|
if target in session.devices:
|
|
try:
|
|
aliasdev.alias = target
|
|
except Exception:
|
|
session.log.exception("could not set '%s' alias", aliasdev)
|
|
break
|
|
|
|
|
|
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
|
|
parameters = {
|
|
'temperature': Param(
|
|
'config for sample temperature', type=anytype, default={}),
|
|
'temperature_regulation': Param(
|
|
'config for temperature regulation', type=anytype, default={}),
|
|
'magneticfield': Param(
|
|
'config for magnetic field', type=anytype, default={}),
|
|
'pressure': Param(
|
|
'config for pressure', type=anytype, default={}),
|
|
'rotation_z': Param(
|
|
'config for sample rotation (to be used as a3)',
|
|
type=anytype, default={}),
|
|
'stick_rotation': Param(
|
|
'config for stick rotation (not necessarily to be used as a3)',
|
|
type=anytype, default={}),
|
|
'nodes': Param(
|
|
'list of names of potential SEC nodes',
|
|
type=listof(str), default=[]),
|
|
}
|
|
|
|
meanings = list(parameters)
|
|
meanings.remove('nodes')
|
|
|
|
def remove_aliases(self):
|
|
for meaning in self.meanings:
|
|
info = getattr(self, meaning)
|
|
aliasnames = info.get('alias', [])
|
|
if isinstance(aliasnames, str):
|
|
aliasnames = [aliasnames]
|
|
for aliasname in aliasnames:
|
|
aliasdev = session.devices.get(aliasname)
|
|
if aliasdev:
|
|
session.destroyDevice(aliasname)
|
|
session.configured_devices.pop(aliasname, None)
|
|
session.dynamic_devices.pop(aliasname, None)
|
|
|
|
def get_se_aliases(self):
|
|
result = {}
|
|
for meaning in self.meanings:
|
|
info = getattr(self, meaning)
|
|
aliasnames = info.get('alias', [])
|
|
if isinstance(aliasnames, str):
|
|
aliasnames = [aliasnames]
|
|
for aliasname in aliasnames:
|
|
aliasdev = session.devices.get(aliasname)
|
|
if isinstance(aliasdev, DeviceAlias):
|
|
result[aliasname] = aliasdev
|
|
return result
|
|
|
|
def set_envlist(self):
|
|
"""create aliases for SECoP devices
|
|
|
|
depending on their meaning
|
|
"""
|
|
previous_aliases = self.get_se_aliases()
|
|
# self.remove_aliases()
|
|
|
|
nodedevs = filter(None, [session.devices.get(devname) for devname in self.nodes])
|
|
sample_devices = {}
|
|
for nodedev in nodedevs:
|
|
secnode = nodedev._secnode
|
|
if not secnode:
|
|
continue
|
|
for devname, (_, desc) in nodedev.setup_info.items():
|
|
secop_module = desc['secop_module']
|
|
meaning = secnode.modules[secop_module]['properties'].get('meaning')
|
|
if meaning:
|
|
meaning_name, importance = meaning
|
|
sample_devices.setdefault(meaning_name, []).append((importance, devname))
|
|
|
|
newenv = {} # to be added to envlist (dict [devname] of aliasname)
|
|
to_remove = set() # items to be removed from previous envlist, if present
|
|
for meaning in self.meanings:
|
|
info = getattr(self, meaning)
|
|
aliasnames = info.get('alias')
|
|
if aliasnames is None:
|
|
aliasnames = []
|
|
elif isinstance(aliasnames, str):
|
|
aliasnames = [aliasnames]
|
|
aliascfg = info.get('targets', {})
|
|
predefined_alias = info.get('predefined_alias')
|
|
if predefined_alias:
|
|
aliases = [a for a in predefined_alias
|
|
if isinstance(session.devices.get(a), DeviceAlias)]
|
|
if aliases:
|
|
if len(aliases) > 1:
|
|
raise TypeError(f'do know to which of {aliases} {meaning} to assign to')
|
|
alias_config = session.alias_config.setdefault(aliases[0], [])
|
|
alias_config.extend(list(aliascfg.items()))
|
|
elif not aliasnames:
|
|
session.log.warn("neither 'predefined_alias' nor 'alias' configured. skip %s", meaning)
|
|
continue
|
|
importance_list = sample_devices.get(meaning, [])
|
|
importance_list.extend([(nr, nam) for nam, nr in aliascfg.items() if nam in session.devices])
|
|
importance_list = sorted(importance_list, reverse=True)
|
|
session.log.debug('%s: %r', meaning, importance_list)
|
|
for _, devname, in importance_list:
|
|
dev = session.devices.get(devname)
|
|
if dev is None or info.get('drivable_only', False) and not isinstance(dev, Moveable):
|
|
continue
|
|
for aliasname in aliasnames:
|
|
devcfg = ('nicos.core.DeviceAlias', {})
|
|
session.configured_devices[aliasname] = devcfg
|
|
session.dynamic_devices[aliasname] = 'frappy' # assign to frappy setup
|
|
aliasdev = previous_aliases.pop(aliasname, None)
|
|
if aliasdev:
|
|
if aliasdev.alias != devname:
|
|
session.log.debug('change alias %r -> %r', aliasname, devname)
|
|
else:
|
|
session.log.debug('create alias %r -> %r', aliasname, devname)
|
|
aliasdev = session.createDevice(aliasname, recreate=True, explicit=True)
|
|
aliasdev.alias = devname
|
|
if aliasnames:
|
|
# only the first item of aliasnames is added to the envlist
|
|
aliasname = aliasnames[0]
|
|
to_remove.add(devname)
|
|
to_remove.add(aliasname)
|
|
if devname not in newenv and info.get('envlist', True):
|
|
# example: when 'temperature' and 'temperature_regulation' are the
|
|
# same device, the first one is kept
|
|
newenv[devname] = aliasname
|
|
break
|
|
else:
|
|
to_remove.union(aliasnames)
|
|
|
|
for aliasname in previous_aliases:
|
|
session.destroyDevice(aliasname)
|
|
session.configured_devices.pop(aliasname, None)
|
|
session.dynamic_devices.pop(aliasname, None)
|
|
|
|
applyAliasConfig() # for other aliases
|
|
|
|
envlist = [k for k in session.experiment.envlist if k not in to_remove] + list(newenv.values())
|
|
if envlist != session.experiment.envlist:
|
|
removed = set(session.experiment.envlist).difference(envlist)
|
|
session.experiment.setEnvironment(envlist)
|
|
if removed:
|
|
session.log.info('removed %s from environment', ', '.join(removed))
|
|
if newenv:
|
|
session.log.info('added %s to environment', ', '.join(newenv.values()))
|
|
|
|
|
|
class FrappyNode(SecNodeDevice, Moveable):
|
|
"""SEC node device
|
|
|
|
with ability to start / restart / stop the frappy server
|
|
"""
|
|
|
|
parameter_overrides = {
|
|
'target': Override(description='configuration for the frappy server or host:port',
|
|
type=str, default=''),
|
|
}
|
|
parameters = {
|
|
'service': Param('frappy service name (main, stick or addons)', type=str, default=''),
|
|
'param_category': Param("category of parameters\n\n"
|
|
"set to 'general' if all parameters should appear in the datafile header",
|
|
type=str, default='', settable=True),
|
|
}
|
|
|
|
_service_manager = FrappyManager()
|
|
_cfgvalue = None
|
|
|
|
def doStart(self, value):
|
|
if value == 'None':
|
|
value = None
|
|
self.restart(value, True) # frappy server will be restarted even when unchanged
|
|
|
|
def doStop(self):
|
|
"""never busy"""
|
|
|
|
def doInit(self, mode):
|
|
if mode != SIMULATION and session.sessiontype != POLLER:
|
|
cfg = self.doRead()
|
|
self.restart(cfg, False) # do not restart when not changed
|
|
super().doInit(mode)
|
|
|
|
def doRead(self, maxage=0):
|
|
try:
|
|
return self.secnode.descriptive_data['_frappy_config']
|
|
except (KeyError, AttributeError):
|
|
pass
|
|
if self._cfgvalue is None and self._cache:
|
|
self._cfgvalue = self._cache.get(self, 'value')
|
|
return self._cfgvalue
|
|
|
|
def createDevices(self):
|
|
super().createDevices()
|
|
if self.param_category:
|
|
for devname, (_, devcfg) in self.setup_info.items():
|
|
params_cfg = devcfg['params_cfg']
|
|
dev = session.devices[devname]
|
|
for pname, pargs in params_cfg.items():
|
|
pinfo = dev.parameters[pname]
|
|
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
|
|
sm = cls._service_manager
|
|
sm.get_info()
|
|
return sm.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 disable(self):
|
|
seaconn = session.devices.get('sea_%s' % self.service)
|
|
if seaconn and seaconn._attached_secnode:
|
|
seaconn.communicate('frappy_remove %s' % self.service)
|
|
self._set_status(*self._status)
|
|
|
|
def _set_status(self, code, text):
|
|
if self.uri == '':
|
|
code, text = status.DISABLED, 'disabled'
|
|
SecNodeDevice._set_status(self, code, text)
|
|
|
|
def restart(self, cfg=None, restart=True):
|
|
"""restart frappy server
|
|
|
|
:param cfg: config for frappy server, if not given, restart with the same config
|
|
:param restart: when false, do not restart when already running with same cfg
|
|
"""
|
|
if cfg is None:
|
|
cfg = self._cfgvalue
|
|
ins = config.instrument
|
|
info = self._service_manager.get_ins_info(ins)
|
|
cfginfo = {}
|
|
self._service_manager.get_procs(cfginfo=cfginfo)
|
|
running_cfg = cfginfo.get((ins, self.service), '')
|
|
if cfg == running_cfg:
|
|
if not restart:
|
|
return
|
|
else:
|
|
self.disable()
|
|
if running_cfg:
|
|
self._disconnect()
|
|
self._service_manager.do_stop(ins, self.service)
|
|
is_cfg = cfg and ':' not in cfg
|
|
if is_cfg:
|
|
available_cfg = self.available_cfg(self.service)
|
|
failed = False
|
|
for cfgitem in cfg.split(','):
|
|
if cfgitem not in available_cfg:
|
|
failed = True
|
|
suggestions = suggest(cfgitem, available_cfg)
|
|
if suggestions:
|
|
session.log.error('%s unknown, did you mean: %s' % (cfgitem, ', '.join(suggestions)))
|
|
if failed:
|
|
raise ValueError('use "frappy_list()" to get a list of valid frappy configurations')
|
|
uri = 'localhost:%d' % info[self.service]
|
|
else:
|
|
uri = cfg
|
|
if uri != self.uri:
|
|
self.uri = '' # disconnect
|
|
if uri:
|
|
if is_cfg:
|
|
self._service_manager.do_start(ins, self.service, cfg, logger=self.log)
|
|
self.uri = uri # connect
|
|
self._cfgvalue = cfg
|
|
if self._cache:
|
|
self._cache.put(self, 'value', cfg)
|
|
self._setROParam('target', cfg)
|
|
|
|
def get_info(self):
|
|
result = self.doRead() or ''
|
|
code, text = self.status()
|
|
if code == status.OK or result == '':
|
|
return result
|
|
if (code, text) == (status.ERROR, 'reconnecting'):
|
|
return '%s (frappy not running)' % result
|
|
return '%s (%s)' % (result, text)
|