Skip to content

Commit 5adeb20

Browse files
committed
fix: add Windows support for file locking
Split lock.go into platform-specific files: - lock_unix.go: Uses syscall.Flock (Linux, macOS) - lock_windows.go: Uses exclusive file creation This fixes the Windows cross-compilation error: undefined: syscall.Flock The Windows implementation uses O_CREATE|O_EXCL for exclusive file creation, with stale lock detection for crash recovery.
1 parent 97ece9a commit 5adeb20

2 files changed

Lines changed: 101 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//go:build unix
2+
13
package settings
24

35
import (

internal/settings/lock_windows.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//go:build windows
2+
3+
package settings
4+
5+
import (
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"time"
10+
)
11+
12+
// FileLock provides advisory file locking for concurrent access protection
13+
// On Windows, this uses exclusive file creation as a simple locking mechanism
14+
type FileLock struct {
15+
path string
16+
file *os.File
17+
}
18+
19+
// lockTimeout is how long to wait for a lock before giving up
20+
const lockTimeout = 10 * time.Second
21+
22+
// NewFileLock creates a new file lock for the given path
23+
// The lock file will be created at path + ".lock"
24+
func NewFileLock(path string) *FileLock {
25+
return &FileLock{
26+
path: path + ".lock",
27+
}
28+
}
29+
30+
// Lock acquires an exclusive lock, blocking until available or timeout
31+
// On Windows, this uses exclusive file creation
32+
func (l *FileLock) Lock() error {
33+
// Ensure directory exists
34+
dir := filepath.Dir(l.path)
35+
if err := os.MkdirAll(dir, 0755); err != nil {
36+
return fmt.Errorf("failed to create lock directory: %w", err)
37+
}
38+
39+
// Try to acquire lock with timeout
40+
deadline := time.Now().Add(lockTimeout)
41+
for {
42+
// Try to create lock file exclusively
43+
// O_CREATE|O_EXCL ensures the file is created only if it doesn't exist
44+
f, err := os.OpenFile(l.path, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0600)
45+
if err == nil {
46+
l.file = f
47+
return nil // Lock acquired
48+
}
49+
50+
// If file exists, check if it's stale (older than lockTimeout)
51+
// This helps recover from crashes that leave lock files behind
52+
if info, statErr := os.Stat(l.path); statErr == nil {
53+
if time.Since(info.ModTime()) > lockTimeout {
54+
// Stale lock, try to remove it
55+
_ = os.Remove(l.path)
56+
}
57+
}
58+
59+
if time.Now().After(deadline) {
60+
return fmt.Errorf("timeout waiting for lock on %s", l.path)
61+
}
62+
63+
// Wait a bit before retrying
64+
time.Sleep(50 * time.Millisecond)
65+
}
66+
}
67+
68+
// Unlock releases the lock
69+
func (l *FileLock) Unlock() error {
70+
if l.file == nil {
71+
return nil
72+
}
73+
74+
closeErr := l.file.Close()
75+
l.file = nil
76+
77+
// Remove the lock file
78+
removeErr := os.Remove(l.path)
79+
80+
if closeErr != nil {
81+
return fmt.Errorf("failed to close lock file: %w", closeErr)
82+
}
83+
if removeErr != nil && !os.IsNotExist(removeErr) {
84+
return fmt.Errorf("failed to remove lock file: %w", removeErr)
85+
}
86+
87+
return nil
88+
}
89+
90+
// WithLock executes a function while holding the lock
91+
func WithLock(path string, fn func() error) error {
92+
lock := NewFileLock(path)
93+
if err := lock.Lock(); err != nil {
94+
return err
95+
}
96+
defer func() { _ = lock.Unlock() }()
97+
98+
return fn()
99+
}

0 commit comments

Comments
 (0)