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 committed 2026-02-17 20:53:03 +07:00
1 parent 91edc9565b
commit 53e18d4c8d
1 file changed
+39 -37
+39 -37
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,45 +231,18 @@ 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) => {
};
const forwardSigInt = () => {
proxy.kill('SIGTERM');
claude.kill('SIGINT');
};
const forwardSighup = () => {
proxy.kill('SIGTERM');
claude.kill('SIGHUP');
};
process.on('SIGTERM', forwardSigTerm);
process.on('SIGINT', forwardSigInt);
process.on('SIGHUP', forwardSighup);
const cleanupSignalHandlers = () => {
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') { if (err.code === 'EACCES') {
console.error(fail(`Claude CLI is not executable: ${claudeCli}`)); console.error(fail(`Claude CLI is not executable: ${claudeCli}`));
console.error(' Check file permissions and executable bit.'); console.error(' Check file permissions and executable bit.');
} else if (err.code === 'ENOENT') { } else if (err.code === 'ENOENT') {
if (isPowerShellScript) { if (isPowerShellScript) {
console.error(fail('PowerShell executable not found (required for .ps1 wrapper launch).')); console.error(
fail('PowerShell executable not found (required for .ps1 wrapper launch).')
);
console.error(' Ensure powershell.exe is available in PATH.'); console.error(' Ensure powershell.exe is available in PATH.');
} else if (needsShell) { } else if (needsShell) {
console.error(fail('Windows command shell not found for Claude wrapper launch.')); console.error(fail('Windows command shell not found for Claude wrapper launch.'));
@@ -269,9 +253,19 @@ async function execClaudeWithProxy(
} else { } else {
console.error(fail(`Claude CLI error: ${err.message}`)); console.error(fail(`Claude CLI error: ${err.message}`));
} }
proxy.kill('SIGTERM'); stopProxy();
runCleanup();
process.exit(1); process.exit(1);
}); },
(code: number | null, signal: NodeJS.Signals | null) => {
stopProxy();
if (signal) {
process.kill(process.pid, signal);
} else {
process.exit(code || 0);
}
}
);
} }
// ========== 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', () => {
try {
runCleanup(); 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', () => {
try {
runCleanup(); 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)