- 新增六层UI自动化架构:从Backend到Capabilities的完整分层实现 - 添加WeChat 4.0分辨率适配Profile与图像模板资源 - 实现幂等缓存、熔断器、重试策略、链路追踪与监控指标 - 新增头像下载安全校验、发布朋友圈路径白名单防护 - 优化密钥缓存、DB校验逻辑与初始化流程 - 补充完整错误码体系与启动清场机制
192 lines
6.9 KiB
Python
192 lines
6.9 KiB
Python
"""WeChat 进程看门狗:10s 检查间隔,连续 2 次失败触发 autofix。
|
||
|
||
autofix 策略:尝试重启 WeChat 进程(通过 backend.activate_window 或 pgrep+kill)。
|
||
失败仅告警不抛异常,避免拖垮 bridge 主流程。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import logging
|
||
import time
|
||
from typing import Optional
|
||
|
||
from woc_bridge.ui.backends.base import BackendProtocol
|
||
|
||
logger = logging.getLogger("woc-bridge")
|
||
|
||
|
||
class WeChatWatchdog:
|
||
"""WeChat 进程看门狗。
|
||
|
||
Attributes:
|
||
backend: L1 Backend 实例(用于 pgrep/kill 等子进程操作)
|
||
interval: 检查间隔秒数,默认 10.0
|
||
fail_threshold: 连续失败触发 autofix 的阈值,默认 2
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
backend: BackendProtocol,
|
||
interval: float = 10.0,
|
||
fail_threshold: int = 2,
|
||
) -> None:
|
||
self.backend = backend
|
||
self.interval = interval
|
||
self.fail_threshold = fail_threshold
|
||
self._fail_count = 0
|
||
self._task: Optional[asyncio.Task] = None
|
||
self._stopped = False
|
||
|
||
async def start(self) -> None:
|
||
"""启动看门狗后台任务。"""
|
||
if self._task is not None and not self._task.done():
|
||
return
|
||
self._stopped = False
|
||
self._task = asyncio.create_task(self._run())
|
||
logger.info("[watchdog] started (interval=%.1fs)", self.interval)
|
||
|
||
async def stop(self) -> None:
|
||
"""停止看门狗。"""
|
||
self._stopped = True
|
||
if self._task is not None and not self._task.done():
|
||
self._task.cancel()
|
||
try:
|
||
await asyncio.wait_for(self._task, timeout=2.0)
|
||
except (asyncio.TimeoutError, asyncio.CancelledError):
|
||
pass
|
||
self._task = None
|
||
logger.info("[watchdog] stopped")
|
||
|
||
async def _run(self) -> None:
|
||
"""主循环:每 interval 秒检查一次。"""
|
||
while not self._stopped:
|
||
try:
|
||
await asyncio.sleep(self.interval)
|
||
await self._check()
|
||
except asyncio.CancelledError:
|
||
break
|
||
except Exception as exc:
|
||
logger.warning("[watchdog] check exception: %s", exc)
|
||
|
||
async def _check(self) -> None:
|
||
"""执行一次检查:WeChat 运行态 + X11 可用性。"""
|
||
wechat_ok = await self._is_wechat_running()
|
||
x11_ok = await self._is_x11_available()
|
||
|
||
if wechat_ok and x11_ok:
|
||
if self._fail_count > 0:
|
||
logger.info(
|
||
"[watchdog] recovered after %d failures", self._fail_count
|
||
)
|
||
self._fail_count = 0
|
||
return
|
||
|
||
self._fail_count += 1
|
||
logger.warning(
|
||
"[watchdog] check failed (wechat=%s x11=%s) fail_count=%d/%d",
|
||
wechat_ok, x11_ok, self._fail_count, self.fail_threshold,
|
||
)
|
||
|
||
if self._fail_count >= self.fail_threshold:
|
||
logger.error(
|
||
"[watchdog] fail threshold reached, trigger autofix"
|
||
)
|
||
await self._autofix_wechat()
|
||
# autofix 后重置计数,等下一轮检查
|
||
self._fail_count = 0
|
||
|
||
async def _is_wechat_running(self) -> bool:
|
||
"""检查 WeChat 进程是否运行(用 pgrep -x wechat)。"""
|
||
try:
|
||
proc = await asyncio.create_subprocess_exec(
|
||
"pgrep", "-x", "wechat",
|
||
stdout=asyncio.subprocess.PIPE,
|
||
stderr=asyncio.subprocess.PIPE,
|
||
)
|
||
try:
|
||
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=3.0)
|
||
except asyncio.TimeoutError:
|
||
proc.kill()
|
||
await proc.wait()
|
||
return False
|
||
return proc.returncode == 0 and bool(stdout.decode(errors="ignore").strip())
|
||
except Exception as exc:
|
||
logger.debug("[watchdog] pgrep wechat exception: %s", exc)
|
||
return False
|
||
|
||
async def _is_x11_available(self) -> bool:
|
||
"""检查 X11 是否可用(用 xdpyinfo)。"""
|
||
try:
|
||
proc = await asyncio.create_subprocess_exec(
|
||
"xdpyinfo",
|
||
stdout=asyncio.subprocess.PIPE,
|
||
stderr=asyncio.subprocess.PIPE,
|
||
)
|
||
try:
|
||
await asyncio.wait_for(proc.communicate(), timeout=3.0)
|
||
except asyncio.TimeoutError:
|
||
proc.kill()
|
||
await proc.wait()
|
||
return False
|
||
return proc.returncode == 0
|
||
except Exception as exc:
|
||
logger.debug("[watchdog] xdpyinfo exception: %s", exc)
|
||
return False
|
||
|
||
async def _autofix_wechat(self) -> None:
|
||
"""自动修复:尝试 kill WeChat 让 autostart 拉起。
|
||
|
||
不直接启动 WeChat(避免与 autostart 竞态),仅 kill 旧进程。
|
||
所有子进程超时均执行 proc.kill() 收尸,避免僵尸进程泄漏。
|
||
"""
|
||
logger.warning("[watchdog] autofix: killing wechat for restart")
|
||
try:
|
||
# 获取所有 wechat PID
|
||
proc = await asyncio.create_subprocess_exec(
|
||
"pgrep", "-x", "wechat",
|
||
stdout=asyncio.subprocess.PIPE,
|
||
stderr=asyncio.subprocess.PIPE,
|
||
)
|
||
try:
|
||
stdout, _ = await asyncio.wait_for(
|
||
proc.communicate(), timeout=3.0
|
||
)
|
||
except asyncio.TimeoutError:
|
||
proc.kill()
|
||
await proc.wait()
|
||
logger.warning("[watchdog] autofix pgrep timeout")
|
||
return
|
||
pids = []
|
||
for line in stdout.decode(errors="ignore").splitlines():
|
||
line = line.strip()
|
||
if line:
|
||
try:
|
||
pids.append(int(line))
|
||
except ValueError:
|
||
pass
|
||
# kill 所有 wechat 进程
|
||
for pid in pids:
|
||
try:
|
||
kill_proc = await asyncio.create_subprocess_exec(
|
||
"kill", "-TERM", str(pid),
|
||
stdout=asyncio.subprocess.PIPE,
|
||
stderr=asyncio.subprocess.PIPE,
|
||
)
|
||
try:
|
||
await asyncio.wait_for(
|
||
kill_proc.communicate(), timeout=3.0
|
||
)
|
||
except asyncio.TimeoutError:
|
||
kill_proc.kill()
|
||
await kill_proc.wait()
|
||
logger.info(
|
||
"[watchdog] autofix killed wechat pid=%d", pid
|
||
)
|
||
except Exception as exc:
|
||
logger.warning(
|
||
"[watchdog] autofix kill pid=%d failed: %s", pid, exc
|
||
)
|
||
except Exception as exc:
|
||
logger.error("[watchdog] autofix exception: %s", exc)
|