Skip to content

Commit a8d579a

Browse files
committed
Improve Logtalk terminal process termination to use escalating signals to avoid orphans in case of busy and non-responding processes
1 parent 32acd10 commit a8d579a

File tree

3 files changed

+85
-3
lines changed

3 files changed

+85
-3
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## [0.88.2]
4+
5+
* Improve Logtalk terminal process termination to use escalating signals to avoid orphans in case of busy and non-responding processes
6+
37
## [0.88.1]
48

59
* Fix language keywords completions to be case sensitive

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "logtalk-for-vscode",
33
"displayName": "Logtalk for VSCode",
44
"description": "Logtalk programming support",
5-
"version": "0.88.1",
5+
"version": "0.88.2",
66
"publisher": "LogtalkDotOrg",
77
"icon": "images/logtalk.png",
88
"license": "MIT",
@@ -1332,7 +1332,7 @@
13321332
"compile": "tsc -watch -p ./",
13331333
"test": "tsc ./tests/runTest.ts",
13341334
"vsix:make": "vsce package --baseImagesUrl https://raw.githubusercontent.com/llvm/llvm-project/master/clang-tools-extra/clangd/clients/clangd-vscode/",
1335-
"vsix:install": "code --install-extension logtalk-for-vscode-0.88.1.vsix"
1335+
"vsix:install": "code --install-extension logtalk-for-vscode-0.88.2.vsix"
13361336
},
13371337
"devDependencies": {
13381338
"@types/bluebird": "^3.5.38",

src/features/terminal.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import * as vscode from "vscode";
1919
import * as path from "path";
2020
import * as jsesc from "jsesc";
2121
import * as fs from "fs";
22-
import { spawn } from "child_process";
22+
import { spawn, execSync } from "child_process";
2323
import LogtalkLinter from "./linterCodeActionProvider";
2424
import LogtalkTestsReporter from "./testsCodeActionProvider";
2525
import LogtalkDeadCodeScanner from "./deadCodeScannerCodeActionProvider";
@@ -49,10 +49,68 @@ export default class LogtalkTerminal {
4949
private static _loadedDirectories: Set<string> = new Set();
5050
private static disposables: Disposable[] = [];
5151

52+
// Terminal process tracking for reliable cleanup
53+
private static _terminalPid: number | null = null;
54+
5255
// Terminal readiness tracking
5356
private static _terminalReadyPromise: Promise<void> | null = null;
5457
private static _terminalReadyResolve: (() => void) | null = null;
5558

59+
/**
60+
* Attempt to kill a process and all its children using escalating signals.
61+
* Waits 2s for SIGHUP (already sent by terminal disposal) to take effect,
62+
* then sends SIGTERM, waits another 2s, and finally sends SIGKILL.
63+
* On Windows, uses taskkill /T for tree kill after the initial 2s grace period.
64+
*/
65+
private static killProcessTree(pid: number): void {
66+
const logger = getLogger();
67+
68+
const isAlive = (p: number): boolean => {
69+
try { process.kill(p, 0); return true; } catch { return false; }
70+
};
71+
72+
const sendSignal = (p: number, signal: NodeJS.Signals): void => {
73+
try {
74+
process.kill(-p, signal);
75+
} catch {
76+
try { process.kill(p, signal); } catch { /* already dead */ }
77+
}
78+
};
79+
80+
// Wait 2s for the default SIGHUP (sent by the terminal close) to work
81+
setTimeout(() => {
82+
if (!isAlive(pid)) {
83+
logger.info(`Logtalk process ${pid} exited after SIGHUP`);
84+
return;
85+
}
86+
87+
try {
88+
if (process.platform === 'win32') {
89+
// /T kills the process tree, /F forces termination
90+
execSync(`taskkill /F /T /PID ${pid}`, { timeout: 5000 });
91+
logger.info(`Killed Logtalk process tree (PID ${pid}) via taskkill`);
92+
} else {
93+
// SIGHUP didn't work — escalate to SIGTERM
94+
logger.info(`Process ${pid} still alive after SIGHUP, sending SIGTERM`);
95+
sendSignal(pid, 'SIGTERM');
96+
97+
// Wait another 2s, then escalate to SIGKILL if needed
98+
setTimeout(() => {
99+
if (!isAlive(pid)) {
100+
logger.info(`Logtalk process ${pid} exited after SIGTERM`);
101+
return;
102+
}
103+
logger.warn(`Process ${pid} still alive after SIGTERM, sending SIGKILL`);
104+
sendSignal(pid, 'SIGKILL');
105+
}, 2000);
106+
}
107+
} catch (error) {
108+
// Process may already be dead; that's fine
109+
logger.debug(`Process tree kill for PID ${pid} returned: ${error}`);
110+
}
111+
}, 2000);
112+
}
113+
56114
/**
57115
* Expands VS Code-style environment variables (${env:VAR}) to their actual values.
58116
* @param value The string containing environment variables to expand
@@ -242,6 +300,12 @@ export default class LogtalkTerminal {
242300
return (<any>window).onDidCloseTerminal(terminal => {
243301
// Only clear if the closed terminal is the Logtalk terminal
244302
if (terminal === LogtalkTerminal._terminal) {
303+
// Kill the underlying Logtalk/Prolog process tree to avoid orphans
304+
if (LogtalkTerminal._terminalPid) {
305+
LogtalkTerminal.killProcessTree(LogtalkTerminal._terminalPid);
306+
LogtalkTerminal._terminalPid = null;
307+
}
308+
245309
// Check if the terminal crashed
246310
if (terminal.exitStatus) {
247311
if (terminal.exitStatus.code !== undefined && terminal.exitStatus.code !== 0) {
@@ -298,6 +362,12 @@ export default class LogtalkTerminal {
298362
* Dispose of all resources
299363
*/
300364
public static dispose(): void {
365+
// Kill the underlying Logtalk/Prolog process tree before disposing
366+
if (LogtalkTerminal._terminalPid) {
367+
LogtalkTerminal.killProcessTree(LogtalkTerminal._terminalPid);
368+
LogtalkTerminal._terminalPid = null;
369+
}
370+
301371
// Explicitly dispose of the terminal to prevent restoration
302372
if (LogtalkTerminal._terminal) {
303373
try {
@@ -439,6 +509,14 @@ export default class LogtalkTerminal {
439509
isTransient: true
440510
});
441511

512+
// Capture the terminal's shell PID for reliable process-tree cleanup
513+
LogtalkTerminal._terminal.processId.then((pid: number | undefined) => {
514+
if (pid !== undefined) {
515+
LogtalkTerminal._terminalPid = pid;
516+
getLogger().info(`Logtalk terminal started with PID ${pid}`);
517+
}
518+
});
519+
442520
// Reset the creation flag now that terminal is created
443521
LogtalkTerminal._terminalCreationInProgress = false;
444522

0 commit comments

Comments
 (0)