#
# Rumba - Process lifecycle management
#
#    Copyright (C) 2017-2026 imec
#
#    Dimitri Staessens <dimitri.staessens@ugent.be>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., http://www.fsf.org/about/contact/.
#

import time
import signal

import rumba.log as log

logger = log.get_logger(__name__)


class Process:
    """Managed background process with lifecycle operations.

    Wraps either a local subprocess.Popen handle or a remote PID
    tracked via node.execute_command().
    """

    def __init__(self, node, pid, command, popen=None):
        """
        :param node: The node the process runs on (or None for local).
        :param pid: The process ID.
        :param command: The command that was started.
        :param popen: A subprocess.Popen handle (local processes only).
        """
        self.node = node
        self.pid = int(pid) if pid is not None else None
        self.command = command
        self._popen = popen

    def is_alive(self):
        """Check if the process is still running.

        :returns: True if the process is alive.
        """
        if self._popen is not None:
            return self._popen.poll() is None

        if self.node is None or self.pid is None:
            return False

        result = self.node.execute_command(
            'kill -0 %d' % self.pid, as_root=True, check=False)
        return result.succeeded

    def wait(self, timeout=None):
        """Wait for the process to exit.

        :param timeout: Maximum seconds to wait (None = forever).
        :returns: The exit code, or None if still running after timeout.
        """
        if self._popen is not None:
            try:
                return self._popen.wait(timeout=timeout)
            except Exception:
                return None

        # For remote processes, poll until dead
        deadline = None if timeout is None else time.time() + timeout
        while True:
            if not self.is_alive():
                return 0  # Process is dead; no way to get real exit code
            if deadline is not None and time.time() >= deadline:
                return None
            time.sleep(0.5)

    def kill(self, sig=signal.SIGTERM):
        """Send a signal to the process.

        :param sig: Signal number (default SIGTERM).
        """
        if self._popen is not None:
            self._popen.send_signal(sig)
            return

        if self.node is None or self.pid is None:
            return

        self.node.execute_command(
            'kill -%d %d' % (sig, self.pid), as_root=True, check=False)

    def output(self):
        """Read stdout from the process (local Popen only).

        :returns: stdout string, or None if not available.
        """
        if self._popen is not None:
            try:
                out, _ = self._popen.communicate(timeout=0)
                return out
            except Exception:
                return None
        return None

    def __repr__(self):
        loc = "local" if self._popen else (
            self.node.name if self.node else "?")
        return "Process(pid=%s, cmd='%s', on=%s)" % (
            self.pid, self.command, loc)
