vendor tsgo
This commit is contained in:
427
tools/tsgo/internal/fswatch/inotify_linux.go
Normal file
427
tools/tsgo/internal/fswatch/inotify_linux.go
Normal file
@@ -0,0 +1,427 @@
|
||||
//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
|
||||
}
|
||||
Reference in New Issue
Block a user