internal/agent/pidfile/pidfile.go
Ref: Size: 2.7 KiB History
// Package pidfile records which process a VM's guest runs as, in a form that
// survives a reboot without lying about it.
//
// A pid on its own is not evidence. The agent's state directory outlives the
// host, pids are recycled out of a small space, and every backend's teardown
// path reaches for the recorded number and signals it — so a pidfile written
// before the last boot names whatever now happens to hold that number, and
// killing a VM means killing a stranger. Writing the host's boot identifier
// beside the pid is what turns "a process had this number once" into "this
// process is ours".
//
// The package is mechanism, not policy: it does not name the file, decide what
// a failed signal means, or read the boot id itself. Each backend keeps those.
// What it owns is the one rule both of them have to agree on, so that a second
// VMM driver cannot quietly disagree with the first about whose process it is
// about to kill.
package pidfile
import (
"fmt"
"os"
"strconv"
"strings"
)
// mode is the pidfile's permissions: readable by the agent alone, like every
// other per-VM artifact in the state directory.
const mode = 0o600
// Write records pid and the boot it was started in at path.
func Write(path string, pid int, bootID string) error {
if err := os.WriteFile(path, []byte(strconv.Itoa(pid)+"\n"+bootID+"\n"), mode); err != nil {
return fmt.Errorf("write pidfile %s: %w", path, err)
}
return nil
}
// Owned returns the pid recorded at path if this agent may signal it, and 0
// otherwise — no file, an unreadable one, or one written before bootID.
//
// Zero is deliberately the same answer for "there is no process" and "there is
// a process but it is not ours to touch", because callers do the same thing
// with both: report not-running, and signal nothing.
//
// A pidfile carrying no boot id at all is treated as OURS, and that is the
// load-bearing choice here. It can only have been written by an agent that
// predates this format, which is to say by the binary being replaced during a
// rolling upgrade. The cautious-looking answer — refuse, report not running —
// is the ruinous one: reconcile reads every live guest as lost and boots a
// SECOND hypervisor onto the same disk image, and two of them writing one disk
// corrupts the guest. Preserving running guests across an agent upgrade is a
// property this fleet relies on. The exposure is one boot wide: the next Boot
// rewrites the pidfile in this format.
func Owned(path, bootID string) int {
raw, err := os.ReadFile(path)
if err != nil {
return 0
}
line, rest, _ := strings.Cut(string(raw), "\n")
pid, err := strconv.Atoi(strings.TrimSpace(line))
if err != nil {
return 0
}
if boot := strings.TrimSpace(rest); boot != "" && boot != bootID {
return 0
}
return pid
}