package editor import ( "sync" "golang.org/x/sys/unix" ) // browserWatchMask covers the changes the browser list cares about: a file or // subdirectory appearing (CREATE/MOVED_TO), disappearing (DELETE/MOVED_FROM), // being rewritten (MODIFY), or having its mtime/attributes change (ATTRIB — // this is the signal for an inbound sync that updates a file in place). Any // event in this set is enough to justify a full re-read of the directory. const browserWatchMask = unix.IN_CREATE | unix.IN_DELETE | unix.IN_MOVED_TO | unix.IN_MOVED_FROM | unix.IN_MODIFY | unix.IN_ATTRIB // dirWatcher watches a single directory for filesystem changes via inotify and // reports them as coarse "something changed" tokens on its event channel. It // backs the browser's live list: while the browser page is on screen the // current directory is watched, and any event there triggers a re-read (see // Logic.refreshBrowserDir). // // Ownership: only the owner goroutine calls SetDir and Close. The run goroutine // only reads the inotify fd and sends on event. The inotify fd is the single // synchronisation point between them: Close() closes the fd, which unblocks // run()'s blocking Read so the goroutine exits promptly. curDir/curWd are // therefore owner-only (no locking needed). type dirWatcher struct { event chan struct{} fd int curDir string // directory currently watched (owner-only) curWd uint32 // its inotify watch descriptor, for removal on re-point closeOnce sync.Once } // newDirWatcher initialises an inotify instance and starts the run goroutine // (no directory is watched yet; call SetDir to begin). It returns nil if // inotify is unavailable, in which case the caller's slow backstop rescan is // the only refresh path. The returned watcher must be Closed on shutdown. func newDirWatcher(event chan struct{}) *dirWatcher { fd, err := unix.InotifyInit1(0) if err != nil { return nil } w := &dirWatcher{event: event, fd: fd} go w.run() return w } // SetDir (re)points the watch at dir, removing the previous watch. It is // best-effort: a failure leaves the prior watch in place and is covered by the // slow backstop rescan. Owner-goroutine-only. func (w *dirWatcher) SetDir(dir string) { if w == nil || dir == "" || dir == w.curDir { return } if w.curWd > 0 { unix.InotifyRmWatch(w.fd, w.curWd) // best-effort w.curWd = 0 } wd, err := unix.InotifyAddWatch(w.fd, dir, browserWatchMask) if err != nil { return } w.curDir, w.curWd = dir, uint32(wd) } // Close stops the watcher: closing the inotify fd unblocks run()'s Read (EBADF) // so the goroutine exits. Safe to call multiple times. Owner-goroutine-only. func (w *dirWatcher) Close() { if w == nil { return } w.closeOnce.Do(func() { unix.Close(w.fd) }) } // run blocks reading inotify events for the life of the fd. It does not parse // events: any non-empty read means "the watched directory changed", which is all // the browser needs (it re-reads the whole directory and re-sorts). The send is // non-blocking into a buffer-1 channel, so a burst of events coalesces to at // most one pending token — the manager's in-flight marker then bounds the // actual re-reads. func (w *dirWatcher) run() { buf := make([]byte, 16*1024) for { n, err := unix.Read(w.fd, buf) if n > 0 { select { case w.event <- struct{}{}: default: } } if err != nil { return // fd closed by Close() (EBADF) or an inotify error } } }