diff --git a/debugger-launchjson.md b/debugger-launchjson.md index c242db2cf..745a418dd 100644 --- a/debugger-launchjson.md +++ b/debugger-launchjson.md @@ -338,3 +338,18 @@ Example: ```json "targetArchitecture": "arm64" ``` + +## Check for DevCert + +This option controls if, on launch, the the debugger should check if the computer has a self-signed HTTPS certificate used to develop web projects running on https endpoints. For this it will try to run `dotnet dev-certs https --check --trust`, if no certs are found it will prompt the user to suggest creating one. If approved by the user, the extension will run `dotnet dev-certs https --trust` to create a trusted self-signed certificate. + +If unspecified, defaults to true when `serverReadyAction` is set. +This option does nothing on Linux, VS Code remote, and VS Code Web UI scenarios. + +You can override this behavior by setting `checkForDevCert` to false in your `launch.json`. + +Example: + +```json + "checkForDevCert": "false" +``` \ No newline at end of file diff --git a/package.json b/package.json index 7dbab1b8b..98312b7a7 100644 --- a/package.json +++ b/package.json @@ -1982,6 +1982,11 @@ "targetArchitecture": { "type": "string", "description": "[Only supported in local macOS debugging] The architecture of the debuggee. This will automatically be detected unless this parameter is set. Allowed values are x86_64 or arm64." + }, + "checkForDevCert": { + "type": "boolean", + "description": "When true, the debugger will check if the computer has a self-signed HTTPS certificate used to develop web servers running on https endpoints. If unspecified, defaults to true when `serverReadyAction` is set. This option does nothing on Linux, VS Code remote, and VS Code Web UI scenarios. If the HTTPS certificate is not found or isn't trusted, the user will be prompted to install/trust it.", + "default": true } } }, @@ -3109,6 +3114,11 @@ "targetArchitecture": { "type": "string", "description": "[Only supported in local macOS debugging] The architecture of the debuggee. This will automatically be detected unless this parameter is set. Allowed values are x86_64 or arm64." + }, + "checkForDevCert": { + "type": "boolean", + "description": "When true, the debugger will check if the computer has a self-signed HTTPS certificate used to develop web servers running on https endpoints. If unspecified, defaults to true when `serverReadyAction` is set. This option does nothing on Linux, VS Code remote, and VS Code Web UI scenarios. If the HTTPS certificate is not found or isn't trusted, the user will be prompted to install/trust it.", + "default": true } } }, diff --git a/src/coreclr-debug/activate.ts b/src/coreclr-debug/activate.ts index 79a0bff8e..50880d755 100644 --- a/src/coreclr-debug/activate.ts +++ b/src/coreclr-debug/activate.ts @@ -32,8 +32,8 @@ export async function activate(thisExtension: vscode.Extension { @@ -59,7 +63,66 @@ export class DotnetDebugConfigurationProvider implements vscode.DebugConfigurati return undefined; } } + + // We want to ask the user if we should run dotnet dev-certs https --trust, but this doesn't work in a few cases -- + // Linux -- not supported by the .NET CLI as there isn't a single root cert store + // VS Code remoting/Web UI -- the trusted cert work would need to happen on the client machine, but we don't have a way to run code there currently + // pipeTransport -- the dev cert on the server will be different from the client + if (!this.platformInformation.isLinux() && !vscode.env.remoteName && vscode.env.uiKind != vscode.UIKind.Web && !debugConfiguration.pipeTransport) + { + if(debugConfiguration.checkForDevCert === undefined && debugConfiguration.serverReadyAction) + { + debugConfiguration.checkForDevCert = true; + } + + if (debugConfiguration.checkForDevCert) + { + checkForDevCerts(this.options.dotNetCliPaths, this.eventStream); + } + } return debugConfiguration; } } + +function checkForDevCerts(dotNetCliPaths: string[], eventStream: EventStream){ + hasDotnetDevCertsHttps(dotNetCliPaths).then(async (returnData) => { + let errorCode = returnData.error?.code; + if(errorCode === CertToolStatusCodes.CertificateNotTrusted || errorCode === CertToolStatusCodes.ErrorNoValidCertificateFound) + { + const labelYes: string = "Yes"; + const labelNotNow: string = "Not Now"; + const labelMoreInfo: string = "More Information"; + + const result = await vscode.window.showInformationMessage( + "The selected launch configuration is configured to launch a web browser but no trusted development certificate was found. Create a trusted self-signed certificate?", + { title:labelYes }, { title:labelNotNow, isCloseAffordance: true }, { title:labelMoreInfo } + ); + if (result?.title === labelYes) + { + let returnData = await createSelfSignedCert(dotNetCliPaths); + if (returnData.error === null) //if the prcess returns 0, returnData.error is null, otherwise the return code can be acessed in returnData.error.code + { + let message = errorCode === CertToolStatusCodes.CertificateNotTrusted ? 'trusted' : 'created'; + vscode.window.showInformationMessage(`Self-signed certificate sucessfully ${message}.`); + } + else + { + eventStream.post(new DevCertCreationFailure(`${returnData.error.message}\ncode: ${returnData.error.code}\nstdout: ${returnData.stdout}`)); + + const labelShowOutput: string = "Show Output"; + const result = await vscode.window.showWarningMessage("Couldn't create self-signed certificate. See output for more information.", labelShowOutput); + if (result === labelShowOutput){ + eventStream.post(new ShowChannel()); + } + } + } + if (result?.title === labelMoreInfo) + { + const launchjsonDescriptionURL = 'https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#check-for-devcert'; + vscode.env.openExternal(vscode.Uri.parse(launchjsonDescriptionURL)); + checkForDevCerts(dotNetCliPaths, eventStream); + } + } + }); +} diff --git a/src/features/documentSymbolProvider.ts b/src/features/documentSymbolProvider.ts index 6f010495c..3d8e88e14 100644 --- a/src/features/documentSymbolProvider.ts +++ b/src/features/documentSymbolProvider.ts @@ -31,13 +31,13 @@ export default class OmnisharpDocumentSymbolProvider extends AbstractSupport imp } } -function createSymbols(elements: Structure.CodeElement[]): vscode.DocumentSymbol[] { +function createSymbols(elements: Structure.CodeElement[], parentElement?: Structure.CodeElement): vscode.DocumentSymbol[] { let results: vscode.DocumentSymbol[] = []; elements.forEach(element => { - let symbol = createSymbolForElement(element); + let symbol = createSymbolForElement(element, parentElement); if (element.Children) { - symbol.children = createSymbols(element.Children); + symbol.children = createSymbols(element.Children, element); } results.push(symbol); @@ -46,11 +46,43 @@ function createSymbols(elements: Structure.CodeElement[]): vscode.DocumentSymbol return results; } -function createSymbolForElement(element: Structure.CodeElement): vscode.DocumentSymbol { +function getNameForElement(element: Structure.CodeElement, parentElement?: Structure.CodeElement): string { + switch (element.Kind) { + case SymbolKinds.Class: + case SymbolKinds.Delegate: + case SymbolKinds.Enum: + case SymbolKinds.Interface: + case SymbolKinds.Struct: + return element.Name; + + case SymbolKinds.Namespace: + return typeof parentElement !== 'undefined' && element.DisplayName.startsWith(`${parentElement.DisplayName}.`) + ? element.DisplayName.slice(parentElement.DisplayName.length + 1) + : element.DisplayName; + + case SymbolKinds.Constant: + case SymbolKinds.Constructor: + case SymbolKinds.Destructor: + case SymbolKinds.EnumMember: + case SymbolKinds.Event: + case SymbolKinds.Field: + case SymbolKinds.Indexer: + case SymbolKinds.Method: + case SymbolKinds.Operator: + case SymbolKinds.Property: + case SymbolKinds.Unknown: + default: + return element.DisplayName; + } +} + +function createSymbolForElement(element: Structure.CodeElement, parentElement?: Structure.CodeElement): vscode.DocumentSymbol { const fullRange = element.Ranges[SymbolRangeNames.Full]; const nameRange = element.Ranges[SymbolRangeNames.Name]; + const name = getNameForElement(element, parentElement); + const details = name === element.DisplayName ? '' : element.DisplayName; - return new vscode.DocumentSymbol(element.DisplayName, /*detail*/ "", toSymbolKind(element.Kind), toRange3(fullRange), toRange3(nameRange)); + return new vscode.DocumentSymbol(name, details, toSymbolKind(element.Kind), toRange3(fullRange), toRange3(nameRange)); } const kinds: { [kind: string]: vscode.SymbolKind; } = {}; diff --git a/src/observers/CsharpChannelObserver.ts b/src/observers/CsharpChannelObserver.ts index 486a24cf9..b331d8f91 100644 --- a/src/observers/CsharpChannelObserver.ts +++ b/src/observers/CsharpChannelObserver.ts @@ -18,6 +18,9 @@ export class CsharpChannelObserver extends BaseChannelObserver { case EventType.ProjectJsonDeprecatedWarning: this.showChannel(true); break; + case EventType.ShowChannel: + this.showChannel(false); + break; } } } \ No newline at end of file diff --git a/src/observers/CsharpLoggerObserver.ts b/src/observers/CsharpLoggerObserver.ts index 831d501da..6452503e1 100644 --- a/src/observers/CsharpLoggerObserver.ts +++ b/src/observers/CsharpLoggerObserver.ts @@ -68,6 +68,9 @@ export class CsharpLoggerObserver extends BaseLoggerObserver { case EventType.IntegrityCheckSuccess: this.handleIntegrityCheckSuccess(event); break; + case EventType.DevCertCreationFailure: + this.handleDevCertCreationFailure(event); + break; } } @@ -146,4 +149,8 @@ export class CsharpLoggerObserver extends BaseLoggerObserver { private handleDocumentSynchronizationFailure(event: Event.DocumentSynchronizationFailure) { this.logger.appendLine(`Failed to synchronize document '${event.documentPath}': ${event.errorMessage}`); } + + private handleDevCertCreationFailure(event: Event.DevCertCreationFailure) { + this.logger.appendLine(`Couldn't create self-signed certificate. ${event.errorMessage}`); + } } diff --git a/src/omnisharp/EventType.ts b/src/omnisharp/EventType.ts index d398f91a5..5e7aa7ff4 100644 --- a/src/omnisharp/EventType.ts +++ b/src/omnisharp/EventType.ts @@ -85,6 +85,8 @@ export enum EventType { TelemetryErrorEvent = 78, OmnisharpServerRequestCancelled = 79, BackgroundDiagnosticStatus = 80, + DevCertCreationFailure = 81, + ShowChannel = 82, } //Note that the EventType protocol is shared with Razor.VSCode and the numbers here should not be altered diff --git a/src/omnisharp/loggingEvents.ts b/src/omnisharp/loggingEvents.ts index f75cf80a8..7ae7304dd 100644 --- a/src/omnisharp/loggingEvents.ts +++ b/src/omnisharp/loggingEvents.ts @@ -350,3 +350,10 @@ export class DotNetTestDebugComplete implements BaseEvent { export class DownloadValidation implements BaseEvent { type = EventType.DownloadValidation; } +export class DevCertCreationFailure implements BaseEvent { + type = EventType.DevCertCreationFailure; + constructor(public errorMessage: string) { } +} +export class ShowChannel implements BaseEvent { + type = EventType.ShowChannel; +} \ No newline at end of file diff --git a/src/razor/syntaxes/aspnetcorerazor.tmLanguage.json b/src/razor/syntaxes/aspnetcorerazor.tmLanguage.json index 8459bdac2..ff23735fe 100644 --- a/src/razor/syntaxes/aspnetcorerazor.tmLanguage.json +++ b/src/razor/syntaxes/aspnetcorerazor.tmLanguage.json @@ -39,6 +39,31 @@ } ] }, + "optionally-transitioned-razor-control-structures": { + "patterns": [ + { + "include": "#razor-comment" + }, + { + "include": "#razor-codeblock" + }, + { + "include": "#explicit-razor-expression" + }, + { + "include": "#escaped-transition" + }, + { + "include": "#directives" + }, + { + "include": "#optionally-transitioned-csharp-control-structures" + }, + { + "include": "#implicit-expression" + } + ] + }, "escaped-transition": { "name": "constant.character.escape.razor.transition", "match": "@@" @@ -87,7 +112,7 @@ "include": "#razor-single-line-markup" }, { - "include": "#razor-control-structures" + "include": "#optionally-transitioned-razor-control-structures" }, { "include": "source.cs" @@ -556,7 +581,7 @@ "contentName": "source.cs", "patterns": [ { - "include": "source.cs" + "include": "source.cs#class-or-struct-members" } ], "end": "(\\})", @@ -964,6 +989,40 @@ } } }, + "optionally-transitioned-csharp-control-structures": { + "patterns": [ + { + "include": "#using-statement-with-optional-transition" + }, + { + "include": "#if-statement-with-optional-transition" + }, + { + "include": "#else-part" + }, + { + "include": "#foreach-statement-with-optional-transition" + }, + { + "include": "#for-statement-with-optional-transition" + }, + { + "include": "#while-statement" + }, + { + "include": "#switch-statement-with-optional-transition" + }, + { + "include": "#lock-statement-with-optional-transition" + }, + { + "include": "#do-statement-with-optional-transition" + }, + { + "include": "#try-statement-with-optional-transition" + } + ] + }, "transitioned-csharp-control-structures": { "patterns": [ { @@ -999,6 +1058,34 @@ ] }, "using-statement": { + "name": "meta.statement.using.razor", + "begin": "(?:(@))(using)\\b\\s*(?=\\()", + "beginCaptures": { + "1": { + "patterns": [ + { + "include": "#transition" + } + ] + }, + "2": { + "name": "keyword.other.using.cs" + } + }, + "patterns": [ + { + "include": "#csharp-condition" + }, + { + "include": "#csharp-code-block" + }, + { + "include": "#razor-codeblock-body" + } + ], + "end": "(?<=})" + }, + "using-statement-with-optional-transition": { "name": "meta.statement.using.razor", "begin": "(?:^\\s*|(@))(using)\\b\\s*(?=\\()", "beginCaptures": { @@ -1027,6 +1114,34 @@ "end": "(?<=})" }, "if-statement": { + "name": "meta.statement.if.razor", + "begin": "(?:(@))(if)\\b\\s*(?=\\()", + "beginCaptures": { + "1": { + "patterns": [ + { + "include": "#transition" + } + ] + }, + "2": { + "name": "keyword.control.conditional.if.cs" + } + }, + "patterns": [ + { + "include": "#csharp-condition" + }, + { + "include": "#csharp-code-block" + }, + { + "include": "#razor-codeblock-body" + } + ], + "end": "(?<=})" + }, + "if-statement-with-optional-transition": { "name": "meta.statement.if.razor", "begin": "(?:^\\s*|(@))(if)\\b\\s*(?=\\()", "beginCaptures": { @@ -1079,6 +1194,34 @@ "end": "(?<=})" }, "for-statement": { + "name": "meta.statement.for.razor", + "begin": "(?:(@))(for)\\b\\s*(?=\\()", + "beginCaptures": { + "1": { + "patterns": [ + { + "include": "#transition" + } + ] + }, + "2": { + "name": "keyword.control.loop.for.cs" + } + }, + "patterns": [ + { + "include": "#csharp-condition" + }, + { + "include": "#csharp-code-block" + }, + { + "include": "#razor-codeblock-body" + } + ], + "end": "(?<=})" + }, + "for-statement-with-optional-transition": { "name": "meta.statement.for.razor", "begin": "(?:^\\s*|(@))(for)\\b\\s*(?=\\()", "beginCaptures": { @@ -1107,6 +1250,41 @@ "end": "(?<=})" }, "foreach-statement": { + "name": "meta.statement.foreach.razor", + "begin": "(?:(@)(await\\s+)?)(foreach)\\b\\s*(?=\\()", + "beginCaptures": { + "1": { + "patterns": [ + { + "include": "#transition" + } + ] + }, + "2": { + "patterns": [ + { + "include": "#await-prefix" + } + ] + }, + "3": { + "name": "keyword.control.loop.foreach.cs" + } + }, + "patterns": [ + { + "include": "#foreach-condition" + }, + { + "include": "#csharp-code-block" + }, + { + "include": "#razor-codeblock-body" + } + ], + "end": "(?<=})" + }, + "foreach-statement-with-optional-transition": { "name": "meta.statement.foreach.razor", "begin": "(?:^\\s*|(@)(await\\s+)?)(foreach)\\b\\s*(?=\\()", "beginCaptures": { @@ -1201,7 +1379,35 @@ }, "do-statement": { "name": "meta.statement.do.razor", - "begin": "(?:^\\s*|(@))(do)\\b\\s*", + "begin": "(?:(@))(do)\\b\\s", + "beginCaptures": { + "1": { + "patterns": [ + { + "include": "#transition" + } + ] + }, + "2": { + "name": "keyword.control.loop.do.cs" + } + }, + "patterns": [ + { + "include": "#csharp-condition" + }, + { + "include": "#csharp-code-block" + }, + { + "include": "#razor-codeblock-body" + } + ], + "end": "(?<=})" + }, + "do-statement-with-optional-transition": { + "name": "meta.statement.do.razor", + "begin": "(?:^\\s*|(@))(do)\\b\\s", "beginCaptures": { "1": { "patterns": [ @@ -1261,6 +1467,34 @@ } }, "switch-statement": { + "name": "meta.statement.switch.razor", + "begin": "(?:(@))(switch)\\b\\s*(?=\\()", + "beginCaptures": { + "1": { + "patterns": [ + { + "include": "#transition" + } + ] + }, + "2": { + "name": "keyword.control.switch.cs" + } + }, + "patterns": [ + { + "include": "#csharp-condition" + }, + { + "include": "#switch-code-block" + }, + { + "include": "#razor-codeblock-body" + } + ], + "end": "(?<=})" + }, + "switch-statement-with-optional-transition": { "name": "meta.statement.switch.razor", "begin": "(?:^\\s*|(@))(switch)\\b\\s*(?=\\()", "beginCaptures": { @@ -1312,6 +1546,34 @@ } }, "lock-statement": { + "name": "meta.statement.lock.razor", + "begin": "(?:(@))(lock)\\b\\s*(?=\\()", + "beginCaptures": { + "1": { + "patterns": [ + { + "include": "#transition" + } + ] + }, + "2": { + "name": "keyword.other.lock.cs" + } + }, + "patterns": [ + { + "include": "#csharp-condition" + }, + { + "include": "#csharp-code-block" + }, + { + "include": "#razor-codeblock-body" + } + ], + "end": "(?<=})" + }, + "lock-statement-with-optional-transition": { "name": "meta.statement.lock.razor", "begin": "(?:^\\s*|(@))(lock)\\b\\s*(?=\\()", "beginCaptures": { @@ -1352,7 +1614,48 @@ } ] }, + "try-statement-with-optional-transition": { + "patterns": [ + { + "include": "#try-block-with-optional-transition" + }, + { + "include": "#catch-clause" + }, + { + "include": "#finally-clause" + } + ] + }, "try-block": { + "name": "meta.statement.try.razor", + "begin": "(?:(@))(try)\\b\\s*", + "beginCaptures": { + "1": { + "patterns": [ + { + "include": "#transition" + } + ] + }, + "2": { + "name": "keyword.control.try.cs" + } + }, + "patterns": [ + { + "include": "#csharp-condition" + }, + { + "include": "#csharp-code-block" + }, + { + "include": "#razor-codeblock-body" + } + ], + "end": "(?<=})" + }, + "try-block-with-optional-transition": { "name": "meta.statement.try.razor", "begin": "(?:^\\s*|(@))(try)\\b\\s*", "beginCaptures": { @@ -1506,4 +1809,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/tools/OptionsSchema.json b/src/tools/OptionsSchema.json index 2b0710fdb..98c788e40 100644 --- a/src/tools/OptionsSchema.json +++ b/src/tools/OptionsSchema.json @@ -407,6 +407,11 @@ "targetArchitecture": { "type": "string", "description": "[Only supported in local macOS debugging] The architecture of the debuggee. This will automatically be detected unless this parameter is set. Allowed values are x86_64 or arm64." + }, + "checkForDevCert": { + "type": "boolean", + "description": "When true, the debugger will check if the computer has a self-signed HTTPS certificate used to develop web servers running on https endpoints. If unspecified, defaults to true when `serverReadyAction` is set. This option does nothing on Linux, VS Code remote, and VS Code Web UI scenarios. If the HTTPS certificate is not found or isn't trusted, the user will be prompted to install/trust it.", + "default": true } } }, diff --git a/src/utils/DotnetDevCertsHttps.ts b/src/utils/DotnetDevCertsHttps.ts new file mode 100644 index 000000000..a1d1533e7 --- /dev/null +++ b/src/utils/DotnetDevCertsHttps.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as cp from 'child_process'; +import { getExtensionPath } from "../common"; +import { getDotNetExecutablePath } from "./getDotnetInfo"; + +// Will return true if `dotnet dev-certs https --check` succesfully finds a trusted development certificate. +export async function hasDotnetDevCertsHttps(dotNetCliPaths: string[]): Promise { + + let dotnetExecutablePath = getDotNetExecutablePath(dotNetCliPaths); + + return await execChildProcess(`${dotnetExecutablePath ?? 'dotnet'} dev-certs https --check --trust`, process.cwd(), process.env); +} + +// Will run `dotnet dev-certs https --trust` to prompt the user to create a trusted self signed certificates. Retruns true if sucessfull. +export async function createSelfSignedCert(dotNetCliPaths: string[]): Promise { + + let dotnetExecutablePath = getDotNetExecutablePath(dotNetCliPaths); + + return await execChildProcess(`${dotnetExecutablePath ?? 'dotnet'} dev-certs https --trust`, process.cwd(), process.env); +} + +async function execChildProcess(command: string, workingDirectory: string = getExtensionPath(), env: NodeJS.ProcessEnv = {}): Promise { + return new Promise((resolve) => { + cp.exec(command, { cwd: workingDirectory, maxBuffer: 500 * 1024, env: env }, (error, stdout, stderr) => { + resolve({error, stdout, stderr}); + }); + }); +} + +interface ExecReturnData { + error: cp.ExecException | null; + stdout: string; + stderr: string; +} + +export enum CertToolStatusCodes +{ + CriticalError = -1, + Success = 0, + // Following are from trusting the certificate (dotnet dev-certs https --trust) + ErrorCreatingTheCertificate = 1, + ErrorSavingTheCertificate = 2, + ErrorExportingTheCertificate = 3, + ErrorTrustingTheCertificate = 4, + UserCancel = 5, + // Following two are from checking for trust (dotnet dev-certs https --check --trust) + ErrorNoValidCertificateFound = 6, + CertificateNotTrusted = 7, +} \ No newline at end of file diff --git a/src/utils/getDotnetInfo.ts b/src/utils/getDotnetInfo.ts index d4071c72f..cb219e912 100644 --- a/src/utils/getDotnetInfo.ts +++ b/src/utils/getDotnetInfo.ts @@ -15,16 +15,7 @@ export async function getDotnetInfo(dotNetCliPaths: string[]): Promise