Three bugs caused all integration tests to fail with -Dmacos_fsevents=true:
- FSEventStreamCreate was passed *CallbackContext directly as the context
parameter, which expects *FSEventStreamContext (a struct with version=0,
info=ptr, retain/release/copyDescription). The handler pointer landed in
the version field (must be 0) and info received by callbacks was garbage,
so the callback returned immediately on every event.
- FSEvents coalesces operations into a single delivery with multiple flags
set (e.g. ItemCreated|ItemModified, ItemRenamed|ItemModified). The
callback used an if/else chain that only emitted the first matching event
type, so a write coalesced with a create or rename produced no 'modified'
event. Fixed by checking each flag independently.
- FSEvents delivers spurious historical events for watched root directories
at stream start (even with kFSEventStreamEventIdSinceNow), causing
phantom dir_created events. Fixed by snapshotting the watched root paths
in CallbackContext at arm() time and skipping events whose path exactly
matches a root.
Also: arm() is now a no-op when no paths are watched yet (stream starts on
the first add_watch call), add_watch/remove_watch restart the stream so
paths added or removed take effect immediately, and makeTempDir resolves
/tmp to /private/tmp on macOS so test-constructed paths match FSEvents
canonical output.
Using realpath() in watch() caused events to be emitted under the resolved
path (e.g. /private/tmp/...) rather than the path the caller provided (e.g.
/tmp/..., where /tmp is a symlink on macOS). This broke all integration
tests on macOS since the test file paths didn't match the emitted event
paths.
For absolute paths, use them unchanged. For relative paths, join with the
current working directory without further symlink resolution. This
preserves the caller's view of the path while still satisfying the kqueue
backend's requirement for absolute paths.
kqueue's NOTE_WRITE on a directory fires only when directory entries are
added or removed, not when file contents change. This meant writes to
existing files were never reported as 'modified' events on macOS/FreeBSD.
Fix by maintaining a second set of kqueue watches on individual files
(file_watches), registered with NOTE_WRITE|NOTE_EXTEND. When either flag
fires on a file fd, a 'modified' event is emitted. File watches are
registered in take_snapshot (for files already present when watch() is
called) and in scan_dir (for newly created files), and deregistered when
files are deleted or the directory is unwatched.
Also fix two related bugs:
- NOTE_DELETE was incorrectly defined as 0x04 (NOTE_EXTEND); the correct
value is 0x01. This could cause NOTE_EXTEND events on watched directories
to be misreported as directory-deleted events.
- scan_dir emitted created events before deleted events, so a rename
(old name disappears, new name appears) reported the destination before
the source. Swapped the order so deletions are always emitted first.
Simplify thread_fn/arm to pass *KQueueBackend directly now that the backend
lives at a stable heap address inside the heap-allocated Interceptor.
All 10 integration tests now pass on FreeBSD.
Handler callbacks invoked while holding snapshots_mutex could deadlock if
the handler called watch() or unwatch(), which also acquires that lock.
Refactor scan_dir to collect all pending events (dir_created, created,
deleted) into temporary lists under the lock, then emit them after
releasing it. Also consolidate the two directory iteration passes into one.