285 lines
9.1 KiB
Python
285 lines
9.1 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', '/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 {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):
|
|
parser = ConfigParser()
|
|
parser.optxform = str
|
|
parser.read([str(filename)])
|
|
parser.read_dict(newvalues)
|
|
with open(filename, 'w') as f:
|
|
parser.write(f)
|
|
change_mode(Path(filename), group='+w')
|
|
|
|
|
|
class ArgError(ValueError):
|
|
pass
|
|
|
|
|
|
class Config:
|
|
this = None
|
|
|
|
def __init__(self):
|
|
configfile = Path('/sq_sw/linse/etc/linsetools.cfg')
|
|
self.sections = read_config(configfile)
|
|
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:
|
|
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 (<target state:bool>, <busy: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: (True, False, 'RUNNING'),
|
|
mj.WARNING: (True, False, 'WARNING'),
|
|
mj.STOPPING: (False, True, '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][1]: # 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
|
|
otherarg = None
|
|
argmap = {k: 'action' for k in ('start', 'restart', 'stop', 'gui', 'cli', 'list', 'listcfg')}
|
|
|
|
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.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
|
|
|
|
def connect(self):
|
|
if self._client is None:
|
|
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?
|
|
|
|
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 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}')
|
|
|
|
@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)
|