index.go 12 KB

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