| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- package service
- import (
- "context"
- "sync"
- "time"
- "github.com/mhsanaei/3x-ui/v3/database"
- "github.com/mhsanaei/3x-ui/v3/database/model"
- "github.com/mhsanaei/3x-ui/v3/web/runtime"
- )
- // LocalDescendants returns this panel's read-only summaries of the nodes it
- // directly manages, so a parent panel can surface them as transitive sub-nodes
- // (#4983). Only nodes with a known GUID are included — a stable identity is
- // required to attribute them one hop up. Not recursive: each panel reports its
- // own direct nodes, and a master walks one level via each direct node's
- // endpoint, which covers the Node1 -> Node2 -> Node3 case.
- func (s *NodeService) LocalDescendants() ([]model.NodeSummary, error) {
- selfGuid, _ := (&SettingService{}).GetPanelGuid()
- db := database.GetDB()
- var nodes []*model.Node
- if err := db.Model(model.Node{}).Order("id asc").Find(&nodes).Error; err != nil {
- return nil, err
- }
- out := make([]model.NodeSummary, 0, len(nodes))
- for _, n := range nodes {
- if n.Guid == "" {
- continue
- }
- out = append(out, model.NodeSummary{
- Guid: n.Guid,
- ParentGuid: selfGuid,
- Name: n.Name,
- Address: n.Address,
- Scheme: n.Scheme,
- Port: n.Port,
- Status: n.Status,
- LastHeartbeat: n.LastHeartbeat,
- LatencyMs: n.LatencyMs,
- PanelVersion: n.PanelVersion,
- XrayVersion: n.XrayVersion,
- })
- }
- return out, nil
- }
- var (
- nodeDescendantsMu sync.RWMutex
- nodeDescendantsCache = map[int][]model.NodeSummary{}
- )
- // RefreshDescendants pulls a direct node's published sub-node summaries and
- // caches them keyed by node id. Best-effort: a fetch error keeps the last good
- // set (the node may be briefly unreachable). Called from the heartbeat job.
- func (s *NodeService) RefreshDescendants(ctx context.Context, n *model.Node) {
- if n == nil {
- return
- }
- mgr := runtime.GetManager()
- if mgr == nil {
- return
- }
- rt, err := mgr.RemoteFor(n)
- if err != nil {
- return
- }
- summaries, err := rt.GetDescendants(ctx)
- if err != nil {
- return
- }
- nodeDescendantsMu.Lock()
- if len(summaries) == 0 {
- delete(nodeDescendantsCache, n.Id)
- } else {
- nodeDescendantsCache[n.Id] = summaries
- }
- nodeDescendantsMu.Unlock()
- }
- // ClearDescendants drops a node's cached sub-node summaries (its probe failed).
- func (s *NodeService) ClearDescendants(nodeID int) {
- nodeDescendantsMu.Lock()
- delete(nodeDescendantsCache, nodeID)
- nodeDescendantsMu.Unlock()
- }
- func cachedDescendants() []model.NodeSummary {
- nodeDescendantsMu.RLock()
- defer nodeDescendantsMu.RUnlock()
- out := make([]model.NodeSummary, 0)
- for _, list := range nodeDescendantsCache {
- out = append(out, list...)
- }
- return out
- }
- // GetNodeTree returns the direct nodes plus any transitive sub-nodes learned
- // from them, with per-GUID counts so each node shows only the inbounds/online
- // it physically hosts (#4983). Direct nodes carry the master's own GUID as
- // ParentGuid; a transitive node carries its parent node's GUID. Transitive
- // nodes are read-only projections (Id == 0). Used by the Nodes page and the
- // heartbeat broadcast — never for probing/syncing, which stay on GetAll.
- func (s *NodeService) GetNodeTree() ([]*model.Node, error) {
- nodes, err := s.GetAll()
- if err != nil {
- return nodes, err
- }
- selfGuid, _ := (&SettingService{}).GetPanelGuid()
- directGuids := make(map[string]struct{}, len(nodes))
- for _, n := range nodes {
- n.ParentGuid = selfGuid
- if n.Guid != "" {
- directGuids[n.Guid] = struct{}{}
- }
- }
- seen := make(map[string]struct{})
- var transitive []*model.Node
- for _, sum := range cachedDescendants() {
- if sum.Guid == "" {
- continue
- }
- if _, ok := directGuids[sum.Guid]; ok {
- continue // already shown as a direct node
- }
- if _, ok := seen[sum.Guid]; ok {
- continue
- }
- seen[sum.Guid] = struct{}{}
- transitive = append(transitive, &model.Node{
- Guid: sum.Guid,
- ParentGuid: sum.ParentGuid,
- Name: sum.Name,
- Address: sum.Address,
- Scheme: sum.Scheme,
- Port: sum.Port,
- Status: sum.Status,
- LastHeartbeat: sum.LastHeartbeat,
- LatencyMs: sum.LatencyMs,
- PanelVersion: sum.PanelVersion,
- XrayVersion: sum.XrayVersion,
- Transitive: true,
- })
- }
- if len(transitive) == 0 {
- return nodes, nil
- }
- all := make([]*model.Node, 0, len(nodes)+len(transitive))
- all = append(all, nodes...)
- all = append(all, transitive...)
- s.recountByGuid(all, selfGuid)
- return all, nil
- }
- // recountByGuid recomputes InboundCount/OnlineCount/DepletedCount for every node
- // in the tree, keyed by the GUID that physically hosts each inbound, so a direct
- // node shows only its own inbounds and each transitive node shows its own
- // (#4983). In a flat topology the per-GUID and per-node-id counts coincide, so
- // this only changes behaviour once a transitive node exists.
- func (s *NodeService) recountByGuid(nodes []*model.Node, selfGuid string) {
- db := database.GetDB()
- type ibRow struct {
- Id int
- NodeID *int `gorm:"column:node_id"`
- OriginNodeGuid string `gorm:"column:origin_node_guid"`
- }
- var ibRows []ibRow
- if err := db.Table("inbounds").Select("id, node_id, origin_node_guid").Scan(&ibRows).Error; err != nil {
- return
- }
- effByInbound := make(map[int]string, len(ibRows))
- inboundCountByGuid := make(map[string]int)
- ids := make([]int, 0, len(ibRows))
- for _, r := range ibRows {
- guid := r.OriginNodeGuid
- if guid == "" {
- if r.NodeID != nil {
- guid = synthNodeGuid(*r.NodeID)
- } else {
- guid = selfGuid
- }
- }
- effByInbound[r.Id] = guid
- inboundCountByGuid[guid]++
- ids = append(ids, r.Id)
- }
- now := time.Now().UnixMilli()
- depletedByGuid := make(map[string]int)
- if len(ids) > 0 {
- type tRow struct {
- InboundID int `gorm:"column:inbound_id"`
- Enable bool
- Total int64
- Up int64
- Down int64
- ExpiryTime int64 `gorm:"column:expiry_time"`
- }
- var tRows []tRow
- if err := db.Table("client_traffics").
- Select("inbound_id, enable, total, up, down, expiry_time").
- Where("inbound_id IN ?", ids).Scan(&tRows).Error; err == nil {
- for _, row := range tRows {
- guid, ok := effByInbound[row.InboundID]
- if !ok {
- continue
- }
- expired := row.ExpiryTime > 0 && row.ExpiryTime <= now
- exhausted := row.Total > 0 && row.Up+row.Down >= row.Total
- if expired || exhausted || !row.Enable {
- depletedByGuid[guid]++
- }
- }
- }
- }
- onlineByGuid := s.onlineEmailsByGuid()
- for _, n := range nodes {
- guid := effectiveNodeGuid(n)
- n.InboundCount = inboundCountByGuid[guid]
- n.OnlineCount = len(onlineByGuid[guid])
- n.DepletedCount = depletedByGuid[guid]
- }
- }
|