根因:1035d00删本地核心时, inbound_node.go 的在线存储层被一并掏空 (GetOnlineClients 写死空/SetNodeOnlineTree 等空函数/SetRemoteTraffic 末尾喂树整块删除), 而节点轮询与 agent 上报两条到达路径完好, 数据 送达即被扔, 客户端/入站在线永远全零。 修复: 把原 xray.Process 上的在线存储(算法/锁/grace 20s 语义逐字 移植)落到 service 包单例 onlineStore(sharedOnlineStore, 对应原 currentXrayProcess 的单一持有语义); 恢复六个 wrapper + 新增 SetNodeActiveInboundTree; setRemoteTrafficLocked 补回 guidShared 与喂树块(原f727d04逻辑, process. 换 s.)。 偏离: 本地集在精简主控无本地核心喂养, 仅 mtproto job 折入(该 job 已不被调度, 路径保留无害); GetOnlineClients 对 nil 归一为 []。 测试: online_store_test 9 例(-race 全过: 并集/按GUID/替换/清空/ grace 边界/跨实例共享/并发); SetRemoteTraffic 集成 2 例(传统节点 GUID 树+active 树落库、旧节点 flat 回退); 全量 go test 串行复跑失败集合与基线 9 红逐字一致(零新增); service 包单跑唯一红为基线既有的 TestGetXrayConfig 红, 其余全过。
123 lines
3.9 KiB
Go
123 lines
3.9 KiB
Go
package service
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
|
|
)
|
|
|
|
// The traditional-node path: NodeTrafficSyncJob fetches a TrafficSnapshot
|
|
// from each node panel (runtime.Remote.FetchTrafficSnapshot) and hands it
|
|
// to InboundService.SetRemoteTraffic. Before the online-store restoration,
|
|
// the snapshot's OnlineTree / ActiveInboundTree were dropped on the floor
|
|
// inside that call (the store setters were no-op stubs), so polled nodes
|
|
// never showed anyone online. This drives the exact public entry the job
|
|
// calls and asserts the snapshot's online data lands in the store.
|
|
func TestSetRemoteTraffic_FeedsOnlineStore(t *testing.T) {
|
|
setupConflictDB(t)
|
|
db := database.GetDB()
|
|
svc := InboundService{}
|
|
|
|
const nodeID = 4242
|
|
const nodeGuid = "trad-node-guid-4242"
|
|
svc.ClearNodeOnlineClients(nodeID)
|
|
t.Cleanup(func() { svc.ClearNodeOnlineClients(nodeID) })
|
|
|
|
if err := db.Create(&model.Node{
|
|
Id: nodeID,
|
|
Name: "trad-node",
|
|
Address: "10.0.4.2",
|
|
Port: 2053,
|
|
ApiToken: "t",
|
|
Guid: nodeGuid,
|
|
}).Error; err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
|
|
snap := &runtime.TrafficSnapshot{
|
|
Inbounds: []*model.Inbound{
|
|
{
|
|
Tag: "in-443-tcp",
|
|
Enable: true,
|
|
Port: 443,
|
|
Protocol: model.VLESS,
|
|
Settings: `{"clients":[{"email":"trad@example.com"}]}`,
|
|
},
|
|
},
|
|
OnlineTree: map[string][]string{nodeGuid: {"trad@example.com"}},
|
|
ActiveInboundTree: map[string][]string{nodeGuid: {"in-443-tcp"}},
|
|
}
|
|
|
|
if _, err := svc.SetRemoteTraffic(nodeID, snap, false, false); err != nil {
|
|
t.Fatalf("SetRemoteTraffic: %v", err)
|
|
}
|
|
|
|
assertStringSet(t, "online union", svc.GetOnlineClients(), []string{"trad@example.com"})
|
|
byGuid := svc.GetOnlineClientsByGuid()
|
|
assertStringSet(t, "online byGuid", byGuid[nodeGuid], []string{"trad@example.com"})
|
|
|
|
// The active tree is normalized from node-side tags to central tags;
|
|
// whatever tag the adopted central inbound carries is what must show.
|
|
var central model.Inbound
|
|
if err := db.Where("node_id = ?", nodeID).First(¢ral).Error; err != nil {
|
|
t.Fatalf("load central inbound: %v", err)
|
|
}
|
|
active := svc.GetActiveInboundsByGuid()
|
|
assertStringSet(t, "active byGuid", active[nodeGuid], []string{central.Tag})
|
|
|
|
// A failed probe makes the job clear the node; its clients must stop
|
|
// showing as online immediately.
|
|
svc.ClearNodeOnlineClients(nodeID)
|
|
if got := svc.GetOnlineClients(); len(got) != 0 {
|
|
t.Fatalf("online union after clear = %v, want empty", got)
|
|
}
|
|
}
|
|
|
|
// Old-build nodes have no GUID tree endpoint: the job passes their flat
|
|
// OnlineEmails in the snapshot and SetRemoteTraffic must key them under
|
|
// the node's own effective identity.
|
|
func TestSetRemoteTraffic_FlatOnlineEmailsFallback(t *testing.T) {
|
|
setupConflictDB(t)
|
|
db := database.GetDB()
|
|
svc := InboundService{}
|
|
|
|
const nodeID = 4243
|
|
const nodeGuid = "flat-node-guid-4243"
|
|
svc.ClearNodeOnlineClients(nodeID)
|
|
t.Cleanup(func() { svc.ClearNodeOnlineClients(nodeID) })
|
|
|
|
if err := db.Create(&model.Node{
|
|
Id: nodeID,
|
|
Name: "flat-node",
|
|
Address: "10.0.4.3",
|
|
Port: 2053,
|
|
ApiToken: "t",
|
|
Guid: nodeGuid,
|
|
}).Error; err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
|
|
snap := &runtime.TrafficSnapshot{
|
|
Inbounds: []*model.Inbound{
|
|
{
|
|
Tag: "in-8443-tcp",
|
|
Enable: true,
|
|
Port: 8443,
|
|
Protocol: model.VLESS,
|
|
Settings: `{"clients":[{"email":"flat@example.com"}]}`,
|
|
},
|
|
},
|
|
OnlineEmails: []string{"flat@example.com"},
|
|
}
|
|
|
|
if _, err := svc.SetRemoteTraffic(nodeID, snap, false, false); err != nil {
|
|
t.Fatalf("SetRemoteTraffic: %v", err)
|
|
}
|
|
|
|
assertStringSet(t, "online union (flat)", svc.GetOnlineClients(), []string{"flat@example.com"})
|
|
byGuid := svc.GetOnlineClientsByGuid()
|
|
assertStringSet(t, "online byGuid (flat)", byGuid[nodeGuid], []string{"flat@example.com"})
|
|
}
|