Add qvm doctor command to diagnose and fix common issues

This commit is contained in:
Joshua Bell 2026-01-27 11:54:26 -06:00
parent 2aec01b3b2
commit eb469f1cd8
8 changed files with 660 additions and 170 deletions

78
internal/lock/lock.go Normal file
View file

@ -0,0 +1,78 @@
// Package lock provides file-based locking for VM operations.
package lock
import (
"fmt"
"os"
"path/filepath"
"syscall"
"time"
"github.com/samber/mo"
"qvm/internal/config"
)
// Lock represents an exclusive file lock on VM operations.
type Lock struct {
file *os.File
}
var lockPath = filepath.Join(config.StateDir, "vm.lock")
// Acquire obtains an exclusive lock on VM operations.
// Blocks until the lock is available or timeout is reached.
func Acquire(timeout time.Duration) mo.Result[*Lock] {
if err := os.MkdirAll(config.StateDir, 0755); err != nil {
return mo.Err[*Lock](fmt.Errorf("failed to create state directory: %w", err))
}
file, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
return mo.Err[*Lock](fmt.Errorf("failed to open lock file: %w", err))
}
deadline := time.Now().Add(timeout)
for {
err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
if err == nil {
return mo.Ok(&Lock{file: file})
}
if time.Now().After(deadline) {
file.Close()
return mo.Err[*Lock](fmt.Errorf("timeout waiting for VM lock after %v", timeout))
}
time.Sleep(100 * time.Millisecond)
}
}
// TryAcquire attempts to obtain an exclusive lock without blocking.
// Returns error if lock is held by another process.
func TryAcquire() mo.Result[*Lock] {
if err := os.MkdirAll(config.StateDir, 0755); err != nil {
return mo.Err[*Lock](fmt.Errorf("failed to create state directory: %w", err))
}
file, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
return mo.Err[*Lock](fmt.Errorf("failed to open lock file: %w", err))
}
err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
if err != nil {
file.Close()
return mo.Err[*Lock](fmt.Errorf("VM operation in progress by another process"))
}
return mo.Ok(&Lock{file: file})
}
// Release releases the lock.
func (l *Lock) Release() error {
if l.file == nil {
return nil
}
syscall.Flock(int(l.file.Fd()), syscall.LOCK_UN)
return l.file.Close()
}