# Copyright 1999-2016. Parallels IP Holdings GmbH. All Rights Reserved.
import os
import sys
import subprocess

from os_detect import OS
from plesk_interproc_locks import InterProcessLock
import plesk_constants

SYSTEM_RC_D = '/etc/init.d'
SYSTEMD_POLICY_D = '/lib/systemd/system'

UPDATE_RC_D_BIN = '/usr/sbin/update-rc.d'
INVOKE_RC_D_BIN = '/usr/sbin/invoke-rc.d'
CHKCONFIG_BIN = '/sbin/chkconfig'
UPSTART_CHKCONFIG_BIN = '/usr/sbin/sysv-rc-conf'
SERVICE_BIN = '/sbin/service'
INSSERV_BIN = '/sbin/insserv'
SYSTEMCTL_BIN = '/bin/systemctl'
REGISTER_SERVICE = os.path.join(plesk_constants.PRODUCT_ROOT_D, "admin/sbin/register_service")

SERVICE_LOCK_TIMEOUT = 10

def _run_cmd_ex(args, hide_stdout=True, hide_stderr=False):
    """ Run command optionally suppressing stdout/stderr. If command return code
        is not zero, CalledProcessError is raised.
        >>> _run_cmd_ex(['/bin/echo', 'hello'])

        >>> _run_cmd_ex(['/bin/false'], hide_stdout=False, hide_stderr=False)
        Traceback (most recent call last):
            ...
        CalledProcessError: Command '['/bin/false']' returned non-zero exit status 1
    """
    with open(os.devnull, 'w') as devnull:
        subprocess.check_call(args, 
                              stdout=(hide_stdout and devnull or None), 
                              stderr=(hide_stderr and devnull or None))

def _run_cmd(args, hide_stdout=True, hide_stderr=False):
    """ Run command optionally suppressing stdout/stderr and return its return code.
        >>> _run_cmd(['/bin/echo', 'hello'])
        0
        >>> _run_cmd(['/bin/false'], hide_stdout=False, hide_stderr=False)
        1
    """
    with open(os.devnull, 'w') as devnull:
        return subprocess.call(args, 
                               stdout=(hide_stdout and devnull or None),
                               stderr=(hide_stderr and devnull or None))

def _is_executable_file(path):
    """ Check that a given file is a regular executable one. """
    return os.path.isfile(path) and os.access(path, os.X_OK)

def _service_locked(service):
    """ Create service-specific lock object with timeout SERVICE_LOCK_TIMEOUT. 
        All operations with a given service should be guarded by this lock. 
    """
    return InterProcessLock('service.' + service, timeout=SERVICE_LOCK_TIMEOUT)

def systemd_name(service):
    """ Returns systemd service name, i.e. '<service>.service'. """
    return service + '.service'

def init_script_exists(service):
    """ Check whether init script exists and executable for a given service. """
    path = os.path.join(SYSTEM_RC_D, service)
    return _is_executable_file(path)

def systemd_unit_exists(service):
    """ Check whether systemd service unit file exists. """
    path = os.path.join(SYSTEMD_POLICY_D, systemd_name(service))
    return os.path.isfile(path)

def register(service, with_resource_controller=False):
    """ Register system service (so it can be operated on and will start on boot). """
    with _service_locked(service):
        if with_resource_controller:
            _run_cmd_ex([REGISTER_SERVICE, "--enable", service, "--with-resource-controller"])
        else:
            _run_cmd_ex([REGISTER_SERVICE, "--enable", service])

def deregister(service):
    """ Deregister system service (so it would not start on boot).
        Ideally you should stop it beforehand.
    """
    with _service_locked(service):
        _run_cmd_ex([REGISTER_SERVICE, "--disable", service])

def is_registered(service):
    """ Check whether service is registered (is started on boot).
        Note that on Debian-like OSes it requires installing package 'chkconfig'
    """
    if systemd_unit_exists(service) and OS.uses_systemd():
        return _is_registered_systemd(service)
    elif init_script_exists(service):
        return _is_registered_init(service)
    else:
        raise RuntimeError("Don't know how to detect if '%s' service is registered on %s %s" % (service, OS.distname(), OS.full_version()))

def _is_registered_init(service):
    # it's better to use Popen because it doesn't throw exception when chkconfig finishes unsuccessfully
    # upstart on Ubt14 has not 'chkconfig' command, it replaced by 'sysv-rc-conf'
    cmd = CHKCONFIG_BIN if _is_executable_file(CHKCONFIG_BIN) else UPSTART_CHKCONFIG_BIN
    command = [cmd, '--list', service]
    process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    output = process.communicate()[0]
    return ':on' in output

def _is_registered_systemd(service):
    command = [SYSTEMCTL_BIN, 'status', systemd_name(service)]
    process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    output = process.communicate()[0]
    for line in output.split("\n"):
        if 'Loaded:' in line:
            chunk = line.split(";")
            if 'enabled' in chunk[1]:
                return True
            elif 'disabled' in chunk[1]:
                return False
            raise RuntimeError("Unable to parse status line: %s" % (line,))
    raise RuntimeError("Cannot find loaded status line in output")

def _emulate_exists(service):
    return init_script_exists(service) or systemd_unit_exists(service)

def _emulate_restart(service):
    action(service, 'stop')
    return action(service, 'start')

def _emulate_condrestart(service):
    if action(service, 'status'):
        return _emulate_restart(service)
    return True

def _emulate_action(service, act):
    """ Emulate a given service action. I.e. execute not by passing it directly 
        to init script or systemctl, but via other means.
        Raises ValueError if emulator is not implemented. 
    """
    emulators = {
        'exists': _emulate_exists,
        'restart': _emulate_restart,
        'condrestart': _emulate_condrestart,
    }
    if act not in emulators:
        raise ValueError("Don't know how to emulate '%s' action for '%s' service" % (act, service))
    return emulators[act](service)

def action(service, act, emulate=False):
    """ Perform service action (e.g. start, stop, restart, reload, status, etc.).
        Returns True on success, False otherwise.
    """
    if act in ('exists',):
        emulate = True
    if emulate:
        return _emulate_action(service, act)

    if OS.uses_systemd():
        act_map = {
            'status': 'is-active',
            'reload': 'reload-or-try-restart',
        }
        act = act_map.get(act, act)
        cmd = [SYSTEMCTL_BIN, act, systemd_name(service)]
    else:
        if _is_executable_file(INVOKE_RC_D_BIN):
            cmd = [INVOKE_RC_D_BIN, service, act]
        elif _is_executable_file(SERVICE_BIN):
            cmd = [SERVICE_BIN, service, act]
        else:
            cmd = [os.path.join(SYSTEM_RC_D, service), act]

    with _service_locked(service):
        return not _run_cmd(cmd)


if __name__ == '__main__':
    import doctest
    sys.exit(doctest.testmod().failed)

# vim: ts=4 sts=4 sw=4 et :
