mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2026-06-19 07:36:59 +00:00
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:
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user