Standardize daemon detection with tryDaemonLock probe (bd-wgu4)
- Extract lock checking to internal/lockfile package - Add lock probe in RPC client before connection attempts - Update daemon discovery to use lock probe - Eliminates unnecessary connection attempts when socket missing Closes bd-wgu4 Amp-Thread-ID: https://ampcode.com/threads/T-3b863f21-3af4-49d3-9214-477d904b80fe Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
118
internal/lockfile/lock.go
Normal file
118
internal/lockfile/lock.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package lockfile
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LockInfo represents the metadata stored in the daemon.lock file
|
||||
type LockInfo struct {
|
||||
PID int `json:"pid"`
|
||||
ParentPID int `json:"parent_pid,omitempty"`
|
||||
Database string `json:"database"`
|
||||
Version string `json:"version"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// This is a cheap probe operation that should be called before attempting
|
||||
// RPC connections to avoid unnecessary connection timeouts.
|
||||
func TryDaemonLock(beadsDir string) (running bool, pid int) {
|
||||
lockPath := filepath.Join(beadsDir, "daemon.lock")
|
||||
|
||||
// Open lock file with read-write access (required for LockFileEx on Windows)
|
||||
// #nosec G304 - controlled path from config
|
||||
f, err := os.OpenFile(lockPath, os.O_RDWR, 0)
|
||||
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 func() { _ = 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 from JSON format (best effort)
|
||||
_, _ = f.Seek(0, 0)
|
||||
var lockInfo LockInfo
|
||||
if err := json.NewDecoder(f).Decode(&lockInfo); err == nil {
|
||||
pid = lockInfo.PID
|
||||
} else {
|
||||
// Fallback: try reading as plain integer (old format)
|
||||
_, _ = f.Seek(0, 0)
|
||||
data := make([]byte, 32)
|
||||
n, _ := f.Read(data)
|
||||
if n > 0 {
|
||||
_, _ = fmt.Sscanf(string(data[:n]), "%d", &pid)
|
||||
}
|
||||
// Fallback to PID file if we couldn't read PID from lock file
|
||||
if pid == 0 {
|
||||
_, pid = checkPIDFile(beadsDir)
|
||||
}
|
||||
}
|
||||
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")
|
||||
// #nosec G304 - controlled path from config
|
||||
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
|
||||
}
|
||||
|
||||
// ReadLockInfo reads and parses the daemon lock file
|
||||
// Returns lock info if available, or error if file doesn't exist or can't be parsed
|
||||
func ReadLockInfo(beadsDir string) (*LockInfo, error) {
|
||||
lockPath := filepath.Join(beadsDir, "daemon.lock")
|
||||
|
||||
// #nosec G304 - controlled path from config
|
||||
data, err := os.ReadFile(lockPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var lockInfo LockInfo
|
||||
if err := json.Unmarshal(data, &lockInfo); err != nil {
|
||||
// Try parsing as old format (plain PID)
|
||||
var pid int
|
||||
if _, err := fmt.Sscanf(string(data), "%d", &pid); err == nil {
|
||||
return &LockInfo{PID: pid}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("cannot parse lock file: %w", err)
|
||||
}
|
||||
|
||||
return &lockInfo, nil
|
||||
}
|
||||
Reference in New Issue
Block a user