MCPcopy Create free account
hub / github.com/colbymchenry/codegraph / installMainThreadWatchdog

Function installMainThreadWatchdog

src/mcp/liveness-watchdog.ts:180–244  ·  view source on GitHub ↗
(options: WatchdogOptions = {})

Source from the content-addressed store, hash-verified

178 * starting).
179 */
180export function installMainThreadWatchdog(options: WatchdogOptions = {}): WatchdogHandle | null {
181 if (isEnvTruthy(process.env.CODEGRAPH_NO_WATCHDOG)) return null;
182
183 const timeoutMs = parseWatchdogTimeoutMs(process.env.CODEGRAPH_WATCHDOG_TIMEOUT_MS);
184 const checkMs = deriveCheckIntervalMs(timeoutMs);
185 const capMs = timeoutMs * PROGRESS_CAP_MULTIPLIER;
186 const progressPaths = options.progressPaths ?? [];
187
188 let child: ChildProcess;
189 try {
190 // No execArgv inheritance (unlike Worker), so the child carries none of our
191 // V8 flags — it runs no WASM and needs none. stderr inherits the parent's
192 // fd 2 so the kill notice lands wherever the parent logs (daemon.log).
193 child = spawn(
194 process.execPath,
195 ['-e', CHILD_SOURCE, String(process.pid), String(timeoutMs), String(capMs), ...progressPaths],
196 {
197 stdio: ['pipe', 'ignore', 'inherit'],
198 windowsHide: true,
199 // The watchdog touches no files; keep its cwd off the project/temp dir
200 // so it can't hold one open (Windows EPERM-on-cleanup, mirrors the
201 // parse-worker quirk).
202 cwd: os.tmpdir(),
203 }
204 );
205 } catch (err) {
206 debug(`spawn failed: ${err instanceof Error ? err.message : String(err)}`);
207 return null;
208 }
209
210 const stdin = child.stdin;
211 if (!stdin) {
212 debug('child has no stdin pipe; not arming');
213 try { child.kill(); } catch { /* ignore */ }
214 return null;
215 }
216 // Writing after the child exits surfaces EPIPE on the stream — swallow it so
217 // it can't escalate to the global handler (which now exits, #850).
218 stdin.on('error', () => { /* child gone; heartbeat writes are best-effort */ });
219 child.on('error', (err) => debug(`child error: ${err.message}`));
220
221 // Heartbeat: a byte per tick. When the main thread wedges, these stop and the
222 // child's timeout fires. unref'd so it never keeps the process alive itself.
223 const heartbeat = setInterval(() => {
224 try { stdin.write('\n'); } catch { /* child gone */ }
225 }, checkMs);
226 heartbeat.unref();
227
228 // Neither the child nor its pipe should keep the parent alive past its work.
229 child.unref();
230 try { (stdin as unknown as { unref?: () => void }).unref?.(); } catch { /* ignore */ }
231
232 debug(`armed (child pid ${child.pid ?? '?'}): timeoutMs=${timeoutMs} checkMs=${checkMs} progressPaths=${progressPaths.length}`);
233
234 let stopped = false;
235 return {
236 stop(): void {
237 if (stopped) return;

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