Skip to content

Adding changes to support use monovsdbg to debug wasm apps. #7220

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1f4d94e
Adding changes to support use monovsdbg to debug wasm apps.
thaystg Jun 10, 2024
d7f8606
Adding VSWebAssemblyPackage.
thaystg Jun 13, 2024
cfb2d67
Addressing Gregg's comments
thaystg Jun 18, 2024
d59699b
Fixing prettier
thaystg Jun 18, 2024
981c74b
Fixing package.json
thaystg Jun 18, 2024
0bc4ca2
Merge branch 'main' into dev/thays/support_wasm_monovsdbg
thaystg Jun 18, 2024
c4d88dc
Adding vswebassembly to gitignore
thaystg Jun 18, 2024
d3b8441
Test if the vswebassembly path exists and also pass DOTNET_MODIFIABLE…
thaystg Jun 19, 2024
b094acf
Fix prettier
thaystg Jun 19, 2024
4796772
Bump VSWebAssemblyBridge.
thaystg Jun 20, 2024
83de7bb
Check targetFramework before enable it.
thaystg Jun 21, 2024
e2802c1
Merge branch 'dev/thays/support_wasm_monovsdbg' of github.com:thaystg…
thaystg Jun 21, 2024
d6fd0b6
Addressing Gregg's comments.
thaystg Jul 1, 2024
04b4c77
Renaming function.
thaystg Jul 1, 2024
a5452a8
Addressing Gregg's comments.
thaystg Jul 2, 2024
2b1ee0d
Merge branch 'main' into dev/thays/support_wasm_monovsdbg
thaystg Jul 2, 2024
e06f914
Addressing Gregg's comments.
thaystg Jul 8, 2024
4e70917
Merge branch 'main' into dev/thays/support_wasm_monovsdbg
thaystg Jul 8, 2024
9dfa93c
Addressing Gregg's suggestion.
thaystg Jul 10, 2024
c12e04b
Merge branch 'main' into dev/thays/support_wasm_monovsdbg
thaystg Jul 29, 2025
b6a67c0
fix vswebassembly version
thaystg Jul 29, 2025
7e49904
Merge branch 'main' into dev/thays/support_wasm_monovsdbg
thaystg Jul 29, 2025
4163249
Fixing merge
thaystg Jul 29, 2025
4f86d19
fix compilation
thaystg Jul 29, 2025
bcb7bdd
Merge branch 'main' into dev/thays/support_wasm_monovsdbg
thaystg Jul 29, 2025
d9176bd
Last adjust to support wasm debugger with icordebug on vscode-csharp
thaystg Aug 1, 2025
8fd232c
fix eslint errors and adding .gitignore.
thaystg Aug 5, 2025
f4c236e
Bump vswebassemblybridge
thaystg Aug 8, 2025
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 omnisharptest/omnisharpUnitTests/assets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,8 @@ function createMSBuildWorkspaceInformation(
isExe = true,
isWebProject = false,
isBlazorWebAssemblyStandalone = false,
isBlazorWebAssemblyHosted = false
isBlazorWebAssemblyHosted = false,
isWebAssemblyProject = false
): ProjectDebugInformation[] {
return [
{
Expand All @@ -527,6 +528,7 @@ function createMSBuildWorkspaceInformation(
isWebProject: isWebProject,
isBlazorWebAssemblyHosted: isBlazorWebAssemblyHosted,
isBlazorWebAssemblyStandalone: isBlazorWebAssemblyStandalone,
isWebAssemblyProject: isWebAssemblyProject,
},
];
}
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,11 @@
"description": "%generateOptionsSchema.expressionEvaluationOptions.showRawValues.description%",
"default": false
},
"csharp.mono.debug.useVSDbg": {
"type": "boolean",
"description": "%generateOptionsSchema.useVSDbg.description%",
"default": false
},
"dotnet.unitTestDebuggingOptions": {
"type": "object",
"description": "%configuration.dotnet.unitTestDebuggingOptions%",
Expand Down Expand Up @@ -4818,6 +4823,11 @@
"type": "object",
"description": "Environment variables passed to dotnet. Only valid for hosted apps."
},
"useVSDbg": {
"type": "boolean",
"default": false,
"description": "%generateOptionsSchema.useVSDbg.description%"
},
"dotNetConfig": {
"description": "Options passed to the underlying .NET debugger. For more info, see https://github.com/dotnet/vscode-csharp/blob/main/debugger.md.",
"type": "object",
Expand Down
3 changes: 2 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -435,5 +435,6 @@
"comment": [
"Markdown text between `` should not be translated or localized (they represent literal text) and the capitalization, spacing, and punctuation (including the ``) should not be altered."
]
}
},
"generateOptionsSchema.useVSDbg.description": "Enable new .NET 8+ Mono Debugger (preview)"
}
6 changes: 0 additions & 6 deletions src/coreclrDebug/activate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,6 @@ export async function activate(
new BaseVsDbgConfigurationProvider(platformInformation, csharpOutputChannel)
)
);
context.subscriptions.push(
vscode.debug.registerDebugConfigurationProvider(
'monovsdbg',
new BaseVsDbgConfigurationProvider(platformInformation, csharpOutputChannel)
)
);
disposables.add(vscode.debug.registerDebugAdapterDescriptorFactory('coreclr', factory));
disposables.add(vscode.debug.registerDebugAdapterDescriptorFactory('clr', factory));
disposables.add(vscode.debug.registerDebugAdapterDescriptorFactory('monovsdbg', factory));
Expand Down
1 change: 1 addition & 0 deletions src/csharpExtensionExports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface CSharpExtensionExports {
determineBrowserType: () => Promise<string | undefined>;
experimental: CSharpExtensionExperimentalExports;
getComponentFolder: (componentName: string) => string;
tryToUseVSDbgForMono: (urlStr: string) => Promise<[string, number, number]>;
}

export interface CSharpExtensionExperimentalExports {
Expand Down
5 changes: 5 additions & 0 deletions src/lsptoolshost/debugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ export function registerDebugger(
context.subscriptions.push(
vscode.debug.registerDebugConfigurationProvider('coreclr', dotnetWorkspaceConfigurationProvider)
);

context.subscriptions.push(
vscode.debug.registerDebugConfigurationProvider('monovsdbg', dotnetWorkspaceConfigurationProvider)
);

context.subscriptions.push(
vscode.commands.registerCommand('dotnet.generateAssets', async (selectedIndex) =>
generateAssets(workspaceInformationProvider, selectedIndex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import {
IWorkspaceDebugInformationProvider,
ProjectDebugInformation,
} from '../shared/IWorkspaceDebugInformationProvider';
import { isBlazorWebAssemblyHosted, isBlazorWebAssemblyProject, isWebProject } from '../shared/utils';
import {
isBlazorWebAssemblyHosted,
isBlazorWebAssemblyProject,
isWebProject,
isWebAssemblyProject,
} from '../shared/utils';
import { RoslynLanguageServer } from './roslynLanguageServer';
import {
ProjectDebugConfiguration,
Expand Down Expand Up @@ -50,6 +55,7 @@ export class RoslynWorkspaceDebugInformationProvider implements IWorkspaceDebugI
// LSP serializes and deserializes URIs as (URI formatted) strings not actual types. So convert to the actual type here.
const projects: ProjectDebugInformation[] | undefined = await mapAsync(response, async (p) => {
const webProject = isWebProject(p.projectPath);
const webAssemblyProject = isWebAssemblyProject(p.projectPath);
const webAssemblyBlazor = await isBlazorWebAssemblyProject(p.projectPath);
return {
projectPath: p.projectPath,
Expand All @@ -58,6 +64,7 @@ export class RoslynWorkspaceDebugInformationProvider implements IWorkspaceDebugI
targetsDotnetCore: p.targetsDotnetCore,
isExe: p.isExe,
isWebProject: webProject,
isWebAssemblyProject: webAssemblyProject,
isBlazorWebAssemblyHosted: isBlazorWebAssemblyHosted(
p.isExe,
webProject,
Expand Down
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ export async function activate(
getComponentFolder: (componentName) => {
return getComponentFolder(componentName, languageServerOptions);
},
tryToUseVSDbgForMono: BlazorDebugConfigurationProvider.tryToUseVSDbgForMono,
};
} else {
return {
Expand Down
1 change: 1 addition & 0 deletions src/omnisharp/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ export interface MSBuildProject {
IsWebProject: boolean;
IsBlazorWebAssemblyStandalone: boolean;
IsBlazorWebAssemblyHosted: boolean;
IsWebAssemblyProject: boolean;
}

export interface TargetFramework {
Expand Down
3 changes: 2 additions & 1 deletion src/omnisharp/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as vscode from 'vscode';
import { CancellationToken } from 'vscode-languageserver-protocol';
import {
isWebProject,
isWebAssemblyProject,
isBlazorWebAssemblyProject,
isBlazorWebAssemblyHosted,
findNetCoreTargetFramework,
Expand Down Expand Up @@ -171,7 +172,7 @@ export async function requestWorkspaceInformation(server: OmniSharpServer) {
if (response.MsBuild && response.MsBuild.Projects) {
for (const project of response.MsBuild.Projects) {
project.IsWebProject = isWebProject(project.Path);

project.IsWebAssemblyProject = isWebAssemblyProject(project.Path);
const isProjectBlazorWebAssemblyProject = await isBlazorWebAssemblyProject(project.Path);

const targetsDotnetCore =
Expand Down
1 change: 1 addition & 0 deletions src/omnisharpWorkspaceDebugInformationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export class OmnisharpWorkspaceDebugInformationProvider implements IWorkspaceDeb
isWebProject: p.IsWebProject,
isBlazorWebAssemblyHosted: p.IsBlazorWebAssemblyHosted,
isBlazorWebAssemblyStandalone: p.IsBlazorWebAssemblyStandalone,
isWebAssemblyProject: p.IsWebAssemblyProject,
solutionPath: null,
};
});
Expand Down
167 changes: 164 additions & 3 deletions src/razor/src/blazorDebug/blazorDebugConfigurationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,22 @@ import { fileURLToPath } from 'url';
import * as vscode from 'vscode';
import { ChromeBrowserFinder, EdgeBrowserFinder } from '@vscode/js-debug-browsers';
import { RazorLogger } from '../razorLogger';
import { JS_DEBUG_NAME, SERVER_APP_NAME } from './constants';
import { ONLY_JS_DEBUG_NAME, MANAGED_DEBUG_NAME, JS_DEBUG_NAME, SERVER_APP_NAME } from './constants';
import { onDidTerminateDebugSession } from './terminateDebugHandler';
import showInformationMessage from '../../../shared/observers/utils/showInformationMessage';
import showErrorMessage from '../../../observers/utils/showErrorMessage';
import path = require('path');
import * as cp from 'child_process';
import { getExtensionPath } from '../../../common';

export class BlazorDebugConfigurationProvider implements vscode.DebugConfigurationProvider {
private static readonly autoDetectUserNotice: string = vscode.l10n.t(
'Run and Debug: auto-detection found {0} for a launch browser'
);
private static readonly edgeBrowserType: string = 'msedge';
private static readonly chromeBrowserType: string = 'chrome';
private static readonly pidsByUrl = new Map<string, number | undefined>();
private static readonly vsWebAssemblyBridgeOutputChannel = vscode.window.createOutputChannel('VsWebAssemblyBridge');

constructor(private readonly logger: RazorLogger, private readonly vscodeType: typeof vscode) {}

Expand Down Expand Up @@ -97,6 +102,7 @@ export class BlazorDebugConfigurationProvider implements vscode.DebugConfigurati
launchBrowser: {
enabled: false,
},
cascadeTerminateToConfigurations: [ONLY_JS_DEBUG_NAME, MANAGED_DEBUG_NAME, JS_DEBUG_NAME],
...configuration.dotNetConfig,
};

Expand Down Expand Up @@ -125,6 +131,13 @@ export class BlazorDebugConfigurationProvider implements vscode.DebugConfigurati
inspectUri: string,
url: string
) {
const wasmConfig = vscode.workspace.getConfiguration('csharp');
const useVSDbg = configuration.useVSDbg || wasmConfig.get<boolean>('mono.debug.useVSDbg') == true;
let portBrowserDebug = -1;
if (useVSDbg) {
[inspectUri, portBrowserDebug] = await this.attachToAppOnBrowser(folder, configuration);
}

const configBrowser = configuration.browser;
const browserType =
configBrowser === 'edge'
Expand All @@ -137,7 +150,7 @@ export class BlazorDebugConfigurationProvider implements vscode.DebugConfigurati
}

const browser = {
name: JS_DEBUG_NAME,
name: useVSDbg ? ONLY_JS_DEBUG_NAME : JS_DEBUG_NAME,
type: browserType,
request: 'launch',
timeout: configuration.timeout || 30000,
Expand All @@ -149,9 +162,13 @@ export class BlazorDebugConfigurationProvider implements vscode.DebugConfigurati
...configuration.browserConfig,
// When the browser debugging session is stopped, propogate
// this and terminate the debugging session of the Blazor dev server.
cascadeTerminateToConfigurations: [SERVER_APP_NAME],
cascadeTerminateToConfigurations: [SERVER_APP_NAME, MANAGED_DEBUG_NAME],
};

if (useVSDbg) {
browser.port = portBrowserDebug;
}

try {
/**
* The browser debugger will immediately launch after the
Expand Down Expand Up @@ -179,6 +196,150 @@ export class BlazorDebugConfigurationProvider implements vscode.DebugConfigurati
}
}

private async attachToAppOnBrowser(
folder: vscode.WorkspaceFolder | undefined,
configuration: vscode.DebugConfiguration
): Promise<[string, number]> {
const [inspectUriRet, portICorDebug, portBrowserDebug] =
await BlazorDebugConfigurationProvider.launchVsWebAssemblyBridge(configuration.url);

const cwd = configuration.cwd || '${workspaceFolder}';
const args = configuration.hosted ? [] : ['run'];
const app = {
name: MANAGED_DEBUG_NAME,
type: 'monovsdbg',
request: 'launch',
//program: 'C:\\diag\\icordebug_wasm_vscode\\test\\obj\\Debug\\net9.0\\test.dll',
args,
cwd,
cascadeTerminateToConfigurations: [ONLY_JS_DEBUG_NAME, SERVER_APP_NAME, JS_DEBUG_NAME],
...configuration.dotNetConfig,
};

app.monoDebuggerOptions = {
ip: '127.0.0.1',
port: portICorDebug,
platform: 'browser',
isServer: true,
//assetsPath: 'C:\\diag\\icordebug_wasm_vscode\\test\\bin\\Debug\\net9.0\\',
};

try {
await this.vscodeType.debug.startDebugging(folder, app);
const terminate = this.vscodeType.debug.onDidTerminateDebugSession(async (event) => {
if (process.platform !== 'win32') {
const blazorDevServer = 'blazor-devserver\\.dll';
const dir = folder && folder.uri && folder.uri.fsPath;
const regexEscapedDir = dir!.toLowerCase()!.replace(/\//g, '\\/');
const launchedApp = configuration.hosted
? app.program
: `${regexEscapedDir}.*${blazorDevServer}|${blazorDevServer}.*${regexEscapedDir}`;
await onDidTerminateDebugSession(event, this.logger, launchedApp);
terminate.dispose();
}
this.vscodeType.debug.stopDebugging();
});
} catch (error) {
this.logger.logError('[DEBUGGER] Error when launching application: ', error as Error);
}
return [inspectUriRet, portBrowserDebug];
}

private static async launchVsWebAssemblyBridge(urlStr: string): Promise<[string, number, number]> {
const dotnetPath = process.platform === 'win32' ? 'dotnet.exe' : 'dotnet';
const vsWebAssemblyBridge = path.join(
getExtensionPath(),
'.vswebassemblybridge',
'Microsoft.Diagnostics.BrowserDebugHost.dll'
);
const devToolsUrl = `http://localhost:0`; // Browser debugging port address
const spawnedProxyArgs = [
`${vsWebAssemblyBridge}`,
'--DevToolsUrl',
`${devToolsUrl}`,
'--UseVsDbg',
'true',
'--iCorDebugPort',
'-1',
'--OwnerPid',
`${process.pid}`,
];
const spawnedProxy = cp.spawn(dotnetPath, spawnedProxyArgs);

try {
let chunksProcessed = 0;
let proxyICorDebugPort = -1;
let proxyBrowserPort = -1;
for await (const output of spawnedProxy.stdout) {
// If we haven't found the URL in the first ten chunks processed
// then bail out.
if (chunksProcessed++ > 10) {
return ['', -1, -1];
}
BlazorDebugConfigurationProvider.vsWebAssemblyBridgeOutputChannel.appendLine(output);
// The debug proxy server outputs the port it is listening on in the
// standard output of the launched application. We need to pass this URL
// back to the debugger so we extract the URL from stdout using a regex.
// The debug proxy will not exit until killed via the `killDebugProxy`
// method so parsing stdout is necessary to extract the URL.
const matchExprProxyUrl = 'Now listening on: (?<url>.*)';
const matchExprICorDebugPort = 'Listening iCorDebug on: (?<port>.*)';
const matchExprBrowserPort = 'Waiting for browser on: (?<port>.*)';
const foundProxyUrl = `${output}`.match(matchExprProxyUrl);
const foundICorDebugPort = `${output}`.match(matchExprICorDebugPort);
const foundBrowserPort = `${output}`.match(matchExprBrowserPort);
const proxyUrlString = foundProxyUrl?.groups?.url;
if (foundICorDebugPort?.groups?.port != undefined) {
proxyICorDebugPort = Number(foundICorDebugPort?.groups?.port);
}
if (foundBrowserPort?.groups?.port != undefined) {
proxyBrowserPort = Number(foundBrowserPort?.groups?.port);
}
if (proxyUrlString) {
BlazorDebugConfigurationProvider.vsWebAssemblyBridgeOutputChannel.appendLine(
`Debugging proxy is running at: ${proxyUrlString}`
);
const oldPid = BlazorDebugConfigurationProvider.pidsByUrl.get(urlStr);
if (oldPid != undefined) {
process.kill(oldPid);
}
BlazorDebugConfigurationProvider.pidsByUrl.set(urlStr, spawnedProxy.pid);
const url = new URL(proxyUrlString);
return [
`${url.protocol.replace(`http`, `ws`)}//${url.host}{browserInspectUriPath}`,
proxyICorDebugPort,
proxyBrowserPort,
];
}
}

for await (const error of spawnedProxy.stderr) {
BlazorDebugConfigurationProvider.vsWebAssemblyBridgeOutputChannel.appendLine(`ERROR: ${error}`);
}
} catch (error: any) {
if (spawnedProxy.pid) {
BlazorDebugConfigurationProvider.vsWebAssemblyBridgeOutputChannel.appendLine(
`Error occured while spawning debug proxy. Terminating debug proxy server.`
);
process.kill(spawnedProxy.pid);
}
throw error;
}
return ['', -1, -1];
}

public static async tryToUseVSDbgForMono(urlStr: string): Promise<[string, number, number]> {
const wasmConfig = vscode.workspace.getConfiguration('csharp');
const useVSDbg = wasmConfig.get<boolean>('mono.debug.useVSDbg') == true;
if (useVSDbg) {
const [inspectUri, portICorDebug, portBrowserDebug] =
await BlazorDebugConfigurationProvider.launchVsWebAssemblyBridge(urlStr);

return [inspectUri, portICorDebug, portBrowserDebug];
}
return ['', -1, -1];
}

public static async determineBrowserType(): Promise<string | undefined> {
// There was no browser specified by the user, so we will do some auto-detection to find a browser,
// favoring Edge if multiple valid options are installed.
Expand Down
2 changes: 2 additions & 0 deletions src/razor/src/blazorDebug/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@

export const SERVER_APP_NAME = '.NET Application Server';
export const JS_DEBUG_NAME = 'Debug Blazor Web Assembly in Browser';
export const ONLY_JS_DEBUG_NAME = 'JavaScript Debugger';
export const MANAGED_DEBUG_NAME = 'Wasm Managed Debugger';
5 changes: 5 additions & 0 deletions src/shared/IWorkspaceDebugInformationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,9 @@ export interface ProjectDebugInformation {
* If this is a standalone blazor web assembly project.
*/
isBlazorWebAssemblyStandalone: boolean;

/**
* If this is a web assembly project.
*/
isWebAssemblyProject: boolean;
}
Loading