index.go 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935
  1. package cache
  2. import (
  3. "context"
  4. "fmt"
  5. "io/fs"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "sync"
  10. "time"
  11. "github.com/0xJacky/Nginx-UI/internal/event"
  12. "github.com/0xJacky/Nginx-UI/internal/nginx"
  13. "github.com/fsnotify/fsnotify"
  14. "github.com/uozi-tech/cosy/logger"
  15. )
  16. // ScanCallback is called during config scanning with file path and content
  17. type ScanCallback func(configPath string, content []byte) error
  18. // CallbackInfo stores callback function with its name for debugging
  19. type CallbackInfo struct {
  20. Name string
  21. Callback ScanCallback
  22. }
  23. // PostScanCallback is called after all scan callbacks are executed
  24. type PostScanCallback func()
  25. // ScanConfig holds scanner configuration
  26. type ScanConfig struct {
  27. PeriodicScanInterval time.Duration
  28. InitialScanTimeout time.Duration
  29. ScanTimeoutGrace time.Duration
  30. FileEventDebounce time.Duration
  31. MaxFileSize int64
  32. CallbackTimeout time.Duration
  33. PostCallbackTimeout time.Duration
  34. ShutdownTimeout time.Duration
  35. ForceCleanupTimeout time.Duration
  36. InitialScanWaitTimeout time.Duration
  37. }
  38. // DefaultScanConfig returns default configuration
  39. func DefaultScanConfig() ScanConfig {
  40. return ScanConfig{
  41. PeriodicScanInterval: 5 * time.Minute,
  42. InitialScanTimeout: 15 * time.Second,
  43. ScanTimeoutGrace: 2 * time.Second,
  44. FileEventDebounce: 100 * time.Millisecond,
  45. MaxFileSize: 1024 * 1024, // 1MB
  46. CallbackTimeout: 5 * time.Second,
  47. PostCallbackTimeout: 10 * time.Second,
  48. ShutdownTimeout: 10 * time.Second,
  49. ForceCleanupTimeout: 3 * time.Second,
  50. InitialScanWaitTimeout: 30 * time.Second,
  51. }
  52. }
  53. var (
  54. postScanCallbacks = make([]PostScanCallback, 0)
  55. postScanCallbacksMutex sync.RWMutex
  56. scanConfig = DefaultScanConfig()
  57. )
  58. // runWithTimeout executes a function with timeout and panic protection
  59. func runWithTimeout(fn func(), timeout time.Duration, name string) error {
  60. done := make(chan struct{})
  61. var panicErr error
  62. go func() {
  63. defer func() {
  64. if r := recover(); r != nil {
  65. panicErr = fmt.Errorf("panic: %v", r)
  66. logger.Errorf("%s panic: %v", name, r)
  67. }
  68. close(done)
  69. }()
  70. fn()
  71. }()
  72. select {
  73. case <-done:
  74. return panicErr
  75. case <-time.After(timeout):
  76. return fmt.Errorf("timeout after %v", timeout)
  77. }
  78. }
  79. // Scanner watches and scans nginx config files
  80. type Scanner struct {
  81. ctx context.Context
  82. cancel context.CancelFunc
  83. watcher *fsnotify.Watcher
  84. scanTicker *time.Ticker
  85. scanning bool
  86. scanMutex sync.RWMutex
  87. wg sync.WaitGroup // Track running goroutines
  88. debouncer *fileEventDebouncer
  89. }
  90. // fileEventDebouncer prevents rapid repeated scans of the same file
  91. type fileEventDebouncer struct {
  92. mu sync.Mutex
  93. timers map[string]*time.Timer
  94. stopped bool
  95. }
  96. func newFileEventDebouncer() *fileEventDebouncer {
  97. return &fileEventDebouncer{
  98. timers: make(map[string]*time.Timer),
  99. }
  100. }
  101. func (d *fileEventDebouncer) debounce(filePath string, delay time.Duration, fn func()) {
  102. d.mu.Lock()
  103. defer d.mu.Unlock()
  104. // Don't create new timers if stopped
  105. if d.stopped {
  106. return
  107. }
  108. // Cancel existing timer if present
  109. if timer, exists := d.timers[filePath]; exists {
  110. timer.Stop()
  111. }
  112. // Create new timer
  113. d.timers[filePath] = time.AfterFunc(delay, func() {
  114. fn()
  115. // Cleanup
  116. d.mu.Lock()
  117. delete(d.timers, filePath)
  118. d.mu.Unlock()
  119. })
  120. }
  121. func (d *fileEventDebouncer) stop() {
  122. d.mu.Lock()
  123. defer d.mu.Unlock()
  124. d.stopped = true
  125. // Stop and clear all pending timers
  126. for path, timer := range d.timers {
  127. timer.Stop()
  128. delete(d.timers, path)
  129. }
  130. }
  131. var (
  132. scanner *Scanner
  133. scannerInitMutex sync.Mutex
  134. scanCallbacks = make([]CallbackInfo, 0)
  135. scanCallbacksMutex sync.RWMutex
  136. // Channel to signal when initial scan and all callbacks are completed
  137. initialScanComplete chan struct{}
  138. initialScanOnce sync.Once
  139. initialScanCompleteMu sync.Mutex // Protects initialScanComplete channel access
  140. )
  141. // InitScanner initializes the config scanner
  142. func InitScanner(ctx context.Context) {
  143. if nginx.GetConfPath() == "" {
  144. logger.Error("Nginx config path is not set")
  145. return
  146. }
  147. // Force release any existing resources before initialization
  148. ForceReleaseResources()
  149. scanner := GetScanner()
  150. if err := scanner.Initialize(ctx); err != nil {
  151. logger.Error("Failed to initialize config scanner:", err)
  152. // On failure, force cleanup
  153. ForceReleaseResources()
  154. }
  155. }
  156. var (
  157. excludedDirs []string
  158. excludedDirsOnce sync.Once
  159. )
  160. // getExcludedDirs returns cached list of excluded directories
  161. func getExcludedDirs() []string {
  162. excludedDirsOnce.Do(func() {
  163. excludedDirs = []string{
  164. nginx.GetConfPath("ssl"),
  165. nginx.GetConfPath("cache"),
  166. nginx.GetConfPath("logs"),
  167. nginx.GetConfPath("temp"),
  168. nginx.GetConfPath("proxy_temp"),
  169. nginx.GetConfPath("client_body_temp"),
  170. nginx.GetConfPath("fastcgi_temp"),
  171. nginx.GetConfPath("uwsgi_temp"),
  172. nginx.GetConfPath("scgi_temp"),
  173. }
  174. })
  175. return excludedDirs
  176. }
  177. // shouldSkipPath checks if a path should be skipped during scanning or watching
  178. func shouldSkipPath(path string) bool {
  179. for _, excludedDir := range getExcludedDirs() {
  180. if excludedDir != "" && strings.HasPrefix(path, excludedDir) {
  181. return true
  182. }
  183. }
  184. return false
  185. }
  186. // GetScanner returns the singleton scanner instance
  187. func GetScanner() *Scanner {
  188. scannerInitMutex.Lock()
  189. defer scannerInitMutex.Unlock()
  190. if scanner == nil {
  191. scanner = &Scanner{
  192. debouncer: newFileEventDebouncer(),
  193. }
  194. }
  195. return scanner
  196. }
  197. // RegisterCallback adds a named callback to be executed during scans
  198. func RegisterCallback(name string, callback ScanCallback) {
  199. scanCallbacksMutex.Lock()
  200. defer scanCallbacksMutex.Unlock()
  201. scanCallbacks = append(scanCallbacks, CallbackInfo{
  202. Name: name,
  203. Callback: callback,
  204. })
  205. }
  206. // RegisterPostScanCallback adds a callback to be executed after all scan callbacks complete
  207. func RegisterPostScanCallback(callback PostScanCallback) {
  208. postScanCallbacksMutex.Lock()
  209. defer postScanCallbacksMutex.Unlock()
  210. postScanCallbacks = append(postScanCallbacks, callback)
  211. }
  212. // Initialize sets up the scanner and starts watching
  213. func (s *Scanner) Initialize(ctx context.Context) error {
  214. // Initialize the completion channel for this scan cycle with lock protection
  215. initialScanCompleteMu.Lock()
  216. initialScanComplete = make(chan struct{})
  217. initialScanOnce = sync.Once{} // Reset for this initialization
  218. initialScanCompleteMu.Unlock()
  219. // Create cancellable context for this scanner instance
  220. s.ctx, s.cancel = context.WithCancel(ctx)
  221. watcher, err := fsnotify.NewWatcher()
  222. if err != nil {
  223. return err
  224. }
  225. s.watcher = watcher
  226. // Watch all directories recursively first (this is faster than scanning)
  227. if err := s.watchAllDirectories(); err != nil {
  228. return err
  229. }
  230. // Start background processes
  231. s.wg.Go(func() {
  232. s.watchForChanges()
  233. })
  234. s.wg.Go(func() {
  235. s.periodicScan()
  236. })
  237. // Perform initial scan asynchronously to avoid blocking boot process
  238. s.wg.Go(func() {
  239. s.initialScanAsync(ctx)
  240. })
  241. return nil
  242. }
  243. // watchAllDirectories recursively adds all directories under nginx config path to watcher
  244. func (s *Scanner) watchAllDirectories() error {
  245. root := nginx.GetConfPath()
  246. return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
  247. if err != nil {
  248. return err
  249. }
  250. if d.IsDir() {
  251. // Skip excluded directories (ssl, cache, logs, temp, etc.)
  252. if shouldSkipPath(path) {
  253. return filepath.SkipDir
  254. }
  255. // Resolve symlinks to get the actual directory path to watch
  256. actualPath := path
  257. if d.Type()&os.ModeSymlink != 0 {
  258. // This is a symlink, resolve it to get the target path
  259. if resolvedPath, err := filepath.EvalSymlinks(path); err == nil {
  260. actualPath = resolvedPath
  261. logger.Debug("Resolved symlink for watching:", path, "->", actualPath)
  262. } else {
  263. logger.Debug("Failed to resolve symlink, skipping:", path, err)
  264. return filepath.SkipDir
  265. }
  266. }
  267. if err := s.watcher.Add(actualPath); err != nil {
  268. logger.Error("Failed to watch directory:", actualPath, err)
  269. return err
  270. }
  271. }
  272. return nil
  273. })
  274. }
  275. // periodicScan runs periodic scans
  276. func (s *Scanner) periodicScan() {
  277. s.scanTicker = time.NewTicker(scanConfig.PeriodicScanInterval)
  278. defer s.scanTicker.Stop()
  279. for {
  280. select {
  281. case <-s.ctx.Done():
  282. logger.Debug("periodicScan: context cancelled, exiting")
  283. return
  284. case <-s.scanTicker.C:
  285. if err := s.ScanAllConfigs(); err != nil {
  286. logger.Error("Periodic scan failed:", err)
  287. }
  288. }
  289. }
  290. }
  291. // initialScanAsync performs the initial config scan asynchronously
  292. func (s *Scanner) initialScanAsync(ctx context.Context) {
  293. // Always use the provided context, not the scanner's internal context
  294. // This ensures we use the fresh boot context, not a potentially cancelled old context
  295. logger.Debugf("Initial scan starting with context: cancelled=%v", ctx.Err() != nil)
  296. // Check if context is already cancelled before starting
  297. select {
  298. case <-ctx.Done():
  299. logger.Warn("Initial scan cancelled before starting - context already done")
  300. // Signal completion even when cancelled early so waiting services don't hang
  301. initialScanOnce.Do(func() {
  302. logger.Warn("Initial config scan cancelled early - signaling completion")
  303. close(initialScanComplete)
  304. })
  305. return
  306. default:
  307. }
  308. logger.Debug("Starting initial config scan...")
  309. logger.Debugf("Config path: %s", nginx.GetConfPath())
  310. // Perform the scan with the fresh context (not scanner's internal context)
  311. if err := s.scanAllConfigsWithContext(ctx); err != nil {
  312. // Only log error if it's not due to context cancellation
  313. if ctx.Err() == nil {
  314. logger.Errorf("Initial config scan failed: %v", err)
  315. } else {
  316. logger.Debugf("Initial config scan cancelled due to context: %v", ctx.Err())
  317. }
  318. // Signal completion even on error so waiting services don't hang
  319. initialScanOnce.Do(func() {
  320. logger.Warn("Initial config scan completed with error - signaling completion anyway")
  321. close(initialScanComplete)
  322. })
  323. } else {
  324. // Signal that initial scan is complete - this allows other services to proceed
  325. // that depend on the scan callbacks to have been processed
  326. initialScanOnce.Do(func() {
  327. logger.Debug("Initial config scan and callbacks completed - signaling completion")
  328. close(initialScanComplete)
  329. })
  330. }
  331. }
  332. // scanAllConfigsWithContext scans all nginx configuration files with context support
  333. func (s *Scanner) scanAllConfigsWithContext(ctx context.Context) error {
  334. s.setScanningState(true)
  335. defer s.setScanningState(false)
  336. root := nginx.GetConfPath()
  337. logger.Debugf("Scanning config directory: %s", root)
  338. // Create a timeout context for the scan operation
  339. scanCtx, scanCancel := context.WithTimeout(ctx, scanConfig.InitialScanTimeout)
  340. defer scanCancel()
  341. // Scan all files in the config directory and subdirectories
  342. logger.Debug("Starting filepath.WalkDir scanning...")
  343. // Use a channel to communicate scan results
  344. type scanResult struct {
  345. err error
  346. fileCount int
  347. dirCount int
  348. }
  349. resultChan := make(chan scanResult, 1)
  350. // Run custom directory traversal in a goroutine to avoid WalkDir blocking issues
  351. go func() {
  352. defer func() {
  353. if r := recover(); r != nil {
  354. logger.Errorf("Scan goroutine panic: %v", r)
  355. resultChan <- scanResult{err: fmt.Errorf("panic during scan: %v", r)}
  356. }
  357. }()
  358. fileCount := 0
  359. dirCount := 0
  360. // Use custom recursive traversal instead of filepath.WalkDir
  361. walkErr := s.scanDirectoryRecursive(scanCtx, root, &fileCount, &dirCount)
  362. // Send result through channel
  363. resultChan <- scanResult{
  364. err: walkErr,
  365. fileCount: fileCount,
  366. dirCount: dirCount,
  367. }
  368. }()
  369. // Wait for scan to complete or timeout
  370. var scanErr error
  371. select {
  372. case result := <-resultChan:
  373. logger.Debugf("Scan completed successfully: dirs=%d, files=%d, error=%v",
  374. result.dirCount, result.fileCount, result.err)
  375. scanErr = result.err
  376. case <-scanCtx.Done():
  377. logger.Warnf("Scan timed out after 25 seconds - cancelling")
  378. scanCancel()
  379. // Wait a bit more for cleanup
  380. select {
  381. case result := <-resultChan:
  382. logger.Debugf("Scan completed after timeout: dirs=%d, files=%d, error=%v",
  383. result.dirCount, result.fileCount, result.err)
  384. scanErr = result.err
  385. case <-time.After(scanConfig.ScanTimeoutGrace):
  386. logger.Warn("Scan failed to complete even after timeout - forcing return")
  387. scanErr = ctx.Err()
  388. }
  389. }
  390. // Trigger post-scan callbacks once after all files are scanned
  391. if scanErr == nil {
  392. s.executePostScanCallbacks()
  393. }
  394. return scanErr
  395. }
  396. // watchForChanges handles file system events
  397. func (s *Scanner) watchForChanges() {
  398. for {
  399. select {
  400. case <-s.ctx.Done():
  401. logger.Debug("watchForChanges: context cancelled, exiting")
  402. return
  403. case event, ok := <-s.watcher.Events:
  404. if !ok {
  405. logger.Debug("watchForChanges: events channel closed, exiting")
  406. return
  407. }
  408. s.handleFileEvent(event)
  409. case err, ok := <-s.watcher.Errors:
  410. if !ok {
  411. logger.Debug("watchForChanges: errors channel closed, exiting")
  412. return
  413. }
  414. logger.Error("Watcher error:", err)
  415. }
  416. }
  417. }
  418. // handleFileEvent processes individual file system events
  419. func (s *Scanner) handleFileEvent(event fsnotify.Event) {
  420. // Only handle relevant events
  421. if !event.Has(fsnotify.Create) && !event.Has(fsnotify.Write) &&
  422. !event.Has(fsnotify.Rename) && !event.Has(fsnotify.Remove) {
  423. return
  424. }
  425. // Skip excluded directories (ssl, cache, etc.)
  426. if shouldSkipPath(event.Name) {
  427. return
  428. }
  429. // Add new directories to watch
  430. if event.Has(fsnotify.Create) {
  431. if fi, err := os.Stat(event.Name); err == nil && fi.IsDir() {
  432. if err := s.watcher.Add(event.Name); err != nil {
  433. logger.Error("Failed to add new directory to watcher:", event.Name, err)
  434. } else {
  435. logger.Debug("Added new directory to watcher:", event.Name)
  436. }
  437. }
  438. }
  439. // Handle file removal - need to trigger rescan to update indices
  440. if event.Has(fsnotify.Remove) {
  441. logger.Debug("Config removed:", event.Name)
  442. // Trigger callbacks with empty content to allow them to clean up their indices
  443. // Don't skip post-scan for single file events (manual operations)
  444. s.executeCallbacks(event.Name, []byte{}, false)
  445. return
  446. }
  447. // Use Lstat to get symlink info without following it
  448. fi, err := os.Lstat(event.Name)
  449. if err != nil {
  450. return
  451. }
  452. // If it's a symlink, we need to check what it points to
  453. var targetIsDir bool
  454. if fi.Mode()&os.ModeSymlink != 0 {
  455. // For symlinks, check the target
  456. targetFi, err := os.Stat(event.Name)
  457. if err != nil {
  458. logger.Debug("Symlink target not accessible:", event.Name, err)
  459. return
  460. }
  461. targetIsDir = targetFi.IsDir()
  462. logger.Debug("Symlink changed:", event.Name, "-> target is dir:", targetIsDir)
  463. } else {
  464. targetIsDir = fi.IsDir()
  465. }
  466. if targetIsDir {
  467. logger.Debug("Directory changed:", event.Name)
  468. } else {
  469. logger.Debug("File changed:", event.Name)
  470. // Use debouncer to avoid rapid repeated scans
  471. s.debouncer.debounce(event.Name, scanConfig.FileEventDebounce, func() {
  472. s.scanSingleFile(event.Name)
  473. })
  474. }
  475. }
  476. // scanSingleFile scans a single config file without recursion
  477. // skipPostScan: if true, skip post-scan callbacks (used during batch scans)
  478. func (s *Scanner) scanSingleFile(filePath string) error {
  479. return s.scanSingleFileInternal(filePath, false)
  480. }
  481. // scanSingleFileInternal is the internal implementation with post-scan control
  482. func (s *Scanner) scanSingleFileInternal(filePath string, skipPostScan bool) error {
  483. s.setScanningState(true)
  484. defer s.setScanningState(false)
  485. // Check if path should be skipped
  486. if shouldSkipPath(filePath) {
  487. logger.Debugf("File skipped by shouldSkipPath: %s", filePath)
  488. return nil
  489. }
  490. // Get file info to check type and size
  491. fileInfo, err := os.Lstat(filePath) // Use Lstat to avoid following symlinks
  492. if err != nil {
  493. return err
  494. }
  495. // Skip directories
  496. if fileInfo.IsDir() {
  497. logger.Debugf("Skipping directory: %s", filePath)
  498. return nil
  499. }
  500. // Handle symlinks carefully
  501. if fileInfo.Mode()&os.ModeSymlink != 0 {
  502. // Check what the symlink points to
  503. targetInfo, err := os.Stat(filePath)
  504. if err != nil {
  505. logger.Debugf("Skipping symlink with inaccessible target: %s (%v)", filePath, err)
  506. return nil
  507. }
  508. // Skip symlinks to directories
  509. if targetInfo.IsDir() {
  510. logger.Debugf("Skipping symlink to directory: %s", filePath)
  511. return nil
  512. }
  513. // Process symlinks to files, but use the target's info for size check
  514. fileInfo = targetInfo
  515. // logger.Debugf("Processing symlink to file: %s", filePath)
  516. }
  517. // Skip non-regular files (devices, pipes, sockets, etc.)
  518. if !fileInfo.Mode().IsRegular() {
  519. logger.Debugf("Skipping non-regular file: %s (mode: %s)", filePath, fileInfo.Mode())
  520. return nil
  521. }
  522. // Skip files larger than max size before reading
  523. if fileInfo.Size() > scanConfig.MaxFileSize {
  524. logger.Debugf("Skipping large file: %s (size: %d bytes)", filePath, fileInfo.Size())
  525. return nil
  526. }
  527. // Read file content
  528. content, err := os.ReadFile(filePath)
  529. if err != nil {
  530. logger.Errorf("os.ReadFile failed for %s: %v", filePath, err)
  531. return err
  532. }
  533. // Execute callbacks
  534. s.executeCallbacks(filePath, content, skipPostScan)
  535. return nil
  536. }
  537. // setScanningState updates the scanning state and publishes events
  538. func (s *Scanner) setScanningState(scanning bool) {
  539. s.scanMutex.Lock()
  540. defer s.scanMutex.Unlock()
  541. if s.scanning != scanning {
  542. s.scanning = scanning
  543. event.Publish(event.Event{
  544. Type: event.TypeIndexScanning,
  545. Data: scanning,
  546. })
  547. }
  548. }
  549. // executeCallbacks runs all registered callbacks
  550. func (s *Scanner) executeCallbacks(filePath string, content []byte, skipPostScan bool) {
  551. scanCallbacksMutex.RLock()
  552. callbacksCopy := make([]CallbackInfo, len(scanCallbacks))
  553. copy(callbacksCopy, scanCallbacks)
  554. scanCallbacksMutex.RUnlock()
  555. for i, callbackInfo := range callbacksCopy {
  556. // Add timeout protection for each callback
  557. done := make(chan error, 1)
  558. go func() {
  559. done <- callbackInfo.Callback(filePath, content)
  560. }()
  561. select {
  562. case err := <-done:
  563. if err != nil {
  564. logger.Errorf("Callback error for %s in '%s': %v", filePath, callbackInfo.Name, err)
  565. }
  566. case <-time.After(scanConfig.CallbackTimeout):
  567. logger.Errorf("Callback [%d/%d] '%s' timed out after %v for: %s", i+1, len(callbacksCopy), callbackInfo.Name, scanConfig.CallbackTimeout, filePath)
  568. // Continue with next callback instead of blocking forever
  569. }
  570. }
  571. // Execute post-scan callbacks only if not skipped (used for batch scans)
  572. if !skipPostScan {
  573. s.executePostScanCallbacks()
  574. }
  575. }
  576. // executePostScanCallbacks runs all registered post-scan callbacks
  577. func (s *Scanner) executePostScanCallbacks() {
  578. postScanCallbacksMutex.RLock()
  579. postCallbacksCopy := make([]PostScanCallback, len(postScanCallbacks))
  580. copy(postCallbacksCopy, postScanCallbacks)
  581. postScanCallbacksMutex.RUnlock()
  582. for i, callback := range postCallbacksCopy {
  583. name := fmt.Sprintf("Post-scan callback [%d/%d]", i+1, len(postCallbacksCopy))
  584. if err := runWithTimeout(callback, scanConfig.PostCallbackTimeout, name); err != nil {
  585. logger.Errorf("%s error: %v", name, err)
  586. }
  587. }
  588. }
  589. // ScanAllConfigs scans all nginx configuration files
  590. func (s *Scanner) ScanAllConfigs() error {
  591. s.setScanningState(true)
  592. defer s.setScanningState(false)
  593. root := nginx.GetConfPath()
  594. fileCount := 0
  595. dirCount := 0
  596. // Use the unified recursive scan logic with no timeout
  597. err := s.scanDirectoryRecursive(context.Background(), root, &fileCount, &dirCount)
  598. logger.Debugf("Scan completed: %d directories, %d files processed", dirCount, fileCount)
  599. // Trigger post-scan callbacks once after all files are scanned
  600. if err == nil {
  601. s.executePostScanCallbacks()
  602. }
  603. return err
  604. }
  605. // scanDirectoryRecursive implements custom recursive directory traversal
  606. // to avoid filepath.WalkDir blocking issues on restart
  607. func (s *Scanner) scanDirectoryRecursive(ctx context.Context, root string, fileCount, dirCount *int) error {
  608. visited := make(map[string]bool)
  609. return s.scanDirectoryRecursiveInternal(ctx, root, fileCount, dirCount, visited)
  610. }
  611. // scanDirectoryRecursiveInternal is the internal implementation with symlink loop detection
  612. func (s *Scanner) scanDirectoryRecursiveInternal(ctx context.Context, root string, fileCount, dirCount *int, visited map[string]bool) error {
  613. // Check for context cancellation
  614. select {
  615. case <-ctx.Done():
  616. return ctx.Err()
  617. default:
  618. }
  619. // Resolve symlinks and check for loops
  620. realPath, err := filepath.EvalSymlinks(root)
  621. if err != nil {
  622. // If we can't resolve, use original path
  623. realPath = root
  624. }
  625. // Check if already visited (prevents symlink loops)
  626. if visited[realPath] {
  627. logger.Debugf("Skipping already visited path (symlink loop): %s -> %s", root, realPath)
  628. return nil
  629. }
  630. visited[realPath] = true
  631. // Read directory entries
  632. entries, err := os.ReadDir(root)
  633. if err != nil {
  634. logger.Errorf("Failed to read directory %s: %v", root, err)
  635. return err
  636. }
  637. // Process each entry
  638. for i, entry := range entries {
  639. // Check context cancellation periodically
  640. if i%10 == 0 {
  641. select {
  642. case <-ctx.Done():
  643. logger.Warnf("Scan cancelled while processing entries in: %s", root)
  644. return ctx.Err()
  645. default:
  646. }
  647. }
  648. fullPath := filepath.Join(root, entry.Name())
  649. entryType := entry.Type()
  650. isDir := entry.IsDir()
  651. if isDir {
  652. (*dirCount)++
  653. // Skip excluded directories
  654. if shouldSkipPath(fullPath) {
  655. logger.Debugf("Skipping excluded directory: %s", fullPath)
  656. continue
  657. }
  658. // Recursively scan subdirectory - continue on error to scan other directories
  659. if err := s.scanDirectoryRecursiveInternal(ctx, fullPath, fileCount, dirCount, visited); err != nil {
  660. logger.Errorf("Failed to scan subdirectory %s: %v", fullPath, err)
  661. // Continue with other directories instead of failing completely
  662. }
  663. } else {
  664. (*fileCount)++
  665. // Handle symlinks
  666. if entryType&os.ModeSymlink != 0 {
  667. targetInfo, err := os.Stat(fullPath)
  668. if err == nil {
  669. if targetInfo.IsDir() {
  670. // Recursively scan symlink directory (with loop detection)
  671. if err := s.scanDirectoryRecursiveInternal(ctx, fullPath, fileCount, dirCount, visited); err != nil {
  672. logger.Errorf("Failed to scan symlink directory %s: %v", fullPath, err)
  673. }
  674. continue
  675. }
  676. } else {
  677. logger.Warnf("os.Stat failed for symlink %s: %v", fullPath, err)
  678. }
  679. }
  680. // Process regular files - skip post-scan during batch scan
  681. if err := s.scanSingleFileInternal(fullPath, true); err != nil {
  682. logger.Errorf("Failed to scan file %s: %v", fullPath, err)
  683. }
  684. }
  685. }
  686. return nil
  687. }
  688. // Shutdown cleans up scanner resources
  689. func (s *Scanner) Shutdown() {
  690. logger.Info("Starting scanner shutdown...")
  691. // Cancel context to signal all goroutines to stop
  692. if s.cancel != nil {
  693. s.cancel()
  694. }
  695. // Stop debouncer to prevent new scans
  696. if s.debouncer != nil {
  697. s.debouncer.stop()
  698. }
  699. // Close watcher first to stop file events
  700. if s.watcher != nil {
  701. s.watcher.Close()
  702. s.watcher = nil
  703. }
  704. // Stop ticker
  705. if s.scanTicker != nil {
  706. s.scanTicker.Stop()
  707. s.scanTicker = nil
  708. }
  709. // Wait for all goroutines to finish with timeout
  710. done := make(chan struct{})
  711. go func() {
  712. s.wg.Wait()
  713. close(done)
  714. }()
  715. select {
  716. case <-done:
  717. logger.Info("All scanner goroutines completed successfully")
  718. case <-time.After(scanConfig.ShutdownTimeout):
  719. logger.Warn("Timeout waiting for scanner goroutines to complete")
  720. }
  721. // Clear the global scanner instance to force recreation on next use
  722. scannerInitMutex.Lock()
  723. scanner = nil
  724. // Reset initialization state for next restart
  725. scannerInitMutex.Unlock()
  726. logger.Info("Scanner shutdown completed and global instance cleared for recreation")
  727. }
  728. // IsScanningInProgress returns whether a scan is currently running
  729. func IsScanningInProgress() bool {
  730. s := GetScanner()
  731. s.scanMutex.RLock()
  732. defer s.scanMutex.RUnlock()
  733. return s.scanning
  734. }
  735. // ForceReleaseResources performs aggressive cleanup of all file system resources
  736. func ForceReleaseResources() {
  737. scannerInitMutex.Lock()
  738. defer scannerInitMutex.Unlock()
  739. logger.Info("Force releasing all scanner resources...")
  740. if scanner != nil {
  741. // Cancel context first to signal all goroutines
  742. if scanner.cancel != nil {
  743. logger.Info("Cancelling scanner context to stop all operations")
  744. scanner.cancel()
  745. }
  746. // Wait a brief moment for operations to respond to cancellation
  747. time.Sleep(200 * time.Millisecond)
  748. // Force close file system watcher - this should release all locks
  749. if scanner.watcher != nil {
  750. logger.Info("Forcefully closing file system watcher and releasing all file locks")
  751. if err := scanner.watcher.Close(); err != nil {
  752. logger.Errorf("Error force-closing watcher: %v", err)
  753. } else {
  754. logger.Info("File system watcher force-closed, locks should be released")
  755. }
  756. scanner.watcher = nil
  757. }
  758. // Stop ticker
  759. if scanner.scanTicker != nil {
  760. logger.Info("Stopping scan ticker")
  761. scanner.scanTicker.Stop()
  762. scanner.scanTicker = nil
  763. }
  764. // Wait for goroutines to complete with short timeout
  765. done := make(chan struct{})
  766. go func() {
  767. scanner.wg.Wait()
  768. close(done)
  769. }()
  770. select {
  771. case <-done:
  772. logger.Info("All scanner goroutines terminated successfully")
  773. case <-time.After(scanConfig.ForceCleanupTimeout):
  774. logger.Warn("Timeout waiting for scanner goroutines - proceeding with force cleanup")
  775. }
  776. scanner = nil
  777. }
  778. }
  779. // WaitForInitialScanComplete waits for the initial config scan and all callbacks to complete
  780. // This is useful for services that depend on site indexing to be ready
  781. func WaitForInitialScanComplete() {
  782. // Get channel reference with lock to avoid race
  783. initialScanCompleteMu.Lock()
  784. ch := initialScanComplete
  785. initialScanCompleteMu.Unlock()
  786. if ch == nil {
  787. logger.Debug("Initial scan completion channel not initialized, returning immediately")
  788. return
  789. }
  790. logger.Debug("Waiting for initial config scan to complete...")
  791. // Add timeout to prevent infinite waiting
  792. select {
  793. case <-ch:
  794. logger.Debug("Initial config scan completion confirmed")
  795. case <-time.After(scanConfig.InitialScanWaitTimeout):
  796. logger.Warn("Timeout waiting for initial config scan completion - proceeding anyway")
  797. }
  798. }