zcbot/core/proc_wrapper.py

118 lines
3.7 KiB
Python

"""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())