A project kept on an ExFAT/FAT external volume (or some network mounts / WSL2 DrvFs) broke the background auto-sync daemon at two points, both because the filesystem lacks POSIX features the daemon relied on: 1. Lock acquisition hard-links a temp file onto .codegraph/daemon.pid for race-free exclusivity (#411) — these filesystems have no hard links. 2. The Unix-domain socket listen() fails regardless of path length, so the old length-only tmpdir fallback never triggered. Both surface as a capability error, but each OS reports a DIFFERENT errno for the same gap (macOS ENOTSUP, Linux EPERM, Windows EISDIR), so the fix is policy-based rather than an enumerated code-set: - Lock: fall back to an O_EXCL create on any non-EEXIST link error. The temp write already proved the directory is writable, so the fallback either succeeds (still atomic + exclusive, "first writer wins") or surfaces its own genuine error. - Socket: an ordered candidate list [in-project, tmpdir] walked by BOTH the daemon (binds) and the proxy (connects) — they converge on the fallback with zero coordination. Relocate past any non-EADDRINUSE bind error; EADDRINUSE still rethrows, preserving the #974 contract. Normal repos are unaffected: the in-project candidate binds first, and the hard-link lock path is unchanged. Validated end-to-end on real removable-drive filesystems: macOS ExFAT (hdiutil image), Linux FAT32 (Docker loop mount), Windows exFAT (diskpart VHD) — each acquires the lock, relocates (or binds a named pipe on Windows), and serves a real client. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7a361ef16e
commit
f83a1ecc8e
+22
-5
@@ -48,7 +48,7 @@ import {
|
||||
tryAcquireDaemonLock,
|
||||
} from './daemon';
|
||||
import { connectWithHello, runLocalHandshakeProxy } from './proxy';
|
||||
import { getDaemonSocketPath } from './daemon-paths';
|
||||
import { getDaemonSocketCandidates } from './daemon-paths';
|
||||
import { getTelemetry } from '../telemetry';
|
||||
import { supervisionLostReason, parsePpidPollMs, parseHostPpid } from './ppid-watchdog';
|
||||
import { installMainThreadWatchdog, WatchdogHandle } from './liveness-watchdog';
|
||||
@@ -376,17 +376,34 @@ export class MCPServer {
|
||||
* never wedges a session.
|
||||
*/
|
||||
private async runProxyWithLocalHandshake(root: string): Promise<void> {
|
||||
const socketPath = getDaemonSocketPath(root);
|
||||
// The daemon may relocate its socket past an in-project filesystem that can't
|
||||
// host one (ExFAT/FAT volumes, WSL2 DrvFs; #997) to the deterministic tmpdir
|
||||
// fallback. We don't read the bound path from the lockfile — both sides walk
|
||||
// the SAME ordered candidate list, so we converge on whichever the daemon
|
||||
// bound with zero coordination. The in-project candidate is tried first, so a
|
||||
// normal repo pays nothing extra (it connects on the very first probe).
|
||||
const candidates = getDaemonSocketCandidates(root);
|
||||
const connectAnyCandidate = async (): Promise<Awaited<ReturnType<typeof connectWithHello>>> => {
|
||||
for (const candidate of candidates) {
|
||||
const s = await connectWithHello(candidate);
|
||||
// A wrong-version daemon IS up — definitive; propagate so the caller
|
||||
// serves in-process instead of spawning + polling for 6s. Don't keep
|
||||
// probing fallbacks past it.
|
||||
if (s === 'version-mismatch') return s;
|
||||
if (s) return s;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const getDaemonSocket = async () => {
|
||||
// Fast path: a daemon may already be listening.
|
||||
const probe = await connectWithHello(socketPath);
|
||||
// Fast path: a daemon may already be listening (on either candidate).
|
||||
const probe = await connectAnyCandidate();
|
||||
if (probe === 'version-mismatch') return null; // definitive — serve in-process, don't poll for 6s
|
||||
if (probe) return probe;
|
||||
// None reachable — spawn one (detached) and poll for its bind.
|
||||
spawnDetachedDaemon(root);
|
||||
for (let attempt = 0; attempt < DAEMON_CONNECT_MAX_RETRIES; attempt++) {
|
||||
await sleep(DAEMON_CONNECT_RETRY_DELAY_MS);
|
||||
const s = await connectWithHello(socketPath);
|
||||
const s = await connectAnyCandidate();
|
||||
if (s === 'version-mismatch') return null;
|
||||
if (s) return s;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user