#!/usr/libexec/platform-python -s
#  -*- coding: utf-8 -*-
# *****************************************************************************
# NICOS, the Networked Instrument Control System of the MLZ
# Copyright (c) 2009-2021 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 <georg.brandl@fz-juelich.de>
#
# *****************************************************************************

import argparse
import atexit
import os
import signal
import subprocess
import sys
from os import path
from time import sleep

import psutil

thisfile = path.realpath(__file__)
bin_path = path.normpath(path.dirname(thisfile))

dev_null = open(os.devnull, 'wb')
background_processes = []

from configparser import NoOptionError, NoSectionError, ConfigParser


def echo(s):
    sys.stdout.write(s)
    sys.stdout.flush()


def spawn(script, *args, **kwds):
    wait = kwds.pop('wait', False)
    stdout = dev_null if not wait else None
    executable = (sys.executable,)
    prof = kwds.get('profile', None)
    if prof == 'mprof':
        executable = ('mprof', 'run', '--include-children', '--python')
    if prof == 'kernprof':
        executable = ('kernprof',)
    if prof == 'line_prof':
        executable = ('kernprof', '--line-by-line')
    p = subprocess.Popen(executable + (path.join(bin_path, script),) + args,
                         stdout=stdout)
    if wait:
        try:
            p.wait()
        except KeyboardInterrupt:
            p.terminate()
    else:
        nice_name = '%s %s' % (script, ' '.join(args))
        background_processes.append((nice_name, p, prof))


def wait():
    echo('Services started, press Enter to stop them.\n')
    try:
        input()
    except (KeyboardInterrupt, EOFError):
        pass


@atexit.register
def cleanup():
    # clean up processes, even if nicos-demo is terminated early
    need_cleanup = bool(background_processes)
    if need_cleanup:
        echo('Cleaning up...\n')
    pgid = None
    for nice_name, proc, prof in reversed(background_processes):
        try:
            if not pgid and hasattr(os, 'getpgid'):
                pgid = os.getpgid(proc.pid)
            if prof == 'mprof' or os.name == 'nt':
                parent = psutil.Process(proc.pid)
                children = parent.children()
                proc.terminate()
                for c in children:
                    c.terminate()
                sleep(0.5)
            else:
                proc.terminate()
        except OSError as e:
            # might already have terminated early
            echo('could not stop %s: %s\n' % (nice_name, e))
        else:
            proc.wait()
    # under some circumstances (re-start due to internal errors)
    # the poller or watchdog may become detached
    # from its parents, so do a final safety kill
    if pgid:
        os.killpg(pgid, signal.SIGTERM)
    if need_cleanup:
        echo('NICOS demo finished.\n')


PROFHELP = '''start processes with profiling:
- mprof: run the mprof memory profiler
- kernprof: run kernprof globally
- line_prof: run kernprof in line_by_line mode
See https://pypi.python.org/pypi/memory_profiler and
https://github.com/rkern/line_profiler for
extended documentation.'''

parser = argparse.ArgumentParser(
    description='Start a demo session of NICOS services and a client.',
    epilog='Remaining arguments are passed to the GUI.',
    formatter_class=argparse.RawTextHelpFormatter,
)

global_cfg = ConfigParser()
nicos_root = path.normpath(path.dirname(path.dirname(__file__)))
global_cfg.read(path.join(nicos_root, 'nicos.conf'))

services = parser.add_argument_group(
    'service selection',
    description='''without options, the following services are started:

    cache, poller, daemon, monitor''')
# These services are opt-out.
services.add_argument('--noelog', '-E', action='store_true',
                      help='disable the electronic logbook')
services.add_argument('--nowatchdog', '-W', action='store_true',
                      help='disable the watchdog')
services.add_argument('--nomonitor', '-M', action='store_true',
                      help='disable the monitor')
services.add_argument('--nopoller', '-P', action='store_true',
                      help='disable the poller')
services.add_argument('--nodaemon', '-D', action='store_true',
                      help='disable the daemon')
# These services are opt-in.
services.add_argument('--htmlmonitor', '-H', action='store_true',
                      help='start the web monitor')
services.add_argument('--pushversioninfo', action='store_true',
                      help='start the version update service')

monitor = parser.add_argument_group('monitor related options')
monitor.add_argument('--monitorsetup', '-m', action='store',
                     help='use given monitor setup instead of the default one')
monitor.add_argument('--htmlmonitorsetup',  action='store',
                     help='use given web monitor setup instead of the default one')

cgroup = parser.add_argument_group(
    'client selection',
    description='without options, the GUI is started')
client = cgroup.add_mutually_exclusive_group()
client.add_argument('--noclient', '-N', action='store_true',
                    help='do not start any client')
client.add_argument('--textclient', '-T', action='store_true',
                    help='start text daemon client instead of GUI')
client.add_argument('--aioclient', action='store_true',
                    help='disable daemon and start console shell')

# try to get the default 'guiconfig' from nicos.conf
try:
    guiconfig = global_cfg.get('nicos-demo', 'guiconfig')
except (NoOptionError, NoSectionError):
    guiconfig = ''
ui = parser.add_argument_group('graphical user interface options')
ui.add_argument('--guiconfig', '-c', action='store', default=guiconfig,
                help='use given GUI config file instead of the default one')
# try to get default 'connect' string from nicos.conf
try:
    connect = global_cfg.get('nicos-demo', 'connect')
except (NoOptionError, NoSectionError):
    connect = 'guest:guest@localhost'
ui.add_argument('-u', dest='guiuser', action='store',
                default=connect,
                help='use given user:password@server:port for the daemon login')

debug = parser.add_argument_group('debug options')
debug.add_argument('--profile', action='store', default=None,
                   choices=['mprof', 'kernprof', 'line_prof'],
                   help=PROFHELP)

debug.add_argument('--clear-cache', action='store_true',
                   help='clear the cache')

parser.add_argument('--other-instruments', '-O', action='store_true',
                    help='allow selecting instruments other than demo (by '
                    'nicos.conf or INSTRUMENT setting)')
parser.add_argument('--version', action='store_true',
                    help='show NICOS version number and exit')
parser.add_argument('guiargs', nargs=argparse.REMAINDER,
                    help=argparse.SUPPRESS)

opts = parser.parse_args()

if not opts.other_instruments:
    os.environ['INSTRUMENT'] = 'nicos_demo.demo'

if opts.profile:
    spawnargs = {'profile': opts.profile}
else:
    spawnargs = {}

if opts.version:
    sys.path.insert(0, path.dirname(bin_path))
    import nicos
    parser.exit(message=nicos.__version__ + '\n')

echo('Starting NICOS demo system, please wait:')

if not opts.clear_cache:
    spawn('nicos-cache', **spawnargs)
else:
    spawn('nicos-cache', '--clear', **spawnargs)
echo(' cache')
sleep(2)

if opts.pushversioninfo:
    spawn('nicos-pushversioninfo', **spawnargs)
    echo(' pushversioninfo')
    sleep(0.1)

if not opts.nowatchdog:
    spawn('nicos-watchdog', **spawnargs)
    echo(' watchdog')
    sleep(0.1)

if not opts.noelog:
    spawn('nicos-elog', **spawnargs)
    echo(' elog')
    sleep(0.1)

if not opts.nopoller:
    spawn('nicos-poller', **spawnargs)
    echo(' poller')
    sleep(0.1)

if not opts.nomonitor:
    monitorargs = ['-S', 'monitor-' + opts.monitorsetup] \
        if opts.monitorsetup else []
    if opts.nodaemon:
        echo('.\n')
        spawn('nicos-monitor', *monitorargs, wait=True, **spawnargs)
    else:
        spawn('nicos-monitor', *monitorargs, **spawnargs)
        echo(' monitor')
        sleep(0.1)

if opts.htmlmonitor:
    monitorargs = ['-S', 'monitor-html']
    spawn('nicos-monitor', *monitorargs, **spawnargs)
    echo(' web-monitor')

if opts.aioclient:
    echo('\nServices started, starting console.\n')
    spawn('nicos-aio', wait=True, **spawnargs)
elif not opts.nodaemon:
    spawn('nicos-daemon', **spawnargs)
    echo(' daemon')
    sleep(1)
    echo('.\n')
    if not opts.noclient:
        if opts.textclient:
            spawn('nicos-client', opts.guiuser, wait=True, **spawnargs)
        else:
            echo('Services started, starting GUI.\n')
            guiargs = ['-c', opts.guiconfig] if opts.guiconfig else []
            guiargs.extend([opts.guiuser] + opts.guiargs)
            spawn('nicos-gui', *guiargs, wait=True, **spawnargs)
    else:
        wait()
else:
    echo('.\n')
    wait()
