Skip to content

Commit adb05d0

Browse files
committed
Add a ProcessManager interface for managing processes by pid file
This commit does the following: - Adds ProcessManager interface and Process struct - Read and write to a pidFile specified by the user - Adds tests for process lifecycle with a dummy process
1 parent 97c5364 commit adb05d0

File tree

2 files changed

+189
-0
lines changed

2 files changed

+189
-0
lines changed

pkg/process/process.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package process
2+
3+
import (
4+
"os"
5+
"strconv"
6+
"strings"
7+
"syscall"
8+
9+
"github.com/pkg/errors"
10+
)
11+
12+
type Manager interface {
13+
Pid() (int, error)
14+
Name() string
15+
PidFilePath() string
16+
Exists() bool
17+
Kill() error
18+
FindProcess() (*os.Process, error)
19+
WritePidFile(pid int) error
20+
}
21+
22+
type Process struct {
23+
name string
24+
pidFilePath string
25+
}
26+
27+
func New(name, pidFilePath string) (*Process, error) {
28+
return &Process{name: name, pidFilePath: pidFilePath}, nil
29+
}
30+
31+
func (p *Process) Name() string {
32+
return p.name
33+
}
34+
35+
func (p *Process) PidFilePath() string {
36+
return p.pidFilePath
37+
}
38+
39+
func (p *Process) Pid() (int, error) {
40+
data, err := os.ReadFile(p.PidFilePath())
41+
if err != nil {
42+
return -1, err
43+
}
44+
pidStr := strings.TrimSpace(string(data))
45+
pid, err := strconv.Atoi(pidStr)
46+
return pid, errors.Wrap(err, "invalid pid file")
47+
}
48+
49+
func (p *Process) FindProcess() (*os.Process, error) {
50+
pid, err := p.Pid()
51+
if err != nil {
52+
return nil, errors.Wrap(err, "cannot find process")
53+
}
54+
55+
proc, err := os.FindProcess(pid)
56+
if err != nil {
57+
return nil, errors.Wrap(err, "cannot find process")
58+
}
59+
return proc, nil
60+
}
61+
62+
func (p *Process) Exists() bool {
63+
proc, err := p.FindProcess()
64+
if err != nil {
65+
return false
66+
}
67+
if err := proc.Signal(syscall.Signal(0)); err != nil {
68+
return false
69+
}
70+
return true
71+
}
72+
73+
func (p *Process) Kill() error {
74+
proc, err := p.FindProcess()
75+
if err != nil {
76+
return errors.Wrap(err, "cannot find process")
77+
}
78+
if err := proc.Signal(syscall.SIGKILL); err != nil {
79+
return errors.Wrap(err, "cannot kill process")
80+
}
81+
_, err = proc.Wait()
82+
if err != nil {
83+
return errors.Wrap(err, "failed to wait for process termination")
84+
}
85+
return nil
86+
}
87+
88+
func (p *Process) WritePidFile(pid int) error {
89+
return os.WriteFile(p.pidFilePath, []byte(strconv.Itoa(pid)), 0600)
90+
}

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)
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, 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())
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)