a
This commit is contained in:
parent
82fd5e3f58
commit
c348e3acbb
|
|
@ -2,6 +2,7 @@ import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Optional, Tuple
|
from typing import Any, Dict, Optional, Tuple
|
||||||
|
|
||||||
|
|
@ -16,6 +17,41 @@ router = APIRouter(prefix="/api/system")
|
||||||
|
|
||||||
LOG_GROUPS = ("error", "warning", "info")
|
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 异常刷屏(前端可能自动刷新)
|
# 避免 Redis 异常刷屏(前端可能自动刷新)
|
||||||
_last_logs_redis_err_ts: float = 0.0
|
_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:
|
try:
|
||||||
raw, resolved_name, status_all = _status_with_fallback(program)
|
raw, resolved_name, status_all = _status_with_fallback(program)
|
||||||
running, pid, state = _parse_supervisor_status(raw)
|
running, pid, state = _parse_supervisor_status(raw)
|
||||||
|
meta = _system_meta_read("trading:last_restart") or {}
|
||||||
return {
|
return {
|
||||||
"mode": "supervisor",
|
"mode": "supervisor",
|
||||||
"program": program,
|
"program": program,
|
||||||
|
|
@ -723,6 +760,7 @@ async def trading_status(x_admin_token: Optional[str] = Header(default=None, ali
|
||||||
"state": state,
|
"state": state,
|
||||||
"raw": raw,
|
"raw": raw,
|
||||||
"status_all": status_all,
|
"status_all": status_all,
|
||||||
|
"meta": meta,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -791,9 +829,22 @@ async def trading_restart(x_admin_token: Optional[str] = Header(default=None, al
|
||||||
|
|
||||||
program = _get_program_name()
|
program = _get_program_name()
|
||||||
try:
|
try:
|
||||||
|
requested_at = _beijing_time_str()
|
||||||
|
requested_at_ms = int(time.time() * 1000)
|
||||||
out, resolved_name, status_all = _action_with_fallback("restart", program)
|
out, resolved_name, status_all = _action_with_fallback("restart", program)
|
||||||
raw, resolved_name2, status_all2 = _status_with_fallback(resolved_name or program)
|
raw, resolved_name2, status_all2 = _status_with_fallback(resolved_name or program)
|
||||||
running, pid, state = _parse_supervisor_status(raw)
|
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 {
|
return {
|
||||||
"message": "交易系统已重启(supervisor)",
|
"message": "交易系统已重启(supervisor)",
|
||||||
"output": out,
|
"output": out,
|
||||||
|
|
@ -807,7 +858,89 @@ async def trading_restart(x_admin_token: Optional[str] = Header(default=None, al
|
||||||
"raw": raw,
|
"raw": raw,
|
||||||
"status_all": status_all2 or status_all,
|
"status_all": status_all2 or status_all,
|
||||||
},
|
},
|
||||||
|
"meta": {
|
||||||
|
"requested_at": requested_at,
|
||||||
|
"requested_at_ms": requested_at_ms,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"supervisorctl restart 失败: {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 [feasibilityCheck, setFeasibilityCheck] = useState(null)
|
||||||
const [checkingFeasibility, setCheckingFeasibility] = useState(false)
|
const [checkingFeasibility, setCheckingFeasibility] = useState(false)
|
||||||
const [systemStatus, setSystemStatus] = useState(null)
|
const [systemStatus, setSystemStatus] = useState(null)
|
||||||
|
const [backendStatus, setBackendStatus] = useState(null)
|
||||||
const [systemBusy, setSystemBusy] = useState(false)
|
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 () => {
|
const handleClearCache = async () => {
|
||||||
setSystemBusy(true)
|
setSystemBusy(true)
|
||||||
setMessage('')
|
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(() => {
|
useEffect(() => {
|
||||||
loadConfigs()
|
loadConfigs()
|
||||||
checkFeasibility()
|
checkFeasibility()
|
||||||
loadSystemStatus()
|
loadSystemStatus()
|
||||||
|
loadBackendStatus()
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
loadSystemStatus()
|
loadSystemStatus()
|
||||||
|
loadBackendStatus()
|
||||||
}, 3000)
|
}, 3000)
|
||||||
|
|
||||||
return () => clearInterval(timer)
|
return () => clearInterval(timer)
|
||||||
|
|
@ -495,6 +525,7 @@ const ConfigPanel = () => {
|
||||||
</span>
|
</span>
|
||||||
{systemStatus?.pid ? <span className="system-status-meta">PID: {systemStatus.pid}</span> : null}
|
{systemStatus?.pid ? <span className="system-status-meta">PID: {systemStatus.pid}</span> : null}
|
||||||
{systemStatus?.program ? <span className="system-status-meta">程序: {systemStatus.program}</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>
|
</div>
|
||||||
<div className="system-actions">
|
<div className="system-actions">
|
||||||
|
|
@ -534,6 +565,22 @@ const ConfigPanel = () => {
|
||||||
>
|
>
|
||||||
重启交易系统
|
重启交易系统
|
||||||
</button>
|
</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>
|
||||||
<div className="system-hint">
|
<div className="system-hint">
|
||||||
建议流程:先更新配置里的 Key → 点击“清除缓存” → 点击“重启交易系统”,确保不再使用旧账号下单。
|
建议流程:先更新配置里的 Key → 点击“清除缓存” → 点击“重启交易系统”,确保不再使用旧账号下单。
|
||||||
|
|
|
||||||
|
|
@ -298,6 +298,28 @@ export const api = {
|
||||||
return response.json();
|
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)
|
// 日志监控(Redis List)
|
||||||
getSystemLogs: async (params = {}) => {
|
getSystemLogs: async (params = {}) => {
|
||||||
const query = new URLSearchParams(params).toString();
|
const query = new URLSearchParams(params).toString();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user