Skip to content

Commit bcc4d29

Browse files
authored
Support optional components (#8703)
2 parents 66e15dd + ef03174 commit bcc4d29

File tree

13 files changed

+159
-64
lines changed

13 files changed

+159
-64
lines changed

src/installRuntimeDependencies.ts

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { PackageInstallation, LogPlatformInfo, InstallationSuccess } from './sha
88
import { EventStream } from './eventStream';
99
import { getRuntimeDependenciesPackages } from './tools/runtimeDependencyPackageUtils';
1010
import { getAbsolutePathPackagesToInstall } from './packageManager/getAbsolutePathPackagesToInstall';
11-
import IInstallDependencies from './packageManager/IInstallDependencies';
11+
import { DependencyInstallationStatus, IInstallDependencies } from './packageManager/IInstallDependencies';
1212
import { AbsolutePathPackage } from './packageManager/absolutePathPackage';
1313

1414
export async function installRuntimeDependencies(
@@ -19,26 +19,39 @@ export async function installRuntimeDependencies(
1919
platformInfo: PlatformInformation,
2020
useFramework: boolean,
2121
requiredPackageIds: string[]
22-
): Promise<boolean> {
22+
): Promise<DependencyInstallationStatus> {
2323
const runTimeDependencies = getRuntimeDependenciesPackages(packageJSON);
2424
const packagesToInstall = await getAbsolutePathPackagesToInstall(runTimeDependencies, platformInfo, extensionPath);
25+
26+
// PackagesToInstall will only return packages that are not already installed. However,
27+
// we need to return the installation status of all required packages, so we need to
28+
// track which required packages are already installed, so that we can return true for them.
29+
const installedPackages = requiredPackageIds.filter(
30+
(id) => packagesToInstall.find((pkg) => pkg.id === id) === undefined
31+
);
32+
const installedPackagesResults = installedPackages.reduce((acc, id) => ({ ...acc, [id]: true }), {});
33+
2534
const filteredPackages = filterOmniSharpPackage(packagesToInstall, useFramework);
2635
const filteredRequiredPackages = filteredRequiredPackage(requiredPackageIds, filteredPackages);
2736

28-
if (filteredRequiredPackages.length > 0) {
29-
eventStream.post(new PackageInstallation('C# dependencies'));
30-
// Display platform information and RID
31-
eventStream.post(new LogPlatformInfo(platformInfo));
37+
if (filteredRequiredPackages.length === 0) {
38+
return installedPackagesResults;
39+
}
40+
41+
eventStream.post(new PackageInstallation('C# dependencies'));
42+
// Display platform information and RID
43+
eventStream.post(new LogPlatformInfo(platformInfo));
44+
45+
const installationResults = await installDependencies(filteredRequiredPackages);
3246

33-
if (await installDependencies(filteredRequiredPackages)) {
34-
eventStream.post(new InstallationSuccess());
35-
} else {
36-
return false;
37-
}
47+
const failedPackages = Object.entries(installationResults)
48+
.filter(([, installed]) => !installed)
49+
.map(([name]) => name);
50+
if (failedPackages.length === 0) {
51+
eventStream.post(new InstallationSuccess());
3852
}
3953

40-
//All the required packages are already downloaded and installed
41-
return true;
54+
return { ...installedPackagesResults, ...installationResults };
4255
}
4356

4457
function filterOmniSharpPackage(packages: AbsolutePathPackage[], useFramework: boolean) {

src/lsptoolshost/extensions/builtInComponents.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import * as vscode from 'vscode';
67
import * as fs from 'fs';
78
import * as path from 'path';
89
import { LanguageServerOptions } from '../../shared/options';
@@ -11,6 +12,7 @@ interface ComponentInfo {
1112
defaultFolderName: string;
1213
optionName: string;
1314
componentDllPaths: string[];
15+
isOptional?: boolean;
1416
}
1517

1618
export const componentInfo: { [key: string]: ComponentInfo } = {
@@ -41,15 +43,27 @@ export const componentInfo: { [key: string]: ComponentInfo } = {
4143
defaultFolderName: '.roslynCopilot',
4244
optionName: 'roslynCopilot',
4345
componentDllPaths: ['Microsoft.VisualStudio.Copilot.Roslyn.LanguageServer.dll'],
46+
isOptional: true,
4447
},
4548
};
4649

47-
export function getComponentPaths(componentName: string, options: LanguageServerOptions | undefined): string[] {
50+
export function getComponentPaths(
51+
componentName: string,
52+
options: LanguageServerOptions | undefined,
53+
channel?: vscode.LogOutputChannel
54+
): string[] {
4855
const component = componentInfo[componentName];
4956
const baseFolder = getComponentFolderPath(component, options);
5057
const paths = component.componentDllPaths.map((dllPath) => path.join(baseFolder, dllPath));
5158
for (const dllPath of paths) {
5259
if (!fs.existsSync(dllPath)) {
60+
if (component.isOptional) {
61+
// Component is optional and doesn't exist - log warning and return empty array
62+
if (channel) {
63+
channel.warn(`Optional component '${componentName}' could not be found at '${dllPath}'.`);
64+
}
65+
return [];
66+
}
5367
throw new Error(`Component DLL not found: ${dllPath}`);
5468
}
5569
}

src/lsptoolshost/server/roslynLanguageServer.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,7 @@ export class RoslynLanguageServer {
651651
: razorOptions.razorServerPath;
652652

653653
let razorComponentPath = '';
654-
getComponentPaths('razorExtension', languageServerOptions).forEach((extPath) => {
654+
getComponentPaths('razorExtension', languageServerOptions, channel).forEach((extPath) => {
655655
additionalExtensionPaths.push(extPath);
656656
razorComponentPath = path.dirname(extPath);
657657
});
@@ -695,10 +695,10 @@ export class RoslynLanguageServer {
695695
// Set command enablement as soon as we know devkit is available.
696696
await vscode.commands.executeCommand('setContext', 'dotnet.server.activationContext', 'RoslynDevKit');
697697

698-
const csharpDevKitArgs = this.getCSharpDevKitExportArgs(additionalExtensionPaths);
698+
const csharpDevKitArgs = this.getCSharpDevKitExportArgs(additionalExtensionPaths, channel);
699699
args = args.concat(csharpDevKitArgs);
700700

701-
await this.setupDevKitEnvironment(dotnetInfo.env, csharpDevkitExtension, additionalExtensionPaths);
701+
await this.setupDevKitEnvironment(dotnetInfo.env, csharpDevkitExtension, additionalExtensionPaths, channel);
702702
} else {
703703
// C# Dev Kit is not installed - continue C#-only activation.
704704
channel.info('Activating C# standalone...');
@@ -1012,10 +1012,13 @@ export class RoslynLanguageServer {
10121012
);
10131013
}
10141014

1015-
private static getCSharpDevKitExportArgs(additionalExtensionPaths: string[]): string[] {
1015+
private static getCSharpDevKitExportArgs(
1016+
additionalExtensionPaths: string[],
1017+
channel: vscode.LogOutputChannel
1018+
): string[] {
10161019
const args: string[] = [];
10171020

1018-
const devKitDepsPath = getComponentPaths('roslynDevKit', languageServerOptions);
1021+
const devKitDepsPath = getComponentPaths('roslynDevKit', languageServerOptions, channel);
10191022
if (devKitDepsPath.length > 1) {
10201023
throw new Error('Expected only one devkit deps path');
10211024
}
@@ -1026,7 +1029,7 @@ export class RoslynLanguageServer {
10261029

10271030
// Also include the Xaml Dev Kit extensions, if enabled.
10281031
if (languageServerOptions.enableXamlTools) {
1029-
getComponentPaths('xamlTools', languageServerOptions).forEach((path) =>
1032+
getComponentPaths('xamlTools', languageServerOptions, channel).forEach((path) =>
10301033
additionalExtensionPaths.push(path)
10311034
);
10321035
}
@@ -1086,7 +1089,8 @@ export class RoslynLanguageServer {
10861089
private static async setupDevKitEnvironment(
10871090
env: NodeJS.ProcessEnv,
10881091
csharpDevkitExtension: vscode.Extension<CSharpDevKitExports>,
1089-
additionalExtensionPaths: string[]
1092+
additionalExtensionPaths: string[],
1093+
channel: vscode.LogOutputChannel
10901094
): Promise<void> {
10911095
const exports: CSharpDevKitExports = await csharpDevkitExtension.activate();
10921096

@@ -1096,7 +1100,7 @@ export class RoslynLanguageServer {
10961100
await exports.setupTelemetryEnvironmentAsync(env);
10971101
}
10981102

1099-
getComponentPaths('roslynCopilot', languageServerOptions).forEach((extPath) => {
1103+
getComponentPaths('roslynCopilot', languageServerOptions, channel).forEach((extPath) => {
11001104
additionalExtensionPaths.push(extPath);
11011105
});
11021106
}

src/main.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { vscodeNetworkSettingsProvider } from './networkSettings';
1717
import createOptionStream from './shared/observables/createOptionStream';
1818
import { AbsolutePathPackage } from './packageManager/absolutePathPackage';
1919
import { downloadAndInstallPackages } from './packageManager/downloadAndInstallPackages';
20-
import IInstallDependencies from './packageManager/IInstallDependencies';
20+
import { IInstallDependencies } from './packageManager/IInstallDependencies';
2121
import { installRuntimeDependencies } from './installRuntimeDependencies';
2222
import { isValidDownload } from './packageManager/isValidDownload';
2323
import { MigrateOptions } from './shared/migrateOptions';
@@ -86,7 +86,7 @@ export async function activate(
8686
const networkSettingsProvider = vscodeNetworkSettingsProvider(vscode);
8787
const useFramework = useOmnisharpServer && omnisharpOptions.useModernNet !== true;
8888
const installDependencies: IInstallDependencies = async (dependencies: AbsolutePathPackage[]) =>
89-
downloadAndInstallPackages(dependencies, networkSettingsProvider, eventStream, isValidDownload);
89+
downloadAndInstallPackages(dependencies, networkSettingsProvider, eventStream, isValidDownload, reporter);
9090

9191
const runtimeDependenciesExist = await installRuntimeDependencies(
9292
context.extension.packageJSON,
@@ -119,7 +119,7 @@ export async function activate(
119119
} else {
120120
const getCoreClrDebugPromise = async (languageServerStartedPromise: Promise<void>) => {
121121
let coreClrDebugPromise = Promise.resolve();
122-
if (runtimeDependenciesExist) {
122+
if (runtimeDependenciesExist['Debugger']) {
123123
// activate coreclr-debug
124124
coreClrDebugPromise = coreclrdebug.activate(
125125
context.extension,

src/omnisharp/omnisharpDownloader.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,16 @@ import { getRuntimeDependenciesPackages } from '../tools/runtimeDependencyPackag
1919
import { getAbsolutePathPackagesToInstall } from '../packageManager/getAbsolutePathPackagesToInstall';
2020
import { isValidDownload } from '../packageManager/isValidDownload';
2121
import { LatestBuildDownloadStart } from './omnisharpLoggingEvents';
22+
import { ITelemetryReporter } from '../shared/telemetryReporter';
2223

2324
export class OmnisharpDownloader {
2425
public constructor(
2526
private networkSettingsProvider: NetworkSettingsProvider,
2627
private eventStream: EventStream,
2728
private packageJSON: any,
2829
private platformInfo: PlatformInformation,
29-
private extensionPath: string
30+
private extensionPath: string,
31+
private reporter?: ITelemetryReporter
3032
) {}
3133

3234
public async DownloadAndInstallOmnisharp(
@@ -51,14 +53,17 @@ export class OmnisharpDownloader {
5153
if (packagesToInstall.length > 0) {
5254
this.eventStream.post(new PackageInstallation(`OmniSharp Version = ${version}`));
5355
this.eventStream.post(new LogPlatformInfo(this.platformInfo));
54-
if (
55-
await downloadAndInstallPackages(
56-
packagesToInstall,
57-
this.networkSettingsProvider,
58-
this.eventStream,
59-
isValidDownload
60-
)
61-
) {
56+
const installationResults = await downloadAndInstallPackages(
57+
packagesToInstall,
58+
this.networkSettingsProvider,
59+
this.eventStream,
60+
isValidDownload,
61+
this.reporter
62+
);
63+
const failedPackages = Object.entries(installationResults)
64+
.filter(([, installed]) => !installed)
65+
.map(([name]) => name);
66+
if (failedPackages.length === 0) {
6267
this.eventStream.post(new InstallationSuccess());
6368
return true;
6469
}

src/omnisharp/omnisharpLanguageServer.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@ export async function activateOmniSharpLanguageServer(
154154
eventStream,
155155
context.extension.packageJSON,
156156
platformInfo,
157-
context.extension.extensionPath
157+
context.extension.extensionPath,
158+
reporter
158159
);
159160

160161
await razorOmnisharpDownloader.DownloadAndInstallRazorOmnisharp(
@@ -178,7 +179,8 @@ export async function activateOmniSharpLanguageServer(
178179
networkSettingsProvider,
179180
eventStream,
180181
context.extension.extensionPath,
181-
omnisharpChannel
182+
omnisharpChannel,
183+
reporter
182184
);
183185
}
184186

@@ -189,7 +191,8 @@ async function activate(
189191
provider: NetworkSettingsProvider,
190192
eventStream: EventStream,
191193
extensionPath: string,
192-
outputChannel: vscode.OutputChannel
194+
outputChannel: vscode.OutputChannel,
195+
reporter: ITelemetryReporter
193196
) {
194197
const disposables = new CompositeDisposable();
195198

@@ -211,7 +214,8 @@ async function activate(
211214
omnisharpDotnetResolver,
212215
context,
213216
outputChannel,
214-
languageMiddlewareFeature
217+
languageMiddlewareFeature,
218+
reporter
215219
);
216220
const advisor = new Advisor(server); // create before server is started
217221
const testManager = new TestManager(server, eventStream, languageMiddlewareFeature);

src/omnisharp/server.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import TestManager from './features/dotnetTest';
3434
import { findLaunchTargets } from './launcher';
3535
import { ProjectConfigurationMessage } from '../shared/projectConfiguration';
3636
import { commonOptions, omnisharpOptions, razorOptions } from '../shared/options';
37+
import { ITelemetryReporter } from '../shared/telemetryReporter';
3738

3839
enum ServerState {
3940
Starting,
@@ -117,14 +118,16 @@ export class OmniSharpServer {
117118
private dotnetResolver: IHostExecutableResolver,
118119
private context: ExtensionContext,
119120
private outputChannel: OutputChannel,
120-
private languageMiddlewareFeature: LanguageMiddlewareFeature
121+
private languageMiddlewareFeature: LanguageMiddlewareFeature,
122+
reporter: ITelemetryReporter
121123
) {
122124
const downloader = new OmnisharpDownloader(
123125
networkSettingsProvider,
124126
this.eventStream,
125127
this.packageJSON,
126128
platformInfo,
127-
extensionPath
129+
extensionPath,
130+
reporter
128131
);
129132
this._omnisharpManager = new OmnisharpManager(downloader, platformInfo);
130133
this.updateProjectDebouncer.pipe(debounceTime(1500)).subscribe(async (_) => {

src/packageManager/IInstallDependencies.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import { AbsolutePathPackage } from './absolutePathPackage';
77

8-
export default interface IInstallDependencies {
9-
(packages: AbsolutePathPackage[]): Promise<boolean>;
8+
export type DependencyInstallationStatus = { [name: string]: boolean };
9+
10+
export interface IInstallDependencies {
11+
(packages: AbsolutePathPackage[]): Promise<DependencyInstallationStatus>;
1012
}

src/packageManager/downloadAndInstallPackages.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,19 @@ import { mkdirpSync } from 'fs-extra';
1616
import { PackageInstallStart } from '../shared/loggingEvents';
1717
import { DownloadValidator } from './isValidDownload';
1818
import { CancellationToken } from 'vscode';
19+
import { ITelemetryReporter } from '../shared/telemetryReporter';
20+
import { DependencyInstallationStatus } from './IInstallDependencies';
1921

2022
export async function downloadAndInstallPackages(
2123
packages: AbsolutePathPackage[],
2224
provider: NetworkSettingsProvider,
2325
eventStream: EventStream,
2426
downloadValidator: DownloadValidator,
27+
telemetryReporter?: ITelemetryReporter,
2528
token?: CancellationToken
26-
): Promise<boolean> {
29+
): Promise<DependencyInstallationStatus> {
2730
eventStream.post(new PackageInstallStart());
31+
const results: DependencyInstallationStatus = {};
2832
for (const pkg of packages) {
2933
let installationStage = 'touchBeginFile';
3034
try {
@@ -48,20 +52,25 @@ export async function downloadAndInstallPackages(
4852
await InstallZip(buffer, pkg.description, pkg.installPath, pkg.binaries, eventStream);
4953
installationStage = 'touchLockFile';
5054
await touchInstallFile(pkg.installPath, InstallFileType.Lock);
55+
results[pkg.id] = true;
5156
break;
5257
} else {
5358
eventStream.post(new IntegrityCheckFailure(pkg.description, pkg.url, willTryInstallingPackage()));
59+
results[pkg.id] = false;
5460
}
5561
}
5662
} catch (error) {
63+
results[pkg.id] = false;
64+
5765
if (error instanceof NestedError) {
5866
const packageError = new PackageError(error.message, pkg, error.err);
5967
eventStream.post(new InstallationFailure(installationStage, packageError));
6068
} else {
6169
eventStream.post(new InstallationFailure(installationStage, error));
6270
}
6371

64-
return false;
72+
// Send telemetry for the failure
73+
sendInstallationFailureTelemetry(pkg, installationStage, error);
6574
} finally {
6675
try {
6776
if (await installFileExists(pkg.installPath, InstallFileType.Begin)) {
@@ -73,5 +82,26 @@ export async function downloadAndInstallPackages(
7382
}
7483
}
7584

76-
return true;
85+
return results;
86+
87+
function sendInstallationFailureTelemetry(pkg: AbsolutePathPackage, installationStage: string, error: any): void {
88+
if (!telemetryReporter) {
89+
return;
90+
}
91+
92+
const telemetryProperties: { [key: string]: string } = {
93+
installStage: installationStage,
94+
packageId: pkg.id,
95+
};
96+
97+
if (error instanceof NestedError && error.err instanceof PackageError) {
98+
telemetryProperties['error.message'] = error.err.message;
99+
telemetryProperties['error.packageUrl'] = error.err.pkg.url;
100+
} else if (error instanceof PackageError) {
101+
telemetryProperties['error.message'] = error.message;
102+
telemetryProperties['error.packageUrl'] = error.pkg.url;
103+
}
104+
105+
telemetryReporter.sendTelemetryEvent('PackageInstallationFailed', telemetryProperties);
106+
}
77107
}

0 commit comments

Comments
 (0)