Files
beads/cmd/bd/daemon_lock.go

117 lines
3.0 KiB
Go

package main
import (
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
)
var ErrDaemonLocked = errors.New("daemon lock already held by another process")
// DaemonLock represents a held lock on the daemon.lock file
type DaemonLock struct {
file *os.File
path string
}
// Close releases the daemon lock
func (l *DaemonLock) Close() error {
if l.file == nil {
return nil
}
// Closing the file descriptor automatically releases the flock
err := l.file.Close()
l.file = nil
return err
}
// acquireDaemonLock attempts to acquire an exclusive lock on daemon.lock
// Returns ErrDaemonLocked if another daemon is already running
func acquireDaemonLock(beadsDir string, global bool) (*DaemonLock, error) {
lockPath := filepath.Join(beadsDir, "daemon.lock")
// Open or create the lock file
f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return nil, fmt.Errorf("cannot open lock file: %w", err)
}
// Try to acquire exclusive non-blocking lock
if err := flockExclusive(f); err != nil {
f.Close()
if err == ErrDaemonLocked {
return nil, ErrDaemonLocked
}
return nil, fmt.Errorf("cannot lock file: %w", err)
}
// Write our PID to the lock file for debugging (optional)
_ = f.Truncate(0)
_, _ = f.Seek(0, 0)
fmt.Fprintf(f, "%d\n", os.Getpid())
_ = f.Sync()
return &DaemonLock{file: f, path: lockPath}, nil
}
// tryDaemonLock attempts to acquire and immediately release the daemon lock
// to check if a daemon is running. Returns true if daemon is running.
// Falls back to PID file check for backward compatibility with pre-lock daemons.
func tryDaemonLock(beadsDir string) (running bool, pid int) {
lockPath := filepath.Join(beadsDir, "daemon.lock")
// Try to open existing lock file
f, err := os.Open(lockPath)
if err != nil {
// No lock file - could be old daemon without lock support
// Fall back to PID file check for backward compatibility
return checkPIDFile(beadsDir)
}
defer f.Close()
// Try to acquire lock non-blocking
if err := flockExclusive(f); err != nil {
if err == ErrDaemonLocked {
// Lock is held - daemon is running
// Try to read PID for display (best effort)
if data := make([]byte, 32); true {
n, _ := f.Read(data)
if n > 0 {
_, _ = fmt.Sscanf(string(data), "%d", &pid)
}
}
return true, pid
}
// Other errors mean we can't determine status
return false, 0
}
// We got the lock - no daemon running
// Release immediately (file close will do this)
return false, 0
}
// checkPIDFile checks if a daemon is running by reading the PID file.
// This is used for backward compatibility with pre-lock daemons.
func checkPIDFile(beadsDir string) (running bool, pid int) {
pidFile := filepath.Join(beadsDir, "daemon.pid")
data, err := os.ReadFile(pidFile)
if err != nil {
return false, 0
}
pidVal, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
return false, 0
}
if !isProcessRunning(pidVal) {
return false, 0
}
return true, pidVal
}