index.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814
  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. // Scanner watches and scans nginx config files
  24. type Scanner struct {
  25. ctx context.Context
  26. cancel context.CancelFunc
  27. watcher *fsnotify.Watcher
  28. scanTicker *time.Ticker
  29. scanning bool
  30. scanMutex sync.RWMutex
  31. wg sync.WaitGroup // Track running goroutines
  32. }
  33. var (
  34. scanner *Scanner
  35. scannerInitMutex sync.Mutex
  36. scanCallbacks = make([]CallbackInfo, 0)
  37. scanCallbacksMutex sync.RWMutex
  38. // Channel to signal when initial scan and all callbacks are completed
  39. initialScanComplete chan struct{}
  40. initialScanOnce sync.Once
  41. )
  42. // InitScanner initializes the config scanner
  43. func InitScanner(ctx context.Context) {
  44. if nginx.GetConfPath() == "" {
  45. logger.Error("Nginx config path is not set")
  46. return
  47. }
  48. // Force release any existing resources before initialization
  49. ForceReleaseResources()
  50. scanner := GetScanner()
  51. if err := scanner.Initialize(ctx); err != nil {
  52. logger.Error("Failed to initialize config scanner:", err)
  53. // On failure, force cleanup
  54. ForceReleaseResources()
  55. }
  56. }
  57. // shouldSkipPath checks if a path should be skipped during scanning or watching
  58. func shouldSkipPath(path string) bool {
  59. // Define directories to exclude from scanning/watching
  60. excludedDirs := []string{
  61. nginx.GetConfPath("ssl"), // SSL certificates and keys
  62. nginx.GetConfPath("cache"), // Nginx cache files
  63. nginx.GetConfPath("logs"), // Log files directory
  64. nginx.GetConfPath("temp"), // Temporary files directory
  65. nginx.GetConfPath("proxy_temp"), // Proxy temporary files
  66. nginx.GetConfPath("client_body_temp"), // Client body temporary files
  67. nginx.GetConfPath("fastcgi_temp"), // FastCGI temporary files
  68. nginx.GetConfPath("uwsgi_temp"), // uWSGI temporary files
  69. nginx.GetConfPath("scgi_temp"), // SCGI temporary files
  70. }
  71. // Check if path starts with any excluded directory
  72. for _, excludedDir := range excludedDirs {
  73. if excludedDir != "" && strings.HasPrefix(path, excludedDir) {
  74. return true
  75. }
  76. }
  77. return false
  78. }
  79. // GetScanner returns the singleton scanner instance
  80. func GetScanner() *Scanner {
  81. scannerInitMutex.Lock()
  82. defer scannerInitMutex.Unlock()
  83. if scanner == nil {
  84. scanner = &Scanner{}
  85. }
  86. return scanner
  87. }
  88. // RegisterCallback adds a named callback to be executed during scans
  89. func RegisterCallback(name string, callback ScanCallback) {
  90. scanCallbacksMutex.Lock()
  91. defer scanCallbacksMutex.Unlock()
  92. scanCallbacks = append(scanCallbacks, CallbackInfo{
  93. Name: name,
  94. Callback: callback,
  95. })
  96. }
  97. // Initialize sets up the scanner and starts watching
  98. func (s *Scanner) Initialize(ctx context.Context) error {
  99. // Initialize the completion channel for this scan cycle
  100. initialScanComplete = make(chan struct{})
  101. initialScanOnce = sync.Once{} // Reset for this initialization
  102. // Create cancellable context for this scanner instance
  103. s.ctx, s.cancel = context.WithCancel(ctx)
  104. watcher, err := fsnotify.NewWatcher()
  105. if err != nil {
  106. return err
  107. }
  108. s.watcher = watcher
  109. // Watch all directories recursively first (this is faster than scanning)
  110. if err := s.watchAllDirectories(); err != nil {
  111. return err
  112. }
  113. // Start background processes with WaitGroup tracking
  114. s.wg.Go(func() {
  115. logger.Debug("Started cache watchForChanges goroutine")
  116. s.watchForChanges()
  117. logger.Info("Cache watchForChanges goroutine completed")
  118. })
  119. s.wg.Go(func() {
  120. logger.Debug("Started cache periodicScan goroutine")
  121. s.periodicScan()
  122. logger.Info("Cache periodicScan goroutine completed")
  123. })
  124. s.wg.Go(func() {
  125. logger.Debug("Started cache handleShutdown goroutine")
  126. s.handleShutdown()
  127. logger.Info("Cache handleShutdown goroutine completed")
  128. })
  129. // Perform initial scan asynchronously to avoid blocking boot process
  130. // Pass the context to ensure proper cancellation
  131. s.wg.Go(func() {
  132. logger.Debug("Started cache initialScanAsync goroutine")
  133. s.initialScanAsync(ctx)
  134. logger.Debug("Cache initialScanAsync goroutine completed")
  135. })
  136. return nil
  137. }
  138. // watchAllDirectories recursively adds all directories under nginx config path to watcher
  139. func (s *Scanner) watchAllDirectories() error {
  140. root := nginx.GetConfPath()
  141. return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
  142. if err != nil {
  143. return err
  144. }
  145. if d.IsDir() {
  146. // Skip excluded directories (ssl, cache, logs, temp, etc.)
  147. if shouldSkipPath(path) {
  148. return filepath.SkipDir
  149. }
  150. // Resolve symlinks to get the actual directory path to watch
  151. actualPath := path
  152. if d.Type()&os.ModeSymlink != 0 {
  153. // This is a symlink, resolve it to get the target path
  154. if resolvedPath, err := filepath.EvalSymlinks(path); err == nil {
  155. actualPath = resolvedPath
  156. logger.Debug("Resolved symlink for watching:", path, "->", actualPath)
  157. } else {
  158. logger.Debug("Failed to resolve symlink, skipping:", path, err)
  159. return filepath.SkipDir
  160. }
  161. }
  162. if err := s.watcher.Add(actualPath); err != nil {
  163. logger.Error("Failed to watch directory:", actualPath, err)
  164. return err
  165. }
  166. }
  167. return nil
  168. })
  169. }
  170. // periodicScan runs periodic scans every 5 minutes
  171. func (s *Scanner) periodicScan() {
  172. s.scanTicker = time.NewTicker(5 * time.Minute)
  173. defer s.scanTicker.Stop()
  174. for {
  175. select {
  176. case <-s.ctx.Done():
  177. logger.Debug("periodicScan: context cancelled, exiting")
  178. return
  179. case <-s.scanTicker.C:
  180. if err := s.ScanAllConfigs(); err != nil {
  181. logger.Error("Periodic scan failed:", err)
  182. }
  183. }
  184. }
  185. }
  186. // handleShutdown listens for context cancellation and shuts down gracefully
  187. func (s *Scanner) handleShutdown() {
  188. <-s.ctx.Done()
  189. logger.Debug("Shutting down Index Scanner")
  190. // Note: Don't call s.Shutdown() here as it would cause deadlock
  191. // Shutdown is called externally, this just handles cleanup
  192. }
  193. // initialScanAsync performs the initial config scan asynchronously
  194. func (s *Scanner) initialScanAsync(ctx context.Context) {
  195. // Always use the provided context, not the scanner's internal context
  196. // This ensures we use the fresh boot context, not a potentially cancelled old context
  197. logger.Debugf("Initial scan starting with context: cancelled=%v", ctx.Err() != nil)
  198. // Check if context is already cancelled before starting
  199. select {
  200. case <-ctx.Done():
  201. logger.Warn("Initial scan cancelled before starting - context already done")
  202. // Signal completion even when cancelled early so waiting services don't hang
  203. initialScanOnce.Do(func() {
  204. logger.Warn("Initial config scan cancelled early - signaling completion")
  205. close(initialScanComplete)
  206. })
  207. return
  208. default:
  209. }
  210. logger.Debug("Starting initial config scan...")
  211. logger.Debugf("Config path: %s", nginx.GetConfPath())
  212. // Perform the scan with the fresh context (not scanner's internal context)
  213. if err := s.scanAllConfigsWithContext(ctx); err != nil {
  214. // Only log error if it's not due to context cancellation
  215. if ctx.Err() == nil {
  216. logger.Errorf("Initial config scan failed: %v", err)
  217. } else {
  218. logger.Debugf("Initial config scan cancelled due to context: %v", ctx.Err())
  219. }
  220. // Signal completion even on error so waiting services don't hang
  221. initialScanOnce.Do(func() {
  222. logger.Warn("Initial config scan completed with error - signaling completion anyway")
  223. close(initialScanComplete)
  224. })
  225. } else {
  226. // Signal that initial scan is complete - this allows other services to proceed
  227. // that depend on the scan callbacks to have been processed
  228. initialScanOnce.Do(func() {
  229. logger.Debug("Initial config scan and callbacks completed - signaling completion")
  230. close(initialScanComplete)
  231. })
  232. }
  233. }
  234. // scanAllConfigsWithContext scans all nginx configuration files with context support
  235. func (s *Scanner) scanAllConfigsWithContext(ctx context.Context) error {
  236. s.setScanningState(true)
  237. defer s.setScanningState(false)
  238. root := nginx.GetConfPath()
  239. logger.Debugf("Scanning config directory: %s", root)
  240. // Create a timeout context for the scan operation
  241. scanCtx, scanCancel := context.WithTimeout(ctx, 15*time.Second)
  242. defer scanCancel()
  243. // Scan all files in the config directory and subdirectories
  244. logger.Debug("Starting filepath.WalkDir scanning...")
  245. // Use a channel to communicate scan results
  246. type scanResult struct {
  247. err error
  248. fileCount int
  249. dirCount int
  250. }
  251. resultChan := make(chan scanResult, 1)
  252. // Run custom directory traversal in a goroutine to avoid WalkDir blocking issues
  253. go func() {
  254. fileCount := 0
  255. dirCount := 0
  256. // Use custom recursive traversal instead of filepath.WalkDir
  257. walkErr := s.scanDirectoryRecursive(scanCtx, root, &fileCount, &dirCount)
  258. // Send result through channel
  259. resultChan <- scanResult{
  260. err: walkErr,
  261. fileCount: fileCount,
  262. dirCount: dirCount,
  263. }
  264. }()
  265. // Wait for scan to complete or timeout
  266. select {
  267. case result := <-resultChan:
  268. logger.Debugf("Scan completed successfully: dirs=%d, files=%d, error=%v",
  269. result.dirCount, result.fileCount, result.err)
  270. return result.err
  271. case <-scanCtx.Done():
  272. logger.Warnf("Scan timed out after 25 seconds - cancelling")
  273. scanCancel()
  274. // Wait a bit more for cleanup
  275. select {
  276. case result := <-resultChan:
  277. logger.Debugf("Scan completed after timeout: dirs=%d, files=%d, error=%v",
  278. result.dirCount, result.fileCount, result.err)
  279. return result.err
  280. case <-time.After(2 * time.Second):
  281. logger.Warn("Scan failed to complete even after timeout - forcing return")
  282. return ctx.Err()
  283. }
  284. }
  285. }
  286. // watchForChanges handles file system events
  287. func (s *Scanner) watchForChanges() {
  288. for {
  289. select {
  290. case <-s.ctx.Done():
  291. logger.Debug("watchForChanges: context cancelled, exiting")
  292. return
  293. case event, ok := <-s.watcher.Events:
  294. if !ok {
  295. logger.Debug("watchForChanges: events channel closed, exiting")
  296. return
  297. }
  298. s.handleFileEvent(event)
  299. case err, ok := <-s.watcher.Errors:
  300. if !ok {
  301. logger.Debug("watchForChanges: errors channel closed, exiting")
  302. return
  303. }
  304. logger.Error("Watcher error:", err)
  305. }
  306. }
  307. }
  308. // handleFileEvent processes individual file system events
  309. func (s *Scanner) handleFileEvent(event fsnotify.Event) {
  310. // Only handle relevant events
  311. if !event.Has(fsnotify.Create) && !event.Has(fsnotify.Write) &&
  312. !event.Has(fsnotify.Rename) && !event.Has(fsnotify.Remove) {
  313. return
  314. }
  315. // Skip excluded directories (ssl, cache, etc.)
  316. if shouldSkipPath(event.Name) {
  317. return
  318. }
  319. // Add new directories to watch
  320. if event.Has(fsnotify.Create) {
  321. if fi, err := os.Stat(event.Name); err == nil && fi.IsDir() {
  322. if err := s.watcher.Add(event.Name); err != nil {
  323. logger.Error("Failed to add new directory to watcher:", event.Name, err)
  324. } else {
  325. logger.Debug("Added new directory to watcher:", event.Name)
  326. }
  327. }
  328. }
  329. // Handle file changes
  330. if event.Has(fsnotify.Remove) {
  331. logger.Debug("Config removed:", event.Name)
  332. return
  333. }
  334. // Use Lstat to get symlink info without following it
  335. fi, err := os.Lstat(event.Name)
  336. if err != nil {
  337. return
  338. }
  339. // If it's a symlink, we need to check what it points to
  340. var targetIsDir bool
  341. if fi.Mode()&os.ModeSymlink != 0 {
  342. // For symlinks, check the target
  343. targetFi, err := os.Stat(event.Name)
  344. if err != nil {
  345. logger.Debug("Symlink target not accessible:", event.Name, err)
  346. return
  347. }
  348. targetIsDir = targetFi.IsDir()
  349. logger.Debug("Symlink changed:", event.Name, "-> target is dir:", targetIsDir)
  350. } else {
  351. targetIsDir = fi.IsDir()
  352. }
  353. if targetIsDir {
  354. logger.Debug("Directory changed:", event.Name)
  355. } else {
  356. logger.Debug("File changed:", event.Name)
  357. time.Sleep(100 * time.Millisecond) // Allow file write to complete
  358. s.scanSingleFile(event.Name)
  359. }
  360. }
  361. // scanSingleFile scans a single config file without recursion
  362. func (s *Scanner) scanSingleFile(filePath string) error {
  363. s.setScanningState(true)
  364. defer s.setScanningState(false)
  365. // Check if path should be skipped
  366. if shouldSkipPath(filePath) {
  367. logger.Debugf("File skipped by shouldSkipPath: %s", filePath)
  368. return nil
  369. }
  370. // Get file info to check type and size
  371. fileInfo, err := os.Lstat(filePath) // Use Lstat to avoid following symlinks
  372. if err != nil {
  373. return err
  374. }
  375. // Skip directories
  376. if fileInfo.IsDir() {
  377. logger.Debugf("Skipping directory: %s", filePath)
  378. return nil
  379. }
  380. // Handle symlinks carefully
  381. if fileInfo.Mode()&os.ModeSymlink != 0 {
  382. // Check what the symlink points to
  383. targetInfo, err := os.Stat(filePath)
  384. if err != nil {
  385. logger.Debugf("Skipping symlink with inaccessible target: %s (%v)", filePath, err)
  386. return nil
  387. }
  388. // Skip symlinks to directories
  389. if targetInfo.IsDir() {
  390. logger.Debugf("Skipping symlink to directory: %s", filePath)
  391. return nil
  392. }
  393. // Process symlinks to files, but use the target's info for size check
  394. fileInfo = targetInfo
  395. // logger.Debugf("Processing symlink to file: %s", filePath)
  396. }
  397. // Skip non-regular files (devices, pipes, sockets, etc.)
  398. if !fileInfo.Mode().IsRegular() {
  399. logger.Debugf("Skipping non-regular file: %s (mode: %s)", filePath, fileInfo.Mode())
  400. return nil
  401. }
  402. // Skip files larger than 1MB before reading
  403. if fileInfo.Size() > 1024*1024 {
  404. logger.Debugf("Skipping large file: %s (size: %d bytes)", filePath, fileInfo.Size())
  405. return nil
  406. }
  407. // Read file content
  408. content, err := os.ReadFile(filePath)
  409. if err != nil {
  410. logger.Errorf("os.ReadFile failed for %s: %v", filePath, err)
  411. return err
  412. }
  413. // Execute callbacks
  414. s.executeCallbacks(filePath, content)
  415. return nil
  416. }
  417. // setScanningState updates the scanning state and publishes events
  418. func (s *Scanner) setScanningState(scanning bool) {
  419. s.scanMutex.Lock()
  420. defer s.scanMutex.Unlock()
  421. if s.scanning != scanning {
  422. s.scanning = scanning
  423. event.Publish(event.Event{
  424. Type: event.TypeIndexScanning,
  425. Data: scanning,
  426. })
  427. }
  428. }
  429. // executeCallbacks runs all registered callbacks
  430. func (s *Scanner) executeCallbacks(filePath string, content []byte) {
  431. scanCallbacksMutex.RLock()
  432. defer scanCallbacksMutex.RUnlock()
  433. for i, callbackInfo := range scanCallbacks {
  434. // Add timeout protection for each callback
  435. done := make(chan error, 1)
  436. go func() {
  437. done <- callbackInfo.Callback(filePath, content)
  438. }()
  439. select {
  440. case err := <-done:
  441. if err != nil {
  442. logger.Errorf("Callback error for %s in '%s': %v", filePath, callbackInfo.Name, err)
  443. }
  444. case <-time.After(5 * time.Second):
  445. logger.Errorf("Callback [%d/%d] '%s' timed out after 5 seconds for: %s", i+1, len(scanCallbacks), callbackInfo.Name, filePath)
  446. // Continue with next callback instead of blocking forever
  447. }
  448. }
  449. }
  450. // ScanAllConfigs scans all nginx configuration files
  451. func (s *Scanner) ScanAllConfigs() error {
  452. s.setScanningState(true)
  453. defer s.setScanningState(false)
  454. root := nginx.GetConfPath()
  455. // Scan all files in the config directory and subdirectories
  456. return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
  457. if err != nil {
  458. return err
  459. }
  460. // Skip excluded directories (ssl, cache, logs, temp, etc.)
  461. if d.IsDir() && shouldSkipPath(path) {
  462. return filepath.SkipDir
  463. }
  464. // Handle symlinks to directories specially
  465. if d.Type()&os.ModeSymlink != 0 {
  466. if targetInfo, err := os.Stat(path); err == nil && targetInfo.IsDir() {
  467. // This is a symlink to a directory, we should traverse its contents
  468. // but not process the symlink itself as a file
  469. logger.Debug("Found symlink to directory, will traverse contents:", path)
  470. // Manually scan the symlink target directory since WalkDir doesn't follow symlinks
  471. if err := s.scanSymlinkDirectory(path); err != nil {
  472. logger.Error("Failed to scan symlink directory:", path, err)
  473. }
  474. return nil
  475. }
  476. }
  477. // Only process regular files (not directories, not symlinks to directories)
  478. if !d.IsDir() {
  479. if err := s.scanSingleFile(path); err != nil {
  480. logger.Error("Failed to scan config:", path, err)
  481. }
  482. }
  483. return nil
  484. })
  485. }
  486. // scanDirectoryRecursive implements custom recursive directory traversal
  487. // to avoid filepath.WalkDir blocking issues on restart
  488. func (s *Scanner) scanDirectoryRecursive(ctx context.Context, root string, fileCount, dirCount *int) error {
  489. // Check for context cancellation
  490. select {
  491. case <-ctx.Done():
  492. return ctx.Err()
  493. default:
  494. }
  495. // Read directory entries
  496. entries, err := os.ReadDir(root)
  497. if err != nil {
  498. logger.Errorf("Failed to read directory %s: %v", root, err)
  499. return err
  500. }
  501. // Process each entry
  502. for i, entry := range entries {
  503. // Check context cancellation periodically
  504. if i%10 == 0 {
  505. select {
  506. case <-ctx.Done():
  507. logger.Warnf("Scan cancelled while processing entries in: %s", root)
  508. return ctx.Err()
  509. default:
  510. }
  511. }
  512. fullPath := filepath.Join(root, entry.Name())
  513. entryType := entry.Type()
  514. isDir := entry.IsDir()
  515. if isDir {
  516. (*dirCount)++
  517. // Skip excluded directories
  518. if shouldSkipPath(fullPath) {
  519. logger.Debugf("Skipping excluded directory: %s", fullPath)
  520. continue
  521. }
  522. // Recursively scan subdirectory
  523. if err := s.scanDirectoryRecursive(ctx, fullPath, fileCount, dirCount); err != nil {
  524. logger.Errorf("Failed to scan subdirectory %s: %v", fullPath, err)
  525. return err
  526. }
  527. } else {
  528. (*fileCount)++
  529. // Handle symlinks
  530. if entryType&os.ModeSymlink != 0 {
  531. targetInfo, err := os.Stat(fullPath)
  532. if err == nil {
  533. if targetInfo.IsDir() {
  534. // Recursively scan symlink directory
  535. if err := s.scanDirectoryRecursive(ctx, fullPath, fileCount, dirCount); err != nil {
  536. logger.Errorf("Failed to scan symlink directory %s: %v", fullPath, err)
  537. // Continue with other entries instead of failing completely
  538. }
  539. continue
  540. }
  541. } else {
  542. logger.Warnf("os.Stat failed for symlink %s: %v", fullPath, err)
  543. }
  544. }
  545. // Process regular files
  546. if err := s.scanSingleFile(fullPath); err != nil {
  547. logger.Errorf("Failed to scan file %s: %v", fullPath, err)
  548. // Continue with other files instead of failing completely
  549. }
  550. }
  551. }
  552. return nil
  553. }
  554. // scanSymlinkDirectory recursively scans a symlink directory and its contents
  555. func (s *Scanner) scanSymlinkDirectory(symlinkPath string) error {
  556. logger.Debugf("scanSymlinkDirectory START: %s", symlinkPath)
  557. // Resolve the symlink to get the actual target path
  558. targetPath, err := filepath.EvalSymlinks(symlinkPath)
  559. if err != nil {
  560. logger.Errorf("Failed to resolve symlink %s: %v", symlinkPath, err)
  561. return fmt.Errorf("failed to resolve symlink %s: %w", symlinkPath, err)
  562. }
  563. logger.Debug("Scanning symlink directory contents:", symlinkPath, "->", targetPath)
  564. // Use WalkDir on the resolved target path
  565. walkErr := filepath.WalkDir(targetPath, func(path string, d fs.DirEntry, err error) error {
  566. logger.Debugf("scanSymlinkDirectory callback: %s (type: %s)", path, d.Type().String())
  567. if err != nil {
  568. return err
  569. }
  570. // Skip excluded directories
  571. if d.IsDir() && shouldSkipPath(path) {
  572. return filepath.SkipDir
  573. }
  574. // Only process regular files (not directories, not symlinks to directories)
  575. if !d.IsDir() {
  576. // Handle symlinks to directories (skip them)
  577. if d.Type()&os.ModeSymlink != 0 {
  578. if targetInfo, err := os.Stat(path); err == nil && targetInfo.IsDir() {
  579. logger.Debug("Skipping symlink to directory in symlink scan:", path)
  580. return nil
  581. }
  582. }
  583. if err := s.scanSingleFile(path); err != nil {
  584. logger.Error("Failed to scan config in symlink directory:", path, err)
  585. }
  586. }
  587. logger.Debugf("scanSymlinkDirectory callback exit: %s", path)
  588. return nil
  589. })
  590. logger.Debugf("scanSymlinkDirectory END: %s -> %s (error: %v)", symlinkPath, targetPath, walkErr)
  591. return walkErr
  592. }
  593. // Shutdown cleans up scanner resources
  594. func (s *Scanner) Shutdown() {
  595. logger.Info("Starting scanner shutdown...")
  596. // Cancel context to signal all goroutines to stop
  597. if s.cancel != nil {
  598. s.cancel()
  599. }
  600. // Close watcher first to stop file events
  601. if s.watcher != nil {
  602. s.watcher.Close()
  603. s.watcher = nil
  604. }
  605. // Stop ticker
  606. if s.scanTicker != nil {
  607. s.scanTicker.Stop()
  608. s.scanTicker = nil
  609. }
  610. // Wait for all goroutines to finish with timeout
  611. done := make(chan struct{})
  612. go func() {
  613. s.wg.Wait()
  614. close(done)
  615. }()
  616. select {
  617. case <-done:
  618. logger.Info("All scanner goroutines completed successfully")
  619. case <-time.After(10 * time.Second):
  620. logger.Warn("Timeout waiting for scanner goroutines to complete")
  621. }
  622. // Clear the global scanner instance to force recreation on next use
  623. scannerInitMutex.Lock()
  624. scanner = nil
  625. // Reset initialization state for next restart
  626. scannerInitMutex.Unlock()
  627. logger.Info("Scanner shutdown completed and global instance cleared for recreation")
  628. }
  629. // IsScanningInProgress returns whether a scan is currently running
  630. func IsScanningInProgress() bool {
  631. s := GetScanner()
  632. s.scanMutex.RLock()
  633. defer s.scanMutex.RUnlock()
  634. return s.scanning
  635. }
  636. // ForceReleaseResources performs aggressive cleanup of all file system resources
  637. func ForceReleaseResources() {
  638. scannerInitMutex.Lock()
  639. defer scannerInitMutex.Unlock()
  640. logger.Info("Force releasing all scanner resources...")
  641. if scanner != nil {
  642. // Cancel context first to signal all goroutines
  643. if scanner.cancel != nil {
  644. logger.Info("Cancelling scanner context to stop all operations")
  645. scanner.cancel()
  646. }
  647. // Wait a brief moment for operations to respond to cancellation
  648. time.Sleep(200 * time.Millisecond)
  649. // Force close file system watcher - this should release all locks
  650. if scanner.watcher != nil {
  651. logger.Info("Forcefully closing file system watcher and releasing all file locks")
  652. if err := scanner.watcher.Close(); err != nil {
  653. logger.Errorf("Error force-closing watcher: %v", err)
  654. } else {
  655. logger.Info("File system watcher force-closed, locks should be released")
  656. }
  657. scanner.watcher = nil
  658. }
  659. // Stop ticker
  660. if scanner.scanTicker != nil {
  661. logger.Info("Stopping scan ticker")
  662. scanner.scanTicker.Stop()
  663. scanner.scanTicker = nil
  664. }
  665. // Wait for goroutines to complete with short timeout
  666. done := make(chan struct{})
  667. go func() {
  668. scanner.wg.Wait()
  669. close(done)
  670. }()
  671. select {
  672. case <-done:
  673. logger.Info("All scanner goroutines terminated successfully")
  674. case <-time.After(3 * time.Second):
  675. logger.Warn("Timeout waiting for scanner goroutines - proceeding with force cleanup")
  676. }
  677. scanner = nil
  678. }
  679. }
  680. // WaitForInitialScanComplete waits for the initial config scan and all callbacks to complete
  681. // This is useful for services that depend on site indexing to be ready
  682. func WaitForInitialScanComplete() {
  683. if initialScanComplete == nil {
  684. logger.Debug("Initial scan completion channel not initialized, returning immediately")
  685. return
  686. }
  687. logger.Debug("Waiting for initial config scan to complete...")
  688. // Add timeout to prevent infinite waiting
  689. select {
  690. case <-initialScanComplete:
  691. logger.Debug("Initial config scan completion confirmed")
  692. case <-time.After(30 * time.Second):
  693. logger.Warn("Timeout waiting for initial config scan completion - proceeding anyway")
  694. }
  695. }