Skip to content

vfkit: More robust state management #20506

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 58 additions & 38 deletions pkg/drivers/vfkit/vfkit.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
Expand All @@ -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"
)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}
Expand Down
102 changes: 102 additions & 0 deletions pkg/minikube/process/process.go
Original file line number Diff line number Diff line change
@@ -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)
}
53 changes: 53 additions & 0 deletions pkg/minikube/process/process_other.go
Original file line number Diff line number Diff line change
@@ -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()
}
Loading
Loading