watch

package
v0.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 17, 2026 License: MIT Imports: 16 Imported by: 0

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

View Source
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

func DebounceDuration() time.Duration

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

func WatchDisabledReason(projectRoot string, p Probe) string

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):

  1. NoWatch flag OR Env("CODEGRAPH_NO_WATCH")=="1" -> off
  2. ForceWatch flag OR Env("CODEGRAPH_FORCE_WATCH")=="1" -> on (beats auto-detect)
  3. WSL2 + /mnt/[a-z] drive -> off
  4. 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

func (d *Debouncer) Add(path string)

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

func Open(root string) (*Watcher, error)

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

func (w *Watcher) Close() error

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.

func (*Watcher) Run

func (w *Watcher) Run(ctx context.Context, deb *Debouncer)

Run consumes fsnotify events until ctx is cancelled, feeding each changed path into deb so a burst coalesces into one debounced flush (Pattern 3). Run returns (and stops deb's pending timer) once ctx is done or the watcher's channels are closed.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL