Files
linsetools/base.py
2026-03-24 08:56:56 +01:00

212 lines
6.5 KiB
Python

# *****************************************************************************
# 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 <markus.zolliker@psi.ch>
# *****************************************************************************
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 (<busy: bool), <target state:bool>, 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 <service> of (<busy>, <running (now or soon)>, <state name>)"""
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