fix(easyocr): 顶层崩溃全程留痕,消除"一声不吭"结束进程

- easyocr_worker.py: 新增 worker 状态文件 easyocr_worker_state.json
  (pid/mode/status/last_img_len/ts/error),就绪/忙碌/空闲实时落盘;
  强化 sys.excepthook / _flush_exit / _signal_handler 崩溃写状态+crash.log
- monitor.py: _kill_easyocr_proc 杀前诊断三路
  (状态文件 + 残留stderr + crash.log尾),以 warning 汇总进主日志;
  _read_easyocr_crash_tail 新增 ignore_age 跳过60s时间窗用于诊断路径
- unicode_radicals.md: 丿(U+4E3F)行改 - 不参与偏旁过滤,修复OCR碎片误杀儿下载等
This commit is contained in:
youpaishidifu
2026-08-06 22:55:29 +08:00
parent 3df59c53ad
commit 9944d74798
3 changed files with 153 additions and 8 deletions
+48 -1
View File
@@ -58,6 +58,44 @@ def _get_crash_log_path() -> str:
CRASH_LOG = _get_crash_log_path()
# ── 状态文件:记录 worker 当前在处理什么/卡多久,父进程 kill 前读取,避免"一声不吭" ──
def _get_state_path() -> str:
try:
base = os.path.dirname(os.path.abspath(__file__))
except Exception:
base = "."
return os.path.join(base, "easyocr_worker_state.json")
STATE_PATH = _get_state_path()
# worker 当前运行模式(GPU/CPU),由 _init_reader 设置
_WORKER_MODE = "未知"
def _write_state(status: str, last_img_len: int = 0, ts: float = None, error: str = ""):
"""把当前 worker 状态落盘为 JSON,便于父进程 kill 前诊断死前状态。
status: idle / busy / crashed / terminated
"""
try:
_ts = ts if ts is not None else datetime.now().timestamp()
_rec = {
"pid": os.getpid(),
"mode": _WORKER_MODE,
"status": status,
"last_img_len": last_img_len,
"ts": _ts,
"iso": datetime.fromtimestamp(_ts).isoformat() if _ts else "",
"error": error,
}
with open(STATE_PATH, "w", encoding="utf-8") as f:
json.dump(_rec, f, ensure_ascii=False)
f.flush()
except Exception:
pass
def _write_crash_log(msg: str):
try:
with open(CRASH_LOG, "a", encoding="utf-8") as f:
@@ -72,6 +110,7 @@ def _flush_exit(msg, code=1):
sys.stderr.write(full + "\n")
sys.stderr.flush()
_write_crash_log(f"{datetime.now()} {full}")
_write_state("crashed", error=full[:500])
sys.exit(code)
# ── 全局未捕获异常钩子:任何漏网的 Python 异常都落盘 ──
@@ -80,6 +119,7 @@ def _global_excepthook(exc_type, exc_val, exc_tb):
sys.stderr.write(f"[easyocr_worker] 未捕获异常:\n{msg}")
sys.stderr.flush()
_write_crash_log(f"{datetime.now()} [easyocr_worker] 未捕获异常:\n{msg}")
_write_state("crashed", error=msg[:500])
sys.excepthook = _global_excepthook
@@ -172,6 +212,7 @@ except Exception as e:
def _init_reader():
"""初始化 EasyOCR reader(GPU 优先,父进程可强制 CPU)。"""
global _WORKER_MODE
# 先检查环境变量是否强制CPU
_force_cpu = os.environ.get("EASYOCR_FORCE_CPU") == "1"
@@ -215,6 +256,7 @@ def _init_reader():
try:
reader = easyocr.Reader(["ch_sim", "en"], gpu=use_gpu)
mode = "GPU" if use_gpu else "CPU"
_WORKER_MODE = mode
sys.stderr.write(f"[easyocr_worker] Reader 就绪({mode}模式)\n")
sys.stderr.flush()
return reader
@@ -223,6 +265,7 @@ def _init_reader():
sys.stderr.write(f"[easyocr_worker] GPU 初始化失败({e}),尝试 CPU\n")
sys.stderr.flush()
reader = easyocr.Reader(["ch_sim", "en"], gpu=False)
_WORKER_MODE = "CPU"
sys.stderr.write("[easyocr_worker] Reader 就绪(CPU 模式)\n")
sys.stderr.flush()
return reader
@@ -256,6 +299,7 @@ def _main():
sys.stdout.flush()
sys.stderr.write("[easyocr_worker] 就绪,等待 stdin 输入\n")
sys.stderr.flush()
_write_state("idle")
for line in sys.stdin:
line = line.strip()
@@ -267,6 +311,7 @@ def _main():
img_b64 = payload.get("img", "")
if not img_b64:
raise ValueError("缺少 img 字段")
_write_state("busy", last_img_len=len(img_b64))
sys.stderr.write(f"[easyocr_worker] 收到请求,处理中... (img长度={len(img_b64)})\n")
sys.stderr.flush()
text = _process_image(img_b64, reader)
@@ -275,6 +320,7 @@ def _main():
out = json.dumps({"ok": True, "text": text}, ensure_ascii=False)
sys.stdout.write(out + "\n")
sys.stdout.flush()
_write_state("idle")
except Exception as e:
estr = str(e)
# ⭐ CUDA OOM / kernel error:GPU context 已损坏,继续跑只会反复卡死占显存
@@ -306,7 +352,8 @@ def _cleanup_gpu():
def _signal_handler(signum, frame):
"""捕获 SIGTERM/SIGINT,先清理 GPU 再退出(proc.terminate() 触发此处理)。"""
"""捕获 SIGTERM/SIGINT,先记录被终止状态再清理 GPU 退出(proc.terminate() 触发此处理)。"""
_write_state("terminated", error=f"收到信号 {signum},父进程请求终止,正在清理退出")
_cleanup_gpu()
sys.exit(0)