189 lines
5.8 KiB
JavaScript
189 lines
5.8 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawn, execFile } from 'node:child_process';
|
|
import { platform } from 'node:os';
|
|
|
|
const TIMEOUT_MS = parseInt(process.env.MEMTRACE_TIMEOUT_MS || '10000', 10);
|
|
|
|
function parseArgs() {
|
|
const args = process.argv.slice(2);
|
|
if (args.includes('--help') || args.includes('-h')) {
|
|
console.log(`Usage: node memtrace-restart.mjs [--dry-run]
|
|
|
|
Restarts the Memtrace MCP server by terminating stale processes and
|
|
verifying a fresh instance can respond to MCP initialize requests.
|
|
|
|
Options:
|
|
--dry-run Report what would be done without killing any processes
|
|
--help, -h Show this help`);
|
|
process.exit(0);
|
|
}
|
|
for (const arg of args) {
|
|
if (!arg.startsWith('-')) continue;
|
|
if (arg !== '--dry-run' && arg !== '--help' && arg !== '-h') {
|
|
console.error(`ERROR: Unknown argument: ${arg}. Use --help to see available options.`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
return { dryRun: args.includes('--dry-run') };
|
|
}
|
|
|
|
async function killStaleProcesses(dryRun) {
|
|
if (platform() === 'win32') {
|
|
if (dryRun) {
|
|
console.error('[restart] DRY-RUN: Would execute: taskkill /f /im memtrace.exe /t');
|
|
return;
|
|
}
|
|
return new Promise((resolvePromise) => {
|
|
execFile('taskkill', ['/f', '/im', 'memtrace.exe', '/t'], { windowsHide: true }, (err, stdout, stderr) => {
|
|
if (err) {
|
|
if (stderr && !stderr.toLowerCase().includes('not found') && !stderr.toLowerCase().includes('instance')) {
|
|
console.error(`[restart] taskkill warning: ${stderr.trim()}`);
|
|
}
|
|
}
|
|
if (stdout) console.error(`[restart] Terminated: ${stdout.trim().replace(/\r?\n/g, ' ')}`);
|
|
resolvePromise();
|
|
});
|
|
});
|
|
}
|
|
if (dryRun) {
|
|
console.error('[restart] DRY-RUN: Would execute: pkill -f "memtrace mcp"');
|
|
return;
|
|
}
|
|
return new Promise((resolvePromise) => {
|
|
execFile('pkill', ['-f', 'memtrace mcp'], (err) => {
|
|
if (err && err.code !== 1) {
|
|
console.error(`[restart] pkill warning: ${err.message}`);
|
|
}
|
|
resolvePromise();
|
|
});
|
|
});
|
|
}
|
|
|
|
async function verifyServerOnline() {
|
|
return new Promise((resolvePromise) => {
|
|
const child = spawn('memtrace', ['mcp'], {
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
shell: platform() === 'win32',
|
|
windowsHide: true
|
|
});
|
|
|
|
let resolved = false;
|
|
let stdoutBuffer = '';
|
|
|
|
const finish = (value) => {
|
|
if (resolved) return;
|
|
resolved = true;
|
|
try { child.stdin.end(); } catch (e) {}
|
|
try { child.kill(); } catch (e) {}
|
|
resolvePromise(value);
|
|
};
|
|
|
|
const timeout = setTimeout(() => {
|
|
if (!resolved) {
|
|
finish(false);
|
|
}
|
|
}, TIMEOUT_MS);
|
|
|
|
child.on('error', (err) => {
|
|
if (!resolved) {
|
|
clearTimeout(timeout);
|
|
if (err.code === 'ENOENT') {
|
|
console.error('[restart] memtrace binary not found on PATH. Verify memtrace is installed.');
|
|
} else {
|
|
console.error(`[restart] Verification spawn error: ${err.message}`);
|
|
}
|
|
finish(false);
|
|
}
|
|
});
|
|
|
|
child.on('exit', (code, signal) => {
|
|
if (!resolved) {
|
|
clearTimeout(timeout);
|
|
if (signal) {
|
|
console.error(`[restart] Verification child terminated by signal ${signal} before responding.`);
|
|
} else {
|
|
console.error(`[restart] Verification child exited with code ${code} before responding.`);
|
|
}
|
|
finish(false);
|
|
}
|
|
});
|
|
|
|
child.stderr.on('data', () => {});
|
|
|
|
const request = JSON.stringify({
|
|
jsonrpc: '2.0',
|
|
id: 1,
|
|
method: 'initialize',
|
|
params: {
|
|
protocolVersion: '2024-11-05',
|
|
capabilities: {},
|
|
clientInfo: { name: 'memtrace-restart-verifier', version: '1.0.0' }
|
|
}
|
|
}) + '\n';
|
|
|
|
child.stdout.on('data', (data) => {
|
|
if (resolved) return;
|
|
stdoutBuffer += data.toString();
|
|
const lines = stdoutBuffer.split('\n');
|
|
stdoutBuffer = lines.pop() || '';
|
|
|
|
for (const line of lines) {
|
|
if (!line.trim()) continue;
|
|
try {
|
|
const response = JSON.parse(line);
|
|
if (response.id === 1) {
|
|
if (!response.error) {
|
|
clearTimeout(timeout);
|
|
finish(true);
|
|
} else {
|
|
console.error(`[restart] MCP error response: ${response.error.message || JSON.stringify(response.error)}`);
|
|
}
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
if (line.trim().startsWith('{')) {
|
|
console.error(`[restart] Verification JSON parse error: ${err.message} (payload: ${line.trim().substring(0, 120)}...)`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
child.stdin.write(request);
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs();
|
|
|
|
console.error('[restart] Memtrace MCP server recovery initiated...');
|
|
|
|
console.error('[restart] Step 1/2: Terminating stale memtrace processes...');
|
|
await killStaleProcesses(args.dryRun);
|
|
|
|
if (args.dryRun) {
|
|
console.error('[restart] DRY-RUN complete. No processes terminated. Exiting with code 0.');
|
|
process.exit(0);
|
|
}
|
|
|
|
// Allow OS to release process handles, ports, and file locks before verification.
|
|
// 500ms is a best-effort delay; loaded systems may need more but we optimise for common case.
|
|
await new Promise(r => setTimeout(r, 500));
|
|
|
|
console.error('[restart] Step 2/2: Verifying memtrace server responds to MCP...');
|
|
const online = await verifyServerOnline();
|
|
|
|
if (!online) {
|
|
console.error(`[restart] FAIL: Memtrace server did not respond within ${TIMEOUT_MS}ms.`);
|
|
console.error('[restart] The IDE/client may need to reconnect on the next MCP tool call.');
|
|
console.error('[restart] If the issue persists, manual intervention is required (Story 4.3).');
|
|
process.exit(1);
|
|
}
|
|
|
|
console.error('[restart] SUCCESS: Memtrace MCP server verified operational.');
|
|
console.error('[restart] The IDE/client will reconnect automatically on the next MCP tool call.');
|
|
process.exit(0);
|
|
}
|
|
|
|
main();
|