diff --git a/README.md b/README.md index f3b936c..0ffd06a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # servman -A manager for starting, stopping and listing services/servers like frappy, nicos and sea. \ No newline at end of file +A manager for starting, stopping and listing services/servers like frappy, nicos and sea. + +Several instances of nicos, frappy and sea might run on the same machine. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..79e43bf --- /dev/null +++ b/__init__.py @@ -0,0 +1,394 @@ +# -*- 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 +# +# ***************************************************************************** +"""start/stop/list of services + +this code is currently used: + +- from NICOS to start and stop frappy servers +- from a script allowing to start/stop/list (and more) multiple frappy and nicos servers +""" +import json +import os +from os.path import expanduser +import subprocess +import time +import re +import socket +import psutil +from collections import OrderedDict, defaultdict +from configparser import ConfigParser + + +class ServiceDown(Exception): + """the service is not running""" + + +class UsageError(Exception): + pass + + +def printTable(headers, items, printfunc, minlen=0, rjust=False): + """Print tabular information nicely formatted. + + accept less columns for some rows. The last item of such a row + may extend over columns. stolen from nicos.utils and modifed. + """ + if not headers and not items: + return + ncolumns = len(headers or items[0]) + rowlens = [minlen] * ncolumns + for row in [headers or []] + items: + # do not consider length of last column when row has less columns + cntrow = row if len(row) == ncolumns else row[:-1] + for i, item in enumerate(cntrow): + rowlens[i] = max(rowlens[i], len(item)) + rfmtstr = ('%%%ds ' * ncolumns) % tuple(rowlens) + lfmtstr = ('%%-%ds ' * ncolumns) % tuple(rowlens) + if headers: + printfunc(lfmtstr % tuple(headers)) + printfunc(lfmtstr % tuple('=' * n for n in rowlens)) + for row in items: + printfunc((rfmtstr if rjust else lfmtstr) % (tuple(row) + ('',) * (ncolumns - len(row)))) + + +def run(serv, arglist): + arglist = arglist + [''] # add dummy argument + action = arglist.pop(0) if hasattr(serv, 'do_' + arglist[0]) else 'gui' + instance = arglist.pop(0) if arglist[0] and arglist[0] not in serv.services else None + if instance is None and len(serv.info) == 1: + instance = list(serv.info)[0] + if instance is not None: + arglist.insert(0, instance) + arglist.pop() # remove dummy argument + try: + serv.action(action, *arglist) + except AttributeError as e: + raise + print(repr(e)) + raise ValueError("do not know '%s'" % ' '.join([serv.group, action] + arglist)) + + +class ServiceManager: + services = None + need_cfg = False + start_dir = None + group = None + all = {} # for the list command, we want to register all service managers + virtualenv = None + pkg = '' + + def __init__(self): + self.env = {} + self.commands = {} + self.revcmd = {} + self.info = {} + self.all[self.group] = self + #prog, args = self.command.split(None, 1) + #self.cmdpat = re.compile('.* ' + # do not match prog, as it might be modified by the os + # (args % dict(ins=r'(?P\S*)', serv=r'(?P\S*)', + # cfg=r'(?P\S*)', port=r'\S*', pkg=r'\S*'))) + self.get_info() + self.stopped = defaultdict(dict) + + def get_services(self, section): + ports = {} + nr = '%02d' % int(section[self.group]) + gr = self.group.upper() + for service in self.services: + sv = '%s_%s' % (gr, service.upper()) + port = section.get('%s_PORT' % sv) + if port or json.loads(section.get(sv, '0').lower()): # e.g. NICOS_POLLER = True leads to port = 0 + ports[service] = int((port or '0').replace('nr', nr)) + return ports + + def get_info(self): + """returns port numbers,commands and environment variables + + the result is a dict[] of dict(ins=.., port= ..., cmd= ...) + if ins is omitted, return a list of above for all ins + """ + result = OrderedDict() + parser = ConfigParser(interpolation=None) + parser.optionxform = str + parser.read(expanduser('servman.cfg')) + defaults = parser['DEFAULT'] + self.commands = {} + self.revcmd = {} + for ins in parser.sections(): + section = dict(parser[ins]) + command = section.get('%s_command' % self.group) + self.revcmd[command] = self.group + if self.group in section: + self.commands[ins] = command + services = self.get_services(parser[ins]) + env = {k: expanduser(section.get(k)) for k in defaults if k.isupper()} + result[ins] = services + self.env[ins] = env + self.info = result + + def get_cmdpats(self, groups): + return self.cmdpats + + def get_ins_info(self, ins): + self.get_info() + return self.info[ins] + + def get_cfg(self, cmd): + """return info about running program, if relevant + + example for frappy: return cfg + """ + return '' + + def get_procs(self, groups=None): + """return processes + + result is a dict[ins] of dict[service] of list of tuples (process, cfg) + """ + + result = {} + cmdpatterns = [] + if groups is None: + groups = [self.group] + for cmd, group in self.revcmd.items(): + if group not in groups: + continue + args = cmd.split(None, 1)[1] + cmdpatterns.append( + re.compile('.* ' + # do not match prog, as it might be modified by the os + (args % dict(ins=r'(?P\S*)', + serv=r'(?P\S*)', + cfg=r'(?P\S*)', + port=r'\S*', + pkg=r'\S*')))) + for p in psutil.process_iter(attrs=['pid', 'cmdline']): + cmdline = p.info['cmdline'] + if cmdline: + cmd = ' '.join(cmdline) + for cmdpat in cmdpatterns: + match = cmdpat.match(cmd) + if match: + gdict = match.groupdict() + ins = gdict['ins'] + serv = gdict['serv'] + result.setdefault(ins, {}).setdefault(serv, []).append(p) + return result + + def check_running(self, ins, service): + self.get_info() + if ins not in self.info: + raise KeyError("don't know %r" % ins) + if not self.get_procs().get(ins, {}).get(service): + raise ServiceDown('%s %s is not running' % (service, ins)) + + def stop(self, ins, service=None): + """stop service (or all services) of instance + + return a dict[][] of for all stopped processes + this information may be used for restarts + """ + procs = self.get_procs() + done = False + services = self.services if service is None else [service] + for service in reversed(services): + for p in procs.get(ins, {}).get(service, []): + print_wait = True + for action in ('terminate', 'kill'): + getattr(p, action)() # p.terminate or p.kill + for i in range(10): # total 0.1 * 10 * 9 / 2 = 4.5 sec + try: + p.wait(0.1 * i) + except psutil.TimeoutExpired: + if p.status() == psutil.STATUS_ZOMBIE: + break + if print_wait and i > 4: + print('wait for %s %s' % (ins, service)) + print_wait = False + continue + self.stopped[ins][service] = ' '.join(p.info['cmdline']) + done = True + break + else: + if action == 'kill': + action = 'kill fail' + break + continue + break + print('%s %s %s' % (ins, service, (action + 'ed').replace('eed', 'ed'))) + return done + + def do_stop(self, ins, service=None, *args): + self.get_info() + if not self.stop(ins, service): + print('nothing to stop') + + def prepare_start(self, ins): + if ins not in self.env: + self.get_info() + gr = self.group.upper() + env = self.env[ins] + return env.get('%s_ROOT' % gr, ''), env + + def do_start(self, ins, service=None, cfg='', restart=False, wait=False): + if ins is None: + print('nothing to start') + return + try: + service_ports = self.get_ins_info(ins) + except ValueError: + raise ValueError('do not know %r' % ins) + services = list(service_ports) if service is None else [service] + if restart: + self.stop(ins, service) + else: + procs = self.get_procs() + to_start = [] + for service in services: + n = len(procs.get(ins, {}).get(service, [])) + if n == 0: + to_start.append(service) + else: + count = '' if n == 1 else ' %sx' % n + print('%s %s is already running%s' % (ins, service, count)) + services = to_start + for service in services: + port = service_ports[service] + cmd = self.commands[ins] % dict(ins=ins, serv=service, port=port, cfg=cfg, pkg=self.pkg) + if '%(cfg)s' in self.commands[ins] and not cfg: + cmd = self.stopped[ins].get(service) + if not cmd: + raise ValueError('missing cfg for %s %s' % (ins, service)) + wd = os.getcwd() + try: + start_dir, env = self.prepare_start(ins) + env = dict(os.environ, **env) + os.chdir(start_dir) + if wait: + proc = subprocess.Popen(cmd.split(), env=env) + proc.wait() + return + process = subprocess.Popen(cmd.split(), env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + if not port: + print('%s %s started' % (ins, service)) + continue + + print_wait = True + for i in range(10): # total 10 * 9 / 2 = 4.5 sec + returnvalue = process.poll() + if returnvalue is not None: + print('started process finished with %r' % returnvalue) + process = subprocess.Popen(cmd.split(), env=env, stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE) + try: + _, erroutput = process.communicate(timeout=10) + except subprocess.TimeoutExpired: + process.kill() + _, erroutput = process.communicate() + print(erroutput.decode()) + print('%s %s died' % (ins, service)) + break + try: + if print_wait and i > 4: + print('wait for port %s' % port) + print_wait = False + s = socket.create_connection(('localhost', port), timeout=5) + s.close() + except socket.error: + time.sleep(0.1 * i) + continue + if restart: + print('%s %s restarted' % (ins, service)) + else: + print('%s %s started' % (ins, service)) + break + else: + print(cmd) + print('starting %s %s failed' % (ins, service)) + finally: + os.chdir(wd) + + def do_restart(self, ins, service=None, cfg=None): + self.do_start(ins, service, cfg, True) + + def do_run(self, ins, service, cfg=None): + """for tests: run and wait""" + self.do_start(ins, service, cfg, wait=True) + + def do_list(self, ins=None, *args): + """info about running services""" + procs = self.get_procs(self.all) + rows = [] + merged = OrderedDict() + show_unused = ins == 'all' + if show_unused: + ins = None + for group, sm in self.all.items(): + sm.get_info() + for ins_i, info_dict in sm.info.items(): + if ins is not None and ins != ins_i: + continue + for serv, port in info_dict.items(): + if ins_i not in merged: + merged[ins_i] = {g: {} for g in self.all} + merged[ins_i][group][serv] = port + for ins_i, info_dict in merged.items(): + show_ins = show_unused + run_info = [[''], [ins_i]] + procs_dict = procs.get(ins_i, {}) + for group, sm in self.all.items(): + info_grp = info_dict.get(group, {}) + for serv, port in info_grp.items(): + plist = procs_dict.get(serv) + if plist: + if sm == self: + show_ins = True + cfg = sm.get_cfg(' '.join(plist[0].info['cmdline'])) + gs = '%s %s' % (group, serv) + port = str(port or '') + run_info.append(('', gs, port, cfg)) + if len(plist) > 1: + rows.append(['', ' WARNING: multiple processes %s' + % ', '.join(str(p.pid) for p, _ in plist)]) + extra = sm.extra_info(ins_i) + if extra and run_info: + run_info.append(['', extra]) + if show_ins: + rows.extend(run_info) + print('') + printTable(('inst', 'service', 'port', 'cfg'), rows, print) + + @staticmethod + def extra_info(ins): + """provide extra info or None""" + return None + + def action(self, action, *args): + method = getattr(self, 'do_' + action, None) + if not callable(method): + raise UsageError('%s is no valid action' % action) + try: + method(*args) + except TypeError as e: + errtxt = str(e) + if ' do_%s(' % action in errtxt and 'argument' in errtxt: + raise UsageError(errtxt) + raise diff --git a/bin/frappy b/bin/frappy new file mode 100755 index 0000000..4f529a2 --- /dev/null +++ b/bin/frappy @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# -*- 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 +from os.path import join, abspath + +sys.path.insert(1, abspath(join(__file__, '../../..'))) + +from servman.frappy import FrappyManager +from servman.nicos import NicosManager +from servman.sea import SeaManager +from servman import run + +NicosManager() +serv = FrappyManager() +SeaManager() + +USAGE = """ +Usage: + + frappy list [] + frappy start + frappy restart [] [] + frappy stop [] + + is one of main, stick, addons + is one of %s +""" % ', '.join(serv.info) + +try: + run(serv, sys.argv[1:]) + +except Exception as e: + print(repr(e)) + print(''.join(USAGE)) diff --git a/bin/nicos b/bin/nicos new file mode 100755 index 0000000..0b9b42b --- /dev/null +++ b/bin/nicos @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- 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 os +from os.path import join, abspath, expanduser +import sys + +#nicos_root = os.environ.get('NICOS_ROOT', expanduser('~/nicos')) +#pkg_dir = abspath(join(nicos_root, 'nicos_linse')) +#sys.path.insert(1, join(pkg_dir, 'common')) + +sys.path.insert(1, abspath(join(__file__, '../../..'))) + +from servman.frappy import FrappyManager +from servman.nicos import NicosManager +from servman.sea import SeaManager +from servman import run, UsageError + +serv = NicosManager() +FrappyManager() +SeaManager() + + +USAGE = """ +Usage: + + nicos gui + nicos (the same as above) + nicos list [] + nicos start [] + nicos restart [] + nicos stop [] + nicos create + nicos create all + nicos link (create links to nicos data and scripts) + + is one of main, stick, addons + is one of %s + +to be done after the experiment: + nicos copy (copy data and scripts from link) + nicos copy [ [/]] (copy specific data) + +""" % ', '.join(serv.info) + +try: + run(serv, sys.argv[1:]) + +except UsageError as e: + print(repr(e)) + print(''.join(USAGE)) diff --git a/bin/nicos-cache b/bin/nicos-cache new file mode 100755 index 0000000..46bbe2b --- /dev/null +++ b/bin/nicos-cache @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# ***************************************************************************** +# NICOS, the Networked Instrument Control System of the MLZ +# Copyright (c) 2009-2019 by the NICOS contributors (see AUTHORS) +# +# 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: +# Georg Brandl +# Markus Zolliker +# +# ***************************************************************************** + +from __future__ import absolute_import, division, print_function + +import argparse +import sys +from os import path, environ + +sys.path.insert(0, path.dirname(path.dirname(path.dirname(path.realpath(__file__))))) + +parser = argparse.ArgumentParser() +parser.add_argument('-d', '--daemon', dest='daemon', action='store_true', + help='daemonize the cache process') +parser.add_argument('-D', '--systemd', dest='daemon', action='store_const', + const='systemd', help='run in systemd service mode') +parser.add_argument('-S', '--setup', action='store', dest='setupname', + default='cache', + help="name of the setup, default is 'cache'") +parser.add_argument('--clear', dest='clear', action='store_true', + default=False, + help='clear the whole cache') +parser.add_argument('-I', '--instrument', action='store', + type=str, default='', + help='instrument as .\n', ) +parser.add_argument('args', nargs=argparse.REMAINDER, help=argparse.SUPPRESS) + +opts = parser.parse_args() + +if opts.clear: + opts.args.append('clear') + +if opts.instrument: + environ['INSTRUMENT'] = opts.instrument + +from nicos.core.sessions.simple import NoninteractiveSession + +NoninteractiveSession.run(opts.setupname, 'Server', setupname=opts.setupname, + daemon=opts.daemon, start_args=opts.args) diff --git a/bin/nicos-daemon b/bin/nicos-daemon new file mode 100755 index 0000000..6b918c1 --- /dev/null +++ b/bin/nicos-daemon @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# ***************************************************************************** +# NICOS, the Networked Instrument Control System of the MLZ +# Copyright (c) 2009-2019 by the NICOS contributors (see AUTHORS) +# +# 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: +# Georg Brandl +# Markus Zolliker +# +# ***************************************************************************** + + +from __future__ import absolute_import, division, print_function + +import argparse +import sys +from os import path, environ + +sys.path.insert(0, path.dirname(path.dirname(path.dirname(path.realpath(__file__))))) + +parser = argparse.ArgumentParser() +parser.add_argument('-d', '--daemon', dest='daemon', action='store_true', + help='daemonize the daemon process') +parser.add_argument('-D', '--systemd', dest='daemon', action='store_const', + const='systemd', help='run in systemd service mode') +parser.add_argument('-S', '--setup', action='store', dest='setupname', + default='daemon', + help="name of the setup, default is 'daemon'") +parser.add_argument('-I', '--instrument', action='store', + type=str, default='', + help='instrument as .\n', ) +opts = parser.parse_args() + +if opts.instrument: + environ['INSTRUMENT'] = opts.instrument + +from nicos.services.daemon.session import DaemonSession + +DaemonSession.run(opts.setupname, 'Daemon', daemon=opts.daemon) diff --git a/bin/nicos-poller b/bin/nicos-poller new file mode 100755 index 0000000..68be43b --- /dev/null +++ b/bin/nicos-poller @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# ***************************************************************************** +# NICOS, the Networked Instrument Control System of the MLZ +# Copyright (c) 2009-2019 by the NICOS contributors (see AUTHORS) +# +# 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: +# Georg Brandl +# Markus Zolliker +# +# ***************************************************************************** + +from __future__ import absolute_import, division, print_function + +import argparse +import sys +from os import path, environ + +sys.path.insert(0, path.dirname(path.dirname(path.dirname(path.realpath(__file__))))) + +parser = argparse.ArgumentParser() +parser.add_argument('-d', '--daemon', dest='daemon', action='store_true', + help='daemonize the poller processes') +parser.add_argument('-D', '--systemd', dest='daemon', action='store_const', + const='systemd', help='run in systemd service mode') +parser.add_argument('-S', '--setup', action='store', dest='setupname', + default='poller', + help="name of the setup, default is 'poller'") +parser.add_argument('-I', '--instrument', action='store', + type=str, default='', + help='instrument as .\n', ) +parser.add_argument('setup', nargs=argparse.OPTIONAL, help=argparse.SUPPRESS) + +opts = parser.parse_args() + +if opts.setup: + appname = 'poller-' + opts.setup + args = [opts.setup] +else: + appname = 'poller' + args = [] + +if opts.instrument: + environ['INSTRUMENT'] = opts.instrument + +from nicos.services.poller.psession import PollerSession + +PollerSession.run(appname, setupname=opts.setupname, maindevname='Poller', + start_args=args, daemon=opts.daemon) \ No newline at end of file diff --git a/bin/sea b/bin/sea new file mode 100755 index 0000000..481d60c --- /dev/null +++ b/bin/sea @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- 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 os +from os.path import join, abspath, split, dirname, expanduser +import sys + +nicos_root = os.environ.get('NICOS_ROOT', expanduser('~/nicos')) +pkg_dir = abspath(join(nicos_root, 'nicos_linse')) +sys.path.insert(1, join(pkg_dir, 'common')) + +from clitools import FrappyManager, NicosManager, SeaManager, run + +NicosManager(pkg_dir) +FrappyManager(pkg_dir) +serv = SeaManager() + +USAGE = """ +Usage: + + sea gui + sea # the same as sea gui + sea cli (the same as old seacmd) + sea start + sea restart [] + sea stop [] + sea list [] + + is one of main, stick, addons + is one of %s +""" % ', '.join(serv.info) + +try: + run(serv, sys.argv[1:]) + +except Exception as e: + print(repr(e)) + print(''.join(USAGE)) diff --git a/frappy.py b/frappy.py new file mode 100644 index 0000000..53a749f --- /dev/null +++ b/frappy.py @@ -0,0 +1,35 @@ +# -*- 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 +# +# ***************************************************************************** + +from servman import ServiceManager + + +class FrappyManager(ServiceManager): + group = 'frappy' + services = ('main', 'stick', 'addons') + + def get_cfg(self, cmd): + if cmd: + match = self.cmdpat.match(cmd) + if match: + return match.groupdict().get('cfg') + return '' diff --git a/nicos.py b/nicos.py new file mode 100644 index 0000000..c64fc41 --- /dev/null +++ b/nicos.py @@ -0,0 +1,255 @@ +# -*- 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 os +import sys +import shutil +from glob import glob +from os.path import join, abspath, dirname, expanduser, exists, islink +from configparser import ConfigParser +from servman import ServiceManager + + +ENV_KEYS = { + 'NICOS_CACHE_PORT', + 'NICOS_DAEMON_PORT', + 'FRAPPY_MAIN_PORT', + 'FRAPPY_STICK_PORT', + 'FRAPPY_ADDONS_PORT', + 'SEA_PORT', +} + + +def copy_all(srcdir, dstdir): + """copy all files from srcdir to dstdir""" + files = glob(join(srcdir, '*')) + for src in files: + shutil.copy2(src, dstdir) + return files + + +class NicosManager(ServiceManager): + group = 'nicos' + services = ('cache', 'daemon', 'poller') + + def do_create(self, ins, nr=None, *args): + """TODO: redo""" + self.get_info() + if ins == 'all' or ins == 'check': + inslist = list(self.info) + else: + inslist = [ins] + for ins_i in inslist: + env = self.env[ins_i] + base = join(env['NICOS_ROOT'], env['NICOS_PACKAGE'], ins) + nicos_conf = join(base, 'nicos.conf') + content = { + 'nicos': { + 'setup_subdirs': '%s, common, frappy' % ins, + 'logging_path': '%s/%s' % (env['NICOS_LOG'], ins), + 'pid_path': '%s/%s' % (env['NICOS_LOG'], ins), + }, + 'environment': { + key: env[key] for key in env if key in ENV_KEYS + } + } + try: + cp = ConfigParser() + cp.optionsxform = str + cp.read(nicos_conf) + if set(cp.sections) != set(content): + oldcontent = None + else: + oldcontent = {key: cp[key] for key in content} + except FileNotFoundError: + oldcontent = None + if content != oldcontent: + cp = ConfigParser() + cp.optionsxform = str + for key, sdict in content.items(): + cp[key] = sdict + cp.write(nicos_conf + 'x') + + # pdir = self.start_dir + # if ins is None or ins == 'common': + # raise ValueError("nothing to do") + # insdict = {info['cache']['port'] % 100: ins_i for ins_i, info in self.info.items()} + # nrdict = {v: k for k, v in insdict.items()} + # if nr is not None: + # nr = int(nr) + # if ins == 'all': + # if nr is not None: + # raise ValueError("'nicos create all' has no argument") + # action = 'update' + # else: + # if nr is None: + # nr = nrdict.get(ins) + # if nr is None: + # raise ValueError('%s not known, has to specified' % ins) + # if insdict.get(nr, ins) != ins: + # raise ValueError('%d conflicts with %s' % (nr, insdict[nr])) + # action = 'create' if ins in insdict.values() else 'update' + # insdict = {nr: ins} + # for nr, ins in insdict.items(): + # print('%s %3d %s %s' % (action, nr, ins, pdir)) + # os.makedirs(join(pdir, ins), exist_ok=True) + # with open(join(pdir, ins, 'nicos.conf'), 'w') as f: + # f.write(NICOS_CONF % dict(insnr='%02d' % nr, ins=ins, sea=8630 + nr, + # logroot=os.environ.get('NICOS_LOG'))) + # shutil.copyfile(join(pdir, 'common', 'guiconfig.py'), join(pdir, ins, 'guiconfig.py')) + + @staticmethod + def extra_info(ins): + try: + datadir = join(os.environ.get('NICOS_DATA', '.'), ins) + datadir = join(datadir, os.readlink(join(datadir, 'current'))) + except FileNotFoundError: + return None + return 'nicos data: %s' % datadir + + @staticmethod + def linked_dir(current_wd): + link = join(current_wd, 'data') + if islink(link): + return join(link, os.readlink(link)) + if exists(link): + raise ValueError('%s is not a symlink' % link) + return None + + @staticmethod + def handle_linked(target, wd=None): + if not target: + print('no links from %s' % os.getcwd()) + return + targetdir = abspath(join(target, '..')) + analist_file = join(targetdir, 'data_links.txt') + linked_from = [] + if exists(analist_file): + with open(analist_file) as f: + for line in f: + line = line.strip() + if line and line != wd: + if islink(join(line, 'data')): + linked_from.append(line) + if wd: + linked_from.append(wd) + linked_from = '\n'.join(linked_from) + if wd: + with open(analist_file, 'w') as f: + f.write('%s\n' % linked_from) + print('links to %s from:\n%s' % (target, linked_from)) + + @staticmethod + def make_symlink(ins): + data = join(os.environ['NICOS_DATA'], ins) + target = join(data, os.readlink(join(data, 'current')), 'data') + link = join(os.getcwd(), 'data') + if islink(link): + os.remove(link) + elif exists(link): + raise ValueError('%s is not a symlink' % link) + os.symlink(target, link, True) + NicosManager.handle_linked(target, os.getcwd()) + + @staticmethod + def copy_linked(datadir=None): + src = NicosManager.linked_dir(os.getcwd()) + if src: + if not datadir: + if src.rsplit('/', 1)[-1] != 'data': + raise ValueError('%s is already a copy' % src) + datadir = dirname(src) + else: + if not datadir: + raise ValueError('missing data dir') + src = join(datadir, 'data') + dst = join(os.getcwd(), '%s_%s_data' % tuple(datadir.rsplit('/', 2)[-2:])) + os.makedirs(dst, exist_ok=True) + n = len(copy_all(src, dst)) + print('copy %d files to %s' % (n, dst)) + + # copy scripts + src = join(datadir, 'scripts') + dst = join(dst, 'scripts') + os.makedirs(dst, exist_ok=True) + n = len(copy_all(src, dst)) + print('copy %d files to %s' % (n, dst)) + + @staticmethod + def do_link(ins, *args): + """make/show symlink to nicos data in current wd""" + if ins is None: + NicosManager.handle_linked(NicosManager.linked_dir(os.getcwd())) + else: + NicosManager.make_symlink(ins) + sys.exit(0) + + @staticmethod + def do_copy(ins, year_prop, *args): + """copy nicos data to current wd""" + if ins is None: + NicosManager.copy_linked() + else: + data = join(os.environ['NICOS_DATA'], ins) + if year_prop: + year, sep, proposal = year_prop.rpartition('/') + if year: + src = join(data, year, proposal) + else: + # get proposal from most recent year + src = next(reversed(sorted(glob(join(data, '*', proposal))))) + else: + src = join(data, os.readlink(join(data, 'current'))) + NicosManager.copy_linked(src) + + def prepare_start(self, ins): + start_dir, env = super().prepare_start(ins) + instr = '%s.%s' % (env['NICOS_PACKAGE'], ins) + env['INSTRUMENT'] = instr + start_dir = env.get('NICOS_START', start_dir) + return start_dir, env + + def prepare_client(self, ins): + self.check_running(ins, 'daemon') + env = self.prepare_start(ins)[1] + os.environ.update(env) + os.chdir(join(os.environ['NICOS_ROOT'], env['NICOS_PACKAGE'])) + + def run_client(self, ins, main, app, **kwargs): + serverhost = os.environ.get('NICOS_SERVER_HOST', 'localhost') + sys.argv[:] = [app, 'guest:guest@%s:%d' % (serverhost, self.info[ins]['daemon'])] + sys.exit(main(sys.argv, **kwargs)) + + def do_cli(self, ins): + self.prepare_client(ins) + from nicos.clients.cli import main + os.environ['NICOS_HISTORY_FILE'] = expanduser('~/.nicoshistory_%s' % ins) + self.run_client(ins, main, 'nicos-client', + userpath=expanduser('~/.config/nicos_client_%s' % ins)) + + def do_gui(self, ins): + self.prepare_client(ins) + from nicos.clients.gui.main import main + print('starting nicos gui %s' % ins, expanduser('~/.config/nicos_%s' % ins)) + self.run_client(ins, main, 'nicos-gui', instance=ins) + + diff --git a/sea.py b/sea.py new file mode 100644 index 0000000..8f63003 --- /dev/null +++ b/sea.py @@ -0,0 +1,62 @@ +# -*- 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 time +import termios +import subprocess +from servman import ServiceManager, ServiceDown + + +def run_command(cmd, wait=False): + if wait: + old = termios.tcgetattr(sys.stdin) + try: + proc = subprocess.Popen(cmd.split()) + proc.wait() + except KeyboardInterrupt: + proc.terminate() + finally: + # in case cmd changed tty attributes + termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, old) + print('') + else: + subprocess.Popen(cmd.split(), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +class SeaManager(ServiceManager): + group = 'sea' + services = ('sea', 'graph') + + def do_cli(self, ins): + self.check_running(ins, 'sea') + run_command('six -sea %s' % ins, wait=True) + + def do_gui(self, ins): + try: + self.check_running(ins, 'sea') + except ServiceDown as e: + print('%s, try to start...' % e) + self.do_start(ins) + time.sleep(1) # make sure caller did read the message + run_command('SeaClient %s' % ins) + print('starting sea gui %s' % ins)