check / check (push) Failing after 4m42s
A backup of a directory that holds the local index stored a torn copy of it, its -wal and -shm side files and vaultik.pid, all being written while they were read. The scan now treats these files as excluded, whatever the config says. Their paths come from index_path and the lock directory, with symlinks in their directories resolved the way source paths are. If index_path is itself a symlink, SQLite writes the file it points to, so that file and its side files are skipped too. pidlock now owns the lock directory and the PID file name, so the CLI and the scanner use the same path. Model: opus-5-5
150 lines
4.0 KiB
Go
150 lines
4.0 KiB
Go
// Package pidlock provides process-level locking using PID files.
|
|
// It prevents multiple instances of vaultik from running simultaneously,
|
|
// which would cause database locking conflicts.
|
|
package pidlock
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/adrg/xdg"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// ErrAlreadyRunning indicates another vaultik instance is running.
|
|
var ErrAlreadyRunning = errors.New("another vaultik instance is already running")
|
|
|
|
// Lock represents an acquired PID lock: an flock(2) on the PID file,
|
|
// held while the file stays open. The kernel drops it when the process
|
|
// exits, however it exits, so a crashed run never leaves the lock held.
|
|
type Lock struct {
|
|
file *os.File
|
|
}
|
|
|
|
const (
|
|
// lockDirPerm is the mode for the lock directory (owner-only).
|
|
lockDirPerm = 0o700
|
|
// pidFilePerm is the mode for the PID file (owner-only).
|
|
pidFilePerm = 0o600
|
|
// pidFileName is the name of the PID file in the lock directory.
|
|
pidFileName = "vaultik.pid"
|
|
)
|
|
|
|
// Dir returns the directory that holds vaultik's PID file.
|
|
func Dir() string {
|
|
return filepath.Join(xdg.DataHome, "vaultik")
|
|
}
|
|
|
|
// Path returns the path of the PID file that Acquire uses in lockDir.
|
|
func Path(lockDir string) string {
|
|
return filepath.Join(lockDir, pidFileName)
|
|
}
|
|
|
|
// Acquire attempts to acquire a PID lock in the specified directory.
|
|
// If another process holds the lock, it returns ErrAlreadyRunning with
|
|
// that process's PID. On success, it writes the current PID to the lock
|
|
// file and returns a Lock that must be released with Release().
|
|
func Acquire(lockDir string) (*Lock, error) {
|
|
// Ensure lock directory exists
|
|
err := os.MkdirAll(lockDir, lockDirPerm)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating lock directory: %w", err)
|
|
}
|
|
|
|
lockPath := Path(lockDir)
|
|
|
|
// No O_TRUNC: the file may hold the PID of the process that has the
|
|
// lock, which the error below reports.
|
|
file, err := os.OpenFile( //nolint:gosec // G304: path is our own lock file
|
|
lockPath, os.O_RDWR|os.O_CREATE, pidFilePerm)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("opening PID file: %w", err)
|
|
}
|
|
|
|
err = unix.Flock(int(file.Fd()), unix.LOCK_EX|unix.LOCK_NB)
|
|
if err != nil {
|
|
_ = file.Close()
|
|
|
|
if errors.Is(err, unix.EWOULDBLOCK) {
|
|
return nil, alreadyRunningError(lockPath)
|
|
}
|
|
|
|
return nil, fmt.Errorf("locking PID file: %w", err)
|
|
}
|
|
|
|
err = writePID(file)
|
|
if err != nil {
|
|
_ = file.Close()
|
|
|
|
return nil, err
|
|
}
|
|
|
|
return &Lock{file: file}, nil
|
|
}
|
|
|
|
// Release empties the PID file and closes it, which drops the lock.
|
|
// It is safe to call Release multiple times.
|
|
func (l *Lock) Release() error {
|
|
if l == nil || l.file == nil {
|
|
return nil
|
|
}
|
|
|
|
file := l.file
|
|
l.file = nil
|
|
|
|
// Do not remove the file here. A process that opened it a moment
|
|
// earlier could then lock the removed file while another creates and
|
|
// locks a new one, and both would run.
|
|
truncateErr := file.Truncate(0)
|
|
closeErr := file.Close()
|
|
|
|
return errors.Join(truncateErr, closeErr)
|
|
}
|
|
|
|
// writePID replaces the contents of the locked PID file with the current
|
|
// PID.
|
|
func writePID(file *os.File) error {
|
|
err := file.Truncate(0)
|
|
if err != nil {
|
|
return fmt.Errorf("truncating PID file: %w", err)
|
|
}
|
|
|
|
_, err = file.WriteAt([]byte(strconv.Itoa(os.Getpid())), 0)
|
|
if err != nil {
|
|
return fmt.Errorf("writing PID file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// alreadyRunningError reports that another process holds the lock,
|
|
// naming its PID when the file holds one. The holder writes its PID just
|
|
// after it locks, so the file can briefly be empty.
|
|
func alreadyRunningError(lockPath string) error {
|
|
pid, err := readPIDFile(lockPath)
|
|
if err != nil {
|
|
return ErrAlreadyRunning
|
|
}
|
|
|
|
return fmt.Errorf("%w (PID %d)", ErrAlreadyRunning, pid)
|
|
}
|
|
|
|
// readPIDFile reads and parses the PID from a lock file.
|
|
func readPIDFile(path string) (int, error) {
|
|
data, err := os.ReadFile(path) //nolint:gosec // G304: path is our own lock file
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("parsing PID: %w", err)
|
|
}
|
|
|
|
return pid, nil
|
|
}
|