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/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' %},
diff --git a/package.json b/package.json
index 1bc3c141..60a74330 100644
--- a/package.json
+++ b/package.json
@@ -769,6 +769,14 @@
"dark": "resources/icon/dark/book.svg"
},
"title": "Module documentation preview"
+ },
+ {
+ "command": "teroshdl.teroshdl.cleanup.openSettings",
+ "title": "TerosHDL: Open Cleanup Settings"
+ },
+ {
+ "command": "teroshdl.teroshdl.cleanup.toggleKillServerProcesses",
+ "title": "TerosHDL: Toggle Kill Server Processes"
}
],
"languages": [
@@ -924,7 +932,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."
+ }
+ }
+ }
},
"scripts": {
diff --git a/src/colibri/config/config_web.ts b/src/colibri/config/config_web.ts
index ba3af11f..7a65caf4 100755
--- a/src/colibri/config/config_web.ts
+++ b/src/colibri/config/config_web.ts
@@ -957,7 +957,11 @@ body.vscode-high-contrast {
-
+
@@ -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() {