#!/usr/share/ucs-test/runner python
## desc: Test the UMC backend process killing
## bugs: [34593]
## exposure: dangerous

import sys
sys.path.insert(0, '.')
from TestUMCSystemModule import TestUMCSystem

from psutil import Process
from os import wait4, WNOHANG, WTERMSIG, fork
import signal
from time import sleep

import univention.testing.utils as utils


class TestUMCProcessKilling(TestUMCSystem):

    def __init__(self):
        """Test Class constructor"""
        super(TestUMCProcessKilling, self).__init__()
        self.Proc = None

    def make_kill_request(self, signal, pids):
        """
        Applies the kill action with a signal 'signal' to the list of 'pids'
        provided by making a UMC request 'top/kill' with respective options.
        """
        try:
            request_result = self.Connection.request('top/kill',
                                                     {'signal': signal,
                                                      'pid': pids})
            if not request_result:
                utils.fail("Request 'top/kill' with signal '%s' for "
                           "pids '%s' failed, no 'True' in response. "
                           "Hostname: '%s'."
                           % (signal, pids, self.hostname))
        except Exception as exc:
            utils.fail("Exception while making 'top/kill' request: '%s'"
                       % exc)

    def query_process_exists(self, pid):
        """
        Checks if process with a provided 'pid' exists
        by making the UMC request 'top/query'.
        Returns True when exists.
        """
        request_result = self.make_top_query_request()
        for result in request_result:
            try:
                if result['pid'] == pid:
                    if sys.executable in result['command']:
                        return True
            except KeyError as exc:
                utils.fail("Failed to find key '%s' in"
                           "the 'top/query' request respone from UMC: %r"
                           % (exc, result))
        return False

    def force_process_kill(self):
        """
        Kills process with SIGKILL signal if not yet terminated.
        """
        if self.Proc.is_running():
            print("Created process with pid '%s' was not terminated, "
                  "forcing kill" % self.Proc.pid)
            self.Proc.kill()
            _pid, exit_status, _res_usage = wait4(self.Proc.pid, WNOHANG)
            if WTERMSIG(exit_status) != signal.SIGKILL:
                print("The exit status while force kill is 0x%x "
                      "instead of 'SIGKILL'(%d)."
                      % (exit_status, signal.SIGKILL))

    def create_process(self, ignore_sigterm=False):
        """
        Initiates a simple test process that should be killed after by forking.
        Creates a psutil Process class to check running state
        before terminating. Also returns process id (pid).
        """
        pid = fork()
        if pid:  # parent
            self.Proc = Process(pid)
            return pid
        else:  # child under test
            if ignore_sigterm:
                signal.signal(signal.SIGTERM, signal.SIG_IGN)
            sleep(30)
            sys.exit(0)

    def test(self, signame, ignore_signal=False):
        print "Testing UMC process killing with signal '%s'" % signame
        signum = getattr(signal, signame)
        try:
            pid = self.create_process(ignore_signal)
            if self.query_process_exists(pid):
                self.make_kill_request(signame, [pid])
                _pid, exit_status, _res_usage = wait4(pid, WNOHANG)
                if ignore_signal != (WTERMSIG(exit_status) != signum):
                    utils.fail("Process exit status is 0x%x instead of "
                               "%s(%d)." % (exit_status, signame, signum))
                if ignore_signal != self.query_process_exists(pid):
                    utils.fail("Process did not terminate after request "
                               "with signal %s" % signame)
            else:
                utils.fail("The process is not running right after creation")
        finally:
            self.force_process_kill()

    def main(self):
        """
        Method to start the test of the UMC backend
        process killing functionality.
        """
        self.get_ucr_credentials()
        self.create_connection_authenticate()

        # case 1: killing process using signal 'SIGTERM'
        self.test('SIGTERM')

        # case 2: killing process using signal 'SIGKILL'
        self.test('SIGKILL')

        # case 3: killing unwilling process using signal 'SIGTERM'
        self.test('SIGTERM', ignore_signal=True)


if __name__ == '__main__':
    TestUMC = TestUMCProcessKilling()
    sys.exit(TestUMC.main())
