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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
79b9601aae
commit
cf7db7cb98
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user