From 9682ccaa704863102677bceff44d7583ee9cd5d8 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Thu, 23 Apr 2026 15:59:30 +0200 Subject: [PATCH] multiple fixes --- __init__.py | 36 ++++++++++ base.py | 169 +++++++++++++++++++++++++++++++++-------------- frappy.py | 184 ++++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 328 insertions(+), 61 deletions(-) diff --git a/__init__.py b/__init__.py index e69de29..1373654 100644 --- a/__init__.py +++ b/__init__.py @@ -0,0 +1,36 @@ +# ***************************************************************************** +# 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 +# ***************************************************************************** + +from configparser import ConfigParser + + +def read_config(filename): + parser = ConfigParser() + parser.optxform = str + parser.read([str(filename)]) + return {k: dict(parser[k].items()) for k in parser.sections()} + + +def write_config(filename, newvalues): + parser = ConfigParser() + parser.optxform = str + parser.read([str(filename)]) + parser.read_dict(newvalues) + with open(filename, 'w') as f: + parser.write(f) diff --git a/base.py b/base.py index 02178a6..0ae9eb1 100644 --- a/base.py +++ b/base.py @@ -28,15 +28,55 @@ DEFAULT_CFG = """[this] instrument={instrument} frappy_ports = 15000-15009 """ -MARCHESRC = ['/home/software/marche'] +MARCHESRC = ['/home/software/marche', '/home/l_samenv/marche'] CFGDIRS = ['/home/linse/config', '/home/l_samenv/linse_config'] - def read_config(filename): parser = ConfigParser() parser.optxform = str parser.read([str(filename)]) - return dict(parser) + return {k: dict(parser[k].items()) for k in parser.sections()} + + +def change_mode(path, *, user=None, group=None, other=None): + # adds given modes + # example change_mode(path, user='+w', group='w', other='-r') + prev = path.stat().st_mode + mask = 0 + bits = 0 + for access in (user, group, other): + mask *= 8 + bits *= 8 + if access is not None: + mode = None + try: + for char in access: + if char in '+-=': + if mode is None: + mode = char + else: + raise ValueError(f'{char} must appear first in mode') + else: + if mode is None: + mode = '=' + bitpos = 'xwr'.index(char) + if mode != '=': + mask |= 1 << bitpos + if mode != '-': + bits |= 1 << bitpos + if mode == '=': + mask |= 7 + except KeyError: + raise ValueError(f'illegal access mode {access!r}') + mode = prev & ~mask | bits + if mode != prev: + path.chmod(mode) + + +def write_content(filename, content, **kwds): + path = Path(filename) + path.write_text(content) + change_mode(path, **kwds) def write_config(filename, newvalues): @@ -46,6 +86,7 @@ def write_config(filename, newvalues): parser.read_dict(newvalues) with open(filename, 'w') as f: parser.write(f) + change_mode(Path(filename), group='+w') class ArgError(ValueError): @@ -53,26 +94,41 @@ class ArgError(ValueError): class Config: + this = None + def __init__(self): - configfile = Path('~/.config/linsetools.cfg').expanduser() + configfile = Path('/sq_sw/linse/etc/linsetools.cfg') self.sections = read_config(configfile) - self.instruments = {} - for key, section in self.sections.items(): - if key == 'this': - host = socket.gethostname().split('.')[0] - section = {'instrument': host, 'frappy_ports': '15000-15009'} - mandatory = set(section) - this = self.sections.get('this', {}) - section.update(this) - self.sections['this'] = section - if set(this) & mandatory != mandatory: - write_config(configfile, self.sections) - self.this = section['instrument'] - self.instruments[self.this] = section - else: - head, _, tail = key.partition('.') - if tail and head == 'instrument': - self.instruments[tail] = section + instconfig = Path('/home/linse/.config/instrument.cfg') + if instconfig.is_file(): + self.instruments = read_config(instconfig) + else: + host = socket.gethostname().split('.')[0] + self.instruments = {'this': {'instrument': host, 'frappy_ports': '15000-15009'}} + try: + write_config(instconfig, self.instruments) + except PermissionError: + pass + this = self.instruments.pop('this', None) + if this: + self.this = this['instrument'] + self.instruments[self.this] = this + + def parse_args(self, argmap, args, other=None): + result = {} + for arg in args: + if arg in self.instruments: + if 'instrument' in result: + raise ArgError('cannot give instrument twice') + result['instrument'] = arg + elif arg in argmap: + kind = argmap.get(arg, other) + if kind is None: + raise ArgError(f'unexpected argument: {arg!r}') + if kind in result: + raise ArgError(f'cannot give {kind} twice') + result[kind] = arg + return result class Logger: @@ -109,14 +165,14 @@ for marchedir in MARCHESRC: break -STATUS_MAP = { # values are (, name) +STATUS_MAP = { # values are (, , name) mj.DEAD: (False, False, 'DEAD'), mj.NOT_RUNNING: (False, False, 'NOT RUNNING'), mj.STARTING: (True, True, 'STARTING'), mj.INITIALIZING: (True, True, 'INITIALIZING'), - mj.RUNNING: (False, True, 'RUNNING'), - mj.WARNING: (False, True, 'WARNING'), - mj.STOPPING: (True, False, 'STOPPING'), + mj.RUNNING: (True, False, 'RUNNING'), + mj.WARNING: (True, False, 'WARNING'), + mj.STOPPING: (False, True, 'STOPPING'), mj.NOT_AVAILABLE: (False, False, 'NOT AVAILABLE'), } @@ -125,7 +181,7 @@ def wait_status(cl, service): delay = 0.2 while True: sts = cl.getServiceStatus(service) - if STATUS_MAP[sts][0]: # busy + if STATUS_MAP[sts][1]: # busy if delay > 1.5: # this happens after about 5 sec return False time.sleep(delay) @@ -136,25 +192,30 @@ def wait_status(cl, service): class MarcheControl: port = 8124 + otherarg = None + argmap = {k: 'action' for k in ('start', 'restart', 'stop', 'gui', 'cli', 'list', 'listcfg')} - def __init__(self, host, port=None, user=None, instrument=None): - self.config = Config() + def __init__(self, instrument=None, host='localhost', port=None, user=None, config=None): + self.config = config or Config() self.host = host self.instance = instrument or 'this' self.instrument = self.config.this if self.instance == 'this' else self.instance - self.ins_config = self.config.instruments[self.insttrument] + self.ins_config = self.config.instruments[self.instrument] self.user = user or self.instrument # SINQ instruments + if not (Path('/home') / self.user).is_dir(): + self.user = 'l_samenv' if port is not None: self.port = port self._client = None - self.argmap = {k: 'action' for k in ('start', 'restart', 'stop', 'gui', 'cli', 'list')} - self.argmap.update((k, 'instrument') for k in self.config.instruments) def connect(self): if self._client is None: - # TODO: may need more generic solution for last arg - print(self.host, self.port, self.user) - self._client = Client(self.host, self.port, self.user, self.user.upper() + 'LNS') + if self.user == 'l_samenv': + x = 1 + arg = f'{2**4+x}lns{x}' + else: + arg = self.user.upper() + 'LNS' + self._client = Client(self.host, self.port, self.user, arg) # TODO; do we need disconnect? @@ -186,26 +247,38 @@ class MarcheControl: self.connect() self._client.reloadJobs() - def _run(self, action, other): + def list(self, service='all', prt=print): + self.connect() + for key, value in self._client.getAllServiceInfo().items(): + if service != 'all' and service != key: + continue + for inst, value in value['instances'].items(): + name = '.'.join([key, inst] if inst else [key]) + state = STATUS_MAP[value['state']][2].lower() + desc = value['desc'] + prt(f'{name:30s} {state:12s} {desc}') + + def do(self, action, other): if action == 'start': self.start(other) elif action == 'restart': self.restart(other) elif action == 'stop': self.stop(other) + elif action == 'list': + self.list() else: raise ArgError(f'unknown action {action!r}') - def run(self, *args): - self._run(self.parse_args(*args)) - - def parse_args(self, *args): - result = {} - for arg in args: - if arg in self.argmap: - kind = self.argmap.get(arg, 'other') - if kind in result: - raise ArgError(f'duplicate {kind}') - result[kind] = arg - return result - + @classmethod + def do_on_ins(cls, args): + config = Config() + argdict = config.parse_args(cls.argmap, args, cls.otherarg) + instrument = argdict.pop('instrument', None) + if instrument is None and argdict.get('action') == 'list': # TODO: make more generic if needed + instruments = list(config.instruments) + else: + instruments = [instrument] + for ins in instruments: + control = cls(ins, config=config) + control.do(**argdict) diff --git a/frappy.py b/frappy.py index 44577e0..f8ec10d 100644 --- a/frappy.py +++ b/frappy.py @@ -1,6 +1,6 @@ import re from pathlib import Path -from .base import MarcheControl, Logger, ArgError +from .base import MarcheControl, Logger, ArgError, write_content WRAPPER_CFG = """interface = '{port}' @@ -10,11 +10,29 @@ overrideNode(interface=interface) WRAPPER_PAT = re.compile(r"interface\s=\s*'(\d*)'\s*\n") -class FrappyMarche(MarcheControl): - services = 'main', 'stick', 'addons' +class Config: + log = None + process_file = None - def __init__(self, instance, host='localhost', port=None, user=None): - super().__init__(host, port, user, instance) + @classmethod + def get(cls, cfgfile): + if not cls.process_file: + import logging + from frappy.config import process_file + from frappy.lib import generalConfig + generalConfig.init() + cls.log = logging.getLogger('linsetools') + cls.process_file = process_file + return cls.process_file(Path(cfgfile), cls.log) + + +class FrappyControl(MarcheControl): + services = {k: 'service' for k in ('main', 'stick', 'addons')} + argmap = dict(MarcheControl.argmap, all='service', **services) + otherarg = 'cfg' + + def __init__(self, instance, host='localhost', port=None, user=None, config=None): + super().__init__(instance, host, port, user, config) superconfig = self.config.sections['superfrappy'] self.instance = instance @@ -28,7 +46,6 @@ class FrappyMarche(MarcheControl): if not Path(self.wrapperdir).is_dir(): raise ValueError(f'{self.wrapperdir} does not exist') self.get_cfg_info() # do we need to update this from time to time? - self.argmap.update((k, 'service') for k in self.services) def get_service(self, instance): return f'frappy.{instance}' if self.instance == 'this' else f'frappy.{self.instrument}-{instance}' @@ -44,6 +61,7 @@ class FrappyMarche(MarcheControl): services = [service] if service else list(self.services) services.append('') + cfgdirs = cfgdirs or self.cfgdirs for servicedir in services: for cfgdir in cfgdirs.split(':'): @@ -61,16 +79,17 @@ class FrappyMarche(MarcheControl): return self.frappy_ports[1] return self.frappy_ports[2:] - def get_local_ports(self): + def get_local_ports(self, run_state=None): + """return dict of (on, busy, cfg)""" self.get_cfg_info() - run_state = self.status('frappy') + run_state = run_state or self.status('frappy') result = {} for host_port, cfg in self.cfg_info.items(): host, port = host_port.split(':') if host == 'localhost': - busy, on, _ = run_state.get(cfg, (0,0,0)) + on, busy, _ = run_state.get(cfg, (0,0,0)) if on or busy: - result.setdefault(port, []).append((on, busy, cfg)) + result.setdefault(int(port), []).append((on, busy, cfg)) return {sorted(v)[-1][-1]: k for k, v in result.items()} def get_port(self, service): @@ -97,11 +116,13 @@ class FrappyMarche(MarcheControl): port = self.get_port(service) wrapper_content = WRAPPER_CFG.format(cfg=str(cfgfile), port=port) cfgname = cfgfile.stem.removesuffix('_cfg') - self.wrapper_file(cfgname).write_text(wrapper_content) + write_content(self.wrapper_file(cfgname), wrapper_content, group='+rw') + self.get_cfg_info() log.info('wrapper %r %r', self.wrapper_file(cfgname), wrapper_content) self.reload() log.info('registered %r', cfgname) + return f'localhost:{port}' def get_cfg_info(self): """get info from wrapper dir""" @@ -120,8 +141,137 @@ class FrappyMarche(MarcheControl): except FileNotFoundError: pass - def _run(self, action, service=None, other=None): - cfg= other + def running(self): + """return a dict of (kind, uri) of running servers""" + run_state = self.status('frappy') + ports = self.get_local_ports(run_state) + result = {} + for cfg, port in ports.items(): + on, busy, _ = run_state.pop(cfg) + if on: + port = ports.get(cfg) + kind = (self.frappy_ports + [port]).index(port) + result[cfg] = kind, f'localhost:{port}' + return result + + def cli(self): + self.get_cfg_info() + from frappy.client.interactive import init, interact + # from frappy.protocol.discovery import scan + # if ins == self.single_ins: + # all_nodes = {} + # for node in nodes: + # host, port = node.split(':') + # if host == 'localhost': + # host = gethostname() + # all_nodes[gethostbyname(host), int(port)] = node + # for a in scan(): + # all_nodes.setdefault((a.address, a.port), f'{a.hostname}:{a.port}') + # nodes = list(all_nodes.values()) + init(*self.cfg_info) + interact(appname=self.instrument) + + def gui(self): + self.get_cfg_info() + nodes = list(self.cfg_info) + import logging + from frappy.gui.qt import QApplication + from frappy.gui.mainwindow import MainWindow + + app = QApplication([]) + args = type('args', (), dict(detailed=True, node=nodes)) + win = MainWindow(args, logging.getLogger('gui')) + win.show() + return app.exec_() + + @staticmethod + def get_cfg_details(cfgfile): + mods = Config.get(cfgfile) + node = mods.pop('node') or {} + sea_cfg = None + for mod, config in mods.items(): + cls = config['cls'] + cls = getattr(cls, '__name__', cls) + if cls.endswith('SeaClient'): + try: + sea_cfg = config['config']['value'] + except KeyError: + sea_cfg = None + return node.get('description', '').strip(), sea_cfg + + 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: get details about relation to sea + :return: set of available cfgs + + implicit results: + self.frappy2sea: a dict of (including extension .config/.stick/.addon) + self.sea2frappy: a dict of set of + self.list_info: dict of of + """ + all_cfg = set() + if not ins: + return {} + if details: + self.frappy2sea = f2s = {} + self.sea2frappy = s2f = {} + self.list_info = list_info = {} + for servicedir in [service] if service else self.services: + for cfgdir in self.cfgdirs.split(':'): + cfgdir = Path(cfgdir) / servicedir + for cfgfile in cfgdir.glob('*_cfg.py'): + cfg = cfgfile.name[:-7] + if details: + try: + desc, sea_cfg = self.get_cfg_details(cfgfile) + except TypeError: + raise + except Exception as e: + sea_cfg = None + desc = repr(e) + if cfg not in all_cfg: + if sea_cfg: + # sea_info.setdefault(sea_cfg, set()).add(cfg) + f2s[cfg] = sea_cfg + s2f.setdefault(sea_cfg, set()).add(cfg) + list_info.setdefault(cfgdir, {})[cfg] = desc.split('\n', 1)[0] + all_cfg.add(cfg) + # if service == 'main': + # sea_info['none.config'] = {''} + return all_cfg + + def listcfg(self, service='', prt=print): + self.all_cfg(self.instrument, service, True) + seacfgpat = re.compile(r'(.*)(\.config|\.stick|\.addon)') + keylen = max((max(len(k) for k in cfgs) for cfgs in self.list_info.values()), default=1) + ambiguous = set() + for cfgdir, cfgs in self.list_info.items(): + if cfgs: + prt('') + prt('--- %s:' % cfgdir) + for cfg, desc in sorted(cfgs.items(), key=lambda v: (v[0].lower(), v)): + seacfg = self.frappy2sea.get(cfg) + if seacfg: + name, ext = seacfgpat.match(seacfg).groups() + if name == cfg or name + 'stick' == cfg: + prefix = '* ' + else: + prefix = f'* ({name}{ext}) ' + if len(self.sea2frappy[seacfg]) > 1: + prefix = '!' + prefix[1:] + ambiguous.add(seacfg) + desc = prefix + desc + prt('%s %s' % (cfg.ljust(keylen), desc)) + prt(' ') + gap = ' ' * keylen + prt(f'{gap} * need sea') + if ambiguous: + prt(f'{gap} ! {len(ambiguous)} ambiguous mappings sea -> frappy') + + def do(self, action=None, service=None, cfg=None): if action == 'start': self.add_frappy_service(service, cfg, None) self.start(cfg) @@ -129,5 +279,13 @@ class FrappyMarche(MarcheControl): self.restart(cfg) elif action == 'stop': self.stop(cfg) + elif action == 'list': + self.list(service or 'frappy') + elif action == 'listcfg': + self.listcfg(service) + elif action == 'gui': + self.gui() + elif action in ('cli', None): + self.cli() else: raise ArgError(f'unknown action {action!r}')