浏览代码

enhance(cache): handle symlinks in file event handler

0xJacky 2 周之前
父节点
当前提交
39ebb729a6

+ 1 - 0
api/analytic/analytic.go

@@ -94,6 +94,7 @@ func Analytic(c *gin.Context) {
 
 		select {
 		case <-kernel.Context.Done():
+			logger.Debug("Analytic: Context cancelled, closing WebSocket")
 			return
 		case <-time.After(1 * time.Second):
 		}

+ 2 - 0
api/analytic/nodes.go

@@ -86,6 +86,7 @@ func GetNodeStat(c *gin.Context) {
 
 		select {
 		case <-kernel.Context.Done():
+			logger.Debug("GetNodeStat: Context cancelled, closing WebSocket")
 			return
 		case <-time.After(10 * time.Second):
 		}
@@ -119,6 +120,7 @@ func GetNodesAnalytic(c *gin.Context) {
 
 		select {
 		case <-kernel.Context.Done():
+			logger.Debug("GetNodesAnalytic: Context cancelled, closing WebSocket")
 			return
 		case <-time.After(10 * time.Second):
 		}

+ 1 - 0
api/event/websocket.go

@@ -179,6 +179,7 @@ func (c *Client) writePump() {
 			return
 
 		case <-kernel.Context.Done():
+			logger.Debug("EventBus: Context cancelled, closing WebSocket")
 			return
 		}
 	}

+ 2 - 0
api/nginx/websocket.go

@@ -80,6 +80,7 @@ func (h *NginxPerformanceHub) run() {
 			h.broadcastPerformanceData()
 
 		case <-kernel.Context.Done():
+			logger.Debug("NginxPerformanceHub: Context cancelled, closing WebSocket")
 			// Shutdown all clients
 			h.mutex.Lock()
 			for client := range h.clients {
@@ -200,6 +201,7 @@ func (c *NginxPerformanceClient) writePump() {
 			return
 
 		case <-kernel.Context.Done():
+			logger.Debug("NginxPerformanceClient: Context cancelled, closing WebSocket")
 			return
 		}
 	}

+ 4 - 7
api/upstream/upstream.go

@@ -7,6 +7,7 @@ import (
 	"time"
 
 	"github.com/0xJacky/Nginx-UI/internal/helper"
+	"github.com/0xJacky/Nginx-UI/internal/kernel"
 	"github.com/0xJacky/Nginx-UI/internal/upstream"
 	"github.com/gin-gonic/gin"
 	"github.com/gorilla/websocket"
@@ -108,6 +109,9 @@ func AvailabilityWebSocket(c *gin.Context) {
 					return
 				}
 			}
+		case <-kernel.Context.Done():
+			logger.Debug("AvailabilityWebSocket: Context cancelled, closing WebSocket")
+			return
 		}
 	}
 }
@@ -143,10 +147,3 @@ func unregisterWebSocketConnection() {
 	}
 	logger.Debug("WebSocket connection unregistered, remaining connections:", wsConnections)
 }
-
-// HasActiveWebSocketConnections returns true if there are active WebSocket connections
-func HasActiveWebSocketConnections() bool {
-	wsConnectionMutex.Lock()
-	defer wsConnectionMutex.Unlock()
-	return wsConnections > 0
-}

+ 107 - 9
internal/cache/index.go

@@ -2,6 +2,7 @@ package cache
 
 import (
 	"context"
+	"fmt"
 	"io/fs"
 	"os"
 	"path/filepath"
@@ -132,11 +133,24 @@ func (s *Scanner) watchAllDirectories() error {
 				return filepath.SkipDir
 			}
 
-			if err := s.watcher.Add(path); err != nil {
-				logger.Error("Failed to watch directory:", path, err)
+			// Resolve symlinks to get the actual directory path to watch
+			actualPath := path
+			if d.Type()&os.ModeSymlink != 0 {
+				// This is a symlink, resolve it to get the target path
+				if resolvedPath, err := filepath.EvalSymlinks(path); err == nil {
+					actualPath = resolvedPath
+					logger.Debug("Resolved symlink for watching:", path, "->", actualPath)
+				} else {
+					logger.Debug("Failed to resolve symlink, skipping:", path, err)
+					return filepath.SkipDir
+				}
+			}
+
+			if err := s.watcher.Add(actualPath); err != nil {
+				logger.Error("Failed to watch directory:", actualPath, err)
 				return err
 			}
-			// logger.Debug("Watching directory:", path)
+			logger.Debug("Watching directory:", actualPath)
 		}
 		return nil
 	})
@@ -216,12 +230,28 @@ func (s *Scanner) handleFileEvent(event fsnotify.Event) {
 		return
 	}
 
-	fi, err := os.Stat(event.Name)
+	// Use Lstat to get symlink info without following it
+	fi, err := os.Lstat(event.Name)
 	if err != nil {
 		return
 	}
 
-	if fi.IsDir() {
+	// If it's a symlink, we need to check what it points to
+	var targetIsDir bool
+	if fi.Mode()&os.ModeSymlink != 0 {
+		// For symlinks, check the target
+		targetFi, err := os.Stat(event.Name)
+		if err != nil {
+			logger.Debug("Symlink target not accessible:", event.Name, err)
+			return
+		}
+		targetIsDir = targetFi.IsDir()
+		logger.Debug("Symlink changed:", event.Name, "-> target is dir:", targetIsDir)
+	} else {
+		targetIsDir = fi.IsDir()
+	}
+
+	if targetIsDir {
 		logger.Debug("Directory changed:", event.Name)
 	} else {
 		logger.Debug("File changed:", event.Name)
@@ -252,10 +282,24 @@ func (s *Scanner) scanSingleFile(filePath string) error {
 		return nil
 	}
 
-	// Skip symlinks to avoid potential issues
+	// Handle symlinks carefully
 	if fileInfo.Mode()&os.ModeSymlink != 0 {
-		logger.Debugf("Skipping symlink: %s", filePath)
-		return nil
+		// Check what the symlink points to
+		targetInfo, err := os.Stat(filePath)
+		if err != nil {
+			logger.Debugf("Skipping symlink with inaccessible target: %s (%v)", filePath, err)
+			return nil
+		}
+
+		// Skip symlinks to directories
+		if targetInfo.IsDir() {
+			logger.Debugf("Skipping symlink to directory: %s", filePath)
+			return nil
+		}
+
+		// Process symlinks to files, but use the target's info for size check
+		fileInfo = targetInfo
+		logger.Debugf("Processing symlink to file: %s", filePath)
 	}
 
 	// Skip non-regular files (devices, pipes, sockets, etc.)
@@ -326,7 +370,22 @@ func (s *Scanner) ScanAllConfigs() error {
 			return filepath.SkipDir
 		}
 
-		// Only process regular files
+		// Handle symlinks to directories specially
+		if d.Type()&os.ModeSymlink != 0 {
+			if targetInfo, err := os.Stat(path); err == nil && targetInfo.IsDir() {
+				// This is a symlink to a directory, we should traverse its contents
+				// but not process the symlink itself as a file
+				logger.Debug("Found symlink to directory, will traverse contents:", path)
+
+				// Manually scan the symlink target directory since WalkDir doesn't follow symlinks
+				if err := s.scanSymlinkDirectory(path); err != nil {
+					logger.Error("Failed to scan symlink directory:", path, err)
+				}
+				return nil
+			}
+		}
+
+		// Only process regular files (not directories, not symlinks to directories)
 		if !d.IsDir() {
 			if err := s.scanSingleFile(path); err != nil {
 				logger.Error("Failed to scan config:", path, err)
@@ -337,6 +396,45 @@ func (s *Scanner) ScanAllConfigs() error {
 	})
 }
 
+// scanSymlinkDirectory recursively scans a symlink directory and its contents
+func (s *Scanner) scanSymlinkDirectory(symlinkPath string) error {
+	// Resolve the symlink to get the actual target path
+	targetPath, err := filepath.EvalSymlinks(symlinkPath)
+	if err != nil {
+		return fmt.Errorf("failed to resolve symlink %s: %w", symlinkPath, err)
+	}
+
+	logger.Debug("Scanning symlink directory contents:", symlinkPath, "->", targetPath)
+
+	// Use WalkDir on the resolved target path
+	return filepath.WalkDir(targetPath, func(path string, d fs.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+
+		// Skip excluded directories
+		if d.IsDir() && shouldSkipPath(path) {
+			return filepath.SkipDir
+		}
+
+		// Only process regular files (not directories, not symlinks to directories)
+		if !d.IsDir() {
+			// Handle symlinks to directories (skip them)
+			if d.Type()&os.ModeSymlink != 0 {
+				if targetInfo, err := os.Stat(path); err == nil && targetInfo.IsDir() {
+					logger.Debug("Skipping symlink to directory in symlink scan:", path)
+					return nil
+				}
+			}
+
+			if err := s.scanSingleFile(path); err != nil {
+				logger.Error("Failed to scan config in symlink directory:", path, err)
+			}
+		}
+		return nil
+	})
+}
+
 // Shutdown cleans up scanner resources
 func (s *Scanner) Shutdown() {
 	if s.watcher != nil {

+ 0 - 13
internal/cron/upstream_availability.go

@@ -3,7 +3,6 @@ package cron
 import (
 	"time"
 
-	apiUpstream "github.com/0xJacky/Nginx-UI/api/upstream"
 	"github.com/0xJacky/Nginx-UI/internal/upstream"
 	"github.com/go-co-op/gocron/v2"
 	"github.com/uozi-tech/cosy/logger"
@@ -41,13 +40,6 @@ func executeUpstreamAvailabilityTest() {
 		return
 	}
 
-	// Check if we should skip this test due to active WebSocket connections
-	// (WebSocket connections trigger more frequent checks)
-	if hasActiveWebSocketConnections() {
-		logger.Debug("Skipping scheduled test due to active WebSocket connections")
-		return
-	}
-
 	start := time.Now()
 	logger.Debug("Starting scheduled upstream availability test for", targetCount, "targets")
 
@@ -57,11 +49,6 @@ func executeUpstreamAvailabilityTest() {
 	logger.Debug("Upstream availability test completed in", duration)
 }
 
-// hasActiveWebSocketConnections checks if there are active WebSocket connections
-func hasActiveWebSocketConnections() bool {
-	return apiUpstream.HasActiveWebSocketConnections()
-}
-
 // RestartUpstreamAvailabilityJob restarts the upstream availability job
 func RestartUpstreamAvailabilityJob() error {
 	logger.Info("Restarting upstream availability job...")