service.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. package sitecheck
  2. import (
  3. "context"
  4. "sync"
  5. "time"
  6. "github.com/0xJacky/Nginx-UI/internal/cache"
  7. "github.com/uozi-tech/cosy/kernel"
  8. "github.com/uozi-tech/cosy/logger"
  9. )
  10. // Service manages site checking operations
  11. type Service struct {
  12. checker *SiteChecker
  13. ctx context.Context
  14. cancel context.CancelFunc
  15. ticker *time.Ticker
  16. mu sync.RWMutex
  17. running bool
  18. }
  19. var (
  20. globalService *Service
  21. )
  22. // Init initializes the site checking service
  23. func Init(ctx context.Context) {
  24. globalService = NewService(ctx, DefaultCheckOptions())
  25. // Register post-scan callback to refresh sites when configs change
  26. cache.RegisterPostScanCallback(func() {
  27. if globalService != nil && globalService.IsRunning() {
  28. globalService.RefreshSites()
  29. }
  30. })
  31. globalService.Start()
  32. }
  33. // GetService returns the singleton service instance
  34. func GetService() *Service {
  35. return globalService
  36. }
  37. // waitForSiteCollection waits for the cache scanner to collect sites with progressive backoff
  38. func (s *Service) waitForSiteCollection(ctx context.Context) {
  39. startTime := time.Now()
  40. logger.Debug("Waiting for site collection to complete...")
  41. // First, wait for the initial cache scan to complete
  42. // This is much more efficient than polling
  43. logger.Debug("Waiting for initial cache scan to complete before site collection...")
  44. cache.WaitForInitialScanComplete()
  45. logger.Debugf("Initial cache scan completed after %v, now collecting sites", time.Since(startTime))
  46. // Now collect sites - the cache scanning should have populated IndexedSites
  47. s.checker.CollectSites()
  48. siteCount := s.checker.GetSiteCount()
  49. logger.Debugf("Site collection completed: found %d sites after %v", siteCount, time.Since(startTime))
  50. // If no sites found after cache scan, do a brief fallback check
  51. if siteCount == 0 {
  52. logger.Debug("No sites found after cache scan completion, doing fallback check...")
  53. maxWaitTime := 10 * time.Second // Reduced from 30s since cache scan already completed
  54. checkInterval := 2 * time.Second // Reduced interval
  55. for {
  56. // Check if context is cancelled
  57. select {
  58. case <-ctx.Done():
  59. logger.Debug("Site collection fallback wait cancelled")
  60. return
  61. default:
  62. }
  63. // Re-check for sites
  64. s.checker.CollectSites()
  65. siteCount = s.checker.GetSiteCount()
  66. logger.Debugf("Fallback site collection check: found %d sites (total waited %v)",
  67. siteCount, time.Since(startTime))
  68. if siteCount > 0 {
  69. logger.Debugf("Site collection completed via fallback: found %d sites", siteCount)
  70. return
  71. }
  72. // Check if we've exceeded max fallback wait time
  73. if time.Since(startTime) >= maxWaitTime {
  74. logger.Debugf("Site collection fallback timeout after %v - proceeding with empty site list",
  75. time.Since(startTime))
  76. return
  77. }
  78. // Wait before next check
  79. select {
  80. case <-ctx.Done():
  81. return
  82. case <-time.After(checkInterval):
  83. // Continue to next iteration
  84. }
  85. }
  86. }
  87. }
  88. // NewService creates a new site checking service
  89. func NewService(ctx context.Context, options CheckOptions) *Service {
  90. return NewServiceWithContext(ctx, options)
  91. }
  92. // NewServiceWithContext creates a new site checking service with a parent context
  93. func NewServiceWithContext(parentCtx context.Context, options CheckOptions) *Service {
  94. ctx, cancel := context.WithCancel(parentCtx)
  95. return &Service{
  96. checker: NewSiteChecker(options),
  97. ctx: ctx,
  98. cancel: cancel,
  99. }
  100. }
  101. // SetUpdateCallback sets the callback function for site updates
  102. func (s *Service) SetUpdateCallback(callback func([]*SiteInfo)) {
  103. s.checker.SetUpdateCallback(callback)
  104. }
  105. // Start begins the site checking service
  106. func (s *Service) Start() {
  107. s.mu.Lock()
  108. defer s.mu.Unlock()
  109. if s.running {
  110. return
  111. }
  112. s.running = true
  113. // Initial collection and check with delay to allow cache scanner to complete
  114. go kernel.Run(s.ctx, "sitecheck initial collection goroutine", func(ctx context.Context) {
  115. sl := logger.NewSessionLogger(ctx)
  116. sl.Debug("Started sitecheck initial collection goroutine")
  117. // Give cache scanner more time to start up before checking
  118. time.Sleep(5 * time.Second)
  119. // Wait for cache scanner to collect sites with progressive backoff
  120. s.waitForSiteCollection(s.ctx)
  121. s.checker.CheckAllSites(s.ctx)
  122. sl.Debug("Sitecheck initial collection goroutine completed")
  123. })
  124. // Start periodic checking (every 5 minutes)
  125. s.ticker = time.NewTicker(5 * time.Minute)
  126. go kernel.Run(s.ctx, "sitecheck periodic check goroutine", func(ctx context.Context) {
  127. sl := logger.NewSessionLogger(ctx)
  128. sl.Debug("Started sitecheck periodicCheck goroutine")
  129. s.periodicCheck()
  130. sl.Debug("Sitecheck periodicCheck goroutine completed")
  131. })
  132. }
  133. // Stop stops the site checking service
  134. func (s *Service) Stop() {
  135. s.mu.Lock()
  136. defer s.mu.Unlock()
  137. if !s.running {
  138. return
  139. }
  140. sl := logger.NewSessionLogger(s.ctx)
  141. s.running = false
  142. sl.Debug("Stopping site checking service")
  143. if s.ticker != nil {
  144. s.ticker.Stop()
  145. }
  146. s.cancel()
  147. }
  148. // Restart restarts the site checking service
  149. func (s *Service) Restart() {
  150. s.Stop()
  151. time.Sleep(100 * time.Millisecond) // Brief pause
  152. s.Start()
  153. }
  154. // periodicCheck runs periodic site checks
  155. func (s *Service) periodicCheck() {
  156. sl := logger.NewSessionLogger(s.ctx)
  157. for {
  158. select {
  159. case <-s.ctx.Done():
  160. return
  161. case <-s.ticker.C:
  162. sl.Debug("Starting periodic site check")
  163. s.checker.CollectSites() // Re-collect in case sites changed
  164. s.checker.CheckAllSites(s.ctx)
  165. }
  166. }
  167. }
  168. // RefreshSites manually triggers a site collection and check
  169. func (s *Service) RefreshSites() {
  170. go func() {
  171. sl := logger.NewSessionLogger(s.ctx)
  172. sl.Debug("Started sitecheck refresh goroutine")
  173. s.checker.CollectSites()
  174. s.checker.CheckAllSites(s.ctx)
  175. sl.Debug("Sitecheck refresh goroutine completed")
  176. }()
  177. }
  178. // GetSites returns all checked sites with custom ordering applied
  179. func (s *Service) GetSites() []*SiteInfo {
  180. sites := s.checker.GetSitesList()
  181. // Apply custom ordering from database
  182. return s.applySiteOrdering(sites)
  183. }
  184. // GetSiteByURL returns a specific site by URL
  185. func (s *Service) GetSiteByURL(url string) *SiteInfo {
  186. sites := s.checker.GetSites()
  187. if site, exists := sites[url]; exists {
  188. return site
  189. }
  190. return nil
  191. }
  192. // IsRunning returns whether the service is currently running
  193. func (s *Service) IsRunning() bool {
  194. s.mu.RLock()
  195. defer s.mu.RUnlock()
  196. return s.running
  197. }
  198. // applySiteOrdering applies custom ordering from database to sites
  199. func (s *Service) applySiteOrdering(sites []*SiteInfo) []*SiteInfo {
  200. return applyCustomOrdering(sites)
  201. }