1package lockfile
2
3import (
4 "errors"
5 "fmt"
6 "io/fs"
7 "os"
8 "syscall"
9)
10
11type Lock struct {
12 file *os.File
13}
14
15func Acquire(path, noun, holder string) (*Lock, error) {
16 file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|syscall.O_NOFOLLOW|syscall.O_CLOEXEC, 0o600)
17 if err != nil {
18 return nil, fmt.Errorf("open %s %s: %w", noun, path, err)
19 }
20 fi, err := file.Stat()
21 if err != nil {
22 file.Close()
23 return nil, fmt.Errorf("inspect %s %s: %w", noun, path, err)
24 }
25 if !fi.Mode().IsRegular() {
26 file.Close()
27 return nil, fmt.Errorf("%s %s is a %s, not a regular file", noun, path, DescribeType(fi.Mode()))
28 }
29 for {
30 err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
31 if err == nil {
32 return &Lock{file: file}, nil
33 }
34 if err == syscall.EINTR {
35 continue
36 }
37 file.Close()
38 if err == syscall.EWOULDBLOCK || err == syscall.EAGAIN {
39 return nil, fmt.Errorf("%s %s is held by another process; %s", noun, path, holder)
40 }
41 return nil, fmt.Errorf("acquire %s %s: %w", noun, path, err)
42 }
43}
44
45func (l *Lock) Release() error {
46 unlockErr := syscall.Flock(int(l.file.Fd()), syscall.LOCK_UN)
47 return errors.Join(unlockErr, l.file.Close())
48}
49
50func DescribeType(m fs.FileMode) string {
51 switch {
52 case m.IsRegular():
53 return "regular file"
54 case m.IsDir():
55 return "directory"
56 case m&fs.ModeSymlink != 0:
57 return "symbolic link"
58 case m&fs.ModeSocket != 0:
59 return "socket"
60 case m&fs.ModeNamedPipe != 0:
61 return "named pipe"
62 case m&fs.ModeDevice != 0:
63 return "device"
64 }
65 return "file of type " + m.Type().String()
66}