1//go:build windows
2
3package db
4
5import (
6 "errors"
7 "fmt"
8 "math"
9 "os"
10
11 "golang.org/x/sys/windows"
12)
13
14// errLockContended is returned by tryFileLock when the lock is held
15// by another process.
16var errLockContended = errors.New("file lock is held by another process")
17
18// tryFileLock takes an exclusive non-blocking lock on path via
19// LockFileEx. On success it returns a release function that unlocks
20// and closes the descriptor.
21//
22// The flags combine LOCKFILE_EXCLUSIVE_LOCK with LOCKFILE_FAIL_IMMEDIATELY
23// to mirror the BSD LOCK_EX|LOCK_NB semantics used on POSIX. The lock
24// is released when the file handle closes, including on process exit,
25// which gives us automatic stale-lock recovery without any bookkeeping.
26func tryFileLock(path string) (func(), error) {
27 f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o600)
28 if err != nil {
29 return nil, fmt.Errorf("open lock file: %w", err)
30 }
31 h := windows.Handle(f.Fd())
32 ol := new(windows.Overlapped)
33 flags := uint32(windows.LOCKFILE_EXCLUSIVE_LOCK | windows.LOCKFILE_FAIL_IMMEDIATELY)
34 if err := windows.LockFileEx(h, flags, 0, math.MaxUint32, math.MaxUint32, ol); err != nil {
35 _ = f.Close()
36 if errors.Is(err, windows.ERROR_LOCK_VIOLATION) || errors.Is(err, windows.ERROR_IO_PENDING) {
37 return nil, errLockContended
38 }
39 return nil, fmt.Errorf("LockFileEx: %w", err)
40 }
41 return func() {
42 ol := new(windows.Overlapped)
43 _ = windows.UnlockFileEx(windows.Handle(f.Fd()), 0, math.MaxUint32, math.MaxUint32, ol)
44 _ = f.Close()
45 }, nil
46}