"""bg proc wrapper:被 detach 启动的独立进程,负责跑子进程 + 写 exit_code。

协议见 core/procs.py。设计约束:
- **stdlib-only、无 zcbot import**:用 `python core/proc_wrapper.py <proc_dir>`
  直跑,不依赖 PYTHONPATH / 虚拟环境激活(解释器路径由 launch_host 用
  sys.executable 定死,与 web 进程同一个 venv)
- 自身 stdout/stderr 已被 launch_host 接到 DEVNULL,不打印任何东西;
  所有输出走 output.log(bytes 写,绕开 Windows GBK console 编码问题)
- zcbot 重启/蓝绿切换不影响本进程(detach + 新 session/进程组)

流程:读 proc.json → 起子进程(stdout+stderr → output.log)→ 回写 child_pid
→ wait(timeout_s) → 超时杀进程树记 124 → 截断日志到最后 10MB → 写 exit_code。
exit_code 文件的出现是唯一"已结束"信号,必须最后写。
"""
from __future__ import annotations

import json
import os
import subprocess
import sys
from pathlib import Path

MAX_LOG_BYTES = 10 * 1024 * 1024


def _read_meta(d: Path) -> dict:
    return json.loads((d / "proc.json").read_text(encoding="utf-8"))


def _write_meta(d: Path, meta: dict) -> None:
    tmp = d / "proc.json.tmp"
    tmp.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
    os.replace(tmp, d / "proc.json")


def _kill_tree(child: subprocess.Popen) -> None:
    if os.name == "posix":
        import signal
        try:
            os.killpg(child.pid, signal.SIGKILL)
        except (ProcessLookupError, PermissionError, OSError):
            try:
                child.kill()
            except OSError:
                pass
    else:
        subprocess.run(
            ["taskkill", "/PID", str(child.pid), "/T", "/F"],
            capture_output=True,
        )


def _truncate_log(p: Path, cap: int = MAX_LOG_BYTES) -> None:
    try:
        size = p.stat().st_size
        if size <= cap:
            return
        with open(p, "rb") as f:
            f.seek(size - cap)
            data = f.read()
        with open(p, "wb") as f:
            f.write(b"[proc_wrapper] log truncated to last %d bytes\n" % cap)
            f.write(data)
    except OSError:
        pass


def main() -> int:
    d = Path(sys.argv[1])
    meta = _read_meta(d)
    timeout_s = int(meta.get("timeout_s") or 7200)
    log = open(d / "output.log", "ab", buffering=0)

    spawn_kw: dict = dict(
        cwd=meta.get("cwd") or None,
        stdin=subprocess.DEVNULL,
        stdout=log,
        stderr=subprocess.STDOUT,
    )
    # 子进程独立进程组:超时/kill 时能整树带走(posix killpg / win taskkill /T)
    if os.name == "posix":
        spawn_kw["start_new_session"] = True
    else:
        spawn_kw["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP

    try:
        if meta.get("argv"):
            child = subprocess.Popen(meta["argv"], **spawn_kw)
        else:
            child = subprocess.Popen(meta["shell_cmd"], shell=True, **spawn_kw)
    except Exception as e:  # 启动失败也要写 exit_code,否则状态永远悬着
        log.write(f"[proc_wrapper] spawn failed: {type(e).__name__}: {e}\n".encode("utf-8"))
        log.close()
        (d / "exit_code").write_text("127", encoding="utf-8")
        return 127

    meta["child_pid"] = child.pid
    _write_meta(d, meta)

    try:
        ec = child.wait(timeout=timeout_s)
    except subprocess.TimeoutExpired:
        _kill_tree(child)
        try:
            child.wait(timeout=10)
        except subprocess.TimeoutExpired:
            pass
        log.write(b"\n[proc_wrapper] timeout after %ds, process tree killed\n" % timeout_s)
        ec = 124
    log.close()
    _truncate_log(d / "output.log")
    (d / "exit_code").write_text(str(ec), encoding="utf-8")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
