# ***************************************************************************** # 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 # # ***************************************************************************** """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"" 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>, , 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(''): 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('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 '' 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