mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-09-21 01:17:56 +00:00
feat: per-forward worker 模型 — 每条转发独立 frpc 配置+独立进程
核心改动(ring 协议不变,Task 本来就是 {Local,Remote,Link} 三元组):
- process.WorkerKey: worker 键改为 'local~remote~port' 三元组
- renderForward: 每条 forward 渲染只含 1 个 proxy 的独立 frpc 配置
(替代 renderRemote 把该 remote 全部 forwards 塞进一个进程)
- ClaimFn/RevokeFn: 认领/撤销只操作这一条 forward 自己的进程
- SyncWorkers/auto-start: 只拉起 localOnly 转发的独立进程,
集群转发由 ring 认领节点拉起 (消除三台争抢 proxy already exists)
- RemoteStatus/StartRemote/StopRemote/RestartRemote: remote 级聚合辅助,
保持 /profiles API 形状不变, 前端零改动
- handleStatus/logs: 按 worker key 解析真实 remote 名
效果: 三台节点的 frpc 各自只注册自己拥有的 proxy, 单条转发故障不再波及兄弟
This commit is contained in:
@ -2,11 +2,13 @@ package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"webui4frpc/internal/cluster"
|
||||
"webui4frpc/internal/process"
|
||||
"webui4frpc/internal/store"
|
||||
)
|
||||
|
||||
@ -127,8 +129,11 @@ func (h *Handler) applyCanvas(w http.ResponseWriter, r *http.Request, canvas *ca
|
||||
if !keep[old.Name] {
|
||||
if old.LocalOnly {
|
||||
if h.Process != nil {
|
||||
// Per-forward model: stop every localOnly worker of this local.
|
||||
if fwd, _ := s.LinksForLocal(old.Name); len(fwd) > 0 {
|
||||
_ = h.Process.Stop(fwd[0].Remote)
|
||||
for _, f := range fwd {
|
||||
_ = h.Process.Stop(process.WorkerKey(old.Name, f.Remote, f.RemotePort))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if h.Ring != nil {
|
||||
@ -324,9 +329,13 @@ func (h *Handler) handleRemoteUpsert(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Start the worker if enabled.
|
||||
// Start the per-forward workers for this remote if enabled.
|
||||
if rem.Enabled {
|
||||
_ = h.Process.Start(rem.Name)
|
||||
// In the per-forward model, merely upserting a remote does not start
|
||||
// any workers — the actual links control which forwards run. The
|
||||
// caller is expected to save the canvas (PUT /canvas) or start
|
||||
// individual forwards via the forwards page.
|
||||
log.Printf("remote %s upserted (enabled=%v); use status page forwards to start", rem.Name, rem.Enabled)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, rem)
|
||||
}
|
||||
@ -341,7 +350,7 @@ func (h *Handler) handleRemoteDelete(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "remote name required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_ = h.Process.Stop(name)
|
||||
_ = h.Process.StopRemote(name)
|
||||
if err := h.Store.DeleteRemote(name); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@ -367,8 +376,10 @@ func (h *Handler) handleProfile(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
switch action {
|
||||
case "":
|
||||
// GET profile status
|
||||
st, has := h.Process.Status(name)
|
||||
// GET profile status — aggregate per-forward workers into a single
|
||||
// remote-centric view. "hasProcess" is true when ANY forward worker
|
||||
// for this remote is running.
|
||||
st, has := h.Process.RemoteStatus(name)
|
||||
forwards, _ := h.Store.LinksForRemote(name)
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"name": name, "process": st, "hasProcess": has, "forwards": forwards,
|
||||
@ -384,11 +395,11 @@ func (h *Handler) handleProfile(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
var err error
|
||||
if action == "start" {
|
||||
err = h.Process.Start(name)
|
||||
err = h.Process.StartRemote(name)
|
||||
} else if action == "stop" {
|
||||
err = h.Process.Stop(name)
|
||||
err = h.Process.StopRemote(name)
|
||||
} else {
|
||||
err = h.Process.Restart(name)
|
||||
err = h.Process.RestartRemote(name)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
@ -399,7 +410,15 @@ func (h *Handler) handleProfile(w http.ResponseWriter, r *http.Request) {
|
||||
methodNotAllowed(w)
|
||||
}
|
||||
case "config":
|
||||
data, err := os.ReadFile(h.Process.ConfigPath(name))
|
||||
// Per-forward model: no single config file for a remote. Return the
|
||||
// first forward's config as a hint; the status page already shows
|
||||
// per-forward configs.
|
||||
workers := h.Process.RemoteWorkers(name)
|
||||
if len(workers) == 0 {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(h.Process.ConfigPath(workers[0]))
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
@ -407,7 +426,13 @@ func (h *Handler) handleProfile(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(data)
|
||||
case "logs":
|
||||
path := h.Process.LogPath(name)
|
||||
// Per-forward model: return the first per-forward log for this remote.
|
||||
workers := h.Process.RemoteWorkers(name)
|
||||
if len(workers) == 0 {
|
||||
http.Error(w, "no workers for this remote", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
path := h.Process.LogPath(workers[0])
|
||||
data, err := tailFile(path, 64*1024)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"webui4frpc/internal/cluster"
|
||||
"webui4frpc/internal/process"
|
||||
"webui4frpc/internal/store"
|
||||
)
|
||||
|
||||
@ -76,8 +77,11 @@ func (h *Handler) handleLocalByName(w http.ResponseWriter, r *http.Request) {
|
||||
case http.MethodDelete:
|
||||
if l.LocalOnly {
|
||||
if h.Process != nil {
|
||||
// Per-forward model: stop every localOnly worker of this local.
|
||||
if fwd, _ := h.Store.LinksForLocal(name); len(fwd) > 0 {
|
||||
_ = h.Process.Stop(fwd[0].Remote)
|
||||
for _, f := range fwd {
|
||||
_ = h.Process.Stop(process.WorkerKey(name, f.Remote, f.RemotePort))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if h.Ring != nil {
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"webui4frpc/internal/process"
|
||||
"webui4frpc/internal/store"
|
||||
)
|
||||
|
||||
@ -74,10 +75,11 @@ func (h *Handler) startForward(local, remote string, port int) error {
|
||||
ln.Disabled = false
|
||||
if loc.LocalOnly {
|
||||
if h.Process != nil {
|
||||
if _, has := h.Process.Status(remote); has {
|
||||
_ = h.Process.Restart(remote)
|
||||
key := process.WorkerKey(local, remote, port)
|
||||
if _, has := h.Process.Status(key); has {
|
||||
_ = h.Process.Restart(key)
|
||||
} else {
|
||||
_ = h.Process.Start(remote)
|
||||
_ = h.Process.Start(key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@ -109,8 +111,9 @@ func (h *Handler) stopForward(local, remote string, port int) error {
|
||||
}
|
||||
if loc.LocalOnly {
|
||||
if h.Process != nil {
|
||||
if _, has := h.Process.Status(remote); has {
|
||||
_ = h.Process.Restart(remote)
|
||||
key := process.WorkerKey(local, remote, port)
|
||||
if _, has := h.Process.Status(key); has {
|
||||
_ = h.Process.Restart(key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@ -6,6 +6,8 @@ import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"webui4frpc/internal/process"
|
||||
)
|
||||
|
||||
// workerLog is one frpc worker's log tail on a node.
|
||||
@ -34,8 +36,13 @@ func (h *Handler) localWorkerLogs() nodeLogsResp {
|
||||
for _, name := range h.Process.ListWorkers() {
|
||||
st, _ := h.Process.Status(name)
|
||||
lines, _ := tailFile(h.Process.LogPath(name), 64*1024)
|
||||
// Parse the worker key to get the real remote name for display.
|
||||
remoteName := name
|
||||
if _, r, _, ok := process.ParseWorkerKey(name); ok {
|
||||
remoteName = r
|
||||
}
|
||||
out.Workers = append(out.Workers, workerLog{
|
||||
Name: name, Remote: name, State: st.State, Lines: lines,
|
||||
Name: name, Remote: remoteName, State: st.State, Lines: lines,
|
||||
})
|
||||
}
|
||||
return out
|
||||
|
||||
@ -257,7 +257,7 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
binary = h.BinaryPath()
|
||||
}
|
||||
for _, rv := range remotes {
|
||||
st, has := h.Process.Status(rv.Name)
|
||||
st, has := h.Process.RemoteStatus(rv.Name)
|
||||
fwd, _ := h.Store.LinksForRemote(rv.Name)
|
||||
p := profileStatus{
|
||||
Name: rv.Name, Enabled: rv.Enabled, Status: st, HasProc: has, Forwards: fwd,
|
||||
@ -271,10 +271,12 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
// Prefer admin-derived conn state; fall back to log inference.
|
||||
// In the per-forward model multiple workers may exist for one remote;
|
||||
// use the first worker's log when available.
|
||||
if p.AdminEnabled && len(p.ProxyStates) > 0 {
|
||||
p.ConnState = connStateAdmin(p.ProxyStates)
|
||||
} else {
|
||||
p.ConnState = connStateOf(h.Process.LogPath(rv.Name), rv.Enabled, st)
|
||||
p.ConnState = connStateOf(firstWorkerLog(h.Process, rv.Name), rv.Enabled, st)
|
||||
}
|
||||
profiles = append(profiles, p)
|
||||
}
|
||||
@ -300,11 +302,17 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
ts := make([]localTargetStatus, 0, len(targets))
|
||||
for _, tg := range targets {
|
||||
st, _ := h.Process.Status(tg.Remote)
|
||||
// Per-forward model: this target's worker is the specific
|
||||
// (local,remote,port) forward; active = its own process running.
|
||||
st, has := h.Process.Status(process.WorkerKey(l.Name, tg.Remote, tg.RemotePort))
|
||||
state := "stopped"
|
||||
if has {
|
||||
state = st.State
|
||||
}
|
||||
ts = append(ts, localTargetStatus{
|
||||
Remote: tg.Remote,
|
||||
RemotePort: tg.RemotePort,
|
||||
WorkerState: st.State,
|
||||
WorkerState: state,
|
||||
})
|
||||
}
|
||||
localStatuses = append(localStatuses, localStatus{Local: l, Targets: ts})
|
||||
@ -346,7 +354,8 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if loc.LocalOnly {
|
||||
fs.Kind = "local"
|
||||
fs.OwnerID = selfID
|
||||
if st, has := h.Process.Status(ln.Remote); has {
|
||||
// Per-forward model: check this exact forward's worker process.
|
||||
if st, has := h.Process.Status(process.WorkerKey(ln.Local, ln.Remote, ln.RemotePort)); has {
|
||||
fs.Active = !ln.Disabled && st.State == "running"
|
||||
}
|
||||
} else {
|
||||
@ -727,6 +736,16 @@ func frpcVersionOf(binPath string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// firstWorkerLog returns the log path of the first per-forward worker of a
|
||||
// remote (worker keys are "local~remote~port"), or "" when none exist. Used
|
||||
// by status aggregation to infer frps connectivity from worker logs.
|
||||
func firstWorkerLog(pm *process.Manager, remote string) string {
|
||||
for _, key := range pm.RemoteWorkers(remote) {
|
||||
return pm.LogPath(key)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
Reference in New Issue
Block a user