428 lines
14 KiB
Go
428 lines
14 KiB
Go
//go:build linux
|
|
|
|
package fswatch
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync/atomic"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// inotify_linux.go: Linux inotify backend
|
|
//
|
|
// Uses the kernel's inotify(7) subsystem to watch directory trees. A single
|
|
// inotify instance serves all subscriptions for the process lifetime.
|
|
//
|
|
// ┌───────────────────────────────────────────────────────────┐
|
|
// │ inotifyBackend │
|
|
// │ │
|
|
// │ ┌───────────┐ poll(2) ┌─────────────────┐ │
|
|
// │ │ pipe[0] ├──────────────────────►│ │ │
|
|
// │ │ (wakeup) │ │ start() │ │
|
|
// │ └───────────┘ │ goroutine │ │
|
|
// │ ┌───────────┐ │ (event loop) │ │
|
|
// │ │ inotify ├──────────────────────►│ │ │
|
|
// │ │ fd │ └────────┬────────┘ │
|
|
// │ └───────────┘ │ │
|
|
// │ handleEvents() │
|
|
// │ │ │
|
|
// │ ▼ │
|
|
// │ ┌─────────────────────────┐ │
|
|
// │ │ subscriptions │ │
|
|
// │ │ map[wd] → []sub │ │
|
|
// │ │ sub.dirWatch.events │ │
|
|
// │ └─────────────────────────┘ │
|
|
// └───────────────────────────────────────────────────────────┘
|
|
//
|
|
// Goroutines and threading:
|
|
// - One long-lived goroutine (start), launched by watcherBase.run(). It
|
|
// owns the poll(2) loop and runs for the process lifetime. All event
|
|
// reading and dispatch (handleEvents, handleEvent, handleSubscription)
|
|
// execute on this goroutine, under b.mu.
|
|
// - subscribe/closeWatch run on the caller's goroutine under
|
|
// watcherBase.mu. The event loop acquires b.mu for watch map
|
|
// access, providing safe interleaving.
|
|
//
|
|
// Callback delivery:
|
|
// dirWatch.notify() posts to the shared process-wide debouncer. After a
|
|
// coalescing window (50 ms min / 500 ms max), the debouncer invokes all
|
|
// registered WatchCallbacks on its own dedicated goroutine; never on
|
|
// the caller's goroutine or the event-loop goroutine.
|
|
//
|
|
// WatchDirectory flow:
|
|
// 1. Walk the target directory (caller goroutine).
|
|
// 2. For every directory found, call inotify_add_watch to obtain a
|
|
// watch descriptor (wd). Map wd → inotifySubscription.
|
|
//
|
|
// Event dispatch (handleEvents → handleSubscription, on start goroutine):
|
|
// - IN_CREATE / IN_MOVED_TO → events.create (→ EventUpdate); if the new
|
|
// entry is a directory (IN_ISDIR), recursively walk and watch it.
|
|
// - IN_MODIFY → events.update.
|
|
// - IN_DELETE* / IN_MOVE* → events.remove; drop inotify subscriptions
|
|
// for the removed path and any descendants.
|
|
// - IN_Q_OVERFLOW → set ErrOverflow on every active dirWatch.
|
|
// After processing all buffered events, call dirWatch.notify() on each
|
|
// touched dirWatch to trigger the debouncer.
|
|
//
|
|
// Shutdown:
|
|
// Write a byte to pipe[1] → poll sees POLLIN on pipe[0] → loop exits →
|
|
// deferred closeFDs closes inotify fd, pipe fds, and signals endedSignal.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const (
|
|
inotifyMask = unix.IN_CREATE |
|
|
unix.IN_DELETE |
|
|
unix.IN_DELETE_SELF |
|
|
unix.IN_MODIFY |
|
|
unix.IN_MOVE_SELF |
|
|
unix.IN_MOVED_FROM |
|
|
unix.IN_MOVED_TO |
|
|
unix.IN_DONT_FOLLOW |
|
|
unix.IN_ONLYDIR |
|
|
unix.IN_EXCL_UNLINK
|
|
inotifyBufferSize = 8192
|
|
)
|
|
|
|
// inotifySubscription.
|
|
type inotifySubscription struct {
|
|
path string
|
|
watchPath string
|
|
dirWatch *dirWatch
|
|
wd int
|
|
}
|
|
|
|
// inotifyBackend.
|
|
type inotifyBackend struct {
|
|
watcherBase
|
|
|
|
pipeFDs [2]int
|
|
// pipeWriteFD shadows pipeFDs[1] as an atomic so shutdown (any goroutine)
|
|
// can safely race against the start goroutine's deferred closeFDs.
|
|
// Sentinel -1 once closed.
|
|
pipeWriteFD atomic.Int32
|
|
inotify int
|
|
subscriptions map[int][]*inotifySubscription // multimap<wd, sub>
|
|
endedSignal chan struct{}
|
|
|
|
// Persistent buffers reused across handleEvents calls. Only accessed
|
|
// from the start goroutine, so no synchronization needed.
|
|
readBuf []byte
|
|
watchersTouched map[*dirWatch]struct{}
|
|
}
|
|
|
|
func init() {
|
|
inotifyWatcher.factory = func() watcherImpl { return newInotifyBackend() }
|
|
}
|
|
|
|
func newInotifyBackend() *inotifyBackend {
|
|
b := &inotifyBackend{
|
|
pipeFDs: [2]int{-1, -1},
|
|
inotify: -1,
|
|
subscriptions: map[int][]*inotifySubscription{},
|
|
endedSignal: make(chan struct{}),
|
|
readBuf: make([]byte, inotifyBufferSize),
|
|
watchersTouched: make(map[*dirWatch]struct{}),
|
|
}
|
|
b.pipeWriteFD.Store(-1)
|
|
b.watcherBase.init(b)
|
|
return b
|
|
}
|
|
|
|
// start mirrors `inotifyBackend::start`.
|
|
func (b *inotifyBackend) start() error {
|
|
// Create a pipe so we can wake the poll(2) loop on shutdown.
|
|
if err := unix.Pipe2(b.pipeFDs[:], unix.O_CLOEXEC|unix.O_NONBLOCK); err != nil {
|
|
return fmt.Errorf("unable to open pipe: %w", err)
|
|
}
|
|
b.pipeWriteFD.Store(int32(b.pipeFDs[1]))
|
|
defer func() {
|
|
b.closeFDs()
|
|
close(b.endedSignal)
|
|
}()
|
|
fd, err := unix.InotifyInit1(unix.IN_NONBLOCK | unix.IN_CLOEXEC)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to initialize inotify: %w", err)
|
|
}
|
|
b.inotify = fd
|
|
|
|
pollfds := []unix.PollFd{
|
|
{Fd: int32(b.pipeFDs[0]), Events: unix.POLLIN},
|
|
{Fd: int32(b.inotify), Events: unix.POLLIN},
|
|
}
|
|
|
|
b.notifyStarted()
|
|
|
|
for {
|
|
_, err := unix.Poll(pollfds, 500)
|
|
if err != nil {
|
|
if errors.Is(err, unix.EINTR) {
|
|
continue
|
|
}
|
|
return fmt.Errorf("unable to poll: %w", err)
|
|
}
|
|
if pollfds[0].Revents != 0 {
|
|
break
|
|
}
|
|
if pollfds[1].Revents != 0 {
|
|
if err := b.handleEvents(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// closeFDs runs in the start goroutine after the poll loop exits. Takes
|
|
// b.mu so the writes to b.inotify / b.pipeFDs synchronize-against the
|
|
// reads in closeWatch / subscribe (both of which run under b.mu).
|
|
func (b *inotifyBackend) closeFDs() {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
if b.pipeFDs[0] >= 0 {
|
|
_ = unix.Close(b.pipeFDs[0])
|
|
b.pipeFDs[0] = -1
|
|
}
|
|
if fd := b.pipeWriteFD.Swap(-1); fd >= 0 {
|
|
_ = unix.Close(int(fd))
|
|
}
|
|
b.pipeFDs[1] = -1
|
|
if b.inotify >= 0 {
|
|
_ = unix.Close(b.inotify)
|
|
b.inotify = -1
|
|
}
|
|
}
|
|
|
|
// shutdown is the equivalent of the destructor's pipe-write+wait.
|
|
// Called by removeSharedBackend when the last watch drops. Reads
|
|
// the pipe write fd via atomic so it's safe to race against the start
|
|
// goroutine's deferred closeFDs.
|
|
func (b *inotifyBackend) shutdown() {
|
|
fd := b.pipeWriteFD.Load()
|
|
if fd < 0 {
|
|
return
|
|
}
|
|
_, _ = unix.Write(int(fd), []byte{'X'})
|
|
<-b.endedSignal
|
|
}
|
|
|
|
// subscribe mirrors `inotifyBackend::subscribe`. Called via the watcherBase
|
|
// virtual dispatch under b.mu (so it's serialized against handleEvent).
|
|
func (b *inotifyBackend) subscribe(w *dirWatch) error {
|
|
if !w.recursive {
|
|
if _, err := b.watchDir(w, w.dir, w.physicalDir); err != nil {
|
|
return &dirWatchError{
|
|
err: fmt.Errorf("inotify_add_watch on '%s' failed: %w", w.dir, err),
|
|
dirWatch: w,
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := walkDir(w.physicalDir, true, func(watchPath string, isDir bool) error {
|
|
if !isDir {
|
|
return nil
|
|
}
|
|
path := w.displayPath(watchPath)
|
|
if _, err := b.watchDir(w, path, watchPath); err != nil {
|
|
return &dirWatchError{
|
|
err: fmt.Errorf("inotify_add_watch on '%s' failed: %w", path, err),
|
|
dirWatch: w,
|
|
}
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
_ = b.closeWatch(w)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// watchDir registers an inotify watch on path and records the resulting
|
|
// subscription. Returns the kernel watch descriptor on success.
|
|
func (b *inotifyBackend) watchDir(w *dirWatch, path string, watchPath string) (int, error) {
|
|
wd, err := unix.InotifyAddWatch(b.inotify, watchPath, inotifyMask)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
sub := &inotifySubscription{path: path, watchPath: watchPath, dirWatch: w, wd: wd}
|
|
b.subscriptions[wd] = append(b.subscriptions[wd], sub)
|
|
return wd, nil
|
|
}
|
|
|
|
// handleEvents mirrors `inotifyBackend::handleEvents`.
|
|
func (b *inotifyBackend) handleEvents() error {
|
|
buf := b.readBuf
|
|
watchersTouched := b.watchersTouched
|
|
|
|
for {
|
|
n, err := unix.Read(b.inotify, buf)
|
|
if err != nil {
|
|
if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EWOULDBLOCK) {
|
|
break
|
|
}
|
|
return fmt.Errorf("Error reading from inotify: %w", err)
|
|
}
|
|
if n == 0 {
|
|
break
|
|
}
|
|
// Walk the buffer.
|
|
for offset := 0; offset < n; {
|
|
ev := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset]))
|
|
recordSize := unix.SizeofInotifyEvent + int(ev.Len)
|
|
var name string
|
|
if ev.Len > 0 {
|
|
// Name is NUL-terminated; trim trailing zeros.
|
|
nameBytes := buf[offset+unix.SizeofInotifyEvent : offset+recordSize]
|
|
for i, c := range nameBytes {
|
|
if c == 0 {
|
|
nameBytes = nameBytes[:i]
|
|
break
|
|
}
|
|
}
|
|
name = string(nameBytes)
|
|
}
|
|
|
|
if ev.Mask&unix.IN_Q_OVERFLOW != 0 {
|
|
b.mu.Lock()
|
|
for _, subs := range b.subscriptions {
|
|
for _, sub := range subs {
|
|
sub.dirWatch.events.setError(ErrOverflow)
|
|
watchersTouched[sub.dirWatch] = struct{}{}
|
|
}
|
|
}
|
|
b.mu.Unlock()
|
|
offset += recordSize
|
|
continue
|
|
}
|
|
|
|
b.handleEvent(ev, name, watchersTouched)
|
|
offset += recordSize
|
|
}
|
|
}
|
|
for w := range watchersTouched {
|
|
w.notify()
|
|
}
|
|
clear(watchersTouched)
|
|
return nil
|
|
}
|
|
|
|
// handleEvent mirrors `inotifyBackend::handleEvent`.
|
|
func (b *inotifyBackend) handleEvent(ev *unix.InotifyEvent, name string, touched map[*dirWatch]struct{}) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
// b.subscriptions[wd] holds at most one entry per *inotifySubscription
|
|
// pointer (watchDir always appends a fresh struct), so no dedup is
|
|
// necessary; the upstream C++ used an unordered_set keyed by
|
|
// shared_ptr identity but the equivalent Go invariant is structural.
|
|
for _, s := range b.subscriptions[int(ev.Wd)] {
|
|
if b.handleSubscription(ev, name, s) {
|
|
touched[s.dirWatch] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleSubscription mirrors `inotifyBackend::handleSubscription`.
|
|
func (b *inotifyBackend) handleSubscription(ev *unix.InotifyEvent, name string, sub *inotifySubscription) bool {
|
|
w := sub.dirWatch
|
|
path := sub.path
|
|
watchPath := sub.watchPath
|
|
isDir := ev.Mask&unix.IN_ISDIR != 0
|
|
if name != "" {
|
|
path = path + "/" + name
|
|
watchPath = watchPath + "/" + name
|
|
}
|
|
|
|
switch {
|
|
case ev.Mask&(unix.IN_CREATE|unix.IN_MOVED_TO) != 0:
|
|
w.events.create(path)
|
|
if isDir && w.recursive {
|
|
_ = walkDir(watchPath, true, func(p string, pIsDir bool) error {
|
|
if !pIsDir {
|
|
return nil
|
|
}
|
|
_, _ = b.watchDir(w, w.displayPath(p), p)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
case ev.Mask&unix.IN_MODIFY != 0:
|
|
w.events.update(path)
|
|
|
|
case ev.Mask&(unix.IN_DELETE|unix.IN_DELETE_SELF|unix.IN_MOVED_FROM|unix.IN_MOVE_SELF) != 0:
|
|
isSelfEvent := ev.Mask&(unix.IN_DELETE_SELF|unix.IN_MOVE_SELF) != 0
|
|
// Ignore delete/move self events unless this is the watch root.
|
|
if isSelfEvent && path != w.dir {
|
|
return false
|
|
}
|
|
// If deleted item is a dir, drop matching subscriptions.
|
|
// XXX: self events don't have IN_ISDIR set.
|
|
if isSelfEvent || isDir {
|
|
for wd, list := range b.subscriptions {
|
|
kept := list[:0]
|
|
for _, s := range list {
|
|
if s.path == path || (len(s.path) > len(path) && s.path[len(path)] == '/' && s.path[:len(path)] == path) {
|
|
continue
|
|
}
|
|
kept = append(kept, s)
|
|
}
|
|
if len(kept) == 0 {
|
|
_, _ = unix.InotifyRmWatch(b.inotify, uint32(wd))
|
|
delete(b.subscriptions, wd)
|
|
} else {
|
|
b.subscriptions[wd] = kept
|
|
}
|
|
}
|
|
}
|
|
w.events.remove(path)
|
|
// If the watched root itself is gone the kernel has already
|
|
// auto-removed every wd associated with this dirWatch and no
|
|
// further events will fire. Surface ErrWatchTerminated so the
|
|
// caller knows to clean up; the delete event above still
|
|
// flows through the same callback.
|
|
if isSelfEvent && path == w.dir {
|
|
w.events.setError(fmt.Errorf("%w: watched directory removed", ErrWatchTerminated))
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// closeWatch mirrors `inotifyBackend::closeWatch`. Iterates every wd that
|
|
// referenced w and removes the matching subscriptions. If a kernel
|
|
// InotifyRmWatch fails we keep processing remaining wds and return the
|
|
// first error encountered; bailing early would leave the internal state
|
|
// half-cleaned and the caller's dirWatch hanging off other wds.
|
|
func (b *inotifyBackend) closeWatch(w *dirWatch) error {
|
|
var firstErr error
|
|
for wd, list := range b.subscriptions {
|
|
kept := list[:0]
|
|
removedAny := false
|
|
for _, s := range list {
|
|
if s.dirWatch == w {
|
|
removedAny = true
|
|
continue
|
|
}
|
|
kept = append(kept, s)
|
|
}
|
|
if !removedAny {
|
|
continue
|
|
}
|
|
if len(kept) == 0 {
|
|
if _, err := unix.InotifyRmWatch(b.inotify, uint32(wd)); err != nil && firstErr == nil {
|
|
firstErr = &dirWatchError{
|
|
err: fmt.Errorf("unable to remove dirWatch: %w", err),
|
|
dirWatch: w,
|
|
}
|
|
}
|
|
delete(b.subscriptions, wd)
|
|
} else {
|
|
b.subscriptions[wd] = kept
|
|
}
|
|
}
|
|
return firstErr
|
|
}
|