a
This commit is contained in:
parent
45a654f654
commit
5a00dc75d7
|
|
@ -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)
|
||||
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,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
||||
|
|
|
|||
|
|
@ -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 可改) */}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user