<commit_before># -*- coding: utf-8 -*-

""" License

    Copyright (C) 2013 YunoHost

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 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 Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program; if not, see http://www.gnu.org/licenses

"""

""" yunohost_service.py

    Manage services
"""
import os
import time
import yaml
import json
import subprocess
import shutil
import hashlib

from difflib import unified_diff
from datetime import datetime

from moulinette import m18n
from yunohost.utils.error import YunohostError
from moulinette.utils import log, filesystem

from yunohost.log import is_unit_operation
from yunohost.hook import hook_callback, hook_list

BASE_CONF_PATH = '/home/yunohost.conf'
BACKUP_CONF_DIR = os.path.join(BASE_CONF_PATH, 'backup')
PENDING_CONF_DIR = os.path.join(BASE_CONF_PATH, 'pending')
MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock"

logger = log.getActionLogger('yunohost.service')


def service_add(name, status=None, log=None, runlevel=None, need_lock=False, description=None):
    """
    Add a custom service

    Keyword argument:
        name -- Service name to add
        status -- Custom status command
        log -- Absolute path to log file to display
        runlevel -- Runlevel priority of the service
        need_lock -- Use this option to prevent deadlocks if the service does invoke yunohost commands.
        description -- description of the service
    """
    services = _get_services()

    if not status:
        services[name] = {'status': 'service'}
    else:
        services[name] = {'status': status}

    if log is not None:
        services[name]['log'] = log

    if runlevel is not None:
        services[name]['runlevel'] = runlevel

    if need_lock:
        services[name]['need_lock'] = True

    if description is not None:
        services[name]['description'] = description

    try:
        _save_services(services)
    except:
        # we'll get a logger.warning with more details in _save_services
        raise YunohostError('service_add_failed', service=name)

    logger.success(m18n.n('service_added', service=name))


def service_remove(name):
    """
    Remove a custom service

    Keyword argument:
        name -- Service name to remove

    """
    services = _get_services()

    try:
        del services[name]
    except KeyError:
        raise YunohostError('service_unknown', service=name)

    try:
        _save_services(services)
    except:
        # we'll get a logger.warning with more details in _save_services
        raise YunohostError('service_remove_failed', service=name)

    logger.success(m18n.n('service_removed', service=name))


def service_start(names):
    """
    Start one or more services

    Keyword argument:
        names -- Services name to start

    """
    if isinstance(names, str):
        names = [names]

    for name in names:
        if _run_service_command('start', name):
            logger.success(m18n.n('service_started', service=name))
        else:
            if service_status(name)['status'] != 'running':
                raise YunohostError('service_start_failed', service=name, logs=_get_journalctl_logs(name))
            logger.debug(m18n.n('service_already_started', service=name))


def service_stop(names):
    """
    Stop one or more services

    Keyword argument:
        name -- Services name to stop

    """
    if isinstance(names, str):
        names = [names]
    for name in names:
        if _run_service_command('stop', name):
            logger.success(m18n.n('service_stopped', service=name))
        else:
            if service_status(name)['status'] != 'inactive':
                raise YunohostError('service_stop_failed', service=name, logs=_get_journalctl_logs(name))
            logger.debug(m18n.n('service_already_stopped', service=name))


def service_reload(names):
    """
    Reload one or more services

    Keyword argument:
        name -- Services name to reload

    """
    if isinstance(names, str):
        names = [names]
    for name in names:
        if _run_service_command('reload', name):
            logger.success(m18n.n('service_reloaded', service=name))
        else:
            if service_status(name)['status'] != 'inactive':
                raise YunohostError('service_reload_failed', service=name, logs=_get_journalctl_logs(name))


def service_restart(names):
    """
    Restart one or more services. If the services are not running yet, they will be started.

    Keyword argument:
        name -- Services name to restart

    """
    if isinstance(names, str):
        names = [names]
    for name in names:
        if _run_service_command('restart', name):
            logger.success(m18n.n('service_restarted', service=name))
        else:
            if service_status(name)['status'] != 'inactive':
                raise YunohostError('service_restart_failed', service=name, logs=_get_journalctl_logs(name))


def service_reload_or_restart(names):
    """
    Reload one or more services if they support it. If not, restart them instead. If the services are not running yet, they will be started.

    Keyword argument:
        name -- Services name to reload or restart

    """
    if isinstance(names, str):
        names = [names]
    for name in names:
        if _run_service_command('reload-or-restart', name):
            logger.success(m18n.n('service_reloaded_or_restarted', service=name))
        else:
            if service_status(name)['status'] != 'inactive':
                raise YunohostError('service_reload_or_restart_failed', service=name, logs=_get_journalctl_logs(name))


@is_unit_operation()
def service_enable(operation_logger, names):
    """
    Enable one or more services

    Keyword argument:
        names -- Services name to enable

    """
    operation_logger.start()
    if isinstance(names, str):
        names = [names]
    for name in names:
        if _run_service_command('enable', name):
            logger.success(m18n.n('service_enabled', service=name))
        else:
            raise YunohostError('service_enable_failed', service=name, logs=_get_journalctl_logs(name))


def service_disable(names):
    """
    Disable one or more services

    Keyword argument:
        names -- Services name to disable

    """
    if isinstance(names, str):
        names = [names]
    for name in names:
        if _run_service_command('disable', name):
            logger.success(m18n.n('service_disabled', service=name))
        else:
            raise YunohostError('service_disable_failed', service=name, logs=_get_journalctl_logs(name))


def service_status(names=[]):
    """
    Show status information about one or more services (all by default)

    Keyword argument:
        names -- Services name to show

    """
    services = _get_services()
    check_names = True
    result = {}

    if isinstance(names, str):
        names = [names]
    elif len(names) == 0:
        names = services.keys()
        check_names = False

    for name in names:
        if check_names and name not in services.keys():
            raise YunohostError('service_unknown', service=name)

        # this "service" isn't a service actually so we skip it
        #
        # the historical reason is because regenconf has been hacked into the
        # service part of YunoHost will in some situation we need to regenconf
        # for things that aren't services
        # the hack was to add fake services...
        # we need to extract regenconf from service at some point, also because
        # some app would really like to use it
        if "status" in services[name] and services[name]["status"] is None:
            continue

        status = _get_service_information_from_systemd(name)

        # try to get status using alternative version if they exists
        # this is for mariadb/mysql but is generic in case of
        alternates = services[name].get("alternates", [])
        while status is None and alternates:
            status = _get_service_information_from_systemd(alternates.pop())

        if status is None:
            logger.error("Failed to get status information via dbus for service %s, systemctl didn't recognize this service ('NoSuchUnit')." % name)
            result[name] = {
                'status': "unknown",
                'loaded': "unknown",
                'active': "unknown",
                'active_at': "unknown",
                'description': "Error: failed to get information for this service, it doesn't exists for systemd",
                'service_file_path': "unknown",
            }

        else:
            translation_key = "service_description_%s" % name
            if "description" in services[name] is not None:
                description = services[name].get("description")
            else:
                description = m18n.n(translation_key)

            # that mean that we don't have a translation for this string
            # that's the only way to test for that for now
            # if we don't have it, uses the one provided by systemd
            if description == translation_key:
                description = str(status.get("Description", ""))

            result[name] = {
                'status': str(status.get("SubState", "unknown")),
                'loaded': "enabled" if str(status.get("LoadState", "unknown")) == "loaded" else str(status.get("LoadState", "unknown")),
                'active': str(status.get("ActiveState", "unknown")),
                'description': description,
                'service_file_path': str(status.get("FragmentPath", "unknown")),
            }
            if "ActiveEnterTimestamp" in status:
                result[name]['active_at'] = datetime.utcfromtimestamp(status["ActiveEnterTimestamp"] / 1000000)
            else:
                result[name]['active_at'] = "unknown"

    if len(names) == 1:
        return result[names[0]]
    return result


def _get_service_information_from_systemd(service):
    "this is the equivalent of 'systemctl status $service'"
    import dbus
    from dbus.exceptions import DBusException

    d = dbus.SystemBus()

    systemd = d.get_object('org.freedesktop.systemd1', '/org/freedesktop/systemd1')
    manager = dbus.Interface(systemd, 'org.freedesktop.systemd1.Manager')

    try:
        service_path = manager.GetUnit(service + ".service")
    except DBusException as exception:
        if exception.get_dbus_name() == 'org.freedesktop.systemd1.NoSuchUnit':
            return None
        raise

    service_proxy = d.get_object('org.freedesktop.systemd1', service_path)

    # unit_proxy = dbus.Interface(service_proxy, 'org.freedesktop.systemd1.Unit',)
    properties_interface = dbus.Interface(service_proxy, 'org.freedesktop.DBus.Properties')

    return properties_interface.GetAll('org.freedesktop.systemd1.Unit')


def service_log(name, number=50):
    """
    Log every log files of a service

    Keyword argument:
        name -- Service name to log
        number -- Number of lines to display

    """
    services = _get_services()

    if name not in services.keys():
        raise YunohostError('service_unknown', service=name)

    if 'log' not in services[name]:
        raise YunohostError('service_no_log', service=name)

    log_list = services[name]['log']

    if not isinstance(log_list, list):
        log_list = [log_list]

    result = {}

    for log_path in log_list:
        # log is a file, read it
        if not os.path.isdir(log_path):
            result[log_path] = _tail(log_path, int(number)) if os.path.exists(log_path) else []
            continue

        for log_file in os.listdir(log_path):
            log_file_path = os.path.join(log_path, log_file)
            # not a file : skip
            if not os.path.isfile(log_file_path):
                continue

            if not log_file.endswith(".log"):
                continue

            result[log_file_path] = _tail(log_file_path, int(number)) if os.path.exists(log_file_path) else []

    return result


@is_unit_operation([('names', 'service')])
def service_regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run=False,
                       list_pending=False):
    """
    Regenerate the configuration file(s) for a service

    Keyword argument:
        names -- Services name to regenerate configuration of
        with_diff -- Show differences in case of configuration changes
        force -- Override all manual modifications in configuration files
        dry_run -- Show what would have been regenerated
        list_pending -- List pending configuration files and exit

    """
    result = {}

    # Return the list of pending conf
    if list_pending:
        pending_conf = _get_pending_conf(names)

        if not with_diff:
            return pending_conf

        for service, conf_files in pending_conf.items():
            for system_path, pending_path in conf_files.items():

                pending_conf[service][system_path] = {
                    'pending_conf': pending_path,
                    'diff': _get_files_diff(
                        system_path, pending_path, True),
                }

        return pending_conf

    if not dry_run:
        operation_logger.related_to = [('service', x) for x in names]
        if not names:
            operation_logger.name_parameter_override = 'all'
        elif len(names) != 1:
            operation_logger.name_parameter_override = str(len(operation_logger.related_to)) + '_services'
        operation_logger.start()

    # Clean pending conf directory
    if os.path.isdir(PENDING_CONF_DIR):
        if not names:
            shutil.rmtree(PENDING_CONF_DIR, ignore_errors=True)
        else:
            for name in names:
                shutil.rmtree(os.path.join(PENDING_CONF_DIR, name),
                              ignore_errors=True)
    else:
        filesystem.mkdir(PENDING_CONF_DIR, 0o755, True)

    # Format common hooks arguments
    common_args = [1 if force else 0, 1 if dry_run else 0]

    # Execute hooks for pre-regen
    pre_args = ['pre', ] + common_args

    def _pre_call(name, priority, path, args):
        # create the pending conf directory for the service
        service_pending_path = os.path.join(PENDING_CONF_DIR, name)
        filesystem.mkdir(service_pending_path, 0o755, True, uid='root')

        # return the arguments to pass to the script
        return pre_args + [service_pending_path, ]

    # Don't regen SSH if not specifically specified
    if not names:
        names = hook_list('conf_regen', list_by='name',
                          show_info=False)['hooks']
        names.remove('ssh')

    pre_result = hook_callback('conf_regen', names, pre_callback=_pre_call)

    # Update the services name
    names = pre_result['succeed'].keys()

    if not names:
        raise YunohostError('service_regenconf_failed',
                            services=', '.join(pre_result['failed']))

    # Set the processing method
    _regen = _process_regen_conf if not dry_run else lambda *a, **k: True

    operation_logger.related_to = []

    # Iterate over services and process pending conf
    for service, conf_files in _get_pending_conf(names).items():
        if not dry_run:
            operation_logger.related_to.append(('service', service))

        logger.debug(m18n.n(
            'service_regenconf_pending_applying' if not dry_run else
            'service_regenconf_dry_pending_applying',
            service=service))

        conf_hashes = _get_conf_hashes(service)
        succeed_regen = {}
        failed_regen = {}

        for system_path, pending_path in conf_files.items():
            logger.debug("processing pending conf '%s' to system conf '%s'",
                         pending_path, system_path)
            conf_status = None
            regenerated = False

            # Get the diff between files
            conf_diff = _get_files_diff(
                system_path, pending_path, True) if with_diff else None

            # Check if the conf must be removed
            to_remove = True if os.path.getsize(pending_path) == 0 else False

            # Retrieve and calculate hashes
            system_hash = _calculate_hash(system_path)
            saved_hash = conf_hashes.get(system_path, None)
            new_hash = None if to_remove else _calculate_hash(pending_path)

            # -> system conf does not exists
            if not system_hash:
                if to_remove:
                    logger.debug("> system conf is already removed")
                    os.remove(pending_path)
                    continue
                if not saved_hash or force:
                    if force:
                        logger.debug("> system conf has been manually removed")
                        conf_status = 'force-created'
                    else:
                        logger.debug("> system conf does not exist yet")
                        conf_status = 'created'
                    regenerated = _regen(
                        system_path, pending_path, save=False)
                else:
                    logger.info(m18n.n(
                        'service_conf_file_manually_removed',
                        conf=system_path))
                    conf_status = 'removed'

            # -> system conf is not managed yet
            elif not saved_hash:
                logger.debug("> system conf is not managed yet")
                if system_hash == new_hash:
                    logger.debug("> no changes to system conf has been made")
                    conf_status = 'managed'
                    regenerated = True
                elif not to_remove:
                    # If the conf exist but is not managed yet, and is not to be removed,
                    # we assume that it is safe to regen it, since the file is backuped
                    # anyway (by default in _regen), as long as we warn the user
                    # appropriately.
                    logger.info(m18n.n('service_conf_now_managed_by_yunohost',
                                       conf=system_path))
                    regenerated = _regen(system_path, pending_path)
                    conf_status = 'new'
                elif force:
                    regenerated = _regen(system_path)
                    conf_status = 'force-removed'
                else:
                    logger.info(m18n.n('service_conf_file_kept_back',
                                       conf=system_path, service=service))
                    conf_status = 'unmanaged'

            # -> system conf has not been manually modified
            elif system_hash == saved_hash:
                if to_remove:
                    regenerated = _regen(system_path)
                    conf_status = 'removed'
                elif system_hash != new_hash:
                    regenerated = _regen(system_path, pending_path)
                    conf_status = 'updated'
                else:
                    logger.debug("> system conf is already up-to-date")
                    os.remove(pending_path)
                    continue

            else:
                logger.debug("> system conf has been manually modified")
                if system_hash == new_hash:
                    logger.debug("> new conf is as current system conf")
                    conf_status = 'managed'
                    regenerated = True
                elif force:
                    regenerated = _regen(system_path, pending_path)
                    conf_status = 'force-updated'
                else:
                    logger.warning(m18n.n(
                        'service_conf_file_manually_modified',
                        conf=system_path))
                    conf_status = 'modified'

            # Store the result
            conf_result = {'status': conf_status}
            if conf_diff is not None:
                conf_result['diff'] = conf_diff
            if regenerated:
                succeed_regen[system_path] = conf_result
                conf_hashes[system_path] = new_hash
                if os.path.isfile(pending_path):
                    os.remove(pending_path)
            else:
                failed_regen[system_path] = conf_result

        # Check for service conf changes
        if not succeed_regen and not failed_regen:
            logger.debug(m18n.n('service_conf_up_to_date', service=service))
            continue
        elif not failed_regen:
            logger.success(m18n.n(
                'service_conf_updated' if not dry_run else
                'service_conf_would_be_updated',
                service=service))

        if succeed_regen and not dry_run:
            _update_conf_hashes(service, conf_hashes)

        # Append the service results
        result[service] = {
            'applied': succeed_regen,
            'pending': failed_regen
        }

    # Return in case of dry run
    if dry_run:
        return result

    # Execute hooks for post-regen
    post_args = ['post', ] + common_args

    def _pre_call(name, priority, path, args):
        # append coma-separated applied changes for the service
        if name in result and result[name]['applied']:
            regen_conf_files = ','.join(result[name]['applied'].keys())
        else:
            regen_conf_files = ''
        return post_args + [regen_conf_files, ]

    hook_callback('conf_regen', names, pre_callback=_pre_call)

    operation_logger.success()

    return result


def _run_service_command(action, service):
    """
    Run services management command (start, stop, enable, disable, restart, reload)

    Keyword argument:
        action -- Action to perform
        service -- Service name

    """
    services = _get_services()
    if service not in services.keys():
        raise YunohostError('service_unknown', service=service)

    possible_actions = ['start', 'stop', 'restart', 'reload', 'reload-or-restart', 'enable', 'disable']
    if action not in possible_actions:
        raise ValueError("Unknown action '%s', available actions are: %s" % (action, ", ".join(possible_actions)))

    cmd = 'systemctl %s %s' % (action, service)

    need_lock = services[service].get('need_lock', False) \
        and action in ['start', 'stop', 'restart', 'reload', 'reload-or-restart']

    try:
        # Launch the command
        logger.debug("Running '%s'" % cmd)
        p = subprocess.Popen(cmd.split(), stderr=subprocess.STDOUT)
        # If this command needs a lock (because the service uses yunohost
        # commands inside), find the PID and add a lock for it
        if need_lock:
            PID = _give_lock(action, service, p)
        # Wait for the command to complete
        p.communicate()

    except subprocess.CalledProcessError as e:
        # TODO: Log output?
        logger.warning(m18n.n('service_cmd_exec_failed', command=' '.join(e.cmd)))
        return False

    finally:
        # Remove the lock if one was given
        if need_lock and PID != 0:
            _remove_lock(PID)

    return True


def _give_lock(action, service, p):

    # Depending of the action, systemctl calls the PID differently :/
    if action == "start" or action == "restart":
        systemctl_PID_name = "MainPID"
    else:
        systemctl_PID_name = "ControlPID"

    cmd_get_son_PID = "systemctl show %s -p %s" % (service, systemctl_PID_name)
    son_PID = 0
    # As long as we did not found the PID and that the command is still running<commit_msg>[fix] _run_service_command was not properly returning False if command failed (#616)<commit_after># -*- coding: utf-8 -*-

""" License

    Copyright (C) 2013 YunoHost

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 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 Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program; if not, see http://www.gnu.org/licenses

"""

""" yunohost_service.py

    Manage services
"""
import os
import time
import yaml
import json
import subprocess
import shutil
import hashlib

from difflib import unified_diff
from datetime import datetime

from moulinette import m18n
from yunohost.utils.error import YunohostError
from moulinette.utils import log, filesystem

from yunohost.log import is_unit_operation
from yunohost.hook import hook_callback, hook_list

BASE_CONF_PATH = '/home/yunohost.conf'
BACKUP_CONF_DIR = os.path.join(BASE_CONF_PATH, 'backup')
PENDING_CONF_DIR = os.path.join(BASE_CONF_PATH, 'pending')
MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock"

logger = log.getActionLogger('yunohost.service')


def service_add(name, status=None, log=None, runlevel=None, need_lock=False, description=None):
    """
    Add a custom service

    Keyword argument:
        name -- Service name to add
        status -- Custom status command
        log -- Absolute path to log file to display
        runlevel -- Runlevel priority of the service
        need_lock -- Use this option to prevent deadlocks if the service does invoke yunohost commands.
        description -- description of the service
    """
    services = _get_services()

    if not status:
        services[name] = {'status': 'service'}
    else:
        services[name] = {'status': status}

    if log is not None:
        services[name]['log'] = log

    if runlevel is not None:
        services[name]['runlevel'] = runlevel

    if need_lock:
        services[name]['need_lock'] = True

    if description is not None:
        services[name]['description'] = description

    try:
        _save_services(services)
    except:
        # we'll get a logger.warning with more details in _save_services
        raise YunohostError('service_add_failed', service=name)

    logger.success(m18n.n('service_added', service=name))


def service_remove(name):
    """
    Remove a custom service

    Keyword argument:
        name -- Service name to remove

    """
    services = _get_services()

    try:
        del services[name]
    except KeyError:
        raise YunohostError('service_unknown', service=name)

    try:
        _save_services(services)
    except:
        # we'll get a logger.warning with more details in _save_services
        raise YunohostError('service_remove_failed', service=name)

    logger.success(m18n.n('service_removed', service=name))


def service_start(names):
    """
    Start one or more services

    Keyword argument:
        names -- Services name to start

    """
    if isinstance(names, str):
        names = [names]

    for name in names:
        if _run_service_command('start', name):
            logger.success(m18n.n('service_started', service=name))
        else:
            if service_status(name)['status'] != 'running':
                raise YunohostError('service_start_failed', service=name, logs=_get_journalctl_logs(name))
            logger.debug(m18n.n('service_already_started', service=name))


def service_stop(names):
    """
    Stop one or more services

    Keyword argument:
        name -- Services name to stop

    """
    if isinstance(names, str):
        names = [names]
    for name in names:
        if _run_service_command('stop', name):
            logger.success(m18n.n('service_stopped', service=name))
        else:
            if service_status(name)['status'] != 'inactive':
                raise YunohostError('service_stop_failed', service=name, logs=_get_journalctl_logs(name))
            logger.debug(m18n.n('service_already_stopped', service=name))


def service_reload(names):
    """
    Reload one or more services

    Keyword argument:
        name -- Services name to reload

    """
    if isinstance(names, str):
        names = [names]
    for name in names:
        if _run_service_command('reload', name):
            logger.success(m18n.n('service_reloaded', service=name))
        else:
            if service_status(name)['status'] != 'inactive':
                raise YunohostError('service_reload_failed', service=name, logs=_get_journalctl_logs(name))


def service_restart(names):
    """
    Restart one or more services. If the services are not running yet, they will be started.

    Keyword argument:
        name -- Services name to restart

    """
    if isinstance(names, str):
        names = [names]
    for name in names:
        if _run_service_command('restart', name):
            logger.success(m18n.n('service_restarted', service=name))
        else:
            if service_status(name)['status'] != 'inactive':
                raise YunohostError('service_restart_failed', service=name, logs=_get_journalctl_logs(name))


def service_reload_or_restart(names):
    """
    Reload one or more services if they support it. If not, restart them instead. If the services are not running yet, they will be started.

    Keyword argument:
        name -- Services name to reload or restart

    """
    if isinstance(names, str):
        names = [names]
    for name in names:
        if _run_service_command('reload-or-restart', name):
            logger.success(m18n.n('service_reloaded_or_restarted', service=name))
        else:
            if service_status(name)['status'] != 'inactive':
                raise YunohostError('service_reload_or_restart_failed', service=name, logs=_get_journalctl_logs(name))


@is_unit_operation()
def service_enable(operation_logger, names):
    """
    Enable one or more services

    Keyword argument:
        names -- Services name to enable

    """
    operation_logger.start()
    if isinstance(names, str):
        names = [names]
    for name in names:
        if _run_service_command('enable', name):
            logger.success(m18n.n('service_enabled', service=name))
        else:
            raise YunohostError('service_enable_failed', service=name, logs=_get_journalctl_logs(name))


def service_disable(names):
    """
    Disable one or more services

    Keyword argument:
        names -- Services name to disable

    """
    if isinstance(names, str):
        names = [names]
    for name in names:
        if _run_service_command('disable', name):
            logger.success(m18n.n('service_disabled', service=name))
        else:
            raise YunohostError('service_disable_failed', service=name, logs=_get_journalctl_logs(name))


def service_status(names=[]):
    """
    Show status information about one or more services (all by default)

    Keyword argument:
        names -- Services name to show

    """
    services = _get_services()
    check_names = True
    result = {}

    if isinstance(names, str):
        names = [names]
    elif len(names) == 0:
        names = services.keys()
        check_names = False

    for name in names:
        if check_names and name not in services.keys():
            raise YunohostError('service_unknown', service=name)

        # this "service" isn't a service actually so we skip it
        #
        # the historical reason is because regenconf has been hacked into the
        # service part of YunoHost will in some situation we need to regenconf
        # for things that aren't services
        # the hack was to add fake services...
        # we need to extract regenconf from service at some point, also because
        # some app would really like to use it
        if "status" in services[name] and services[name]["status"] is None:
            continue

        status = _get_service_information_from_systemd(name)

        # try to get status using alternative version if they exists
        # this is for mariadb/mysql but is generic in case of
        alternates = services[name].get("alternates", [])
        while status is None and alternates:
            status = _get_service_information_from_systemd(alternates.pop())

        if status is None:
            logger.error("Failed to get status information via dbus for service %s, systemctl didn't recognize this service ('NoSuchUnit')." % name)
            result[name] = {
                'status': "unknown",
                'loaded': "unknown",
                'active': "unknown",
                'active_at': "unknown",
                'description': "Error: failed to get information for this service, it doesn't exists for systemd",
                'service_file_path': "unknown",
            }

        else:
            translation_key = "service_description_%s" % name
            if "description" in services[name] is not None:
                description = services[name].get("description")
            else:
                description = m18n.n(translation_key)

            # that mean that we don't have a translation for this string
            # that's the only way to test for that for now
            # if we don't have it, uses the one provided by systemd
            if description == translation_key:
                description = str(status.get("Description", ""))

            result[name] = {
                'status': str(status.get("SubState", "unknown")),
                'loaded': "enabled" if str(status.get("LoadState", "unknown")) == "loaded" else str(status.get("LoadState", "unknown")),
                'active': str(status.get("ActiveState", "unknown")),
                'description': description,
                'service_file_path': str(status.get("FragmentPath", "unknown")),
            }
            if "ActiveEnterTimestamp" in status:
                result[name]['active_at'] = datetime.utcfromtimestamp(status["ActiveEnterTimestamp"] / 1000000)
            else:
                result[name]['active_at'] = "unknown"

    if len(names) == 1:
        return result[names[0]]
    return result


def _get_service_information_from_systemd(service):
    "this is the equivalent of 'systemctl status $service'"
    import dbus
    from dbus.exceptions import DBusException

    d = dbus.SystemBus()

    systemd = d.get_object('org.freedesktop.systemd1', '/org/freedesktop/systemd1')
    manager = dbus.Interface(systemd, 'org.freedesktop.systemd1.Manager')

    try:
        service_path = manager.GetUnit(service + ".service")
    except DBusException as exception:
        if exception.get_dbus_name() == 'org.freedesktop.systemd1.NoSuchUnit':
            return None
        raise

    service_proxy = d.get_object('org.freedesktop.systemd1', service_path)

    # unit_proxy = dbus.Interface(service_proxy, 'org.freedesktop.systemd1.Unit',)
    properties_interface = dbus.Interface(service_proxy, 'org.freedesktop.DBus.Properties')

    return properties_interface.GetAll('org.freedesktop.systemd1.Unit')


def service_log(name, number=50):
    """
    Log every log files of a service

    Keyword argument:
        name -- Service name to log
        number -- Number of lines to display

    """
    services = _get_services()

    if name not in services.keys():
        raise YunohostError('service_unknown', service=name)

    if 'log' not in services[name]:
        raise YunohostError('service_no_log', service=name)

    log_list = services[name]['log']

    if not isinstance(log_list, list):
        log_list = [log_list]

    result = {}

    for log_path in log_list:
        # log is a file, read it
        if not os.path.isdir(log_path):
            result[log_path] = _tail(log_path, int(number)) if os.path.exists(log_path) else []
            continue

        for log_file in os.listdir(log_path):
            log_file_path = os.path.join(log_path, log_file)
            # not a file : skip
            if not os.path.isfile(log_file_path):
                continue

            if not log_file.endswith(".log"):
                continue

            result[log_file_path] = _tail(log_file_path, int(number)) if os.path.exists(log_file_path) else []

    return result


@is_unit_operation([('names', 'service')])
def service_regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run=False,
                       list_pending=False):
    """
    Regenerate the configuration file(s) for a service

    Keyword argument:
        names -- Services name to regenerate configuration of
        with_diff -- Show differences in case of configuration changes
        force -- Override all manual modifications in configuration files
        dry_run -- Show what would have been regenerated
        list_pending -- List pending configuration files and exit

    """
    result = {}

    # Return the list of pending conf
    if list_pending:
        pending_conf = _get_pending_conf(names)

        if not with_diff:
            return pending_conf

        for service, conf_files in pending_conf.items():
            for system_path, pending_path in conf_files.items():

                pending_conf[service][system_path] = {
                    'pending_conf': pending_path,
                    'diff': _get_files_diff(
                        system_path, pending_path, True),
                }

        return pending_conf

    if not dry_run:
        operation_logger.related_to = [('service', x) for x in names]
        if not names:
            operation_logger.name_parameter_override = 'all'
        elif len(names) != 1:
            operation_logger.name_parameter_override = str(len(operation_logger.related_to)) + '_services'
        operation_logger.start()

    # Clean pending conf directory
    if os.path.isdir(PENDING_CONF_DIR):
        if not names:
            shutil.rmtree(PENDING_CONF_DIR, ignore_errors=True)
        else:
            for name in names:
                shutil.rmtree(os.path.join(PENDING_CONF_DIR, name),
                              ignore_errors=True)
    else:
        filesystem.mkdir(PENDING_CONF_DIR, 0o755, True)

    # Format common hooks arguments
    common_args = [1 if force else 0, 1 if dry_run else 0]

    # Execute hooks for pre-regen
    pre_args = ['pre', ] + common_args

    def _pre_call(name, priority, path, args):
        # create the pending conf directory for the service
        service_pending_path = os.path.join(PENDING_CONF_DIR, name)
        filesystem.mkdir(service_pending_path, 0o755, True, uid='root')

        # return the arguments to pass to the script
        return pre_args + [service_pending_path, ]

    # Don't regen SSH if not specifically specified
    if not names:
        names = hook_list('conf_regen', list_by='name',
                          show_info=False)['hooks']
        names.remove('ssh')

    pre_result = hook_callback('conf_regen', names, pre_callback=_pre_call)

    # Update the services name
    names = pre_result['succeed'].keys()

    if not names:
        raise YunohostError('service_regenconf_failed',
                            services=', '.join(pre_result['failed']))

    # Set the processing method
    _regen = _process_regen_conf if not dry_run else lambda *a, **k: True

    operation_logger.related_to = []

    # Iterate over services and process pending conf
    for service, conf_files in _get_pending_conf(names).items():
        if not dry_run:
            operation_logger.related_to.append(('service', service))

        logger.debug(m18n.n(
            'service_regenconf_pending_applying' if not dry_run else
            'service_regenconf_dry_pending_applying',
            service=service))

        conf_hashes = _get_conf_hashes(service)
        succeed_regen = {}
        failed_regen = {}

        for system_path, pending_path in conf_files.items():
            logger.debug("processing pending conf '%s' to system conf '%s'",
                         pending_path, system_path)
            conf_status = None
            regenerated = False

            # Get the diff between files
            conf_diff = _get_files_diff(
                system_path, pending_path, True) if with_diff else None

            # Check if the conf must be removed
            to_remove = True if os.path.getsize(pending_path) == 0 else False

            # Retrieve and calculate hashes
            system_hash = _calculate_hash(system_path)
            saved_hash = conf_hashes.get(system_path, None)
            new_hash = None if to_remove else _calculate_hash(pending_path)

            # -> system conf does not exists
            if not system_hash:
                if to_remove:
                    logger.debug("> system conf is already removed")
                    os.remove(pending_path)
                    continue
                if not saved_hash or force:
                    if force:
                        logger.debug("> system conf has been manually removed")
                        conf_status = 'force-created'
                    else:
                        logger.debug("> system conf does not exist yet")
                        conf_status = 'created'
                    regenerated = _regen(
                        system_path, pending_path, save=False)
                else:
                    logger.info(m18n.n(
                        'service_conf_file_manually_removed',
                        conf=system_path))
                    conf_status = 'removed'

            # -> system conf is not managed yet
            elif not saved_hash:
                logger.debug("> system conf is not managed yet")
                if system_hash == new_hash:
                    logger.debug("> no changes to system conf has been made")
                    conf_status = 'managed'
                    regenerated = True
                elif not to_remove:
                    # If the conf exist but is not managed yet, and is not to be removed,
                    # we assume that it is safe to regen it, since the file is backuped
                    # anyway (by default in _regen), as long as we warn the user
                    # appropriately.
                    logger.info(m18n.n('service_conf_now_managed_by_yunohost',
                                       conf=system_path))
                    regenerated = _regen(system_path, pending_path)
                    conf_status = 'new'
                elif force:
                    regenerated = _regen(system_path)
                    conf_status = 'force-removed'
                else:
                    logger.info(m18n.n('service_conf_file_kept_back',
                                       conf=system_path, service=service))
                    conf_status = 'unmanaged'

            # -> system conf has not been manually modified
            elif system_hash == saved_hash:
                if to_remove:
                    regenerated = _regen(system_path)
                    conf_status = 'removed'
                elif system_hash != new_hash:
                    regenerated = _regen(system_path, pending_path)
                    conf_status = 'updated'
                else:
                    logger.debug("> system conf is already up-to-date")
                    os.remove(pending_path)
                    continue

            else:
                logger.debug("> system conf has been manually modified")
                if system_hash == new_hash:
                    logger.debug("> new conf is as current system conf")
                    conf_status = 'managed'
                    regenerated = True
                elif force:
                    regenerated = _regen(system_path, pending_path)
                    conf_status = 'force-updated'
                else:
                    logger.warning(m18n.n(
                        'service_conf_file_manually_modified',
                        conf=system_path))
                    conf_status = 'modified'

            # Store the result
            conf_result = {'status': conf_status}
            if conf_diff is not None:
                conf_result['diff'] = conf_diff
            if regenerated:
                succeed_regen[system_path] = conf_result
                conf_hashes[system_path] = new_hash
                if os.path.isfile(pending_path):
                    os.remove(pending_path)
            else:
                failed_regen[system_path] = conf_result

        # Check for service conf changes
        if not succeed_regen and not failed_regen:
            logger.debug(m18n.n('service_conf_up_to_date', service=service))
            continue
        elif not failed_regen:
            logger.success(m18n.n(
                'service_conf_updated' if not dry_run else
                'service_conf_would_be_updated',
                service=service))

        if succeed_regen and not dry_run:
            _update_conf_hashes(service, conf_hashes)

        # Append the service results
        result[service] = {
            'applied': succeed_regen,
            'pending': failed_regen
        }

    # Return in case of dry run
    if dry_run:
        return result

    # Execute hooks for post-regen
    post_args = ['post', ] + common_args

    def _pre_call(name, priority, path, args):
        # append coma-separated applied changes for the service
        if name in result and result[name]['applied']:
            regen_conf_files = ','.join(result[name]['applied'].keys())
        else:
            regen_conf_files = ''
        return post_args + [regen_conf_files, ]

    hook_callback('conf_regen', names, pre_callback=_pre_call)

    operation_logger.success()

    return result


def _run_service_command(action, service):
    """
    Run services management command (start, stop, enable, disable, restart, reload)

    Keyword argument:
        action -- Action to perform
        service -- Service name

    """
    services = _get_services()
    if service not in services.keys():
        raise YunohostError('service_unknown', service=service)

    possible_actions = ['start', 'stop', 'restart', 'reload', 'reload-or-restart', 'enable', 'disable']
    if action not in possible_actions:
        raise ValueError("Unknown action '%s', available actions are: %s" % (action, ", ".join(possible_actions)))

    cmd = 'systemctl %s %s' % (action, service)

    need_lock = services[service].get('need_lock', False) \
        and action in ['start', 'stop', 'restart', 'reload', 'reload-or-restart']

    try:
        # Launch the command
        logger.debug("Running '%s'" % cmd)
        p = subprocess.check_call(cmd.split(), stderr=subprocess.STDOUT)
        # If this command needs a lock (because the service uses yunohost
        # commands inside), find the PID and add a lock for it
        if need_lock:
            PID = _give_lock(action, service, p)
        # Wait for the command to complete
        p.communicate()

    except subprocess.CalledProcessError as e:
        # TODO: Log output?
        logger.warning(m18n.n('service_cmd_exec_failed', command=' '.join(e.cmd)))
        return False

    finally:
        # Remove the lock if one was given
        if need_lock and PID != 0:
            _remove_lock(PID)

    return True


def _give_lock(action, service, p):

    # Depending of the action, systemctl calls the PID differently :/
    if action == "start" or action == "restart":
        systemctl_PID_name = "MainPID"
    else:
        systemctl_PID_name = "ControlPID"

    cmd_get_son_PID = "systemctl show %s -p %s" % (service, systemctl_PID_name)
    son_PID = 0
    # As long as we did not found the PID and that the command is still running
{'fraction_non_alphanumeric': 0.08792099792099792, 'fraction_numerical': 0.0035135135135135136, 'mean_word_length': 2.2138037014765817, 'pattern_counts': {'":': 2, '<': 3, '<?xml version=': 0, '>': 25, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6476579', 'n_tokens_mistral': 13500, 'n_tokens_neox': 12886, 'n_words': 3809}