From 9f534918fc5693ca1b45ea816b5e473a84946733 Mon Sep 17 00:00:00 2001 From: Gustavo Martin Date: Sun, 9 Nov 2025 18:51:33 +0100 Subject: [PATCH 1/3] Squashed commits. Language server shutdown process when using ssh --- package.json | 26 +++ src/colibri/config/config_web.ts | 19 ++- src/teroshdl/features/config.ts | 16 ++ .../language_provider/lsp/rust_hdl.ts | 160 +++++++++++++++++- .../features/language_provider/lsp/verible.ts | 144 +++++++++++++++- src/teroshdl/teroshdl.ts | 22 +++ 6 files changed, 370 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 6e0768e6..25862ca9 100644 --- a/package.json +++ b/package.json @@ -770,6 +770,14 @@ }, "title": "Module documentation preview" } + ,{ + "command": "teroshdl.cleanup.openSettings", + "title": "TerosHDL: Open Cleanup Settings" + } + ,{ + "command": "teroshdl.cleanup.toggleKillServerProcesses", + "title": "TerosHDL: Toggle Kill Server Processes" + } ], "languages": [ @@ -893,6 +901,24 @@ "when": "editorTextFocus" } ] + , + "configuration": { + "type": "object", + "title": "TerosHDL", + "properties": { + "teroshdl.cleanup.killServerProcesses.enabled": { + "type": "boolean", + "default": false, + "description": "Enable automatic cleanup of language server processes on extension exit/deactivate (disabled by default)." + }, + "teroshdl.cleanup.killServerProcesses.gracePeriodMs": { + "type": "number", + "default": 500, + "minimum": 0, + "description": "Grace period in milliseconds between SIGTERM and SIGKILL when killing language server processes." + } + } + } }, "scripts": { diff --git a/src/colibri/config/config_web.ts b/src/colibri/config/config_web.ts index f84dabc5..b038e7ba 100755 --- a/src/colibri/config/config_web.ts +++ b/src/colibri/config/config_web.ts @@ -957,7 +957,11 @@ body.vscode-high-contrast { - +
+
+ unknown +
+
@@ -5782,6 +5786,12 @@ body.vscode-high-contrast { }); } + function open_cleanup_settings(){ + vscode.postMessage({ + command: 'openCleanupSettings' + }); + } + window.addEventListener('message', event => { const message = event.data; switch (message.command) { @@ -5793,6 +5803,13 @@ body.vscode-high-contrast { if (tool != undefined && tool != ""){ enable_tab("tools", tool); } + try { + const el = document.getElementById('cleanupIndicator'); + if (el) { + const effective = message.cleanupEffective || (message.cleanupEnabled ? ( (message.remoteName||'').startsWith('ssh-remote') ? 'enabled (SSH only)' : 'enabled (not active: not SSH)') : 'disabled'); + el.textContent = 'Auto cleanup language server processes: ' + effective; + } + } catch (e) { /* ignore */ } break; } }); diff --git a/src/teroshdl/features/config.ts b/src/teroshdl/features/config.ts index 02aa7187..0a267e5a 100644 --- a/src/teroshdl/features/config.ts +++ b/src/teroshdl/features/config.ts @@ -196,6 +196,12 @@ export class Config_manager { case 'load': this.loadConfigFromFile(); return; + case 'openCleanupSettings': + // Open the cleanup settings UI (calls the command registered elsewhere) + try { + await vscode.commands.executeCommand('teroshdl.cleanup.openSettings'); + } catch (e) { /* ignore */ } + return; } }, undefined, @@ -281,6 +287,13 @@ export class Config_manager { * @returns A promise that resolves when the update is complete. */ private async updateWebConfig(tabToOpen: string): Promise { + const cleanupCfg = vscode.workspace.getConfiguration('teroshdl.cleanup'); + const cleanupEnabled = cleanupCfg.get('killServerProcesses.enabled', false); + const remoteName = (vscode.env.remoteName ?? '').toString(); + const cleanupEffective = cleanupEnabled + ? (remoteName.startsWith('ssh-remote') ? 'enabled (SSH only)' : 'enabled (not active: not SSH)') + : 'disabled'; + await this.panel?.webview.postMessage({ command: "set_config", config: this.currentConfig, @@ -288,6 +301,9 @@ export class Config_manager { title: this.getTitle(), projectName: this.currentProjectName, tool: tabToOpen, + cleanupEnabled: cleanupEnabled, + remoteName: remoteName, + cleanupEffective: cleanupEffective }); } diff --git a/src/teroshdl/features/language_provider/lsp/rust_hdl.ts b/src/teroshdl/features/language_provider/lsp/rust_hdl.ts index 7359f069..3037ba90 100644 --- a/src/teroshdl/features/language_provider/lsp/rust_hdl.ts +++ b/src/teroshdl/features/language_provider/lsp/rust_hdl.ts @@ -10,6 +10,7 @@ import semver = require('semver'); import vscode = require('vscode'); import { ExtensionContext } from 'vscode'; import util = require('util'); +import { debugLogger } from '../../../logger'; import { Multi_project_manager } from 'colibri/project_manager/multi_project_manager'; import * as utils from '../../utils/utils'; @@ -33,6 +34,7 @@ export class Rusthdl_lsp { private client: LanguageClient | undefined = undefined; private context: ExtensionContext; private languageServerDisposable; + private serverCommandPath: string | undefined; private manager: Multi_project_manager; public stop_client: boolean = false; private errorCounter = 0; @@ -43,14 +45,14 @@ export class Rusthdl_lsp { this.context.subscriptions.push( vscode.commands.registerCommand('teroshdl.vhdlls.restart', async () => { - if (this.client != undefined && this.client.isRunning() && this.client.state === State.Running) { + if (this.client !== undefined && this.client.isRunning() && this.client.state === State.Running) { try { await this.client.restart(); } catch (error) { this.errorCounter++; this.client.dispose(); this.client = undefined; - console.log(error); + debugLogger.error(String(error)); if (this.errorCounter < 5) { await this.run_rusthdl(); } @@ -58,6 +60,25 @@ export class Rusthdl_lsp { } }) ); + + // Ensure we attempt to kill any leftover servers when the extension host exits + process.on('exit', async () => { + try { + await this.killServerProcesses(); + } catch (e) { /* ignore */ } + }); + process.on('SIGINT', async () => { + try { + await this.killServerProcesses(); + } catch (e) { /* ignore */ } + process.exit(); + }); + process.on('SIGHUP', async () => { + try { + await this.killServerProcesses(); + } catch (e) { /* ignore */ } + process.exit(); + }); } async run_rusthdl(): Promise { @@ -100,13 +121,12 @@ export class Rusthdl_lsp { async check_rust_hdl(rust_hdl_bin_path: string) { let command = rust_hdl_bin_path + ' --version'; - // eslint-disable-next-line no-console - console.log(`[colibri][info] Linting with command: ${command}`); + debugLogger.info(`[colibri][info] Linting with command: ${command}`); const exec = require('child_process').exec; return new Promise((resolve) => { exec(command, (err, stdout, stderr) => { if (stderr !== '') { - console.log(`[rusthdl][error] ${stderr}`); + debugLogger.error(`[rusthdl][error] ${stderr}`); } if (stderr === '') { resolve(true); @@ -118,10 +138,49 @@ export class Rusthdl_lsp { } async deactivate() { + const logFile = require('os').homedir() + '/vhdl_ls_deactivate.log'; + const log = (msg: string) => { + try { + require('fs').appendFileSync(logFile, `${new Date().toISOString()} - ${msg}\n`); + } catch (e) { /* ignore */ } + }; + + log('=== DEACTIVATE CALLED ==='); if (!this.client) { + log('No client to deactivate'); return undefined; } - await this.client.stop(1000); + try { + log(`Client state before stop: ${this.client.state}`); + log('Calling client.stop(5000)...'); + // Increase timeout to 5 seconds to ensure proper shutdown in SSH scenarios + await this.client.stop(5000); + log('Client stopped successfully'); + + // Explicitly dispose of the language server disposable + if (this.languageServerDisposable) { + log('Disposing languageServerDisposable...'); + this.languageServerDisposable.dispose(); + log('Disposable cleaned up'); + } + debugLogger.info('[vhdl_ls] Language server stopped successfully'); + } catch (error) { + log(`ERROR during stop: ${error}`); + debugLogger.error(`[vhdl_ls] Error stopping language server: ${String(error)}`); + // Force dispose even if stop fails + if (this.languageServerDisposable) { + this.languageServerDisposable.dispose(); + log('Disposable force-disposed after error'); + } + } finally { + this.client = undefined; + this.languageServerDisposable = undefined; + log('=== DEACTIVATE COMPLETE ==='); + // Try to kill any lingering server processes that match our server binary + try { + await this.killServerProcesses(); + } catch (e) { /* ignore */ } + } } embeddedVersion(languageServerDir: string): string { @@ -148,6 +207,8 @@ export class Rusthdl_lsp { args.push('--silent'); let serverCommand = context.asAbsolutePath(languageServer); + // remember server path for cleanup + this.serverCommandPath = serverCommand; let serverOptions: ServerOptions = { run: { command: serverCommand, @@ -155,7 +216,10 @@ export class Rusthdl_lsp { options: { env: { VHDL_LS_CONFIG: this.fileListPath - } + }, + // Ensure process is killed when parent terminates (important for SSH scenarios) + detached: true, + shell: false } }, debug: { @@ -164,10 +228,90 @@ export class Rusthdl_lsp { options: { env: { VHDL_LS_CONFIG: this.fileListPath - } + }, + // Ensure process is killed when parent terminates (important for SSH scenarios) + detached: true, + shell: false } } }; + + // We rely on explicit cleanup (killServerProcesses) called on deactivate/exit + return serverOptions; } + + private async killServerProcesses(): Promise { + if (!this.serverCommandPath) { + return; + } + const serverPath = this.serverCommandPath; + + try { + const cfg = vscode.workspace.getConfiguration('teroshdl.cleanup'); + const enabled = cfg.get('killServerProcesses.enabled', false); + if (!enabled) { + return; + } + + // Safety: only perform process kill when running over an SSH remote session + const remoteName = (vscode.env.remoteName ?? '').toString(); + const isSSH = remoteName.startsWith('ssh-remote'); + if (!isSSH) { + debugLogger.info('[vhdl_ls] Not an SSH remote session — skipping killServerProcesses'); + return; + } + + const grace = cfg.get('killServerProcesses.gracePeriodMs', 500); + + // Platform guard: pgrep is Unix-specific + if (process.platform === 'win32') { + return; + } + + const execFile = require('child_process').execFile; + // Use pgrep -f to find matching processes, then kill them gracefully, then force kill + return new Promise((resolve) => { + execFile('pgrep', ['-f', serverPath], (err: any, stdout: string) => { + if (err || !stdout) { + return resolve(); + } + const pids = stdout + .split(/\s+/) + .map((s: string) => s.trim()) + .filter(Boolean) + .filter((x: string) => /^\d+$/.test(x)); + if (pids.length === 0) { + return resolve(); + } + + // First try SIGTERM + pids.forEach((pid: string) => { + const n = parseInt(pid, 10); + if (Number.isNaN(n)) { + return; + } + try { + process.kill(n, 'SIGTERM'); + } catch (e) { /* ignore */ } + }); + // After configurable delay, force kill remaining + setTimeout(() => { + pids.forEach((pid: string) => { + const n = parseInt(pid, 10); + if (Number.isNaN(n)) { + return; + } + try { + process.kill(n, 'SIGKILL'); + } catch (e) { /* ignore */ } + }); + resolve(); + }, Math.max(0, grace)); + }); + }); + } catch (e) { + return; + } + } } diff --git a/src/teroshdl/features/language_provider/lsp/verible.ts b/src/teroshdl/features/language_provider/lsp/verible.ts index c7037075..6b2229df 100644 --- a/src/teroshdl/features/language_provider/lsp/verible.ts +++ b/src/teroshdl/features/language_provider/lsp/verible.ts @@ -9,6 +9,7 @@ import * as path from 'path'; import vscode = require('vscode'); import { ExtensionContext } from 'vscode'; import util = require('util'); +import { debugLogger } from '../../../logger'; import { Multi_project_manager } from 'colibri/project_manager/multi_project_manager'; import * as os from 'os'; @@ -30,6 +31,7 @@ export class Verilbe_lsp { private client: LanguageClient | undefined = undefined; private context: ExtensionContext; private languageServerDisposable; + private serverCommandPath: string | undefined; private manager: Multi_project_manager; public stop_client: boolean = false; private errorCounter = 0; @@ -49,7 +51,7 @@ export class Verilbe_lsp { // this.errorCounter++; // this.client.dispose(); // this.client = undefined; - // console.log(error); + // debugLogger.error(String(error)); // if (this.errorCounter < 5) { // await this.run(); // } @@ -57,6 +59,25 @@ export class Verilbe_lsp { // } }) ); + + // Ensure we attempt to kill any leftover servers when the extension host exits + process.on('exit', async () => { + try { + await this.killServerProcesses(); + } catch (e) { /* ignore */ } + }); + process.on('SIGINT', async () => { + try { + await this.killServerProcesses(); + } catch (e) { /* ignore */ } + process.exit(); + }); + process.on('SIGHUP', async () => { + try { + await this.killServerProcesses(); + } catch (e) { /* ignore */ } + process.exit(); + }); } async run(): Promise { @@ -102,13 +123,12 @@ export class Verilbe_lsp { async check_run(binPath: string) { let command = binPath + ' --version'; - // eslint-disable-next-line no-console - console.log(`[colibri][info] Linting with command: ${command}`); + debugLogger.info(`[colibri][info] Linting with command: ${command}`); const exec = require('child_process').exec; return new Promise((resolve) => { exec(command, (err, stdout: string, stderr) => { if (stderr !== '') { - console.log(`[verible][error] ${stderr}`); + debugLogger.error(`[verible][error] ${stderr}`); } if (err !== null || !stdout.toLowerCase().includes('version')) { resolve(false); @@ -123,7 +143,100 @@ export class Verilbe_lsp { if (!this.client) { return undefined; } - await this.client.stop(1000); + try { + // Increase timeout to 5 seconds to ensure proper shutdown in SSH scenarios + await this.client.stop(5000); + // Explicitly dispose of the language server disposable + if (this.languageServerDisposable) { + this.languageServerDisposable.dispose(); + } + debugLogger.info('[verible] Language server stopped successfully'); + } catch (error) { + debugLogger.error(`[verible] Error stopping language server: ${String(error)}`); + // Force dispose even if stop fails + if (this.languageServerDisposable) { + this.languageServerDisposable.dispose(); + } + } finally { + this.client = undefined; + this.languageServerDisposable = undefined; + // Try to kill any lingering server processes that match our server binary + try { + await this.killServerProcesses(); + } catch (e) { /* ignore */ } + } + } + + private async killServerProcesses(): Promise { + if (!this.serverCommandPath) { + return; + } + const serverPath = this.serverCommandPath; + + try { + const cfg = vscode.workspace.getConfiguration('teroshdl.cleanup'); + const enabled = cfg.get('killServerProcesses.enabled', false); + if (!enabled) { + return; + } + + // Safety: only perform process kill when running over an SSH remote session + const remoteName = (vscode.env.remoteName ?? '').toString(); + const isSSH = remoteName.startsWith('ssh-remote'); + if (!isSSH) { + debugLogger.info('[verible] Not an SSH remote session — skipping killServerProcesses'); + return; + } + + const grace = cfg.get('killServerProcesses.gracePeriodMs', 500); + + // Platform guard: pgrep is Unix-specific + if (process.platform === 'win32') { + return; + } + + const execFile = require('child_process').execFile; + return new Promise((resolve) => { + execFile('pgrep', ['-f', serverPath], (err: any, stdout: string) => { + if (err || !stdout) { + return resolve(); + } + const pids = stdout + .split(/\s+/) + .map((s: string) => s.trim()) + .filter(Boolean) + .filter((x: string) => /^\d+$/.test(x)); + if (pids.length === 0) { + return resolve(); + } + // First try SIGTERM + pids.forEach((pid: string) => { + const n = parseInt(pid, 10); + if (Number.isNaN(n)) { + return; + } + try { + process.kill(n, 'SIGTERM'); + } catch (e) { /* ignore */ } + }); + // After configurable delay, force kill remaining + setTimeout(() => { + pids.forEach((pid: string) => { + const n = parseInt(pid, 10); + if (Number.isNaN(n)) { + return; + } + try { + process.kill(n, 'SIGKILL'); + } catch (e) { /* ignore */ } + }); + resolve(); + }, Math.max(0, grace)); + }); + }); + } catch (e) { + return; + } } embeddedVersion(languageServerDir: string): string { @@ -139,17 +252,32 @@ export class Verilbe_lsp { getServerOptionsEmbedded(context: ExtensionContext) { const args = ["--file_list_path", this.fileListPath, '--ruleset=none']; - let serverCommand = context.asAbsolutePath(languageServer); + let serverCommand = context.asAbsolutePath(languageServer); + // remember server path for cleanup + this.serverCommandPath = serverCommand; let serverOptions: ServerOptions = { run: { command: serverCommand, - args: args + args: args, + options: { + // Ensure process is killed when parent terminates (important for SSH scenarios) + detached: true, + shell: false + } }, debug: { command: serverCommand, - args: args + args: args, + options: { + // Ensure process is killed when parent terminates (important for SSH scenarios) + detached: true, + shell: false + } } }; + + // We rely on explicit cleanup (killServerProcesses) called on deactivate/exit + return serverOptions; } } diff --git a/src/teroshdl/teroshdl.ts b/src/teroshdl/teroshdl.ts index 796015eb..ca426e9f 100644 --- a/src/teroshdl/teroshdl.ts +++ b/src/teroshdl/teroshdl.ts @@ -124,6 +124,28 @@ export class Teroshdl { this.init_comander(); debugLogger.info('activated comander'); + + // Register cleanup settings commands: open settings and toggle the enabled flag + this.context.subscriptions.push( + vscode.commands.registerCommand('teroshdl.cleanup.openSettings', async () => { + // Open Settings UI filtered on teroshdl.cleanup + await vscode.commands.executeCommand('workbench.action.openSettings', 'teroshdl.cleanup'); + }) + ); + this.context.subscriptions.push( + vscode.commands.registerCommand('teroshdl.cleanup.toggleKillServerProcesses', async () => { + const cfg = vscode.workspace.getConfiguration('teroshdl.cleanup'); + const key = 'killServerProcesses.enabled'; + const current = cfg.get(key, false); + try { + await cfg.update(key, !current, vscode.ConfigurationTarget.Global); + const message = `TerosHDL cleanup: Kill Server Processes is now ${!current ? 'enabled' : 'disabled'}.`; + vscode.window.showInformationMessage(message); + } catch (e) { + vscode.window.showErrorMessage('Unable to update TerosHDL cleanup setting: ' + String(e)); + } + }) + ); } private async init_multi_project_manager() { From 0f18bfdb7a5a8c80ad05490c9ddae3514533f85f Mon Sep 17 00:00:00 2001 From: Gustavo Martin Date: Sun, 14 Dec 2025 20:01:18 +0100 Subject: [PATCH 2/3] added command to auto package --- auto_package/command.yml | 6 ++++++ package.json | 12 ++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/auto_package/command.yml b/auto_package/command.yml index 1b151bf8..795baffe 100644 --- a/auto_package/command.yml +++ b/auto_package/command.yml @@ -294,6 +294,12 @@ icon: book where: ["editor/title"] +- name: teroshdl.cleanup.openSettings + title: "TerosHDL: Open Cleanup Settings" + +- name: teroshdl.cleanup.toggleKillServerProcesses + title: "TerosHDL: Toggle Kill Server Processes" + # - name: quartus.add_ip # title: Add Quartus IP core # group: "TerosHDL" diff --git a/package.json b/package.json index 4c9d7367..60a74330 100644 --- a/package.json +++ b/package.json @@ -769,13 +769,13 @@ "dark": "resources/icon/dark/book.svg" }, "title": "Module documentation preview" - } - ,{ - "command": "teroshdl.cleanup.openSettings", + }, + { + "command": "teroshdl.teroshdl.cleanup.openSettings", "title": "TerosHDL: Open Cleanup Settings" - } - ,{ - "command": "teroshdl.cleanup.toggleKillServerProcesses", + }, + { + "command": "teroshdl.teroshdl.cleanup.toggleKillServerProcesses", "title": "TerosHDL: Toggle Kill Server Processes" } ], From 0211949c01e8acb96219d15ef68f3cdfb22b190b Mon Sep 17 00:00:00 2001 From: Gustavo Martin Date: Sun, 14 Dec 2025 20:31:01 +0100 Subject: [PATCH 3/3] fix in auto_package --- auto_package/templates/package.nj | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/auto_package/templates/package.nj b/auto_package/templates/package.nj index 72e222eb..b411ef01 100644 --- a/auto_package/templates/package.nj +++ b/auto_package/templates/package.nj @@ -109,7 +109,24 @@ "mac": "command+delete", "when": "editorTextFocus" } - ] + ], + "configuration": { + "type": "object", + "title": "TerosHDL", + "properties": { + "teroshdl.cleanup.killServerProcesses.enabled": { + "type": "boolean", + "default": false, + "description": "Enable automatic cleanup of language server processes on extension exit/deactivate (disabled by default)." + }, + "teroshdl.cleanup.killServerProcesses.gracePeriodMs": { + "type": "number", + "default": 500, + "minimum": 0, + "description": "Grace period in milliseconds between SIGTERM and SIGKILL when killing language server processes." + } + } + } }, {% filter indent(width=4) %} {% include 'script.nj' %},