瀏覽代碼

feat(nginx): add SbinPath option in settings

Jacky 4 周之前
父節點
當前提交
1fd24eb175

+ 1 - 0
app/src/constants/errors/nginx.ts

@@ -2,4 +2,5 @@ export default {
   50000: () => $gettext('Nginx error: {0}'),
   50001: () => $gettext('Block is nil'),
   50002: () => $gettext('Reload nginx failed: {0}'),
+  50003: () => $gettext('Nginx -T output is empty'),
 }

+ 10 - 0
docs/guide/config-nginx.md

@@ -69,6 +69,16 @@ In Nginx UI v2, we parse the output of the `nginx -V` command to get the default
 
 If you need to override the default path, you can use this option.
 
+### SbinPath
+- Type: `string`
+- Version: `>= v2.1.10`
+
+This option is used to set the path for the Nginx executable file.
+
+By default, Nginx UI will try to find the Nginx executable file in `$PATH`.
+
+If you need to override the default path, you can use this option.
+
 ### TestConfigCmd
 - Type: `string`
 - Default: `nginx -t`

+ 10 - 0
docs/zh_CN/guide/config-nginx.md

@@ -70,6 +70,16 @@ Nginx 日志对于监控、排查问题和维护您的 Web 服务器至关重要
 
 如果您需要覆盖默认路径,您可以使用此选项。
 
+### SbinPath
+- 类型:`string`
+- 版本:`>= v2.1.10`
+
+此选项用于设置 Nginx 可执行文件的路径。
+
+默认情况下,Nginx UI 会尝试在 `$PATH` 中查找 Nginx 可执行文件。
+
+如果您需要覆盖默认路径,您可以使用此选项。
+
 ### TestConfigCmd
 - 类型:`string`
 - 默认值:`nginx -t`

+ 10 - 0
docs/zh_TW/guide/config-nginx.md

@@ -69,6 +69,16 @@ Nginx 日誌對於監控、排查問題和維護您的 Web 伺服器至關重要
 
 如果您需要覆蓋預設路徑,您可以使用此選項。
 
+### SbinPath
+- 類型:`string`
+- 版本:`>= v2.1.10`
+
+此選項用於設定 Nginx 可執行檔的路徑。
+
+預設情況下,Nginx UI 會嘗試在 `$PATH` 中查找 Nginx 可執行檔。
+
+如果您需要覆蓋預設路徑,您可以使用此選項。
+
 ### TestConfigCmd
 - 類型:`string`
 - 預設值:`nginx -t`

+ 0 - 0
internal/nginx/log.go → internal/nginx/log_level.go


+ 143 - 0
internal/nginx/log_path.go

@@ -0,0 +1,143 @@
+package nginx
+
+import (
+	"os"
+	"path/filepath"
+	"regexp"
+	"strings"
+
+	"github.com/uozi-tech/cosy/logger"
+)
+
+// Regular expressions for parsing log directives from nginx -T output
+const (
+	// AccessLogRegexPattern matches access_log directive with unquoted path
+	// Matches: access_log /path/to/file
+	AccessLogRegexPattern = `(?m)^\s*access_log\s+([^\s;]+)`
+
+	// ErrorLogRegexPattern matches error_log directive with unquoted path
+	// Matches: error_log /path/to/file
+	ErrorLogRegexPattern = `(?m)^\s*error_log\s+([^\s;]+)`
+)
+
+var (
+	accessLogRegex *regexp.Regexp
+	errorLogRegex  *regexp.Regexp
+)
+
+func init() {
+	accessLogRegex = regexp.MustCompile(AccessLogRegexPattern)
+	errorLogRegex = regexp.MustCompile(ErrorLogRegexPattern)
+}
+
+// isValidRegularFile checks if the given path is a valid regular file
+// Returns true if the path exists and is a regular file (not a directory or special file)
+func isValidRegularFile(path string) bool {
+	if path == "" {
+		return false
+	}
+
+	fileInfo, err := os.Stat(path)
+	if err != nil {
+		logger.Debug("nginx.isValidRegularFile: failed to stat file", "path", path, "error", err)
+		return false
+	}
+
+	// Check if it's a regular file (not a directory or special file)
+	if !fileInfo.Mode().IsRegular() {
+		logger.Debug("nginx.isValidRegularFile: path is not a regular file", "path", path, "mode", fileInfo.Mode())
+		return false
+	}
+
+	return true
+}
+
+// isCommentedLine checks if a line is commented (starts with #)
+func isCommentedLine(line string) bool {
+	trimmed := strings.TrimSpace(line)
+	return strings.HasPrefix(trimmed, "#")
+}
+
+// getAccessLogPathFromNginxT extracts the first access_log path from nginx -T output
+func getAccessLogPathFromNginxT() string {
+	output := getNginxT()
+	if output == "" {
+		logger.Error("nginx.getAccessLogPathFromNginxT: nginx -T output is empty")
+		return ""
+	}
+
+	lines := strings.Split(output, "\n")
+
+	for _, line := range lines {
+		// Skip commented lines
+		if isCommentedLine(line) {
+			continue
+		}
+
+		matches := accessLogRegex.FindStringSubmatch(line)
+		if len(matches) >= 2 {
+			logPath := matches[1]
+
+			// Skip 'off' directive
+			if logPath == "off" {
+				continue
+			}
+			// Handle relative paths
+			if !filepath.IsAbs(logPath) {
+				logPath = filepath.Join(GetPrefix(), logPath)
+			}
+			resolvedPath := resolvePath(logPath)
+
+			// Validate that the path is a regular file
+			if !isValidRegularFile(resolvedPath) {
+				logger.Warn("nginx.getAccessLogPathFromNginxT: path is not a valid regular file", "path", resolvedPath)
+				continue
+			}
+
+			return resolvedPath
+		}
+	}
+
+	logger.Error("nginx.getAccessLogPathFromNginxT: no valid access_log file found")
+	return ""
+}
+
+// getErrorLogPathFromNginxT extracts the first error_log path from nginx -T output
+func getErrorLogPathFromNginxT() string {
+	output := getNginxT()
+	if output == "" {
+		logger.Error("nginx.getErrorLogPathFromNginxT: nginx -T output is empty")
+		return ""
+	}
+
+	lines := strings.Split(output, "\n")
+
+	for _, line := range lines {
+		// Skip commented lines
+		if isCommentedLine(line) {
+			continue
+		}
+
+		matches := errorLogRegex.FindStringSubmatch(line)
+		if len(matches) >= 2 {
+			logPath := matches[1]
+
+			// Handle relative paths
+			if !filepath.IsAbs(logPath) {
+				logPath = filepath.Join(GetPrefix(), logPath)
+			}
+			resolvedPath := resolvePath(logPath)
+
+			// Validate that the path is a regular file
+			if !isValidRegularFile(resolvedPath) {
+				logger.Warn("nginx.getErrorLogPathFromNginxT: path is not a valid regular file", "path", resolvedPath)
+				continue
+			}
+
+			return resolvedPath
+		}
+	}
+
+	logger.Error("nginx.getErrorLogPathFromNginxT: no valid error_log file found")
+	return ""
+}

+ 639 - 0
internal/nginx/log_path_test.go

@@ -0,0 +1,639 @@
+package nginx
+
+import (
+	"path/filepath"
+	"regexp"
+	"testing"
+)
+
+// Mock nginx -T output for testing purposes
+const mockNginxTOutput = `
+# configuration file /etc/nginx/nginx.conf:
+user  nginx;
+worker_processes  auto;
+
+error_log  /var/log/nginx/error.log notice;
+error_log  /var/log/nginx/error.local.log notice;
+pid        /var/run/nginx.pid;
+
+events {
+    worker_connections  1024;
+}
+
+http {
+    include       /etc/nginx/mime.types;
+    default_type  application/octet-stream;
+
+    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
+                      '$status $body_bytes_sent "$http_referer" '
+                      '"$http_user_agent" "$http_x_forwarded_for"';
+
+    access_log  /var/log/nginx/access.log  main;
+    access_log  /var/log/nginx/access.local.log  main;
+
+    sendfile        on;
+    keepalive_timeout  65;
+    gzip  on;
+
+    server {
+        listen       80;
+        server_name  localhost;
+        
+        access_log   /var/log/nginx/server.access.log;
+        error_log    /var/log/nginx/server.error.log warn;
+
+        location / {
+            root   /usr/share/nginx/html;
+            index  index.html index.htm;
+        }
+    }
+}
+
+stream {
+    error_log /var/log/nginx/stream.error.log info;
+    
+    server {
+        listen 3306;
+        proxy_pass backend;
+    }
+}
+`
+
+// Mock nginx -T output with relative paths
+const mockNginxTOutputRelative = `
+# configuration file /etc/nginx/nginx.conf:
+user  nginx;
+worker_processes  auto;
+
+error_log  logs/error.log notice;
+pid        /var/run/nginx.pid;
+
+http {
+    access_log  logs/access.log  main;
+    
+    server {
+        listen       80;
+        server_name  localhost;
+        
+        access_log   logs/server.access.log;
+        error_log    logs/server.error.log warn;
+    }
+}
+`
+
+// Mock nginx -T output with access_log off
+const mockNginxTOutputOff = `
+# configuration file /etc/nginx/nginx.conf:
+user  nginx;
+worker_processes  auto;
+
+error_log  /var/log/nginx/error.log notice;
+
+http {
+    access_log  off;
+    
+    server {
+        listen       80;
+        server_name  localhost;
+        
+        access_log   /var/log/nginx/server.access.log;
+        error_log    /var/log/nginx/server.error.log warn;
+    }
+}
+`
+
+// Mock nginx -T output with commented log directives
+const mockNginxTOutputCommented = `
+# configuration file /etc/nginx/nginx.conf:
+user  nginx;
+worker_processes  auto;
+
+# error_log  /var/log/nginx/commented.error.log notice;
+error_log  /var/log/nginx/error.log notice;
+
+http {
+    # access_log  /var/log/nginx/commented.access.log  main;
+    access_log  /var/log/nginx/access.log  main;
+    
+    server {
+        listen       80;
+        server_name  localhost;
+        
+        # access_log   /var/log/nginx/commented.server.access.log;
+        access_log   /var/log/nginx/server.access.log;
+        # error_log    /var/log/nginx/commented.server.error.log warn;
+        error_log    /var/log/nginx/server.error.log warn;
+    }
+}
+`
+
+func TestAccessLogRegexParsing(t *testing.T) {
+	testCases := []struct {
+		name          string
+		nginxTOutput  string
+		expectedPath  string
+		shouldHaveLog bool
+	}{
+		{
+			name:          "standard access log",
+			nginxTOutput:  "access_log  /var/log/nginx/access.log  main;",
+			expectedPath:  "/var/log/nginx/access.log",
+			shouldHaveLog: true,
+		},
+		{
+			name:          "access log turned off",
+			nginxTOutput:  "access_log  off;",
+			expectedPath:  "",
+			shouldHaveLog: false,
+		},
+		{
+			name:          "no access log directive",
+			nginxTOutput:  "server_name  localhost;",
+			expectedPath:  "",
+			shouldHaveLog: false,
+		},
+		{
+			name:          "indented access log",
+			nginxTOutput:  "    access_log  /var/log/nginx/server.log;",
+			expectedPath:  "/var/log/nginx/server.log",
+			shouldHaveLog: true,
+		},
+		{
+			name:          "multiple access logs - should get first",
+			nginxTOutput:  "access_log  /var/log/nginx/access1.log  main;\naccess_log  /var/log/nginx/access2.log  combined;",
+			expectedPath:  "/var/log/nginx/access1.log",
+			shouldHaveLog: true,
+		},
+		{
+			name:          "commented access log should be ignored",
+			nginxTOutput:  "# access_log  /var/log/nginx/commented.access.log  main;\naccess_log  /var/log/nginx/access.log  main;",
+			expectedPath:  "/var/log/nginx/access.log",
+			shouldHaveLog: true,
+		},
+		{
+			name:          "only commented access log",
+			nginxTOutput:  "# access_log  /var/log/nginx/commented.access.log  main;",
+			expectedPath:  "",
+			shouldHaveLog: false,
+		},
+	}
+
+	accessLogRegex := regexp.MustCompile(AccessLogRegexPattern)
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			matches := accessLogRegex.FindAllStringSubmatch(tc.nginxTOutput, -1)
+
+			if !tc.shouldHaveLog {
+				if len(matches) > 0 {
+					// Check if it's the "off" directive
+					if len(matches[0]) >= 2 {
+						logPath := matches[0][1]
+						if logPath != "off" {
+							t.Errorf("Expected no valid access log, but found: %s", logPath)
+						}
+					}
+				}
+				return
+			}
+
+			if len(matches) == 0 {
+				t.Errorf("Expected to find access log directive, but found none")
+				return
+			}
+
+			if len(matches[0]) < 2 {
+				t.Errorf("Expected regex match to have at least 2 groups, got %d", len(matches[0]))
+				return
+			}
+
+			logPath := matches[0][1]
+
+			if logPath != tc.expectedPath {
+				t.Errorf("Expected access log path %s, got %s", tc.expectedPath, logPath)
+			}
+		})
+	}
+}
+
+func TestErrorLogRegexParsing(t *testing.T) {
+	testCases := []struct {
+		name          string
+		nginxTOutput  string
+		expectedPath  string
+		shouldHaveLog bool
+	}{
+		{
+			name:          "standard error log",
+			nginxTOutput:  "error_log  /var/log/nginx/error.log notice;",
+			expectedPath:  "/var/log/nginx/error.log",
+			shouldHaveLog: true,
+		},
+		{
+			name:          "error log without level",
+			nginxTOutput:  "error_log  /var/log/nginx/error.log;",
+			expectedPath:  "/var/log/nginx/error.log",
+			shouldHaveLog: true,
+		},
+		{
+			name:          "no error log directive",
+			nginxTOutput:  "server_name  localhost;",
+			expectedPath:  "",
+			shouldHaveLog: false,
+		},
+		{
+			name:          "indented error log",
+			nginxTOutput:  "        error_log  /var/log/nginx/server.error.log warn;",
+			expectedPath:  "/var/log/nginx/server.error.log",
+			shouldHaveLog: true,
+		},
+		{
+			name:          "multiple error logs - should get first",
+			nginxTOutput:  "error_log  /var/log/nginx/error1.log  notice;\nerror_log  /var/log/nginx/error2.log  warn;",
+			expectedPath:  "/var/log/nginx/error1.log",
+			shouldHaveLog: true,
+		},
+		{
+			name:          "commented error log should be ignored",
+			nginxTOutput:  "# error_log  /var/log/nginx/commented.error.log  notice;\nerror_log  /var/log/nginx/error.log  notice;",
+			expectedPath:  "/var/log/nginx/error.log",
+			shouldHaveLog: true,
+		},
+		{
+			name:          "only commented error log",
+			nginxTOutput:  "# error_log  /var/log/nginx/commented.error.log  notice;",
+			expectedPath:  "",
+			shouldHaveLog: false,
+		},
+	}
+
+	errorLogRegex := regexp.MustCompile(ErrorLogRegexPattern)
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			matches := errorLogRegex.FindAllStringSubmatch(tc.nginxTOutput, -1)
+
+			if !tc.shouldHaveLog {
+				if len(matches) > 0 {
+					t.Errorf("Expected no error log directive, but found: %v", matches)
+				}
+				return
+			}
+
+			if len(matches) == 0 {
+				t.Errorf("Expected to find error log directive, but found none")
+				return
+			}
+
+			if len(matches[0]) < 2 {
+				t.Errorf("Expected regex match to have at least 2 groups, got %d", len(matches[0]))
+				return
+			}
+
+			logPath := matches[0][1]
+
+			if logPath != tc.expectedPath {
+				t.Errorf("Expected error log path %s, got %s", tc.expectedPath, logPath)
+			}
+		})
+	}
+}
+
+func TestLogPathParsing(t *testing.T) {
+	testCases := []struct {
+		name               string
+		nginxTOutput       string
+		expectedAccessPath string
+		expectedErrorPath  string
+		shouldHaveAccess   bool
+		shouldHaveError    bool
+	}{
+		{
+			name:               "complete configuration",
+			nginxTOutput:       mockNginxTOutput,
+			expectedAccessPath: "/var/log/nginx/access.log",
+			expectedErrorPath:  "/var/log/nginx/error.log",
+			shouldHaveAccess:   true,
+			shouldHaveError:    true,
+		},
+		{
+			name:               "configuration with commented directives",
+			nginxTOutput:       mockNginxTOutputCommented,
+			expectedAccessPath: "/var/log/nginx/access.log",
+			expectedErrorPath:  "/var/log/nginx/error.log",
+			shouldHaveAccess:   true,
+			shouldHaveError:    true,
+		},
+		{
+			name:               "access log turned off",
+			nginxTOutput:       mockNginxTOutputOff,
+			expectedAccessPath: "/var/log/nginx/server.access.log", // Should get the server-level access log
+			expectedErrorPath:  "/var/log/nginx/error.log",
+			shouldHaveAccess:   true,
+			shouldHaveError:    true,
+		},
+		{
+			name:               "empty configuration",
+			nginxTOutput:       "",
+			expectedAccessPath: "",
+			expectedErrorPath:  "",
+			shouldHaveAccess:   false,
+			shouldHaveError:    false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Test access log parsing
+			accessLogRegex := regexp.MustCompile(AccessLogRegexPattern)
+			accessMatches := accessLogRegex.FindAllStringSubmatch(tc.nginxTOutput, -1)
+
+			var foundAccessPath string
+			for _, match := range accessMatches {
+				if len(match) >= 2 {
+					logPath := match[1]
+					if logPath != "off" {
+						foundAccessPath = logPath
+						break
+					}
+				}
+			}
+
+			if tc.shouldHaveAccess {
+				if foundAccessPath == "" {
+					t.Errorf("Expected access log path %s, but found none", tc.expectedAccessPath)
+				} else if foundAccessPath != tc.expectedAccessPath {
+					t.Errorf("Expected access log path %s, got %s", tc.expectedAccessPath, foundAccessPath)
+				}
+			} else {
+				if foundAccessPath != "" {
+					t.Errorf("Expected no access log path, but found %s", foundAccessPath)
+				}
+			}
+
+			// Test error log parsing
+			errorLogRegex := regexp.MustCompile(ErrorLogRegexPattern)
+			errorMatches := errorLogRegex.FindAllStringSubmatch(tc.nginxTOutput, -1)
+
+			var foundErrorPath string
+			if len(errorMatches) > 0 && len(errorMatches[0]) >= 2 {
+				foundErrorPath = errorMatches[0][1]
+			}
+
+			if tc.shouldHaveError {
+				if foundErrorPath == "" {
+					t.Errorf("Expected error log path %s, but found none", tc.expectedErrorPath)
+				} else if foundErrorPath != tc.expectedErrorPath {
+					t.Errorf("Expected error log path %s, got %s", tc.expectedErrorPath, foundErrorPath)
+				}
+			} else {
+				if foundErrorPath != "" {
+					t.Errorf("Expected no error log path, but found %s", foundErrorPath)
+				}
+			}
+		})
+	}
+}
+
+func TestRelativePathHandling(t *testing.T) {
+	// Mock GetPrefix function for testing
+	originalGetPrefix := GetPrefix
+	defer func() {
+		// Restore original function (if needed for other tests)
+		_ = originalGetPrefix
+	}()
+
+	testPrefix := "/usr/local/nginx"
+
+	testCases := []struct {
+		name         string
+		inputPath    string
+		expectedPath string
+		isRelative   bool
+	}{
+		{
+			name:         "absolute path",
+			inputPath:    "/var/log/nginx/access.log",
+			expectedPath: "/var/log/nginx/access.log",
+			isRelative:   false,
+		},
+		{
+			name:         "relative path",
+			inputPath:    "logs/access.log",
+			expectedPath: filepath.Join(testPrefix, "logs/access.log"),
+			isRelative:   true,
+		},
+		{
+			name:         "relative path with ./",
+			inputPath:    "./logs/access.log",
+			expectedPath: filepath.Join(testPrefix, "./logs/access.log"),
+			isRelative:   true,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			var result string
+
+			if tc.isRelative {
+				result = filepath.Join(testPrefix, tc.inputPath)
+			} else {
+				result = tc.inputPath
+			}
+
+			if result != tc.expectedPath {
+				t.Errorf("Expected path %s, got %s", tc.expectedPath, result)
+			}
+		})
+	}
+}
+
+func TestComplexNginxConfiguration(t *testing.T) {
+	complexConfig := `
+# Main configuration
+user nginx;
+worker_processes auto;
+error_log /var/log/nginx/error.log warn;
+pid /var/run/nginx.pid;
+
+events {
+    worker_connections 1024;
+}
+
+http {
+    include /etc/nginx/mime.types;
+    default_type application/octet-stream;
+    
+    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+                   '$status $body_bytes_sent "$http_referer" '
+                   '"$http_user_agent" "$http_x_forwarded_for"';
+    
+    access_log /var/log/nginx/access.log main;
+    
+    sendfile on;
+    tcp_nopush on;
+    tcp_nodelay on;
+    keepalive_timeout 65;
+    types_hash_max_size 2048;
+    
+    # Virtual Host Configs
+    include /etc/nginx/conf.d/*.conf;
+    include /etc/nginx/sites-enabled/*;
+    
+    server {
+        listen 80 default_server;
+        listen [::]:80 default_server;
+        server_name _;
+        root /var/www/html;
+        index index.html index.htm index.nginx-debian.html;
+        
+        access_log /var/log/nginx/default.access.log;
+        error_log /var/log/nginx/default.error.log;
+        
+        location / {
+            try_files $uri $uri/ =404;
+        }
+        
+        location ~ /\.ht {
+            deny all;
+        }
+    }
+    
+    server {
+        listen 443 ssl http2;
+        server_name example.com;
+        root /var/www/example.com;
+        
+        access_log /var/log/nginx/example.access.log combined;
+        error_log /var/log/nginx/example.error.log info;
+        
+        ssl_certificate /etc/ssl/certs/example.com.pem;
+        ssl_certificate_key /etc/ssl/private/example.com.key;
+    }
+}
+
+stream {
+    error_log /var/log/nginx/stream.error.log info;
+    
+    upstream backend {
+        server 192.168.1.100:3306;
+        server 192.168.1.101:3306;
+    }
+    
+    server {
+        listen 3306;
+        proxy_pass backend;
+        proxy_timeout 1s;
+        proxy_responses 1;
+    }
+}
+`
+
+	// Test that we can extract the main access log and error log from complex config
+	accessLogRegex := regexp.MustCompile(AccessLogRegexPattern)
+	errorLogRegex := regexp.MustCompile(ErrorLogRegexPattern)
+
+	// Find all access logs
+	accessMatches := accessLogRegex.FindAllStringSubmatch(complexConfig, -1)
+	if len(accessMatches) == 0 {
+		t.Error("Expected to find access log directives in complex config")
+	} else {
+		firstAccessLog := accessMatches[0][1]
+		expectedFirstAccess := "/var/log/nginx/access.log"
+		if firstAccessLog != expectedFirstAccess {
+			t.Errorf("Expected first access log to be %s, got %s", expectedFirstAccess, firstAccessLog)
+		}
+		t.Logf("Found %d access log directives, first: %s", len(accessMatches), firstAccessLog)
+	}
+
+	// Find all error logs
+	errorMatches := errorLogRegex.FindAllStringSubmatch(complexConfig, -1)
+	if len(errorMatches) == 0 {
+		t.Error("Expected to find error log directives in complex config")
+	} else {
+		firstErrorLog := errorMatches[0][1]
+		expectedFirstError := "/var/log/nginx/error.log"
+		if firstErrorLog != expectedFirstError {
+			t.Errorf("Expected first error log to be %s, got %s", expectedFirstError, firstErrorLog)
+		}
+		t.Logf("Found %d error log directives, first: %s", len(errorMatches), firstErrorLog)
+	}
+}
+
+func TestCommentedDirectivesIgnored(t *testing.T) {
+	testConfig := `
+# Main configuration
+user nginx;
+worker_processes auto;
+
+# These should be ignored
+# error_log  /var/log/nginx/commented.error.log notice;
+# access_log  /var/log/nginx/commented.access.log  main;
+
+# Real directives
+error_log /var/log/nginx/error.log warn;
+
+http {
+    # This should be ignored too
+    # access_log /var/log/nginx/commented.http.access.log combined;
+    
+    # Real directive
+    access_log /var/log/nginx/access.log main;
+    
+    server {
+        listen 80;
+        server_name example.com;
+        
+        # Commented server-level logs should be ignored
+        # access_log /var/log/nginx/commented.server.access.log;
+        # error_log /var/log/nginx/commented.server.error.log warn;
+        
+        # Real server-level logs
+        access_log /var/log/nginx/server.access.log;
+        error_log /var/log/nginx/server.error.log info;
+    }
+}
+`
+
+	// Test access log parsing ignores comments
+	accessLogRegex := regexp.MustCompile(AccessLogRegexPattern)
+	accessMatches := accessLogRegex.FindAllStringSubmatch(testConfig, -1)
+
+	expectedAccessLogs := []string{
+		"/var/log/nginx/access.log",
+		"/var/log/nginx/server.access.log",
+	}
+
+	if len(accessMatches) != len(expectedAccessLogs) {
+		t.Errorf("Expected %d access log matches, got %d", len(expectedAccessLogs), len(accessMatches))
+	}
+
+	for i, match := range accessMatches {
+		if i < len(expectedAccessLogs) {
+			if match[1] != expectedAccessLogs[i] {
+				t.Errorf("Expected access log %d to be %s, got %s", i, expectedAccessLogs[i], match[1])
+			}
+		}
+	}
+
+	// Test error log parsing ignores comments
+	errorLogRegex := regexp.MustCompile(ErrorLogRegexPattern)
+	errorMatches := errorLogRegex.FindAllStringSubmatch(testConfig, -1)
+
+	expectedErrorLogs := []string{
+		"/var/log/nginx/error.log",
+		"/var/log/nginx/server.error.log",
+	}
+
+	if len(errorMatches) != len(expectedErrorLogs) {
+		t.Errorf("Expected %d error log matches, got %d", len(expectedErrorLogs), len(errorMatches))
+	}
+
+	for i, match := range errorMatches {
+		if i < len(expectedErrorLogs) {
+			if match[1] != expectedErrorLogs[i] {
+				t.Errorf("Expected error log %d to be %s, got %s", i, expectedErrorLogs[i], match[1])
+			}
+		}
+	}
+}

+ 80 - 0
internal/nginx/resolve_cmd.go

@@ -0,0 +1,80 @@
+package nginx
+
+import (
+	"os/exec"
+	"runtime"
+
+	"github.com/0xJacky/Nginx-UI/settings"
+	"github.com/uozi-tech/cosy/logger"
+)
+
+var (
+	nginxSbinPath string
+	nginxVOutput  string
+	nginxTOutput  string
+)
+
+// Returns the path to the nginx executable
+func getNginxSbinPath() string {
+	// load from cache
+	if nginxSbinPath != "" {
+		return nginxSbinPath
+	}
+
+	// load from settings
+	if settings.NginxSettings.SbinPath != "" {
+		nginxSbinPath = settings.NginxSettings.SbinPath
+		return nginxSbinPath
+	}
+
+	// load from system
+	var path string
+	var err error
+	if runtime.GOOS == "windows" {
+		path, err = exec.LookPath("nginx.exe")
+	} else {
+		path, err = exec.LookPath("nginx")
+	}
+	if err == nil {
+		nginxSbinPath = path
+		return nginxSbinPath
+	}
+	return nginxSbinPath
+}
+
+func getNginxV() string {
+	// load from cache
+	if nginxVOutput != "" {
+		return nginxVOutput
+	}
+
+	// load from system
+	exePath := getNginxSbinPath()
+	out, err := execCommand(exePath, "-V")
+	if err != nil {
+		logger.Error(err)
+		return ""
+	}
+
+	nginxVOutput = string(out)
+	return nginxVOutput
+}
+
+// getNginxT executes nginx -T and returns the output
+func getNginxT() string {
+	// load from cache
+	if nginxTOutput != "" {
+		return nginxTOutput
+	}
+
+	// load from system
+	exePath := getNginxSbinPath()
+	out, err := execCommand(exePath, "-T")
+	if err != nil {
+		logger.Error(err)
+		return ""
+	}
+
+	nginxTOutput = out
+	return nginxTOutput
+}

+ 34 - 84
internal/nginx/config_args.go → internal/nginx/resolve_path.go

@@ -1,7 +1,6 @@
 package nginx
 
 import (
-	"os/exec"
 	"path/filepath"
 	"regexp"
 	"runtime"
@@ -12,52 +11,9 @@ import (
 	"github.com/uozi-tech/cosy/logger"
 )
 
-var nginxExePath string
-
-// Returns the path to the nginx executable
-func getNginxExePath() string {
-	if nginxExePath != "" {
-		return nginxExePath
-	}
-
-	var path string
-	var err error
-	if runtime.GOOS == "windows" {
-		path, err = exec.LookPath("nginx.exe")
-	} else {
-		path, err = exec.LookPath("nginx")
-	}
-	if err == nil {
-		nginxExePath = path
-		return nginxExePath
-	}
-	return nginxExePath
-}
-
 // Returns the directory containing the nginx executable
 func GetNginxExeDir() string {
-	return filepath.Dir(getNginxExePath())
-}
-
-func getNginxV() string {
-	exePath := getNginxExePath()
-	out, err := execCommand(exePath, "-V")
-	if err != nil {
-		logger.Error(err)
-		return ""
-	}
-	return string(out)
-}
-
-// getNginxT executes nginx -T and returns the output
-func getNginxT() string {
-	exePath := getNginxExePath()
-	out, err := execCommand(exePath, "-T")
-	if err != nil {
-		logger.Error(err)
-		return ""
-	}
-	return out
+	return filepath.Dir(getNginxSbinPath())
 }
 
 // Resolves relative paths by joining them with the nginx executable directory on Windows
@@ -86,7 +42,7 @@ func GetPrefix() string {
 	return resolvePath(match[1])
 }
 
-// GetConfPath returns the path to the nginx configuration file
+// GetConfPath returns the path of the nginx configuration file
 func GetConfPath(dir ...string) (confPath string) {
 	if settings.NginxSettings.ConfigDir == "" {
 		out := getNginxV()
@@ -110,7 +66,7 @@ func GetConfPath(dir ...string) (confPath string) {
 	return joined
 }
 
-// GetConfEntryPath returns the path to the nginx configuration file
+// GetConfEntryPath returns the path of the nginx configuration file
 func GetConfEntryPath() (path string) {
 	if settings.NginxSettings.ConfigPath == "" {
 		out := getNginxV()
@@ -128,14 +84,14 @@ func GetConfEntryPath() (path string) {
 	return resolvePath(path)
 }
 
-// GetPIDPath returns the path to the nginx PID file
+// GetPIDPath returns the path of the nginx PID file
 func GetPIDPath() (path string) {
 	if settings.NginxSettings.PIDPath == "" {
 		out := getNginxV()
 		r, _ := regexp.Compile("--pid-path=(.*.pid)")
 		match := r.FindStringSubmatch(out)
 		if len(match) < 1 {
-			logger.Error("nginx.GetPIDPath len(match) < 1")
+			logger.Error("pid path not found in nginx -V output")
 			return ""
 		}
 		path = match[1]
@@ -146,65 +102,59 @@ func GetPIDPath() (path string) {
 	return resolvePath(path)
 }
 
-// GetSbinPath returns the path to the nginx executable
+// GetSbinPath returns the path of the nginx executable
 func GetSbinPath() (path string) {
-	out := getNginxV()
-	r, _ := regexp.Compile(`--sbin-path=(\S+)`)
-	match := r.FindStringSubmatch(out)
-	if len(match) < 1 {
-		logger.Error("nginx.GetPIDPath len(match) < 1")
-		return ""
-	}
-	path = match[1]
-
-	return resolvePath(path)
+	return getNginxSbinPath()
 }
 
-// GetAccessLogPath returns the path to the nginx access log file
+// GetAccessLogPath returns the path of the nginx access log file
 func GetAccessLogPath() (path string) {
-	if settings.NginxSettings.AccessLogPath == "" {
+	path = settings.NginxSettings.AccessLogPath
+
+	if path == "" {
 		out := getNginxV()
 		r, _ := regexp.Compile(`--http-log-path=(\S+)`)
 		match := r.FindStringSubmatch(out)
-		if len(match) < 1 {
-			logger.Error("nginx.GetAccessLogPath len(match) < 1")
-			return ""
+		if len(match) > 1 {
+			path = match[1]
+		}
+		if path == "" {
+			logger.Debug("access log path not found in nginx -V output, try to get from nginx -T output")
+			path = getAccessLogPathFromNginxT()
 		}
-		path = match[1]
-	} else {
-		path = settings.NginxSettings.AccessLogPath
 	}
 
 	return resolvePath(path)
 }
 
-// GetErrorLogPath returns the path to the nginx error log file
+// GetErrorLogPath returns the path of the nginx error log file
 func GetErrorLogPath() string {
-	if settings.NginxSettings.ErrorLogPath == "" {
+	path := settings.NginxSettings.ErrorLogPath
+
+	if path == "" {
 		out := getNginxV()
 		r, _ := regexp.Compile(`--error-log-path=(\S+)`)
 		match := r.FindStringSubmatch(out)
-		if len(match) < 1 {
-			logger.Error("nginx.GetErrorLogPath len(match) < 1")
-			return ""
+		if len(match) > 1 {
+			path = match[1]
+		}
+		if path == "" {
+			logger.Debug("error log path not found in nginx -V output, try to get from nginx -T output")
+			path = getErrorLogPathFromNginxT()
 		}
-		return resolvePath(match[1])
-	} else {
-		return resolvePath(settings.NginxSettings.ErrorLogPath)
 	}
+
+	return resolvePath(path)
 }
 
-// GetModulesPath returns the nginx modules path
+// GetModulesPath returns the path of the nginx modules
 func GetModulesPath() string {
 	// First try to get from nginx -V output
-	stdOut, stdErr := execCommand(getNginxExePath(), "-V")
-	if stdErr != nil {
-		return ""
-	}
-	if stdOut != "" {
+	out := getNginxV()
+	if out != "" {
 		// Look for --modules-path in the output
-		if strings.Contains(stdOut, "--modules-path=") {
-			parts := strings.Split(stdOut, "--modules-path=")
+		if strings.Contains(out, "--modules-path=") {
+			parts := strings.Split(out, "--modules-path=")
 			if len(parts) > 1 {
 				// Extract the path
 				path := strings.Split(parts[1], " ")[0]

+ 1 - 0
settings/nginx.go

@@ -7,6 +7,7 @@ type Nginx struct {
 	ConfigDir       string   `json:"config_dir" protected:"true"`
 	ConfigPath      string   `json:"config_path" protected:"true"`
 	PIDPath         string   `json:"pid_path" protected:"true"`
+	SbinPath        string   `json:"sbin_path" protected:"true"`
 	TestConfigCmd   string   `json:"test_config_cmd" protected:"true"`
 	ReloadCmd       string   `json:"reload_cmd" protected:"true"`
 	RestartCmd      string   `json:"restart_cmd" protected:"true"`