Documentation
¶
Overview ¶
Package watch implements SYNC-01: a native, debounced filesystem watcher built on github.com/fsnotify/fsnotify (D-04) — the mandated cross-platform primitive; no polling default. fsnotify does not recurse on its own, so Watcher walks the tree at Open time and re-adds any newly-created directory on a Create event. A burst of events is coalesced by a Debouncer (see debounce.go) into one flush call over the union of changed paths.
internal/watch depends only on internal/indexer's exported ShouldSkipDir predicate — never on internal/graphstore or pebble directly (D-04a archtest boundary; this package has no storage concerns of its own).
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrWatchDisabled = errors.New("daemon: watching is disabled by policy")
ErrWatchDisabled is the sentinel error internal/daemon wraps when WatchDisabledReason reports a non-empty reason, and internal/cli errors.Is against to print the human-visible disabled message (D-09/D-11). It lives in internal/watch (not internal/daemon) so both internal/daemon (wraps it) and internal/cli (errors.Is it) can import it without an import cycle.
Functions ¶
func DebounceDuration ¶
DebounceDuration returns the debounce window: CODEGRAPH_DEBOUNCE_MS (a positive integer number of milliseconds) overrides the 2000ms default; a missing, zero, negative, or non-numeric value falls back to the default.
func DetectWSL ¶
func DetectWSL() bool
DetectWSL reports whether the process is running under WSL2, cached after the first call (sync.Once-guarded, D-10): non-linux GOOS is unconditionally false (no I/O); WSL_DISTRO_NAME or WSL_INTEROP env presence is true (no I/O); otherwise /proc/version is read and lowercased, true if it contains "microsoft" or "wsl"; any read failure degrades to false (never panics — V5/V12: a hostile or missing /proc must not crash or hang the process).
func WatchDisabledReason ¶
WatchDisabledReason returns "" when the watcher should run, or a short human-readable reason (the documented disabled-reason strings, D-12/D-13) when it should not. Precedence, first match wins (D-04):
- NoWatch flag OR Env("CODEGRAPH_NO_WATCH")=="1" -> off
- ForceWatch flag OR Env("CODEGRAPH_FORCE_WATCH")=="1" -> on (beats auto-detect)
- WSL2 + /mnt/[a-z] drive -> off
- default -> on
Env comparisons are strict `== "1"` (D-10) — never strconv.ParseBool or non-empty truthiness — so unrelated env noise ("true", "yes", "0", padded values) can never silently flip watch state (T-03-01).
The --no-watch flag and the CODEGRAPH_NO_WATCH env var are two inputs to the same tier-1 check, both producing the same reason string, so the disabled message is identical whichever one triggered it.
Types ¶
type Debouncer ¶
type Debouncer struct {
// contains filtered or unexported fields
}
Debouncer coalesces a burst of Add calls within window into a single flush call over the deduplicated union of changed paths (Pattern 3). A quiet gap longer than window flushes and resets; a subsequent burst flushes again.
func NewDebouncer ¶
func NewDebouncer(ctx context.Context, window time.Duration, flush func(paths map[string]struct{})) *Debouncer
NewDebouncer returns a Debouncer bound to ctx: once ctx is cancelled, no further flush fires (fire checks ctx.Err(), and Stop cancels any pending timer — Pattern 7's two-part guarantee against a late-firing timer goroutine). flush is invoked from the timer's own goroutine (time.AfterFunc) and must be safe to call from an arbitrary goroutine.
func (*Debouncer) Add ¶
Add records path as changed and (re)starts the debounce timer so a burst of Adds within window coalesces into one flush. Once ctx is cancelled, Add is a no-op (03-REVIEW.md IN-04): arming a timer post-cancel could only ever produce a no-op fire (fire checks ctx.Err()) but would still make a caller's Wait block up to a full window on it — the daemon's requeue-vs-shutdown TOCTOU. The early return happens before fireWG.Add(1), so Wait's accounting is untouched.
func (*Debouncer) Stop ¶
func (d *Debouncer) Stop()
Stop cancels any pending timer so no late flush fires after shutdown (Pattern 7) — required for Plan 04-09's leak-free soak gate: an unstopped timer.AfterFunc callback goroutine, still scheduled to fire, is exactly what goleak.VerifyNone would catch as a leak. Stop does NOT wait for a fire() that has already started running — call Wait after Stop for that (CR-01).
func (*Debouncer) Wait ¶
func (d *Debouncer) Wait()
Wait blocks until every fire() invocation that has actually started running — i.e. that Stop could not cancel in time — has fully completed, including the flush(...) call it makes (CR-01). Callers join the Debouncer's lifecycle via Stop (cancel anything not yet running) followed by Wait (join anything that is): together these give a caller a genuine "no debounce-triggered work is still in flight" guarantee, which Stop alone cannot provide since a timer that has already fired is no longer cancellable.
type DisabledError ¶
type DisabledError struct{ Reason string }
DisabledError carries the human-readable disabled reason alongside the ErrWatchDisabled sentinel (03-REVIEW.md IN-05): daemon.Run returns this type so CLI consumers extract the exact reason Run's own policy gate saw via errors.As, instead of re-deriving it with fresh WatchDisabledReason calls that can silently desynchronize (different root normalization, different Probe inputs). errors.Is(err, ErrWatchDisabled) keeps working everywhere via the Is method below.
func (*DisabledError) Error ¶
func (e *DisabledError) Error() string
Error preserves the exact string the previous fmt.Errorf("%w: %s", ...) wrap produced, so any consumer of the rendered message is unaffected.
func (*DisabledError) Is ¶
func (e *DisabledError) Is(target error) bool
Is makes errors.Is(err, ErrWatchDisabled) match a *DisabledError, keeping every existing sentinel check working unchanged.
type Probe ¶
type Probe struct {
// Env looks up an environment variable by name. Defaults to os.Getenv.
Env func(string) string
// IsWSL reports whether the process is running under WSL2. Defaults to
// the cached DetectWSL.
IsWSL func() bool
// NoWatch is the --no-watch flag (D-01/D-02): opt-out, always wins.
NoWatch bool
// ForceWatch is the --watch flag, repurposed as the explicit force-on
// escape hatch (D-03): overrides the WSL2 auto-off, but a plain
// default-on run does not override anything.
ForceWatch bool
}
Probe carries WatchDisabledReason's inputs — env lookup, WSL detection, and the two CLI flags — as explicit values/functions rather than reading process state directly (D-05: the watcher is in-process, so this package never mutates the process environment; CODEGRAPH_NO_WATCH is read directly via the Env probe). Nil Env/IsWSL default to os.Getenv/DetectWSL inside WatchDisabledReason, so callers only need to set them explicitly in tests.
type Watcher ¶
type Watcher struct {
// contains filtered or unexported fields
}
Watcher wraps a *fsnotify.Watcher covering a repo root recursively. It has no graph or storage knowledge of its own — Run's flush callback is the caller's seam into indexer.Sync.
func Open ¶
Open opens a native fsnotify watcher rooted at root and walks the tree, adding every directory not excluded by indexer.ShouldSkipDir to the watch set (so .codegraph/, vendor/, and other dot-prefixed directories are never watched — the same exclusion discover.go applies, per D-04's "watcher and indexer agree on the file set").
func (*Watcher) Close ¶
Close idempotently releases the underlying fsnotify watcher — the same atomic.Bool-swap-guarded idiom internal/graphstore's pebbleStore.Close uses, since closing an *fsnotify.Watcher a second time can also misbehave.