# -*- 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 from glob import glob from os.path import join, isdir, basename, expanduser, exists from configparser import ConfigParser from .base import ServiceManager, ServiceDown, UsageError from .seaman import SeaManager def dummy(*args, **kwds): pass class Node: description = '' def __call__(self, equipment_id, description, *args, **kwds): self.description = description 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) for key, value in env.items(): if '' in value: env[key] = value.replace('', service) os.environ[key] = env[key] cfgpaths = self.config_dirs(ins, service) if cfgpaths: env['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['PYTHONPATH'] = os.environ['PYTHONPATH'] = ':'.join(pypathlist) return start_dir, env 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)) # TODO: check if we really need this # if (cfg or '').startswith('~'): # cfg = cfg[1:] # seacfg = self.cfg_from_sea(ins).get(service) # if seacfg: # cfg = seacfg # if not cfg: # return 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 all_cfg(self, ins, service, details=False): """get available cfg files :param ins: instance :param service: service nor None for all services :param details: True: return a dict of of of False: return a set of """ result = {} all_cfg = set() if not ins: return {} namespace = {k: dummy for k in ('Mod', 'Param', 'Command', 'Group')} namespace['Node'] = node = Node() for service in [service] if service else self.services: for cfgdir in self.config_dirs(ins, service): result.setdefault(cfgdir, {}) cfgs = result[cfgdir] root = self.env[ins].get('FRAPPY_ROOT') cfg_pattern ='*_cfg.py' if exists(join(root, 'frappy')) else '*.cfg' for cfgfile in glob(join(cfgdir, cfg_pattern)): desc = '' if details: try: if cfgfile.endswith('.py'): node.description = '' try: with open(cfgfile, encoding='utf-8') as f: exec(f.read(), namespace) except Exception as e: node.description = repr(e) desc = node.description else: parser = ConfigParser() parser.read(cfgfile) for s in parser.sections(): if s == 'NODE' or s.startswith('node '): desc = parser[s].get('description', '').split('\n')[0] break except Exception: pass cfg = basename(cfgfile)[:1-len(cfg_pattern)] if cfg not in all_cfg: all_cfg.add(cfg) cfgs[cfg] = desc return result if details else all_cfg def do_listcfg(self, ins='', service='', prt=print): if not ins: raise UsageError('missing instance') if service: all_cfg = self.all_cfg(ins, service, True) else: all_cfg = {} for service in self.services: all_cfg.update(self.all_cfg(ins, service, True)) for cfgdir, cfgs in all_cfg.items(): if cfgs: prt('') prt('--- %s:' % cfgdir) keylen = max(len(k) for k in cfgs) for cfg, desc in cfgs.items(): prt('%s %s' % (cfg.ljust(keylen), desc)) 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 cfg in self.all_cfg(argdict.get('ins'), argdict.get('service'))): return super().treat_args(argdict, (), unknown) return super().treat_args(argdict, unknown, extra) def cfg_from_sea(self, ins): sea = SeaManager() sea.get_info() cfgs, confirmed = sea.get_cfg(ins, 'sea', True) cfgs = cfgs.split('/') result = {} def check_cfg_file(cfg, allcfg): if cfg is 'none': return '' return cfg if cfg in allcfg else cfg + '?' allmain = self.all_cfg(ins, 'main') allsticks = self.all_cfg(ins, 'stick') if cfgs[0]: result['main'] = check_cfg_file(cfgs[0], allmain) if len(cfgs) > 1: stick = cfgs[1] if stick: if stick not in allsticks and stick in allmain: stick += 'stick' result['stick'] = check_cfg_file(stick, allsticks) addons = [check_cfg_file(a) for a in cfgs[2:]] if addons: result['addons'] = ','.join(addons) result['confirmed'] = confirmed return result