a
This commit is contained in:
parent
45a654f654
commit
5a00dc75d7
|
|
@ -20,6 +20,7 @@ from api.supervisor_account import (
|
||||||
parse_supervisor_status,
|
parse_supervisor_status,
|
||||||
program_name_for_account,
|
program_name_for_account,
|
||||||
tail_supervisor,
|
tail_supervisor,
|
||||||
|
tail_supervisord_log,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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)
|
running, pid, state = parse_supervisor_status(raw)
|
||||||
# 仅 owner/admin 可看 tail(便于自助排障)
|
# 仅 owner/admin 可看 tail(便于自助排障)
|
||||||
stderr_tail = ""
|
stderr_tail = ""
|
||||||
|
stderr_tail_error = ""
|
||||||
|
supervisord_tail = ""
|
||||||
try:
|
try:
|
||||||
is_admin = (user.get("role") or "user") == "admin"
|
is_admin = (user.get("role") or "user") == "admin"
|
||||||
role = UserAccountMembership.get_role(int(user["id"]), int(account_id)) if not is_admin else "admin"
|
role = UserAccountMembership.get_role(int(user["id"]), int(account_id)) if not is_admin else "admin"
|
||||||
if is_admin or role == "owner":
|
if is_admin or role == "owner":
|
||||||
if state in {"FATAL", "EXITED", "BACKOFF"}:
|
if state in {"FATAL", "EXITED", "BACKOFF"}:
|
||||||
stderr_tail = tail_supervisor(program, "stderr", 120)
|
stderr_tail = tail_supervisor(program, "stderr", 120)
|
||||||
|
if not stderr_tail:
|
||||||
|
try:
|
||||||
|
supervisord_tail = tail_supervisord_log(80)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
supervisord_tail = ""
|
||||||
|
except Exception as te:
|
||||||
|
stderr_tail_error = str(te)
|
||||||
stderr_tail = ""
|
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}
|
resp = {"program": program, "running": running, "pid": pid, "state": state, "raw": raw}
|
||||||
if stderr_tail:
|
if stderr_tail:
|
||||||
resp["stderr_tail"] = 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
|
return resp
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"读取交易进程状态失败: {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}}
|
return {"message": "已启动", "output": out, "status": {"program": program, "running": running, "pid": pid, "state": state, "raw": raw}}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
tail = ""
|
tail = ""
|
||||||
|
tail_err = ""
|
||||||
|
status_raw = ""
|
||||||
|
supervisord_tail = ""
|
||||||
try:
|
try:
|
||||||
tail = tail_supervisor(program, "stderr", 120)
|
tail = tail_supervisor(program, "stderr", 120)
|
||||||
except Exception:
|
except Exception as te:
|
||||||
|
tail_err = str(te)
|
||||||
tail = ""
|
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(
|
raise HTTPException(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
detail={
|
detail={
|
||||||
|
|
@ -297,6 +328,9 @@ async def trading_start_for_account(account_id: int, user: Dict[str, Any] = Depe
|
||||||
"program": program,
|
"program": program,
|
||||||
"hint": "如果是 spawn error,重点看 stderr_tail(常见:python 路径不可执行/依赖缺失/权限/工作目录不存在)",
|
"hint": "如果是 spawn error,重点看 stderr_tail(常见:python 路径不可执行/依赖缺失/权限/工作目录不存在)",
|
||||||
"stderr_tail": tail,
|
"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}}
|
return {"message": "已重启", "output": out, "status": {"program": program, "running": running, "pid": pid, "state": state, "raw": raw}}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
tail = ""
|
tail = ""
|
||||||
|
tail_err = ""
|
||||||
|
status_raw = ""
|
||||||
|
supervisord_tail = ""
|
||||||
try:
|
try:
|
||||||
tail = tail_supervisor(program, "stderr", 120)
|
tail = tail_supervisor(program, "stderr", 120)
|
||||||
except Exception:
|
except Exception as te:
|
||||||
|
tail_err = str(te)
|
||||||
tail = ""
|
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(
|
raise HTTPException(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
detail={
|
detail={
|
||||||
|
|
@ -341,6 +388,9 @@ async def trading_restart_for_account(account_id: int, user: Dict[str, Any] = De
|
||||||
"program": program,
|
"program": program,
|
||||||
"hint": "如果是 spawn error,重点看 stderr_tail(常见:python 路径不可执行/依赖缺失/权限/工作目录不存在)",
|
"hint": "如果是 spawn error,重点看 stderr_tail(常见:python 路径不可执行/依赖缺失/权限/工作目录不存在)",
|
||||||
"stderr_tail": tail,
|
"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])
|
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
|
@dataclass
|
||||||
class EnsureProgramResult:
|
class EnsureProgramResult:
|
||||||
ok: bool
|
ok: bool
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ const AccountSelector = ({ onChanged }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentAccountId(accountId)
|
setCurrentAccountId(accountId)
|
||||||
if (typeof onChanged === 'function') onChanged(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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [accountId])
|
}, [accountId])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -447,6 +447,16 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
loadCurrentAccountMeta()
|
loadCurrentAccountMeta()
|
||||||
}, [accountId])
|
}, [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更新),这里做一个轻量同步
|
// 顶部导航切换账号时(localStorage更新),这里做一个轻量同步
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
|
|
@ -819,7 +829,7 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
{/* 我的交易进程(按账号;owner/admin 可启停) */}
|
{/* 我的交易进程(按账号;owner/admin 可启停) */}
|
||||||
<div className="system-section">
|
<div className="system-section">
|
||||||
<div className="system-header">
|
<div className="system-header">
|
||||||
<h3>我的交易进程(当前账号)</h3>
|
<h3>我的交易进程(当前账号 #{accountId})</h3>
|
||||||
<div className="system-status">
|
<div className="system-status">
|
||||||
<span className={`system-status-badge ${accountTradingStatus?.running ? 'running' : 'stopped'}`}>
|
<span className={`system-status-badge ${accountTradingStatus?.running ? 'running' : 'stopped'}`}>
|
||||||
{accountTradingStatus?.running ? '运行中' : '未运行/未知'}
|
{accountTradingStatus?.running ? '运行中' : '未运行/未知'}
|
||||||
|
|
@ -863,6 +873,18 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
<pre style={{ whiteSpace: 'pre-wrap', marginTop: '8px' }}>{String(accountTradingStatus.stderr_tail || '')}</pre>
|
<pre style={{ whiteSpace: 'pre-wrap', marginTop: '8px' }}>{String(accountTradingStatus.stderr_tail || '')}</pre>
|
||||||
</details>
|
</details>
|
||||||
) : null}
|
) : 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>
|
</div>
|
||||||
|
|
||||||
{/* 账号密钥(当前账号;owner/admin 可改) */}
|
{/* 账号密钥(当前账号;owner/admin 可改) */}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user