Replace the 1 s polling re-read of the browser directory with an inotify watch (dirWatcher, internal/editor/dirwatcher.go) using golang.org/x/sys/unix, which is already in the build list — no new dependency, no go.mod change. - The watcher watches the current browser directory and coalesces a burst of events to at most one token; the owner re-reads on the token. Create, delete, rename, modify, and attrib (mtime) changes are all covered, so an inbound sync that touches a file's mtime re-sorts the list in the current sort mode. - The watch is re-pointed when the directory changes (a no-op while the path is unchanged) and is closed when the logic goroutine exits (covers both Shutdown and the test harness Done+WaitForExit path). - A slow 30 s backstop rescan remains as a safety net for the rare event the FUSE inotify layer drops (syncthing uses the same inotify+rescan pattern on Android). The change-detection from the previous commit means the backstop emits no frame when nothing changed, so TestNoFramesWhileIdle_Browser still holds. - If inotify is unavailable (newDirWatcher returns nil) or a watch add fails, the 30 s backstop is the only path — a graceful degradation, not a break. Verified on-device (Notes dir, Date-descending): an external add appears at the top in well under a second (impossibly fast for the 30 s backstop, so the inotify path); touching a file's mtime re-sorts it to the top; a delete drops the row. All sub-second.
99 lines
3.4 KiB
Go
99 lines
3.4 KiB
Go
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
|
|
}
|
|
}
|
|
}
|