@@ -19,7 +19,7 @@ import * as vscode from "vscode";
1919import * as path from "path" ;
2020import * as jsesc from "jsesc" ;
2121import * as fs from "fs" ;
22- import { spawn } from "child_process" ;
22+ import { spawn , execSync } from "child_process" ;
2323import LogtalkLinter from "./linterCodeActionProvider" ;
2424import LogtalkTestsReporter from "./testsCodeActionProvider" ;
2525import 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