Skip to content
Open
Show file tree
Hide file tree
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
22 changes: 16 additions & 6 deletions src/activateRoslyn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ export function activateRoslyn(
csharpChannel: vscode.LogOutputChannel,
reporter: TelemetryReporter,
csharpDevkitExtension: vscode.Extension<CSharpDevKitExports> | undefined,
getCoreClrDebugPromise: (languageServerStarted: Promise<any>) => Promise<void>
getCoreClrDebugPromise: (
languageServerStarted: Promise<any>,
csharpDevKitExports: Promise<CSharpDevKitExports | undefined>
) => Promise<void>
): CSharpExtensionExports {
const roslynLanguageServerEvents = new RoslynLanguageServerEvents();
context.subscriptions.push(roslynLanguageServerEvents);
Expand All @@ -62,8 +65,8 @@ export function activateRoslyn(
);

debugSessionTracker.initializeDebugSessionHandlers(context);
tryGetCSharpDevKitExtensionExports(csharpDevkitExtension, observableCsharpChannel);
const coreClrDebugPromise = getCoreClrDebugPromise(roslynLanguageServerStartedPromise);
const csharpDevKitExports = tryGetCSharpDevKitExtensionExports(csharpDevkitExtension, observableCsharpChannel);
const coreClrDebugPromise = getCoreClrDebugPromise(roslynLanguageServerStartedPromise, csharpDevKitExports);

const languageServerExport = new RoslynLanguageServerExport(roslynLanguageServerStartedPromise);
const activeDocumentLanguageSupport = new ActiveDocumentLanguageSupportService(
Expand Down Expand Up @@ -107,11 +110,15 @@ export function activateRoslyn(
* This method will try to get the CSharpDevKitExports through a thenable promise,
* awaiting `activate` will cause this extension's activation to hang.
*/
function tryGetCSharpDevKitExtensionExports(
async function tryGetCSharpDevKitExtensionExports(
csharpDevKit: vscode.Extension<CSharpDevKitExports> | undefined,
csharpChannel: vscode.LogOutputChannel
): void {
csharpDevKit?.activate().then(
): Promise<CSharpDevKitExports | undefined> {
if (!csharpDevKit) {
return Promise.resolve(undefined);
}

return Promise.resolve(csharpDevKit.activate()).then(
async (exports: CSharpDevKitExports) => {
if (exports && exports.serviceBroker) {
// When proffering this IServiceBroker into our own container,
Expand All @@ -131,9 +138,12 @@ function tryGetCSharpDevKitExtensionExports(
} else {
csharpChannel.error(`'${csharpDevkitExtensionId}' activated but did not return expected Exports.`);
}

return exports;
},
() => {
csharpChannel.error(`Failed to activate '${csharpDevkitExtensionId}'`);
return undefined;
}
);
}
Expand Down
74 changes: 61 additions & 13 deletions src/coreclrDebug/activate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,39 @@ import { BaseVsDbgConfigurationProvider } from '../shared/configurationProvider'
import { omnisharpOptions } from '../shared/options';
import { ActionOption, CommandOption, showErrorMessage } from '../shared/observers/utils/showMessage';
import { getCSharpDevKit } from '../utils/getCSharpDevKit';
import { CSharpDevKitExports } from '../csharpDevKitExports';
import { resolveWorkspaceDotnetHost, WorkspaceDotnetHostResolution } from './workspaceDotnetHost';

export async function activate(
thisExtension: vscode.Extension<any>,
context: vscode.ExtensionContext,
platformInformation: PlatformInformation,
eventStream: EventStream,
csharpOutputChannel: vscode.OutputChannel,
languageServerStartedPromise: Promise<any> | undefined
languageServerStartedPromise: Promise<any> | undefined,
csharpDevKitExports: Promise<CSharpDevKitExports | undefined> | undefined
) {
const disposables = new CompositeDisposable();
let disposed = false;
context.subscriptions.push({
dispose: () => {
disposed = true;
},
});

const debugUtil = new CoreClrDebugUtil(context.extensionPath);
const workspaceDotnetHost = resolveWorkspaceDotnetHost(csharpDevKitExports);
let completeDebuggerInstallPromise: Promise<boolean> | undefined;
const ensureDebuggerInstallComplete = async () => {
completeDebuggerInstallPromise ??= completeDebuggerInstall(
debugUtil,
platformInformation,
eventStream,
workspaceDotnetHost,
() => disposed
);
return await completeDebuggerInstallPromise;
};

if (!CoreClrDebugUtil.existsSync(debugUtil.debugAdapterDir())) {
const isValidArchitecture: boolean = await checkIsValidArchitecture(platformInformation, eventStream);
Expand All @@ -48,7 +69,7 @@ export async function activate(
showInstallErrorMessage(eventStream);
}
} else if (!CoreClrDebugUtil.existsSync(debugUtil.installCompleteFilePath())) {
await completeDebuggerInstall(debugUtil, platformInformation, eventStream);
await ensureDebuggerInstallComplete();
}

// register process picker for attach for legacy configurations.
Expand Down Expand Up @@ -97,11 +118,12 @@ export async function activate(
);

const factory = new DebugAdapterExecutableFactory(
debugUtil,
platformInformation,
eventStream,
thisExtension.packageJSON,
thisExtension.extensionPath
thisExtension.extensionPath,
ensureDebuggerInstallComplete,
workspaceDotnetHost
);
/** 'clr' type does not have a intial configuration provider, but we need to register it to support the common debugger features listed in {@link BaseVsDbgConfigurationProvider} */
context.subscriptions.push(
Expand Down Expand Up @@ -177,10 +199,24 @@ async function checkIsValidArchitecture(
async function completeDebuggerInstall(
debugUtil: CoreClrDebugUtil,
platformInformation: PlatformInformation,
eventStream: EventStream
eventStream: EventStream,
workspaceDotnetHost: Promise<WorkspaceDotnetHostResolution>,
isDisposed: () => boolean
): Promise<boolean> {
try {
await debugUtil.checkDotNetCli(omnisharpOptions.dotNetCliPaths);
const workspaceHost = await workspaceDotnetHost;
if (workspaceHost.kind === 'blocked') {
return false;
}

if (workspaceHost.kind === 'ready') {
await debugUtil.checkDotNetCli([], {
dotnetExecutablePath: workspaceHost.dotnetPath,
environment: workspaceHost.environment,
});
} else {
await debugUtil.checkDotNetCli(omnisharpOptions.dotNetCliPaths);
}
const isValidArchitecture = await checkIsValidArchitecture(platformInformation, eventStream);
if (!isValidArchitecture) {
eventStream.post(new DebuggerNotInstalledFailure());
Expand All @@ -201,8 +237,10 @@ async function completeDebuggerInstall(
const error = err as Error;

// Check for dotnet tools failed. pop the UI
showDotnetToolsWarning(error.message);
eventStream.post(new DebuggerPrerequisiteWarning(error.message));
if (!isDisposed()) {
showDotnetToolsWarning(error.message);
eventStream.post(new DebuggerPrerequisiteWarning(error.message));
}
// TODO: log telemetry?
return false;
}
Expand Down Expand Up @@ -257,11 +295,12 @@ function showDotnetToolsWarning(message: string): void {
// Else it will launch the debug adapter
export class DebugAdapterExecutableFactory implements vscode.DebugAdapterDescriptorFactory {
constructor(
private readonly debugUtil: CoreClrDebugUtil,
private readonly platformInfo: PlatformInformation,
private readonly eventStream: EventStream,
private readonly packageJSON: any,
private readonly extensionPath: string
private readonly extensionPath: string,
private readonly ensureDebuggerInstallComplete: () => Promise<boolean>,
private readonly workspaceDotnetHost: Promise<WorkspaceDotnetHostResolution>
) {}

async createDebugAdapterDescriptor(
Expand Down Expand Up @@ -301,7 +340,7 @@ export class DebugAdapterExecutableFactory implements vscode.DebugAdapterDescrip
}
// install.complete does not exist, check dotnetCLI to see if we can complete.
else if (!CoreClrDebugUtil.existsSync(util.installCompleteFilePath())) {
const success = await completeDebuggerInstall(this.debugUtil, this.platformInfo, this.eventStream);
const success = await this.ensureDebuggerInstallComplete();
if (!success) {
this.eventStream.post(new DebuggerNotInstalledFailure());
throw new Error(
Expand All @@ -317,7 +356,14 @@ export class DebugAdapterExecutableFactory implements vscode.DebugAdapterDescrip

// use the executable specified in the package.json if it exists or determine it based on some other information (e.g. the session)
if (!executable) {
const dotNetInfo = await getDotnetInfo(omnisharpOptions.dotNetCliPaths);
const workspaceHost = await this.workspaceDotnetHost;
const dotNetInfo =
workspaceHost.kind === 'ready'
? await getDotnetInfo([], {
dotnetExecutablePath: workspaceHost.dotnetPath,
environment: workspaceHost.environment,
})
: await getDotnetInfo(omnisharpOptions.dotNetCliPaths);
const targetArchitecture = getTargetArchitecture(
this.platformInfo,
_session.configuration.targetArchitecture,
Expand All @@ -332,7 +378,9 @@ export class DebugAdapterExecutableFactory implements vscode.DebugAdapterDescrip

// Look to see if DOTNET_ROOT is set, then use dotnet cli path
const dotnetRoot: string =
process.env.DOTNET_ROOT ?? (dotNetInfo.CliPath ? path.dirname(dotNetInfo.CliPath) : '');
(workspaceHost.kind === 'ready' && workspaceHost.environment?.DOTNET_ROOT) ||
process.env.DOTNET_ROOT ||
(dotNetInfo.CliPath ? path.dirname(dotNetInfo.CliPath) : '');

let options: vscode.DebugAdapterExecutableOptions | undefined = undefined;
if (dotnetRoot) {
Expand Down
9 changes: 7 additions & 2 deletions src/coreclrDebug/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import { PlatformInformation } from '../shared/platform';
import { getDotnetInfo } from '../shared/utils/getDotnetInfo';
import { DotnetInfo } from '../shared/utils/dotnetInfo';

export interface DotnetCliCheckOptions {
dotnetExecutablePath: string;
environment?: Readonly<Record<string, string | null>>;
}

const MINIMUM_SUPPORTED_DOTNET_CLI = '1.0.0';

// .NET 8 requires macOS 12+, however the build machines are on macOS 13, which is Darwin 22.0+
Expand Down Expand Up @@ -65,9 +70,9 @@ export class CoreClrDebugUtil {

// This function checks for the presence of dotnet on the path and ensures the Version
// is new enough for us.
public async checkDotNetCli(dotNetCliPaths: string[]): Promise<void> {
public async checkDotNetCli(dotNetCliPaths: string[], options?: DotnetCliCheckOptions): Promise<void> {
try {
const dotnetInfo = await getDotnetInfo(dotNetCliPaths);
const dotnetInfo = await getDotnetInfo(dotNetCliPaths, options);
if (semver.lt(dotnetInfo.Version, MINIMUM_SUPPORTED_DOTNET_CLI)) {
throw new Error(
vscode.l10n.t(
Expand Down
79 changes: 79 additions & 0 deletions src/coreclrDebug/workspaceDotnetHost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { CSharpDevKitExports, WorkspaceDotnetHost } from '../csharpDevKitExports';

export type WorkspaceDotnetHostResolution =
| { kind: 'standalone' }
| { kind: 'blocked' }
| {
kind: 'ready';
dotnetPath: string;
environment?: Readonly<Record<string, string | null>>;
};

const DEV_KIT_ACTIVATION_TIMEOUT_MS = 90_000;
const timedOut = Symbol('timedOut');

/**
* Waits for the already-started C# Dev Kit activation without participating in extension activation.
* Older Dev Kit versions and failed or bounded-out activation preserve standalone C# behavior.
*/
export async function resolveWorkspaceDotnetHost(
devKitExports: Promise<CSharpDevKitExports | undefined> | undefined,
timeoutMs = DEV_KIT_ACTIVATION_TIMEOUT_MS
): Promise<WorkspaceDotnetHostResolution> {
if (!devKitExports) {
return { kind: 'standalone' };
}

const exports = await settleWithin(devKitExports, timeoutMs);
if (exports === timedOut) {
// Dev Kit is installed and still activating. Do not race its Workspace Requirements remediation.
return { kind: 'blocked' };
}
if (!exports || typeof exports.getWorkspaceDotnetHost !== 'function') {
return { kind: 'standalone' };
}

const host = await settleWithin(exports.getWorkspaceDotnetHost(), timeoutMs);
if (host === timedOut) {
return { kind: 'blocked' };
}
return mapWorkspaceDotnetHost(host);
}

function mapWorkspaceDotnetHost(host: WorkspaceDotnetHost | undefined): WorkspaceDotnetHostResolution {
if (!host || host.status === 'not-applicable') {
return { kind: 'standalone' };
}
if (host.status === 'blocked') {
return { kind: 'blocked' };
}
if (!host.dotnetPath) {
return { kind: 'standalone' };
}
return {
kind: 'ready',
dotnetPath: host.dotnetPath,
environment: host.environment,
};
}

async function settleWithin<T>(promise: Promise<T>, timeoutMs: number): Promise<T | undefined | typeof timedOut> {
let timer: NodeJS.Timeout | undefined;
try {
return await Promise.race([
promise.catch(() => undefined),
new Promise<typeof timedOut>((resolve) => {
timer = setTimeout(() => resolve(timedOut), timeoutMs);
}),
]);
} finally {
if (timer) {
clearTimeout(timer);
}
}
}
11 changes: 11 additions & 0 deletions src/csharpDevKitExports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,22 @@ import * as vscode from 'vscode';

import { IServiceBroker } from '@microsoft/servicehub-framework';

export type WorkspaceDotnetHost =
| {
status: 'ready';
dotnetPath: string;
environment?: Readonly<Record<string, string | null>>;
}
| { status: 'blocked' }
| { status: 'not-applicable' };

export interface CSharpDevKitExports {
serviceBroker: IServiceBroker;
getBrokeredServiceServerPipeName: () => Promise<string>;
components: Readonly<{ [key: string]: string }>;
hasServerProcessLoaded: () => boolean;
serverProcessLoaded: vscode.Event<void>;
setupTelemetryEnvironmentAsync: (env: NodeJS.ProcessEnv) => Promise<string | undefined>;
/** Gets the immutable dotnet host selected for this workspace by C# Dev Kit. */
getWorkspaceDotnetHost?: () => Promise<WorkspaceDotnetHost>;
}
9 changes: 7 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { checkDotNetRuntimeExtensionVersion } from './checkDotNetRuntimeExtensio
import { checkIsSupportedPlatform } from './checkSupportedPlatform';
import { activateRoslyn } from './activateRoslyn';
import { LimitedActivationStatus } from './shared/limitedActivationStatus';
import { CSharpDevKitExports } from './csharpDevKitExports';

export async function activate(
context: vscode.ExtensionContext
Expand Down Expand Up @@ -121,7 +122,10 @@ export async function activate(
})
);
} else {
const getCoreClrDebugPromise = async (languageServerStartedPromise: Promise<void>) => {
const getCoreClrDebugPromise = async (
languageServerStartedPromise: Promise<void>,
csharpDevKitExports?: Promise<CSharpDevKitExports | undefined>
) => {
let coreClrDebugPromise = Promise.resolve();
if (runtimeDependenciesExist['Debugger']) {
// activate coreclr-debug
Expand All @@ -131,7 +135,8 @@ export async function activate(
platformInfo,
eventStream,
csharpChannel,
languageServerStartedPromise
languageServerStartedPromise,
csharpDevKitExports
);
}

Expand Down
Loading
Loading