118 lines
3.7 KiB
Python
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())
|