fix(interactive): clean up leaked SIGINT and extension selector listeners (#2172)

- Wrap handleCtrlZ() suspend logic in try-catch so the SIGINT listener
  is removed if process.kill() or ui.stop() throws
- Dispose previous extension selector in showExtensionSelector() before
  creating a new one, preventing promise leaks on rapid calls
This commit is contained in:
Juan Francisco Lebrero 2026-03-23 12:48:18 -03:00 committed by GitHub
parent eb48a7cdde
commit a9667209ef

View file

@ -1519,6 +1519,13 @@ export class InteractiveMode {
options: string[],
opts?: ExtensionUIDialogOptions,
): Promise<string | undefined> {
// If a previous selector is still active, dispose it before creating a
// new one. This avoids leaking the previous promise and DOM state when
// showExtensionSelector is called rapidly.
if (this.extensionSelector) {
this.hideExtensionSelector();
}
return new Promise((resolve) => {
if (opts?.signal?.aborted) {
resolve(undefined);
@ -2331,18 +2338,24 @@ export class InteractiveMode {
const ignoreSigint = () => {};
process.on("SIGINT", ignoreSigint);
// Set up handler to restore TUI when resumed
process.once("SIGCONT", () => {
try {
// Set up handler to restore TUI when resumed
process.once("SIGCONT", () => {
process.removeListener("SIGINT", ignoreSigint);
this.ui.start();
this.ui.requestRender(true);
});
// Stop the TUI (restore terminal to normal mode)
this.ui.stop();
// Send SIGTSTP to process group (pid=0 means all processes in group)
process.kill(0, "SIGTSTP");
} catch {
// If suspend fails (e.g. SIGTSTP not supported), ensure the
// SIGINT listener doesn't leak.
process.removeListener("SIGINT", ignoreSigint);
this.ui.start();
this.ui.requestRender(true);
});
// Stop the TUI (restore terminal to normal mode)
this.ui.stop();
// Send SIGTSTP to process group (pid=0 means all processes in group)
process.kill(0, "SIGTSTP");
}
}
private async handleFollowUp(): Promise<void> {