119 lines
4.8 KiB
Python
119 lines
4.8 KiB
Python
"""check_process: 查看/终止 shell·run_python `background=true` 启动的后台进程。
|
|
|
|
host in-process 工具(不进 CONTAINER_TOOLS):状态文件在宿主侧 user_root/.zcbot_procs,
|
|
docker 模式的容器探测/回收走宿主 docker CLI —— 两种 backend 一个工具通吃。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from core import procs
|
|
|
|
from .base import Tool, compact_tool_output
|
|
|
|
|
|
class CheckProcessTool(Tool):
|
|
name = "check_process"
|
|
description = (
|
|
"Check status / output of background processes started with background=true "
|
|
"(shell or run_python), or kill one. "
|
|
"Call without proc_id to list this task's background processes. "
|
|
"With proc_id: returns status + tail of the process log. "
|
|
"If still running: do NOT poll in a tight loop — do other useful work, or end "
|
|
"the turn and tell the user to ask later ('跑完了吗'). "
|
|
"action='kill' force-terminates the process."
|
|
)
|
|
parameters = {
|
|
"type": "object",
|
|
"properties": {
|
|
"proc_id": {
|
|
"type": "string",
|
|
"description": "后台进程 id(启动时返回的 p 开头短串)。缺省 = 列出本 task 全部后台进程。",
|
|
},
|
|
"action": {
|
|
"type": "string",
|
|
"enum": ["status", "kill"],
|
|
"description": "status(默认)= 查状态+日志尾部;kill = 强制终止该进程。",
|
|
},
|
|
"tail_bytes": {
|
|
"type": "integer",
|
|
"description": "返回日志尾部的最大字节数,默认 4000,上限 20000。",
|
|
},
|
|
},
|
|
"required": [],
|
|
}
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
task_id: str,
|
|
base_dir: Optional[Path] = None,
|
|
user_root: Optional[Path] = None,
|
|
) -> None:
|
|
super().__init__(base_dir, user_root=user_root)
|
|
self.task_id = str(task_id)
|
|
|
|
@property
|
|
def _anchor(self) -> Path:
|
|
# 常规部署有 user_root;CLI --working-dir 指外部目录时退 base_dir
|
|
return self.user_root or self.base_dir
|
|
|
|
def execute(
|
|
self,
|
|
proc_id: Optional[str] = None,
|
|
action: str = "status",
|
|
tail_bytes: int = 4000,
|
|
) -> str:
|
|
if not proc_id:
|
|
items = procs.list_procs(self._anchor, self.task_id)
|
|
if not items:
|
|
return "本 task 没有后台进程。用 shell/run_python 的 background=true 可启动。"
|
|
return "后台进程列表:\n" + "\n".join(procs.format_proc_line(m) for m in items)
|
|
|
|
d = procs.proc_dir(self._anchor, self.task_id, proc_id.strip())
|
|
if d is None or not d.is_dir():
|
|
return f"[Error] 后台进程不存在: {proc_id!r}(用 check_process 不带参数列出现有进程)"
|
|
meta = procs.read_meta(d)
|
|
if meta is None:
|
|
return f"[Error] 后台进程元数据损坏: {proc_id!r}"
|
|
|
|
if action == "kill":
|
|
msg = procs.kill_proc(meta, d)
|
|
return f"[check_process] {proc_id}: {msg}"
|
|
|
|
st, ec = procs.status_of(meta, d)
|
|
tail_bytes = max(500, min(int(tail_bytes or 4000), 20_000))
|
|
tail = procs.tail_log(d, max_bytes=tail_bytes)
|
|
|
|
# docker 模式:已结束但专用容器还在空转 → 顺手回收(幂等,sweep 也会兜底)
|
|
if st != "running" and meta.get("backend") == "docker":
|
|
name = str(meta.get("container") or "")
|
|
if name:
|
|
try:
|
|
subprocess.run(["docker", "rm", "-f", name],
|
|
capture_output=True, timeout=30)
|
|
except (OSError, subprocess.TimeoutExpired):
|
|
pass
|
|
|
|
header = f"[check_process] {proc_id} · {meta.get('kind')} · {meta.get('command', '')[:150]}"
|
|
if st == "running":
|
|
elapsed = procs._fmt_elapsed(time.time() - float(meta.get("created_ts") or time.time()))
|
|
body = (
|
|
f"状态: running(已运行 {elapsed},上限 {meta.get('timeout_s')}s)\n"
|
|
f"--- 日志尾部 ---\n{tail}\n"
|
|
f"提示:仍在运行。别循环轮询 —— 先做别的工作,或结束回合告知用户稍后询问进度。"
|
|
)
|
|
elif st == "finished":
|
|
note = " (timeout killed)" if ec == 124 else (" (killed)" if meta.get("killed") else "")
|
|
body = f"状态: finished, exit_code={ec}{note}\n--- 日志尾部 ---\n{tail}"
|
|
else:
|
|
body = (
|
|
f"状态: lost(进程已不在,且未留下退出码 —— 可能宿主重启把它带走了)\n"
|
|
f"--- 日志尾部(截至中断) ---\n{tail}\n"
|
|
f"如需结果请重新以 background=true 启动。"
|
|
)
|
|
return compact_tool_output(f"{header}\n{body}")
|