#!/usr/bin/env python3
"""Immutable Linux PID-1 supervisor; host mode is development-only."""
import ctypes
import json
import os
import select
import signal
import socket
import subprocess
import sys
import time
from pathlib import Path

libc = ctypes.CDLL(None, use_errno=True)
# Adopt double-forked descendants on hosts, as well as under container PID 1.
if libc.prctl(36, 1, 0, 0, 0) != 0:  # PR_SET_CHILD_SUBREAPER
    sys.exit(1)
if libc.prctl(4, 0, 0, 0, 0) != 0:  # PR_SET_DUMPABLE: deny same-UID proc memory/fd access
    sys.exit(1)
container = os.getpid() == 1
if not container and '--dev' not in sys.argv:
    sys.stderr.write('Supervisor requires container PID 1; use --dev only for host tests.\n')
    sys.exit(1)
# PID 1 ignores namespace-local SIGKILL/SIGSTOP. Other signals terminate the
# run or are ignored; none can pause or extend the deadline. No exec after prctl.
for sig in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP, signal.SIGUSR1, signal.SIGUSR2):
    signal.signal(sig, signal.SIG_IGN)
started = time.monotonic()
deadline = started + 180
parent, child = socket.socketpair()
child.set_inheritable(True)
root = Path(__file__).resolve().parent
entry = root / 'dist' / 'bridge.js'
args = ['node', str(entry)]
env = {'PATH': '/usr/local/bin:/usr/bin:/bin', 'HOME': '/home/agent', 'LANG': 'C.UTF-8',
       'AGENT_SUPERVISOR_FD': str(child.fileno())}
if '--dev' in sys.argv:
    env['PATH'] = os.environ.get('PATH', env['PATH'])
    env['HOME'] = os.environ.get('HOME', '/tmp')
    env['AGENT_WORK_DIR'] = os.environ.get('AGENT_WORK_DIR', '/work')
bridge = subprocess.Popen(args, env=env, pass_fds=(child.fileno(),))
child.close()
parent.setblocking(False)
protected = {os.getpid(), bridge.pid}
buffer = b''


def descendants():
    """Never signal unrelated host processes. Adopted children retain this ancestry."""
    parents = {}
    for item in Path('/proc').iterdir():
        if item.name.isdecimal():
            try:
                fields = (item / 'stat').read_text().rsplit(')', 1)[1].split()
                parents[int(item.name)] = int(fields[1])
            except (OSError, ValueError, IndexError):
                pass
    owned = {os.getpid()}
    while True:
        extra = {pid for pid, ppid in parents.items() if ppid in owned} - owned
        if not extra:
            break
        owned.update(extra)
    return owned - protected


def reap():
    # Re-scan until every descendant is gone, including children forked during
    # termination. Reap only adopted children, leaving Popen to reap the bridge.
    while time.monotonic() < deadline:
        victims = descendants()
        if not victims:
            return
        for pid in victims:
            try:
                os.kill(pid, signal.SIGKILL)
            except ProcessLookupError:
                pass
            try:
                os.waitpid(pid, os.WNOHANG)
            except ChildProcessError:
                pass
        time.sleep(0.01)
    raise TimeoutError()


code = 1
try:
    deadline_set = False
    collecting = False
    while time.monotonic() < deadline:
        if bridge.poll() is not None:
            code = bridge.returncode
            break
        readable, _, _ = select.select([parent], [], [], min(0.05, max(0, deadline - time.monotonic())))
        if not readable:
            continue
        data = parent.recv(4096)
        if not data:
            try:
                code = bridge.wait(timeout=min(1, max(0.01, deadline - time.monotonic())))
            except subprocess.TimeoutExpired:
                pass
            break
        buffer += data
        if len(buffer) > 8192:
            break
        while b'\n' in buffer:
            line, buffer = buffer.split(b'\n', 1)
            msg = json.loads(line)
            if not isinstance(msg, dict):
                raise ValueError()
            if msg.get('type') == 'set_deadline' and not deadline_set:
                ms = msg.get('remaining_ms')
                if type(ms) is not int or not 1 <= ms <= 180000:
                    raise ValueError()
                deadline = min(deadline, time.monotonic() + ms / 1000)
                deadline_set = True
            elif msg == {'type': 'reap_descendants'} and not collecting:
                reap()
                collecting = True
                parent.sendall(b'{"type":"reaped"}\n')
            else:
                raise ValueError()
except (OSError, ValueError, TimeoutError):
    pass
finally:
    if container and time.monotonic() >= deadline:
        sys.exit(1)  # Kernel destroys the namespace; do not add cleanup time.
    # Killing PID 1 ends the container namespace. Host development cleanup is
    # still restricted to descendants of this supervisor.
    protected.discard(bridge.pid)
    deadline = max(deadline, time.monotonic() + 1)
    try:
        reap()
    except (OSError, TimeoutError):
        pass
    try:
        bridge.wait(timeout=1)
    except subprocess.TimeoutExpired:
        bridge.kill()
    parent.close()
sys.exit(code if code >= 0 else 1)
