Skip to content

Commit 5d1692d

Browse files
committed
Add process package
Current code contains multiple implementations for managing a process using pids, with various issues: - Some are unsafe, terminating a process by pid without validating that the pid belongs to the right process. Some use unclear - Using unclear terms like checkPid() (what does it mean?) - Some are missing tests Lets clean up the mess by introducing a process package. The package provides: - process.WritePidfile(): write a pid to file - process.ReadPidfile(): read pid from file - process.Exists(): tells if process exists matching pid and name - process.Signal() send a signal to process matching pid and name - process.Kil() kill a process matching pid and name I want to use this package in vfkit and the new vment package, and later we can use it for qemu, hyperkit, and other code using managing processes with pids.
1 parent 8ed8f15 commit 5d1692d

File tree

4 files changed

+417
-0
lines changed

4 files changed

+417
-0
lines changed

pkg/minikube/process/process.go

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package process
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"strconv"
23+
"strings"
24+
"syscall"
25+
26+
"github.com/mitchellh/go-ps"
27+
)
28+
29+
const pidfileMode = 0o600
30+
31+
// WritePidfile writes pid to path.
32+
func WritePidfile(path string, pid int) error {
33+
data := fmt.Sprintf("%d", pid)
34+
return os.WriteFile(path, []byte(data), pidfileMode)
35+
}
36+
37+
// ReadPid reads a pid from path.
38+
func ReadPidfile(path string) (int, error) {
39+
data, err := os.ReadFile(path)
40+
if err != nil {
41+
return -1, err
42+
}
43+
s := strings.TrimSpace(string(data))
44+
pid, err := strconv.Atoi(s)
45+
if err != nil {
46+
return -1, err
47+
}
48+
return pid, nil
49+
}
50+
51+
// Exists tells if a process with pid and executable name exist. Executable is
52+
// not the path to the executable.
53+
func Exists(pid int, executable string) (bool, error) {
54+
// Fast path if pid does not exist.
55+
process, err := os.FindProcess(pid)
56+
if err != nil {
57+
return true, err
58+
}
59+
if err := process.Signal(syscall.Signal(0)); err != nil {
60+
if err == os.ErrProcessDone {
61+
return false, nil
62+
}
63+
}
64+
// Slow path, if pid exist.
65+
entry, err := ps.FindProcess(pid)
66+
if err != nil {
67+
return true, err
68+
}
69+
if entry == nil {
70+
return false, nil
71+
}
72+
return entry.Executable() == executable, nil
73+
}
74+
75+
// Signal sends signal to the process with pid matching name. Returns
76+
// os.ErrProcessDone if the process does not exist, or nil if the signal was
77+
// sent.
78+
func Signal(pid int, executable string, sig syscall.Signal) error {
79+
exists, err := Exists(pid, executable)
80+
if err != nil {
81+
return err
82+
}
83+
if !exists {
84+
return os.ErrProcessDone
85+
}
86+
p, err := os.FindProcess(pid)
87+
if err != nil {
88+
return err
89+
}
90+
// Returns os.ErrProcessDone if process does not exist (ESRCH).
91+
return p.Signal(sig)
92+
}
93+
94+
// Terminate kills a process with pid matching executable name. Returns
95+
// os.ErrProcessDone if the process does not exist or nil the kill was
96+
// requested.
97+
func Kill(pid int, executable string) error {
98+
exists, err := Exists(pid, executable)
99+
if err != nil {
100+
return err
101+
}
102+
if !exists {
103+
return os.ErrProcessDone
104+
}
105+
p, err := os.FindProcess(pid)
106+
if err != nil {
107+
return err
108+
}
109+
// Returns os.ErrProcessDone if process does not exist (ESRCH).
110+
return p.Kill()
111+
}

pkg/minikube/process/process_test.go

+249
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package process_test
18+
19+
import (
20+
"bufio"
21+
"errors"
22+
"fmt"
23+
"os"
24+
"os/exec"
25+
"path/filepath"
26+
"runtime"
27+
"strings"
28+
"syscall"
29+
"testing"
30+
"time"
31+
32+
"k8s.io/minikube/pkg/minikube/process"
33+
)
34+
35+
func TestPidfile(t *testing.T) {
36+
pidfile := filepath.Join(t.TempDir(), "pid")
37+
if err := process.WritePidfile(pidfile, 42); err != nil {
38+
t.Fatal(err)
39+
}
40+
pid, err := process.ReadPidfile(pidfile)
41+
if err != nil {
42+
t.Fatal(err)
43+
}
44+
if pid != 42 {
45+
t.Fatalf("expected 42, got %d", pid)
46+
}
47+
}
48+
49+
func TestPidfileMissing(t *testing.T) {
50+
pidfile := filepath.Join(t.TempDir(), "pid")
51+
_, err := process.ReadPidfile(pidfile)
52+
if !errors.Is(err, os.ErrNotExist) {
53+
t.Fatalf("unexpected error: %s", err)
54+
}
55+
}
56+
57+
func TestPidfileInvalid(t *testing.T) {
58+
pidfile := filepath.Join(t.TempDir(), "pid")
59+
if err := os.WriteFile(pidfile, []byte("invalid"), 0o600); err != nil {
60+
t.Fatal(err)
61+
}
62+
_, err := process.ReadPidfile(pidfile)
63+
if err == nil {
64+
t.Fatal("parsing invalid pidfile did not fail")
65+
}
66+
}
67+
68+
func TestProcess(t *testing.T) {
69+
sleep, err := build(t, "sleep")
70+
if err != nil {
71+
t.Fatal(err)
72+
}
73+
74+
noterm, err := build(t, "noterm")
75+
if err != nil {
76+
t.Fatal(err)
77+
}
78+
79+
t.Run("exists", func(t *testing.T) {
80+
cmd := startProcess(t, sleep)
81+
defer cmd.Process.Kill()
82+
exists, err := process.Exists(cmd.Process.Pid, executable(sleep))
83+
if err != nil {
84+
t.Fatal(err)
85+
}
86+
if !exists {
87+
t.Fatal("existing process not found")
88+
}
89+
exists, err = process.Exists(-1, executable(sleep))
90+
if err != nil {
91+
t.Fatal(err)
92+
}
93+
if exists {
94+
t.Fatal("process with non-existing pid found")
95+
}
96+
exists, err = process.Exists(cmd.Process.Pid, "no-such-executable")
97+
if err != nil {
98+
t.Fatal(err)
99+
}
100+
if exists {
101+
t.Fatal("process with non-existing executable found")
102+
}
103+
})
104+
105+
t.Run("terminate", func(t *testing.T) {
106+
cmd := startProcess(t, sleep)
107+
defer cmd.Process.Kill()
108+
err := process.Signal(cmd.Process.Pid, executable(sleep), syscall.SIGTERM)
109+
if err != nil {
110+
t.Fatal(err)
111+
}
112+
waitForTermination(t, cmd)
113+
114+
})
115+
116+
t.Run("terminate name mismatch", func(t *testing.T) {
117+
cmd := startProcess(t, sleep)
118+
defer cmd.Process.Kill()
119+
err := process.Signal(cmd.Process.Pid, "no-such-process", syscall.SIGTERM)
120+
if err == nil {
121+
t.Fatalf("Signaled unrelated process")
122+
}
123+
if err != os.ErrProcessDone {
124+
t.Fatalf("Unexpected error: %s", err)
125+
}
126+
})
127+
128+
t.Run("terminate ignored", func(t *testing.T) {
129+
cmd := startProcess(t, noterm)
130+
defer cmd.Process.Kill()
131+
err := process.Signal(cmd.Process.Pid, executable(noterm), syscall.SIGTERM)
132+
if err != nil {
133+
t.Fatal(err)
134+
}
135+
time.Sleep(200 * time.Millisecond)
136+
exists, err := process.Exists(cmd.Process.Pid, executable(noterm))
137+
if err != nil {
138+
t.Fatal(err)
139+
}
140+
if !exists {
141+
t.Fatalf("process terminated")
142+
}
143+
})
144+
145+
t.Run("kill", func(t *testing.T) {
146+
cmd := startProcess(t, noterm)
147+
defer cmd.Process.Kill()
148+
err := process.Kill(cmd.Process.Pid, executable(noterm))
149+
if err != nil {
150+
t.Fatal(err)
151+
}
152+
waitForTermination(t, cmd)
153+
})
154+
155+
t.Run("kill name mismatch", func(t *testing.T) {
156+
cmd := startProcess(t, noterm)
157+
defer cmd.Process.Kill()
158+
err := process.Kill(cmd.Process.Pid, "no-such-process")
159+
if err == nil {
160+
t.Fatalf("Killed unrelated process")
161+
}
162+
if err != os.ErrProcessDone {
163+
t.Fatalf("Unexpected error: %s", err)
164+
}
165+
})
166+
}
167+
168+
func startProcess(t *testing.T, cmd string) *exec.Cmd {
169+
c := exec.Command(cmd)
170+
stdout, err := c.StdoutPipe()
171+
if err != nil {
172+
t.Fatal(err)
173+
}
174+
defer stdout.Close()
175+
176+
start := time.Now()
177+
if err := c.Start(); err != nil {
178+
t.Fatal(err)
179+
}
180+
t.Logf("Started process %q (pid=%v)", executable(cmd), c.Process.Pid)
181+
182+
// Synchronize with the process to ensure it set up signal handlers before
183+
// we send a signal.
184+
r := bufio.NewReader(stdout)
185+
line, err := r.ReadString('\n')
186+
if err != nil {
187+
c.Process.Kill()
188+
c.Wait()
189+
t.Fatal(err)
190+
}
191+
if line != "READY\n" {
192+
c.Process.Kill()
193+
c.Wait()
194+
t.Fatalf("Unexpected response: %q", line)
195+
}
196+
t.Logf("Process %q ready in %.6f seconds", executable(cmd), time.Since(start).Seconds())
197+
198+
// We must reap the process for testing termination. Otherwise it will
199+
// become a zombie and Exists() will return true after termination.
200+
go c.Wait()
201+
202+
return c
203+
}
204+
205+
func waitForTermination(t *testing.T, cmd *exec.Cmd) {
206+
timeout := 200 * time.Millisecond
207+
start := time.Now()
208+
for {
209+
exists, err := process.Exists(cmd.Process.Pid, executable(cmd.Path))
210+
if err != nil {
211+
t.Fatal(err)
212+
}
213+
if !exists {
214+
t.Logf("Process %q terminated in %.6f seconds", executable(cmd.Path), time.Since(start).Seconds())
215+
break
216+
}
217+
if time.Since(start) > timeout {
218+
t.Fatalf("Timeout waiting for %q", cmd)
219+
}
220+
time.Sleep(10 * time.Millisecond)
221+
}
222+
}
223+
224+
func build(t *testing.T, name string) (string, error) {
225+
source := fmt.Sprintf("testdata/%s.go", name)
226+
227+
out := filepath.Join(t.TempDir(), name)
228+
if runtime.GOOS == "windows" {
229+
out += ".exe"
230+
}
231+
232+
t.Logf("Building %q", name)
233+
cmd := exec.Command("go", "build", "-o", out, source)
234+
cmd.Stdout = os.Stdout
235+
cmd.Stderr = os.Stderr
236+
237+
if err := cmd.Run(); err != nil {
238+
return "", err
239+
}
240+
return out, nil
241+
}
242+
243+
func executable(s string) string {
244+
base := filepath.Base(s)
245+
if strings.HasSuffix(base, ".exe") {
246+
return base[:len(base)-4]
247+
}
248+
return base
249+
}
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"fmt"
21+
"os/signal"
22+
"syscall"
23+
"time"
24+
)
25+
26+
func main() {
27+
signal.Ignore(syscall.SIGTERM)
28+
fmt.Println("READY") // Synchronize with parent
29+
time.Sleep(10 * time.Second)
30+
}

0 commit comments

Comments
 (0)