index.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. package cache
  2. import (
  3. "os"
  4. "path/filepath"
  5. "regexp"
  6. "strings"
  7. "sync"
  8. "time"
  9. "github.com/0xJacky/Nginx-UI/internal/nginx"
  10. "github.com/fsnotify/fsnotify"
  11. "github.com/uozi-tech/cosy/logger"
  12. )
  13. // ScanCallback is a function that gets called during config scanning
  14. // It receives the config file path and contents
  15. type ScanCallback func(configPath string, content []byte) error
  16. // Scanner is responsible for scanning and watching nginx config files
  17. type Scanner struct {
  18. watcher *fsnotify.Watcher // File system watcher
  19. scanTicker *time.Ticker // Ticker for periodic scanning
  20. initialized bool // Whether the scanner has been initialized
  21. scanning bool // Whether a scan is currently in progress
  22. scanMutex sync.RWMutex // Mutex for protecting the scanning state
  23. statusChan chan bool // Channel to broadcast scanning status changes
  24. subscribers map[chan bool]struct{} // Set of subscribers
  25. subscriberMux sync.RWMutex // Mutex for protecting the subscribers map
  26. }
  27. // Global variables
  28. var (
  29. // scanner is the singleton instance of Scanner
  30. scanner *Scanner
  31. configScannerInitMux sync.Mutex
  32. // This regex matches: include directives in nginx config files
  33. includeRegex = regexp.MustCompile(`include\s+([^;]+);`)
  34. // Global callbacks that will be executed during config file scanning
  35. scanCallbacks []ScanCallback
  36. scanCallbacksMutex sync.RWMutex
  37. )
  38. func init() {
  39. // Initialize the callbacks slice
  40. scanCallbacks = make([]ScanCallback, 0)
  41. }
  42. // InitScanner initializes the config scanner
  43. func InitScanner() {
  44. if nginx.GetConfPath() == "" {
  45. logger.Error("Nginx config path is not set")
  46. return
  47. }
  48. s := GetScanner()
  49. err := s.Initialize()
  50. if err != nil {
  51. logger.Error("Failed to initialize config scanner:", err)
  52. }
  53. }
  54. // GetScanner returns the singleton instance of Scanner
  55. func GetScanner() *Scanner {
  56. configScannerInitMux.Lock()
  57. defer configScannerInitMux.Unlock()
  58. if scanner == nil {
  59. scanner = &Scanner{
  60. statusChan: make(chan bool, 10), // Buffer to prevent blocking
  61. subscribers: make(map[chan bool]struct{}),
  62. }
  63. // Start broadcaster goroutine
  64. go scanner.broadcastStatus()
  65. }
  66. return scanner
  67. }
  68. // RegisterCallback adds a callback function to be executed during scans
  69. // This function can be called before Scanner is initialized
  70. func RegisterCallback(callback ScanCallback) {
  71. scanCallbacksMutex.Lock()
  72. defer scanCallbacksMutex.Unlock()
  73. scanCallbacks = append(scanCallbacks, callback)
  74. }
  75. // broadcastStatus listens for status changes and broadcasts to all subscribers
  76. func (s *Scanner) broadcastStatus() {
  77. for status := range s.statusChan {
  78. s.subscriberMux.RLock()
  79. for ch := range s.subscribers {
  80. // Non-blocking send to prevent slow subscribers from blocking others
  81. select {
  82. case ch <- status:
  83. default:
  84. // Skip if channel buffer is full
  85. }
  86. }
  87. s.subscriberMux.RUnlock()
  88. }
  89. }
  90. // SubscribeScanningStatus allows a client to subscribe to scanning status changes
  91. func SubscribeScanningStatus() chan bool {
  92. s := GetScanner()
  93. ch := make(chan bool, 5) // Buffer to prevent blocking
  94. // Add to subscribers
  95. s.subscriberMux.Lock()
  96. s.subscribers[ch] = struct{}{}
  97. s.subscriberMux.Unlock()
  98. // Send current status immediately
  99. s.scanMutex.RLock()
  100. currentStatus := s.scanning
  101. s.scanMutex.RUnlock()
  102. // Non-blocking send
  103. select {
  104. case ch <- currentStatus:
  105. default:
  106. }
  107. return ch
  108. }
  109. // UnsubscribeScanningStatus removes a subscriber from receiving status updates
  110. func UnsubscribeScanningStatus(ch chan bool) {
  111. s := GetScanner()
  112. s.subscriberMux.Lock()
  113. delete(s.subscribers, ch)
  114. s.subscriberMux.Unlock()
  115. // Close the channel so the client knows it's unsubscribed
  116. close(ch)
  117. }
  118. // Initialize sets up the scanner and starts watching for file changes
  119. func (s *Scanner) Initialize() error {
  120. if s.initialized {
  121. return nil
  122. }
  123. // Create a new watcher
  124. watcher, err := fsnotify.NewWatcher()
  125. if err != nil {
  126. return err
  127. }
  128. s.watcher = watcher
  129. // Scan for the first time
  130. err = s.ScanAllConfigs()
  131. if err != nil {
  132. return err
  133. }
  134. // Setup watcher for config directory
  135. configDir := filepath.Dir(nginx.GetConfPath())
  136. availableDir := nginx.GetConfPath("sites-available")
  137. enabledDir := nginx.GetConfPath("sites-enabled")
  138. streamAvailableDir := nginx.GetConfPath("stream-available")
  139. streamEnabledDir := nginx.GetConfPath("stream-enabled")
  140. // Watch the main directories
  141. err = s.watcher.Add(configDir)
  142. if err != nil {
  143. logger.Error("Failed to watch config directory:", err)
  144. }
  145. // Watch sites-available and sites-enabled if they exist
  146. if _, err := os.Stat(availableDir); err == nil {
  147. err = s.watcher.Add(availableDir)
  148. if err != nil {
  149. logger.Error("Failed to watch sites-available directory:", err)
  150. }
  151. }
  152. if _, err := os.Stat(enabledDir); err == nil {
  153. err = s.watcher.Add(enabledDir)
  154. if err != nil {
  155. logger.Error("Failed to watch sites-enabled directory:", err)
  156. }
  157. }
  158. // Watch stream-available and stream-enabled if they exist
  159. if _, err := os.Stat(streamAvailableDir); err == nil {
  160. err = s.watcher.Add(streamAvailableDir)
  161. if err != nil {
  162. logger.Error("Failed to watch stream-available directory:", err)
  163. }
  164. }
  165. if _, err := os.Stat(streamEnabledDir); err == nil {
  166. err = s.watcher.Add(streamEnabledDir)
  167. if err != nil {
  168. logger.Error("Failed to watch stream-enabled directory:", err)
  169. }
  170. }
  171. // Start the watcher goroutine
  172. go s.watchForChanges()
  173. // Setup a ticker for periodic scanning (every 5 minutes)
  174. s.scanTicker = time.NewTicker(5 * time.Minute)
  175. go func() {
  176. for range s.scanTicker.C {
  177. err := s.ScanAllConfigs()
  178. if err != nil {
  179. logger.Error("Periodic config scan failed:", err)
  180. }
  181. }
  182. }()
  183. s.initialized = true
  184. return nil
  185. }
  186. // watchForChanges handles the fsnotify events and triggers rescans when necessary
  187. func (s *Scanner) watchForChanges() {
  188. for {
  189. select {
  190. case event, ok := <-s.watcher.Events:
  191. if !ok {
  192. return
  193. }
  194. // Skip irrelevant events
  195. if !event.Has(fsnotify.Create) && !event.Has(fsnotify.Write) &&
  196. !event.Has(fsnotify.Rename) && !event.Has(fsnotify.Remove) {
  197. continue
  198. }
  199. // Add newly created directories to the watch list
  200. if event.Has(fsnotify.Create) {
  201. if fi, err := os.Stat(event.Name); err == nil && fi.IsDir() {
  202. _ = s.watcher.Add(event.Name)
  203. }
  204. }
  205. // For remove events, perform a full scan
  206. if event.Has(fsnotify.Remove) {
  207. logger.Debug("Config item removed:", event.Name)
  208. if err := s.ScanAllConfigs(); err != nil {
  209. logger.Error("Failed to rescan configs after removal:", err)
  210. }
  211. continue
  212. }
  213. // Handle non-remove events
  214. fi, err := os.Stat(event.Name)
  215. if err != nil {
  216. logger.Error("Failed to stat changed path:", err)
  217. continue
  218. }
  219. if fi.IsDir() {
  220. // Directory change, perform full scan
  221. logger.Debug("Config directory changed:", event.Name)
  222. if err := s.ScanAllConfigs(); err != nil {
  223. logger.Error("Failed to rescan configs after directory change:", err)
  224. }
  225. } else {
  226. // File change, scan only the single file
  227. logger.Debug("Config file changed:", event.Name)
  228. // Give the system a moment to finish writing the file
  229. time.Sleep(100 * time.Millisecond)
  230. if err := s.scanSingleFile(event.Name); err != nil {
  231. logger.Error("Failed to scan changed file:", err)
  232. }
  233. }
  234. case err, ok := <-s.watcher.Errors:
  235. if !ok {
  236. return
  237. }
  238. logger.Error("Watcher error:", err)
  239. }
  240. }
  241. }
  242. // scanSingleFile scans a single file and executes all registered callbacks
  243. func (s *Scanner) scanSingleFile(filePath string) error {
  244. // Set scanning state to true
  245. s.scanMutex.Lock()
  246. wasScanning := s.scanning
  247. s.scanning = true
  248. if !wasScanning {
  249. // Only broadcast if status changed from not scanning to scanning
  250. s.statusChan <- true
  251. }
  252. s.scanMutex.Unlock()
  253. // Ensure we reset scanning state when done
  254. defer func() {
  255. s.scanMutex.Lock()
  256. s.scanning = false
  257. // Broadcast the completion
  258. s.statusChan <- false
  259. s.scanMutex.Unlock()
  260. }()
  261. // Open the file
  262. file, err := os.Open(filePath)
  263. if err != nil {
  264. return err
  265. }
  266. defer file.Close()
  267. // Read the entire file content
  268. content, err := os.ReadFile(filePath)
  269. if err != nil {
  270. return err
  271. }
  272. // Execute all registered callbacks
  273. scanCallbacksMutex.RLock()
  274. for _, callback := range scanCallbacks {
  275. err := callback(filePath, content)
  276. if err != nil {
  277. logger.Error("Callback error for file", filePath, ":", err)
  278. }
  279. }
  280. scanCallbacksMutex.RUnlock()
  281. // Look for include directives to process included files
  282. includeMatches := includeRegex.FindAllSubmatch(content, -1)
  283. for _, match := range includeMatches {
  284. if len(match) >= 2 {
  285. includePath := string(match[1])
  286. // Handle glob patterns in include directives
  287. if strings.Contains(includePath, "*") {
  288. // If it's a relative path, make it absolute based on nginx config dir
  289. if !filepath.IsAbs(includePath) {
  290. configDir := filepath.Dir(nginx.GetConfPath("", ""))
  291. includePath = filepath.Join(configDir, includePath)
  292. }
  293. // Expand the glob pattern
  294. matchedFiles, err := filepath.Glob(includePath)
  295. if err != nil {
  296. logger.Error("Error expanding glob pattern:", includePath, err)
  297. continue
  298. }
  299. // Process each matched file
  300. for _, matchedFile := range matchedFiles {
  301. fileInfo, err := os.Stat(matchedFile)
  302. if err == nil && !fileInfo.IsDir() {
  303. err = s.scanSingleFile(matchedFile)
  304. if err != nil {
  305. logger.Error("Failed to scan included file:", matchedFile, err)
  306. }
  307. }
  308. }
  309. } else {
  310. // Handle single file include
  311. // If it's a relative path, make it absolute based on nginx config dir
  312. if !filepath.IsAbs(includePath) {
  313. configDir := filepath.Dir(nginx.GetConfPath("", ""))
  314. includePath = filepath.Join(configDir, includePath)
  315. }
  316. fileInfo, err := os.Stat(includePath)
  317. if err == nil && !fileInfo.IsDir() {
  318. err = s.scanSingleFile(includePath)
  319. if err != nil {
  320. logger.Error("Failed to scan included file:", includePath, err)
  321. }
  322. }
  323. }
  324. }
  325. }
  326. return nil
  327. }
  328. // ScanAllConfigs scans all nginx config files and executes all registered callbacks
  329. func (s *Scanner) ScanAllConfigs() error {
  330. // Set scanning state to true
  331. s.scanMutex.Lock()
  332. wasScanning := s.scanning
  333. s.scanning = true
  334. if !wasScanning {
  335. // Only broadcast if status changed from not scanning to scanning
  336. s.statusChan <- true
  337. }
  338. s.scanMutex.Unlock()
  339. // Ensure we reset scanning state when done
  340. defer func() {
  341. s.scanMutex.Lock()
  342. s.scanning = false
  343. // Broadcast the completion
  344. s.statusChan <- false
  345. s.scanMutex.Unlock()
  346. }()
  347. // Get the main config file
  348. mainConfigPath := nginx.GetConfPath("", "nginx.conf")
  349. err := s.scanSingleFile(mainConfigPath)
  350. if err != nil {
  351. logger.Error("Failed to scan main config:", err)
  352. }
  353. // Scan sites-available directory
  354. sitesAvailablePath := nginx.GetConfPath("sites-available", "")
  355. sitesAvailableFiles, err := os.ReadDir(sitesAvailablePath)
  356. if err == nil {
  357. for _, file := range sitesAvailableFiles {
  358. if !file.IsDir() {
  359. configPath := filepath.Join(sitesAvailablePath, file.Name())
  360. err := s.scanSingleFile(configPath)
  361. if err != nil {
  362. logger.Error("Failed to scan config:", configPath, err)
  363. }
  364. }
  365. }
  366. }
  367. // Scan stream-available directory if it exists
  368. streamAvailablePath := nginx.GetConfPath("stream-available", "")
  369. streamAvailableFiles, err := os.ReadDir(streamAvailablePath)
  370. if err == nil {
  371. for _, file := range streamAvailableFiles {
  372. if !file.IsDir() {
  373. configPath := filepath.Join(streamAvailablePath, file.Name())
  374. err := s.scanSingleFile(configPath)
  375. if err != nil {
  376. logger.Error("Failed to scan stream config:", configPath, err)
  377. }
  378. }
  379. }
  380. }
  381. return nil
  382. }
  383. // Shutdown cleans up resources used by the scanner
  384. func (s *Scanner) Shutdown() {
  385. if s.watcher != nil {
  386. s.watcher.Close()
  387. }
  388. if s.scanTicker != nil {
  389. s.scanTicker.Stop()
  390. }
  391. // Clean up subscriber resources
  392. s.subscriberMux.Lock()
  393. // Close all subscriber channels
  394. for ch := range s.subscribers {
  395. close(ch)
  396. }
  397. // Clear the map
  398. s.subscribers = make(map[chan bool]struct{})
  399. s.subscriberMux.Unlock()
  400. // Close the status channel
  401. close(s.statusChan)
  402. }
  403. // IsScanningInProgress returns whether a scan is currently in progress
  404. func IsScanningInProgress() bool {
  405. s := GetScanner()
  406. s.scanMutex.RLock()
  407. defer s.scanMutex.RUnlock()
  408. return s.scanning
  409. }