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

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)
}