af145cfaef
* fix(windows): enable worker logging on Windows Previously, Windows worker startup via PowerShell Start-Process did not redirect stdout/stderr to log files, making debugging startup failures impossible. This adds -RedirectStandardOutput and -RedirectStandardError to capture worker logs to ~/.claude-mem/logs/worker-YYYY-MM-DD.log. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(windows): improve worker stop/restart reliability - Use HTTP shutdown endpoint as primary stop method (worker kills itself) - Only remove PID file after confirming worker is actually dead - Remove auto-respawn from wrapper to prevent PID file mismatches - Wrapper now exits when inner worker crashes (hooks will restart) This hopefully fixes issues where npm run worker:stop would fail silently when the worker was started from hooks, leaving zombie processes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
158 lines
4.3 KiB
TypeScript
158 lines
4.3 KiB
TypeScript
/**
|
|
* Worker Wrapper - Manages worker process lifecycle
|
|
*
|
|
* This wrapper exists to solve the Windows zombie port problem.
|
|
* The wrapper spawns the actual worker as a child process.
|
|
* When shutdown is requested, the wrapper kills the child and exits.
|
|
* The hooks will start a fresh wrapper+worker if needed.
|
|
*
|
|
* The wrapper itself has no sockets, so Bun's socket cleanup bug
|
|
* doesn't affect it.
|
|
*
|
|
* NOTE: The wrapper does NOT auto-restart the worker on crash.
|
|
* This is intentional - the hooks handle startup via ensureWorkerRunning().
|
|
* Auto-restart would cause PID file mismatches and potential infinite loops.
|
|
*/
|
|
|
|
import { spawn, ChildProcess, execSync } from 'child_process';
|
|
import path from 'path';
|
|
|
|
const isWindows = process.platform === 'win32';
|
|
|
|
const SCRIPT_DIR = __dirname;
|
|
const INNER_SCRIPT = path.join(SCRIPT_DIR, 'worker-service.cjs');
|
|
|
|
let inner: ChildProcess | null = null;
|
|
let isShuttingDown = false;
|
|
|
|
function log(msg: string) {
|
|
const timestamp = new Date().toISOString();
|
|
console.log(`[${timestamp}] [wrapper] ${msg}`);
|
|
}
|
|
|
|
function spawnInner() {
|
|
log(`Spawning inner worker: ${INNER_SCRIPT}`);
|
|
|
|
inner = spawn(process.execPath, [INNER_SCRIPT], {
|
|
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
|
env: { ...process.env, CLAUDE_MEM_MANAGED: 'true' },
|
|
cwd: path.dirname(INNER_SCRIPT),
|
|
});
|
|
|
|
inner.on('message', async (msg: { type: string }) => {
|
|
if (msg.type === 'restart' || msg.type === 'shutdown') {
|
|
// Both restart and shutdown: kill inner and exit wrapper
|
|
// The hooks will start a fresh wrapper+inner if needed
|
|
log(`${msg.type} requested by inner`);
|
|
isShuttingDown = true;
|
|
await killInner();
|
|
log('Exiting wrapper');
|
|
process.exit(0);
|
|
}
|
|
});
|
|
|
|
inner.on('exit', (code, signal) => {
|
|
log(`Inner exited with code=${code}, signal=${signal}`);
|
|
inner = null;
|
|
|
|
// Don't auto-restart - let hooks handle it via ensureWorkerRunning()
|
|
// Auto-restart causes PID file mismatches and potential infinite loops
|
|
if (!isShuttingDown) {
|
|
log('Inner exited unexpectedly, wrapper exiting (hooks will restart if needed)');
|
|
process.exit(code ?? 1);
|
|
}
|
|
});
|
|
|
|
inner.on('error', (err) => {
|
|
log(`Inner error: ${err.message}`);
|
|
});
|
|
}
|
|
|
|
async function killInner(): Promise<void> {
|
|
if (!inner || !inner.pid) {
|
|
log('No inner process to kill');
|
|
return;
|
|
}
|
|
|
|
const pid = inner.pid;
|
|
log(`Killing inner process tree (pid=${pid})`);
|
|
|
|
if (isWindows) {
|
|
// On Windows, use taskkill /T /F to kill entire process tree
|
|
// This ensures all children (MCP server, ChromaSync, etc.) are killed
|
|
// which is necessary to properly release the socket
|
|
try {
|
|
execSync(`taskkill /PID ${pid} /T /F`, { timeout: 10000, stdio: 'ignore' });
|
|
log(`taskkill completed for pid=${pid}`);
|
|
} catch (error) {
|
|
// Process may already be dead
|
|
log(`taskkill failed (process may be dead): ${error}`);
|
|
}
|
|
} else {
|
|
// On Unix, SIGTERM then SIGKILL
|
|
inner.kill('SIGTERM');
|
|
|
|
// Wait for exit with timeout
|
|
const exitPromise = new Promise<void>(resolve => {
|
|
if (!inner) {
|
|
resolve();
|
|
return;
|
|
}
|
|
inner.on('exit', () => resolve());
|
|
});
|
|
|
|
const timeoutPromise = new Promise<void>(resolve =>
|
|
setTimeout(() => resolve(), 5000)
|
|
);
|
|
|
|
await Promise.race([exitPromise, timeoutPromise]);
|
|
|
|
// Force kill if still alive
|
|
if (inner && !inner.killed) {
|
|
log('Inner did not exit gracefully, force killing');
|
|
inner.kill('SIGKILL');
|
|
}
|
|
}
|
|
|
|
// Wait for the process to fully exit
|
|
await waitForProcessExit(pid, 5000);
|
|
|
|
inner = null;
|
|
log('Inner process terminated');
|
|
}
|
|
|
|
async function waitForProcessExit(pid: number, timeoutMs: number): Promise<void> {
|
|
const start = Date.now();
|
|
|
|
while (Date.now() - start < timeoutMs) {
|
|
try {
|
|
process.kill(pid, 0); // Check if process exists
|
|
await new Promise(r => setTimeout(r, 100));
|
|
} catch {
|
|
// Process is dead
|
|
return;
|
|
}
|
|
}
|
|
|
|
log(`Timeout waiting for process ${pid} to exit`);
|
|
}
|
|
|
|
// Handle wrapper signals
|
|
process.on('SIGTERM', async () => {
|
|
log('Wrapper received SIGTERM');
|
|
isShuttingDown = true;
|
|
await killInner();
|
|
process.exit(0);
|
|
});
|
|
|
|
process.on('SIGINT', async () => {
|
|
log('Wrapper received SIGINT');
|
|
isShuttingDown = true;
|
|
await killInner();
|
|
process.exit(0);
|
|
});
|
|
|
|
// Start the inner worker
|
|
log('Wrapper starting');
|
|
spawnInner();
|