agent-Specialization/modules/sbx_debug.py

148 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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→KeyboardInterruptSIGBREAK→默认终止
只能在主线程安装,失败静默。
"""
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)