Browse Source

feat(nginx): add SbinPath option in settings

Jacky 1 month ago
parent
commit
1fd24eb175

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

@@ -2,4 +2,5 @@ export default {
   50000: () => $gettext('Nginx error: {0}'),
   50000: () => $gettext('Nginx error: {0}'),
   50001: () => $gettext('Block is nil'),
   50001: () => $gettext('Block is nil'),
   50002: () => $gettext('Reload nginx failed: {0}'),
   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.
 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
 ### TestConfigCmd
 - Type: `string`
 - Type: `string`
 - Default: `nginx -t`
 - 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
 ### TestConfigCmd
 - 类型:`string`
 - 类型:`string`
 - 默认值:`nginx -t`
 - 默认值:`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
 ### TestConfigCmd
 - 類型:`string`
 - 類型:`string`
 - 預設值:`nginx -t`
 - 預設值:`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
 package nginx
 
 
 import (
 import (
-	"os/exec"
 	"path/filepath"
 	"path/filepath"
 	"regexp"
 	"regexp"
 	"runtime"
 	"runtime"
@@ -12,52 +11,9 @@ import (
 	"github.com/uozi-tech/cosy/logger"
 	"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
 // Returns the directory containing the nginx executable
 func GetNginxExeDir() string {
 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
 // Resolves relative paths by joining them with the nginx executable directory on Windows
@@ -86,7 +42,7 @@ func GetPrefix() string {
 	return resolvePath(match[1])
 	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) {
 func GetConfPath(dir ...string) (confPath string) {
 	if settings.NginxSettings.ConfigDir == "" {
 	if settings.NginxSettings.ConfigDir == "" {
 		out := getNginxV()
 		out := getNginxV()
@@ -110,7 +66,7 @@ func GetConfPath(dir ...string) (confPath string) {
 	return joined
 	return joined
 }
 }
 
 
-// GetConfEntryPath returns the path to the nginx configuration file
+// GetConfEntryPath returns the path of the nginx configuration file
 func GetConfEntryPath() (path string) {
 func GetConfEntryPath() (path string) {
 	if settings.NginxSettings.ConfigPath == "" {
 	if settings.NginxSettings.ConfigPath == "" {
 		out := getNginxV()
 		out := getNginxV()
@@ -128,14 +84,14 @@ func GetConfEntryPath() (path string) {
 	return resolvePath(path)
 	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) {
 func GetPIDPath() (path string) {
 	if settings.NginxSettings.PIDPath == "" {
 	if settings.NginxSettings.PIDPath == "" {
 		out := getNginxV()
 		out := getNginxV()
 		r, _ := regexp.Compile("--pid-path=(.*.pid)")
 		r, _ := regexp.Compile("--pid-path=(.*.pid)")
 		match := r.FindStringSubmatch(out)
 		match := r.FindStringSubmatch(out)
 		if len(match) < 1 {
 		if len(match) < 1 {
-			logger.Error("nginx.GetPIDPath len(match) < 1")
+			logger.Error("pid path not found in nginx -V output")
 			return ""
 			return ""
 		}
 		}
 		path = match[1]
 		path = match[1]
@@ -146,65 +102,59 @@ func GetPIDPath() (path string) {
 	return resolvePath(path)
 	return resolvePath(path)
 }
 }
 
 
-// GetSbinPath returns the path to the nginx executable
+// GetSbinPath returns the path of the nginx executable
 func GetSbinPath() (path string) {
 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) {
 func GetAccessLogPath() (path string) {
-	if settings.NginxSettings.AccessLogPath == "" {
+	path = settings.NginxSettings.AccessLogPath
+
+	if path == "" {
 		out := getNginxV()
 		out := getNginxV()
 		r, _ := regexp.Compile(`--http-log-path=(\S+)`)
 		r, _ := regexp.Compile(`--http-log-path=(\S+)`)
 		match := r.FindStringSubmatch(out)
 		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)
 	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 {
 func GetErrorLogPath() string {
-	if settings.NginxSettings.ErrorLogPath == "" {
+	path := settings.NginxSettings.ErrorLogPath
+
+	if path == "" {
 		out := getNginxV()
 		out := getNginxV()
 		r, _ := regexp.Compile(`--error-log-path=(\S+)`)
 		r, _ := regexp.Compile(`--error-log-path=(\S+)`)
 		match := r.FindStringSubmatch(out)
 		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 {
 func GetModulesPath() string {
 	// First try to get from nginx -V output
 	// 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
 		// 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 {
 			if len(parts) > 1 {
 				// Extract the path
 				// Extract the path
 				path := strings.Split(parts[1], " ")[0]
 				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"`
 	ConfigDir       string   `json:"config_dir" protected:"true"`
 	ConfigPath      string   `json:"config_path" protected:"true"`
 	ConfigPath      string   `json:"config_path" protected:"true"`
 	PIDPath         string   `json:"pid_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"`
 	TestConfigCmd   string   `json:"test_config_cmd" protected:"true"`
 	ReloadCmd       string   `json:"reload_cmd" protected:"true"`
 	ReloadCmd       string   `json:"reload_cmd" protected:"true"`
 	RestartCmd      string   `json:"restart_cmd" protected:"true"`
 	RestartCmd      string   `json:"restart_cmd" protected:"true"`