Skip to content

Commit 5d6ee02

Browse files
committed
Add a ProcessManager interface for managing processes by pid file
This commit adds a process package with Manager interface and Process struct The interface has the following methods: - ReadPidFile() (int, error) - Name() string - PidFilePath() string - Exists() bool - Terminate() error - Kill() error - FindProcess() (*os.Process, error) - WritePidFile(pid int) error The commit also adds tests for process lifecycle with a dummy process
1 parent 97c5364 commit 5d6ee02

File tree

2 files changed

+218
-0
lines changed

2 files changed

+218
-0
lines changed

pkg/process/process.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package process
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strconv"
7+
"strings"
8+
9+
"github.com/shirou/gopsutil/v4/process"
10+
)
11+
12+
type Manager interface {
13+
ReadPidFile() (int, error)
14+
Name() string
15+
PidFilePath() string
16+
Exists() bool
17+
Terminate() error
18+
Kill() error
19+
FindProcess() (*os.Process, error)
20+
WritePidFile(pid int) error
21+
}
22+
23+
type Process struct {
24+
name string
25+
pidFilePath string
26+
executablePath string
27+
}
28+
29+
func New(name, pidFilePath, executablePath string) (*Process, error) {
30+
return &Process{name: name, pidFilePath: pidFilePath, executablePath: executablePath}, nil
31+
}
32+
33+
func (p *Process) Name() string {
34+
return p.name
35+
}
36+
37+
func (p *Process) PidFilePath() string {
38+
return p.pidFilePath
39+
}
40+
41+
func (p *Process) ExecutablePath() string {
42+
return p.executablePath
43+
}
44+
45+
func (p *Process) ReadPidFile() (int, error) {
46+
data, err := os.ReadFile(p.PidFilePath())
47+
if err != nil {
48+
return -1, err
49+
}
50+
pidStr := strings.TrimSpace(string(data))
51+
pid, err := strconv.Atoi(pidStr)
52+
if err != nil {
53+
return -1, fmt.Errorf("invalid pid file: %v", err)
54+
}
55+
return pid, nil
56+
}
57+
58+
func (p *Process) FindProcess() (*process.Process, error) {
59+
pid, err := p.ReadPidFile()
60+
if err != nil {
61+
return nil, fmt.Errorf("cannot find process: %v", err)
62+
}
63+
64+
exists, err := process.PidExists(int32(pid))
65+
if err != nil {
66+
return nil, err
67+
}
68+
if !exists {
69+
return nil, fmt.Errorf("process not found")
70+
}
71+
72+
proc, err := process.NewProcess(int32(pid))
73+
if err != nil {
74+
return nil, fmt.Errorf("cannot find process: %v", err)
75+
}
76+
if proc == nil {
77+
return nil, fmt.Errorf("process not found")
78+
}
79+
name, err := proc.Name()
80+
if err != nil {
81+
return nil, fmt.Errorf("cannot find process name: %v", err)
82+
}
83+
if name != p.Name() {
84+
return nil, fmt.Errorf("pid %d is stale, and is being used by %s", pid, name)
85+
}
86+
exe, err := proc.Exe()
87+
if err != nil {
88+
return nil, fmt.Errorf("cannot find process exe: %v", err)
89+
}
90+
if exe != p.ExecutablePath() {
91+
return nil, fmt.Errorf("pid %d is stale, and is being used by %s", pid, exe)
92+
}
93+
return proc, nil
94+
}
95+
96+
func (p *Process) Exists() bool {
97+
proc, err := p.FindProcess()
98+
return err == nil && proc != nil
99+
}
100+
101+
func (p *Process) Terminate() error {
102+
proc, err := p.FindProcess()
103+
if err != nil {
104+
return fmt.Errorf("cannot find process: %v", err)
105+
}
106+
return proc.Terminate()
107+
}
108+
109+
func (p *Process) Kill() error {
110+
proc, err := p.FindProcess()
111+
if err != nil {
112+
return fmt.Errorf("cannot find process: %v", err)
113+
}
114+
return proc.Kill()
115+
}
116+
117+
func (p *Process) WritePidFile(pid int) error {
118+
return os.WriteFile(p.pidFilePath, []byte(strconv.Itoa(pid)), 0600)
119+
}

pkg/process/process_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package process
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
const (
15+
dummyProcessName = "sleep"
16+
dummyProcessArgs = "60"
17+
)
18+
19+
var (
20+
dummyProcess *exec.Cmd
21+
managedProcess *Process
22+
pidFilePath = filepath.Join(os.TempDir(), "pid")
23+
)
24+
25+
func startDummyProcess() error {
26+
dummyProcess = exec.Command(dummyProcessName, dummyProcessArgs)
27+
err := dummyProcess.Start()
28+
if err != nil {
29+
return err
30+
}
31+
return nil
32+
}
33+
34+
func TestMain(m *testing.M) {
35+
err := startDummyProcess()
36+
if err != nil {
37+
fmt.Fprintln(os.Stderr, "Failed to start process:", err)
38+
os.Exit(1)
39+
}
40+
41+
managedProcess, err = New(dummyProcessName, pidFilePath, dummyProcess.Path)
42+
if err != nil {
43+
fmt.Fprintln(os.Stderr, "Failed to create process:", err)
44+
os.Exit(1)
45+
}
46+
err = managedProcess.WritePidFile(dummyProcess.Process.Pid)
47+
if err != nil {
48+
fmt.Fprintln(os.Stderr, err)
49+
os.Exit(1)
50+
}
51+
exitCode := m.Run()
52+
if dummyProcess.Process != nil {
53+
_ = dummyProcess.Process.Kill()
54+
}
55+
56+
os.Exit(exitCode)
57+
}
58+
59+
func TestProcess_Name(t *testing.T) {
60+
assert.Equal(t, dummyProcessName, managedProcess.Name())
61+
}
62+
63+
func TestProcess_FindProcess(t *testing.T) {
64+
foundProcess, err := managedProcess.FindProcess()
65+
assert.NoError(t, err)
66+
assert.NotNil(t, foundProcess)
67+
assert.Equal(t, dummyProcess.Process.Pid, int(foundProcess.Pid))
68+
69+
assert.True(t, managedProcess.Exists())
70+
}
71+
72+
func TestProcess_KillProcess(t *testing.T) {
73+
err := managedProcess.Kill()
74+
assert.NoError(t, err)
75+
assert.False(t, managedProcess.Exists())
76+
77+
// Try to kill the non-existent process
78+
// This should result in an error
79+
err = managedProcess.Kill()
80+
assert.Error(t, err)
81+
}
82+
83+
func TestProcess_FindProcess_InvalidPidFile(t *testing.T) {
84+
tmpfile, err := os.CreateTemp("", "invalid_pid")
85+
require.NoError(t, err)
86+
defer os.Remove(tmpfile.Name())
87+
88+
// Write non-numeric content into the file to mimic an invalid pid
89+
_, err = tmpfile.WriteString("non-numeric")
90+
require.NoError(t, err)
91+
tmpfile.Close()
92+
93+
invalidProcess, err := New("invalid-process", tmpfile.Name(), "invalid-path")
94+
assert.NoError(t, err)
95+
96+
foundProcess, err := invalidProcess.FindProcess()
97+
assert.Error(t, err)
98+
assert.Nil(t, foundProcess)
99+
}

0 commit comments

Comments
 (0)