diff --git a/src/installRuntimeDependencies.ts b/src/installRuntimeDependencies.ts index 7218d6c931..3cdab86cd2 100644 --- a/src/installRuntimeDependencies.ts +++ b/src/installRuntimeDependencies.ts @@ -8,7 +8,7 @@ import { PackageInstallation, LogPlatformInfo, InstallationSuccess } from './sha import { EventStream } from './eventStream'; import { getRuntimeDependenciesPackages } from './tools/runtimeDependencyPackageUtils'; import { getAbsolutePathPackagesToInstall } from './packageManager/getAbsolutePathPackagesToInstall'; -import IInstallDependencies from './packageManager/IInstallDependencies'; +import { DependencyInstallationStatus, IInstallDependencies } from './packageManager/IInstallDependencies'; import { AbsolutePathPackage } from './packageManager/absolutePathPackage'; export async function installRuntimeDependencies( @@ -19,26 +19,39 @@ export async function installRuntimeDependencies( platformInfo: PlatformInformation, useFramework: boolean, requiredPackageIds: string[] -): Promise { +): Promise { const runTimeDependencies = getRuntimeDependenciesPackages(packageJSON); const packagesToInstall = await getAbsolutePathPackagesToInstall(runTimeDependencies, platformInfo, extensionPath); + + // PackagesToInstall will only return packages that are not already installed. However, + // we need to return the installation status of all required packages, so we need to + // track which required packages are already installed, so that we can return true for them. + const installedPackages = requiredPackageIds.filter( + (id) => packagesToInstall.find((pkg) => pkg.id === id) === undefined + ); + const installedPackagesResults = installedPackages.reduce((acc, id) => ({ ...acc, [id]: true }), {}); + const filteredPackages = filterOmniSharpPackage(packagesToInstall, useFramework); const filteredRequiredPackages = filteredRequiredPackage(requiredPackageIds, filteredPackages); - if (filteredRequiredPackages.length > 0) { - eventStream.post(new PackageInstallation('C# dependencies')); - // Display platform information and RID - eventStream.post(new LogPlatformInfo(platformInfo)); + if (filteredRequiredPackages.length === 0) { + return installedPackagesResults; + } + + eventStream.post(new PackageInstallation('C# dependencies')); + // Display platform information and RID + eventStream.post(new LogPlatformInfo(platformInfo)); + + const installationResults = await installDependencies(filteredRequiredPackages); - if (await installDependencies(filteredRequiredPackages)) { - eventStream.post(new InstallationSuccess()); - } else { - return false; - } + const failedPackages = Object.entries(installationResults) + .filter(([, installed]) => !installed) + .map(([name]) => name); + if (failedPackages.length === 0) { + eventStream.post(new InstallationSuccess()); } - //All the required packages are already downloaded and installed - return true; + return { ...installedPackagesResults, ...installationResults }; } function filterOmniSharpPackage(packages: AbsolutePathPackage[], useFramework: boolean) { diff --git a/src/lsptoolshost/extensions/builtInComponents.ts b/src/lsptoolshost/extensions/builtInComponents.ts index af657fa165..4f67c80e28 100644 --- a/src/lsptoolshost/extensions/builtInComponents.ts +++ b/src/lsptoolshost/extensions/builtInComponents.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; import { LanguageServerOptions } from '../../shared/options'; @@ -11,6 +12,7 @@ interface ComponentInfo { defaultFolderName: string; optionName: string; componentDllPaths: string[]; + isOptional?: boolean; } export const componentInfo: { [key: string]: ComponentInfo } = { @@ -41,15 +43,27 @@ export const componentInfo: { [key: string]: ComponentInfo } = { defaultFolderName: '.roslynCopilot', optionName: 'roslynCopilot', componentDllPaths: ['Microsoft.VisualStudio.Copilot.Roslyn.LanguageServer.dll'], + isOptional: true, }, }; -export function getComponentPaths(componentName: string, options: LanguageServerOptions | undefined): string[] { +export function getComponentPaths( + componentName: string, + options: LanguageServerOptions | undefined, + channel?: vscode.LogOutputChannel +): string[] { const component = componentInfo[componentName]; const baseFolder = getComponentFolderPath(component, options); const paths = component.componentDllPaths.map((dllPath) => path.join(baseFolder, dllPath)); for (const dllPath of paths) { if (!fs.existsSync(dllPath)) { + if (component.isOptional) { + // Component is optional and doesn't exist - log warning and return empty array + if (channel) { + channel.warn(`Optional component '${componentName}' could not be found at '${dllPath}'.`); + } + return []; + } throw new Error(`Component DLL not found: ${dllPath}`); } } diff --git a/src/lsptoolshost/server/roslynLanguageServer.ts b/src/lsptoolshost/server/roslynLanguageServer.ts index 8d1951d1f4..6ddc1ccadc 100644 --- a/src/lsptoolshost/server/roslynLanguageServer.ts +++ b/src/lsptoolshost/server/roslynLanguageServer.ts @@ -651,7 +651,7 @@ export class RoslynLanguageServer { : razorOptions.razorServerPath; let razorComponentPath = ''; - getComponentPaths('razorExtension', languageServerOptions).forEach((extPath) => { + getComponentPaths('razorExtension', languageServerOptions, channel).forEach((extPath) => { additionalExtensionPaths.push(extPath); razorComponentPath = path.dirname(extPath); }); @@ -695,10 +695,10 @@ export class RoslynLanguageServer { // Set command enablement as soon as we know devkit is available. await vscode.commands.executeCommand('setContext', 'dotnet.server.activationContext', 'RoslynDevKit'); - const csharpDevKitArgs = this.getCSharpDevKitExportArgs(additionalExtensionPaths); + const csharpDevKitArgs = this.getCSharpDevKitExportArgs(additionalExtensionPaths, channel); args = args.concat(csharpDevKitArgs); - await this.setupDevKitEnvironment(dotnetInfo.env, csharpDevkitExtension, additionalExtensionPaths); + await this.setupDevKitEnvironment(dotnetInfo.env, csharpDevkitExtension, additionalExtensionPaths, channel); } else { // C# Dev Kit is not installed - continue C#-only activation. channel.info('Activating C# standalone...'); @@ -1012,10 +1012,13 @@ export class RoslynLanguageServer { ); } - private static getCSharpDevKitExportArgs(additionalExtensionPaths: string[]): string[] { + private static getCSharpDevKitExportArgs( + additionalExtensionPaths: string[], + channel: vscode.LogOutputChannel + ): string[] { const args: string[] = []; - const devKitDepsPath = getComponentPaths('roslynDevKit', languageServerOptions); + const devKitDepsPath = getComponentPaths('roslynDevKit', languageServerOptions, channel); if (devKitDepsPath.length > 1) { throw new Error('Expected only one devkit deps path'); } @@ -1026,7 +1029,7 @@ export class RoslynLanguageServer { // Also include the Xaml Dev Kit extensions, if enabled. if (languageServerOptions.enableXamlTools) { - getComponentPaths('xamlTools', languageServerOptions).forEach((path) => + getComponentPaths('xamlTools', languageServerOptions, channel).forEach((path) => additionalExtensionPaths.push(path) ); } @@ -1086,7 +1089,8 @@ export class RoslynLanguageServer { private static async setupDevKitEnvironment( env: NodeJS.ProcessEnv, csharpDevkitExtension: vscode.Extension, - additionalExtensionPaths: string[] + additionalExtensionPaths: string[], + channel: vscode.LogOutputChannel ): Promise { const exports: CSharpDevKitExports = await csharpDevkitExtension.activate(); @@ -1096,7 +1100,7 @@ export class RoslynLanguageServer { await exports.setupTelemetryEnvironmentAsync(env); } - getComponentPaths('roslynCopilot', languageServerOptions).forEach((extPath) => { + getComponentPaths('roslynCopilot', languageServerOptions, channel).forEach((extPath) => { additionalExtensionPaths.push(extPath); }); } diff --git a/src/main.ts b/src/main.ts index 977e415f5f..434e2e9c52 100644 --- a/src/main.ts +++ b/src/main.ts @@ -17,7 +17,7 @@ import { vscodeNetworkSettingsProvider } from './networkSettings'; import createOptionStream from './shared/observables/createOptionStream'; import { AbsolutePathPackage } from './packageManager/absolutePathPackage'; import { downloadAndInstallPackages } from './packageManager/downloadAndInstallPackages'; -import IInstallDependencies from './packageManager/IInstallDependencies'; +import { IInstallDependencies } from './packageManager/IInstallDependencies'; import { installRuntimeDependencies } from './installRuntimeDependencies'; import { isValidDownload } from './packageManager/isValidDownload'; import { MigrateOptions } from './shared/migrateOptions'; @@ -86,7 +86,7 @@ export async function activate( const networkSettingsProvider = vscodeNetworkSettingsProvider(vscode); const useFramework = useOmnisharpServer && omnisharpOptions.useModernNet !== true; const installDependencies: IInstallDependencies = async (dependencies: AbsolutePathPackage[]) => - downloadAndInstallPackages(dependencies, networkSettingsProvider, eventStream, isValidDownload); + downloadAndInstallPackages(dependencies, networkSettingsProvider, eventStream, isValidDownload, reporter); const runtimeDependenciesExist = await installRuntimeDependencies( context.extension.packageJSON, @@ -119,7 +119,7 @@ export async function activate( } else { const getCoreClrDebugPromise = async (languageServerStartedPromise: Promise) => { let coreClrDebugPromise = Promise.resolve(); - if (runtimeDependenciesExist) { + if (runtimeDependenciesExist['Debugger']) { // activate coreclr-debug coreClrDebugPromise = coreclrdebug.activate( context.extension, diff --git a/src/omnisharp/omnisharpDownloader.ts b/src/omnisharp/omnisharpDownloader.ts index 03be20748f..dd0ea5bab0 100644 --- a/src/omnisharp/omnisharpDownloader.ts +++ b/src/omnisharp/omnisharpDownloader.ts @@ -19,6 +19,7 @@ import { getRuntimeDependenciesPackages } from '../tools/runtimeDependencyPackag import { getAbsolutePathPackagesToInstall } from '../packageManager/getAbsolutePathPackagesToInstall'; import { isValidDownload } from '../packageManager/isValidDownload'; import { LatestBuildDownloadStart } from './omnisharpLoggingEvents'; +import { ITelemetryReporter } from '../shared/telemetryReporter'; export class OmnisharpDownloader { public constructor( @@ -26,7 +27,8 @@ export class OmnisharpDownloader { private eventStream: EventStream, private packageJSON: any, private platformInfo: PlatformInformation, - private extensionPath: string + private extensionPath: string, + private reporter?: ITelemetryReporter ) {} public async DownloadAndInstallOmnisharp( @@ -51,14 +53,17 @@ export class OmnisharpDownloader { if (packagesToInstall.length > 0) { this.eventStream.post(new PackageInstallation(`OmniSharp Version = ${version}`)); this.eventStream.post(new LogPlatformInfo(this.platformInfo)); - if ( - await downloadAndInstallPackages( - packagesToInstall, - this.networkSettingsProvider, - this.eventStream, - isValidDownload - ) - ) { + const installationResults = await downloadAndInstallPackages( + packagesToInstall, + this.networkSettingsProvider, + this.eventStream, + isValidDownload, + this.reporter + ); + const failedPackages = Object.entries(installationResults) + .filter(([, installed]) => !installed) + .map(([name]) => name); + if (failedPackages.length === 0) { this.eventStream.post(new InstallationSuccess()); return true; } diff --git a/src/omnisharp/omnisharpLanguageServer.ts b/src/omnisharp/omnisharpLanguageServer.ts index 619a10ba1a..1c5325c328 100644 --- a/src/omnisharp/omnisharpLanguageServer.ts +++ b/src/omnisharp/omnisharpLanguageServer.ts @@ -154,7 +154,8 @@ export async function activateOmniSharpLanguageServer( eventStream, context.extension.packageJSON, platformInfo, - context.extension.extensionPath + context.extension.extensionPath, + reporter ); await razorOmnisharpDownloader.DownloadAndInstallRazorOmnisharp( @@ -178,7 +179,8 @@ export async function activateOmniSharpLanguageServer( networkSettingsProvider, eventStream, context.extension.extensionPath, - omnisharpChannel + omnisharpChannel, + reporter ); } @@ -189,7 +191,8 @@ async function activate( provider: NetworkSettingsProvider, eventStream: EventStream, extensionPath: string, - outputChannel: vscode.OutputChannel + outputChannel: vscode.OutputChannel, + reporter: ITelemetryReporter ) { const disposables = new CompositeDisposable(); @@ -211,7 +214,8 @@ async function activate( omnisharpDotnetResolver, context, outputChannel, - languageMiddlewareFeature + languageMiddlewareFeature, + reporter ); const advisor = new Advisor(server); // create before server is started const testManager = new TestManager(server, eventStream, languageMiddlewareFeature); diff --git a/src/omnisharp/server.ts b/src/omnisharp/server.ts index bc60e6f8b8..49a2cc879d 100644 --- a/src/omnisharp/server.ts +++ b/src/omnisharp/server.ts @@ -34,6 +34,7 @@ import TestManager from './features/dotnetTest'; import { findLaunchTargets } from './launcher'; import { ProjectConfigurationMessage } from '../shared/projectConfiguration'; import { commonOptions, omnisharpOptions, razorOptions } from '../shared/options'; +import { ITelemetryReporter } from '../shared/telemetryReporter'; enum ServerState { Starting, @@ -117,14 +118,16 @@ export class OmniSharpServer { private dotnetResolver: IHostExecutableResolver, private context: ExtensionContext, private outputChannel: OutputChannel, - private languageMiddlewareFeature: LanguageMiddlewareFeature + private languageMiddlewareFeature: LanguageMiddlewareFeature, + reporter: ITelemetryReporter ) { const downloader = new OmnisharpDownloader( networkSettingsProvider, this.eventStream, this.packageJSON, platformInfo, - extensionPath + extensionPath, + reporter ); this._omnisharpManager = new OmnisharpManager(downloader, platformInfo); this.updateProjectDebouncer.pipe(debounceTime(1500)).subscribe(async (_) => { diff --git a/src/packageManager/IInstallDependencies.ts b/src/packageManager/IInstallDependencies.ts index 6ca5a476f1..d4c12b3cfc 100644 --- a/src/packageManager/IInstallDependencies.ts +++ b/src/packageManager/IInstallDependencies.ts @@ -5,6 +5,8 @@ import { AbsolutePathPackage } from './absolutePathPackage'; -export default interface IInstallDependencies { - (packages: AbsolutePathPackage[]): Promise; +export type DependencyInstallationStatus = { [name: string]: boolean }; + +export interface IInstallDependencies { + (packages: AbsolutePathPackage[]): Promise; } diff --git a/src/packageManager/downloadAndInstallPackages.ts b/src/packageManager/downloadAndInstallPackages.ts index a148a666e4..da39e0e352 100644 --- a/src/packageManager/downloadAndInstallPackages.ts +++ b/src/packageManager/downloadAndInstallPackages.ts @@ -16,15 +16,19 @@ import { mkdirpSync } from 'fs-extra'; import { PackageInstallStart } from '../shared/loggingEvents'; import { DownloadValidator } from './isValidDownload'; import { CancellationToken } from 'vscode'; +import { ITelemetryReporter } from '../shared/telemetryReporter'; +import { DependencyInstallationStatus } from './IInstallDependencies'; export async function downloadAndInstallPackages( packages: AbsolutePathPackage[], provider: NetworkSettingsProvider, eventStream: EventStream, downloadValidator: DownloadValidator, + telemetryReporter?: ITelemetryReporter, token?: CancellationToken -): Promise { +): Promise { eventStream.post(new PackageInstallStart()); + const results: DependencyInstallationStatus = {}; for (const pkg of packages) { let installationStage = 'touchBeginFile'; try { @@ -48,12 +52,16 @@ export async function downloadAndInstallPackages( await InstallZip(buffer, pkg.description, pkg.installPath, pkg.binaries, eventStream); installationStage = 'touchLockFile'; await touchInstallFile(pkg.installPath, InstallFileType.Lock); + results[pkg.id] = true; break; } else { eventStream.post(new IntegrityCheckFailure(pkg.description, pkg.url, willTryInstallingPackage())); + results[pkg.id] = false; } } } catch (error) { + results[pkg.id] = false; + if (error instanceof NestedError) { const packageError = new PackageError(error.message, pkg, error.err); eventStream.post(new InstallationFailure(installationStage, packageError)); @@ -61,7 +69,8 @@ export async function downloadAndInstallPackages( eventStream.post(new InstallationFailure(installationStage, error)); } - return false; + // Send telemetry for the failure + sendInstallationFailureTelemetry(pkg, installationStage, error); } finally { try { if (await installFileExists(pkg.installPath, InstallFileType.Begin)) { @@ -73,5 +82,26 @@ export async function downloadAndInstallPackages( } } - return true; + return results; + + function sendInstallationFailureTelemetry(pkg: AbsolutePathPackage, installationStage: string, error: any): void { + if (!telemetryReporter) { + return; + } + + const telemetryProperties: { [key: string]: string } = { + installStage: installationStage, + packageId: pkg.id, + }; + + if (error instanceof NestedError && error.err instanceof PackageError) { + telemetryProperties['error.message'] = error.err.message; + telemetryProperties['error.packageUrl'] = error.err.pkg.url; + } else if (error instanceof PackageError) { + telemetryProperties['error.message'] = error.message; + telemetryProperties['error.packageUrl'] = error.pkg.url; + } + + telemetryReporter.sendTelemetryEvent('PackageInstallationFailed', telemetryProperties); + } } diff --git a/src/razor/razorOmnisharpDownloader.ts b/src/razor/razorOmnisharpDownloader.ts index 965f7cf99a..83ffd4edc5 100644 --- a/src/razor/razorOmnisharpDownloader.ts +++ b/src/razor/razorOmnisharpDownloader.ts @@ -11,6 +11,7 @@ import { downloadAndInstallPackages } from '../packageManager/downloadAndInstall import { getRuntimeDependenciesPackages } from '../tools/runtimeDependencyPackageUtils'; import { getAbsolutePathPackagesToInstall } from '../packageManager/getAbsolutePathPackagesToInstall'; import { isValidDownload } from '../packageManager/isValidDownload'; +import { ITelemetryReporter } from '../shared/telemetryReporter'; export class RazorOmnisharpDownloader { public constructor( @@ -18,7 +19,8 @@ export class RazorOmnisharpDownloader { private eventStream: EventStream, private packageJSON: any, private platformInfo: PlatformInformation, - private extensionPath: string + private extensionPath: string, + private reporter?: ITelemetryReporter ) {} public async DownloadAndInstallRazorOmnisharp(version: string): Promise { @@ -33,14 +35,17 @@ export class RazorOmnisharpDownloader { if (packagesToInstall.length > 0) { this.eventStream.post(new PackageInstallation(`Razor OmniSharp Version = ${version}`)); this.eventStream.post(new LogPlatformInfo(this.platformInfo)); - if ( - await downloadAndInstallPackages( - packagesToInstall, - this.networkSettingsProvider, - this.eventStream, - isValidDownload - ) - ) { + const installationResults = await downloadAndInstallPackages( + packagesToInstall, + this.networkSettingsProvider, + this.eventStream, + isValidDownload, + this.reporter + ); + const failedPackages = Object.entries(installationResults) + .filter(([, installed]) => !installed) + .map(([name]) => name); + if (failedPackages.length === 0) { this.eventStream.post(new InstallationSuccess()); return true; } diff --git a/src/razor/src/extension.ts b/src/razor/src/extension.ts index 8fdac6c67b..a2555ac0b7 100644 --- a/src/razor/src/extension.ts +++ b/src/razor/src/extension.ts @@ -104,7 +104,7 @@ export async function activate( await setupDevKitEnvironment(dotnetInfo.env, csharpDevkitExtension, logger); if (vscode.env.isTelemetryEnabled) { - const razorComponentPaths = getComponentPaths('razorDevKit', undefined); + const razorComponentPaths = getComponentPaths('razorDevKit', undefined, logger.outputChannel); if (razorComponentPaths.length !== 1) { logger.logError('Failed to find Razor DevKit telemetry extension path.', undefined); } else { diff --git a/tasks/offlinePackagingTasks.ts b/tasks/offlinePackagingTasks.ts index 531e0b7adf..af16c0e1a0 100644 --- a/tasks/offlinePackagingTasks.ts +++ b/tasks/offlinePackagingTasks.ts @@ -280,8 +280,19 @@ async function installPackageJsonDependency( codeExtensionPath ); const provider = () => new NetworkSettings('', true); - if (!(await downloadAndInstallPackages(packagesToInstall, provider, eventStream, isValidDownload, token))) { - throw Error('Failed to download package.'); + const installationResults = await downloadAndInstallPackages( + packagesToInstall, + provider, + eventStream, + isValidDownload, + undefined, + token + ); + const failedPackages = Object.entries(installationResults) + .filter(([, installed]) => !installed) + .map(([name]) => name); + if (failedPackages.length > 0) { + throw Error('The following packages failed to install: ' + failedPackages.join(', ')); } } diff --git a/test/omnisharp/omnisharpUnitTests/installRuntimeDependencies.test.ts b/test/omnisharp/omnisharpUnitTests/installRuntimeDependencies.test.ts index e1a3b89f70..0ce27b8f82 100644 --- a/test/omnisharp/omnisharpUnitTests/installRuntimeDependencies.test.ts +++ b/test/omnisharp/omnisharpUnitTests/installRuntimeDependencies.test.ts @@ -5,7 +5,7 @@ import { describe, test, expect, beforeEach } from '@jest/globals'; import { installRuntimeDependencies } from '../../../src/installRuntimeDependencies'; -import IInstallDependencies from '../../../src/packageManager/IInstallDependencies'; +import { IInstallDependencies } from '../../../src/packageManager/IInstallDependencies'; import { EventStream } from '../../../src/eventStream'; import { PlatformInformation } from '../../../src/shared/platform'; import TestEventBus from './testAssets/testEventBus'; @@ -28,7 +28,8 @@ describe(`${installRuntimeDependencies.name}`, () => { beforeEach(() => { eventStream = new EventStream(); eventBus = new TestEventBus(eventStream); - installDependencies = async () => Promise.resolve(true); + installDependencies = async (packages) => + Promise.resolve(packages.reduce((acc, pkg) => ({ ...acc, [pkg.id]: true }), {})); }); describe('When all the dependencies already exist', () => { @@ -48,7 +49,9 @@ describe(`${installRuntimeDependencies.name}`, () => { useFramework, ['Debugger', 'Omnisharp', 'Razor'] ); - expect(installed).toBe(true); + expect(installed['Debugger']).toBe(true); + expect(installed['Omnisharp']).toBe(true); + expect(installed['Razor']).toBe(true); }); test("Doesn't log anything to the eventStream", async () => { @@ -90,7 +93,7 @@ describe(`${installRuntimeDependencies.name}`, () => { let inputPackage: AbsolutePathPackage[]; installDependencies = async (packages) => { inputPackage = packages; - return Promise.resolve(true); + return Promise.resolve(packages.reduce((acc, pkg) => ({ ...acc, [pkg.id]: true }), {})); }; const installed = await installRuntimeDependencies( @@ -102,7 +105,7 @@ describe(`${installRuntimeDependencies.name}`, () => { useFramework, ['myPackage'] ); - expect(installed).toBe(true); + expect(installed['myPackage']).toBe(true); isNotNull(inputPackage!); expect(inputPackage).toHaveLength(1); expect(inputPackage[0]).toStrictEqual( @@ -111,7 +114,8 @@ describe(`${installRuntimeDependencies.name}`, () => { }); test('Returns false when installDependencies returns false', async () => { - installDependencies = async () => Promise.resolve(false); + installDependencies = async (packages) => + Promise.resolve(packages.reduce((acc, pkg) => ({ ...acc, [pkg.id]: false }), {})); const installed = await installRuntimeDependencies( packageJSON, extensionPath, @@ -121,7 +125,7 @@ describe(`${installRuntimeDependencies.name}`, () => { useFramework, ['myPackage'] ); - expect(installed).toBe(false); + expect(installed['myPackage']).toBe(false); }); }); });