commit dcf5944b8e995926975f41531ca2e394b60036a2 Author: Markus Zolliker Date: Tue Mar 24 08:56:56 2026 +0100 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/base.py b/base.py new file mode 100644 index 0000000..02178a6 --- /dev/null +++ b/base.py @@ -0,0 +1,211 @@ +# ***************************************************************************** +# 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 time +import socket +from pathlib import Path +from configparser import ConfigParser + + +DEFAULT_CFG = """[this] +instrument={instrument} +frappy_ports = 15000-15009 +""" +MARCHESRC = ['/home/software/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) + + +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) + + +class ArgError(ValueError): + pass + + +class Config: + def __init__(self): + configfile = Path('~/.config/linsetools.cfg').expanduser() + 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 + + +class Logger: + loggers = {} + levels = ['error', 'warning', 'info', 'debug'] + + def __new__(cls, level): + log = cls.loggers.get(level) + if log: + print('old') + return log + log = object.__new__(cls) + cls.loggers[level] = log + method = log.show + for lev in log.levels: + setattr(log, lev, method) + print(lev, level) + if lev == level: + method = log.hide + return log + + def show(self, fmt, *args): + print(fmt % args) + + def hide(self, fmt, *args): + pass + + +for marchedir in MARCHESRC: + if Path(marchedir).is_dir(): + sys.path.append(marchedir) + import marche.jobs as mj + from marche.client import Client + break + + +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.NOT_AVAILABLE: (False, False, 'NOT AVAILABLE'), +} + + +def wait_status(cl, service): + delay = 0.2 + while True: + sts = cl.getServiceStatus(service) + if STATUS_MAP[sts][0]: # busy + if delay > 1.5: # this happens after about 5 sec + return False + time.sleep(delay) + delay *= 1.225 + continue + return True + + +class MarcheControl: + port = 8124 + + def __init__(self, host, port=None, user=None, instrument=None): + self.config = 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.user = user or self.instrument # SINQ instruments + 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') + + # TODO; do we need disconnect? + + def get_service(self, instance): + return instance + + def start(self, instance): + self.connect() + self._client.startService(self.get_service(instance)) + + def restart(self, instance): + self.connect() + self._client.restartService(self.get_service(instance)) + + def stop(self, instance): + self.connect() + self._client.stopService(self.get_service(instance)) + + def status(self, service): + """returns a dict of (, , )""" + self.connect() + servdict = self._client.getAllServiceInfo().get(service) + if not servdict: + return {} + statedict = servdict['instances'] + return {k: STATUS_MAP[v['state']] for k, v in statedict.items()} + + def reload(self): + self.connect() + self._client.reloadJobs() + + def _run(self, action, other): + if action == 'start': + self.start(other) + elif action == 'restart': + self.restart(other) + elif action == 'stop': + self.stop(other) + 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 + diff --git a/frappy.py b/frappy.py new file mode 100644 index 0000000..44577e0 --- /dev/null +++ b/frappy.py @@ -0,0 +1,133 @@ +import re +from pathlib import Path +from .base import MarcheControl, Logger, ArgError + + +WRAPPER_CFG = """interface = '{port}' +include({cfg!r}) +overrideNode(interface=interface) +""" +WRAPPER_PAT = re.compile(r"interface\s=\s*'(\d*)'\s*\n") + + +class FrappyMarche(MarcheControl): + services = 'main', 'stick', 'addons' + + def __init__(self, instance, host='localhost', port=None, user=None): + super().__init__(host, port, user, instance) + superconfig = self.config.sections['superfrappy'] + self.instance = instance + + self.wrapperdir = superconfig.pop('wrapperdir') + self.cfgdirs = superconfig.pop('cfgdirs') + frappy_ports = self.ins_config.get('frappy_ports') + ports = frappy_ports.split('-') + self.frappy_ports = list(range(int(ports[0]), int(ports[-1]))) + self.frappy_servers = [f'{host}:{p}' for p in self.frappy_ports] + + 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}' + + def wrapper_file(self, cfg): + return Path(self.wrapperdir) / f'{cfg}_cfg.py' + + def cfg_file(self, cfgdirs, service, cfg): + if '/' in cfg: + return Path(cfg) + cfgpy = f'{cfg}_cfg.py' + tries = [] + + services = [service] if service else list(self.services) + services.append('') + + for servicedir in services: + for cfgdir in cfgdirs.split(':'): + cfgfile = Path(cfgdir) / servicedir / cfgpy + tries.append(cfgfile) + if cfgfile.is_file(): + return servicedir, cfgfile + else: + raise FileNotFoundError(f'can not find {cfgpy} in {tries}') + + def get_std_port(self, service): + if service == 'main': + return self.frappy_ports[0] + if service == 'stick': + return self.frappy_ports[1] + return self.frappy_ports[2:] + + def get_local_ports(self): + self.get_cfg_info() + run_state = 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)) + if on or busy: + result.setdefault(port, []).append((on, busy, cfg)) + return {sorted(v)[-1][-1]: k for k, v in result.items()} + + def get_port(self, service): + if service not in {'main', 'stick', 'addons', 'addon'}: + raise ArgError('illegal service argument') + ports = self.get_std_port(service) + if isinstance(ports, int): + return ports + self.get_cfg_info() + used_ports = self.get_local_ports() + for port in ports: + if port not in used_ports: + return port + raise ValueError('too many frappy servers') + + def add_frappy_service(self, service, cfg, port, log=None): + if log is None: + log = Logger('info') + log.info('add %r port=%r', cfg, port) + service, cfgfile = self.cfg_file(self.cfgdirs, service, cfg) + if not port: + if not service: + raise ArgError('service is not given and can not be determined from cfg file location') + 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) + self.get_cfg_info() + log.info('wrapper %r %r', self.wrapper_file(cfgname), wrapper_content) + self.reload() + log.info('registered %r', cfgname) + + def get_cfg_info(self): + """get info from wrapper dir""" + result = {} + for cfgfile in Path(self.wrapperdir).glob('*_cfg.py'): + cfg = cfgfile.stem[:-4] + match = WRAPPER_PAT.match(cfgfile.read_text()) + if match: + result[f'localhost:{match.group(1)}'] = cfg + self.cfg_info = result + + def delete_frappy_service(self, cfg): + try: + self.wrapper_file(cfg).unlink() + self.reload() + except FileNotFoundError: + pass + + def _run(self, action, service=None, other=None): + cfg= other + if action == 'start': + self.add_frappy_service(service, cfg, None) + self.start(cfg) + elif action == 'restart': + self.restart(cfg) + elif action == 'stop': + self.stop(cfg) + else: + raise ArgError(f'unknown action {action!r}')