1package kvconf
2
3import (
4 "errors"
5 "os"
6 "sync/atomic"
7 "syscall"
8 "time"
9 "unsafe"
10)
11
12// inotify watches directories with Linux inotify. Its descriptor goes
13// through the Go runtime poller, so a wait costs no thread and is woken by a
14// read deadline.
15type inotify struct {
16 f *os.File
17 fd int
18 dirs map[int32]string // watch descriptor -> directory
19 wds map[string]int32
20 buf []byte
21 woken atomic.Bool
22 // names interns event names: the same few files change again and again.
23 names map[string]string
24}
25
26const movedMask = syscall.IN_CREATE | syscall.IN_DELETE | syscall.IN_MOVED_FROM | syscall.IN_MOVED_TO
27
28const inotifyMask = syscall.IN_CLOSE_WRITE | syscall.IN_MODIFY | syscall.IN_ATTRIB |
29 syscall.IN_CREATE | syscall.IN_DELETE | syscall.IN_MOVED_FROM | syscall.IN_MOVED_TO |
30 syscall.IN_DELETE_SELF | syscall.IN_MOVE_SELF | syscall.IN_ONLYDIR
31
32func newNative() (backend, error) {
33 fd, err := syscall.InotifyInit1(syscall.IN_CLOEXEC | syscall.IN_NONBLOCK)
34 if err != nil {
35 return nil, os.NewSyscallError("inotify_init1", err)
36 }
37 return &inotify{
38 f: os.NewFile(uintptr(fd), "inotify"),
39 fd: fd,
40 dirs: map[int32]string{},
41 wds: map[string]int32{},
42 buf: make([]byte, 16<<10),
43 names: map[string]string{},
44 }, nil
45}
46
47func (in *inotify) set(targets []*target) (missing bool, err error) {
48 want := make(map[string]bool, len(targets))
49 for _, t := range targets {
50 want[t.dir] = true
51 }
52 // Keep unchanged watches: removing one queues an IN_IGNORED.
53 for dir, wd := range in.wds {
54 if !want[dir] {
55 _, _ = syscall.InotifyRmWatch(in.fd, uint32(wd))
56 delete(in.wds, dir)
57 delete(in.dirs, wd)
58 }
59 }
60 for dir := range want {
61 // Always add: a directory renamed away and replaced keeps its path
62 // but not its inode, and adding returns the watch of the current
63 // one (the same descriptor when nothing moved).
64 wd, err := syscall.InotifyAddWatch(in.fd, dir, inotifyMask)
65 if old, ok := in.wds[dir]; ok && (err != nil || old != int32(wd)) {
66 _, _ = syscall.InotifyRmWatch(in.fd, uint32(old))
67 delete(in.wds, dir)
68 delete(in.dirs, old)
69 }
70 if errors.Is(err, syscall.ENOENT) || errors.Is(err, syscall.ENOTDIR) {
71 missing = true
72 continue
73 }
74 if err != nil {
75 return missing, os.NewSyscallError("inotify_add_watch", err)
76 }
77 in.wds[dir] = int32(wd)
78 in.dirs[int32(wd)] = dir
79 }
80 return missing, nil
81}
82
83func (in *inotify) wait(timeout time.Duration, evs []event) ([]event, error) {
84 var deadline time.Time
85 if timeout >= 0 {
86 deadline = time.Now().Add(timeout)
87 }
88 if err := in.f.SetReadDeadline(deadline); err != nil {
89 return evs, err
90 }
91 if in.woken.Load() {
92 return evs, nil
93 }
94 n, err := in.f.Read(in.buf)
95 if errors.Is(err, os.ErrDeadlineExceeded) {
96 return evs, nil
97 }
98 if err != nil {
99 return evs, err
100 }
101 for off := 0; off+syscall.SizeofInotifyEvent <= n; {
102 ev := (*syscall.InotifyEvent)(unsafe.Pointer(&in.buf[off]))
103 size := syscall.SizeofInotifyEvent + int(ev.Len)
104 if off+size > n {
105 break
106 }
107 name := in.buf[off+syscall.SizeofInotifyEvent : off+size]
108 off += size
109 if ev.Mask&syscall.IN_Q_OVERFLOW != 0 {
110 evs = append(evs, event{}) // events were lost: check everything
111 continue
112 }
113 dir, ok := in.dirs[ev.Wd]
114 if !ok {
115 continue // a watch we removed
116 }
117 if ev.Mask&(syscall.IN_IGNORED|syscall.IN_DELETE_SELF|syscall.IN_MOVE_SELF) != 0 {
118 // The watch follows the inode, not the path: drop it.
119 if ev.Mask&syscall.IN_IGNORED == 0 {
120 _, _ = syscall.InotifyRmWatch(in.fd, uint32(ev.Wd))
121 }
122 delete(in.dirs, ev.Wd)
123 if in.wds[dir] == ev.Wd {
124 delete(in.wds, dir)
125 }
126 evs = append(evs, event{dir: dir, lost: true})
127 continue
128 }
129 for len(name) > 0 && name[len(name)-1] == 0 {
130 name = name[:len(name)-1]
131 }
132 if len(name) > 0 {
133 evs = append(evs, event{dir: dir, name: in.intern(name), moved: ev.Mask&movedMask != 0})
134 }
135 }
136 return evs, nil
137}
138
139// intern returns name as a string without allocating once it was seen.
140func (in *inotify) intern(name []byte) string {
141 if s, ok := in.names[string(name)]; ok { // no allocation for the lookup
142 return s
143 }
144 s := string(name)
145 if len(in.names) < 1024 { // a directory full of churn cannot grow it forever
146 in.names[s] = s
147 }
148 return s
149}
150
151func (in *inotify) wake() {
152 in.woken.Store(true)
153 _ = in.f.SetReadDeadline(time.Now())
154}
155
156func (in *inotify) close() error { return in.f.Close() }