feat(windows): 沙箱最小根文件系统与崩溃排查调试埋点
This commit is contained in:
parent
77be044c44
commit
04c9db8a5b
@ -252,15 +252,15 @@ class ModeMixin:
|
||||
"- 命令解释器:Linux bash(例如可用 ls、cat、grep、find、git、python3、curl 等常见 Linux 工具)\n"
|
||||
f"- 当前工作区:{ws_win}(Windows 视角)= {ws_wsl}(沙箱内视角)\n"
|
||||
f"- 命令的工作目录已默认落在 {ws_wsl},操作工作区内文件请直接使用相对路径\n"
|
||||
"- 引用其他 Windows 路径时必须转换:盘符小写、反斜杠变正斜杠,例如 D:\\tools\\a.txt → /mnt/d/tools/a.txt\n"
|
||||
"- 沙箱内只能访问当前工作区:其他 Windows 路径(如 D:\\tools\\a.txt)未挂载进沙箱、根本不存在,需要操作时请把文件复制进工作区,或请用户调整工作区\n"
|
||||
"- 【禁止】Windows 程序在沙箱内不存在:cmd、powershell、bat 脚本、.exe、Windows 版 python/node 均无法运行\n"
|
||||
"- 【禁止】不要写 Windows 风格路径,bash 会把反斜杠当作转义符\n"
|
||||
"\n"
|
||||
"### 文件权限\n"
|
||||
"- 工作区内:可读可写\n"
|
||||
"- 工作区外:全部只读(包括 Windows 各盘与 Linux 系统目录),写入会报 Read-only file system\n"
|
||||
"- /tmp 与 HOME 目录可写(npm/pip 等工具的缓存可正常使用)\n"
|
||||
"- 用户敏感目录(~/.ssh 等凭据)已被屏蔽,不可读取"
|
||||
"- 工作区外:数据在沙箱内不存在(Windows 各盘与其余目录均未挂载),访问报 No such file or directory;仅通往工作区的空壳目录链可见\n"
|
||||
"- Linux 系统目录(/bin /usr /lib /etc 等,沙箱自带工具链):只读\n"
|
||||
"- /tmp 与 HOME 目录可写(npm/pip 等工具的缓存可正常使用)"
|
||||
)
|
||||
|
||||
def _build_windows_mode_rules(self, mode: str) -> str:
|
||||
@ -268,7 +268,7 @@ class ModeMixin:
|
||||
if mode == "sandbox":
|
||||
return (
|
||||
"- 所有命令默认在 WSL2 Linux 沙箱中执行\n"
|
||||
"- 命令报 Read-only file system / Permission denied:说明操作超出沙箱授权边界,这是用户刻意设置的。不要尝试绕过(换路径、提权等),应向用户说明并请求调整权限或切换执行环境\n"
|
||||
"- 命令报 Read-only file system / Permission denied,或访问工作区外路径报 No such file or directory:说明操作超出沙箱授权边界,这是用户刻意设置的。不要尝试绕过(换路径、提权等),应向用户说明并请求调整权限或切换执行环境\n"
|
||||
"- 命令报 command not found:先检查是否误用了 Windows 程序;Linux 工具缺失时可建议用户安装\n"
|
||||
"- 需要 Windows 原生工具链的任务(如为 Windows 版 node 安装依赖):明确告知用户需切换到“完全访问权限”执行"
|
||||
)
|
||||
@ -297,11 +297,11 @@ class ModeMixin:
|
||||
"你后续的终端命令将不再在 Windows 中执行,而是在 WSL2 Linux 沙箱中以 bash 执行。请立即调整命令写法:\n"
|
||||
"\n"
|
||||
"1. 解释器变为 Linux bash:使用 ls、cat、grep 等 Linux 命令;cmd、powershell、.exe 等 Windows 程序从此刻起不可用\n"
|
||||
f"2. 路径必须转换:当前工作区为 {ws_wsl}(即 Windows 的 {ws_win}),工作目录已默认落在此处,工作区内操作请用相对路径;其他 Windows 路径按 D:\\a\\b → /mnt/d/a/b 转换\n"
|
||||
f"2. 路径必须转换:当前工作区为 {ws_wsl}(即 Windows 的 {ws_win}),工作目录已默认落在此处,工作区内操作请用相对路径;沙箱内只能访问工作区,其他 Windows 路径均未挂载、无法读写\n"
|
||||
"3. 禁止再写 Windows 风格路径,反斜杠会被 bash 当作转义符\n"
|
||||
"4. 写权限:仅工作区、/tmp、HOME 可写,其余全部只读\n"
|
||||
"4. 写权限:仅工作区、/tmp、HOME 可写;Linux 系统目录只读;其余路径在沙箱内不存在\n"
|
||||
f"{network_part}"
|
||||
f"{final_no}. 遇到 Read-only file system / Permission denied 不要绕过,向用户说明并请求调整\n"
|
||||
f"{final_no}. 遇到 Read-only file system / Permission denied / 区外路径 No such file or directory 不要绕过,向用户说明并请求调整\n"
|
||||
"\n"
|
||||
"此前的命令如果是按 Windows 环境编写的,请按上述规则改写后重新执行。"
|
||||
)
|
||||
|
||||
@ -18,6 +18,11 @@ from modules.host_sandbox_runner import (
|
||||
build_host_sandbox_plan,
|
||||
host_sandbox_enabled,
|
||||
)
|
||||
from modules import sbx_debug # [SBX-DEBUG]
|
||||
try: # [SBX-DEBUG]
|
||||
sbx_debug.install_ctrl_event_probes("bg_manager") # [SBX-DEBUG]
|
||||
except Exception: # [SBX-DEBUG]
|
||||
pass # [SBX-DEBUG]
|
||||
|
||||
|
||||
TERMINAL_STATUSES = {"completed", "failed", "timeout", "cancelled"}
|
||||
@ -289,6 +294,7 @@ class BackgroundCommandManager:
|
||||
rec["pid"] = process.pid
|
||||
rec["updated_at"] = time.time()
|
||||
self._processes[command_id] = process
|
||||
sbx_debug.sbx_log("BG-SPAWNED", pid=getattr(process, "pid", None), command=str(command)[:200], timeout=timeout, console_pids=sbx_debug.console_pids()) # [SBX-DEBUG]
|
||||
|
||||
def _reader(stream, collector, rec_key: str):
|
||||
try:
|
||||
@ -322,6 +328,7 @@ class BackgroundCommandManager:
|
||||
status = "timeout"
|
||||
message = f"命令执行超时 ({timeout}秒)"
|
||||
# 跨平台终止:POSIX killpg(SIGINT→SIGKILL),Windows CTRL_BREAK→taskkill
|
||||
sbx_debug.sbx_log_stack("BG-TIMEOUT-TERMINATE", pid=getattr(process, "pid", None), timeout=timeout) # [SBX-DEBUG]
|
||||
self._terminate_pid(process.pid)
|
||||
try:
|
||||
process.wait(timeout=2)
|
||||
@ -429,6 +436,7 @@ class BackgroundCommandManager:
|
||||
return True
|
||||
|
||||
def _terminate_pid(self, pid: Any) -> bool:
|
||||
sbx_debug.sbx_log_stack("TERMINATE-PID", pid=pid) # [SBX-DEBUG]
|
||||
normalized = self._coerce_pid(pid)
|
||||
if not normalized:
|
||||
return False
|
||||
|
||||
@ -7,12 +7,11 @@ import shutil
|
||||
import subprocess
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from typing import Dict, List, Optional
|
||||
from modules.host_sandbox_policy import (
|
||||
get_macos_writable_paths,
|
||||
get_macos_deny_read_paths,
|
||||
get_macos_deny_read_regexes,
|
||||
get_windows_deny_read_paths,
|
||||
)
|
||||
|
||||
|
||||
@ -395,13 +394,21 @@ def _build_linux_common_plan(
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# Windows:WSL2 + bubblewrap 沙箱
|
||||
#
|
||||
# 设计要点(依据 .wsl-poc 概念验证,见 wsl2-sandbox-poc-report.md):
|
||||
# 设计要点(依据 .wsl-poc 与 .wsl-exp 两轮实验,见 wsl2-sandbox-poc-report.md
|
||||
# 与项目记忆 wsl_sandbox_minimal_root):
|
||||
# - 使用专用沙箱发行版(默认 astrion-sandbox),必须关闭 interop,
|
||||
# 否则沙箱内可经 cmd.exe 逃逸到 Windows 宿主机;
|
||||
# - 隔离原语与 Linux 方案同构:bwrap --unshare-all + ro-bind / + 工作区 bind;
|
||||
# - 最小根文件系统:只挂载发行版的 Linux 系统目录(/bin /sbin /usr /lib /etc,
|
||||
# 纯工具链、无用户数据)+ 工作区 bind;不挂载 / 本身与任何 /mnt/<盘>,
|
||||
# 工作区外的数据在命名空间内根本不存在(报 No such file or directory)。
|
||||
# bwrap 是挂载命名空间构造器而非访问过滤器,因此可以实现 macOS Seatbelt
|
||||
# 做不到的“指令正常运行 + 默认拒绝的完美读限制”;
|
||||
# - bwrap 为挂载点自动创建的中间父目录是会话内可写 tmpfs(不落盘、无安全
|
||||
# 问题但缺报错语义),启动后先 mount -o remount,ro / 恢复只读报错;
|
||||
# - 网络档位:full → --share-net;restricted(仅回环)/ none → 不 share-net
|
||||
# (unshare-net 下 lo 自动可用,语义对齐 macOS 的 restricted);
|
||||
# - 敏感路径用 --tmpfs(目录)/ --ro-bind /dev/null(文件)掩蔽;
|
||||
# - 敏感路径掩蔽清单(windows_deny_read_paths)不再需要:数据根本不进命名
|
||||
# 空间;该清单仍由 server/chat/permission.py 用于原生读工具的禁读判断;
|
||||
# - wsl.exe 的 localhost 代理警告经 stderr_ignore_regexes 由执行器过滤。
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
|
||||
@ -468,26 +475,6 @@ def _ensure_wsl_sandbox_distro() -> str:
|
||||
return name
|
||||
|
||||
|
||||
def _windows_deny_read_targets() -> List[Tuple[str, bool]]:
|
||||
"""敏感路径掩蔽清单:返回 ``(wsl路径, 是否目录)``,仅包含实际存在的路径。"""
|
||||
targets: List[Tuple[str, bool]] = []
|
||||
for raw in get_windows_deny_read_paths():
|
||||
try:
|
||||
p = Path(raw).expanduser()
|
||||
except Exception:
|
||||
continue
|
||||
if not p.is_absolute():
|
||||
continue
|
||||
try:
|
||||
if p.is_dir():
|
||||
targets.append((_win_path_to_wsl(p), True))
|
||||
elif p.exists():
|
||||
targets.append((_win_path_to_wsl(p), False))
|
||||
except OSError:
|
||||
continue
|
||||
return targets
|
||||
|
||||
|
||||
def _build_windows_bwrap_argv(
|
||||
ws_wsl: str,
|
||||
shell_cmd: List[str],
|
||||
@ -504,16 +491,26 @@ def _build_windows_bwrap_argv(
|
||||
# full → 共享网络;restricted(仅回环)与 none → unshare-net(lo 仍可用)
|
||||
if permission == NETWORK_PERMISSION_FULL:
|
||||
argv.append("--share-net")
|
||||
argv += ["--ro-bind", "/", "/"]
|
||||
# 最小根文件系统:只挂沙箱发行版的 Linux 系统目录(纯工具链、无用户数据)。
|
||||
# 不挂载 / 本身与任何 /mnt/<盘>——工作区外的数据在命名空间内不存在。
|
||||
# 注意:若日后改用 glibc 发行版(如 Ubuntu),需补 --ro-bind /lib64 /lib64。
|
||||
for sysdir in ("/bin", "/sbin", "/usr", "/lib", "/etc"):
|
||||
argv += ["--ro-bind", sysdir, sysdir]
|
||||
argv += (["--ro-bind"] if readonly else ["--bind"]) + [ws_wsl, ws_wsl]
|
||||
for target, is_dir in _windows_deny_read_targets():
|
||||
argv += ["--tmpfs", target] if is_dir else ["--ro-bind", "/dev/null", target]
|
||||
argv += [
|
||||
"--chdir", ws_wsl,
|
||||
"--proc", "/proc",
|
||||
"--dev", "/dev",
|
||||
"--tmpfs", "/tmp",
|
||||
"--tmpfs", "/var/tmp",
|
||||
"--dir", "/root",
|
||||
"--",
|
||||
# bwrap 为挂载点自动创建的中间父目录(如 /mnt/e)是会话内可写 tmpfs
|
||||
# (写入不落盘、退出即消失,无安全问题),但写入不报错、与 mac 的审批
|
||||
# 关键词语义不一致;启动后先把根 remount 为只读,再 exec 真正的命令。
|
||||
# $0="bwrap-sh" 仅作占位,$@ 从 shell_cmd 开始,exec "$@" 按 argv 原样
|
||||
# 透传,避免对用户命令做字符串拼接(切勿加 shift,否则会丢掉 argv[0])。
|
||||
"bash", "-c", 'mount -o remount,ro / 2>/dev/null; exec "$@"', "bwrap-sh",
|
||||
*shell_cmd,
|
||||
]
|
||||
return argv
|
||||
@ -534,8 +531,11 @@ def _build_windows_wsl_plan(
|
||||
argv = _build_windows_bwrap_argv(ws_wsl, shell_cmd, readonly, network_permission)
|
||||
plan_env = dict(env or {})
|
||||
plan_env["WSL_UTF8"] = "1"
|
||||
# 必须用 -e(exec,不经默认 shell)而非 --:-- 形式会把尾部交给 /bin/sh 重新解析,
|
||||
# 带空格/引号的参数会被拆散(bash -c 后的位置参数全部丢失),-e 原样传递 argv
|
||||
# (.wsl-exp/test_argprobe2.py 实测:P4/P6(--) 参数丢失,P5/P8(-e) 完整)。
|
||||
return SandboxPlan(
|
||||
command=[wsl, "-d", distro, "--", *argv],
|
||||
command=[wsl, "-d", distro, "-e", *argv],
|
||||
env=plan_env,
|
||||
cwd=str(work_path),
|
||||
stderr_ignore_regexes=list(_WSL_STDERR_IGNORE),
|
||||
|
||||
147
modules/sbx_debug.py
Normal file
147
modules/sbx_debug.py
Normal file
@ -0,0 +1,147 @@
|
||||
# modules/sbx_debug.py - 沙箱崩溃排查的临时调试日志模块(诊断完成后移除)
|
||||
#
|
||||
# 双通道日志:
|
||||
# 1) UDP 发送到独立监控进程(后端死亡前消息已送达,监控进程独立存活)
|
||||
# 2) 文件追加 .wsl-exp/sbx_debug.log(每行立即落盘,保底冗余)
|
||||
# 环境变量:
|
||||
# SBX_DEBUG=0 关闭全部调试日志
|
||||
# SBX_DEBUG_PORT=9956 UDP 监控端口
|
||||
# SBX_DEBUG_SWALLOW_CTRL=0 控制台事件不吞掉(记录后恢复默认行为=真实死亡)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import signal
|
||||
import socket
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
_ENABLED = os.environ.get("SBX_DEBUG", "1").strip().lower() not in {"0", "false", "no", "off"}
|
||||
_PORT = int(os.environ.get("SBX_DEBUG_PORT", "8092") or "8092") # UDP,与后端 TCP 端口不冲突
|
||||
_SWALLOW = os.environ.get("SBX_DEBUG_SWALLOW_CTRL", "1").strip().lower() not in {"0", "false", "no", "off"}
|
||||
|
||||
_LOCK = threading.Lock()
|
||||
_SOCK = None
|
||||
_PROBES_INSTALLED = False
|
||||
|
||||
|
||||
def _log_path() -> str:
|
||||
try:
|
||||
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
except Exception:
|
||||
root = os.getcwd()
|
||||
d = os.path.join(root, ".wsl-exp")
|
||||
try:
|
||||
os.makedirs(d, exist_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
return os.path.join(d, "sbx_debug.log")
|
||||
|
||||
|
||||
def _get_sock():
|
||||
global _SOCK
|
||||
if _SOCK is None:
|
||||
try:
|
||||
_SOCK = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
except Exception:
|
||||
_SOCK = False
|
||||
return _SOCK or None
|
||||
|
||||
|
||||
def sbx_log(event: str, **fields) -> None:
|
||||
"""写一条调试日志(任何异常都吞掉,绝不影响主流程)。"""
|
||||
if not _ENABLED:
|
||||
return
|
||||
try:
|
||||
ts = datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
||||
parts = [f"{k}={fields[k]!r}" for k in sorted(fields)]
|
||||
line = f"[{ts}] [pid={os.getpid()}] [th={threading.current_thread().name}] {event}"
|
||||
if parts:
|
||||
line += " | " + " ".join(parts)
|
||||
data = (line + "\n").encode("utf-8", "replace")
|
||||
with _LOCK:
|
||||
sock = _get_sock()
|
||||
if sock is not None:
|
||||
try:
|
||||
sock.sendto(data, ("127.0.0.1", _PORT))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
with open(_log_path(), "a", encoding="utf-8") as f:
|
||||
f.write(line + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def sbx_log_stack(event: str, **fields) -> None:
|
||||
"""写一条带调用堆栈的调试日志。"""
|
||||
try:
|
||||
stack = "".join(traceback.format_stack(limit=14)[:-2])
|
||||
except Exception:
|
||||
stack = "<no stack>"
|
||||
fields["stack"] = stack
|
||||
sbx_log(event, **fields)
|
||||
|
||||
|
||||
def console_pids():
|
||||
"""返回当前控制台附加的进程 pid 列表(Windows),用于判断子进程是否共享后端控制台。"""
|
||||
if os.name != "nt":
|
||||
return []
|
||||
try:
|
||||
import ctypes
|
||||
arr = (ctypes.c_ulong * 64)()
|
||||
got = ctypes.windll.kernel32.GetConsoleProcessList(arr, 64)
|
||||
if not got:
|
||||
return []
|
||||
return [int(arr[i]) for i in range(min(got, 64))]
|
||||
except Exception:
|
||||
return ["?"]
|
||||
|
||||
|
||||
def install_ctrl_event_probes(tag: str = "backend") -> None:
|
||||
"""安装 SIGINT/SIGBREAK 探针:控制台事件到达时先记录日志。
|
||||
|
||||
SBX_DEBUG_SWALLOW_CTRL=1(默认):记录后吞掉事件,进程存活,可继续观察后续行为;
|
||||
=0:记录后恢复原处理器语义(SIGINT→KeyboardInterrupt;SIGBREAK→默认终止)。
|
||||
只能在主线程安装,失败静默。
|
||||
"""
|
||||
global _PROBES_INSTALLED
|
||||
if not _ENABLED or os.name != "nt" or _PROBES_INSTALLED:
|
||||
return
|
||||
if threading.current_thread() is not threading.main_thread():
|
||||
sbx_log("CTRL-PROBES-SKIPPED", reason="not main thread", tag=tag)
|
||||
return
|
||||
|
||||
def _make_handler(sig, name):
|
||||
prev = signal.getsignal(sig)
|
||||
|
||||
def _handler(signum, frame):
|
||||
sbx_log_stack("CTRL-EVENT-RECEIVED", signal=name, tag=tag, swallowed=_SWALLOW)
|
||||
if _SWALLOW:
|
||||
return
|
||||
if callable(prev):
|
||||
prev(signum, frame)
|
||||
return
|
||||
try:
|
||||
signal.signal(sig, signal.SIG_DFL)
|
||||
os.kill(os.getpid(), sig)
|
||||
except Exception:
|
||||
os._exit(128 + int(signum))
|
||||
|
||||
return _handler
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGINT, _make_handler(signal.SIGINT, "SIGINT(CTRL_C)"))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
sigbreak = getattr(signal, "SIGBREAK", None)
|
||||
if sigbreak is not None:
|
||||
signal.signal(sigbreak, _make_handler(sigbreak, "SIGBREAK(CTRL_BREAK)"))
|
||||
except Exception:
|
||||
pass
|
||||
_PROBES_INSTALLED = True
|
||||
sbx_log("CTRL-PROBES-INSTALLED", tag=tag, swallow=_SWALLOW, port=_PORT)
|
||||
@ -40,6 +40,11 @@ from modules.host_sandbox_runner import (
|
||||
build_host_sandbox_readonly_plan,
|
||||
host_sandbox_enabled,
|
||||
)
|
||||
from modules import sbx_debug # [SBX-DEBUG]
|
||||
try: # [SBX-DEBUG]
|
||||
sbx_debug.install_ctrl_event_probes("terminal_ops") # [SBX-DEBUG]
|
||||
except Exception: # [SBX-DEBUG]
|
||||
pass # [SBX-DEBUG]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from modules.user_container_manager import ContainerHandle
|
||||
@ -56,6 +61,7 @@ class RunMixin:
|
||||
子进程以 start_new_session=True 启动(映射为 CREATE_NEW_PROCESS_GROUP),
|
||||
可用 CTRL_BREAK_EVENT 发送到该进程组,失败则直接强杀。
|
||||
"""
|
||||
sbx_debug.sbx_log_stack("INTERRUPT-SUBPROCESS", pid=getattr(process, "pid", None), nt=(os.name == "nt")) # [SBX-DEBUG]
|
||||
if os.name == "nt":
|
||||
try:
|
||||
process.send_signal(signal.CTRL_BREAK_EVENT)
|
||||
@ -79,6 +85,7 @@ class RunMixin:
|
||||
POSIX 用 killpg(SIGKILL);Windows 没有 SIGKILL,用 taskkill /F /T 终止整棵进程树,
|
||||
避免 shell 子进程被杀后孙进程残留为孤儿。
|
||||
"""
|
||||
sbx_debug.sbx_log_stack("KILL-SUBPROCESS", pid=getattr(process, "pid", None), nt=(os.name == "nt")) # [SBX-DEBUG]
|
||||
if os.name == "nt":
|
||||
try:
|
||||
await asyncio.to_thread(
|
||||
@ -161,6 +168,7 @@ class RunMixin:
|
||||
network_permission: Optional[str] = None,
|
||||
) -> Dict:
|
||||
start_ts = time.time()
|
||||
sbx_debug.sbx_log("RUN-CMD-ENTER", command=str(command)[:200], timeout=timeout, mode=getattr(self, "host_execution_mode", None), sandbox_write=sandbox_write_access) # [SBX-DEBUG]
|
||||
try:
|
||||
process = None
|
||||
exec_cmd = None
|
||||
@ -234,6 +242,7 @@ class RunMixin:
|
||||
os.close(seccomp_fd)
|
||||
except OSError:
|
||||
pass
|
||||
sbx_debug.sbx_log("SANDBOX-SPAWNED", pid=getattr(process, "pid", None), argv0=(cmd_args[0] if cmd_args else None), argv_tail=str(cmd_args[-3:])[:160], console_pids=sbx_debug.console_pids()) # [SBX-DEBUG]
|
||||
elif use_host_sandbox:
|
||||
return {
|
||||
"success": False,
|
||||
@ -298,11 +307,13 @@ class RunMixin:
|
||||
pass
|
||||
|
||||
timed_out = False
|
||||
sbx_debug.sbx_log("WAIT-BEGIN", pid=getattr(process, "pid", None), timeout=timeout) # [SBX-DEBUG]
|
||||
try:
|
||||
try:
|
||||
await asyncio.wait_for(process.wait(), timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
timed_out = True
|
||||
sbx_debug.sbx_log_stack("TIMEOUT-INTERRUPT", pid=getattr(process, "pid", None), timeout=timeout) # [SBX-DEBUG]
|
||||
await self._interrupt_subprocess(process)
|
||||
try:
|
||||
await asyncio.wait_for(process.wait(), timeout=2)
|
||||
@ -311,6 +322,7 @@ class RunMixin:
|
||||
await process.wait()
|
||||
except asyncio.CancelledError:
|
||||
# 用户主动停止任务或会话断开,立即终止子进程
|
||||
sbx_debug.sbx_log_stack("CANCELLED-KILL", pid=getattr(process, "pid", None)) # [SBX-DEBUG]
|
||||
await self._kill_subprocess(process)
|
||||
raise
|
||||
finally:
|
||||
@ -319,6 +331,7 @@ class RunMixin:
|
||||
# (Task was destroyed but it is pending / unclosed transport 告警)
|
||||
await _finish_reader_tasks(force=timed_out)
|
||||
|
||||
sbx_debug.sbx_log("WAIT-END", pid=getattr(process, "pid", None), returncode=getattr(process, "returncode", None), timed_out=timed_out, stdout_len=sum(len(c) for c in stdout_buf), stderr_len=sum(len(c) for c in stderr_buf)) # [SBX-DEBUG]
|
||||
# 非超时场景下兜底再读一次,防止剩余缓冲未被读取
|
||||
if not timed_out:
|
||||
try:
|
||||
@ -385,6 +398,7 @@ class RunMixin:
|
||||
"elapsed_ms": int((time.time() - start_ts) * 1000)
|
||||
}
|
||||
except Exception as exc:
|
||||
sbx_debug.sbx_log_stack("RUN-EXCEPTION", error=str(exc)[:200]) # [SBX-DEBUG]
|
||||
return {
|
||||
"success": False,
|
||||
"status": "error",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user