This commit is contained in:
薇薇安 2026-01-21 22:02:25 +08:00
parent 45a654f654
commit 5a00dc75d7
4 changed files with 160 additions and 4 deletions

View File

@ -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,
},
)

View File

@ -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

View File

@ -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])

View File

@ -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 可启停) */}
<div className="system-section">
<div className="system-header">
<h3>我的交易进程当前账号</h3>
<h3>我的交易进程当前账号 #{accountId}</h3>
<div className="system-status">
<span className={`system-status-badge ${accountTradingStatus?.running ? 'running' : 'stopped'}`}>
{accountTradingStatus?.running ? '运行中' : '未运行/未知'}
@ -863,6 +873,18 @@ const ConfigPanel = ({ currentUser }) => {
<pre style={{ whiteSpace: 'pre-wrap', marginTop: '8px' }}>{String(accountTradingStatus.stderr_tail || '')}</pre>
</details>
) : null}
{accountTradingStatus?.stderr_tail_error ? (
<details style={{ marginTop: '8px' }}>
<summary style={{ cursor: 'pointer' }}>读取 stderr 失败原因用于排障</summary>
<pre style={{ whiteSpace: 'pre-wrap', marginTop: '8px' }}>{String(accountTradingStatus.stderr_tail_error || '')}</pre>
</details>
) : null}
{accountTradingStatus?.supervisord_tail ? (
<details style={{ marginTop: '8px' }}>
<summary style={{ cursor: 'pointer' }}>supervisord 主日志尾部spawn error 常见原因在这里</summary>
<pre style={{ whiteSpace: 'pre-wrap', marginTop: '8px' }}>{String(accountTradingStatus.supervisord_tail || '')}</pre>
</details>
) : null}
</div>
{/* 账号密钥当前账号owner/admin 可改) */}