From 5a00dc75d7fee921c90ffa385b6fe3525d84d33b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Wed, 21 Jan 2026 22:02:25 +0800 Subject: [PATCH] a --- backend/api/routes/accounts.py | 56 ++++++++++++++- backend/api/supervisor_account.py | 79 +++++++++++++++++++++ frontend/src/components/AccountSelector.jsx | 5 ++ frontend/src/components/ConfigPanel.jsx | 24 ++++++- 4 files changed, 160 insertions(+), 4 deletions(-) diff --git a/backend/api/routes/accounts.py b/backend/api/routes/accounts.py index 50331a0..25f5d37 100644 --- a/backend/api/routes/accounts.py +++ b/backend/api/routes/accounts.py @@ -20,6 +20,7 @@ from api.supervisor_account import ( parse_supervisor_status, program_name_for_account, tail_supervisor, + tail_supervisord_log, ) logger = logging.getLogger(__name__) @@ -234,18 +235,35 @@ async def trading_status_for_account(account_id: int, user: Dict[str, Any] = Dep running, pid, state = parse_supervisor_status(raw) # 仅 owner/admin 可看 tail(便于自助排障) stderr_tail = "" + stderr_tail_error = "" + supervisord_tail = "" try: is_admin = (user.get("role") or "user") == "admin" role = UserAccountMembership.get_role(int(user["id"]), int(account_id)) if not is_admin else "admin" if is_admin or role == "owner": if state in {"FATAL", "EXITED", "BACKOFF"}: stderr_tail = tail_supervisor(program, "stderr", 120) - except Exception: + if not stderr_tail: + try: + supervisord_tail = tail_supervisord_log(80) + except Exception: + supervisord_tail = "" + except Exception as te: + stderr_tail_error = str(te) stderr_tail = "" + # spawn error 时 program stderr 可能为空,尝试给 supervisord 主日志做兜底 + try: + supervisord_tail = tail_supervisord_log(80) + except Exception: + supervisord_tail = "" resp = {"program": program, "running": running, "pid": pid, "state": state, "raw": raw} if stderr_tail: resp["stderr_tail"] = stderr_tail + if stderr_tail_error: + resp["stderr_tail_error"] = stderr_tail_error + if supervisord_tail: + resp["supervisord_tail"] = supervisord_tail return resp except Exception as e: raise HTTPException(status_code=500, detail=f"读取交易进程状态失败: {e}") @@ -286,10 +304,23 @@ async def trading_start_for_account(account_id: int, user: Dict[str, Any] = Depe return {"message": "已启动", "output": out, "status": {"program": program, "running": running, "pid": pid, "state": state, "raw": raw}} except Exception as e: tail = "" + tail_err = "" + status_raw = "" + supervisord_tail = "" try: tail = tail_supervisor(program, "stderr", 120) - except Exception: + except Exception as te: + tail_err = str(te) tail = "" + try: + status_raw = run_supervisorctl(["status", program]) + except Exception: + status_raw = "" + if not tail: + try: + supervisord_tail = tail_supervisord_log(120) + except Exception: + supervisord_tail = "" raise HTTPException( status_code=500, detail={ @@ -297,6 +328,9 @@ async def trading_start_for_account(account_id: int, user: Dict[str, Any] = Depe "program": program, "hint": "如果是 spawn error,重点看 stderr_tail(常见:python 路径不可执行/依赖缺失/权限/工作目录不存在)", "stderr_tail": tail, + "stderr_tail_error": tail_err, + "status_raw": status_raw, + "supervisord_tail": supervisord_tail, }, ) @@ -330,10 +364,23 @@ async def trading_restart_for_account(account_id: int, user: Dict[str, Any] = De return {"message": "已重启", "output": out, "status": {"program": program, "running": running, "pid": pid, "state": state, "raw": raw}} except Exception as e: tail = "" + tail_err = "" + status_raw = "" + supervisord_tail = "" try: tail = tail_supervisor(program, "stderr", 120) - except Exception: + except Exception as te: + tail_err = str(te) tail = "" + try: + status_raw = run_supervisorctl(["status", program]) + except Exception: + status_raw = "" + if not tail: + try: + supervisord_tail = tail_supervisord_log(120) + except Exception: + supervisord_tail = "" raise HTTPException( status_code=500, detail={ @@ -341,6 +388,9 @@ async def trading_restart_for_account(account_id: int, user: Dict[str, Any] = De "program": program, "hint": "如果是 spawn error,重点看 stderr_tail(常见:python 路径不可执行/依赖缺失/权限/工作目录不存在)", "stderr_tail": tail, + "stderr_tail_error": tail_err, + "status_raw": status_raw, + "supervisord_tail": supervisord_tail, }, ) diff --git a/backend/api/supervisor_account.py b/backend/api/supervisor_account.py index 1b23641..a7fa687 100644 --- a/backend/api/supervisor_account.py +++ b/backend/api/supervisor_account.py @@ -272,6 +272,85 @@ def tail_supervisor(program: str, stream: str = "stderr", lines: int = 120) -> s return run_supervisorctl(["tail", f"-{n}", str(program), s]) +def _tail_text_file(path: Path, lines: int = 200, max_bytes: int = 64 * 1024) -> str: + """ + 读取文本文件末尾(用于 supervisor spawn error 等场景,program stderr 可能为空)。 + 尽量只读最后 max_bytes,避免大文件占用内存。 + """ + try: + p = Path(path) + if not p.exists(): + return "" + size = p.stat().st_size + read_size = min(int(max_bytes), int(size)) + with p.open("rb") as f: + if size > read_size: + f.seek(-read_size, os.SEEK_END) + data = f.read() + text = data.decode("utf-8", errors="ignore") + # 仅保留最后 N 行 + parts = text.splitlines() + if not parts: + return "" + n = int(lines or 200) + if n < 20: + n = 20 + if n > 500: + n = 500 + return "\n".join(parts[-n:]).strip() + except Exception: + return "" + + +def _parse_supervisord_logfile_from_conf(conf_path: Path) -> Optional[Path]: + """ + 解析 supervisord.conf 中 [supervisord] 的 logfile= 路径。 + """ + try: + text = conf_path.read_text(encoding="utf-8", errors="ignore") + except Exception: + return None + in_section = False + for raw in text.splitlines(): + line = raw.strip() + if not line or line.startswith(";") or line.startswith("#"): + continue + if re.match(r"^\[supervisord\]\s*$", line, flags=re.I): + in_section = True + continue + if in_section and line.startswith("[") and line.endswith("]"): + break + if not in_section: + continue + m = re.match(r"^logfile\s*=\s*(.+)$", line, flags=re.I) + if not m: + continue + val = (m.group(1) or "").strip().strip('"').strip("'") + if not val: + continue + p = Path(val) + if not p.is_absolute(): + p = (conf_path.parent / p).resolve() + return p + return None + + +def tail_supervisord_log(lines: int = 200) -> str: + """ + 读取 supervisord 主日志尾部(spawn error 的根因经常在这里)。 + 可通过环境变量 SUPERVISOR_LOGFILE 指定。 + """ + env_p = (os.getenv("SUPERVISOR_LOGFILE") or "").strip() + if env_p: + return _tail_text_file(Path(env_p), lines=lines) + conf = _detect_supervisor_conf_path() + if conf and conf.exists(): + lp = _parse_supervisord_logfile_from_conf(conf) + if lp: + return _tail_text_file(lp, lines=lines) + return "" + + @dataclass class EnsureProgramResult: ok: bool diff --git a/frontend/src/components/AccountSelector.jsx b/frontend/src/components/AccountSelector.jsx index 29a1244..27e5e89 100644 --- a/frontend/src/components/AccountSelector.jsx +++ b/frontend/src/components/AccountSelector.jsx @@ -22,6 +22,11 @@ const AccountSelector = ({ onChanged }) => { useEffect(() => { setCurrentAccountId(accountId) if (typeof onChanged === 'function') onChanged(accountId) + try { + window.dispatchEvent(new CustomEvent('ats:account:changed', { detail: { accountId } })) + } catch (e) { + // ignore + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [accountId]) diff --git a/frontend/src/components/ConfigPanel.jsx b/frontend/src/components/ConfigPanel.jsx index 6dd2d3f..93ccc8e 100644 --- a/frontend/src/components/ConfigPanel.jsx +++ b/frontend/src/components/ConfigPanel.jsx @@ -447,6 +447,16 @@ const ConfigPanel = ({ currentUser }) => { loadCurrentAccountMeta() }, [accountId]) + // 顶部导航切换账号时:即时同步(比 setInterval 更及时) + useEffect(() => { + const onChanged = (e) => { + const next = parseInt(String(e?.detail?.accountId || ''), 10) + if (Number.isFinite(next) && next > 0) setAccountId(next) + } + window.addEventListener('ats:account:changed', onChanged) + return () => window.removeEventListener('ats:account:changed', onChanged) + }, []) + // 顶部导航切换账号时(localStorage更新),这里做一个轻量同步 useEffect(() => { const timer = setInterval(() => { @@ -819,7 +829,7 @@ const ConfigPanel = ({ currentUser }) => { {/* 我的交易进程(按账号;owner/admin 可启停) */}
-

我的交易进程(当前账号)

+

我的交易进程(当前账号 #{accountId})

{accountTradingStatus?.running ? '运行中' : '未运行/未知'} @@ -863,6 +873,18 @@ const ConfigPanel = ({ currentUser }) => {
{String(accountTradingStatus.stderr_tail || '')}
) : null} + {accountTradingStatus?.stderr_tail_error ? ( +
+ 读取 stderr 失败原因(用于排障) +
{String(accountTradingStatus.stderr_tail_error || '')}
+
+ ) : null} + {accountTradingStatus?.supervisord_tail ? ( +
+ supervisord 主日志尾部(spawn error 常见原因在这里) +
{String(accountTradingStatus.supervisord_tail || '')}
+
+ ) : null}
{/* 账号密钥(当前账号;owner/admin 可改) */}