# -*- coding: utf-8 -*- # ***************************************************************************** # # 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 # # ***************************************************************************** import sys import os import re import builtins from glob import glob from collections import defaultdict from os.path import join, isdir, basename, expanduser, exists from configparser import ConfigParser from .base import ServiceManager, ServiceDown, UsageError from .seaman import SeaManager class Namespace(dict): def __init__(self): self['Node'] = self.node self['Mod'] = self.mod for fun in 'Param', 'Command', 'Group': self[fun] = self.dummy self.init() def init(self): self.description = '' self.sea_cfg = None def node(self, equipment_id, description, *args, **kwds): self.description = description def mod(self, name, cls, description, config=None, **kwds): cls = getattr(cls, '__name__', cls) if cls.endswith('SeaClient'): self.sea_cfg = config def dummy(self, *args, **kwds): return None __builtins__ = builtins class FrappyManager(ServiceManager): group = 'frappy' services = ('main', 'stick', 'addons') USAGE = """ Usage: frappy list [instance] * frappy start frappy restart [] [] * frappy stop [] * frappy listcfg [ | develop] # list available cfg files is one of main, stick, addons %s * wildcards allowed, using '.' to replace 0 or more arbitrary characters in """ def config_dirs(self, ins, service): cfgpaths = [] cfgparser = ConfigParser() cfgparser.optionxform = str cfgfile = self.env[ins].get('FRAPPY_CONFIG_FILE') confdir = self.env[ins].get('FRAPPY_CONFDIR') if cfgfile: cfgfile = self.env[ins]['FRAPPY_CONFIG_FILE'].replace('', service) cfgparser.read(cfgfile) try: section = cfgparser['FRAPPY'] except KeyError: raise ValueError('%s does not exist or has no FRAPPY section' % cfgfile) confdir = section.get('confdir', confdir) for cfgpath in confdir.split(os.pathsep): if cfgpath.endswith(''): cfgpaths.append(expanduser(cfgpath[:-6] + service)) else: scfg = join(cfgpath, service) if isdir(scfg): cfgpaths.append(scfg) cfgpaths.append(expanduser(cfgpath)) return cfgpaths def prepare_start(self, ins, service, cfg=''): start_dir, env = super().prepare_start(ins, service) env_update = {} for key, value in env.items(): if '' in value: env_update[key] = value.replace('', service) os.environ[key] = env[key] cfgpaths = self.config_dirs(ins, service) if cfgpaths: env_update['FRAPPY_CONFDIR'] = os.pathsep.join(cfgpaths) # merge PYTHONPATH from servicemanager.cfg with the one from environment pypathlist = [] for pypath in env.get('PYTHONPATH'), os.environ.get('PYTHONPATH'): if pypath is not None: pypathlist.extend(p for p in pypath.split(':') if p not in pypathlist) if pypathlist: env_update['PYTHONPATH'] = os.environ['PYTHONPATH'] = ':'.join(pypathlist) return start_dir, dict(env, **env_update) def do_start(self, ins, service=None, cfg='', restart=False, wait=False, logger=None): if self.wildcard(ins) is not None: raise UsageError('no wildcards allowed with %s start' % self.group) if cfg and not service and len(self.services) != 1: raise UsageError('need service to start (one of %s)' % ', '.join(self.services)) super().do_start(ins, service, cfg, restart, wait, logger) def do_restart(self, ins, service=None, cfg=None, logger=None): ins_list = super().do_restart(ins, service, cfg, logger) if ins_list: # wildcard used # determine running processes with cfg cfginfo = {} self.get_procs(None, cfginfo) cfgs = {i for i, s in cfginfo if s == service or service is None} return [i for i in ins_list if i in cfgs] def get_nodes(self, ins='', service=None): start_dir = ServiceManager.prepare_start(self, ins, None)[0] if start_dir not in sys.path: sys.path.insert(0, start_dir) nodes = [] services = self.services if service is None else [service] for service in services: try: self.check_running(ins, service) nodes.append('localhost:%d' % self.info[ins][service]) except ServiceDown: if len(services) == 1: raise UsageError('frappy %s %s is not running' % (ins, service)) except KeyError: if ins: raise UsageError('unknown instance %s' % ins) raise UsageError('missing instance') if not nodes: raise UsageError(f"frappy {ins}: none of {'/'.join(services)} is running") return nodes def do_gui(self, ins='', service=None): print(f'starting frappy gui {ins} {service or ""}') nodes = self.get_nodes(ins, service) import logging from frappy.gui.qt import QApplication from frappy.gui.mainwindow import MainWindow app = QApplication([]) win = MainWindow(nodes, logging.getLogger('gui')) win.show() return app.exec_() def do_cli(self, ins='', service=None): nodes = self.get_nodes(ins, service) from frappy.client.interactive import init, interact init(*nodes) interact() def get_cfg_details(self, namespace, cfgfile): namespace.init() local = {} with open(cfgfile, encoding='utf-8') as f: exec(f.read(), namespace, local) return namespace.description, local.get('sea_cfg', namespace.sea_cfg) def cfg_details(self, ins, service, cfg): namespace = Namespace() for cfgdir in self.config_dirs(ins, service): cfgfile = join(cfgdir, f'{cfg}_cfg.py') if exists(cfgfile): return self.get_cfg_details(namespace, cfgfile) raise FileNotFoundError(f'{cfg} not found') def is_cfg(self, ins, service, cfg): try: self.cfg_details(ins, service, cfg) return True except Exception: return False def all_cfg(self, ins, service, list_info=None, sea_info=None): """get available cfg files :param ins: instance :param service: service nor None for all services :param list_info: None or a dict to collect info in the form dict of of :param sea_info: None or a dict of set() to be populated :return: set of available config """ all_cfg = set() if not ins: return {} namespace = Namespace() details = sea_info is not None or list_info is not None for service in [service] if service else self.services: for cfgdir in self.config_dirs(ins, service): for cfgfile in glob(join(cfgdir, '*_cfg.py')): cfg = basename(cfgfile)[:-7] if details: try: desc, sea_cfg = self.get_cfg_details(namespace, cfgfile) except Exception as e: sea_cfg = None desc = repr(e) if cfg not in all_cfg: if sea_cfg and sea_info is not None: sea_info.setdefault(sea_cfg, set()).add(cfg) all_cfg.add(cfg) if list_info is not None: list_info.setdefault(cfgdir, {})[cfg] = desc.split('\n', 1)[0] return all_cfg def do_listcfg(self, ins='', service='', prt=print): if not ins: raise UsageError('missing instance') list_info = {} sea_info = {} if service: self.all_cfg(ins, service, list_info, sea_info) else: for service in self.services: self.all_cfg(ins, service, list_info, sea_info) sea_ambig = {} # collect info about ambiguous sea info seacfgpat = re.compile(r'(.*)(\.config|\.stick|\.addon)') for seacfg, cfgset in sea_info.items(): name, ext = seacfgpat.match(seacfg).groups() sea_ambig.update({k: (name, ext, len(cfgset)) for k in cfgset}) ambiguous = 0 keylen = max(max(len(k) for k in cfgs) for cfgs in list_info.values()) for cfgdir, cfgs in list_info.items(): if cfgs: prt('') prt('--- %s:' % cfgdir) for cfg, desc in cfgs.items(): if cfg in sea_ambig: name, ext, n = sea_ambig[cfg] if name == cfg or name + 'stick' == cfg: prefix = '* ' else: prefix = f'* ({name}{ext}) ' if n > 1: prefix = '!' + prefix[1:] ambiguous += 1 desc = prefix + desc prt('%s %s' % (cfg.ljust(keylen), desc)) prt(' ') gap = ' ' * keylen prt(f'{gap} * need sea') if ambiguous: print(f'{gap} ! {ambiguous} ambiguous mappings sea -> frappy') def treat_args(self, argdict, unknown=(), extra=()): if len(unknown) == 1: cfg = unknown[0] if cfg == 'develop': argdict['service'] = cfg return super().treat_args(argdict, (), ()) if (',' in cfg or cfg.endswith('.cfg') or self.is_cfg(argdict.get('ins'), argdict.get('service'), cfg)): return super().treat_args(argdict, (), unknown) return super().treat_args(argdict, unknown, extra) def check_cfg_file(self, ins, service, cfg, needsea=False): if cfg is 'none': return '' try: desc, sea_cfg = self.cfg_details(self, ins, service, cfg) if needsea and not sea_cfg: return None return cfg except Exception: return cfg + '?' def guess_cfgs(self, ins, cfgs): sea = SeaManager() seacfgs = sea.get_cfg(ins, 'sea').split('/') sea_info = {} if len(seacfgs) < 2: seacfgs.append('') self.all_cfg(ins, None, sea_info=sea_info) guess = defaultdict(lambda: defaultdict(list)) def check_cfg(service, ext, frappyset, seacfgs): for seacfg in seacfgs: available = sea_info.get(seacfg + ext, ()) if available: for a in available: if a in frappyset: guess[service]['ok'].append(a) break else: if len(available) == 1: available = next(iter(available)) # available is either a string or a set of strings guess[service]['proposed'].append(available) else: guess[service]['missing'].append(seacfg) main = cfgs.get('main') check_cfg('main', '.config', set() if main is None else {main}, {seacfgs[0]}) if len(seacfgs) > 1: stick = cfgs.get('stick') check_cfg('stick', '.stick', set() if stick is None else {stick}, {seacfgs[1]}) if len(seacfgs) > 2: addons = set(cfgs.get('addons').split(',')) check_cfg('addons', '.addons', set(addons), set(seacfgs[2:])) return guess