MCPcopy
hub / github.com/colbymchenry/codegraph / installMainThreadWatchdog

Function installMainThreadWatchdog

src/mcp/liveness-watchdog.ts:108–170  ·  view source on GitHub ↗
()

Source from the content-addressed store, hash-verified

106 * starting).
107 */
108export function installMainThreadWatchdog(): WatchdogHandle | null {
109 if (isEnvTruthy(process.env.CODEGRAPH_NO_WATCHDOG)) return null;
110
111 const timeoutMs = parseWatchdogTimeoutMs(process.env.CODEGRAPH_WATCHDOG_TIMEOUT_MS);
112 const checkMs = deriveCheckIntervalMs(timeoutMs);
113
114 let child: ChildProcess;
115 try {
116 // No execArgv inheritance (unlike Worker), so the child carries none of our
117 // V8 flags — it runs no WASM and needs none. stderr inherits the parent's
118 // fd 2 so the kill notice lands wherever the parent logs (daemon.log).
119 child = spawn(
120 process.execPath,
121 ['-e', CHILD_SOURCE, String(process.pid), String(timeoutMs)],
122 {
123 stdio: ['pipe', 'ignore', 'inherit'],
124 windowsHide: true,
125 // The watchdog touches no files; keep its cwd off the project/temp dir
126 // so it can't hold one open (Windows EPERM-on-cleanup, mirrors the
127 // parse-worker quirk).
128 cwd: os.tmpdir(),
129 }
130 );
131 } catch (err) {
132 debug(`spawn failed: ${err instanceof Error ? err.message : String(err)}`);
133 return null;
134 }
135
136 const stdin = child.stdin;
137 if (!stdin) {
138 debug('child has no stdin pipe; not arming');
139 try { child.kill(); } catch { /* ignore */ }
140 return null;
141 }
142 // Writing after the child exits surfaces EPIPE on the stream — swallow it so
143 // it can't escalate to the global handler (which now exits, #850).
144 stdin.on('error', () => { /* child gone; heartbeat writes are best-effort */ });
145 child.on('error', (err) => debug(`child error: ${err.message}`));
146
147 // Heartbeat: a byte per tick. When the main thread wedges, these stop and the
148 // child's timeout fires. unref'd so it never keeps the process alive itself.
149 const heartbeat = setInterval(() => {
150 try { stdin.write('\n'); } catch { /* child gone */ }
151 }, checkMs);
152 heartbeat.unref();
153
154 // Neither the child nor its pipe should keep the parent alive past its work.
155 child.unref();
156 try { (stdin as unknown as { unref?: () => void }).unref?.(); } catch { /* ignore */ }
157
158 debug(`armed (child pid ${child.pid ?? '?'}): timeoutMs=${timeoutMs} checkMs=${checkMs}`);
159
160 let stopped = false;
161 return {
162 stop(): void {
163 if (stopped) return;
164 stopped = true;
165 clearInterval(heartbeat);

Callers 4

startDirectMethod · 0.90
startDaemonProcessMethod · 0.90

Calls 6

isEnvTruthyFunction · 0.85
parseWatchdogTimeoutMsFunction · 0.85
deriveCheckIntervalMsFunction · 0.85
debugFunction · 0.70
onMethod · 0.65
writeMethod · 0.45

Tested by

no test coverage detected