Skip to content

Commit 1c4a96e

Browse files
committed
check workers parent id
1 parent eb8d868 commit 1c4a96e

File tree

6 files changed

+363
-259
lines changed

6 files changed

+363
-259
lines changed
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// Copyright (c) F5, Inc.
2+
//
3+
// This source code is licensed under the Apache License, Version 2.0 license found in the
4+
// LICENSE file in the root directory of this source tree.
5+
6+
package nginx
7+
8+
import (
9+
"bufio"
10+
"bytes"
11+
"context"
12+
"fmt"
13+
"log/slog"
14+
"path"
15+
"regexp"
16+
"strings"
17+
18+
"github.com/nginx/agent/v3/internal/datasource/host/exec"
19+
"github.com/nginx/agent/v3/internal/model"
20+
"github.com/nginx/agent/v3/pkg/nginxprocess"
21+
)
22+
23+
const (
24+
keyValueLen = 2
25+
flagLen = 1
26+
)
27+
28+
var versionRegex = regexp.MustCompile(`(?P<name>\S+)\/(?P<version>.*)`)
29+
30+
func ProcessInfo(ctx context.Context, proc *nginxprocess.Process,
31+
executer exec.ExecInterface,
32+
) (*model.ProcessInfo, error) {
33+
exePath := proc.Exe
34+
35+
if exePath == "" {
36+
exePath = Exe(ctx, executer)
37+
if exePath == "" {
38+
return nil, fmt.Errorf("unable to find NGINX exe for process %d", proc.PID)
39+
}
40+
}
41+
42+
confPath := ConfPathFromCommand(proc.Cmd)
43+
44+
var nginxInfo *model.ProcessInfo
45+
46+
outputBuffer, err := executer.RunCmd(ctx, exePath, "-V")
47+
if err != nil {
48+
return nil, err
49+
}
50+
51+
nginxInfo = ParseNginxVersionCommandOutput(ctx, outputBuffer)
52+
53+
nginxInfo.ExePath = exePath
54+
nginxInfo.ProcessID = proc.PID
55+
56+
if nginxInfo.ConfPath = NginxConfPath(ctx, nginxInfo); confPath != "" {
57+
nginxInfo.ConfPath = confPath
58+
}
59+
60+
return nginxInfo, err
61+
}
62+
63+
func Exe(ctx context.Context, executer exec.ExecInterface) string {
64+
exePath := ""
65+
66+
out, commandErr := executer.RunCmd(ctx, "sh", "-c", "command -v nginx")
67+
if commandErr == nil {
68+
exePath = strings.TrimSuffix(out.String(), "\n")
69+
}
70+
71+
if exePath == "" {
72+
exePath = defaultToNginxCommandForProcessPath(executer)
73+
}
74+
75+
if strings.Contains(exePath, "(deleted)") {
76+
exePath = sanitizeExeDeletedPath(exePath)
77+
}
78+
79+
return exePath
80+
}
81+
82+
func defaultToNginxCommandForProcessPath(executer exec.ExecInterface) string {
83+
exePath, err := executer.FindExecutable("nginx")
84+
if err != nil {
85+
return ""
86+
}
87+
88+
return exePath
89+
}
90+
91+
func sanitizeExeDeletedPath(exe string) string {
92+
firstSpace := strings.Index(exe, "(deleted)")
93+
if firstSpace != -1 {
94+
return strings.TrimSpace(exe[0:firstSpace])
95+
}
96+
97+
return strings.TrimSpace(exe)
98+
}
99+
100+
func ConfPathFromCommand(command string) string {
101+
commands := strings.Split(command, " ")
102+
103+
for i, command := range commands {
104+
if command == "-c" {
105+
if i < len(commands)-1 {
106+
return commands[i+1]
107+
}
108+
}
109+
}
110+
111+
return ""
112+
}
113+
114+
func NginxConfPath(ctx context.Context, nginxInfo *model.ProcessInfo) string {
115+
var confPath string
116+
117+
if nginxInfo.ConfigureArgs["conf-path"] != nil {
118+
var ok bool
119+
confPath, ok = nginxInfo.ConfigureArgs["conf-path"].(string)
120+
if !ok {
121+
slog.DebugContext(ctx, "failed to cast nginxInfo conf-path to string")
122+
}
123+
} else {
124+
confPath = path.Join(nginxInfo.Prefix, "/conf/nginx.conf")
125+
}
126+
127+
return confPath
128+
}
129+
130+
func ParseNginxVersionCommandOutput(ctx context.Context, output *bytes.Buffer) *model.ProcessInfo {
131+
nginxInfo := &model.ProcessInfo{}
132+
133+
scanner := bufio.NewScanner(output)
134+
for scanner.Scan() {
135+
line := strings.TrimSpace(scanner.Text())
136+
switch {
137+
case strings.HasPrefix(line, "nginx version"):
138+
nginxInfo.Version = parseNginxVersion(line)
139+
case strings.HasPrefix(line, "configure arguments"):
140+
nginxInfo.ConfigureArgs = parseConfigureArguments(line)
141+
}
142+
}
143+
144+
nginxInfo.Prefix = nginxPrefix(ctx, nginxInfo)
145+
146+
return nginxInfo
147+
}
148+
149+
func parseNginxVersion(line string) string {
150+
return strings.TrimPrefix(versionRegex.FindString(line), "nginx/")
151+
}
152+
153+
func parseConfigureArguments(line string) map[string]interface{} {
154+
// need to check for empty strings
155+
flags := strings.Split(line[len("configure arguments:"):], " --")
156+
result := make(map[string]interface{})
157+
158+
for _, flag := range flags {
159+
vals := strings.Split(flag, "=")
160+
if isFlag(vals) {
161+
result[vals[0]] = true
162+
} else if isKeyValueFlag(vals) {
163+
result[vals[0]] = vals[1]
164+
}
165+
}
166+
167+
return result
168+
}
169+
170+
func nginxPrefix(ctx context.Context, nginxInfo *model.ProcessInfo) string {
171+
var prefix string
172+
173+
if nginxInfo.ConfigureArgs["prefix"] != nil {
174+
var ok bool
175+
prefix, ok = nginxInfo.ConfigureArgs["prefix"].(string)
176+
if !ok {
177+
slog.DebugContext(ctx, "Failed to cast nginxInfo prefix to string")
178+
}
179+
} else {
180+
prefix = "/usr/local/nginx"
181+
}
182+
183+
return prefix
184+
}
185+
186+
func isFlag(vals []string) bool {
187+
return len(vals) == flagLen && vals[0] != ""
188+
}
189+
190+
func isKeyValueFlag(vals []string) bool {
191+
return len(vals) == keyValueLen
192+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) F5, Inc.
2+
//
3+
// This source code is licensed under the Apache License, Version 2.0 license found in the
4+
// LICENSE file in the root directory of this source tree.
5+
6+
package nginx
7+
8+
import (
9+
"bytes"
10+
"context"
11+
"errors"
12+
"testing"
13+
14+
"github.com/nginx/agent/v3/internal/datasource/host/exec/execfakes"
15+
"github.com/stretchr/testify/assert"
16+
)
17+
18+
func TestGetConfigPathFromCommand(t *testing.T) {
19+
result := ConfPathFromCommand("nginx: master process nginx -c /tmp/nginx.conf")
20+
assert.Equal(t, "/tmp/nginx.conf", result)
21+
22+
result = ConfPathFromCommand("nginx: master process nginx -c")
23+
assert.Empty(t, result)
24+
25+
result = ConfPathFromCommand("")
26+
assert.Empty(t, result)
27+
}
28+
29+
func TestNginxProcessParser_GetExe(t *testing.T) {
30+
ctx := context.Background()
31+
32+
tests := []struct {
33+
commandError error
34+
name string
35+
expected string
36+
commandOutput []byte
37+
}{
38+
{
39+
name: "Test 1: Default exe if error executing command -v nginx",
40+
commandOutput: []byte{},
41+
commandError: errors.New("command error"),
42+
expected: "/usr/bin/nginx",
43+
},
44+
{
45+
name: "Test 2: Sanitize Exe Deleted Path",
46+
commandOutput: []byte("/usr/sbin/nginx (deleted)"),
47+
commandError: nil,
48+
expected: "/usr/sbin/nginx",
49+
},
50+
}
51+
for _, test := range tests {
52+
t.Run(test.name, func(tt *testing.T) {
53+
mockExec := &execfakes.FakeExecInterface{}
54+
mockExec.RunCmdReturns(bytes.NewBuffer(test.commandOutput), test.commandError)
55+
mockExec.FindExecutableReturns("/usr/bin/nginx", nil)
56+
57+
result := Exe(ctx, mockExec)
58+
59+
assert.Equal(tt, test.expected, result)
60+
})
61+
}
62+
}

internal/model/process.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) F5, Inc.
2+
//
3+
// This source code is licensed under the Apache License, Version 2.0 license found in the
4+
// LICENSE file in the root directory of this source tree.
5+
6+
package model
7+
8+
type ProcessInfo struct {
9+
ConfigureArgs map[string]interface{}
10+
Version string
11+
Prefix string
12+
ConfPath string
13+
ExePath string
14+
LoadableModules []string
15+
DynamicModules []string
16+
ProcessID int32
17+
}

0 commit comments

Comments
 (0)