|
3 | 3 | package main |
4 | 4 |
|
5 | 5 | import ( |
| 6 | + "fmt" |
6 | 7 | "os" |
| 8 | + "path/filepath" |
| 9 | + "time" |
7 | 10 |
|
8 | 11 | "golang.org/x/sys/windows" |
9 | 12 | ) |
10 | 13 |
|
| 14 | +const fileLockRetryInterval = 50 * time.Millisecond |
| 15 | + |
| 16 | +// openNonBlock mirrors the unix constant ORed into clipboard opens. Windows |
| 17 | +// has no FIFO-in-the-filesystem equivalent whose open blocks, so it is a |
| 18 | +// no-op here. |
| 19 | +const openNonBlock = 0 |
| 20 | + |
11 | 21 | // fileLock is an exclusive lock backed by LockFileEx. The lock is tied to the |
12 | 22 | // open handle, so the OS releases it automatically if the process dies while |
13 | 23 | // holding it; no stale-lock cleanup is ever needed. |
14 | 24 | type fileLock struct { |
15 | 25 | f *os.File |
16 | 26 | } |
17 | 27 |
|
18 | | -// acquireFileLock blocks until an exclusive lock on path is acquired, |
19 | | -// creating the lock file if needed. |
20 | | -func acquireFileLock(path string) (*fileLock, error) { |
| 28 | +// acquireFileLock acquires an exclusive lock on path, creating the lock file |
| 29 | +// if needed. The wait is bounded: it retries a non-blocking lock until |
| 30 | +// timeout, then fails, so a stuck or hostile lock holder can never hang the |
| 31 | +// caller indefinitely. |
| 32 | +func acquireFileLock(path string, timeout time.Duration) (*fileLock, error) { |
21 | 33 | f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644) |
22 | 34 | if err != nil { |
23 | 35 | return nil, err |
24 | 36 | } |
25 | | - // Lock one byte at offset 0 (the offset lives in the Overlapped struct). |
26 | | - // Without LOCKFILE_FAIL_IMMEDIATELY this blocks until the lock is free. |
27 | | - ol := new(windows.Overlapped) |
28 | | - err = windows.LockFileEx(windows.Handle(f.Fd()), windows.LOCKFILE_EXCLUSIVE_LOCK, 0, 1, 0, ol) |
29 | | - if err != nil { |
30 | | - f.Close() |
31 | | - return nil, err |
| 37 | + deadline := time.Now().Add(timeout) |
| 38 | + for { |
| 39 | + // Lock one byte at offset 0 (the offset lives in the Overlapped |
| 40 | + // struct). LOCKFILE_FAIL_IMMEDIATELY keeps each attempt non-blocking. |
| 41 | + ol := new(windows.Overlapped) |
| 42 | + err = windows.LockFileEx(windows.Handle(f.Fd()), |
| 43 | + windows.LOCKFILE_EXCLUSIVE_LOCK|windows.LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, ol) |
| 44 | + if err == nil { |
| 45 | + return &fileLock{f: f}, nil |
| 46 | + } |
| 47 | + if err != windows.ERROR_LOCK_VIOLATION { |
| 48 | + f.Close() |
| 49 | + return nil, err |
| 50 | + } |
| 51 | + if time.Now().After(deadline) { |
| 52 | + f.Close() |
| 53 | + return nil, fmt.Errorf("timed out waiting for lock on %s", filepath.Base(path)) |
| 54 | + } |
| 55 | + time.Sleep(fileLockRetryInterval) |
32 | 56 | } |
33 | | - return &fileLock{f: f}, nil |
34 | 57 | } |
35 | 58 |
|
36 | 59 | func (l *fileLock) release() { |
|
0 commit comments