import { execFile } from 'child_process'; import { rmSync } from 'fs'; import { homedir } from 'os'; import path from 'path'; import { promisify } from 'util'; import { logger } from '../utils/logger.js'; import { HOOK_TIMEOUTS } from '../shared/hook-constants.js'; import { isPidAlive, type ManagedProcessRecord, type ProcessRegistry } from './process-registry.js'; const execFileAsync = promisify(execFile); const DATA_DIR = path.join(homedir(), '.claude-mem'); const PID_FILE = path.join(DATA_DIR, 'worker.pid'); type TreeKillFn = (pid: number, signal?: string, callback?: (error?: Error | null) => void) => void; export interface ShutdownCascadeOptions { registry: ProcessRegistry; currentPid?: number; pidFilePath?: string; } export async function runShutdownCascade(options: ShutdownCascadeOptions): Promise { const currentPid = options.currentPid ?? process.pid; const pidFilePath = options.pidFilePath ?? PID_FILE; const allRecords = options.registry.getAll(); const childRecords = [...allRecords] .filter(record => record.pid !== currentPid) .sort((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt)); for (const record of childRecords) { if (!isPidAlive(record.pid)) { options.registry.unregister(record.id); continue; } try { await signalProcess(record.pid, 'SIGTERM'); } catch (error) { logger.debug('SYSTEM', 'Failed to send SIGTERM to child process', { pid: record.pid, type: record.type }, error as Error); } } await waitForExit(childRecords, 5000); const survivors = childRecords.filter(record => isPidAlive(record.pid)); for (const record of survivors) { try { await signalProcess(record.pid, 'SIGKILL'); } catch (error) { logger.debug('SYSTEM', 'Failed to force kill child process', { pid: record.pid, type: record.type }, error as Error); } } await waitForExit(survivors, 1000); for (const record of childRecords) { options.registry.unregister(record.id); } for (const record of allRecords.filter(record => record.pid === currentPid)) { options.registry.unregister(record.id); } try { rmSync(pidFilePath, { force: true }); } catch (error) { logger.debug('SYSTEM', 'Failed to remove PID file during shutdown', { pidFilePath }, error as Error); } options.registry.pruneDeadEntries(); } async function waitForExit(records: ManagedProcessRecord[], timeoutMs: number): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const survivors = records.filter(record => isPidAlive(record.pid)); if (survivors.length === 0) { return; } await new Promise(resolve => setTimeout(resolve, 100)); } } async function signalProcess(pid: number, signal: 'SIGTERM' | 'SIGKILL'): Promise { if (signal === 'SIGTERM') { try { process.kill(pid, signal); } catch (error) { const errno = (error as NodeJS.ErrnoException).code; if (errno === 'ESRCH') { return; } throw error; } return; } if (process.platform === 'win32') { const treeKill = await loadTreeKill(); if (treeKill) { await new Promise((resolve, reject) => { treeKill(pid, signal, (error) => { if (!error) { resolve(); return; } const errno = (error as NodeJS.ErrnoException).code; if (errno === 'ESRCH') { resolve(); return; } reject(error); }); }); return; } const args = ['/PID', String(pid), '/T']; if (signal === 'SIGKILL') { args.push('/F'); } await execFileAsync('taskkill', args, { timeout: HOOK_TIMEOUTS.POWERSHELL_COMMAND, windowsHide: true }); return; } try { process.kill(pid, signal); } catch (error) { const errno = (error as NodeJS.ErrnoException).code; if (errno === 'ESRCH') { return; } throw error; } } async function loadTreeKill(): Promise { const moduleName = 'tree-kill'; try { const treeKillModule = await import(moduleName); return (treeKillModule.default ?? treeKillModule) as TreeKillFn; } catch { return null; } }