// 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() }