Skip to content
Open
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
109 changes: 106 additions & 3 deletions packages/mi-extension/src/debugger/activate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,19 @@
*/

import * as vscode from 'vscode';
import { CancellationToken, DebugConfiguration, ProviderResult, Uri, window, workspace, WorkspaceFolder } from 'vscode';
import { CancellationToken, DebugConfiguration, ProviderResult, Uri, ViewColumn, window, workspace, WorkspaceFolder } from 'vscode';
import { MiDebugAdapter } from './debugAdapter';
import { COMMANDS } from '../constants';
import { extension } from '../MIExtensionContext';
import {executeBuildTask, executeRemoteDeployTask, getServerPath, stopServer} from './debugHelper';
import { getDockerTask } from './tasks';
import { getStateMachine, refreshUI } from '../stateMachine';
import { getStateMachine, openView, refreshUI } from '../stateMachine';
import { openPopupView } from '../stateMachinePopup';
import * as fs from 'fs';
import * as path from 'path';
import { SELECTED_SERVER_PATH, SELECTED_JAVA_HOME } from './constants';
import { buildBallerinaModule, isConsolidatedProject, setPathsInWorkSpace, verifyJavaHomePath, verifyMIPath } from '../util/onboardingUtils';
import { MACHINE_VIEW } from '@wso2/mi-core';
import { EVENT_TYPE, MACHINE_VIEW, POPUP_EVENT_TYPE, PomNodeDetails, PopupVisualizerLocation } from '@wso2/mi-core';
import { askForProject } from '../util/workspace';
import { webviews } from '../visualizer/webview';
import { getWSO2AIEnvVariables } from '../ai-features/configUtils';
Expand Down Expand Up @@ -71,6 +72,12 @@ class MiConfigurationProvider implements vscode.DebugConfigurationProvider {
}
}

for (const projectPath of config.projectList as string[]) {
if (!(await confirmConfigurableValues(projectPath, config.projectList.length > 1))) {
return undefined;
}
}

return config;
}
}
Expand Down Expand Up @@ -417,6 +424,102 @@ export function activateDebugger(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory('mi', factory));
}

function getConfigurablesWithValues(projectUri: string): PomNodeDetails[] {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There should be a LS method that we could use for this. Please check

const configFilePath = path.join(projectUri, 'src', 'main', 'wso2mi', 'resources', 'conf', 'config.properties');
if (!fs.existsSync(configFilePath)) {
return [];
}

const envValues: Map<string, string> = new Map();
const envPath = path.join(projectUri, '.env');
if (fs.existsSync(envPath)) {
for (const line of fs.readFileSync(envPath, 'utf-8').split('\n')) {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine.startsWith('#')) {
continue;
}
const separatorIndex = trimmedLine.indexOf('=');
if (separatorIndex === -1) {
continue;
}
const key = trimmedLine.substring(0, separatorIndex).trim();
const value = trimmedLine.substring(separatorIndex + 1).trim();
if (key && value) {
envValues.set(key, value);
}
}
}

const configurables: PomNodeDetails[] = [];
for (const line of fs.readFileSync(configFilePath, 'utf-8').split('\n')) {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine.startsWith('#')) {
continue;
}
const separatorIndex = trimmedLine.indexOf(':');
const key = (separatorIndex === -1 ? trimmedLine : trimmedLine.substring(0, separatorIndex)).trim();
const type = separatorIndex === -1 ? 'string' : (trimmedLine.substring(separatorIndex + 1).trim() || 'string');
if (key) {
configurables.push({ key, type, value: envValues.get(key) ?? '' });
}
}
return configurables;
}
Comment on lines +435 to +467

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle potential file read exceptions.

Synchronous file reads using fs.readFileSync can throw exceptions if the file is locked or if there are permission issues. An unhandled exception here will propagate and crash the debug configuration resolution. Wrap the file reading operations in a try/catch block to ensure a graceful fallback if the files are inaccessible. As per path instructions, ensure correctness and best practices.

🛡️ Proposed fix to handle file read exceptions
     const envValues: Map<string, string> = new Map();
     const envPath = path.join(projectUri, '.env');
     if (fs.existsSync(envPath)) {
+        try {
             for (const line of fs.readFileSync(envPath, 'utf-8').split('\n')) {
                 const trimmedLine = line.trim();
                 if (!trimmedLine || trimmedLine.startsWith('#')) {
                     continue;
                 }
                 const separatorIndex = trimmedLine.indexOf('=');
                 if (separatorIndex === -1) {
                     continue;
                 }
                 const key = trimmedLine.substring(0, separatorIndex).trim();
                 const value = trimmedLine.substring(separatorIndex + 1).trim();
                 if (key && value) {
                     envValues.set(key, value);
                 }
             }
+        } catch (error) {
+            console.error(`Failed to read .env file at ${envPath}:`, error);
+        }
     }
 
     const configurables: PomNodeDetails[] = [];
+    try {
         for (const line of fs.readFileSync(configFilePath, 'utf-8').split('\n')) {
             const trimmedLine = line.trim();
             if (!trimmedLine || trimmedLine.startsWith('#')) {
                 continue;
             }
             const separatorIndex = trimmedLine.indexOf(':');
             const key = (separatorIndex === -1 ? trimmedLine : trimmedLine.substring(0, separatorIndex)).trim();
             const type = separatorIndex === -1 ? 'string' : (trimmedLine.substring(separatorIndex + 1).trim() || 'string');
             if (key) {
                 configurables.push({ key, type, value: envValues.get(key) ?? '' });
             }
         }
+    } catch (error) {
+        console.error(`Failed to read config.properties at ${configFilePath}:`, error);
+    }
     return configurables;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (fs.existsSync(envPath)) {
for (const line of fs.readFileSync(envPath, 'utf-8').split('\n')) {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine.startsWith('#')) {
continue;
}
const separatorIndex = trimmedLine.indexOf('=');
if (separatorIndex === -1) {
continue;
}
const key = trimmedLine.substring(0, separatorIndex).trim();
const value = trimmedLine.substring(separatorIndex + 1).trim();
if (key && value) {
envValues.set(key, value);
}
}
}
const configurables: PomNodeDetails[] = [];
for (const line of fs.readFileSync(configFilePath, 'utf-8').split('\n')) {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine.startsWith('#')) {
continue;
}
const separatorIndex = trimmedLine.indexOf(':');
const key = (separatorIndex === -1 ? trimmedLine : trimmedLine.substring(0, separatorIndex)).trim();
const type = separatorIndex === -1 ? 'string' : (trimmedLine.substring(separatorIndex + 1).trim() || 'string');
if (key) {
configurables.push({ key, type, value: envValues.get(key) ?? '' });
}
}
return configurables;
}
if (fs.existsSync(envPath)) {
try {
for (const line of fs.readFileSync(envPath, 'utf-8').split('\n')) {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine.startsWith('#')) {
continue;
}
const separatorIndex = trimmedLine.indexOf('=');
if (separatorIndex === -1) {
continue;
}
const key = trimmedLine.substring(0, separatorIndex).trim();
const value = trimmedLine.substring(separatorIndex + 1).trim();
if (key && value) {
envValues.set(key, value);
}
}
} catch (error) {
console.error(`Failed to read .env file at ${envPath}:`, error);
}
}
const configurables: PomNodeDetails[] = [];
try {
for (const line of fs.readFileSync(configFilePath, 'utf-8').split('\n')) {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine.startsWith('#')) {
continue;
}
const separatorIndex = trimmedLine.indexOf(':');
const key = (separatorIndex === -1 ? trimmedLine : trimmedLine.substring(0, separatorIndex)).trim();
const type = separatorIndex === -1 ? 'string' : (trimmedLine.substring(separatorIndex + 1).trim() || 'string');
if (key) {
configurables.push({ key, type, value: envValues.get(key) ?? '' });
}
}
} catch (error) {
console.error(`Failed to read config.properties at ${configFilePath}:`, error);
}
return configurables;
}
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 435-435: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(envPath, 'utf-8')
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)


[warning] 453-453: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(configFilePath, 'utf-8')
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mi-extension/src/debugger/activate.ts` around lines 435 - 467, Wrap
the synchronous reads in the configurables-loading function, including the
optional env file read and required config file read, in a try/catch so
filesystem errors do not propagate or crash debug configuration resolution. On
failure, return the established graceful fallback (an empty configurables list),
while preserving the current parsing behavior for successfully read files.

Source: Path instructions


async function confirmConfigurableValues(projectUri: string, showProjectName: boolean): Promise<boolean> {
const configurables = getConfigurablesWithValues(projectUri);
const missing = configurables.filter(config => !config.value);
if (missing.length === 0) {
return true;
}

const proceed = 'Proceed';
const addValues = 'Add Values';
const projectInfo = showProjectName ? ` in project '${path.basename(projectUri)}'` : '';
const response = await vscode.window.showWarningMessage(
`The following configurable${missing.length > 1 ? 's have' : ' has'} no value in the .env file${projectInfo}: ${missing.map(config => config.key).join(', ')}. How do you want to proceed?`,
{ modal: true },
proceed,
addValues
);

if (response === proceed) {
return true;
}
if (response === addValues) {
await openManageConfigurables(projectUri, configurables);
}
return false;
}

async function openManageConfigurables(projectUri: string, configs: PomNodeDetails[]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Without opening the same configuration popup page shall we open a dialog box to get the values only? Refer to the Remote Deployment feature.

if (webviews.has(projectUri)) {
webviews.get(projectUri)?.getWebview()?.reveal(ViewColumn.Active);
} else {
openView(EVENT_TYPE.OPEN_VIEW, { view: MACHINE_VIEW.Overview, projectUri });
await new Promise<void>((resolve) => {
const stateMachine = getStateMachine(projectUri);
const currentState = stateMachine.state() as any;
if (typeof currentState === 'object' && currentState?.ready === 'viewReady') {
return resolve();
}
const listener = (state: { value: { ready?: string; }; }) => {
if (state?.value?.ready === 'viewReady') {
stateMachine.service().off(listener);
resolve();
}
};
stateMachine.service().onTransition(listener);
});
Comment on lines +500 to +513

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Prevent potential hang during debug resolution with a timeout.

The Promise waiting for the viewReady state lacks a timeout. If the state machine fails to transition (e.g., an error during view load) or if the user closes the overview before it finishes loading, the promise will hang indefinitely. Because this function is awaited in the debug configuration resolution path, an unresolved promise will block resolveDebugConfiguration and leave the debugging session in a locked, unresponsive state. As per path instructions, ensure correctness and best practices.

⏱️ Proposed fix to add a timeout
         await new Promise<void>((resolve) => {
             const stateMachine = getStateMachine(projectUri);
             const currentState = stateMachine.state() as any;
             if (typeof currentState === 'object' && currentState?.ready === 'viewReady') {
                 return resolve();
             }
+            let timeoutId: ReturnType<typeof setTimeout>;
             const listener = (state: { value: { ready?: string; }; }) => {
                 if (state?.value?.ready === 'viewReady') {
+                    clearTimeout(timeoutId);
                     stateMachine.service().off(listener);
                     resolve();
                 }
             };
+            timeoutId = setTimeout(() => {
+                stateMachine.service().off(listener);
+                console.warn('Timeout waiting for state machine to reach viewReady');
+                resolve();
+            }, 5000);
             stateMachine.service().onTransition(listener);
         });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await new Promise<void>((resolve) => {
const stateMachine = getStateMachine(projectUri);
const currentState = stateMachine.state() as any;
if (typeof currentState === 'object' && currentState?.ready === 'viewReady') {
return resolve();
}
const listener = (state: { value: { ready?: string; }; }) => {
if (state?.value?.ready === 'viewReady') {
stateMachine.service().off(listener);
resolve();
}
};
stateMachine.service().onTransition(listener);
});
await new Promise<void>((resolve) => {
const stateMachine = getStateMachine(projectUri);
const currentState = stateMachine.state() as any;
if (typeof currentState === 'object' && currentState?.ready === 'viewReady') {
return resolve();
}
let timeoutId: ReturnType<typeof setTimeout>;
const listener = (state: { value: { ready?: string; }; }) => {
if (state?.value?.ready === 'viewReady') {
clearTimeout(timeoutId);
stateMachine.service().off(listener);
resolve();
}
};
timeoutId = setTimeout(() => {
stateMachine.service().off(listener);
console.warn('Timeout waiting for state machine to reach viewReady');
resolve();
}, 5000);
stateMachine.service().onTransition(listener);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mi-extension/src/debugger/activate.ts` around lines 500 - 513,
Update the Promise waiting for viewReady in the debug resolution flow to include
a bounded timeout, ensuring it resolves when the state machine never transitions
or the overview closes. Clean up the transition listener on both the viewReady
path and timeout path, while preserving the immediate resolution when
currentState is already viewReady.

Source: Path instructions

// Allow the webview app to mount and subscribe to popup state changes
await new Promise(resolve => setTimeout(resolve, 200));
}
openPopupView(projectUri, POPUP_EVENT_TYPE.OPEN_VIEW, {
view: MACHINE_VIEW.ManageConfigurables,
customProps: { configs }
} as PopupVisualizerLocation);
}

class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory {

createDebugAdapterDescriptor(session: vscode.DebugSession): ProviderResult<vscode.DebugAdapterDescriptor> {
Expand Down
Loading