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.
This commit is contained in:
0xJacky
2026-02-08 12:23:50 +00:00
parent 72932f4a6c
commit 0649cf786f
3 changed files with 242 additions and 19 deletions
+43 -1
View File
@@ -9,7 +9,7 @@ import (
"github.com/uozi-tech/cosy/logger"
)
// Regular expressions for parsing log directives from nginx -T output
// Regular expressions for parsing directives from nginx -T output
const (
// AccessLogRegexPattern matches access_log directive with unquoted path
// Matches: access_log /path/to/file
@@ -18,16 +18,22 @@ const (
// ErrorLogRegexPattern matches error_log directive with unquoted path
// Matches: error_log /path/to/file
ErrorLogRegexPattern = `(?m)^\s*error_log\s+([^\s;]+)`
// PIDRegexPattern matches pid directive with unquoted path
// Matches: pid /path/to/file;
PIDRegexPattern = `(?m)^\s*pid\s+([^\s;]+)`
)
var (
accessLogRegex *regexp.Regexp
errorLogRegex *regexp.Regexp
pidRegex *regexp.Regexp
)
func init() {
accessLogRegex = regexp.MustCompile(AccessLogRegexPattern)
errorLogRegex = regexp.MustCompile(ErrorLogRegexPattern)
pidRegex = regexp.MustCompile(PIDRegexPattern)
}
// isValidRegularFile checks if the given path is a valid regular file
@@ -141,3 +147,39 @@ func getErrorLogPathFromNginxT() string {
logger.Error("nginx.getErrorLogPathFromNginxT: no valid error_log file found")
return ""
}
// getPIDPathFromNginxT extracts the pid file path from nginx -T output.
// This is needed because nginx -V returns the compile-time default --pid-path,
// but Docker images like nginx-unprivileged override this at runtime via the
// "pid" directive in nginx.conf (e.g., pid /tmp/nginx.pid;).
func getPIDPathFromNginxT() string {
output := getNginxT()
if output == "" {
logger.Error("nginx.getPIDPathFromNginxT: nginx -T output is empty")
return ""
}
lines := strings.Split(output, "\n")
for _, line := range lines {
// Skip commented lines
if isCommentedLine(line) {
continue
}
matches := pidRegex.FindStringSubmatch(line)
if len(matches) >= 2 {
pidPath := matches[1]
// Handle relative paths
if !filepath.IsAbs(pidPath) {
pidPath = filepath.Join(GetPrefix(), pidPath)
}
return resolvePath(pidPath)
}
}
logger.Debug("nginx.getPIDPathFromNginxT: no pid directive found in nginx -T output")
return ""
}
+150
View File
@@ -637,3 +637,153 @@ http {
}
}
}
func TestPIDRegexParsing(t *testing.T) {
testCases := []struct {
name string
nginxTOutput string
expectedPath string
shouldMatch bool
}{
{
name: "standard pid path",
nginxTOutput: "pid /var/run/nginx.pid;",
expectedPath: "/var/run/nginx.pid",
shouldMatch: true,
},
{
name: "nginx-unprivileged pid path",
nginxTOutput: "pid /tmp/nginx.pid;",
expectedPath: "/tmp/nginx.pid",
shouldMatch: true,
},
{
name: "indented pid directive",
nginxTOutput: " pid /run/nginx.pid;",
expectedPath: "/run/nginx.pid",
shouldMatch: true,
},
{
name: "no pid directive",
nginxTOutput: "worker_processes auto;",
expectedPath: "",
shouldMatch: false,
},
{
name: "commented pid directive should not match",
nginxTOutput: "# pid /var/run/nginx.pid;",
expectedPath: "",
shouldMatch: false,
},
{
name: "pid in full config",
nginxTOutput: "user nginx;\nworker_processes auto;\npid /tmp/nginx.pid;\nevents {\n worker_connections 1024;\n}",
expectedPath: "/tmp/nginx.pid",
shouldMatch: true,
},
{
name: "commented pid followed by real pid",
nginxTOutput: "# pid /var/run/nginx.pid;\npid /tmp/nginx.pid;",
expectedPath: "/tmp/nginx.pid",
shouldMatch: true,
},
}
pidRegex := regexp.MustCompile(PIDRegexPattern)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Filter out commented lines (same as getPIDPathFromNginxT does)
var firstMatch string
for _, line := range regexp.MustCompile(`\n`).Split(tc.nginxTOutput, -1) {
if isCommentedLine(line) {
continue
}
matches := pidRegex.FindStringSubmatch(line)
if len(matches) >= 2 {
firstMatch = matches[1]
break
}
}
if tc.shouldMatch {
if firstMatch == "" {
t.Errorf("Expected to find pid directive, but found none")
return
}
if firstMatch != tc.expectedPath {
t.Errorf("Expected pid path %s, got %s", tc.expectedPath, firstMatch)
}
} else {
if firstMatch != "" {
t.Errorf("Expected no pid directive, but found: %s", firstMatch)
}
}
})
}
}
func TestPIDPathFromMockConfigs(t *testing.T) {
pidRegex := regexp.MustCompile(PIDRegexPattern)
testCases := []struct {
name string
config string
expectedPath string
}{
{
name: "standard nginx config",
config: mockNginxTOutput,
expectedPath: "/var/run/nginx.pid",
},
{
name: "config with relative paths",
config: mockNginxTOutputRelative,
expectedPath: "/var/run/nginx.pid",
},
{
name: "nginx-unprivileged config",
config: `
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /tmp/nginx.pid;
events {
worker_connections 1024;
}
http {
access_log /var/log/nginx/access.log main;
}
`,
expectedPath: "/tmp/nginx.pid",
},
{
name: "config without pid directive",
config: mockNginxTOutputOff,
expectedPath: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var foundPath string
for _, line := range regexp.MustCompile(`\n`).Split(tc.config, -1) {
if isCommentedLine(line) {
continue
}
matches := pidRegex.FindStringSubmatch(line)
if len(matches) >= 2 {
foundPath = matches[1]
break
}
}
if foundPath != tc.expectedPath {
t.Errorf("Expected pid path %q, got %q", tc.expectedPath, foundPath)
}
})
}
}
+49 -18
View File
@@ -6,6 +6,7 @@ import (
"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"
@@ -161,35 +162,65 @@ func GetConfEntryPath() (path string) {
}
// GetPIDPath returns the nginx master process PID file path.
// We try to read it from `nginx -V --pid-path=...`.
// If that fails (which often happens in container images), we probe common
// locations like /run/nginx.pid and /var/run/nginx.pid instead of just failing.
// 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 == "" {
out := getNginxV()
path = extractConfigureArg(out, "--pid-path")
if settings.NginxSettings.PIDPath != "" {
return resolvePath(settings.NginxSettings.PIDPath)
}
if path == "" {
candidates := []string{
"/var/run/nginx.pid",
"/run/nginx.pid",
// 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)
}
}
}
for _, c := range candidates {
// 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 ""
}
}
} else {
path = settings.NginxSettings.PIDPath
if path == "" {
logger.Error("GetPIDPath: could not determine PID path")
return ""
}
}
return resolvePath(path)