Recursive fs.watch on a WSL2 /mnt NTFS/9p mount walks the directory tree with every readdir/stat crossing the Windows boundary, stalling the event loop long enough to blow past opencode's 30s MCP handshake timeout so the tools never appear. This is the file-watcher half of the #172 fix, which moved the DB/WASM open off the handshake but left the watcher on the critical path. - Add watchDisabledReason() policy: CODEGRAPH_NO_WATCH (off) > CODEGRAPH_FORCE_WATCH (force on) > WSL2 + /mnt auto-detect (off). FileWatcher.start() and the MCP server both honor it; the server now logs why watching is off and how to refresh. - Add `codegraph serve --mcp --no-watch`. - When watching is off, init/install offer git sync hooks (post-commit, post-merge, post-checkout) that run `codegraph sync` in the background, or fall back to manual sync; either way the user is told the index stays frozen until re-synced. uninit removes the hooks. - Tests: watch-policy + git-hooks (idempotency, user-content preservation, core.hooksPath). Root-cause analysis and workaround by @mengfanbo123. Closes #199 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
105 lines
3.2 KiB
TypeScript
105 lines
3.2 KiB
TypeScript
/**
|
|
* Watch Policy
|
|
*
|
|
* Decides whether the live file watcher should run for a given project.
|
|
*
|
|
* Native recursive `fs.watch` is pathologically slow on WSL2 `/mnt/*`
|
|
* drives (NTFS exposed over the 9p/drvfs bridge): setting up the recursive
|
|
* watch walks the directory tree, and every readdir/stat crosses the
|
|
* Windows boundary. Inside an MCP server this stalls the event loop during
|
|
* startup long enough to blow past host handshake timeouts (opencode's 30s),
|
|
* so the tools never appear. See issue #199.
|
|
*
|
|
* This module centralizes the on/off decision so the watcher, the MCP
|
|
* server (for diagnostics), and the installer all agree.
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import { normalizePath } from '../utils';
|
|
|
|
let wslChecked = false;
|
|
let wslValue = false;
|
|
|
|
/**
|
|
* Detect whether the current process is running under WSL (Windows
|
|
* Subsystem for Linux). Result is cached after the first call.
|
|
*
|
|
* Checks the WSL-specific env vars first (no I/O), then falls back to
|
|
* `/proc/version`, which contains "microsoft" on WSL kernels.
|
|
*/
|
|
export function detectWsl(): boolean {
|
|
if (wslChecked) return wslValue;
|
|
wslChecked = true;
|
|
|
|
if (process.platform !== 'linux') {
|
|
wslValue = false;
|
|
return wslValue;
|
|
}
|
|
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) {
|
|
wslValue = true;
|
|
return wslValue;
|
|
}
|
|
try {
|
|
const version = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
|
|
wslValue = version.includes('microsoft') || version.includes('wsl');
|
|
} catch {
|
|
wslValue = false;
|
|
}
|
|
return wslValue;
|
|
}
|
|
|
|
/**
|
|
* True for WSL Windows-drive mounts like `/mnt/c` or `/mnt/d/project`.
|
|
* Deliberately matches only single-letter drive mounts, so genuinely fast
|
|
* Linux mounts such as `/mnt/wsl/...` are not flagged.
|
|
*/
|
|
function isWindowsDriveMount(projectRoot: string): boolean {
|
|
return /^\/mnt\/[a-z](\/|$)/i.test(normalizePath(projectRoot));
|
|
}
|
|
|
|
/**
|
|
* Inputs that can be overridden in tests so the decision is deterministic
|
|
* without touching real env vars or `/proc/version`.
|
|
*/
|
|
export interface WatchProbe {
|
|
/** Defaults to `process.env`. */
|
|
env?: NodeJS.ProcessEnv;
|
|
/** Defaults to `detectWsl()`. */
|
|
isWsl?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Decide whether the file watcher should be disabled for a project, and why.
|
|
*
|
|
* Returns a short human-readable reason when watching should be skipped, or
|
|
* `null` when it should run normally.
|
|
*
|
|
* Precedence (first match wins):
|
|
* 1. `CODEGRAPH_NO_WATCH=1` → off (explicit opt-out always wins)
|
|
* 2. `CODEGRAPH_FORCE_WATCH=1` → on (overrides auto-detection)
|
|
* 3. WSL2 + `/mnt/*` drive → off (recursive fs.watch is too slow; #199)
|
|
*/
|
|
export function watchDisabledReason(projectRoot: string, probe: WatchProbe = {}): string | null {
|
|
const env = probe.env ?? process.env;
|
|
|
|
if (env.CODEGRAPH_NO_WATCH === '1') {
|
|
return 'CODEGRAPH_NO_WATCH=1 is set';
|
|
}
|
|
if (env.CODEGRAPH_FORCE_WATCH === '1') {
|
|
return null;
|
|
}
|
|
|
|
const isWsl = probe.isWsl ?? detectWsl();
|
|
if (isWsl && isWindowsDriveMount(projectRoot)) {
|
|
return 'project is on a WSL2 /mnt/ drive, where recursive fs.watch is too slow to be reliable';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Test-only: reset the cached WSL detection. */
|
|
export function __resetWslCacheForTests(): void {
|
|
wslChecked = false;
|
|
wslValue = false;
|
|
}
|