Raw
 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}