#!/usr/bin/python3 -I
"""Unprivileged core supervisor; closes a session when its GUI owner disappears."""
import argparse
import ctypes
import json
import os
import signal
import stat
import subprocess
import sys
import time

def stamp(pid):
    if os.stat("/proc/" + str(pid)).st_uid != os.getuid():
        raise ValueError("owner mismatch")
    with open("/proc/" + str(pid) + "/stat", encoding="ascii") as stream:
        value = stream.read(8192)
    return value[value.rfind(")") + 2:].split()[19]

def interrupted(_signal, _frame):
    raise KeyboardInterrupt

def parent_signal():
    parent = os.getppid()
    if ctypes.CDLL(None, use_errno=True).prctl(1, signal.SIGTERM, 0, 0, 0) != 0:
        raise ValueError("watchdog unavailable")
    if os.getppid() != parent:
        raise KeyboardInterrupt

def restore_proxy(path):
    if not os.path.isfile(path):
        return
    with open(path, encoding="utf-8") as stream:
        settings = json.loads(stream.read(16385))
    if not isinstance(settings, list) or len(settings) > 8:
        raise ValueError("journal limit")
    for item in reversed(settings):
        schema, key = item["Schema"], item["Key"]
        allowed = schema == "org.gnome.system.proxy" and key == "mode" or schema in ["org.gnome.system.proxy." + name for name in ["http", "https", "socks"]] and key in ["host", "port"]
        if not allowed or any(not isinstance(item[name], str) or len(item[name]) > 1024 for name in ["Previous", "Applied"]):
            raise ValueError("invalid journal")
        current = subprocess.check_output(["/usr/bin/gsettings", "get", schema, key], timeout=5, stderr=subprocess.DEVNULL).decode().strip()
        if current == item["Applied"]:
            subprocess.run(["/usr/bin/gsettings", "set", schema, key, item["Previous"]], timeout=5, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    os.unlink(path)

def main():
    signal.signal(signal.SIGTERM, interrupted)
    signal.signal(signal.SIGINT, interrupted)
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("--owner-pid", type=int, required=True)
    parser.add_argument("--journal", required=True)
    parser.add_argument("--core", choices=["xray", "mihomo", "sing-box", "lumen-awg"], required=True)
    parser.add_argument("arguments", nargs=argparse.REMAINDER)
    args = parser.parse_args()
    if os.geteuid() == 0 or args.owner_pid < 2:
        raise ValueError("must run as desktop user")
    owner = stamp(args.owner_pid)
    parent_signal()
    binary = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "bin", args.core)
    arguments = args.arguments[1:] if args.arguments[:1] == ["--"] else args.arguments
    process = None
    try:
        process = subprocess.Popen([binary] + arguments, stdin=subprocess.DEVNULL, preexec_fn=parent_signal)
        while process.poll() is None and stamp(args.owner_pid) == owner:
            time.sleep(0.5)
    finally:
        if process is not None and process.poll() is None:
            process.terminate()
            try:
                process.wait(timeout=5)
            except subprocess.TimeoutExpired:
                process.kill()
                process.wait()
        # Only this user's GNOME settings are restored; external changes win.
        restore_proxy(args.journal)

if __name__ == "__main__":
    try:
        main()
    except (Exception, KeyboardInterrupt):
        sys.exit(1)
