//go:build linux package hostfiles import ( "errors" "fmt" "io/fs" "os" "path/filepath" "sync" "sync/atomic" "unsafe" "golang.org/x/sys/unix" ) const watchMask = unix.IN_MODIFY | unix.IN_CLOSE_WRITE | unix.IN_MOVED_FROM | unix.IN_MOVED_TO | unix.IN_CREATE | unix.IN_DELETE | unix.IN_ATTRIB type inotifyWatcher struct { root string skip func(string, bool) bool f *os.File // the inotify descriptor, non-blocking, so Close unblocks Read ch chan event mu sync.Mutex wds map[int32]string // watch descriptor -> directory, relative to root ("" is root) // Coverage is tracked by counting failures, not with a flag. failures // goes up whenever a directory may have been left unwatched; repaired // is the count Repair had seen when it started the walk that // succeeded. A failure that arrives while Repair is walking is // therefore never erased by that Repair: the two differ again. failures atomic.Int64 repaired atomic.Int64 dead atomic.Bool // the reader has stopped; no event will ever arrive } // afterRepairWalk runs between Repair's walk and its bookkeeping. A test seam. var afterRepairWalk = func() {} func newWatcher(root string, skip func(string, bool) bool) (watcher, error) { fd, err := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK) if err != nil { return nil, fmt.Errorf("inotify init: %w", err) } w := &inotifyWatcher{root: root, skip: skip, f: os.NewFile(uintptr(fd), "inotify"), ch: make(chan event, 4096), wds: map[int32]string{}} if err := w.addTree(""); err != nil { w.failures.Add(1) // partial coverage: Run polls and calls Repair } go w.loop() return w, nil } func (w *inotifyWatcher) Events() <-chan event { return w.ch } func (w *inotifyWatcher) Degraded() bool { return w.dead.Load() || w.failures.Load() != w.repaired.Load() } func (w *inotifyWatcher) Close() error { return w.f.Close() } // Repair re-adds a watch on every directory. InotifyAddWatch on a path // already watched returns the same descriptor, so this is safe to repeat. // A watcher whose reader has stopped cannot be repaired. func (w *inotifyWatcher) Repair() error { if w.dead.Load() { return errors.New("the inotify reader has stopped") } seen := w.failures.Load() if err := w.addTree(""); err != nil { return err } afterRepairWalk() // Only the failures this walk could have covered are cleared. One that // arrived during the walk may concern a directory the walk had already // passed, so it leaves the watcher degraded and Repair runs again. w.repaired.Store(seen) return nil } // addTree watches rel and every directory under it. It keeps going after a // failure, so one unreadable directory does not cost the rest their // watches, and returns the first error. func (w *inotifyWatcher) addTree(rel string) error { var first error start := filepath.Join(w.root, filepath.FromSlash(rel)) err := filepath.WalkDir(start, func(full string, d fs.DirEntry, err error) error { if err != nil { if first == nil { first = err } return nil } if !d.IsDir() { return nil // WalkDir does not follow symlinks, so a linked directory is never watched } r, rerr := filepath.Rel(w.root, full) if rerr != nil { return nil } r = filepath.ToSlash(r) if r == "." { r = "" } if r != "" && w.skip != nil && w.skip(r, true) { return filepath.SkipDir } wd, aerr := unix.InotifyAddWatch(int(w.f.Fd()), full, watchMask|unix.IN_DONT_FOLLOW|unix.IN_ONLYDIR) if aerr != nil { if first == nil { first = fmt.Errorf("watch %s: %w", full, aerr) } return nil } w.mu.Lock() w.wds[int32(wd)] = r w.mu.Unlock() return nil }) if err != nil && first == nil { first = err } return first } func (w *inotifyWatcher) send(ev event) { select { case w.ch <- ev: default: // Our own buffer is full. What was dropped is a file path (a new // directory's watch is added before its event is sent), so coverage // is intact; only a scan can find the dropped path now. select { case <-w.ch: default: } select { case w.ch <- event{rescan: true}: default: } } } func (w *inotifyWatcher) loop() { defer close(w.ch) buf := make([]byte, 64<<10) for { n, err := w.f.Read(buf) if err != nil { // Closed by us, or broken. Either way nothing more will be // read: the closed channel tells Run to poll from here on. w.dead.Store(true) return } for off := 0; off+unix.SizeofInotifyEvent <= n; { raw := (*unix.InotifyEvent)(unsafe.Pointer(&buf[off])) nameLen := int(raw.Len) name := "" if nameLen > 0 { b := buf[off+unix.SizeofInotifyEvent : off+unix.SizeofInotifyEvent+nameLen] for i, c := range b { if c == 0 { b = b[:i] break } } name = string(b) } off += unix.SizeofInotifyEvent + nameLen w.handle(raw.Wd, raw.Mask, name) } } } func (w *inotifyWatcher) handle(wd int32, mask uint32, name string) { if mask&unix.IN_Q_OVERFLOW != 0 { // Events were dropped, and one of them may have been a new // directory, which is then unwatched. Coverage is uncertain until // Repair has walked the tree again. w.failures.Add(1) w.send(event{rescan: true}) return } w.mu.Lock() dir, known := w.wds[wd] if mask&unix.IN_IGNORED != 0 { delete(w.wds, wd) } w.mu.Unlock() if !known || name == "" { return } rel := name if dir != "" { rel = dir + "/" + name } if mask&unix.IN_ISDIR != 0 { if mask&(unix.IN_CREATE|unix.IN_MOVED_TO) != 0 { if w.skip != nil && w.skip(rel, true) { return } if err := w.addTree(rel); err != nil { w.failures.Add(1) } } // Files may have appeared in the new directory before its watch // did, and a directory moved away takes its files with it: only a // scan finds either. w.send(event{rescan: true}) return } if w.skip != nil && w.skip(rel, false) { return } w.send(event{rel: rel}) }