carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package hostfiles
4
5import (
6 "errors"
7 "fmt"
8 "io/fs"
9 "os"
10 "path/filepath"
11 "sync"
12 "sync/atomic"
13 "unsafe"
14
15 "golang.org/x/sys/unix"
16)
17
18const watchMask = unix.IN_MODIFY | unix.IN_CLOSE_WRITE | unix.IN_MOVED_FROM | unix.IN_MOVED_TO |
19 unix.IN_CREATE | unix.IN_DELETE | unix.IN_ATTRIB
20
21type inotifyWatcher struct {
22 root string
23 skip func(string, bool) bool
24 f *os.File // the inotify descriptor, non-blocking, so Close unblocks Read
25 ch chan event
26
27 mu sync.Mutex
28 wds map[int32]string // watch descriptor -> directory, relative to root ("" is root)
29
30 // Coverage is tracked by counting failures, not with a flag. failures
31 // goes up whenever a directory may have been left unwatched; repaired
32 // is the count Repair had seen when it started the walk that
33 // succeeded. A failure that arrives while Repair is walking is
34 // therefore never erased by that Repair: the two differ again.
35 failures atomic.Int64
36 repaired atomic.Int64
37 dead atomic.Bool // the reader has stopped; no event will ever arrive
38}
39
40// afterRepairWalk runs between Repair's walk and its bookkeeping. A test seam.
41var afterRepairWalk = func() {}
42
43func newWatcher(root string, skip func(string, bool) bool) (watcher, error) {
44 fd, err := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
45 if err != nil {
46 return nil, fmt.Errorf("inotify init: %w", err)
47 }
48 w := &inotifyWatcher{root: root, skip: skip, f: os.NewFile(uintptr(fd), "inotify"),
49 ch: make(chan event, 4096), wds: map[int32]string{}}
50 if err := w.addTree(""); err != nil {
51 w.failures.Add(1) // partial coverage: Run polls and calls Repair
52 }
53 go w.loop()
54 return w, nil
55}
56
57func (w *inotifyWatcher) Events() <-chan event { return w.ch }
58func (w *inotifyWatcher) Degraded() bool {
59 return w.dead.Load() || w.failures.Load() != w.repaired.Load()
60}
61func (w *inotifyWatcher) Close() error { return w.f.Close() }
62
63// Repair re-adds a watch on every directory. InotifyAddWatch on a path
64// already watched returns the same descriptor, so this is safe to repeat.
65// A watcher whose reader has stopped cannot be repaired.
66func (w *inotifyWatcher) Repair() error {
67 if w.dead.Load() {
68 return errors.New("the inotify reader has stopped")
69 }
70 seen := w.failures.Load()
71 if err := w.addTree(""); err != nil {
72 return err
73 }
74 afterRepairWalk()
75 // Only the failures this walk could have covered are cleared. One that
76 // arrived during the walk may concern a directory the walk had already
77 // passed, so it leaves the watcher degraded and Repair runs again.
78 w.repaired.Store(seen)
79 return nil
80}
81
82// addTree watches rel and every directory under it. It keeps going after a
83// failure, so one unreadable directory does not cost the rest their
84// watches, and returns the first error.
85func (w *inotifyWatcher) addTree(rel string) error {
86 var first error
87 start := filepath.Join(w.root, filepath.FromSlash(rel))
88 err := filepath.WalkDir(start, func(full string, d fs.DirEntry, err error) error {
89 if err != nil {
90 if first == nil {
91 first = err
92 }
93 return nil
94 }
95 if !d.IsDir() {
96 return nil // WalkDir does not follow symlinks, so a linked directory is never watched
97 }
98 r, rerr := filepath.Rel(w.root, full)
99 if rerr != nil {
100 return nil
101 }
102 r = filepath.ToSlash(r)
103 if r == "." {
104 r = ""
105 }
106 if r != "" && w.skip != nil && w.skip(r, true) {
107 return filepath.SkipDir
108 }
109 wd, aerr := unix.InotifyAddWatch(int(w.f.Fd()), full, watchMask|unix.IN_DONT_FOLLOW|unix.IN_ONLYDIR)
110 if aerr != nil {
111 if first == nil {
112 first = fmt.Errorf("watch %s: %w", full, aerr)
113 }
114 return nil
115 }
116 w.mu.Lock()
117 w.wds[int32(wd)] = r
118 w.mu.Unlock()
119 return nil
120 })
121 if err != nil && first == nil {
122 first = err
123 }
124 return first
125}
126
127func (w *inotifyWatcher) send(ev event) {
128 select {
129 case w.ch <- ev:
130 default:
131 // Our own buffer is full. What was dropped is a file path (a new
132 // directory's watch is added before its event is sent), so coverage
133 // is intact; only a scan can find the dropped path now.
134 select {
135 case <-w.ch:
136 default:
137 }
138 select {
139 case w.ch <- event{rescan: true}:
140 default:
141 }
142 }
143}
144
145func (w *inotifyWatcher) loop() {
146 defer close(w.ch)
147 buf := make([]byte, 64<<10)
148 for {
149 n, err := w.f.Read(buf)
150 if err != nil {
151 // Closed by us, or broken. Either way nothing more will be
152 // read: the closed channel tells Run to poll from here on.
153 w.dead.Store(true)
154 return
155 }
156 for off := 0; off+unix.SizeofInotifyEvent <= n; {
157 raw := (*unix.InotifyEvent)(unsafe.Pointer(&buf[off]))
158 nameLen := int(raw.Len)
159 name := ""
160 if nameLen > 0 {
161 b := buf[off+unix.SizeofInotifyEvent : off+unix.SizeofInotifyEvent+nameLen]
162 for i, c := range b {
163 if c == 0 {
164 b = b[:i]
165 break
166 }
167 }
168 name = string(b)
169 }
170 off += unix.SizeofInotifyEvent + nameLen
171 w.handle(raw.Wd, raw.Mask, name)
172 }
173 }
174}
175
176func (w *inotifyWatcher) handle(wd int32, mask uint32, name string) {
177 if mask&unix.IN_Q_OVERFLOW != 0 {
178 // Events were dropped, and one of them may have been a new
179 // directory, which is then unwatched. Coverage is uncertain until
180 // Repair has walked the tree again.
181 w.failures.Add(1)
182 w.send(event{rescan: true})
183 return
184 }
185 w.mu.Lock()
186 dir, known := w.wds[wd]
187 if mask&unix.IN_IGNORED != 0 {
188 delete(w.wds, wd)
189 }
190 w.mu.Unlock()
191 if !known || name == "" {
192 return
193 }
194 rel := name
195 if dir != "" {
196 rel = dir + "/" + name
197 }
198 if mask&unix.IN_ISDIR != 0 {
199 if mask&(unix.IN_CREATE|unix.IN_MOVED_TO) != 0 {
200 if w.skip != nil && w.skip(rel, true) {
201 return
202 }
203 if err := w.addTree(rel); err != nil {
204 w.failures.Add(1)
205 }
206 }
207 // Files may have appeared in the new directory before its watch
208 // did, and a directory moved away takes its files with it: only a
209 // scan finds either.
210 w.send(event{rescan: true})
211 return
212 }
213 if w.skip != nil && w.skip(rel, false) {
214 return
215 }
216 w.send(event{rel: rel})
217}
218