Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
4 changes: 3 additions & 1 deletion workspaces/mi/mi-extension/src/debugger/debugHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { serverLog, showServerOutputChannel } from '../util/serverLogger';
import { getJavaHomeFromConfig, getServerPathFromConfig } from '../util/onboardingUtils';
import * as crypto from 'crypto';
import { Uri, workspace } from "vscode";
import { MILanguageClient } from '../lang-client/activator';

const child_process = require('child_process');
const findProcess = require('find-process');
Expand Down Expand Up @@ -422,7 +423,8 @@ export async function stopServer(projectUri: string, serverPath: string, isWindo
export async function executeTasks(projectUri: string, serverPath: string, isDebug: boolean): Promise<void> {
const maxTimeout = 120000;
return new Promise<void>(async (resolve, reject) => {
const isTerminated = await getStateMachine(projectUri).context().langClient?.shutdownTryoutServer();
const langClient = await MILanguageClient.getInstance(projectUri);
const isTerminated = await langClient.shutdownTryoutServer();
if (!isTerminated) {
reject('Failed to terminate the tryout server. Kill the server manually and try again.');
}
Expand Down
9 changes: 5 additions & 4 deletions workspaces/mi/mi-extension/src/debugger/debugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { webviews } from '../visualizer/webview';
import { extension } from '../MIExtensionContext';
import { reject } from 'lodash';
import { LogLevel, logDebug } from '../util/logger';
import { MILanguageClient } from '../lang-client/activator';

export interface RuntimeBreakpoint {
id: number;
Expand Down Expand Up @@ -87,7 +88,7 @@ export class Debugger extends EventEmitter {
return;
}
const projectUri = workspace.uri.fsPath;
const langClient = getStateMachine(projectUri).context().langClient!;
const langClient = await MILanguageClient.getInstance(this.projectUri);
const breakpointPerFile: RuntimeBreakpoint[] = [];
// To maintain the valid and invalid breakpoints in the vscode
const vscodeBreakpointsPerFile: RuntimeBreakpoint[] = [];
Expand Down Expand Up @@ -191,7 +192,7 @@ export class Debugger extends EventEmitter {
return;
}
const projectUri = workspace.uri.fsPath;
const langClient = getStateMachine(projectUri).context().langClient!;
const langClient = await MILanguageClient.getInstance(this.projectUri);
const stepOverBreakpoints: RuntimeBreakpoint[] = [];
if (path) {
// create BreakpointPosition array
Expand Down Expand Up @@ -278,7 +279,7 @@ export class Debugger extends EventEmitter {
throw new Error(`No workspace found for path: ${filePath}`);
}
const projectUri = workspace.uri.fsPath;
const langClient = getStateMachine(projectUri).context().langClient!;
const langClient = await MILanguageClient.getInstance(this.projectUri);
// create BreakpointPosition[] array
const breakpointPositions = breakpoints.map((breakpoint) => {
return { line: breakpoint.line, column: breakpoint?.column };
Expand All @@ -296,7 +297,7 @@ export class Debugger extends EventEmitter {

public async getNextMediatorBreakpoint(): Promise<StepOverBreakpointResponse> {
try {
const langClient = getStateMachine(this.projectUri).context().langClient;
const langClient = await MILanguageClient.getInstance(this.projectUri);
if (!langClient) {
throw new Error('Language client is not initialized');
}
Expand Down
7 changes: 6 additions & 1 deletion workspaces/mi/mi-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { webviews } from './visualizer/webview';
import { v4 as uuidv4 } from 'uuid';
import path from 'path';
import { COMMANDS } from './constants';
import { enableLS } from './util/workspace';
const os = require('os');

export async function activate(context: vscode.ExtensionContext) {
Expand Down Expand Up @@ -92,12 +93,16 @@ export async function activate(context: vscode.ExtensionContext) {
activateRuntimeService(context, firstProject);
activateVisualizer(context, firstProject);
activateAiPanel(context);

workspace.workspaceFolders?.forEach(folder => {
context.subscriptions.push(...enableLS());
});
}

export async function deactivate(): Promise<void> {
const clients = await MILanguageClient.getAllInstances();
clients.forEach(async client => {
await client?.languageClient?.stop();
await client?.stop();
});

// close all webviews
Expand Down
57 changes: 47 additions & 10 deletions workspaces/mi/mi-extension/src/lang-client/activator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,30 +101,67 @@ const versionRegex = /(\d+\.\d+\.?\d*)/g;

export class MILanguageClient {
private static _instances: Map<string, MILanguageClient> = new Map();
private static lsChannelCache: Map<string, vscode.OutputChannel> = new Map();
public languageClient: ExtendedLanguageClient | undefined;
private static lsChannels: Map<string, vscode.OutputChannel> = new Map();
private static stopTimers: Map<string, NodeJS.Timeout> = new Map();
private static stoppingInstances: Set<string> = new Set();
private static readonly STOP_DEBOUNCE_MS = 3000; // 30 seconds
private languageClient: ExtendedLanguageClient | undefined;

// eslint-disable-next-line @typescript-eslint/naming-convention
private COMPATIBLE_JDK_VERSION = "11"; // Minimum JDK version required to run the language server
private _errorStack: ErrorType[] = [];

constructor(private projectUri: string) { }

public static async getInstance(projectUri: string): Promise<MILanguageClient> {
public static async getInstance(projectUri: string): Promise<ExtendedLanguageClient> {
// Cancel any pending stop operation for this project
const existingTimer = this.stopTimers.get(projectUri);
if (existingTimer) {
clearTimeout(existingTimer);
this.stopTimers.delete(projectUri);
}

// If instance is currently stopping, wait for it to complete and create a new one
if (this.stoppingInstances.has(projectUri)) {
// Wait a bit for the stop operation to complete
await new Promise(resolve => setTimeout(resolve, 100));
this.stoppingInstances.delete(projectUri);
}

if (!this._instances.has(projectUri)) {
const instance = new MILanguageClient(projectUri);
await instance.launch(projectUri);
this._instances.set(projectUri, instance);
}
return this._instances.get(projectUri)!;
const languageClient = this._instances.get(projectUri)!.languageClient;
if (!languageClient) {
const errorMessage = "Language client failed to initialize";
window.showErrorMessage(errorMessage);
throw new Error(errorMessage);
}
return languageClient;
}

public static async stopInstance(projectUri: string) {
const instance = this._instances.get(projectUri);
if (instance) {
await instance.stop();
this._instances.delete(projectUri);
// Cancel any existing timer for this project
const existingTimer = this.stopTimers.get(projectUri);
if (existingTimer) {
clearTimeout(existingTimer);
}

// Schedule the stop operation with debounce
const timer = setTimeout(async () => {
this.stoppingInstances.add(projectUri);
const instance = this._instances.get(projectUri);
if (instance) {
await instance.stop();
this._instances.delete(projectUri);
}
this.stopTimers.delete(projectUri);
this.stoppingInstances.delete(projectUri);
}, this.STOP_DEBOUNCE_MS);

this.stopTimers.set(projectUri, timer);
}
Comment on lines +116 to 165
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "activator.ts" | grep -E "mi-extension.*lang-client"

Repository: wso2/vscode-extensions

Length of output: 188


Fix race between debounced stopInstance and getInstance returning a stopping client.

A real race exists in the current implementation:

  • stopInstance's timer callback adds projectUri to stoppingInstances, awaits instance.stop(), then deletes the _instances entry.
  • If getInstance runs while await instance.stop() is in progress:
    • It detects stoppingInstances.has(projectUri), sleeps 100ms, then unconditionally deletes the flag.
    • _instances still holds the old instance (because stop() has not finished), so !this._instances.has(projectUri) is false.
    • getInstance returns this._instances.get(projectUri)!.languageClient, which is the same client currently being stopped.
  • When stop() eventually completes, the timer callback deletes _instances, leaving callers holding a reference to a stopped ExtendedLanguageClient.

Replace the 100ms delay and bare Set with tracking the actual in-flight stop Promise per project. Change stoppingInstances: Set<string> to stoppingInstances: Map<string, Promise<void>> and have getInstance await the actual Promise instead of using a time-based delay. Update stopInstance to store the instance.stop() Promise in the map so getInstance can wait for it to complete before proceeding.

🤖 Prompt for AI Agents
In workspaces/mi/mi-extension/src/lang-client/activator.ts around lines 116-165,
the current use of stoppingInstances as a Set plus a 100ms sleep allows
getInstance to return a client that is still being stopped; replace the Set with
stoppingInstances: Map<string, Promise<void>> and in stopInstance store the
actual in-flight stop Promise (e.g. const stopPromise = instance.stop();
this.stoppingInstances.set(projectUri, stopPromise); await stopPromise; then
cleanup the map entry), and in getInstance if a stop promise exists await that
exact Promise (await this.stoppingInstances.get(projectUri)) before deleting the
map entry and proceeding to check/create the instance, ensuring getInstance
never returns a client that is mid-stop.


public static async getAllInstances(): Promise<MILanguageClient[]> {
Expand All @@ -136,10 +173,10 @@ export class MILanguageClient {
}

public static getOrCreateOutputChannel(projectUri: string): vscode.OutputChannel {
let channel = this.lsChannelCache.get(projectUri);
let channel = this.lsChannels.get(projectUri);
if (!channel) {
channel = vscode.window.createOutputChannel(`Synapse Language Server - ${path.basename(projectUri)}`);
this.lsChannelCache.set(projectUri, channel);
this.lsChannels.set(projectUri, channel);
}
return channel;
}
Expand Down
15 changes: 8 additions & 7 deletions workspaces/mi/mi-extension/src/project-explorer/activate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,28 @@ import { webviews } from '../visualizer/webview';
import { MILanguageClient } from '../lang-client/activator';

let isProjectExplorerInitialized = false;
export async function activateProjectExplorer(treeviewId: string, context: ExtensionContext, lsClient: ExtendedLanguageClient, isInWI: boolean) {
export async function activateProjectExplorer(treeviewId: string, context: ExtensionContext, projectUri: string, isInWI: boolean) {
if (isProjectExplorerInitialized) {
return;
}
isProjectExplorerInitialized = true;
const lsClient: ExtendedLanguageClient = await MILanguageClient.getInstance(projectUri);

const projectExplorerDataProvider = new ProjectExplorerEntryProvider(context);
await projectExplorerDataProvider.refresh();
let registryExplorerDataProvider;
const projectTree = window.createTreeView(treeviewId, { treeDataProvider: projectExplorerDataProvider });

const projectDetailsRes = await lsClient?.getProjectDetails();
const projectDetailsRes = await lsClient.getProjectDetails();
const runtimeVersion = projectDetailsRes.primaryDetails.runtimeVersion.value;
const isRegistrySupported = compareVersions(runtimeVersion, RUNTIME_VERSION_440) < 0;

commands.registerCommand(COMMANDS.REFRESH_COMMAND, () => {
commands.registerCommand(COMMANDS.REFRESH_COMMAND, () => {
if (isInWI) {
commands.executeCommand(COMMANDS.WI_PROJECT_EXPLORER_VIEW_REFRESH);
return;
}
return projectExplorerDataProvider.refresh();
return projectExplorerDataProvider.refresh();
});

commands.registerCommand(COMMANDS.ADD_ARTIFACT_COMMAND, (entry: ProjectExplorerEntry) => {
Expand Down Expand Up @@ -512,7 +513,7 @@ export async function activateProjectExplorer(treeviewId: string, context: Exten
window.showErrorMessage('Cannot find workspace folder');
return;
}
const langClient = getStateMachine(workspace.uri.fsPath).context().langClient;
const langClient = await MILanguageClient.getInstance(workspace.uri.fsPath);
if (!langClient) {
window.showErrorMessage('Language client not found.');
return;
Expand Down Expand Up @@ -571,7 +572,7 @@ export async function activateProjectExplorer(treeviewId: string, context: Exten
if (filePath !== "") {
const fileName = path.basename(filePath);
const langClient = await MILanguageClient.getInstance(workspace.uri.fsPath);
const fileUsageIdentifiers = await langClient?.languageClient?.getResourceUsages(filePath);
const fileUsageIdentifiers = await langClient.getResourceUsages(filePath);
const fileUsageMessage = fileUsageIdentifiers?.length && fileUsageIdentifiers?.length > 0 ? "It is used in:\n" + fileUsageIdentifiers.join(", ") : "No usage found";
window.showInformationMessage("Do you want to delete : " + fileName + "\n\n" + fileUsageMessage, { modal: true }, "Yes")
.then(async answer => {
Expand Down Expand Up @@ -659,7 +660,7 @@ export async function activateProjectExplorer(treeviewId: string, context: Exten
window.showErrorMessage('Cannot find workspace folder');
throw new Error('Cannot find workspace folder');
}
const langClient = getStateMachine(workspace.uri.fsPath).context().langClient;
const langClient = await MILanguageClient.getInstance(workspace.uri.fsPath);

// Read the POM file
const workspaceFolder = vscode.workspace.getWorkspaceFolder(Uri.file(filePath));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ async function getProjectStructureData(): Promise<ProjectExplorerEntry[]> {
continue;
}
const langClient = await MILanguageClient.getInstance(rootPath);
const resp = await langClient?.languageClient?.getProjectExplorerModel(rootPath);
const projectDetailsRes = await langClient?.languageClient?.getProjectDetails();
const resp = await langClient.getProjectExplorerModel(rootPath);
const projectDetailsRes = await langClient.getProjectDetails();
const runtimeVersion = projectDetailsRes.primaryDetails.runtimeVersion.value;
const projectTree = await generateTreeData(workspace, resp, runtimeVersion);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { codeDiagnostics } from "../../ai-panel/copilot/diagnostics/diagnostics"
import { getLoginMethod } from '../../ai-panel/auth';
import { LoginMethod } from '@wso2/mi-core';
import { logInfo, logWarn, logError, logDebug } from '../../ai-panel/copilot/logger';
import { MILanguageClient } from '../../lang-client/activator';

export class MIAIPanelRpcManager implements MIAIPanelAPI {
private eventHandler: CopilotEventHandler;
Expand Down Expand Up @@ -374,13 +375,7 @@ export class MIAIPanelRpcManager implements MIAIPanelAPI {
this.eventHandler.handleCodeDiagnosticStart(xmlCodes);

// Get diagnostics using existing RPC infrastructure
const { getStateMachine } = await import('../../stateMachine');
const stateMachine = getStateMachine(this.projectUri);
if (!stateMachine) {
throw new Error('State machine not found for project');
}

const langClient = stateMachine.context().langClient;
const langClient = await MILanguageClient.getInstance(this.projectUri);
if (!langClient) {
throw new Error('Language client not available');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { DM_OPERATORS_FILE_NAME, DM_OPERATORS_IMPORT_NAME, READONLY_MAPPING_FUNC
import { readTSFile, removeMapFunctionEntry, showMappingEndNotification } from "../../util/ai-datamapper-utils";
import { compareVersions } from "../../util/onboardingUtils";
import { mapDataMapper } from "../../ai-panel/copilot/data-mapper/mapper";
import { MILanguageClient } from "../../lang-client/activator";

const undoRedoManager = new UndoRedoManager();

Expand Down Expand Up @@ -331,7 +332,7 @@ export class MiDataMapperRpcManager implements MIDataMapperAPI {
const workspaceFolder = workspace.getWorkspaceFolder(Uri.file(filePath));
let miDiagramRpcManager: MiDiagramRpcManager = new MiDiagramRpcManager(this.projectUri);

const langClient = getStateMachine(this.projectUri).context().langClient;
const langClient = await MILanguageClient.getInstance(this.projectUri);
const projectDetailsRes = await langClient?.getProjectDetails();
const runtimeVersion = projectDetailsRes.primaryDetails.runtimeVersion.value;
const isResourceContentUsed = compareVersions(runtimeVersion, RUNTIME_VERSION_440) >= 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ import {
} from "@wso2/mi-core";
import * as vscode from "vscode";
import { getStateMachine, refreshUI } from "../../stateMachine";
import { MILanguageClient } from "../../lang-client/activator";

export class MiDebuggerRpcManager implements MiDebuggerAPI {
constructor(private projectUri: string) { }

async validateBreakpoints(params: ValidateBreakpointsRequest): Promise<ValidateBreakpointsResponse> {
return new Promise(async (resolve) => {
const langClient = getStateMachine(this.projectUri).context().langClient!;
const langClient = await MILanguageClient.getInstance(this.projectUri);
const definition = await langClient.validateBreakpoints(params);

resolve(definition);
Expand All @@ -48,7 +49,7 @@ export class MiDebuggerRpcManager implements MiDebuggerAPI {

async getBreakpointInfo(params: GetBreakpointInfoRequest): Promise<GetBreakpointInfoResponse> {
return new Promise(async (resolve) => {
const langClient = getStateMachine(this.projectUri).context().langClient!;
const langClient = await MILanguageClient.getInstance(this.projectUri);
const breakpointInfo = await langClient.getBreakpointInfo(params);

resolve(breakpointInfo);
Expand Down Expand Up @@ -113,7 +114,7 @@ export class MiDebuggerRpcManager implements MiDebuggerAPI {

async getStepOverBreakpoint(params: StepOverBreakpointRequest): Promise<StepOverBreakpointResponse> {
return new Promise(async (resolve) => {
const langClient = getStateMachine(this.projectUri).context().langClient!;
const langClient = await MILanguageClient.getInstance(this.projectUri);
const breakpointInfo = await langClient.getStepOverBreakpoint(params);

resolve(breakpointInfo);
Expand Down
Loading
Loading