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 { execClaude, escapeShellArg } from './utils/shell-executor';
import { wireChildProcessSignals } from './utils/signal-forwarder';
// Import target adapter system
import {
@@ -117,6 +118,15 @@ async function execClaudeWithProxy(
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)
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(` - Check proxy logs in ${getCcsDir()}/logs/ (if debug enabled)`);
console.error('');
proxy.kill();
stopProxy();
runCleanup();
process.exit(1);
}
@@ -220,58 +231,41 @@ async function execClaudeWithProxy(
});
}
// 5. Cleanup: kill proxy when Claude exits
const forwardSigTerm = () => {
proxy.kill('SIGTERM');
claude.kill('SIGTERM');
};
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') {
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.');
// 5. Shared signal forwarding + proxy cleanup lifecycle
wireChildProcessSignals(
claude,
(err: 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 {
console.error(fail(`Claude CLI not found: ${claudeCli}`));
}
} 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 ==========
@@ -1019,7 +1013,11 @@ process.on('unhandledRejection', (reason: unknown) => {
// Handle process termination signals for cleanup
process.on('SIGTERM', () => {
runCleanup();
try {
runCleanup();
} catch {
// Cleanup failure should not block termination.
}
// If a target exec path registered additional signal listeners, let those
// listeners forward/coordinate child shutdown and final exit codes.
if (process.listenerCount('SIGTERM') <= 1) {
@@ -1028,7 +1026,11 @@ process.on('SIGTERM', () => {
});
process.on('SIGINT', () => {
runCleanup();
try {
runCleanup();
} catch {
// Cleanup failure should not block termination.
}
// Same coordination rule as SIGTERM.
if (process.listenerCount('SIGINT') <= 1) {
process.exit(130); // 128 + SIGINT(2)