fix(core): prevent GLMT proxy leaks in child lifecycle

- reuse shared wireChildProcessSignals for GLMT Claude execution

- centralize proxy stop logic on child error and exit paths

- guard global signal cleanup so cleanup failures do not block exit
This commit is contained in:
Tam Nhu Tran
2026-02-17 20:53:03 +07:00
parent 91edc9565b
commit 53e18d4c8d
+54 -52
View File
@@ -37,6 +37,7 @@ import { handleUpdateCommand } from './commands/update-command';
// Import extracted utility functions // Import extracted utility functions
import { execClaude, escapeShellArg } from './utils/shell-executor'; import { execClaude, escapeShellArg } from './utils/shell-executor';
import { wireChildProcessSignals } from './utils/signal-forwarder';
// Import target adapter system // Import target adapter system
import { import {
@@ -117,6 +118,15 @@ async function execClaudeWithProxy(
ANTHROPIC_BASE_URL: envData['ANTHROPIC_BASE_URL'], ANTHROPIC_BASE_URL: envData['ANTHROPIC_BASE_URL'],
}, },
}); });
const stopProxy = (): void => {
try {
if (!proxy.killed) {
proxy.kill('SIGTERM');
}
} catch {
// Best-effort cleanup on process teardown.
}
};
// 3. Wait for proxy ready signal (with timeout) // 3. Wait for proxy ready signal (with timeout)
const { ProgressIndicator } = await import('./utils/progress-indicator'); const { ProgressIndicator } = await import('./utils/progress-indicator');
@@ -167,7 +177,8 @@ async function execClaudeWithProxy(
console.error(' - Enable verbose logging: ccs glmt --verbose "prompt"'); console.error(' - Enable verbose logging: ccs glmt --verbose "prompt"');
console.error(` - Check proxy logs in ${getCcsDir()}/logs/ (if debug enabled)`); console.error(` - Check proxy logs in ${getCcsDir()}/logs/ (if debug enabled)`);
console.error(''); console.error('');
proxy.kill(); stopProxy();
runCleanup();
process.exit(1); process.exit(1);
} }
@@ -220,58 +231,41 @@ async function execClaudeWithProxy(
}); });
} }
// 5. Cleanup: kill proxy when Claude exits // 5. Shared signal forwarding + proxy cleanup lifecycle
const forwardSigTerm = () => { wireChildProcessSignals(
proxy.kill('SIGTERM'); claude,
claude.kill('SIGTERM'); (err: NodeJS.ErrnoException) => {
}; if (err.code === 'EACCES') {
const forwardSigInt = () => { console.error(fail(`Claude CLI is not executable: ${claudeCli}`));
proxy.kill('SIGTERM'); console.error(' Check file permissions and executable bit.');
claude.kill('SIGINT'); } else if (err.code === 'ENOENT') {
}; if (isPowerShellScript) {
const forwardSighup = () => { console.error(
proxy.kill('SIGTERM'); fail('PowerShell executable not found (required for .ps1 wrapper launch).')
claude.kill('SIGHUP'); );
}; console.error(' Ensure powershell.exe is available in PATH.');
process.on('SIGTERM', forwardSigTerm); } else if (needsShell) {
process.on('SIGINT', forwardSigInt); console.error(fail('Windows command shell not found for Claude wrapper launch.'));
process.on('SIGHUP', forwardSighup); console.error(' Ensure cmd.exe is available and accessible.');
} else {
const cleanupSignalHandlers = () => { console.error(fail(`Claude CLI not found: ${claudeCli}`));
process.removeListener('SIGTERM', forwardSigTerm); }
process.removeListener('SIGINT', forwardSigInt);
process.removeListener('SIGHUP', forwardSighup);
};
claude.on('exit', (code, signal) => {
cleanupSignalHandlers();
proxy.kill('SIGTERM');
if (signal) process.kill(process.pid, signal as NodeJS.Signals);
else process.exit(code || 0);
});
claude.on('error', (error) => {
cleanupSignalHandlers();
const err = error as NodeJS.ErrnoException;
if (err.code === 'EACCES') {
console.error(fail(`Claude CLI is not executable: ${claudeCli}`));
console.error(' Check file permissions and executable bit.');
} else if (err.code === 'ENOENT') {
if (isPowerShellScript) {
console.error(fail('PowerShell executable not found (required for .ps1 wrapper launch).'));
console.error(' Ensure powershell.exe is available in PATH.');
} else if (needsShell) {
console.error(fail('Windows command shell not found for Claude wrapper launch.'));
console.error(' Ensure cmd.exe is available and accessible.');
} else { } else {
console.error(fail(`Claude CLI not found: ${claudeCli}`)); console.error(fail(`Claude CLI error: ${err.message}`));
}
stopProxy();
runCleanup();
process.exit(1);
},
(code: number | null, signal: NodeJS.Signals | null) => {
stopProxy();
if (signal) {
process.kill(process.pid, signal);
} else {
process.exit(code || 0);
} }
} else {
console.error(fail(`Claude CLI error: ${err.message}`));
} }
proxy.kill('SIGTERM'); );
process.exit(1);
});
} }
// ========== Main Execution ========== // ========== Main Execution ==========
@@ -1019,7 +1013,11 @@ process.on('unhandledRejection', (reason: unknown) => {
// Handle process termination signals for cleanup // Handle process termination signals for cleanup
process.on('SIGTERM', () => { process.on('SIGTERM', () => {
runCleanup(); try {
runCleanup();
} catch {
// Cleanup failure should not block termination.
}
// If a target exec path registered additional signal listeners, let those // If a target exec path registered additional signal listeners, let those
// listeners forward/coordinate child shutdown and final exit codes. // listeners forward/coordinate child shutdown and final exit codes.
if (process.listenerCount('SIGTERM') <= 1) { if (process.listenerCount('SIGTERM') <= 1) {
@@ -1028,7 +1026,11 @@ process.on('SIGTERM', () => {
}); });
process.on('SIGINT', () => { process.on('SIGINT', () => {
runCleanup(); try {
runCleanup();
} catch {
// Cleanup failure should not block termination.
}
// Same coordination rule as SIGTERM. // Same coordination rule as SIGTERM.
if (process.listenerCount('SIGINT') <= 1) { if (process.listenerCount('SIGINT') <= 1) {
process.exit(130); // 128 + SIGINT(2) process.exit(130); // 128 + SIGINT(2)