Close() in backend_kqueue.go marks the watcher as closed via shared.close() before iterating the remove loop. Then remove() checks isClosed() at entry and returns nil without closing any file descriptors.
On macOS with kqueue, watching a directory also opens individual file descriptors for every file in the directory. A watcher monitoring N directories with M total files leaks N+M descriptors on every Close() call. Long-running processes that recreate watchers (e.g. dev servers with hot reload) accumulate leaked descriptors until hitting EMFILE ("too many open files").
Discovered by inspecting a Go dev server that hit 10,376 open fds after ~9 hot reload cycles, each leaking ~1,400 file/directory descriptors from watcher.Close().
Version: v1.9.0
Repro:
- Create a watcher with Add() on a directory containing many files
- Close() the watcher
- Check /proc/self/fd or lsof: all watched file/directory fds are still open
The fix is to close watch descriptors directly in Close() by iterating the wd map instead of delegating to Remove() which short-circuits on isClosed():
aperturerobotics#1
Close() in backend_kqueue.go marks the watcher as closed via shared.close() before iterating the remove loop. Then remove() checks isClosed() at entry and returns nil without closing any file descriptors.
On macOS with kqueue, watching a directory also opens individual file descriptors for every file in the directory. A watcher monitoring N directories with M total files leaks N+M descriptors on every Close() call. Long-running processes that recreate watchers (e.g. dev servers with hot reload) accumulate leaked descriptors until hitting EMFILE ("too many open files").
Discovered by inspecting a Go dev server that hit 10,376 open fds after ~9 hot reload cycles, each leaking ~1,400 file/directory descriptors from watcher.Close().
Version: v1.9.0
Repro:
The fix is to close watch descriptors directly in Close() by iterating the wd map instead of delegating to Remove() which short-circuits on isClosed():
aperturerobotics#1