Files
frappy_sinq/devices.py
Markus Zolliker 3d907bd11c resolve problem with DefuncDevice
renew envlist even when not changed
if not, devices in envlist are not exchanged by new instances.
2025-06-20 13:43:58 +02:00

905 lines
38 KiB
Python

# *****************************************************************************
# 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 threading
import socket
import json
from nicos import config, session
from nicos.core import Override, Param, Moveable, status, POLLER, SIMULATION, DeviceAlias, \
Device, anytype, listof, MASTER
from nicos.devices.secop.devices import SecNodeDevice
from nicos.core.utils import createThread
from nicos.utils.comparestrings import compare
from nicos.devices.secop.devices import get_attaching_devices
from nicos.commands.basic import AddSetup, CreateAllDevices, CreateDevice
from nicos.utils import loggers
from servicemanager import FrappyManager, SeaManager, Reconnect, Keep
SECOP_UDP_PORT = 10767
SERVICES = FrappyManager.services
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 meantime
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
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: '):
addkwd = False
info = []
for srv in SERVICES:
cfglist = all_cfg.get(srv)
if cfglist is None:
addkwd = True
else:
# if cfglist is True:
# cfglist = ['reconnect']
if isinstance(cfglist, str):
cfglist = [cfglist]
cfginfo = ','.join(c if isinstance(c, str)
else f"<one of {', '.join(repr(v) for v in c)}>"
for c in cfglist)
if addkwd:
info.append('%s=%r' % (srv, cfginfo))
else:
info.append(repr(cfginfo))
return f"{prefix}frappy({', '.join(info)})"
def get_frappy_config():
try:
return session.devices['frappy']
except KeyError:
session.log.exception("the frappy device is not available - 'frappy' setup is not loaded")
return None
def send_other_udp(uri, instrument, device=None):
"""inform the feeder about the start of a frappy server"""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
msg = {
'SECoP': 'for_other_node',
'uri': uri,
'instrument': instrument,
}
if device:
msg['device'] = device
msg = json.dumps(msg, ensure_ascii=False, separators=(',', ':')).encode('utf-8')
sock.sendto(msg, ('255.255.255.255', SECOP_UDP_PORT))
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 = [n for n, p in parameters.items() if p.type is anytype]
_update_setup = 'on_frappy'
_back_to_normal = None
_initial_info = None
_shutdown_event = None
_restarting = False
_within_update_setup = False
_current_cfgs = None
_target_cfgs = None
_rebuild_env = None
def doInit(self, mode):
if mode != SIMULATION and session.sessiontype != POLLER:
self._shutdown_event = threading.Event()
for name in self.nodes:
secnode = session.devices.get(name)
if secnode:
secnode.uri = ''
createThread('check frappy and sea servers', self.detect_changes)
def doShutdown(self):
self._shutdown_event.set()
def detect_changes(self):
before_check = before_change = prev_shown = None
cnt = 0
self._back_to_normal = None
while not self._shutdown_event.wait(1):
busy = session.daemon_device._controller.status >= 0
if self._restarting or (busy and cnt < 10):
# do not check while restarting and check only every 10 sec when busy
cnt += 1
continue
if not busy and self._back_to_normal:
session.log.info(' %s', 75*'_')
session.log.info(' ')
session.log.info(' sample environment servers match configuration:')
session.log.info(all_info(self._current_cfgs, ' '))
session.log.info(' %s', 75*'_')
self._back_to_normal = None
cnt = 0
need_change, changes, fm = to_consider = self.to_consider()
if fm.state == before_change:
continue
if not need_change:
if self._back_to_normal == 0:
# we had a change, but are back to normal state
self._back_to_normal = True
continue
if fm.state != before_check:
# must be twice the same
before_check = fm.state
continue
if busy or fm.state == prev_shown:
continue
self._back_to_normal = 0 # indicates that we had a change
self.show(True, to_consider)
prev_shown = fm.state
def to_consider(self, cfgs=None):
"""return info about a proposed changes
:param cfgs: previous configuration
:return: <'need change' flag>, <change dict>, <frappy manager instance>
the information takes into account running frappy and sea servers
and their configuration
side effect: try to reconnect a frappy server which should run with
the target cfg
"""
current_cfgs, target_cfgs = self.check_services()
if cfgs is None:
cfgs = current_cfgs
fm = FrappyManager()
proposed = fm.get_server_state(config.instrument, cfgs)
# if fm.error:
# self.log.error('%s', fm.error)
changes = dict(proposed)
need_change = False
for service in SERVICES:
cfg = changes.get(service) # proposed cfg
running = fm.frappy_cfgs.get(service) # running cfg
if running:
prop = changes.get(service)
if prop:
if prop == target_cfgs.get(service) == running:
secnode = session.devices.get('se_' + service)
if secnode:
if secnode._secnode:
secnode._secnode.connect()
else:
secnode._connect()
else:
changes[service] = ''
need_change = True
cfg = ''
else:
if cfg == '':
changes.pop(service)
cfg = None
if target_cfgs.get(service, '-') not in ('', '-'):
need_change = 1
if cfg and not isinstance(cfg, Keep):
need_change = cfg
if not need_change and all(isinstance(v, Keep) for v in changes.values()):
self._rebuild_env = self.check_envalias()
if self._rebuild_env:
need_change = True
else:
self._rebuild_env = None
return need_change, changes, fm
def check_services(self):
cfgs = {}
targets = {}
for secnodename in self.nodes:
secnode = session.devices.get(secnodename)
if secnode:
cfgs[secnode.service] = secnode.get_info()
targets[secnode.service] = secnode.target
self._current_cfgs = cfgs
self._target_cfgs = targets
return cfgs, targets
def start_services(self, main=None, stick=None, addons=None,):
"""start/stop frappy servers
:param main, stick, addons: cfg for frappy servers, '' to stop, None to keep
for example: start_services(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)
"""
self._restarting = True
try:
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:
try:
seaconn.communicate('frappy_remove %s' % service)
except Exception:
pass
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 and (cfginfo != chkinfo or not isinstance(cfginfo, Reconnect)):
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())):
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 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()
fm = FrappyManager()
ins = config.instrument
fm.get_server_state(ins, new_cfg)
recorders = {}
for service, secnode in secnodes.items():
if services.get(service) and secnode:
cfg = fm.frappy_cfgs.get(service)
seacfg = fm.frappy2sea.get(cfg)
if secnode() and not seacfg:
if cfg:
recorders[service] = f'localhost:{fm.info[ins].get(service, 0)}/{cfg}'
else:
recorders[service] = secnode.uri
secnode._secnode.connect()
if recorders:
try:
fm.sea.sea_recorder(ins, recorders)
except Exception:
pass
self.set_envalias()
for secnode in remove_cfg:
secnode.disable()
finally:
self._restarting = False
return all_cfg
def __call__(self, *args, main=None, stick=None, addons=None, force=False, update=True):
"""(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('reconnect') # reconnect to running frappy servers
"""
self._back_to_normal = None # reset 'back to normal' machanism
stickarg = stick
need_change, changes, fm = to_consider = self.to_consider()
seacfg = fm.sea.get_cfg(config.instrument, 'sea', True).split('/', 1)
confirmed = seacfg[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':
main = self._current_cfgs.get('main')
stick = self._current_cfgs.get('stick')
addons = self._current_cfgs.get('addons')
elif main == 'reconnect':
main = None
elif stick is None: # auto stick
if main == '':
stick = '' # remove stick with main
elif (len(seacfg) < 3 or seacfg[2] == '' # currently no stick
or seacfg[1].lower() != main.lower()): # or main has changed
stickcfg = main + 'stick'
if fm.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
if update and need_change:
self.update()
else:
self.show(False, to_consider)
return
if confirmed and main not in (None, 'restart') and confirmed.lower() != main.lower() 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')
session.log.info(all_info(self.start_services(main, stick, addons)))
def show(self, changes_only=False, consider_result=None):
need_changes, changes, fm = consider_result or self.to_consider()
if need_changes:
session.log.warning('sample environment configuration should be changed:')
elif changes_only:
return
else:
session.log.info('status of sample environment services:')
session.log.info(' %s', 75*'_')
session.log.info(' ')
kwargs = {}
frappy_equals_given = True
rows = [['service', 'sea', 'frappy', 'nicos', 'proposed'], [''] * 5]
for service in SERVICES:
cfg = changes.get(service)
frappy_cfg = fm.frappy_cfgs.get(service, '-')
prev = self._target_cfgs.get(service, '-')
if not (cfg is None or isinstance(cfg, Keep)):
kwargs[service] = str(cfg)
frappy_equals_given = False
elif frappy_cfg != prev:
frappy_equals_given = False
rows.append([service, fm.sea_cfgs.get(service, '-'), frappy_cfg,
prev, '-' if cfg is None else cfg])
try:
wid = [max(len(v) for v in column) for column in zip(*rows)]
except Exception as e:
print(e)
raise
for row in rows:
if not kwargs:
row.pop()
if frappy_equals_given:
row.pop()
session.log.info(' %s', ' '.join(v.ljust(w) for w, v in zip(wid, row)))
if self._rebuild_env:
session.log.info(' %s', self._rebuild_env)
if kwargs:
alternative = f" or {all_info(kwargs, '')}" if kwargs else ''
session.log.info(' ')
session.log.info(' use frappy()%s to configure sample environment', alternative)
session.log.info(' %s', 75*'_')
def update(self, main=None, stick=None, addons=None):
self._back_to_normal = None
if main is None and stick is None and addons is None:
changes = self.to_consider()[1]
else:
changes = {k: Keep(v) for k, v in zip(SERVICES, (main, stick, addons))
if v is not None}
session.log.info(all_info(self.start_services(**changes)))
def get_init_info(self, service):
"""check whether a connect of this service is required"""
if self._initial_info is None:
# we do this only once for all services
fm = FrappyManager()
running = fm.get_cfg(config.instrument, None)
cache = self._getCache()
cfgs = {}
for serv, secnode in zip(fm.services, self.nodes):
if cache:
cfg = cache.get(secnode, 'previous_config', '')
if cfg:
cfgs[serv] = cfg
self._initial_info = {s: (cfgs.get(s, '-'), running.get(s)) for s in fm.services}
fm.get_server_state(config.instrument, cfgs)
return self._initial_info[service]
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 needed_envalias(self):
"""create aliases and envlist for SECoP devices
depending on their meaning
"""
nodedevs = filter(None, [session.devices.get(devname) for devname in self.nodes])
# value to add to the importance given in meanings targets list
add_importance = {k: 10 * i for i, k in enumerate(self.nodes)}
sample_devices = {k: [] for k in self.meanings}
for nodedev in nodedevs:
secnode = nodedev._secnode
if not secnode:
continue
for devname, (_, desc) in nodedev.setup_info.items():
secop_module = desc['secop_module']
try:
meaning = secnode.modules[secop_module]['properties'].get('meaning')
except KeyError:
meaning = None
if meaning:
meaning_name, importance = meaning
targetlist = sample_devices.get(meaning_name)
if targetlist is None:
session.log.warning('%s: meaning %r is unknown', devname, meaning_name)
continue
sample_devices[meaning_name].append((importance, devname))
head, _, tail = meaning_name.partition('_')
if tail == 'regulation':
# add temperature_regulation to temperature list, with very low importance
sample_devices[head].append((importance - 100, devname))
elif not tail:
reg = f'{meaning_name}_regulation'
if reg in sample_devices and isinstance(session.devices.get(devname), Moveable):
sample_devices[reg].append((importance, devname))
previous_aliases = self.get_se_aliases()
newenv = {} # to be added to envlist (dict [devname] of aliasname)
to_remove = set() # items to be removed or replaced, if present
needed_aliases = {}
predef_aliases = []
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 = {k: v - i * 0.01 for i, (k, v) in enumerate(info.get('targets', {}).items())}
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')
predef_aliases.append((aliases[0], 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, [])
for devname, nr in aliascfg.items():
dev = session.devices.get(devname)
if dev:
# increase importance by 10 for stick or 20 for addons, in case the device is
# living on the stick/addons node
nr += add_importance.get(str(getattr(dev, 'secnode', '')), 0)
importance_list.append((nr, devname))
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
# determine aliases
for aliasname in aliasnames:
if aliasname not in needed_aliases:
needed_aliases[aliasname] = devname
# determine envlist
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.update(aliasnames)
# determine aliases to be changed
for aliasname, dev in previous_aliases.items():
target = needed_aliases.get(aliasname)
if target:
if dev.alias == target:
needed_aliases.pop(aliasname)
else:
needed_aliases[aliasname] = None
# build new env list. keep order as before
addedenv = [v for v in newenv.values()]
to_remove = to_remove.difference(addedenv)
prevenv = [k for k in session.experiment.envlist if k not in to_remove]
envlist = prevenv + [k for k in addedenv if k not in prevenv]
predef_changes = []
for aliasname, cfg in predef_aliases:
if cfg and cfg[0] not in session.alias_config.get(aliasname, []):
predef_changes.append((aliasname, cfg))
return envlist, needed_aliases, predef_changes
def check_envalias(self):
envlist, new_aliases, predef_aliases = self.needed_envalias()
if set(envlist) != set(session.experiment.envlist):
return f"envlist should be {', '.join(envlist)}"
anew = [k for k, v in new_aliases.items() if v is not None]
removed = set(new_aliases).difference(anew)
anew.extend([k for k, _ in predef_aliases])
if anew:
return f"aliases {', '.join(anew)} should change"
if removed:
return f"aliases {', '.join(anew)} should be removed"
return None
def set_envalias(self):
"""create aliases and envlist for SECoP devices
depending on their meaning (and name)
"""
envlist, new_aliases, predef_aliases = self.needed_envalias()
if new_aliases or predef_aliases:
for aliasname, devname in new_aliases.items():
if devname is None:
session.destroyDevice(aliasname)
session.configured_devices.pop(aliasname, None)
session.dynamic_devices.pop(aliasname, None)
else:
dev = session.devices.get(devname)
devcfg = ('nicos.core.DeviceAlias', {})
session.configured_devices[aliasname] = devcfg
session.dynamic_devices[aliasname] = 'frappy' # assign to frappy setup
adev = session.devices.get(aliasname)
if adev:
session.log.debug('change alias %r -> %r', aliasname, devname)
else:
session.log.debug('create alias %r -> %r', aliasname, devname)
session.cache.put(aliasname, 'visibility', dev.visibility)
session.cache.put(aliasname, 'loglevel', dev.loglevel)
session.cache.put(aliasname, 'description', dev.description)
adev = session.createDevice(aliasname, recreate=True, explicit=True)
adev.alias = devname
# make sure device panel is updated
try:
session.cache.put(devname, 'status', dev.status())
session.cache.put(devname, 'value', dev.read())
except Exception:
pass
for aliasname, cfg in predef_aliases:
alias_config = session.alias_config.setdefault(aliasname, [])
if cfg not in alias_config:
alias_config.extend(cfg)
applyAliasConfig() # for other aliases
prev = set(session.experiment.envlist)
remove = ', '.join(prev.difference(envlist))
session.experiment.setEnvironment(envlist)
show = []
keep = ', '.join(d for d in envlist if d in prev)
if keep:
show.append(f'keep {keep}')
add = ', '.join(d for d in envlist if d not in prev)
if add:
show.append(f'add {add}')
if remove:
show.append(f'remove {remove}')
session.log.info('environment: %s', '; '.join(show))
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),
'quiet_init': Param('flag to set loglevel to error while initializing',
type=bool, default=True, settable=True)
}
_cfgvalue = None
_lastcfg = None
__log_recording = ()
def doStart(self, value):
if value in ('-', 'None'):
value = None
self.restart(value)
def doInit(self, mode):
if mode != SIMULATION and session.sessiontype != POLLER:
fc = session.devices.get('frappy')
if fc:
cfg, running = fc.get_init_info(self.service)
self._cfgvalue = running
self._setROParam('target', cfg)
if cfg and (':' not in cfg and cfg != running):
self._set_status(status.ERROR, 'cfg changed')
return
super().doInit(mode)
def doStop(self):
"""never busy"""
def doRead(self, maxage=0):
return self._cfgvalue or ''
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
def makeDynamicDevices(self, setup_info):
patched_loggers = {}
if self.quiet_init:
for devname, (_, devcfg) in setup_info.items():
log = session.getLogger(devname)
if log not in patched_loggers:
result = [loggers.INFO] # default level
patched_loggers[log] = result
log.setLevel(loggers.ERROR)
# avoid level change when the loglevel parameter is treated
# store level instead in result
log.__dict__['setLevel'] = result.append
try:
super().makeDynamicDevices(setup_info)
finally:
for log, result in patched_loggers.items():
log.__dict__.pop('setLevel', None) # re-enable setLevel
log.setLevel(result[-1]) # set to stored or default value
def showInitLog(self):
for devname, record in self.__log_recording:
session.getLogger(devname).handle(record)
self.__log_recording = ()
def nodeStateChange(self, online, state):
if online:
super().nodeStateChange(online, state)
if self._cfgvalue is None:
running_cfg = FrappyManager().get_cfg(config.instrument, self.service)
if running_cfg:
fc = session.devices.get('frappy')
if running_cfg != self.target and fc and not fc._restarting:
self.log.warning(f'server info {running_cfg!r} does not match target cfg {self.target!r}')
self._cfgvalue = running_cfg
else:
self._cfgvalue = self.uri
else:
if self.target == self._cfgvalue:
# SecNodeDevice.nodeStateChange will change status to 'reconnecting'
super().nodeStateChange(online, state)
else:
# do not reconnect
self._cfgvalue = None
def descriptiveDataChange(self, module, description):
running_cfg = FrappyManager().get_cfg(config.instrument, self.service)
if not running_cfg or running_cfg == self.target:
super().descriptiveDataChange(module, description)
else:
self._disconnect(keeptarget=True)
self._cfgvalue = running_cfg
self._set_status(status.ERROR, 'cfg changed')
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 frappy server
:param cfg: config for frappy server, if not given, restart with the same config
"""
if cfg is None:
cfg = self._cfgvalue
if cfg is None:
self.log.error('can not restart - previous cfg unknown')
return
ins = config.instrument
fm = FrappyManager()
info = fm.get_ins_info(ins)
running_cfg = fm.get_cfg(ins, self.service) or ''
if cfg != running_cfg:
self.disable()
if running_cfg:
self._disconnect(keeptarget=not cfg)
session.log.info('stop frappy_%s %r %r', self.service, running_cfg, cfg)
fm.do_stop(ins, self.service)
self._setROParam('target', cfg)
try:
is_cfg = cfg and ':' not in cfg
if is_cfg:
if cfg == 'reconnect':
is_cfg = False
else:
available_cfg = None
for cfgitem in cfg.split(','):
if not fm.is_cfg(config.instrument, self.service, cfgitem):
if available_cfg is None:
available_cfg = fm.all_cfg(config.instrument, self.service)
suggestions = suggest(cfgitem, available_cfg)
if suggestions:
session.log.error('%s unknown, did you mean: %s' % (cfgitem, ', '.join(suggestions)))
if available_cfg is not None:
raise ValueError('use "frappy_list()" to get a list of valid frappy configurations')
uri = 'localhost:%d' % info[self.service]
send_other_udp(uri, config.instrument, device=cfg)
else:
uri = cfg
send_other_udp(uri, config.instrument)
if uri != self.uri:
self._disconnect(keeptarget=True)
if uri:
if is_cfg:
session.log.info('start frappy_%s', self.service)
fm.do_start(ins, self.service, cfg, logger=self.log)
self.uri = uri # connect (or disconnect)
self._cfgvalue = cfg
if self._cache:
self._cache.put(self, 'value', cfg)
finally:
self._cache.put(self, 'previous_config', self._cfgvalue or self.uri)
def _disconnect(self, keeptarget=False):
super()._disconnect()
if not keeptarget:
self._setROParam('target', '')
def get_info(self):
result = self.doRead() or ''
code, text = self.status()
if not result and self.target not in ('', '-'):
return '<disconnected>'
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)
def doFinish(self):
return False # avoid warning in finish() when target does not match