Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions auto_package/command.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
19 changes: 18 additions & 1 deletion auto_package/templates/package.nj
Original file line number Diff line number Diff line change
Expand Up @@ -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' %},
Expand Down
27 changes: 26 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -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": {
Expand Down
19 changes: 18 additions & 1 deletion src/colibri/config/config_web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -957,7 +957,11 @@ body.vscode-high-contrast {
</div>
</div>


<div class="setting-item">
<div class="setting-item-label">
<span id="cleanupIndicator" onclick="open_cleanup_settings()" style="font-weight:600;margin-left:8px;cursor:pointer;" title="Click to open cleanup settings">unknown</span>
</div>
</div>

</div>
<div class="settings-section" id="documentation-general">
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}
});
Expand Down
16 changes: 16 additions & 0 deletions src/teroshdl/features/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -281,13 +287,23 @@ export class Config_manager {
* @returns A promise that resolves when the update is complete.
*/
private async updateWebConfig(tabToOpen: string): Promise<void> {
const cleanupCfg = vscode.workspace.getConfiguration('teroshdl.cleanup');
const cleanupEnabled = cleanupCfg.get<boolean>('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,
diff_config: this.getMark(),
title: this.getTitle(),
projectName: this.currentProjectName,
tool: tabToOpen,
cleanupEnabled: cleanupEnabled,
remoteName: remoteName,
cleanupEffective: cleanupEffective
});
}

Expand Down
160 changes: 152 additions & 8 deletions src/teroshdl/features/language_provider/lsp/rust_hdl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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;
Expand All @@ -43,21 +45,40 @@ 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();
}
}
}
})
);

// 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<boolean> {
Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand All @@ -148,14 +207,19 @@ 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,
args: args,
options: {
env: {
VHDL_LS_CONFIG: this.fileListPath
}
},
// Ensure process is killed when parent terminates (important for SSH scenarios)
detached: true,
shell: false
}
},
debug: {
Expand All @@ -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<void> {
if (!this.serverCommandPath) {
return;
}
const serverPath = this.serverCommandPath;

try {
const cfg = vscode.workspace.getConfiguration('teroshdl.cleanup');
const enabled = cfg.get<boolean>('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<number>('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;
}
}
}
Loading
Loading