"""bg proc wrapper:被 detach 启动的独立进程,负责跑子进程 + 写 exit_code。 协议见 core/procs.py。设计约束: - **stdlib-only、无 zcbot import**:用 `python core/proc_wrapper.py ` 直跑,不依赖 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())