// Package lock provides per-app exclusive locking, so that concurrent skiff // invocations against the same app - a deploy racing a config change, either // racing a manual rollback - can't interleave their snapshot/apply/restart // sequences against the same dataset and jail. package lock import ( "fmt" "os" "path/filepath" "syscall" ) // Handle is a held lock; call Release to give it up. type Handle struct { f *os.File } // Acquire blocks until it holds an exclusive lock on path, creating the file // (and its parent directory) if needed. The lock is held via flock(2) on the // open file descriptor, so it's released automatically if the process dies - // a crash never leaves a stale lock behind. func Acquire(path string) (*Handle, error) { if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return nil, fmt.Errorf("create lock dir: %w", err) } f, err := os.OpenFile(path, os.O_CREATE|os.O_RDONLY, 0644) if err != nil { return nil, fmt.Errorf("open lock file: %w", err) } if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil { f.Close() return nil, fmt.Errorf("flock: %w", err) } return &Handle{f: f}, nil } // Release gives up the lock. Safe to call via defer immediately after a // successful Acquire. func (h *Handle) Release() error { defer h.f.Close() return syscall.Flock(int(h.f.Fd()), syscall.LOCK_UN) }