feat: Add Windows-compatible file locking for daemon

Replace Unix-only syscall.Flock with gofrs/flock library for
cross-platform file locking. This enables the daemon to run on
Windows in addition to Unix-like systems.

- Add github.com/gofrs/flock v0.13.0 dependency
- Replace syscall.Flock calls with flock.TryLock/Unlock
- Maintain same non-blocking exclusive lock semantics

(gt-5354h)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown
2026-01-05 00:10:58 -08:00
committed by Steve Yegge
parent b88d3e8ee7
commit 43cca06460
3 changed files with 42 additions and 7 deletions

View File

@@ -13,6 +13,7 @@ import (
"syscall"
"time"
"github.com/gofrs/flock"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/boot"
"github.com/steveyegge/gastown/internal/config"
@@ -70,18 +71,19 @@ func (d *Daemon) Run() error {
// Acquire exclusive lock to prevent multiple daemons from running.
// This prevents the TOCTOU race condition where multiple concurrent starts
// can all pass the IsRunning() check before any writes the PID file.
// Uses gofrs/flock for cross-platform compatibility (Unix + Windows).
lockFile := filepath.Join(d.config.TownRoot, "daemon", "daemon.lock")
lock, err := os.OpenFile(lockFile, os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
return fmt.Errorf("opening lock file: %w", err)
}
defer lock.Close()
fileLock := flock.New(lockFile)
// Try to acquire exclusive lock (non-blocking)
if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
locked, err := fileLock.TryLock()
if err != nil {
return fmt.Errorf("acquiring lock: %w", err)
}
if !locked {
return fmt.Errorf("daemon already running (lock held by another process)")
}
defer func() { _ = syscall.Flock(int(lock.Fd()), syscall.LOCK_UN) }()
defer fileLock.Unlock()
// Write PID file
if err := os.WriteFile(d.config.PidFile, []byte(strconv.Itoa(os.Getpid())), 0644); err != nil {