diff --git a/.vscodeignore b/.vscodeignore index e8448b607..f9a0d5a60 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -57,3 +57,4 @@ webpack.config.js +RuntimeLicenses/dependencies/* coreclr-debug/install.log +!.vswebassemblybridge/** \ No newline at end of file diff --git a/omnisharptest/omnisharpUnitTests/assets.test.ts b/omnisharptest/omnisharpUnitTests/assets.test.ts index 781dac691..3afd7595c 100644 --- a/omnisharptest/omnisharpUnitTests/assets.test.ts +++ b/omnisharptest/omnisharpUnitTests/assets.test.ts @@ -503,7 +503,8 @@ function createMSBuildWorkspaceInformation( isExe = true, isWebProject = false, isBlazorWebAssemblyStandalone = false, - isBlazorWebAssemblyHosted = false + isBlazorWebAssemblyHosted = false, + isWebAssemblyProject = false ): ProjectDebugInformation[] { return [ { @@ -525,6 +526,7 @@ function createMSBuildWorkspaceInformation( isWebProject: isWebProject, isBlazorWebAssemblyHosted: isBlazorWebAssemblyHosted, isBlazorWebAssemblyStandalone: isBlazorWebAssemblyStandalone, + isWebAssemblyProject: isWebAssemblyProject, }, ]; } diff --git a/package.json b/package.json index 24dc4f50f..a32bb0f13 100644 --- a/package.json +++ b/package.json @@ -686,6 +686,23 @@ "binaries": [ "./rzls" ] + }, + { + "id": "VSWebAssemblyBridge", + "description": "VSWebAssemblyBridge (Platform Agnostic)", + "url": "https://vsdebugger.blob.core.windows.net/vswebassemblybridge-8-0-532005/vswebassemblybridge.zip", + "installPath": ".vswebassemblybridge", + "platforms": [ + "win32", + "linux", + "darwin" + ], + "architectures": [ + "x86", + "x86_64", + "arm64" + ], + "integrity": "4C4B641AF5F8EFC975EB26D71A6DB052DDF2E3AF3AA263FC660C65F0B7044DDD" } ], "engines": { @@ -1155,6 +1172,11 @@ "description": "%generateOptionsSchema.expressionEvaluationOptions.showRawValues.description%", "default": false }, + "csharp.wasm.debug.useVSDbg": { + "type": "boolean", + "description": "%generateOptionsSchema.useVSDbg.description%", + "default": false + }, "dotnet.unitTestDebuggingOptions": { "type": "object", "description": "%configuration.dotnet.unitTestDebuggingOptions%", @@ -5570,4 +5592,4 @@ } ] } -} +} \ No newline at end of file diff --git a/package.nls.json b/package.nls.json index e5885cba1..6ff61a410 100644 --- a/package.nls.json +++ b/package.nls.json @@ -489,5 +489,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 9+ Wasm Debugger (preview)" +} \ No newline at end of file diff --git a/src/coreclrDebug/activate.ts b/src/coreclrDebug/activate.ts index 11380f51c..d9a03f25c 100644 --- a/src/coreclrDebug/activate.ts +++ b/src/coreclrDebug/activate.ts @@ -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)); diff --git a/src/coreclrDebug/provisionalDebugSessionTracker.ts b/src/coreclrDebug/provisionalDebugSessionTracker.ts index 28bf067bd..f86452c74 100644 --- a/src/coreclrDebug/provisionalDebugSessionTracker.ts +++ b/src/coreclrDebug/provisionalDebugSessionTracker.ts @@ -40,7 +40,7 @@ export class ProvisionalDebugSessionTracker { * @param session Debug session. */ onDidStartDebugSession(session: vscode.DebugSession): void { - if (session.type !== 'coreclr') { + if (session.type !== 'coreclr' && session.type !== 'monovsdbg') { return; } @@ -88,6 +88,18 @@ export class ProvisionalDebugSessionTracker { this._onDidStartDebugSession?.dispose(); this._onDidTerminateDebugSession?.dispose(); } + getDebugSessionByType(type: string): vscode.DebugSession | undefined { + const sessions = this._sessions; + if (sessions != undefined) { + const sessionsIt = sessions.entries(); + for (const session of sessionsIt) { + if (session[0].type == type) { + return session[0]; + } + } + } + return undefined; + } } export const debugSessionTracker = new ProvisionalDebugSessionTracker(); diff --git a/src/csharpExtensionExports.ts b/src/csharpExtensionExports.ts index a6a717a2f..387de9d92 100644 --- a/src/csharpExtensionExports.ts +++ b/src/csharpExtensionExports.ts @@ -26,6 +26,7 @@ export interface CSharpExtensionExports { determineBrowserType: () => Promise; experimental: CSharpExtensionExperimentalExports; getComponentFolder: (componentName: string) => string; + tryToUseVSDbgForMono: (urlStr: string, projectPath: string) => Promise<[string, number, number]>; } export interface CSharpExtensionExperimentalExports { diff --git a/src/lsptoolshost/debugger.ts b/src/lsptoolshost/debugger.ts index afae73b7f..d7a2f867a 100644 --- a/src/lsptoolshost/debugger.ts +++ b/src/lsptoolshost/debugger.ts @@ -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) diff --git a/src/lsptoolshost/roslynWorkspaceDebugConfigurationProvider.ts b/src/lsptoolshost/roslynWorkspaceDebugConfigurationProvider.ts index 9e820c656..e49d42141 100644 --- a/src/lsptoolshost/roslynWorkspaceDebugConfigurationProvider.ts +++ b/src/lsptoolshost/roslynWorkspaceDebugConfigurationProvider.ts @@ -49,7 +49,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 [webProject, webAssemblyProject] = isWebProject(p.projectPath); const webAssemblyBlazor = await isBlazorWebAssemblyProject(p.projectPath); return { projectPath: p.projectPath, @@ -58,6 +58,7 @@ export class RoslynWorkspaceDebugInformationProvider implements IWorkspaceDebugI targetsDotnetCore: p.targetsDotnetCore, isExe: p.isExe, isWebProject: webProject, + isWebAssemblyProject: webAssemblyProject, isBlazorWebAssemblyHosted: isBlazorWebAssemblyHosted( p.isExe, webProject, diff --git a/src/lsptoolshost/services/descriptors.ts b/src/lsptoolshost/services/descriptors.ts index 2f1a62d79..60b86d811 100644 --- a/src/lsptoolshost/services/descriptors.ts +++ b/src/lsptoolshost/services/descriptors.ts @@ -41,4 +41,16 @@ export default class Descriptors { protocolMajorVersion: 3, } ); + + /** + * The descriptor for token acquisition service that is hosted within the VS Code Extension Host process. + * Use {@link IQueryExecutionService} for the RPC interface. + */ + static readonly projectQueryExecutionService: ServiceRpcDescriptor = Object.freeze( + new ServiceJsonRpcDescriptor( + ServiceMoniker.create('Microsoft.VisualStudio.ProjectSystem.Query.Remoting.QueryExecutionService', '0.2'), + Formatters.Utf8, + MessageDelimiters.HttpLikeHeaders + ) + ); } diff --git a/src/main.ts b/src/main.ts index 699a15462..b1b6a2f97 100644 --- a/src/main.ts +++ b/src/main.ts @@ -100,6 +100,8 @@ export async function activate( requiredPackageIds.push('OmniSharp'); } + requiredPackageIds.push('VSWebAssemblyBridge'); + // If the dotnet bundle is installed, this will ensure the dotnet CLI is on the path. await initializeDotnetPath(); @@ -360,6 +362,7 @@ export async function activate( getComponentFolder: (componentName) => { return getComponentFolder(componentName, languageServerOptions); }, + tryToUseVSDbgForMono: BlazorDebugConfigurationProvider.tryToUseVSDbgForMono, }; } else { return { diff --git a/src/omnisharp/protocol.ts b/src/omnisharp/protocol.ts index ec444e363..b4ef34965 100644 --- a/src/omnisharp/protocol.ts +++ b/src/omnisharp/protocol.ts @@ -343,6 +343,7 @@ export interface MSBuildProject { IsWebProject: boolean; IsBlazorWebAssemblyStandalone: boolean; IsBlazorWebAssemblyHosted: boolean; + IsWebAssemblyProject: boolean; } export interface TargetFramework { diff --git a/src/omnisharp/utils.ts b/src/omnisharp/utils.ts index 1d28bf689..4b4468078 100644 --- a/src/omnisharp/utils.ts +++ b/src/omnisharp/utils.ts @@ -170,8 +170,7 @@ export async function requestWorkspaceInformation(server: OmniSharpServer) { const response = await server.makeRequest(protocol.Requests.Projects); if (response.MsBuild && response.MsBuild.Projects) { for (const project of response.MsBuild.Projects) { - project.IsWebProject = isWebProject(project.Path); - + [project.IsWebProject, project.IsWebAssemblyProject] = isWebProject(project.Path); const isProjectBlazorWebAssemblyProject = await isBlazorWebAssemblyProject(project.Path); const targetsDotnetCore = diff --git a/src/omnisharpWorkspaceDebugInformationProvider.ts b/src/omnisharpWorkspaceDebugInformationProvider.ts index 76c035b0b..9f25951dc 100644 --- a/src/omnisharpWorkspaceDebugInformationProvider.ts +++ b/src/omnisharpWorkspaceDebugInformationProvider.ts @@ -36,6 +36,7 @@ export class OmnisharpWorkspaceDebugInformationProvider implements IWorkspaceDeb isWebProject: p.IsWebProject, isBlazorWebAssemblyHosted: p.IsBlazorWebAssemblyHosted, isBlazorWebAssemblyStandalone: p.IsBlazorWebAssemblyStandalone, + isWebAssemblyProject: p.IsWebAssemblyProject, solutionPath: null, }; }); diff --git a/src/razor/src/blazorDebug/blazorDebugConfigurationProvider.ts b/src/razor/src/blazorDebug/blazorDebugConfigurationProvider.ts index 9530e493b..fc0cd11be 100644 --- a/src/razor/src/blazorDebug/blazorDebugConfigurationProvider.ts +++ b/src/razor/src/blazorDebug/blazorDebugConfigurationProvider.ts @@ -4,16 +4,71 @@ *--------------------------------------------------------------------------------------------*/ import execa = require('execa'); -import { promises, readFileSync } from 'fs'; +import { promises, readFileSync, existsSync } from 'fs'; import { join } from 'path'; 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'; +import { debugSessionTracker } from '../../../coreclrDebug/provisionalDebugSessionTracker'; +import { getCSharpDevKit } from '../../../utils/getCSharpDevKit'; +import Descriptors from '../../../lsptoolshost/services/descriptors'; +import { CancellationToken } from 'vscode'; +import { IDisposable, IObserver } from '@microsoft/servicehub-framework'; +import { EventEmitter } from 'events'; + +export interface Project { + Id: { + ProjectPath: string; + ProjectGuid?: string; + }; + ActiveConfigurations?: [ProjectConfiguration]; +} + +export interface QueryResult { + Versions?: any; + + Items?: [T]; +} + +export interface ProjectConfiguration { + BuildProperties?: any; +} + +/** + * This service provides implementation to execute a project query. + */ +export interface IQueryExecutionService { + /** + * execute a query. + * @param query a query string. + */ + ExecuteQueryAsync(query: string, cancellationToken?: CancellationToken): Promise; + + /** + * execute an update query. + * @param query a query string. + */ + ExecuteRemoteExecutableAsync(query: string, cancellationToken?: CancellationToken): Promise; + + /** + * Subscribe query results/ + * @param query a query string. + * @param resultsReceiver an observer to receive results. + */ + SubscribeQueryResultsAsync( + query: string, + resultsReceiver: IObserver, + cancellationToken?: CancellationToken + ): Promise; +} export class BlazorDebugConfigurationProvider implements vscode.DebugConfigurationProvider { private static readonly autoDetectUserNotice: string = vscode.l10n.t( @@ -21,6 +76,8 @@ export class BlazorDebugConfigurationProvider implements vscode.DebugConfigurati ); private static readonly edgeBrowserType: string = 'msedge'; private static readonly chromeBrowserType: string = 'chrome'; + private static readonly pidsByUrl = new Map(); + private static readonly vsWebAssemblyBridgeOutputChannel = vscode.window.createOutputChannel('VsWebAssemblyBridge'); constructor(private readonly logger: RazorLogger, private readonly vscodeType: typeof vscode) {} @@ -92,11 +149,13 @@ export class BlazorDebugConfigurationProvider implements vscode.DebugConfigurati cwd, env: { ASPNETCORE_ENVIRONMENT: 'Development', + DOTNET_MODIFIABLE_ASSEMBLIES: 'debug', ...configuration.env, }, launchBrowser: { enabled: false, }, + cascadeTerminateToConfigurations: [ONLY_JS_DEBUG_NAME, MANAGED_DEBUG_NAME, JS_DEBUG_NAME], ...configuration.dotNetConfig, }; @@ -125,6 +184,18 @@ export class BlazorDebugConfigurationProvider implements vscode.DebugConfigurati inspectUri: string, url: string ) { + const originalInspectUri = inspectUri; + let folderPath = configuration.cwd; + if (folder && folderPath) { + folderPath = folderPath.replace('${workspaceFolder}', fileURLToPath(folder.uri.toString())); + folderPath = folderPath.replaceAll(/[\\/]/g, path.sep); + } + const useVSDbg = await BlazorDebugConfigurationProvider.useVSDbg(folderPath ? folderPath : ''); + let portBrowserDebug = -1; + if (useVSDbg) { + [inspectUri, portBrowserDebug] = await this.attachToAppOnBrowser(folder, configuration, url); + } + const configBrowser = configuration.browser; const browserType = configBrowser === 'edge' @@ -137,7 +208,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, @@ -149,9 +220,27 @@ 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; + } + + const monovsdbgSession = debugSessionTracker.getDebugSessionByType('monovsdbg'); + if (useVSDbg) { + //means that we don't have c#devkit installed so we get information from configSettings + if (monovsdbgSession === undefined && getCSharpDevKit() === undefined) { + //something wrong happened kill the vswebassembly and use the older debugger + browser.inspectUri = originalInspectUri; + browser.port = undefined; + const oldPid = BlazorDebugConfigurationProvider.pidsByUrl.get(browser.url); + if (oldPid != undefined) { + process.kill(oldPid); + } + } + } + try { /** * The browser debugger will immediately launch after the @@ -179,6 +268,228 @@ export class BlazorDebugConfigurationProvider implements vscode.DebugConfigurati } } + private async attachToAppOnBrowser( + folder: vscode.WorkspaceFolder | undefined, + configuration: vscode.DebugConfiguration, + url: string + ): Promise<[string, number]> { + const [inspectUriRet, portICorDebug, portBrowserDebug] = + await BlazorDebugConfigurationProvider.launchVsWebAssemblyBridge(configuration.url || url); + + const cwd = configuration.cwd || '${workspaceFolder}'; + const args = configuration.hosted ? [] : ['run']; + const app = { + name: MANAGED_DEBUG_NAME, + type: 'monovsdbg', + request: 'launch', + 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, + }; + + 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 devToolsUrl = `http://localhost:0`; // Browser debugging port address + const spawnedProxyArgs = [ + `${BlazorDebugConfigurationProvider.getWebAssemblyWebBridgePath()}`, + '--DevToolsUrl', + `${devToolsUrl}`, + '--UseVsDbg', + 'true', + '--iCorDebugPort', + '-1', + '--OwnerPid', + `${process.pid}`, + ]; + const cpOptions: cp.SpawnOptionsWithoutStdio = { + detached: true, + windowsHide: true, + }; + let chunksProcessed = 0; + let proxyICorDebugPort = -1; + let proxyBrowserPort = -1; + let newUri = ''; + const spawnedProxy = cp.spawn(dotnetPath, spawnedProxyArgs, cpOptions); + const eventEmmiter = new EventEmitter(); + function handleData(stream: NodeJS.ReadableStream) { + stream.on('data', (chunk) => { + BlazorDebugConfigurationProvider.vsWebAssemblyBridgeOutputChannel.appendLine(chunk.toString()); + if (newUri != '') { + return; + } + if (chunksProcessed++ > 10) { + eventEmmiter.emit('vsWebAssemblyReady'); + } + const matchExprProxyUrl = 'Now listening on: (?.*)'; + const matchExprICorDebugPort = 'Listening iCorDebug on: (?.*)'; + const matchExprBrowserPort = 'Waiting for browser on: (?.*)'; + const foundProxyUrl = `${chunk}`.match(matchExprProxyUrl); + const foundICorDebugPort = `${chunk}`.match(matchExprICorDebugPort); + const foundBrowserPort = `${chunk}`.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); + newUri = `${url.protocol.replace(`http`, `ws`)}//${url.host}{browserInspectUriPath}`; + eventEmmiter.emit('vsWebAssemblyReady'); + } + }); + + stream.on('err', (err) => { + BlazorDebugConfigurationProvider.vsWebAssemblyBridgeOutputChannel.appendLine(err.toString()); + eventEmmiter.emit('vsWebAssemblyReady'); + }); + } + + handleData(spawnedProxy.stdout); + handleData(spawnedProxy.stderr); + await new Promise((resolve) => eventEmmiter.once('vsWebAssemblyReady', resolve)); + return [newUri, proxyICorDebugPort, proxyBrowserPort]; + } + + private static async isNet9OrNewer(projectPath: string): Promise { + let isNet9OrNewer = false; + const configurationsObject = { + $properties: ['BuildProperties'], + BuildProperties: { + $properties: ['Id', 'Name', 'Value', 'StorageType'], + $where: { + '&&': [ + { '==': [{ '.': 'Name' }, 'TargetFramework'] }, + { '==': [{ '.': 'StorageType' }, 'ProjectFile'] }, + ], + }, + }, + }; + const query = { + context: 'Projects', + query: { + $properties: ['ActiveConfigurations', 'Path'], + ActiveConfigurations: configurationsObject, + $filter: { ProjectsByCapabilities: ['WebAssembly'] }, + }, + }; + let queryString = ''; + if (projectPath != '') { + const query = { + context: 'Projects', + query: { + $properties: ['ActiveConfigurations', 'Path'], + ActiveConfigurations: configurationsObject, + $where: { startswith: [{ '.': 'Path' }, projectPath] }, + }, + }; + queryString = JSON.stringify(query); + } else { + queryString = JSON.stringify(query); + } + const proxy = await getCSharpDevKit()?.exports.serviceBroker.getProxy( + Descriptors.projectQueryExecutionService + ); + if (!proxy) { + throw new Error('Unable to obtain required service from C# Dev Kit.'); + } + try { + const result = await proxy.ExecuteQueryAsync(queryString); + const queryResult = JSON.parse(result) as QueryResult; + const pattern = /^net(\d+\.\d+)\b/; + if (queryResult && queryResult.Items) { + isNet9OrNewer = false; + queryResult.Items.forEach((project) => { + project.ActiveConfigurations?.forEach((activeConfig) => { + const match = activeConfig.BuildProperties[0].Value.match(pattern); + if (match && match[1] >= 9) { + isNet9OrNewer = true; + } + }); + }); + return isNet9OrNewer; + } + } catch (err) { + throw new Error('Exception while talking to proxy: ' + err); + } finally { + proxy?.dispose(); + } + return isNet9OrNewer; + } + private static async useVSDbg(projectPath: string): Promise { + const wasmConfig = vscode.workspace.getConfiguration('csharp'); + const useVSDbg = wasmConfig.get('wasm.debug.useVSDbg') == true; + if (!useVSDbg) { + return false; + } + const existWebAssemblyWebBridge = existsSync(BlazorDebugConfigurationProvider.getWebAssemblyWebBridgePath()); + const csharpDevKitExtension = getCSharpDevKit(); + let isNet9OrNewer = true; + if (existWebAssemblyWebBridge && csharpDevKitExtension !== undefined) { + isNet9OrNewer = await BlazorDebugConfigurationProvider.isNet9OrNewer(projectPath); + } + return useVSDbg && existWebAssemblyWebBridge && isNet9OrNewer; + } + + private static getWebAssemblyWebBridgePath() { + const vsWebAssemblyBridge = path.join( + getExtensionPath(), + '.vswebassemblybridge', + 'Microsoft.Diagnostics.BrowserDebugHost.dll' + ); + return vsWebAssemblyBridge; + } + + public static async tryToUseVSDbgForMono(urlStr: string, projectPath: string): Promise<[string, number, number]> { + const useVSDbg = await BlazorDebugConfigurationProvider.useVSDbg(projectPath); + + if (useVSDbg) { + const [inspectUri, portICorDebug, portBrowserDebug] = + await BlazorDebugConfigurationProvider.launchVsWebAssemblyBridge(urlStr); + + return [inspectUri, portICorDebug, portBrowserDebug]; + } + return ['', -1, -1]; + } + public static async determineBrowserType(): Promise { // 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. diff --git a/src/razor/src/blazorDebug/constants.ts b/src/razor/src/blazorDebug/constants.ts index b13163af2..d41894e2f 100644 --- a/src/razor/src/blazorDebug/constants.ts +++ b/src/razor/src/blazorDebug/constants.ts @@ -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'; diff --git a/src/shared/IWorkspaceDebugInformationProvider.ts b/src/shared/IWorkspaceDebugInformationProvider.ts index de316d3b1..029d4b5b6 100644 --- a/src/shared/IWorkspaceDebugInformationProvider.ts +++ b/src/shared/IWorkspaceDebugInformationProvider.ts @@ -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; } diff --git a/src/shared/assets.ts b/src/shared/assets.ts index 41a7db31d..79660e485 100644 --- a/src/shared/assets.ts +++ b/src/shared/assets.ts @@ -344,6 +344,49 @@ export class AssetGenerator { return result; } + + public getAssetsPathAndProgram(): [string, string] { + let assetsPath = ``; + this.executableProjects.forEach((project) => { + if (project.isWebAssemblyProject) { + assetsPath += path.join(path.dirname(project.outputPath), path.sep); + assetsPath += ';'; + } + }); + assetsPath = assetsPath.slice(0, -1); + return [assetsPath, this.executableProjects[0].outputPath]; + } + + public isDotNet9OrNewer(): boolean { + let ret = false; + for (let i = 0; i < this.executableProjects.length; i++) { + const project = this.executableProjects.at(i); + if (project?.isWebAssemblyProject) { + let projectFileText = fs.readFileSync(project.projectPath, 'utf8'); + projectFileText = projectFileText.toLowerCase(); + const pattern = + /.*.*<\/targetframework>.*|.*.*<\/targetframeworks>.*/; + const pattern2 = /^net(\d+\.\d+)\b/g; + const match = projectFileText.match(pattern); + if (match) { + const matches = match[0] + .replace('', '') + .replace('', '') + .replace('', '') + .replace('', '') + .trim() + .matchAll(pattern2); + for (const match of matches) { + ret = true; + if (match && +match[1] < 9) { + return false; + } + } + } + } + } + return ret; + } } export enum ProgramLaunchType { diff --git a/src/shared/configurationProvider.ts b/src/shared/configurationProvider.ts index 942f36771..e02c393ac 100644 --- a/src/shared/configurationProvider.ts +++ b/src/shared/configurationProvider.ts @@ -18,6 +18,7 @@ import { import { PlatformInformation } from './platform'; import { getCSharpDevKit } from '../utils/getCSharpDevKit'; import { commonOptions } from './options'; +import { DotnetWorkspaceConfigurationProvider } from './workspaceConfigurationProvider'; /** * Class used for debug configurations that will be sent to the debugger registered by {@link DebugAdapterExecutableFactory} @@ -148,7 +149,27 @@ export class BaseVsDbgConfigurationProvider implements vscode.DebugConfiguration this.checkForDevCerts(commonOptions.dotnetPath); } } - + if ( + debugConfiguration.type === 'monovsdbg' && + debugConfiguration.monoDebuggerOptions.platform === 'browser' && + this instanceof DotnetWorkspaceConfigurationProvider + ) { + const configProvider = this as DotnetWorkspaceConfigurationProvider; + if (folder && debugConfiguration.monoDebuggerOptions.assetsPath == null) { + const csharpDevKitExtension = getCSharpDevKit(); + if (csharpDevKitExtension === undefined) { + if (!(await configProvider.isDotNet9OrNewer(folder))) { + return undefined; + } + } + const [assetsPath, programName] = await configProvider.getAssetsPathAndProgram(folder); + debugConfiguration.monoDebuggerOptions.assetsPath = assetsPath; + debugConfiguration.program = programName; + if (debugConfiguration.program == null) { + return undefined; + } + } + } return debugConfiguration; } diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 4aaeb80e5..cfb626065 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -35,12 +35,15 @@ export function findNetStandardTargetFramework(tfmShortNames: string[]): string return tfmShortNames.find((tf) => tf.startsWith('netstandard')); } -export function isWebProject(projectPath: string): boolean { - const projectFileText = fs.readFileSync(projectPath, 'utf8'); +export function isWebProject(projectPath: string): [boolean, boolean] { + const projectFileText = fs.readFileSync(projectPath, 'utf8').toLowerCase(); // Assume that this is an MSBuild project. In that case, look for the 'Sdk="Microsoft.NET.Sdk.Web"' attribute. // TODO: Have OmniSharp provide the list of SDKs used by a project and check that list instead. - return projectFileText.toLowerCase().indexOf('sdk="microsoft.net.sdk.web"') >= 0; + return [ + projectFileText.indexOf('sdk="microsoft.net.sdk.web"') >= 0, + projectFileText.indexOf('sdk="microsoft.net.sdk.blazorwebassembly"') >= 0, + ]; } export async function isBlazorWebAssemblyProject(projectPath: string): Promise { diff --git a/src/shared/workspaceConfigurationProvider.ts b/src/shared/workspaceConfigurationProvider.ts index f13ebbddb..54aef9931 100644 --- a/src/shared/workspaceConfigurationProvider.ts +++ b/src/shared/workspaceConfigurationProvider.ts @@ -96,4 +96,22 @@ export class DotnetWorkspaceConfigurationProvider extends BaseVsDbgConfiguration return [createFallbackLaunchConfiguration(), parse(createAttachConfiguration())]; } } + + async getAssetsPathAndProgram(folder: vscode.WorkspaceFolder): Promise<[string, string]> { + const info = await this.workspaceDebugInfoProvider.getWorkspaceDebugInformation(folder.uri); + if (!info) { + return ['', '']; + } + const generator = new AssetGenerator(info, folder); + return generator.getAssetsPathAndProgram(); + } + + async isDotNet9OrNewer(folder: vscode.WorkspaceFolder): Promise { + const info = await this.workspaceDebugInfoProvider.getWorkspaceDebugInformation(folder.uri); + if (!info) { + return false; + } + const generator = new AssetGenerator(info, folder); + return generator.isDotNet9OrNewer(); + } }