fix: worker 进程并发 double-close panic + 状态页连接语义重构 + 集群页骨架

- process: spawn 不再重复起 supervise goroutine(Restart 触发旧 supervise close doneCh 后再 close → panic);新增 closeOnce/supervising 守卫;Start 首次起 supervise,重启循环在同一 goroutine 内

- httpapi: status 增加 connState(connected/connecting/failed/not_started/disabled),由 worker 日志探测真实 frps 连接(不再把本地 worker 状态误当远程 frps 状态)

- StatusView: 远程卡片主状态改为连接状态徽标;启停按钮标注为控制本地 frpc worker;本地转发区保留 worker 状态显示

- 新增 ClusterView 集群页骨架(M3 LB+健康检查载体)+ App.vue 导航接入

- render: bandwidthLimit 从 proxy 顶层移入 transport 段(frpc 0.61 正确 schema,修复真实连不上的 bug)
This commit is contained in:
2026-08-17 15:13:25 +08:00
parent 58906c5a81
commit bd3a62a017
12 changed files with 221 additions and 57 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webui-frpc</title>
<script type="module" crossorigin src="/assets/index-DHaXL5Wt.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-m13EHiyb.css">
<script type="module" crossorigin src="/assets/index-C04K5jB8.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B6NLA8yp.css">
</head>
<body>
<div id="app"></div>

View File

@ -146,6 +146,9 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
Status process.Status `json:"process"`
HasProc bool `json:"hasProcess"`
Forwards []store.Forward `json:"forwards"`
// ConnState is the derived frps connection state:
// connected | connecting | failed | not_started | disabled.
ConnState string `json:"connState"`
}
profiles := make([]profileStatus, 0, len(remotes))
@ -158,6 +161,7 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
fwd, _ := h.Store.LinksForRemote(rv.Name)
profiles = append(profiles, profileStatus{
Name: rv.Name, Enabled: rv.Enabled, Status: st, HasProc: has, Forwards: fwd,
ConnState: connStateOf(h.Process.LogPath(rv.Name), rv.Enabled, st),
})
}
@ -204,6 +208,49 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, resp)
}
// connStateOf derives the frps connection state from the local worker process
// state plus a probe of its log. We never control the remote frps: the worker is
// our local frpc; "connected" means frpc has successfully logged in to frps.
// - disabled -> remote disabled, worker not started
// - running+login -> frpc logged in to frps (connected)
// - running -> frpc up but not yet logged in (connecting)
// - starting/restarting -> connecting
// - crashed/exit nonzero -> failed
// - stopped clean / no worker -> not_started
func connStateOf(workerLog string, enabled bool, st process.Status) string {
if !enabled {
return "disabled"
}
switch st.State {
case "running":
tail, _ := tailFile(workerLog, 64*1024)
if tail != "" {
// frpc prints "login to server success" once the control link is up.
if strings.Contains(tail, "login to server success") ||
strings.Contains(tail, "start proxy success") {
return "connected"
}
if strings.Contains(tail, "login to server error") ||
strings.Contains(tail, "connect to server error") ||
strings.Contains(tail, "connect server error") {
return "failed"
}
}
return "connecting"
case "starting", "restarting":
return "connecting"
case "crashed":
return "failed"
case "stopped":
if st.ExitCode != 0 || st.Err != "" {
return "failed"
}
return "not_started"
default:
return "not_started"
}
}
func methodNotAllowed(w http.ResponseWriter) {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}