Files
nginx-ui/internal/nginx/resolve_path.go
0xJacky 0649cf786f feat: Implement PID path extraction from nginx -T output
- Added regex pattern for parsing the pid directive in nginx configurations.
- Introduced `getPIDPathFromNginxT` function to extract the pid file path, handling both absolute and relative paths.
- Enhanced `GetPIDPath` function to prioritize user settings, compile-time defaults, and runtime overrides, ensuring robust path resolution.
- Added unit tests for PID regex parsing to validate various scenarios, including standard, indented, and commented directives.

This update improves the handling of pid paths, particularly for nginx-unprivileged setups, and ensures accurate logging and configuration management.
2026-02-08 12:23:50 +00:00

308 lines
7.7 KiB
Go

package nginx
import (
"os"
"path/filepath"
"runtime"
"strings"
"github.com/0xJacky/Nginx-UI/internal/docker"
"github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/uozi-tech/cosy/logger"
)
var (
nginxPrefix string
)
// GetNginxExeDir Returns the directory containing the nginx executable
func GetNginxExeDir() string {
return filepath.Dir(getNginxSbinPath())
}
// Resolves relative paths by joining them with the nginx executable directory on Windows
func resolvePath(path string) string {
if path == "" {
return ""
}
// Handle relative paths on Windows
if runtime.GOOS == "windows" && !filepath.IsAbs(path) {
return filepath.Join(GetNginxExeDir(), path)
}
return path
}
func extractConfigureArg(out, flag string) string {
if out == "" || flag == "" {
return ""
}
if !strings.HasPrefix(flag, "--") {
flag = "--" + flag
}
needle := flag + "="
idx := strings.Index(out, needle)
if idx == -1 {
return ""
}
start := idx + len(needle)
if start >= len(out) {
return ""
}
value := out[start:]
value = strings.TrimLeft(value, " \t")
if value == "" {
return ""
}
if value[0] == '"' || value[0] == '\'' {
quoteChar := value[0]
rest := value[1:]
closingIdx := strings.IndexByte(rest, quoteChar)
if closingIdx == -1 {
return strings.TrimSpace(rest)
}
return strings.TrimSpace(rest[:closingIdx])
}
cut := len(value)
if idx := strings.Index(value, " --"); idx != -1 && idx < cut {
cut = idx
}
if idx := strings.IndexAny(value, "\r\n"); idx != -1 && idx < cut {
cut = idx
}
return strings.TrimSpace(value[:cut])
}
// GetPrefix returns the prefix of the nginx executable
func GetPrefix() string {
if nginxPrefix != "" {
return nginxPrefix
}
out := getNginxV()
prefix := extractConfigureArg(out, "--prefix")
if prefix == "" {
logger.Debug("nginx.GetPrefix len(match) < 1")
if runtime.GOOS == "windows" {
nginxPrefix = GetNginxExeDir()
} else {
nginxPrefix = "/usr/local/nginx"
}
return nginxPrefix
}
nginxPrefix = resolvePath(prefix)
return nginxPrefix
}
// GetConfPath returns the nginx configuration directory (e.g. "/etc/nginx").
// It tries to derive it from `nginx -V --conf-path=...`.
// If parsing fails, it falls back to a reasonable default instead of returning "".
func GetConfPath(dir ...string) (confPath string) {
if settings.NginxSettings.ConfigDir == "" {
out := getNginxV()
fullConf := extractConfigureArg(out, "--conf-path")
if fullConf != "" {
confPath = filepath.Dir(fullConf)
} else {
if runtime.GOOS == "windows" {
confPath = GetPrefix()
} else {
confPath = "/etc/nginx"
}
logger.Debug("nginx.GetConfPath fallback used", "base", confPath)
}
} else {
confPath = settings.NginxSettings.ConfigDir
}
confPath = resolvePath(confPath)
joined := filepath.Clean(filepath.Join(confPath, filepath.Join(dir...)))
if !helper.IsUnderDirectory(joined, confPath) {
return confPath
}
return joined
}
// GetConfEntryPath returns the absolute path to the main nginx.conf.
// It prefers the value from `nginx -V --conf-path=...`.
// If that can't be parsed, it falls back to "<confDir>/nginx.conf".
func GetConfEntryPath() (path string) {
if settings.NginxSettings.ConfigPath == "" {
out := getNginxV()
path = extractConfigureArg(out, "--conf-path")
if path == "" {
baseDir := GetConfPath()
if baseDir != "" {
path = filepath.Join(baseDir, "nginx.conf")
} else {
logger.Error("nginx.GetConfEntryPath: cannot determine nginx.conf path")
path = ""
}
}
} else {
path = settings.NginxSettings.ConfigPath
}
return resolvePath(path)
}
// GetPIDPath returns the nginx master process PID file path.
// Resolution order:
// 1. User override via settings (PIDPath)
// 2. Compile-time default from `nginx -V --pid-path=...`
// 3. Runtime override from `nginx -T` pid directive (handles nginx-unprivileged etc.)
// 4. Probing common candidate paths (Docker-aware)
func GetPIDPath() (path string) {
if settings.NginxSettings.PIDPath != "" {
return resolvePath(settings.NginxSettings.PIDPath)
}
// Try compile-time default from nginx -V
out := getNginxV()
path = extractConfigureArg(out, "--pid-path")
// When running in another container, verify the path actually exists there.
// Docker images like nginx-unprivileged override the compile-time pid-path
// at runtime via the "pid" directive in nginx.conf (e.g., pid /tmp/nginx.pid).
if path != "" && settings.NginxSettings.RunningInAnotherContainer() {
if !docker.StatPath(path) {
logger.Debug("GetPIDPath: compile-time pid-path not found in container, trying nginx -T", "path", path)
if tPath := getPIDPathFromNginxT(); tPath != "" {
return resolvePath(tPath)
}
}
}
// If nginx -V didn't provide a path, try nginx -T
if path == "" {
path = getPIDPathFromNginxT()
}
// Fallback: probe common candidate locations
if path == "" {
candidates := []string{
"/var/run/nginx.pid",
"/run/nginx.pid",
"/tmp/nginx.pid",
}
for _, c := range candidates {
if settings.NginxSettings.RunningInAnotherContainer() {
if docker.StatPath(c) {
logger.Debug("GetPIDPath fallback hit (docker)", "path", c)
path = c
break
}
} else {
if _, err := os.Stat(c); err == nil {
logger.Debug("GetPIDPath fallback hit", "path", c)
path = c
break
}
}
}
if path == "" {
logger.Error("GetPIDPath: could not determine PID path")
return ""
}
}
return resolvePath(path)
}
// GetSbinPath returns the path of the nginx executable
func GetSbinPath() (path string) {
return getNginxSbinPath()
}
// GetAccessLogPath returns the path of the nginx access log file
func GetAccessLogPath() (path string) {
path = settings.NginxSettings.AccessLogPath
if path == "" {
out := getNginxV()
path = extractConfigureArg(out, "--http-log-path")
if path != "" {
resolvedPath := resolvePath(path)
// Check if the matched path exists but is not a regular file
if !isValidRegularFile(resolvedPath) {
logger.Debug("access log path from nginx -V exists but is not a regular file, try to get from nginx -T output", "path", resolvedPath)
fallbackPath := getAccessLogPathFromNginxT()
if fallbackPath != "" {
path = fallbackPath
return path // Already resolved in getAccessLogPathFromNginxT
}
}
}
if path == "" {
logger.Debug("access log path not found in nginx -V output, try to get from nginx -T output")
path = getAccessLogPathFromNginxT()
}
}
return resolvePath(path)
}
// GetErrorLogPath returns the path of the nginx error log file
func GetErrorLogPath() string {
path := settings.NginxSettings.ErrorLogPath
if path == "" {
out := getNginxV()
path = extractConfigureArg(out, "--error-log-path")
if path != "" {
resolvedPath := resolvePath(path)
// Check if the matched path exists but is not a regular file
if !isValidRegularFile(resolvedPath) {
logger.Debug("error log path from nginx -V exists but is not a regular file, try to get from nginx -T output", "path", resolvedPath)
fallbackPath := getErrorLogPathFromNginxT()
if fallbackPath != "" {
path = fallbackPath
return path // Already resolved in getErrorLogPathFromNginxT
}
}
}
if path == "" {
logger.Debug("error log path not found in nginx -V output, try to get from nginx -T output")
path = getErrorLogPathFromNginxT()
}
}
return resolvePath(path)
}
// GetModulesPath returns the path of the nginx modules
func GetModulesPath() string {
// First try to get from nginx -V output
out := getNginxV()
if out != "" {
if path := extractConfigureArg(out, "--modules-path"); path != "" {
return resolvePath(path)
}
}
// Default path if not found
if runtime.GOOS == "windows" {
return resolvePath("modules")
}
return resolvePath("/usr/lib/nginx/modules")
}