Help us shape the future of Go! Take the 2026 Go Developer Survey

watch

package
v0.16.1 Latest Latest
Warning

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

Go to latest
Published: Oct 31, 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