641 lines
20 KiB
Go
641 lines
20 KiB
Go
//go:build darwin && (amd64 || arm64)
|
|
|
|
package fswatch
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"runtime"
|
|
"slices"
|
|
"sort"
|
|
"sync"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// fsevents_darwin.go: macOS FSEvents backend (event processing)
|
|
//
|
|
// Uses Apple's FSEvents API to receive file-level notifications for watched
|
|
// directory trees. FSEvents is a high-level, path-based API that watches
|
|
// recursively without requiring an fd per file (unlike kqueue). Events are
|
|
// coalesced by the kernel and delivered in batches.
|
|
//
|
|
// This file contains the event classification and stream lifecycle logic.
|
|
// The low-level FFI plumbing (cgo-free CoreFoundation/CoreServices calls,
|
|
// assembly trampolines, pipe-based callback synchronization) lives in
|
|
// fsevents_darwin_ffi.go and the companion .s files.
|
|
//
|
|
// ┌───────────────────────────────────────────────────────────┐
|
|
// │ fsEventsBackend │
|
|
// │ (no event loop; start() just signals readiness) │
|
|
// │ │
|
|
// │ subscribe/closeWatch rebuild the shared stream set: │
|
|
// │ │ │
|
|
// │ ▼ │
|
|
// │ ┌─────────────────────────────────────────────────────┐ │
|
|
// │ │ fseventsStream[] │ │
|
|
// │ │ │ │
|
|
// │ │ each FSEventStream watches up to N paths ────────► │ │
|
|
// │ │ per-stream GCD dispatch queue │ │
|
|
// │ │ (UseCFTypes | FileEvents = 0x11) │ │
|
|
// │ │ │ │
|
|
// │ │ callback fires on GCD thread: │ │
|
|
// │ │ ┌─────────────────────────────────────────┐ │ │
|
|
// │ │ │ asm: retain/copy callback payload │ │ │
|
|
// │ │ │ asm: write(eventPipe) ──────────────► │ │ │
|
|
// │ │ │ eventLoop() │ │ │
|
|
// │ │ │ goroutine │ │ │
|
|
// │ │ │ │ │ │ │
|
|
// │ │ │ fsEventsCallback() │ │ │
|
|
// │ │ │ asm: return to FSEvents │ │ │
|
|
// │ │ └─────────────────────────────────────────┘ │ │
|
|
// │ └─────────────────────────────────────────────────────┘ │
|
|
// └───────────────────────────────────────────────────────────┘
|
|
//
|
|
// Goroutines and threading:
|
|
// - FSEvents delivers the raw C callback on a GCD dispatch queue thread
|
|
// (an OS thread managed by libdispatch, not a Go goroutine).
|
|
// - The assembly callback (fsEventsCallbackASM, in the .s files) runs on
|
|
// that GCD thread in the C calling convention. It never enters Go ABI.
|
|
// It retains/copies the callback payload and passes it to Go through
|
|
// eventPipe.
|
|
// - One Go goroutine per stream chunk (eventLoop, in fsevents_darwin_ffi.go)
|
|
// blocks on eventFile.Read(), integrated with Go's netpoll so it parks
|
|
// without consuming an OS thread. When woken by the asm callback, it calls
|
|
// fsEventsCallback() to classify events and route them to matching
|
|
// dirWatch event lists.
|
|
// - subscribe/closeWatch rebuild the stream chunks on the caller's goroutine
|
|
// under watcherBase.mu. Old streams are swapped out before teardown so a
|
|
// callback cannot deadlock against stream teardown while routing events.
|
|
//
|
|
// 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 GCD thread, the eventLoop goroutine, or the caller's goroutine.
|
|
// On all backends, events matching a WithIgnore function are filtered
|
|
// per-subscriber before delivery.
|
|
//
|
|
// WatchDirectory flow (caller goroutine):
|
|
// subscribe/closeWatch snapshots active dirWatches and creates one or more
|
|
// FSEventStreams with kFSEventStreamEventIdSinceNow. Each stream receives a
|
|
// chunk of physical watch roots. No directory walk or tree is needed;
|
|
// FSEvents watches recursively via the kernel, and event classification uses
|
|
// only the flags.
|
|
//
|
|
// Event classification (fsEventsCallback, on eventLoop goroutine):
|
|
// Each batch delivers arrays of paths, flags, and event IDs. The flags
|
|
// bitmask may combine multiple states (created + modified + renamed).
|
|
// Pure removes emit EventDelete with no syscalls. Renames and
|
|
// remove+create combos do one Lstat to check existence (the kernel
|
|
// reports some deletions as renames). Everything else emits EventUpdate
|
|
// with no syscalls.
|
|
//
|
|
// Overflow:
|
|
// flagMustScanSubDirs → ErrOverflow with detail (user/kernel/too-many).
|
|
//
|
|
// Root deletion:
|
|
// Detected in the callback; the logical watch is marked terminated and
|
|
// receives ErrWatchTerminated. The shared stream remains active for other
|
|
// watches until the owner closes or reconciles the terminated watch.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// ----- FSEvents flag bits (from FSEvents.h) ------------------------------
|
|
|
|
const (
|
|
flagMustScanSubDirs = 0x00000001
|
|
flagUserDropped = 0x00000002
|
|
flagKernelDropped = 0x00000004
|
|
flagHistoryDone = 0x00000010
|
|
|
|
flagItemCreated = 0x00000100
|
|
flagItemRemoved = 0x00000200
|
|
flagItemInodeMetaMod = 0x00000400
|
|
flagItemRenamed = 0x00000800
|
|
flagItemModified = 0x00001000
|
|
flagItemFinderInfoMod = 0x00002000
|
|
flagItemChangeOwner = 0x00004000
|
|
flagItemXattrMod = 0x00008000
|
|
flagItemIsFile = 0x00010000
|
|
flagItemIsDir = 0x00020000
|
|
flagItemIsSymlink = 0x00040000
|
|
flagItemIsHardlink = 0x00100000
|
|
flagItemIsLastHardlink = 0x00200000
|
|
flagItemCloned = 0x00400000
|
|
|
|
// kFSEventStreamCreateFlagUseCFTypes (0x1) |
|
|
// kFSEventStreamCreateFlagFileEvents (0x10) is hardcoded in the
|
|
// arch-specific assembly trampolines (fsevents_darwin_ffi_{arm64,amd64}.s).
|
|
|
|
cfStringEncodingUTF8 = 0x08000100
|
|
|
|
// kFSEventStreamEventIdSinceNow == ((FSEventStreamEventId)0xFFFFFFFFFFFFFFFFULL)
|
|
eventIDSinceNow = uint64(0xFFFFFFFFFFFFFFFF)
|
|
)
|
|
|
|
const ignoredFlags = flagItemIsHardlink | flagItemIsLastHardlink |
|
|
flagItemIsSymlink | flagItemIsDir | flagItemIsFile | flagItemCloned
|
|
|
|
// fsEventStreamContext mirrors the C struct of the same name.
|
|
//
|
|
// typedef struct {
|
|
// CFIndex version; // signed long, 8 bytes on 64-bit
|
|
// void *info; // pointer
|
|
// void *retain; // pointer
|
|
// void *release; // pointer
|
|
// void *copyDescription; // pointer
|
|
// } FSEventStreamContext;
|
|
type fsEventStreamContext struct {
|
|
version int
|
|
info uintptr
|
|
retain uintptr
|
|
release uintptr
|
|
copyDescription uintptr
|
|
}
|
|
|
|
type fseventsState struct {
|
|
terminated atomic.Bool
|
|
}
|
|
|
|
type fseventsStream struct {
|
|
stream atomic.Uintptr
|
|
cb *streamCallback
|
|
pinner runtime.Pinner
|
|
}
|
|
|
|
// ----- the watcherImpl -------------------------------------------------------
|
|
|
|
// fsEventsBackend.
|
|
type fsEventsBackend struct {
|
|
watcherBase
|
|
|
|
mu sync.Mutex
|
|
watches map[*dirWatch]*fseventsState
|
|
streams []*fseventsStream
|
|
}
|
|
|
|
func init() {
|
|
fseventsWatcher.factory = func() watcherImpl { return newFSEventsBackend() }
|
|
fseventsWatcher.sequence = fsEventsGetCurrentEventID
|
|
}
|
|
|
|
func newFSEventsBackend() *fsEventsBackend {
|
|
b := &fsEventsBackend{
|
|
watches: make(map[*dirWatch]*fseventsState),
|
|
}
|
|
b.watcherBase.init(b)
|
|
return b
|
|
}
|
|
|
|
func (b *fsEventsBackend) start() error {
|
|
b.notifyStarted()
|
|
return nil
|
|
}
|
|
|
|
// checkWatcher mirrors the helper of the same name.
|
|
func checkWatcher(w *dirWatch) error {
|
|
info, err := os.Stat(w.physicalDir)
|
|
if err != nil {
|
|
return &dirWatchError{err: err, dirWatch: w}
|
|
}
|
|
if !info.IsDir() {
|
|
return &dirWatchError{err: syscall.ENOTDIR, dirWatch: w}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
errCFStringCreateNull = errors.New("CFStringCreate returned NULL")
|
|
errCFArrayCreateNull = errors.New("CFArrayCreate returned NULL")
|
|
errStreamCreateNull = errors.New("FSEventStreamCreate returned NULL")
|
|
errStreamStartFailed = errors.New("error starting FSEvents stream")
|
|
)
|
|
|
|
var (
|
|
errFSEventsUserDropped = fmt.Errorf("events were dropped by the FSEvents client: %w", ErrOverflow)
|
|
errFSEventsKernelDropped = fmt.Errorf("events were dropped by the kernel: %w", ErrOverflow)
|
|
errFSEventsTooMany = fmt.Errorf("too many events: %w", ErrOverflow)
|
|
)
|
|
|
|
const fseventsPathsPerStream = 512
|
|
|
|
type fseventsWatchSnapshot struct {
|
|
w *dirWatch
|
|
state *fseventsState
|
|
}
|
|
|
|
func (b *fsEventsBackend) activeWatchesLocked() []fseventsWatchSnapshot {
|
|
watches := make([]fseventsWatchSnapshot, 0, len(b.watches))
|
|
for w, state := range b.watches {
|
|
if state.terminated.Load() {
|
|
continue
|
|
}
|
|
watches = append(watches, fseventsWatchSnapshot{w: w, state: state})
|
|
}
|
|
return watches
|
|
}
|
|
|
|
func (b *fsEventsBackend) startStreams(watches []fseventsWatchSnapshot) ([]*fseventsStream, error) {
|
|
return startFSEventsStreams(watches, b.startStream)
|
|
}
|
|
|
|
func startFSEventsStreams(watches []fseventsWatchSnapshot, startStream func([]string, []fseventsWatchSnapshot) (*fseventsStream, error)) ([]*fseventsStream, error) {
|
|
if len(watches) == 0 {
|
|
return nil, nil
|
|
}
|
|
seen := make(map[string]struct{}, len(watches))
|
|
paths := make([]string, 0, len(watches))
|
|
for _, watch := range watches {
|
|
path := watch.w.physicalDir
|
|
if _, ok := seen[path]; ok {
|
|
continue
|
|
}
|
|
seen[path] = struct{}{}
|
|
paths = append(paths, path)
|
|
}
|
|
sort.Strings(paths)
|
|
|
|
stream, err := startStream(paths, watches)
|
|
if err == nil {
|
|
return []*fseventsStream{stream}, nil
|
|
}
|
|
|
|
streams := make([]*fseventsStream, 0, (len(paths)+fseventsPathsPerStream-1)/fseventsPathsPerStream)
|
|
remainingPaths := paths
|
|
for len(remainingPaths) > 0 {
|
|
chunkLen := min(len(remainingPaths), fseventsPathsPerStream)
|
|
chunkPaths := remainingPaths[:chunkLen]
|
|
stream, err := startStream(chunkPaths, watchesForFSEventsPaths(watches, chunkPaths))
|
|
if err != nil {
|
|
stopFSEventsStreams(streams)
|
|
return nil, err
|
|
}
|
|
streams = append(streams, stream)
|
|
remainingPaths = remainingPaths[chunkLen:]
|
|
}
|
|
return streams, nil
|
|
}
|
|
|
|
func watchesForFSEventsPaths(watches []fseventsWatchSnapshot, paths []string) []fseventsWatchSnapshot {
|
|
if len(paths) == 0 {
|
|
return nil
|
|
}
|
|
filtered := make([]fseventsWatchSnapshot, 0, len(watches))
|
|
for _, watch := range watches {
|
|
if _, ok := slices.BinarySearch(paths, watch.w.physicalDir); ok {
|
|
filtered = append(filtered, watch)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
// startStream creates and starts one FSEventStream watching all supplied paths.
|
|
func (b *fsEventsBackend) startStream(paths []string, watches []fseventsWatchSnapshot) (*fseventsStream, error) {
|
|
if len(paths) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
cfStrings := make([]uintptr, 0, len(paths))
|
|
for _, path := range paths {
|
|
dirCStr := append([]byte(path), 0)
|
|
cfDir := cfStringCreate(0, unsafe.Pointer(&dirCStr[0]), cfStringEncodingUTF8)
|
|
if cfDir == 0 {
|
|
for _, cfString := range cfStrings {
|
|
cfRelease(cfString)
|
|
}
|
|
return nil, errCFStringCreateNull
|
|
}
|
|
cfStrings = append(cfStrings, cfDir)
|
|
}
|
|
defer func() {
|
|
for _, cfString := range cfStrings {
|
|
cfRelease(cfString)
|
|
}
|
|
}()
|
|
|
|
pathsToWatch := cfArrayCreate(0, unsafe.Pointer(&cfStrings[0]), len(cfStrings), 0)
|
|
if pathsToWatch == 0 {
|
|
return nil, errCFArrayCreateNull
|
|
}
|
|
defer cfRelease(pathsToWatch)
|
|
|
|
cb, err := newStreamCallback(watches)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
state := &fseventsStream{cb: cb}
|
|
state.pinner.Pin(cb)
|
|
|
|
ctx := fsEventStreamContext{info: uintptr(unsafe.Pointer(cb))}
|
|
|
|
stream := fsEventStreamCreate(
|
|
0,
|
|
fsEventsCallbackAsmAddr,
|
|
unsafe.Pointer(&ctx),
|
|
pathsToWatch,
|
|
eventIDSinceNow,
|
|
0.001,
|
|
)
|
|
if stream == 0 {
|
|
cb.close()
|
|
state.cb = nil
|
|
state.pinner.Unpin()
|
|
return nil, errStreamCreateNull
|
|
}
|
|
|
|
fsEventStreamSetDispatchQueue(stream, cb.queue)
|
|
if fsEventStreamStart(stream) == 0 {
|
|
fsEventStreamInvalidate(stream)
|
|
fsEventStreamRelease(stream)
|
|
cb.close()
|
|
state.cb = nil
|
|
state.pinner.Unpin()
|
|
return nil, errStreamStartFailed
|
|
}
|
|
fsEventStreamFlushSync(stream)
|
|
state.stream.Store(stream)
|
|
return state, nil
|
|
}
|
|
|
|
// teardownStream performs the full FSEventStream cleanup. Stop and Invalidate
|
|
// prevent new callbacks, waitDispatchQueue waits for callbacks already queued
|
|
// on the stream's serial dispatch queue, and cb.close joins the Go event loop
|
|
// after it drains payloads already written to the pipe.
|
|
func teardownStream(stream uintptr, cb *streamCallback) {
|
|
fsEventStreamStop(stream)
|
|
if cb != nil {
|
|
fsEventStreamInvalidate(stream)
|
|
cb.waitDispatchQueue()
|
|
cb.close()
|
|
} else {
|
|
fsEventStreamInvalidate(stream)
|
|
}
|
|
fsEventStreamRelease(stream)
|
|
}
|
|
|
|
// The atomic Swap gates teardown so concurrent or repeated calls are safe:
|
|
// only the goroutine that observes a non-zero stream performs the cleanup.
|
|
func stopFSEventsStreams(streams []*fseventsStream) {
|
|
for _, stream := range streams {
|
|
stopFSEventsStream(stream)
|
|
}
|
|
}
|
|
|
|
func stopFSEventsStream(state *fseventsStream) {
|
|
if state == nil {
|
|
return
|
|
}
|
|
stream := state.stream.Swap(0)
|
|
if stream == 0 {
|
|
return
|
|
}
|
|
cb := state.cb
|
|
teardownStream(stream, cb)
|
|
state.cb = nil
|
|
state.pinner.Unpin()
|
|
}
|
|
|
|
// subscribe mirrors `fsEventsBackend::subscribe`.
|
|
func (b *fsEventsBackend) subscribe(w *dirWatch) error {
|
|
return b.subscribeMany([]*dirWatch{w})
|
|
}
|
|
|
|
func (b *fsEventsBackend) subscribeMany(watchesToAdd []*dirWatch) error {
|
|
if len(watchesToAdd) == 0 {
|
|
return nil
|
|
}
|
|
states := make(map[*dirWatch]*fseventsState, len(watchesToAdd))
|
|
for _, w := range watchesToAdd {
|
|
if err := checkWatcher(w); err != nil {
|
|
return err
|
|
}
|
|
states[w] = &fseventsState{}
|
|
}
|
|
|
|
b.mu.Lock()
|
|
for w, state := range states {
|
|
w.state = state
|
|
b.watches[w] = state
|
|
}
|
|
watches := b.activeWatchesLocked()
|
|
b.mu.Unlock()
|
|
|
|
streams, err := b.startStreams(watches)
|
|
if err != nil {
|
|
b.mu.Lock()
|
|
for w, state := range states {
|
|
if b.watches[w] == state {
|
|
delete(b.watches, w)
|
|
w.state = nil
|
|
}
|
|
}
|
|
b.mu.Unlock()
|
|
return &dirWatchError{err: err, dirWatch: watchesToAdd[0]}
|
|
}
|
|
|
|
b.mu.Lock()
|
|
oldStreams := b.streams
|
|
b.streams = streams
|
|
b.mu.Unlock()
|
|
stopFSEventsStreams(oldStreams)
|
|
return nil
|
|
}
|
|
|
|
// closeWatch mirrors `fsEventsBackend::closeWatch`.
|
|
func (b *fsEventsBackend) closeWatch(w *dirWatch) error {
|
|
state, _ := w.state.(*fseventsState)
|
|
w.state = nil
|
|
if state == nil {
|
|
return nil
|
|
}
|
|
state.terminated.Store(true)
|
|
|
|
b.mu.Lock()
|
|
delete(b.watches, w)
|
|
watches := b.activeWatchesLocked()
|
|
b.mu.Unlock()
|
|
|
|
streams, err := b.startStreams(watches)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b.mu.Lock()
|
|
oldStreams := b.streams
|
|
b.streams = streams
|
|
b.mu.Unlock()
|
|
stopFSEventsStreams(oldStreams)
|
|
return nil
|
|
}
|
|
|
|
// fsEventsCallback processes a batch of FSEvents. The payload contains callback
|
|
// data retained/copied by the assembly before it returned control to Go.
|
|
//
|
|
// Called by streamCallback.eventLoop on a per-stream Go goroutine (not the
|
|
// dispatch queue thread). The C callback assembly signals the event loop via a
|
|
// pipe; see fsevents_darwin_ffi.go.
|
|
func fsEventsCallback(cb *streamCallback, payload *fsEventsCallbackPayload) {
|
|
defer payload.close()
|
|
|
|
const (
|
|
flagSize = unsafe.Sizeof(uint32(0))
|
|
idSize = unsafe.Sizeof(uint64(0))
|
|
)
|
|
|
|
if payload == nil || payload.paths == 0 || payload.flags == 0 || payload.ids == 0 {
|
|
return
|
|
}
|
|
|
|
numEvents := payload.numEvents
|
|
paths := payload.paths
|
|
flags := payload.flags
|
|
ids := payload.ids
|
|
|
|
watches := cb.watches
|
|
touched := map[*dirWatch]struct{}{}
|
|
|
|
for i := range numEvents {
|
|
flag := *(*uint32)(unsafe.Add(nil, flags+i*flagSize))
|
|
eventID := *(*uint64)(unsafe.Add(nil, ids+i*idSize))
|
|
pathRef := cfArrayGetValueAtIndex(paths, int(i))
|
|
path := cfStringToNFC(pathRef)
|
|
if path == "" {
|
|
continue
|
|
}
|
|
|
|
isRemoved := flag&flagItemRemoved != 0
|
|
isRenamed := flag&flagItemRenamed != 0
|
|
isCreated := flag&flagItemCreated != 0
|
|
isDone := flag&flagHistoryDone != 0
|
|
|
|
if flag&flagMustScanSubDirs != 0 {
|
|
var overflow error
|
|
switch {
|
|
case flag&flagUserDropped != 0:
|
|
overflow = errFSEventsUserDropped
|
|
case flag&flagKernelDropped != 0:
|
|
overflow = errFSEventsKernelDropped
|
|
default:
|
|
overflow = errFSEventsTooMany
|
|
}
|
|
for _, watch := range watches {
|
|
if watch.state.terminated.Load() {
|
|
continue
|
|
}
|
|
if fseventsOverflowMatches(watch.w, path) {
|
|
watch.w.events.setError(overflow)
|
|
touched[watch.w] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
if isDone {
|
|
break
|
|
}
|
|
|
|
if flag&^uint32(ignoredFlags) == 0 {
|
|
continue
|
|
}
|
|
|
|
rawPath := path
|
|
pathExists := false
|
|
pathExistsKnown := false
|
|
|
|
for _, watch := range watches {
|
|
if watch.state.terminated.Load() {
|
|
continue
|
|
}
|
|
w := watch.w
|
|
displayPath, ok := fseventsDisplayPath(w, rawPath)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Skip events for the watched directory itself unless it's been
|
|
// removed. fseventsd reports a change on the watched dir when a
|
|
// child is added or removed; subscribers observe changes *within*
|
|
// the directory, not the dir's own metadata churn.
|
|
// (A removal of the dir is still propagated because Watcher
|
|
// relies on it to tear down the stream.)
|
|
if displayPath == w.dir && !isRemoved && !isRenamed {
|
|
continue
|
|
}
|
|
|
|
switch {
|
|
case isRemoved && !isCreated:
|
|
if displayPath == w.dir {
|
|
w.events.removeWatchRootAt(displayPath, eventID)
|
|
} else {
|
|
w.events.removeAt(displayPath, eventID)
|
|
}
|
|
if w.terminateCallbacksForDeletedRoot(displayPath, eventID, fmt.Errorf("%w: watched directory removed", ErrWatchTerminated)) {
|
|
touched[w] = struct{}{}
|
|
}
|
|
if displayPath == w.dir {
|
|
watch.state.terminated.Store(true)
|
|
w.events.setError(fmt.Errorf("%w: watched directory removed", ErrWatchTerminated))
|
|
}
|
|
case isRenamed || (isRemoved && isCreated):
|
|
if !pathExistsKnown {
|
|
var st unix.Stat_t
|
|
pathExists = unix.Lstat(rawPath, &st) == nil
|
|
pathExistsKnown = true
|
|
}
|
|
if pathExists {
|
|
if displayPath == w.dir {
|
|
w.events.updateWatchRootAt(displayPath, eventID)
|
|
} else {
|
|
w.events.updateAt(displayPath, eventID)
|
|
}
|
|
} else {
|
|
if displayPath == w.dir {
|
|
w.events.removeWatchRootAt(displayPath, eventID)
|
|
} else {
|
|
w.events.removeAt(displayPath, eventID)
|
|
}
|
|
if w.terminateCallbacksForDeletedRoot(displayPath, eventID, fmt.Errorf("%w: watched directory removed", ErrWatchTerminated)) {
|
|
touched[w] = struct{}{}
|
|
}
|
|
if displayPath == w.dir {
|
|
watch.state.terminated.Store(true)
|
|
w.events.setError(fmt.Errorf("%w: watched directory removed", ErrWatchTerminated))
|
|
}
|
|
}
|
|
default:
|
|
if displayPath == w.dir {
|
|
w.events.updateWatchRootAt(displayPath, eventID)
|
|
} else {
|
|
w.events.updateAt(displayPath, eventID)
|
|
}
|
|
}
|
|
touched[w] = struct{}{}
|
|
}
|
|
}
|
|
|
|
for w := range touched {
|
|
w.notify()
|
|
}
|
|
}
|
|
|
|
func fseventsDisplayPath(w *dirWatch, rawPath string) (string, bool) {
|
|
if isInDirectoryOrSelf(w.physicalDir, rawPath) {
|
|
return w.displayPath(rawPath), true
|
|
}
|
|
if w.physicalDir != w.dir && isInDirectoryOrSelf(w.dir, rawPath) {
|
|
return rawPath, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func fseventsOverflowMatches(w *dirWatch, rawPath string) bool {
|
|
if isInDirectoryOrSelf(w.physicalDir, rawPath) || isInDirectoryOrSelf(rawPath, w.physicalDir) {
|
|
return true
|
|
}
|
|
return w.physicalDir != w.dir && (isInDirectoryOrSelf(w.dir, rawPath) || isInDirectoryOrSelf(rawPath, w.dir))
|
|
}
|