130 lines
3.3 KiB
Go
130 lines
3.3 KiB
Go
package daemonrunner
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var ErrDaemonLocked = errors.New("daemon lock already held by another process")
|
|
|
|
// DaemonLockInfo represents the metadata stored in the daemon.lock file
|
|
type DaemonLockInfo struct {
|
|
PID int `json:"pid"`
|
|
Database string `json:"database"`
|
|
Version string `json:"version"`
|
|
StartedAt time.Time `json:"started_at"`
|
|
}
|
|
|
|
// 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
|
|
}
|
|
err := l.file.Close()
|
|
l.file = nil
|
|
return err
|
|
}
|
|
|
|
// getPIDFilePath returns the path to daemon.pid in the given beads directory
|
|
func getPIDFilePath(beadsDir string) string {
|
|
return filepath.Join(beadsDir, "daemon.pid")
|
|
}
|
|
|
|
// getSocketPath returns the path to bd.sock in the given beads directory
|
|
func getSocketPath(beadsDir string) string {
|
|
return filepath.Join(beadsDir, "bd.sock")
|
|
}
|
|
|
|
// readPIDFile reads the PID from daemon.pid
|
|
func readPIDFile(pidFile string) (int, error) {
|
|
// #nosec G304 - controlled path from config
|
|
data, err := os.ReadFile(pidFile)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid PID in file: %w", err)
|
|
}
|
|
return pid, nil
|
|
}
|
|
|
|
// writePIDFile writes the current process PID to daemon.pid
|
|
func writePIDFile(pidFile string) error {
|
|
return os.WriteFile(pidFile, []byte(fmt.Sprintf("%d\n", os.Getpid())), 0600)
|
|
}
|
|
|
|
// ensurePIDFileCorrect verifies PID file has correct PID, fixes if wrong
|
|
func ensurePIDFileCorrect(pidFile string) error {
|
|
myPID := os.Getpid()
|
|
if pid, err := readPIDFile(pidFile); err == nil && pid == myPID {
|
|
return nil
|
|
}
|
|
return writePIDFile(pidFile)
|
|
}
|
|
|
|
// checkVersionMismatch checks if database version matches daemon version
|
|
func checkVersionMismatch(dbVersion, daemonVersion string) (mismatch bool, missing bool) {
|
|
if dbVersion == "" {
|
|
return false, true
|
|
}
|
|
if dbVersion != daemonVersion {
|
|
return true, false
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
// acquireDaemonLock attempts to acquire an exclusive lock on daemon.lock
|
|
func acquireDaemonLock(beadsDir string, dbPath string, version string) (*DaemonLock, error) {
|
|
lockPath := filepath.Join(beadsDir, "daemon.lock")
|
|
|
|
// Open or create the lock file
|
|
// #nosec G304 - controlled path from config
|
|
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 JSON metadata to the lock file
|
|
lockInfo := DaemonLockInfo{
|
|
PID: os.Getpid(),
|
|
Database: dbPath,
|
|
Version: version,
|
|
StartedAt: time.Now().UTC(),
|
|
}
|
|
|
|
_ = f.Truncate(0)
|
|
_, _ = f.Seek(0, 0)
|
|
encoder := json.NewEncoder(f)
|
|
encoder.SetIndent("", " ")
|
|
_ = encoder.Encode(lockInfo)
|
|
_ = f.Sync()
|
|
|
|
// Also write PID file for Windows compatibility
|
|
pidFile := getPIDFilePath(beadsDir)
|
|
_ = writePIDFile(pidFile)
|
|
|
|
return &DaemonLock{file: f, path: lockPath}, nil
|
|
}
|