node_tree.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. package service
  2. import (
  3. "context"
  4. "sync"
  5. "github.com/mhsanaei/3x-ui/v3/internal/database"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  7. "github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
  8. )
  9. // LocalDescendants returns this panel's read-only summaries of the nodes it
  10. // directly manages, so a parent panel can surface them as transitive sub-nodes
  11. // (#4983). Only nodes with a known GUID are included — a stable identity is
  12. // required to attribute them one hop up. Not recursive: each panel reports its
  13. // own direct nodes, and a master walks one level via each direct node's
  14. // endpoint, which covers the Node1 -> Node2 -> Node3 case.
  15. func (s *NodeService) LocalDescendants() ([]model.NodeSummary, error) {
  16. selfGuid, _ := (&SettingService{}).GetPanelGuid()
  17. db := database.GetDB()
  18. var nodes []*model.Node
  19. if err := db.Model(model.Node{}).Order("id asc").Find(&nodes).Error; err != nil {
  20. return nil, err
  21. }
  22. out := make([]model.NodeSummary, 0, len(nodes))
  23. for _, n := range nodes {
  24. if n.Guid == "" {
  25. continue
  26. }
  27. out = append(out, model.NodeSummary{
  28. Guid: n.Guid,
  29. ParentGuid: selfGuid,
  30. Name: n.Name,
  31. Address: n.Address,
  32. Scheme: n.Scheme,
  33. Port: n.Port,
  34. Status: n.Status,
  35. LastHeartbeat: n.LastHeartbeat,
  36. LatencyMs: n.LatencyMs,
  37. PanelVersion: n.PanelVersion,
  38. XrayVersion: n.XrayVersion,
  39. XrayState: n.XrayState,
  40. XrayError: n.XrayError,
  41. })
  42. }
  43. return out, nil
  44. }
  45. var (
  46. nodeDescendantsMu sync.RWMutex
  47. nodeDescendantsCache = map[int][]model.NodeSummary{}
  48. )
  49. // RefreshDescendants pulls a direct node's published sub-node summaries and
  50. // caches them keyed by node id. Best-effort: a fetch error keeps the last good
  51. // set (the node may be briefly unreachable). Called from the heartbeat job.
  52. func (s *NodeService) RefreshDescendants(ctx context.Context, n *model.Node) {
  53. if n == nil {
  54. return
  55. }
  56. mgr := runtime.GetManager()
  57. if mgr == nil {
  58. return
  59. }
  60. rt, err := mgr.RemoteFor(n)
  61. if err != nil {
  62. return
  63. }
  64. summaries, err := rt.GetDescendants(ctx)
  65. if err != nil {
  66. return
  67. }
  68. nodeDescendantsMu.Lock()
  69. if len(summaries) == 0 {
  70. delete(nodeDescendantsCache, n.Id)
  71. } else {
  72. nodeDescendantsCache[n.Id] = summaries
  73. }
  74. nodeDescendantsMu.Unlock()
  75. }
  76. // ClearDescendants drops a node's cached sub-node summaries (its probe failed).
  77. func (s *NodeService) ClearDescendants(nodeID int) {
  78. nodeDescendantsMu.Lock()
  79. delete(nodeDescendantsCache, nodeID)
  80. nodeDescendantsMu.Unlock()
  81. }
  82. // RetainEnabledNodeDescendants drops sub-nodes learned from nodes the heartbeat no
  83. // longer probes: disabled ones it skips, deleted ones missing from nodes.
  84. func (s *NodeService) RetainEnabledNodeDescendants(nodes []*model.Node) {
  85. enabled := make(map[int]bool, len(nodes))
  86. for _, n := range nodes {
  87. enabled[n.Id] = n.Enable
  88. }
  89. nodeDescendantsMu.Lock()
  90. for nodeID := range nodeDescendantsCache {
  91. if !enabled[nodeID] {
  92. delete(nodeDescendantsCache, nodeID)
  93. }
  94. }
  95. nodeDescendantsMu.Unlock()
  96. }
  97. func cachedDescendants() []model.NodeSummary {
  98. nodeDescendantsMu.RLock()
  99. defer nodeDescendantsMu.RUnlock()
  100. out := make([]model.NodeSummary, 0)
  101. for _, list := range nodeDescendantsCache {
  102. out = append(out, list...)
  103. }
  104. return out
  105. }
  106. // GetNodeTree returns the direct nodes plus any transitive sub-nodes learned
  107. // from them, with per-GUID counts so each node shows only the inbounds/online
  108. // it physically hosts (#4983). Direct nodes carry the master's own GUID as
  109. // ParentGuid; a transitive node carries its parent node's GUID. Transitive
  110. // nodes are read-only projections (Id == 0). Used by the Nodes page and the
  111. // heartbeat broadcast — never for probing/syncing, which stay on GetAll.
  112. func (s *NodeService) GetNodeTree() ([]*model.Node, error) {
  113. nodes, err := s.GetAll()
  114. if err != nil {
  115. return nodes, err
  116. }
  117. selfGuid, _ := (&SettingService{}).GetPanelGuid()
  118. directGuids := make(map[string]struct{}, len(nodes))
  119. for _, n := range nodes {
  120. n.ParentGuid = selfGuid
  121. if n.Guid != "" {
  122. directGuids[n.Guid] = struct{}{}
  123. }
  124. }
  125. seen := make(map[string]struct{})
  126. var transitive []*model.Node
  127. for _, sum := range cachedDescendants() {
  128. if sum.Guid == "" {
  129. continue
  130. }
  131. if _, ok := directGuids[sum.Guid]; ok {
  132. continue // already shown as a direct node
  133. }
  134. if _, ok := seen[sum.Guid]; ok {
  135. continue
  136. }
  137. seen[sum.Guid] = struct{}{}
  138. transitive = append(transitive, &model.Node{
  139. Guid: sum.Guid,
  140. ParentGuid: sum.ParentGuid,
  141. Name: sum.Name,
  142. Address: sum.Address,
  143. Scheme: sum.Scheme,
  144. Port: sum.Port,
  145. Status: sum.Status,
  146. LastHeartbeat: sum.LastHeartbeat,
  147. LatencyMs: sum.LatencyMs,
  148. PanelVersion: sum.PanelVersion,
  149. XrayVersion: sum.XrayVersion,
  150. XrayState: sum.XrayState,
  151. XrayError: sum.XrayError,
  152. Transitive: true,
  153. })
  154. }
  155. if len(transitive) == 0 {
  156. return nodes, nil
  157. }
  158. all := make([]*model.Node, 0, len(nodes)+len(transitive))
  159. all = append(all, nodes...)
  160. all = append(all, transitive...)
  161. s.recountByGuid(all, selfGuid)
  162. return all, nil
  163. }
  164. func (s *NodeService) GetNodeTreeView() ([]*NodeView, error) {
  165. nodes, err := s.GetNodeTree()
  166. if err != nil {
  167. return nil, err
  168. }
  169. return toNodeViews(nodes), nil
  170. }
  171. // recountByGuid recomputes InboundCount/OnlineCount/DepletedCount for every node
  172. // in the tree, keyed by the GUID that physically hosts each inbound, so a direct
  173. // node shows only its own inbounds and each transitive node shows its own
  174. // (#4983). In a flat topology the per-GUID and per-node-id counts coincide, so
  175. // this only changes behaviour once a transitive node exists.
  176. func (s *NodeService) recountByGuid(nodes []*model.Node, selfGuid string) {
  177. db := database.GetDB()
  178. type ibRow struct {
  179. Id int
  180. NodeID *int `gorm:"column:node_id"`
  181. OriginNodeGuid string `gorm:"column:origin_node_guid"`
  182. }
  183. var ibRows []ibRow
  184. if err := db.Table("inbounds").Select("id, node_id, origin_node_guid").Scan(&ibRows).Error; err != nil {
  185. return
  186. }
  187. ambiguous := ambiguousNodeGuids(nodes, selfGuid)
  188. effByInbound := make(map[int]string, len(ibRows))
  189. inboundCountByGuid := make(map[string]int)
  190. for _, r := range ibRows {
  191. guid := r.OriginNodeGuid
  192. if guid == "" {
  193. if r.NodeID != nil {
  194. guid = synthNodeGuid(*r.NodeID)
  195. } else {
  196. guid = selfGuid
  197. }
  198. } else if r.NodeID != nil {
  199. // Origin still holds an ambiguous GUID (cloned server / master-shared,
  200. // not yet re-attributed): bucket under the hosting node's unique id so
  201. // the clones don't merge.
  202. if _, bad := ambiguous[guid]; bad {
  203. guid = synthNodeGuid(*r.NodeID)
  204. }
  205. }
  206. effByInbound[r.Id] = guid
  207. inboundCountByGuid[guid]++
  208. }
  209. // Classify by EMAIL (not the stale client_traffics.inbound_id) and bucket
  210. // each client under its inbound's effective attribution GUID, deduping a
  211. // client attached to several inbounds under the same GUID.
  212. depletedByGuid := make(map[string]int)
  213. disabledByGuid := make(map[string]int)
  214. activeByGuid := make(map[string]int)
  215. if statuses, err := s.nodeClientStatuses(); err == nil {
  216. seen := make(map[string]map[int]struct{})
  217. for _, st := range statuses {
  218. guid, ok := effByInbound[st.InboundID]
  219. if !ok {
  220. continue
  221. }
  222. clientsSeen := seen[guid]
  223. if clientsSeen == nil {
  224. clientsSeen = make(map[int]struct{})
  225. seen[guid] = clientsSeen
  226. }
  227. if _, dup := clientsSeen[st.ClientID]; dup {
  228. continue
  229. }
  230. clientsSeen[st.ClientID] = struct{}{}
  231. switch {
  232. case st.Depleted:
  233. depletedByGuid[guid]++
  234. case st.Disabled:
  235. disabledByGuid[guid]++
  236. default:
  237. activeByGuid[guid]++
  238. }
  239. }
  240. }
  241. onlineByGuid := s.onlineEmailsByGuid()
  242. for _, n := range nodes {
  243. guid := effectiveNodeGuid(n, ambiguous)
  244. n.InboundCount = inboundCountByGuid[guid]
  245. n.OnlineCount = len(onlineByGuid[guid])
  246. n.DepletedCount = depletedByGuid[guid]
  247. n.DisabledCount = disabledByGuid[guid]
  248. n.ActiveCount = activeByGuid[guid]
  249. }
  250. }