Allow LS to be bundled, versioned (#12034)

Co-authored-by: Eric Snow <ericsnowcurrently@gmail.com>
This commit is contained in:
Jake Bailey 2020-06-01 13:19:53 -07:00 коммит произвёл GitHub
Родитель b859a3de88
Коммит e42fc1e7bc
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
14 изменённых файлов: 283 добавлений и 11 удалений

2
.gitignore поставляемый
Просмотреть файл

@ -42,4 +42,4 @@ ptvsd*.log
pydevd*.log
nodeLanguageServer/**
nodeLanguageServer.*/**
bundledLanguageServer/**

Просмотреть файл

@ -58,6 +58,8 @@ webpack.datascience-*.config.js
.vscode test/**
languageServer/**
languageServer.*/**
nodeLanguageServer/**
nodeLanguageServer.*/**
bin/**
build/**
BuildOutput/**

Просмотреть файл

@ -14,6 +14,7 @@ import { createDeferred } from '../../common/utils/async';
import { Common, LanguageService } from '../../common/utils/localize';
import { StopWatch } from '../../common/utils/stopWatch';
import { IServiceContainer } from '../../ioc/types';
import { traceError } from '../../logging';
import { sendTelemetryEvent } from '../../telemetry';
import { EventName } from '../../telemetry/constants';
import {
@ -58,6 +59,12 @@ export class LanguageServerDownloader implements ILanguageServerDownloader {
}
public async downloadLanguageServer(destinationFolder: string, resource: Resource): Promise<void> {
if (this.lsFolderService.isBundled()) {
// Sanity check; a bundled LS should never be downloaded.
traceError('Attempted to download bundled language server');
return;
}
const [downloadUri, lsVersion, lsName] = await this.getDownloadInfo(resource);
const timer: StopWatch = new StopWatch();
let success: boolean = true;

Просмотреть файл

@ -26,6 +26,10 @@ export abstract class LanguageServerFolderService implements ILanguageServerFold
@unmanaged() protected readonly languageServerFolder: string
) {}
public isBundled(): boolean {
return false;
}
@traceDecorators.verbose('Get language server folder name')
public async getLanguageServerFolderName(resource: Resource): Promise<string> {
const currentFolder = await this.getCurrentLanguageServerDirectory();

Просмотреть файл

@ -10,7 +10,7 @@ import { LanguageServerFolderService } from '../common/languageServerFolderServi
import { DotNetLanguageServerFolder } from '../types';
// Must match languageServerVersion* keys in package.json
const DotNetLanguageServerMinVersionKey = 'languageServerVersion';
export const DotNetLanguageServerMinVersionKey = 'languageServerVersion';
@injectable()
export class DotNetLanguageServerFolderService extends LanguageServerFolderService {

Просмотреть файл

@ -4,13 +4,25 @@
'use strict';
import { inject, injectable } from 'inversify';
import * as semver from 'semver';
import { IApplicationEnvironment, IWorkspaceService } from '../../common/application/types';
import { NugetPackage } from '../../common/nuget/types';
import { IConfigurationService, Resource } from '../../common/types';
import { IServiceContainer } from '../../ioc/types';
import { traceWarning } from '../../logging';
import { LanguageServerFolderService } from '../common/languageServerFolderService';
import { NodeLanguageServerFolder } from '../types';
import {
BundledLanguageServerFolder,
FolderVersionPair,
ILanguageServerFolderService,
NodeLanguageServerFolder
} from '../types';
@injectable()
export class NodeLanguageServerFolderService extends LanguageServerFolderService {
constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
// Must match languageServerVersion* keys in package.json
export const NodeLanguageServerVersionKey = 'languageServerVersionV2';
class FallbackNodeLanguageServerFolderService extends LanguageServerFolderService {
constructor(serviceContainer: IServiceContainer) {
super(serviceContainer, NodeLanguageServerFolder);
}
@ -18,3 +30,63 @@ export class NodeLanguageServerFolderService extends LanguageServerFolderService
return '0.0.0';
}
}
@injectable()
export class NodeLanguageServerFolderService implements ILanguageServerFolderService {
private readonly _bundledVersion: semver.SemVer | undefined;
private readonly fallback: FallbackNodeLanguageServerFolderService;
constructor(
@inject(IServiceContainer) serviceContainer: IServiceContainer,
@inject(IConfigurationService) configService: IConfigurationService,
@inject(IWorkspaceService) workspaceService: IWorkspaceService,
@inject(IApplicationEnvironment) appEnv: IApplicationEnvironment
) {
this.fallback = new FallbackNodeLanguageServerFolderService(serviceContainer);
// downloadLanguageServer is a bit of a misnomer; if false then this indicates that a local
// development copy should be run instead of a "real" build, telemetry discarded, etc.
// So, we require it to be true, even though in the bundled case no real download happens.
if (
configService.getSettings().downloadLanguageServer &&
!workspaceService.getConfiguration('python').get<string>('packageName')
) {
const ver = appEnv.packageJson[NodeLanguageServerVersionKey] as string;
this._bundledVersion = semver.parse(ver) || undefined;
if (this._bundledVersion === undefined) {
traceWarning(
`invalid language server version ${ver} in package.json (${NodeLanguageServerVersionKey})`
);
}
}
}
public get bundledVersion(): semver.SemVer | undefined {
return this._bundledVersion;
}
public isBundled(): boolean {
return this._bundledVersion !== undefined;
}
public async getLanguageServerFolderName(resource: Resource): Promise<string> {
if (this._bundledVersion) {
return BundledLanguageServerFolder;
}
return this.fallback.getLanguageServerFolderName(resource);
}
public async getLatestLanguageServerVersion(resource: Resource): Promise<NugetPackage | undefined> {
if (this._bundledVersion) {
return undefined;
}
return this.fallback.getLatestLanguageServerVersion(resource);
}
public async getCurrentLanguageServerDirectory(): Promise<FolderVersionPair | undefined> {
if (this._bundledVersion) {
return { path: BundledLanguageServerFolder, version: this._bundledVersion };
}
return this.fallback.getCurrentLanguageServerDirectory();
}
}

Просмотреть файл

@ -79,8 +79,8 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy {
options: LanguageClientOptions
): Promise<void> {
if (!this.languageClient) {
const lsVersion = await this.folderService.getLatestLanguageServerVersion(resource);
this.lsVersion = lsVersion?.version.format();
const directory = await this.folderService.getCurrentLanguageServerDirectory();
this.lsVersion = directory?.version.format();
this.cancellationStrategy = new FileBasedCancellationStrategy();
options.connectionOptions = { cancellationStrategy: this.cancellationStrategy };

Просмотреть файл

@ -72,6 +72,7 @@ export enum LanguageServerType {
export const DotNetLanguageServerFolder = 'languageServer';
export const NodeLanguageServerFolder = 'nodeLanguageServer';
export const BundledLanguageServerFolder = 'bundledLanguageServer';
// tslint:disable-next-line: interface-name
export interface DocumentHandler {
@ -116,6 +117,7 @@ export interface ILanguageServerFolderService {
getLanguageServerFolderName(resource: Resource): Promise<string>;
getLatestLanguageServerVersion(resource: Resource): Promise<NugetPackage | undefined>;
getCurrentLanguageServerDirectory(): Promise<FolderVersionPair | undefined>;
isBundled(): boolean;
}
export const ILanguageServerDownloader = Symbol('ILanguageServerDownloader');

Просмотреть файл

@ -260,12 +260,29 @@ suite('Language Server Activation - Downloader', () => {
throw failure;
}
}
class LanguageServeBundledTest extends LanguageServerDownloader {
// tslint:disable-next-line:no-unnecessary-override
public async downloadLanguageServer(destinationFolder: string, res?: Resource): Promise<void> {
return super.downloadLanguageServer(destinationFolder, res);
}
// tslint:disable-next-line:no-unnecessary-override
public async getDownloadInfo(_res?: Resource): Promise<string[]> {
throw failure;
}
public async downloadFile(): Promise<string> {
throw failure;
}
protected async unpackArchive(_extensionPath: string, _tempFilePath: string): Promise<void> {
throw failure;
}
}
let output: TypeMoq.IMock<IOutputChannel>;
let appShell: TypeMoq.IMock<IApplicationShell>;
let fs: TypeMoq.IMock<IFileSystem>;
let platformData: TypeMoq.IMock<IPlatformData>;
let languageServerDownloaderTest: LanguageServerDownloaderTest;
let languageServerExtractorTest: LanguageServerExtractorTest;
let languageServerBundledTest: LanguageServeBundledTest;
setup(() => {
appShell = TypeMoq.Mock.ofType<IApplicationShell>(undefined, TypeMoq.MockBehavior.Strict);
folderService = TypeMoq.Mock.ofType<ILanguageServerFolderService>(undefined, TypeMoq.MockBehavior.Strict);
@ -293,8 +310,18 @@ suite('Language Server Activation - Downloader', () => {
workspaceService.object,
undefined as any
);
languageServerBundledTest = new LanguageServeBundledTest(
lsOutputChannel.object,
undefined as any,
folderService.object,
appShell.object,
fs.object,
workspaceService.object,
undefined as any
);
});
test('Display error message if LS downloading fails', async () => {
folderService.setup((f) => f.isBundled()).returns(() => false);
const pkg = makePkgInfo('ls', 'xyz');
folderService.setup((f) => f.getLatestLanguageServerVersion(resource)).returns(() => Promise.resolve(pkg));
output.setup((o) => o.appendLine(LanguageService.downloadFailedOutputMessage()));
@ -318,6 +345,7 @@ suite('Language Server Activation - Downloader', () => {
platformData.verifyAll();
});
test('Display error message if LS extraction fails', async () => {
folderService.setup((f) => f.isBundled()).returns(() => false);
const pkg = makePkgInfo('ls', 'xyz');
folderService.setup((f) => f.getLatestLanguageServerVersion(resource)).returns(() => Promise.resolve(pkg));
output.setup((o) => o.appendLine(LanguageService.extractionFailedOutputMessage()));
@ -340,6 +368,17 @@ suite('Language Server Activation - Downloader', () => {
fs.verifyAll();
platformData.verifyAll();
});
test('No download if bundled', async () => {
folderService.setup((f) => f.isBundled()).returns(() => true);
await languageServerBundledTest.downloadLanguageServer('', resource);
folderService.verifyAll();
output.verifyAll();
appShell.verifyAll();
fs.verifyAll();
platformData.verifyAll();
});
});
});

Просмотреть файл

@ -267,4 +267,15 @@ suite('Language Server Folder Service', () => {
assert.deepEqual(result, expectedLSDirectory);
});
});
suite('Method isBundled()', () => {
setup(() => {
serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>();
languageServerFolderService = new DotNetLanguageServerFolderService(serviceContainer.object);
});
test('isBundled is false', () => {
expect(languageServerFolderService.isBundled()).to.be.equal(false, 'isBundled should be false');
});
});
});

Просмотреть файл

@ -8,6 +8,7 @@
import { expect } from 'chai';
import * as typeMoq from 'typemoq';
import { WorkspaceConfiguration } from 'vscode';
import { DotNetLanguageServerMinVersionKey } from '../../../client/activation/languageServer/languageServerFolderService';
import { DotNetLanguageServerPackageService } from '../../../client/activation/languageServer/languageServerPackageService';
import { IApplicationEnvironment, IWorkspaceService } from '../../../client/common/application/types';
import { AzureBlobStoreNugetRepository } from '../../../client/common/nuget/azureBlobStoreNugetRepository';
@ -44,7 +45,7 @@ suite('Language Server Package Service', () => {
);
serviceContainer.setup((c) => c.get(typeMoq.It.isValue(INugetRepository))).returns(() => nugetRepo);
const appEnv = typeMoq.Mock.ofType<IApplicationEnvironment>();
const packageJson = { languageServerVersion: '0.0.1' };
const packageJson = { [DotNetLanguageServerMinVersionKey]: '0.0.1' };
appEnv.setup((e) => e.packageJson).returns(() => packageJson);
const platform = typeMoq.Mock.ofType<IPlatformService>();
const lsPackageService = new DotNetLanguageServerPackageService(

Просмотреть файл

@ -12,6 +12,7 @@ import {
azureCDNBlobStorageAccount,
LanguageServerDownloadChannel
} from '../../../client/activation/common/packageRepository';
import { DotNetLanguageServerMinVersionKey } from '../../../client/activation/languageServer/languageServerFolderService';
import { DotNetLanguageServerPackageService } from '../../../client/activation/languageServer/languageServerPackageService';
import { PlatformName } from '../../../client/activation/types';
import { IApplicationEnvironment } from '../../../client/common/application/types';
@ -41,7 +42,7 @@ suite('Language Server - Package Service', () => {
lsPackageService.getLanguageServerDownloadChannel = () => 'stable';
});
function setMinVersionOfLs(version: string) {
const packageJson = { languageServerVersion: version };
const packageJson = { [DotNetLanguageServerMinVersionKey]: version };
appVersion.setup((e) => e.packageJson).returns(() => packageJson);
}
[true, false].forEach((is64Bit) => {

Просмотреть файл

@ -0,0 +1,132 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
'use strict';
import { assert, expect } from 'chai';
import * as TypeMoq from 'typemoq';
import { Uri, WorkspaceConfiguration } from 'vscode';
import {
NodeLanguageServerFolderService,
NodeLanguageServerVersionKey
} from '../../../client/activation/node/languageServerFolderService';
import { BundledLanguageServerFolder } from '../../../client/activation/types';
import { IApplicationEnvironment, IWorkspaceService } from '../../../client/common/application/types';
import { IConfigurationService, IPythonSettings } from '../../../client/common/types';
import { IServiceContainer } from '../../../client/ioc/types';
// tslint:disable:max-func-body-length
suite('Node Language Server Folder Service', () => {
const resource = Uri.parse('a');
const version = '0.0.1-test';
let serviceContainer: TypeMoq.IMock<IServiceContainer>;
let pythonSettings: TypeMoq.IMock<IPythonSettings>;
let configService: TypeMoq.IMock<IConfigurationService>;
let workspaceConfiguration: TypeMoq.IMock<WorkspaceConfiguration>;
let workspaceService: TypeMoq.IMock<IWorkspaceService>;
let appEnvironment: TypeMoq.IMock<IApplicationEnvironment>;
setup(() => {
serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>();
configService = TypeMoq.Mock.ofType<IConfigurationService>();
pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>();
configService.setup((c) => c.getSettings(undefined)).returns(() => pythonSettings.object);
workspaceConfiguration = TypeMoq.Mock.ofType<WorkspaceConfiguration>();
workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>();
workspaceService
.setup((ws) => ws.getConfiguration('python', TypeMoq.It.isAny()))
.returns(() => workspaceConfiguration.object);
appEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>();
});
test('With packageName set', () => {
pythonSettings.setup((p) => p.downloadLanguageServer).returns(() => true);
appEnvironment.setup((e) => e.packageJson).returns(() => ({ [NodeLanguageServerVersionKey]: version }));
workspaceConfiguration.setup((wc) => wc.get('packageName')).returns(() => 'somePackageName');
const folderService = new NodeLanguageServerFolderService(
serviceContainer.object,
configService.object,
workspaceService.object,
appEnvironment.object
);
expect(folderService.bundledVersion).to.be.equal(undefined, 'expected bundledVersion to be undefined');
expect(folderService.isBundled()).to.be.equal(false, 'isBundled should be false');
});
test('Invalid version', () => {
pythonSettings.setup((p) => p.downloadLanguageServer).returns(() => true);
appEnvironment.setup((e) => e.packageJson).returns(() => ({ [NodeLanguageServerVersionKey]: 'fakeversion' }));
workspaceConfiguration.setup((wc) => wc.get('packageName')).returns(() => undefined);
const folderService = new NodeLanguageServerFolderService(
serviceContainer.object,
configService.object,
workspaceService.object,
appEnvironment.object
);
expect(folderService.bundledVersion).to.be.equal(undefined, 'expected bundledVersion to be undefined');
expect(folderService.isBundled()).to.be.equal(false, 'isBundled should be false');
});
test('downloadLanguageServer set to false', () => {
pythonSettings.setup((p) => p.downloadLanguageServer).returns(() => false);
appEnvironment.setup((e) => e.packageJson).returns(() => ({ [NodeLanguageServerVersionKey]: 'fakeversion' }));
workspaceConfiguration.setup((wc) => wc.get('packageName')).returns(() => undefined);
const folderService = new NodeLanguageServerFolderService(
serviceContainer.object,
configService.object,
workspaceService.object,
appEnvironment.object
);
expect(folderService.bundledVersion).to.be.equal(undefined, 'expected bundledVersion to be undefined');
expect(folderService.isBundled()).to.be.equal(false, 'isBundled should be false');
});
suite('Valid configuration', () => {
let folderService: NodeLanguageServerFolderService;
setup(() => {
pythonSettings.setup((p) => p.downloadLanguageServer).returns(() => true);
appEnvironment.setup((e) => e.packageJson).returns(() => ({ [NodeLanguageServerVersionKey]: version }));
workspaceConfiguration.setup((wc) => wc.get('packageName')).returns(() => undefined);
folderService = new NodeLanguageServerFolderService(
serviceContainer.object,
configService.object,
workspaceService.object,
appEnvironment.object
);
});
test('isBundled is true', () => {
expect(folderService.isBundled()).to.be.equal(true, 'isBundled should be true');
});
test('Parsed version is correct', () => {
expect(folderService.bundledVersion!.format()).to.be.equal(version);
});
test('getLanguageServerFolderName', async () => {
const folderName = await folderService.getLanguageServerFolderName(resource);
expect(folderName).to.be.equal(BundledLanguageServerFolder);
});
test('getLatestLanguageServerVersion', async () => {
const pkg = await folderService.getLatestLanguageServerVersion(resource);
expect(pkg).to.equal(undefined, 'expected latest version to be undefined');
});
test('Method getCurrentLanguageServerDirectory()', async () => {
const dir = await folderService.getCurrentLanguageServerDirectory();
assert(dir);
expect(dir!.path).to.equal(BundledLanguageServerFolder);
expect(dir!.version.format()).to.be.equal(version);
});
});
});

Просмотреть файл

@ -7,6 +7,7 @@ import { expect } from 'chai';
import { SemVer } from 'semver';
import * as typeMoq from 'typemoq';
import { WorkspaceConfiguration } from 'vscode';
import { DotNetLanguageServerMinVersionKey } from '../../../client/activation/languageServer/languageServerFolderService';
import { DotNetLanguageServerPackageService } from '../../../client/activation/languageServer/languageServerPackageService';
import { IApplicationEnvironment, IWorkspaceService } from '../../../client/common/application/types';
import { AzureBlobStoreNugetRepository } from '../../../client/common/nuget/azureBlobStoreNugetRepository';
@ -53,7 +54,7 @@ suite('Nuget Azure Storage Repository', () => {
// tslint:disable-next-line:no-invalid-this
this.timeout(15000);
const platformService = new PlatformService();
const packageJson = { languageServerVersion: '0.0.1' };
const packageJson = { [DotNetLanguageServerMinVersionKey]: '0.0.1' };
const appEnv = typeMoq.Mock.ofType<IApplicationEnvironment>();
appEnv.setup((e) => e.packageJson).returns(() => packageJson);
const lsPackageService = new DotNetLanguageServerPackageService(