a
This commit is contained in:
parent
82fd5e3f58
commit
c348e3acbb
|
|
@ -2,6 +2,7 @@ import os
|
|||
import re
|
||||
import subprocess
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
|
|
@ -16,6 +17,41 @@ router = APIRouter(prefix="/api/system")
|
|||
|
||||
LOG_GROUPS = ("error", "warning", "info")
|
||||
|
||||
# 后端服务启动时间(用于前端展示“运行多久/是否已重启”)
|
||||
_BACKEND_STARTED_AT_MS: int = int(time.time() * 1000)
|
||||
|
||||
# 系统元信息存储(优先 Redis;用于记录重启时间等)
|
||||
def _system_meta_prefix() -> str:
|
||||
return (os.getenv("SYSTEM_META_PREFIX", "ats:system").strip() or "ats:system")
|
||||
|
||||
|
||||
def _system_meta_key(name: str) -> str:
|
||||
name = (name or "").strip()
|
||||
return f"{_system_meta_prefix()}:{name}"
|
||||
|
||||
|
||||
def _system_meta_read(name: str) -> Optional[Dict[str, Any]]:
|
||||
client = _get_redis_client_for_logs()
|
||||
if client is None:
|
||||
return None
|
||||
try:
|
||||
raw = client.get(_system_meta_key(name))
|
||||
if not raw:
|
||||
return None
|
||||
return json.loads(raw)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _system_meta_write(name: str, payload: Dict[str, Any], ttl_sec: int = 30 * 24 * 3600) -> None:
|
||||
client = _get_redis_client_for_logs()
|
||||
if client is None:
|
||||
return
|
||||
try:
|
||||
client.setex(_system_meta_key(name), int(ttl_sec), json.dumps(payload, ensure_ascii=False))
|
||||
except Exception:
|
||||
return
|
||||
|
||||
# 避免 Redis 异常刷屏(前端可能自动刷新)
|
||||
_last_logs_redis_err_ts: float = 0.0
|
||||
|
||||
|
|
@ -714,6 +750,7 @@ async def trading_status(x_admin_token: Optional[str] = Header(default=None, ali
|
|||
try:
|
||||
raw, resolved_name, status_all = _status_with_fallback(program)
|
||||
running, pid, state = _parse_supervisor_status(raw)
|
||||
meta = _system_meta_read("trading:last_restart") or {}
|
||||
return {
|
||||
"mode": "supervisor",
|
||||
"program": program,
|
||||
|
|
@ -723,6 +760,7 @@ async def trading_status(x_admin_token: Optional[str] = Header(default=None, ali
|
|||
"state": state,
|
||||
"raw": raw,
|
||||
"status_all": status_all,
|
||||
"meta": meta,
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
|
|
@ -791,9 +829,22 @@ async def trading_restart(x_admin_token: Optional[str] = Header(default=None, al
|
|||
|
||||
program = _get_program_name()
|
||||
try:
|
||||
requested_at = _beijing_time_str()
|
||||
requested_at_ms = int(time.time() * 1000)
|
||||
out, resolved_name, status_all = _action_with_fallback("restart", program)
|
||||
raw, resolved_name2, status_all2 = _status_with_fallback(resolved_name or program)
|
||||
running, pid, state = _parse_supervisor_status(raw)
|
||||
|
||||
# 记录交易系统重启时间(用于前端展示)
|
||||
_system_meta_write(
|
||||
"trading:last_restart",
|
||||
{
|
||||
"requested_at": requested_at,
|
||||
"requested_at_ms": requested_at_ms,
|
||||
"pid": pid,
|
||||
"program": resolved_name2 or resolved_name or program,
|
||||
},
|
||||
)
|
||||
return {
|
||||
"message": "交易系统已重启(supervisor)",
|
||||
"output": out,
|
||||
|
|
@ -807,7 +858,89 @@ async def trading_restart(x_admin_token: Optional[str] = Header(default=None, al
|
|||
"raw": raw,
|
||||
"status_all": status_all2 or status_all,
|
||||
},
|
||||
"meta": {
|
||||
"requested_at": requested_at,
|
||||
"requested_at_ms": requested_at_ms,
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"supervisorctl restart 失败: {e}")
|
||||
|
||||
|
||||
@router.get("/backend/status")
|
||||
async def backend_status(x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token")) -> Dict[str, Any]:
|
||||
"""
|
||||
查看后端服务状态(当前 uvicorn 进程)。
|
||||
|
||||
说明:
|
||||
- pid 使用 os.getpid()(当前 FastAPI 进程)
|
||||
- last_restart 从 Redis 读取(若可用)
|
||||
"""
|
||||
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
|
||||
meta = _system_meta_read("backend:last_restart") or {}
|
||||
return {
|
||||
"running": True,
|
||||
"pid": os.getpid(),
|
||||
"started_at_ms": _BACKEND_STARTED_AT_MS,
|
||||
"started_at": _beijing_time_str(),
|
||||
"meta": meta,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/backend/restart")
|
||||
async def backend_restart(x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token")) -> Dict[str, Any]:
|
||||
"""
|
||||
重启后端服务(uvicorn)。
|
||||
|
||||
实现方式:
|
||||
- 后端启动脚本为 nohup uvicorn ... &
|
||||
- 这里通过后台启动 backend/restart.sh 来完成:
|
||||
1) grep 找到 uvicorn api.main:app 进程并 kill
|
||||
2) 再执行 backend/start.sh 拉起新进程
|
||||
|
||||
注意:
|
||||
- 为了让接口能先返回,这里会延迟 1s 再执行 restart.sh
|
||||
"""
|
||||
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
|
||||
|
||||
backend_dir = Path(__file__).parent.parent.parent # backend/
|
||||
restart_script = backend_dir / "restart.sh"
|
||||
if not restart_script.exists():
|
||||
raise HTTPException(status_code=500, detail=f"找不到重启脚本: {restart_script}")
|
||||
|
||||
requested_at = _beijing_time_str()
|
||||
requested_at_ms = int(time.time() * 1000)
|
||||
cur_pid = os.getpid()
|
||||
|
||||
_system_meta_write(
|
||||
"backend:last_restart",
|
||||
{
|
||||
"requested_at": requested_at,
|
||||
"requested_at_ms": requested_at_ms,
|
||||
"pid_before": cur_pid,
|
||||
"script": str(restart_script),
|
||||
},
|
||||
)
|
||||
|
||||
# 后台执行:sleep 1 后再重启,保证当前请求可以返回
|
||||
cmd = ["bash", "-lc", f"sleep 1; '{restart_script}'"]
|
||||
try:
|
||||
subprocess.Popen(
|
||||
cmd,
|
||||
cwd=str(backend_dir),
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"启动重启脚本失败: {e}")
|
||||
|
||||
return {
|
||||
"message": "已发起后端重启(1s 后执行)",
|
||||
"pid_before": cur_pid,
|
||||
"requested_at": requested_at,
|
||||
"requested_at_ms": requested_at_ms,
|
||||
"script": str(restart_script),
|
||||
"note": "重启期间接口可能短暂不可用,页面可等待 3-5 秒后刷新状态。",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const ConfigPanel = () => {
|
|||
const [feasibilityCheck, setFeasibilityCheck] = useState(null)
|
||||
const [checkingFeasibility, setCheckingFeasibility] = useState(false)
|
||||
const [systemStatus, setSystemStatus] = useState(null)
|
||||
const [backendStatus, setBackendStatus] = useState(null)
|
||||
const [systemBusy, setSystemBusy] = useState(false)
|
||||
|
||||
// 配置快照(用于整体分析/导出)
|
||||
|
|
@ -99,6 +100,15 @@ const ConfigPanel = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const loadBackendStatus = async () => {
|
||||
try {
|
||||
const res = await api.getBackendStatus()
|
||||
setBackendStatus(res)
|
||||
} catch (error) {
|
||||
// 静默失败:不影响配置页使用
|
||||
}
|
||||
}
|
||||
|
||||
const handleClearCache = async () => {
|
||||
setSystemBusy(true)
|
||||
setMessage('')
|
||||
|
|
@ -156,13 +166,33 @@ const ConfigPanel = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const handleRestartBackend = async () => {
|
||||
if (!window.confirm('确定要重启后端服务吗?重启期间页面接口会短暂不可用(约 3-10 秒)。')) return
|
||||
setSystemBusy(true)
|
||||
setMessage('')
|
||||
try {
|
||||
const res = await api.restartBackend()
|
||||
setMessage(res.message || '已发起后端重启')
|
||||
// 给后端一点时间完成重启,再刷新状态
|
||||
setTimeout(() => {
|
||||
loadBackendStatus()
|
||||
}, 4000)
|
||||
} catch (error) {
|
||||
setMessage('重启后端失败: ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
setSystemBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadConfigs()
|
||||
checkFeasibility()
|
||||
loadSystemStatus()
|
||||
loadBackendStatus()
|
||||
|
||||
const timer = setInterval(() => {
|
||||
loadSystemStatus()
|
||||
loadBackendStatus()
|
||||
}, 3000)
|
||||
|
||||
return () => clearInterval(timer)
|
||||
|
|
@ -495,6 +525,7 @@ const ConfigPanel = () => {
|
|||
</span>
|
||||
{systemStatus?.pid ? <span className="system-status-meta">PID: {systemStatus.pid}</span> : null}
|
||||
{systemStatus?.program ? <span className="system-status-meta">程序: {systemStatus.program}</span> : null}
|
||||
{systemStatus?.meta?.requested_at ? <span className="system-status-meta">上次重启: {systemStatus.meta.requested_at}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="system-actions">
|
||||
|
|
@ -534,6 +565,22 @@ const ConfigPanel = () => {
|
|||
>
|
||||
重启交易系统
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="system-btn primary"
|
||||
onClick={handleRestartBackend}
|
||||
disabled={systemBusy}
|
||||
title="通过 backend/restart.sh 重启后端(uvicorn)。重启期间接口会短暂不可用。"
|
||||
>
|
||||
重启后端服务
|
||||
</button>
|
||||
</div>
|
||||
<div className="system-status" style={{ marginTop: '10px' }}>
|
||||
<span className={`system-status-badge ${backendStatus?.running ? 'running' : 'stopped'}`}>
|
||||
后端 {backendStatus?.running ? '运行中' : '未知'}
|
||||
</span>
|
||||
{backendStatus?.pid ? <span className="system-status-meta">PID: {backendStatus.pid}</span> : null}
|
||||
{backendStatus?.meta?.requested_at ? <span className="system-status-meta">上次重启: {backendStatus.meta.requested_at}</span> : null}
|
||||
</div>
|
||||
<div className="system-hint">
|
||||
建议流程:先更新配置里的 Key → 点击“清除缓存” → 点击“重启交易系统”,确保不再使用旧账号下单。
|
||||
|
|
|
|||
|
|
@ -298,6 +298,28 @@ export const api = {
|
|||
return response.json();
|
||||
},
|
||||
|
||||
// 后端控制(uvicorn)
|
||||
getBackendStatus: async () => {
|
||||
const response = await fetch(buildUrl('/api/system/backend/status'));
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: '获取后端状态失败' }));
|
||||
throw new Error(error.detail || '获取后端状态失败');
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
restartBackend: async () => {
|
||||
const response = await fetch(buildUrl('/api/system/backend/restart'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: '重启后端失败' }));
|
||||
throw new Error(error.detail || '重启后端失败');
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
// 日志监控(Redis List)
|
||||
getSystemLogs: async (params = {}) => {
|
||||
const query = new URLSearchParams(params).toString();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user