Skip to content

Commit e77e14c

Browse files
authored
Out of band updates to the language server (#2615)
Fixes #2580
1 parent 6870c7c commit e77e14c

33 files changed

Lines changed: 1026 additions & 245 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ coverage/
1616
pythonFiles/experimental/ptvsd/**
1717
debug_coverage*/**
1818
languageServer/**
19+
languageServer.*/**
1920
bin/**
2021
obj/**
2122
.pytest_cache

.vscodeignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ yarn.lock
2929
.vscode/**
3030
.vscode-test/**
3131
languageServer/**
32+
languageServer.*/**
3233
bin/**
3334
BuildOutput/**
3435
coverage/**

news/1 Enhancements/2580.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for out of band updates to the language server.

package-lock.json

Lines changed: 91 additions & 73 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1543,6 +1543,7 @@
15431543
},
15441544
"dependencies": {
15451545
"arch": "2.1.0",
1546+
"azure-storage": "2.10.1",
15461547
"diff-match-patch": "1.0.0",
15471548
"dotenv": "5.0.1",
15481549
"fs-extra": "4.0.3",
@@ -1608,7 +1609,6 @@
16081609
"@types/winreg": "^1.2.30",
16091610
"@types/xml2js": "^0.4.2",
16101611
"JSONStream": "^1.3.2",
1611-
"azure-storage": "^2.8.1",
16121612
"chai": "^4.1.2",
16131613
"chai-arrays": "^2.0.0",
16141614
"chai-as-promised": "^7.1.1",

src/client/activation/downloader.ts

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,48 +8,42 @@ import * as requestProgress from 'request-progress';
88
import { ProgressLocation, window } from 'vscode';
99
import { createDeferred } from '../../utils/async';
1010
import { StopWatch } from '../../utils/stopWatch';
11+
import { STANDARD_OUTPUT_CHANNEL } from '../common/constants';
1112
import { IFileSystem } from '../common/platform/types';
1213
import { IExtensionContext, IOutputChannel } from '../common/types';
14+
import { IServiceContainer } from '../ioc/types';
1315
import { sendTelemetryEvent } from '../telemetry';
1416
import {
1517
PYTHON_LANGUAGE_SERVER_DOWNLOADED,
1618
PYTHON_LANGUAGE_SERVER_EXTRACTED
1719
} from '../telemetry/constants';
18-
import { PlatformData, PlatformName } from './platformData';
19-
import { IDownloadFileService } from './types';
20+
import { PlatformData } from './platformData';
21+
import { IHttpClient, ILanguageServerDownloader, ILanguageServerFolderService } from './types';
2022

2123
// tslint:disable-next-line:no-require-imports no-var-requires
2224
const StreamZip = require('node-stream-zip');
23-
24-
const downloadUriPrefix = 'https://pvsc.blob.core.windows.net/python-language-server';
25-
const downloadBaseFileName = 'Python-Language-Server';
26-
const downloadVersion = 'beta';
2725
const downloadFileExtension = '.nupkg';
2826

29-
export const DownloadLinks = {
30-
[PlatformName.Windows32Bit]: `${downloadUriPrefix}/${downloadBaseFileName}-${PlatformName.Windows32Bit}.${downloadVersion}${downloadFileExtension}`,
31-
[PlatformName.Windows64Bit]: `${downloadUriPrefix}/${downloadBaseFileName}-${PlatformName.Windows64Bit}.${downloadVersion}${downloadFileExtension}`,
32-
[PlatformName.Linux64Bit]: `${downloadUriPrefix}/${downloadBaseFileName}-${PlatformName.Linux64Bit}.${downloadVersion}${downloadFileExtension}`,
33-
[PlatformName.Mac64Bit]: `${downloadUriPrefix}/${downloadBaseFileName}-${PlatformName.Mac64Bit}.${downloadVersion}${downloadFileExtension}`
34-
};
35-
36-
export class LanguageServerDownloader {
37-
27+
export class LanguageServerDownloader implements ILanguageServerDownloader {
28+
private readonly output: IOutputChannel;
29+
private readonly fs: IFileSystem;
3830
constructor(
39-
private readonly output: IOutputChannel,
40-
private readonly fs: IFileSystem,
4131
private readonly platformData: PlatformData,
42-
private requestHandler: IDownloadFileService,
43-
private engineFolder: string
44-
) { }
32+
private readonly engineFolder: string,
33+
private readonly serviceContainer: IServiceContainer
34+
) {
35+
this.output = this.serviceContainer.get<IOutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL);
36+
this.fs = this.serviceContainer.get<IFileSystem>(IFileSystem);
4537

46-
public getDownloadUri() {
47-
const platformString = this.platformData.getPlatformName();
48-
return DownloadLinks[platformString];
38+
}
39+
40+
public async getDownloadUri() {
41+
const lsFolderService = this.serviceContainer.get<ILanguageServerFolderService>(ILanguageServerFolderService);
42+
return lsFolderService.getLatestLanguageServerVersion().then(info => info!.uri);
4943
}
5044

5145
public async downloadLanguageServer(context: IExtensionContext): Promise<void> {
52-
const downloadUri = this.getDownloadUri();
46+
const downloadUri = await this.getDownloadUri();
5347
const timer: StopWatch = new StopWatch();
5448
let success: boolean = true;
5549
let localTempFilePath = '';
@@ -103,9 +97,8 @@ export class LanguageServerDownloader {
10397
await window.withProgress({
10498
location: ProgressLocation.Window
10599
}, (progress) => {
106-
107-
requestProgress(
108-
this.requestHandler!.downloadFile(uri))
100+
const httpClient = this.serviceContainer.get<IHttpClient>(IHttpClient);
101+
requestProgress(httpClient.downloadFile(uri))
109102
.on('progress', (state) => {
110103
// https://www.npmjs.com/package/request-progress
111104
const received = Math.round(state.size.transferred / 1024);
@@ -152,15 +145,15 @@ export class LanguageServerDownloader {
152145
if (!await this.fs.directoryExists(installFolder)) {
153146
await this.fs.createDirectory(installFolder);
154147
}
155-
zip.extract(null, installFolder, (err, count) => {
148+
zip.extract(null, installFolder, (err) => {
156149
if (err) {
157150
deferred.reject(err);
158151
} else {
159152
deferred.resolve();
160153
}
161154
zip.close();
162155
});
163-
}).on('extract', (entry, file) => {
156+
}).on('extract', () => {
164157
extractedFiles += 1;
165158
progress.report({ message: `${title}${Math.round(100 * extractedFiles / totalFiles)}%` });
166159
}).on('error', e => {

src/client/activation/languageServer.ts

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import { isTestExecution, STANDARD_OUTPUT_CHANNEL } from '../common/constants';
2424
import { IFileSystem, IPlatformService } from '../common/platform/types';
2525
import {
2626
BANNER_NAME_LS_SURVEY, DeprecatedFeatureInfo, IConfigurationService,
27-
IExtensionContext, IFeatureDeprecationManager, ILogger, IOutputChannel,
28-
IPathUtils, IPythonExtensionBanner, IPythonSettings
27+
IDisposableRegistry, IExtensionContext, IFeatureDeprecationManager, ILogger,
28+
IOutputChannel, IPathUtils, IPythonExtensionBanner, IPythonSettings
2929
} from '../common/types';
3030
import { IEnvironmentVariablesProvider } from '../common/variables/types';
3131
import { IServiceContainer } from '../ioc/types';
@@ -40,13 +40,11 @@ import { LanguageServerDownloader } from './downloader';
4040
import { InterpreterData, InterpreterDataService } from './interpreterDataService';
4141
import { PlatformData } from './platformData';
4242
import { ProgressReporting } from './progress';
43-
import { RequestWithProxy } from './requestWithProxy';
44-
import { IExtensionActivator } from './types';
43+
import { IExtensionActivator, ILanguageServerFolderService } from './types';
4544

4645
const PYTHON = 'python';
4746
const dotNetCommand = 'dotnet';
4847
const languageClientName = 'Python Tools';
49-
const languageServerFolder = 'languageServer';
5048
const loadExtensionCommand = 'python._loadLanguageServerExtension';
5149
const buildSymbolsCmdDeprecatedInfo: DeprecatedFeatureInfo = {
5250
doNotDisplayPromptStateKey: 'SHOW_DEPRECATED_FEATURE_PROMPT_BUILD_WORKSPACE_SYMBOLS',
@@ -75,8 +73,8 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
7573
private typeshedPaths: string[] = [];
7674
private loadExtensionArgs: {} | undefined;
7775
private surveyBanner: IPythonExtensionBanner;
78-
// tslint:disable-next-line:no-unused-variable
79-
private progressReporting: ProgressReporting | undefined;
76+
private languageServerFolder!: string;
77+
private languageServerFolderService: ILanguageServerFolderService;
8078

8179
constructor(@inject(IServiceContainer) private readonly services: IServiceContainer) {
8280
this.context = this.services.get<IExtensionContext>(IExtensionContext);
@@ -86,6 +84,7 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
8684
this.fs = this.services.get<IFileSystem>(IFileSystem);
8785
this.platformData = new PlatformData(services.get<IPlatformService>(IPlatformService), this.fs);
8886
this.workspace = this.services.get<IWorkspaceService>(IWorkspaceService);
87+
this.languageServerFolderService = this.services.get<ILanguageServerFolderService>(ILanguageServerFolderService);
8988
const deprecationManager: IFeatureDeprecationManager =
9089
this.services.get<IFeatureDeprecationManager>(IFeatureDeprecationManager);
9190

@@ -116,6 +115,7 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
116115

117116
public async activate(): Promise<boolean> {
118117
this.sw.reset();
118+
this.languageServerFolder = await this.languageServerFolderService.getLanguageServerFolderName();
119119
const clientOptions = await this.getAnalysisOptions();
120120
if (!clientOptions) {
121121
return false;
@@ -155,18 +155,13 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
155155
return true;
156156
}
157157

158-
const mscorlib = path.join(this.context.extensionPath, languageServerFolder, 'mscorlib.dll');
158+
const mscorlib = path.join(this.context.extensionPath, this.languageServerFolder, 'mscorlib.dll');
159159
if (!await this.fs.fileExists(mscorlib)) {
160-
const downloader = new LanguageServerDownloader(
161-
this.output,
162-
this.fs,
163-
this.platformData,
164-
new RequestWithProxy(this.workspace.getConfiguration('http').get('proxy', '')),
165-
languageServerFolder);
160+
const downloader = new LanguageServerDownloader(this.platformData, this.languageServerFolder, this.services);
166161
await downloader.downloadLanguageServer(this.context);
167162
}
168163

169-
const serverModule = path.join(this.context.extensionPath, languageServerFolder, this.platformData.getEngineExecutableName());
164+
const serverModule = path.join(this.context.extensionPath, this.languageServerFolder, this.platformData.getEngineExecutableName());
170165
this.languageClient = this.createSelfContainedLanguageClient(serverModule, clientOptions);
171166
try {
172167
await this.startLanguageClient();
@@ -181,7 +176,9 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
181176
private async startLanguageClient(): Promise<void> {
182177
this.context.subscriptions.push(this.languageClient!.start());
183178
await this.serverReady();
184-
this.progressReporting = new ProgressReporting(this.languageClient!);
179+
const disposables = this.services.get<Disposable[]>(IDisposableRegistry);
180+
const progressReporting = new ProgressReporting(this.languageClient!);
181+
disposables.push(progressReporting);
185182
}
186183

187184
private async serverReady(): Promise<void> {
@@ -197,7 +194,7 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
197194

198195
private createSimpleLanguageClient(clientOptions: LanguageClientOptions): LanguageClient {
199196
const commandOptions = { stdio: 'pipe' };
200-
const serverModule = path.join(this.context.extensionPath, languageServerFolder, this.platformData.getEngineDllName());
197+
const serverModule = path.join(this.context.extensionPath, this.languageServerFolder, this.platformData.getEngineDllName());
201198
const serverOptions: ServerOptions = {
202199
run: { command: dotNetCommand, args: [serverModule], options: commandOptions },
203200
debug: { command: dotNetCommand, args: [serverModule, '--debug'], options: commandOptions }
@@ -240,7 +237,7 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
240237
}
241238

242239
// tslint:disable-next-line:no-string-literal
243-
properties['DatabasePath'] = path.join(this.context.extensionPath, languageServerFolder);
240+
properties['DatabasePath'] = path.join(this.context.extensionPath, this.languageServerFolder);
244241

245242
let searchPaths = interpreterData ? interpreterData.searchPaths.split(path.delimiter) : [];
246243
const settings = this.configuration.getSettings();
@@ -310,8 +307,7 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
310307
this.getVsCodeExcludeSection('search.exclude', list);
311308
this.getVsCodeExcludeSection('files.exclude', list);
312309
this.getVsCodeExcludeSection('files.watcherExclude', list);
313-
this.getPythonExcludeSection('linting.ignorePatterns', list);
314-
this.getPythonExcludeSection('workspaceSymbols.exclusionPattern', list);
310+
this.getPythonExcludeSection(list);
315311
return list;
316312
}
317313

@@ -324,7 +320,7 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
324320
}
325321
}
326322

327-
private getPythonExcludeSection(setting: string, list: string[]): void {
323+
private getPythonExcludeSection(list: string[]): void {
328324
const pythonSettings = this.configuration.getSettings(this.root);
329325
const paths = pythonSettings && pythonSettings.linting ? pythonSettings.linting.ignorePatterns : undefined;
330326
if (paths && Array.isArray(paths)) {
@@ -337,7 +333,7 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
337333
private getTypeshedPaths(settings: IPythonSettings): string[] {
338334
return settings.analysis.typeshedPaths && settings.analysis.typeshedPaths.length > 0
339335
? settings.analysis.typeshedPaths
340-
: [path.join(this.context.extensionPath, 'languageServer', 'Typeshed')];
336+
: [path.join(this.context.extensionPath, this.languageServerFolder, 'Typeshed')];
341337
}
342338

343339
private async onSettingsChanged(): Promise<void> {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { inject, injectable } from 'inversify';
7+
import * as path from 'path';
8+
import * as semver from 'semver';
9+
import { EXTENSION_ROOT_DIR } from '../common/constants';
10+
import { NugetPackage } from '../common/nuget/types';
11+
import { IFileSystem } from '../common/platform/types';
12+
import { IConfigurationService, ILogger } from '../common/types';
13+
import { IServiceContainer } from '../ioc/types';
14+
import { FolderVersionPair, ILanguageServerFolderService, ILanguageServerPackageService } from './types';
15+
16+
const languageServerFolder = 'languageServer';
17+
18+
@injectable()
19+
export class LanguageServerFolderService implements ILanguageServerFolderService {
20+
constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { }
21+
22+
public async getLanguageServerFolderName(): Promise<string> {
23+
const currentFolder = await this.getcurrentLanguageServerDirectory();
24+
let serverVersion: NugetPackage | undefined;
25+
26+
const configService = this.serviceContainer.get<IConfigurationService>(IConfigurationService);
27+
if (currentFolder && !configService.getSettings().autoUpdateLanguageServer) {
28+
return path.basename(currentFolder.path);
29+
}
30+
31+
serverVersion = await this.getLatestLanguageServerVersion()
32+
.catch(ex => {
33+
const logger = this.serviceContainer.get<ILogger>(ILogger);
34+
logger.logError('Failed to get latest version of Language Server.', ex);
35+
return undefined;
36+
});
37+
38+
if (currentFolder && (!serverVersion || serverVersion.version.compare(currentFolder.version) <= 0)) {
39+
return path.basename(currentFolder.path);
40+
}
41+
42+
return `${languageServerFolder}.${serverVersion!.version.raw}`;
43+
}
44+
public getLatestLanguageServerVersion(): Promise<NugetPackage | undefined> {
45+
const lsPackageService = this.serviceContainer.get<ILanguageServerPackageService>(ILanguageServerPackageService);
46+
return lsPackageService.getLatestNugetPackageVersion();
47+
}
48+
public async getcurrentLanguageServerDirectory(): Promise<FolderVersionPair | undefined> {
49+
const dirs = await this.getExistingLanguageServerDirectories();
50+
if (dirs.length === 0) {
51+
return;
52+
}
53+
const sortedDirs = dirs.sort((a, b) => a.version.compare(b.version));
54+
return sortedDirs[sortedDirs.length - 1];
55+
}
56+
public async getExistingLanguageServerDirectories(): Promise<FolderVersionPair[]> {
57+
const fs = this.serviceContainer.get<IFileSystem>(IFileSystem);
58+
const subDirs = await fs.getSubDirectories(EXTENSION_ROOT_DIR);
59+
return subDirs
60+
.filter(dir => path.basename(dir).startsWith(languageServerFolder))
61+
.map(dir => { return { path: dir, version: this.getFolderVersion(path.basename(dir)) }; });
62+
}
63+
64+
public getFolderVersion(dirName: string): semver.SemVer {
65+
const suffix = dirName.substring(languageServerFolder.length + 1);
66+
return suffix.length === 0 ? new semver.SemVer('0.0.0') : (semver.parse(suffix, true) || new semver.SemVer('0.0.0'));
67+
}
68+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { inject, injectable } from 'inversify';
7+
import { Architecture, OSType } from '../../utils/platform';
8+
import { INugetRepository, INugetService, NugetPackage } from '../common/nuget/types';
9+
import { IPlatformService } from '../common/platform/types';
10+
import { IServiceContainer } from '../ioc/types';
11+
import { PlatformName } from './platformData';
12+
import { ILanguageServerPackageService } from './types';
13+
14+
const downloadBaseFileName = 'Python-Language-Server';
15+
export const maxMajorVersion = 0;
16+
export const PackageNames = {
17+
[PlatformName.Windows32Bit]: `${downloadBaseFileName}-${PlatformName.Windows32Bit}`,
18+
[PlatformName.Windows64Bit]: `${downloadBaseFileName}-${PlatformName.Windows64Bit}`,
19+
[PlatformName.Linux64Bit]: `${downloadBaseFileName}-${PlatformName.Linux64Bit}`,
20+
[PlatformName.Mac64Bit]: `${downloadBaseFileName}-${PlatformName.Mac64Bit}`
21+
};
22+
23+
@injectable()
24+
export class LanguageServerPackageService implements ILanguageServerPackageService {
25+
public maxMajorVersion: number = maxMajorVersion;
26+
constructor(@inject(IServiceContainer) protected readonly serviceContainer: IServiceContainer) { }
27+
public getNugetPackageName(): string {
28+
const plaform = this.serviceContainer.get<IPlatformService>(IPlatformService);
29+
switch (plaform.info.type) {
30+
case OSType.Windows: {
31+
const is64Bit = plaform.info.architecture === Architecture.x64;
32+
return PackageNames[is64Bit ? PlatformName.Windows64Bit : PlatformName.Windows32Bit];
33+
}
34+
case OSType.OSX: {
35+
return PackageNames[PlatformName.Mac64Bit];
36+
}
37+
default: {
38+
return PackageNames[PlatformName.Linux64Bit];
39+
}
40+
}
41+
}
42+
43+
public async getLatestNugetPackageVersion(): Promise<NugetPackage> {
44+
const nugetRepo = this.serviceContainer.get<INugetRepository>(INugetRepository);
45+
const nugetService = this.serviceContainer.get<INugetService>(INugetService);
46+
const packageName = this.getNugetPackageName();
47+
const packages = await nugetRepo.getPackages(packageName);
48+
49+
const validPackages = packages
50+
.filter(item => item.version.major === this.maxMajorVersion)
51+
.filter(item => nugetService.isReleaseVersion(item.version))
52+
.sort((a, b) => a.version.compare(b.version));
53+
return validPackages[validPackages.length - 1];
54+
}
55+
}

0 commit comments

Comments
 (0)