Repository: kubernetes/minikube
Pull request #20590 (2025-04-02 00:04:54)

Update Yearly Leaderboard
Committing changes resulting from `make update-yearly-leaderboard`.
This PR is auto-generated by the [update-yearly-leaderboard](https://github.com/kubernetes/minikube/blob/master/.github/workflows/yearly-leaderboard.yml) CI workflow.

k8s-ci-robot: Hi @minikube-bot. Thanks for your PR.

I'm waiting for a [kubernetes](https://github.com/orgs/kubernetes/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://git.k8s.io/community/community-membership.md#member) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Fminikube).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>


Patch:
diff --git a/pkg/drivers/vfkit/vfkit.go b/pkg/drivers/vfkit/vfkit.go
index b27118067..de1749fb2 100644
--- a/pkg/drivers/vfkit/vfkit.go
+++ b/pkg/drivers/vfkit/vfkit.go
@@ -30,7 +30,6 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
-	"strconv"
 	"strings"
 	"syscall"
 	"time"
@@ -47,6 +46,7 @@ import (
 	"k8s.io/minikube/pkg/minikube/exit"
 	"k8s.io/minikube/pkg/minikube/firewall"
 	"k8s.io/minikube/pkg/minikube/out"
+	"k8s.io/minikube/pkg/minikube/process"
 	"k8s.io/minikube/pkg/minikube/reason"
 	"k8s.io/minikube/pkg/minikube/style"
 )
@@ -136,42 +136,27 @@ func (d *Driver) GetIP() (string, error) {
 	return d.IPAddress, nil
 }
 
-func checkPid(pid int) error {
-	process, err := os.FindProcess(pid)
-	if err != nil {
-		return err
-	}
-	return process.Signal(syscall.Signal(0))
-}
-
 func (d *Driver) GetState() (state.State, error) {
-	if _, err := os.Stat(d.pidfilePath()); err != nil {
-		return state.Stopped, nil
-	}
-	p, err := os.ReadFile(d.pidfilePath())
-	if err != nil {
-		return state.Error, err
-	}
-	pid, err := strconv.Atoi(strings.TrimSpace(string(p)))
+	pidfile := d.pidfilePath()
+	pid, err := process.ReadPidfile(pidfile)
 	if err != nil {
-		return state.Error, err
-	}
-	if err := checkPid(pid); err != nil {
-		// No pid, remove pidfile
-		os.Remove(d.pidfilePath())
+		if !errors.Is(err, os.ErrNotExist) {
+			return state.Error, err
+		}
 		return state.Stopped, nil
 	}
-	ret, err := d.GetVFKitState()
+	exists, err := process.Exists(pid, "vfkit")
 	if err != nil {
 		return state.Error, err
 	}
-	switch ret {
-	case "running", "VirtualMachineStateRunning":
-		return state.Running, nil
-	case "stopped", "VirtualMachineStateStopped":
+	if !exists {
+		// No process, stale pidfile.
+		if err := os.Remove(pidfile); err != nil {
+			log.Debugf("failed to remove %q: %s", pidfile, err)
+		}
 		return state.Stopped, nil
 	}
-	return state.None, nil
+	return state.Running, nil
 }
 
 func (d *Driver) Create() error {
@@ -260,11 +245,9 @@ func (d *Driver) Start() error {
 	if err := cmd.Start(); err != nil {
 		return err
 	}
-	pid := cmd.Process.Pid
-	if err := os.WriteFile(d.pidfilePath(), []byte(fmt.Sprintf("%v", pid)), 0600); err != nil {
+	if err := process.WritePidfile(d.pidfilePath(), cmd.Process.Pid); err != nil {
 		return err
 	}
-
 	if err := d.setupIP(mac); err != nil {
 		return err
 	}
@@ -314,7 +297,29 @@ func isBootpdError(err error) bool {
 
 func (d *Driver) Stop() error {
 	if err := d.SetVFKitState("Stop"); err != nil {
-		return err
+		// vfkit may be already stopped, shutting down, or not listening.
+		// We don't fallback to "HardStop" since it typically fails due to
+		// https://github.com/crc-org/vfkit/issues/277.
+		log.Debugf("Failed to set vfkit state to 'Stop': %s", err)
+		pidfile := d.pidfilePath()
+		pid, err := process.ReadPidfile(pidfile)
+		if err != nil {
+			if !errors.Is(err, os.ErrNotExist) {
+				return err
+			}
+			// No pidfile.
+			return nil
+		}
+		if err := process.Terminate(pid, "vfkit"); err != nil {
+			if err != os.ErrProcessDone {
+				return err
+			}
+			// No process, stale pidfile.
+			if err := os.Remove(pidfile); err != nil {
+				log.Debugf("failed to remove %q: %s", pidfile, err)
+			}
+			return nil
+		}
 	}
 	return nil
 }
@@ -329,11 +334,6 @@ func (d *Driver) Remove() error {
 			return errors.Wrap(err, "kill")
 		}
 	}
-	if s != state.Stopped {
-		if err := d.SetVFKitState("Stop"); err != nil {
-			return errors.Wrap(err, "quit")
-		}
-	}
 	return nil
 }
 
@@ -369,7 +369,27 @@ func (d *Driver) extractKernel(isoPath string) error {
 
 func (d *Driver) Kill() error {
 	if err := d.SetVFKitState("HardStop"); err != nil {
-		return err
+		// Typically fails with EOF due to https://github.com/crc-org/vfkit/issues/277.
+		log.Debugf("Failed to set vfkit state to 'HardStop': %s", err)
+		pidfile := d.pidfilePath()
+		pid, err := process.ReadPidfile(pidfile)
+		if err != nil {
+			if !errors.Is(err, os.ErrNotExist) {
+				return err
+			}
+			// No pidfile.
+			return nil
+		}
+		if err := process.Kill(pid, "vfkit"); err != nil {
+			if err != os.ErrProcessDone {
+				return err
+			}
+			// No process, stale pidfile.
+			if err := os.Remove(pidfile); err != nil {
+				log.Debugf("failed to remove %q: %s", pidfile, err)
+			}
+			return nil
+		}
 	}
 	return nil
 }
diff --git a/pkg/minikube/process/process.go b/pkg/minikube/process/process.go
new file mode 100644
index 000000000..f461414fd
--- /dev/null
+++ b/pkg/minikube/process/process.go
@@ -0,0 +1,102 @@
+/*
+Copyright 2025 The Kubernetes Authors All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package process
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+
+	"github.com/mitchellh/go-ps"
+)
+
+const pidfileMode = 0o600
+
+// WritePidfile writes pid to path.
+func WritePidfile(path string, pid int) error {
+	data := fmt.Sprintf("%d", pid)
+	return os.WriteFile(path, []byte(data), pidfileMode)
+}
+
+// ReadPid reads a pid from path.
+func ReadPidfile(path string) (int, error) {
+	data, err := os.ReadFile(path)
+	if err != nil {
+		// Pass os.ErrNotExist
+		return -1, err
+	}
+	s := strings.TrimSpace(string(data))
+	pid, err := strconv.Atoi(s)
+	if err != nil {
+		return -1, fmt.Errorf("invalid pid %q: %s", s, err)
+	}
+	return pid, nil
+}
+
+// Exists tells if a process matching pid and executable name exist. Executable is
+// not the path to the executable.
+func Exists(pid int, executable string) (bool, error) {
+	// Fast path if pid does not exist.
+	exists, err := pidExists(pid)
+	if err != nil {
+		return true, err
+	}
+	if !exists {
+		return false, nil
+	}
+
+	// Slow path if pid exist, depending on the platform. On windows and darwin
+	// this fetch all processes from the krenel and find a process with pid. On
+	// linux this reads /proc/pid/stat
+	entry, err := ps.FindProcess(pid)
+	if err != nil {
+		return true, err
+	}
+	if entry == nil {
+		return false, nil
+	}
+	return entry.Executable() == executable, nil
+}
+
+// Terminate a process with pid and matching name. Returns os.ErrProcessDone if
+// the process does not exist, or nil if termiation was requested. Caller need
+// to wait until the process disappears.
+func Terminate(pid int, executable string) error {
+	exists, err := Exists(pid, executable)
+	if err != nil {
+		return err
+	}
+	if !exists {
+		return os.ErrProcessDone
+	}
+	return terminatePid(pid)
+}
+
+// Kill a process with pid matching executable name. Returns os.ErrProcessDone
+// if the process does not exist or nil the kill was requested. Caller need to
+// wait until the process disappears.
+func Kill(pid int, executable string) error {
+	exists, err := Exists(pid, executable)
+	if err != nil {
+		return err
+	}
+	if !exists {
+		return os.ErrProcessDone
+	}
+	return killPid(pid)
+}
diff --git a/pkg/minikube/process/process_other.go b/pkg/minikube/process/process_other.go
new file mode 100644
index 000000000..cdeefe1e9
--- /dev/null
+++ b/pkg/minikube/process/process_other.go
@@ -0,0 +1,53 @@
+//go:build !windows
+
+/*
+Copyright 2025 The Kubernetes Authors All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package process
+
+import (
+	"os"
+	"syscall"
+)
+
+func pidExists(pid int) (bool, error) {
+	// Never fails and we get a process in "done" state that returns
+	// os.ErrProcessDone from Signal or Wait.
+	process, err := os.FindProcess(pid)
+	if err != nil {
+		return true, err
+	}
+	if process.Signal(syscall.Signal(0)) == os.ErrProcessDone {
+		return false, nil
+	}
+	return true, nil
+}
+
+func terminatePid(pid int) error {
+	p, err := os.FindProcess(pid)
+	if err != nil {
+		return err
+	}
+	return p.Signal(syscall.SIGTERM)
+}
+
+func killPid(pid int) error {
+	p, err := os.FindProcess(pid)
+	if err != nil {
+		return err
+	}
+	return p.Kill()
+}
diff --git a/pkg/minikube/process/process_windows.go b/pkg/minikube/process/process_windows.go
new file mode 100644
index 000000000..7e78ecfab
--- /dev/null
+++ b/pkg/minikube/process/process_windows.go
@@ -0,0 +1,40 @@
+/*
+Copyright 2025 The Kubernetes Authors All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package process
+
+import (
+	"os"
+)
+
+func pidExists(pid int) (bool, error) {
+	// Fails with "OpenProcess: The parameter is incorrect" if the process does
+	// not exist.
+	_, err := os.FindProcess(pid)
+	return err == nil, nil
+}
+
+func terminatePid(pid int) error {
+	return killPid(pid)
+}
+
+func killPid(pid int) error {
+	p, err := os.FindProcess(pid)
+	if err != nil {
+		return os.ErrProcessDone
+	}
+	return p.Kill()
+}
diff --git a/site/content/en/docs/contrib/leaderboard/2025.html b/site/content/en/docs/contrib/leaderboard/2025.html
index 7637df561..349d1cad3 100644
--- a/site/content/en/docs/contrib/leaderboard/2025.html
+++ b/site/content/en/docs/contrib/leaderboard/2025.html
@@ -87,7 +87,7 @@ weight: -99992025
 </head>
 <body>
     <h1>kubernetes/minikube</h1>
-    <div class="subtitle">2025-01-01 &mdash; 2025-02-28</div>
+    <div class="subtitle">2025-01-01 &mdash; 2025-03-31</div>
 
     
         <h2>Reviewers</h2>
@@ -103,7 +103,8 @@ weight: -99992025
                 function drawreviewCounts() {
                     var data = new google.visualization.arrayToDataTable([
                     [{label:'',type:'string'},{label: '# of Merged PRs reviewed', type: 'number'}, { role: 'annotation' }],
-                    ["medyagh", 9, "9"],
+                    ["medyagh", 11, "11"],
+                    ["cfergeau", 1, "1"],
                     ["nirs", 1, "1"],
                     
                     ]);
@@ -137,8 +138,9 @@ weight: -99992025
                 function drawreviewWords() {
                     var data = new google.visualization.arrayToDataTable([
                     [{label:'',type:'string'},{label: '# of words written in merged PRs', type: 'number'}, { role: 'annotation' }],
-                    ["medyagh", 258, "258"],
+                    ["medyagh", 305, "305"],
                     ["nirs", 62, "62"],
+                    ["cfergeau", 10, "10"],
                     
                     ]);
 
@@ -171,8 +173,9 @@ weight: -99992025
                 function drawreviewComments() {
                     var data = new google.visualization.arrayToDataTable([
                     [{label:'',type:'string'},{label: '# of Review Comments in merged PRs', type: 'number'}, { role: 'annotation' }],
-                    ["medyagh", 6, "6"],
+                    ["medyagh", 9, "9"],
                     ["nirs", 2, "2"],
+                    ["cfergeau", 1, "1"],
                     
                     ]);
 
@@ -209,16 +212,19 @@ weight: -99992025
                 function drawprCounts() {
                     var data = new google.visualization.arrayToDataTable([
                     [{label:'',type:'string'},{label: '# of Pull Requests Merged', type: 'number'}, { role: 'annotation' }],
-                    ["ComradeProgrammer", 7, "7"],
                     ["medyagh", 7, "7"],
+                    ["ComradeProgrammer", 7, "7"],
                     ["prezha", 5, "5"],
-                    ["jeffmaury", 1, "1"],
-                    ["joaquimrocha", 1, "1"],
-                    ["nirs", 1, "1"],
-                    ["wuwentao", 1, "1"],
+                    ["nirs", 2, "2"],
                     ["luchenhan", 1, "1"],
-                    ["SzymonNadbrzezny", 1, "1"],
+                    ["wuwentao", 1, "1"],
+                    ["LJTian", 1, "1"],
+                    ["joaquimrocha", 1, "1"],
+                    ["Aaina26", 1, "1"],
                     ["liangyuanpeng", 1, "1"],
+                    ["jeffmaury", 1, "1"],
+                    ["danielcristho", 1, "1"],
+                    ["SzymonNadbrzezny", 1, "1"],
                     
                     ]);
 
@@ -254,13 +260,16 @@ weight: -99992025
                     ["ComradeProgrammer", 262, "262"],
                     ["medyagh", 235, "235"],
                     ["prezha", 98, "98"],
-                    ["nirs", 10, "10"],
+                    ["nirs", 12, "12"],
                     ["liangyuanpeng", 10, "10"],
                     ["luchenhan", 4, "4"],
                     ["wuwentao", 2, "2"],
                     ["joaquimrocha", 2, "2"],
                     ["SzymonNadbrzezny", 2, "2"],
+                    ["danielcristho", 0, "0"],
+                    ["LJTian", 0, "0"],
                     ["jeffmaury", 0, "0"],
+                    ["Aaina26", 0, "0"],
                     
                     ]);
 
@@ -296,13 +305,16 @@ weight: -99992025
                     ["ComradeProgrammer", 30, "30"],
                     ["medyagh", 24, "24"],
                     ["prezha", 11, "11"],
-                    ["nirs", 10, "10"],
+                    ["nirs", 5, "5"],
                     ["liangyuanpeng", 5, "5"],
                     ["luchenhan", 2, "2"],
+                    ["wuwentao", 1, "1"],
                     ["joaquimrocha", 1, "1"],
                     ["SzymonNadbrzezny", 1, "1"],
-                    ["wuwentao", 1, "1"],
+                    ["Aaina26", 0, "0"],
                     ["jeffmaury", 0, "0"],
+                    ["danielcristho", 0, "0"],
+                    ["LJTian", 0, "0"],
                     
                     ]);
 
@@ -339,21 +351,21 @@ weight: -99992025
                 function drawcomments() {
                     var data = new google.visualization.arrayToDataTable([
                     [{label:'',type:'string'},{label: '# of comments', type: 'number'}, { role: 'annotation' }],
-                    ["afbjorklund", 14, "14"],
-                    ["Ruchi1499", 10, "10"],
-                    ["medyagh", 6, "6"],
-                    ["Ritikaa96", 6, "6"],
-                    ["nirs", 3, "3"],
-                    ["danmaninc", 3, "3"],
+                    ["Ritikaa96", 60, "60"],
+                    ["Ruchi1499", 42, "42"],
+                    ["dhairya-seth", 31, "31"],
+                    ["afbjorklund", 17, "17"],
+                    ["AmarNathChary", 10, "10"],
+                    ["medyagh", 9, "9"],
+                    ["kundan2707", 7, "7"],
+                    ["nirs", 6, "6"],
+                    ["ljluestc", 4, "4"],
                     ["iankingori", 3, "3"],
-                    ["kundan2707", 3, "3"],
-                    ["ptiseo-mcs", 2, "2"],
-                    ["mardonner", 2, "2"],
-                    ["Mayerus", 2, "2"],
-                    ["Zenner2", 2, "2"],
-                    ["svinther", 2, "2"],
-                    ["paulwalrath", 2, "2"],
+                    ["danmaninc", 3, "3"],
+                    ["arlandi1974", 3, "3"],
+                    ["azrulafiq", 3, "3"],
                     ["skol101", 2, "2"],
+                    ["ptiseo-mcs", 2, "2"],
                     
                     ]);
 
@@ -387,20 +399,20 @@ weight: -99992025
                     var data = new google.visualization.arrayToDataTable([
                     [{label:'',type:'string'},{label: '# of words (excludes authored)', type: 'number'}, { role: 'annotation' }],
                     ["huttsMichael", 2234, "2234"],
+                    ["Ruchi1499", 1084, "1084"],
+                    ["Ritikaa96", 988, "988"],
                     ["mardonner", 790, "790"],
-                    ["Ruchi1499", 441, "441"],
+                    ["ljluestc", 715, "715"],
+                    ["dhairya-seth", 615, "615"],
+                    ["afbjorklund", 440, "440"],
                     ["Zenner2", 423, "423"],
-                    ["afbjorklund", 373, "373"],
+                    ["nirs", 361, "361"],
                     ["danmaninc", 336, "336"],
                     ["ptiseo-mcs", 326, "326"],
-                    ["medyagh", 238, "238"],
+                    ["medyagh", 301, "301"],
+                    ["ComradeProgrammer", 221, "221"],
+                    ["azrulafiq", 137, "137"],
                     ["skol101", 128, "128"],
-                    ["carlosaml500", 115, "115"],
-                    ["nirs", 114, "114"],
-                    ["masalinas", 110, "110"],
-                    ["Ritikaa96", 102, "102"],
-                    ["iankingori", 93, "93"],
-                    ["Mayerus", 83, "83"],
                     
                     ]);
 
@@ -433,7 +445,7 @@ weight: -99992025
                 function drawissueCloser() {
                     var data = new google.visualization.arrayToDataTable([
                     [{label:'',type:'string'},{label: '# of issues closed (excludes authored)', type: 'number'}, { role: 'annotation' }],
-                    ["medyagh", 2, "2"],
+                    ["medyagh", 6, "6"],
                     ["prezha", 2, "2"],
                     ["afbjorklund", 1, "1"],
                     


Test patch:
diff --git a/pkg/minikube/process/process_test.go b/pkg/minikube/process/process_test.go
new file mode 100644
index 000000000..435d3672e
--- /dev/null
+++ b/pkg/minikube/process/process_test.go
@@ -0,0 +1,238 @@
+/*
+Copyright 2025 The Kubernetes Authors All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package process
+
+import (
+	"bufio"
+	"errors"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"testing"
+	"time"
+)
+
+var waitTimeout = 250 * time.Millisecond
+
+func TestPidfile(t *testing.T) {
+	pidfile := filepath.Join(t.TempDir(), "pid")
+	if err := WritePidfile(pidfile, 42); err != nil {
+		t.Fatal(err)
+	}
+	pid, err := ReadPidfile(pidfile)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if pid != 42 {
+		t.Fatalf("expected 42, got %d", pid)
+	}
+}
+
+func TestPidfileMissing(t *testing.T) {
+	pidfile := filepath.Join(t.TempDir(), "pid")
+	_, err := ReadPidfile(pidfile)
+	if !errors.Is(err, os.ErrNotExist) {
+		t.Fatalf("unexpected error: %s", err)
+	}
+}
+
+func TestPidfileInvalid(t *testing.T) {
+	pidfile := filepath.Join(t.TempDir(), "pid")
+	if err := os.WriteFile(pidfile, []byte("invalid"), 0o600); err != nil {
+		t.Fatal(err)
+	}
+	_, err := ReadPidfile(pidfile)
+	if err == nil {
+		t.Fatal("parsing invalid pidfile did not fail")
+	}
+}
+
+func TestProcess(t *testing.T) {
+	sleep, err := build(t, "sleep")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	noterm, err := build(t, "noterm")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	t.Run("exists", func(t *testing.T) {
+		cmd := startProcess(t, sleep)
+		exists, err := Exists(cmd.Process.Pid, filepath.Base(sleep))
+		if err != nil {
+			t.Fatal(err)
+		}
+		if !exists {
+			t.Fatal("existing process not found")
+		}
+		exists, err = Exists(0xfffffffc, filepath.Base(sleep))
+		if err != nil {
+			t.Fatal(err)
+		}
+		if exists {
+			t.Fatal("process with non-existing pid found")
+		}
+		exists, err = Exists(cmd.Process.Pid, "no-such-executable")
+		if err != nil {
+			t.Fatal(err)
+		}
+		if exists {
+			t.Fatal("process with non-existing executable found")
+		}
+	})
+
+	t.Run("terminate", func(t *testing.T) {
+		cmd := startProcess(t, sleep)
+		err := Terminate(cmd.Process.Pid, filepath.Base(sleep))
+		if err != nil {
+			t.Fatal(err)
+		}
+		waitForTermination(t, cmd)
+		exists, err := Exists(cmd.Process.Pid, filepath.Base(sleep))
+		if err != nil {
+			t.Fatal(err)
+		}
+		if exists {
+			t.Fatalf("reaped process exists")
+		}
+	})
+
+	t.Run("terminate name mismatch", func(t *testing.T) {
+		cmd := startProcess(t, sleep)
+		err := Terminate(cmd.Process.Pid, "no-such-process")
+		if err == nil {
+			t.Fatalf("Signaled unrelated process")
+		}
+		if err != os.ErrProcessDone {
+			t.Fatalf("Unexpected error: %s", err)
+		}
+	})
+
+	t.Run("terminate ignored", func(t *testing.T) {
+		if runtime.GOOS == "windows" {
+			t.Skip("no way to ignore termination on windows")
+		}
+		cmd := startProcess(t, noterm)
+		err := Terminate(cmd.Process.Pid, filepath.Base(noterm))
+		if err != nil {
+			t.Fatal(err)
+		}
+		time.Sleep(waitTimeout)
+		exists, err := Exists(cmd.Process.Pid, filepath.Base(noterm))
+		if err != nil {
+			t.Fatal(err)
+		}
+		if !exists {
+			t.Fatalf("process terminated")
+		}
+	})
+
+	t.Run("kill", func(t *testing.T) {
+		cmd := startProcess(t, noterm)
+		err := Kill(cmd.Process.Pid, filepath.Base(noterm))
+		if err != nil {
+			t.Fatal(err)
+		}
+		waitForTermination(t, cmd)
+		exists, err := Exists(cmd.Process.Pid, filepath.Base(noterm))
+		if err != nil {
+			t.Fatal(err)
+		}
+		if exists {
+			t.Fatalf("reaped process exists")
+		}
+	})
+
+	t.Run("kill name mismatch", func(t *testing.T) {
+		cmd := startProcess(t, noterm)
+		err := Kill(cmd.Process.Pid, "no-such-process")
+		if err == nil {
+			t.Fatalf("Killed unrelated process")
+		}
+		if err != os.ErrProcessDone {
+			t.Fatalf("Unexpected error: %s", err)
+		}
+	})
+}
+
+func startProcess(t *testing.T, cmd string) *exec.Cmd {
+	name := filepath.Base(cmd)
+	c := exec.Command(cmd)
+	stdout, err := c.StdoutPipe()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	start := time.Now()
+	if err := c.Start(); err != nil {
+		t.Fatal(err)
+	}
+	t.Logf("Started process %q (pid=%v)", name, c.Process.Pid)
+
+	t.Cleanup(func() {
+		_ = c.Process.Kill()
+		_ = c.Wait()
+	})
+
+	// Synchronize with the process to ensure it set up signal handlers before
+	// we send a signal.
+	r := bufio.NewReader(stdout)
+	line, err := r.ReadString('\n')
+	if err != nil {
+		t.Fatal(err)
+	}
+	if line != "READY\n" {
+		t.Fatalf("Unexpected response: %q", line)
+	}
+	t.Logf("Process %q ready in %.6f seconds", name, time.Since(start).Seconds())
+
+	return c
+}
+
+func waitForTermination(t *testing.T, cmd *exec.Cmd) {
+	name := filepath.Base(cmd.Path)
+	timer := time.AfterFunc(waitTimeout, func() {
+		t.Fatalf("Timeout waiting for %q", name)
+	})
+	defer timer.Stop()
+	start := time.Now()
+	err := cmd.Wait()
+	t.Logf("Process %q terminated in %.6f seconds: %s", name, time.Since(start).Seconds(), err)
+}
+
+func build(t *testing.T, name string) (string, error) {
+	source := fmt.Sprintf("testdata/%s.go", name)
+
+	out := filepath.Join(t.TempDir(), name)
+	if runtime.GOOS == "windows" {
+		out += ".exe"
+	}
+
+	t.Logf("Building %q", name)
+	cmd := exec.Command("go", "build", "-o", out, source)
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+
+	if err := cmd.Run(); err != nil {
+		return "", err
+	}
+	return out, nil
+}
diff --git a/pkg/minikube/process/testdata/noterm.go b/pkg/minikube/process/testdata/noterm.go
new file mode 100644
index 000000000..5d10f2e3a
--- /dev/null
+++ b/pkg/minikube/process/testdata/noterm.go
@@ -0,0 +1,30 @@
+/*
+Copyright 2025 The Kubernetes Authors All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"fmt"
+	"os/signal"
+	"syscall"
+	"time"
+)
+
+func main() {
+	signal.Ignore(syscall.SIGTERM)
+	fmt.Println("READY") // Synchronize with parent
+	time.Sleep(10 * time.Second)
+}
diff --git a/pkg/minikube/process/testdata/sleep.go b/pkg/minikube/process/testdata/sleep.go
new file mode 100644
index 000000000..df2249aa9
--- /dev/null
+++ b/pkg/minikube/process/testdata/sleep.go
@@ -0,0 +1,27 @@
+/*
+Copyright 2025 The Kubernetes Authors All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func main() {
+	fmt.Println("READY") // Synchronize with parent
+	time.Sleep(10 * time.Second)
+}