Skip to content

Commit

Permalink
Out of band updates to the language server (#2615)
Browse files Browse the repository at this point in the history
Fixes #2580
  • Loading branch information
DonJayamanne authored Sep 19, 2018
1 parent 6870c7c commit e77e14c
Show file tree
Hide file tree
Showing 33 changed files with 1,026 additions and 245 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ coverage/
pythonFiles/experimental/ptvsd/**
debug_coverage*/**
languageServer/**
languageServer.*/**
bin/**
obj/**
.pytest_cache
Expand Down
1 change: 1 addition & 0 deletions .vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ yarn.lock
.vscode/**
.vscode-test/**
languageServer/**
languageServer.*/**
bin/**
BuildOutput/**
coverage/**
Expand Down
1 change: 1 addition & 0 deletions news/1 Enhancements/2580.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for out of band updates to the language server.
164 changes: 91 additions & 73 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1543,6 +1543,7 @@
},
"dependencies": {
"arch": "2.1.0",
"azure-storage": "2.10.1",
"diff-match-patch": "1.0.0",
"dotenv": "5.0.1",
"fs-extra": "4.0.3",
Expand Down Expand Up @@ -1608,7 +1609,6 @@
"@types/winreg": "^1.2.30",
"@types/xml2js": "^0.4.2",
"JSONStream": "^1.3.2",
"azure-storage": "^2.8.1",
"chai": "^4.1.2",
"chai-arrays": "^2.0.0",
"chai-as-promised": "^7.1.1",
Expand Down
51 changes: 22 additions & 29 deletions src/client/activation/downloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,42 @@ import * as requestProgress from 'request-progress';
import { ProgressLocation, window } from 'vscode';
import { createDeferred } from '../../utils/async';
import { StopWatch } from '../../utils/stopWatch';
import { STANDARD_OUTPUT_CHANNEL } from '../common/constants';
import { IFileSystem } from '../common/platform/types';
import { IExtensionContext, IOutputChannel } from '../common/types';
import { IServiceContainer } from '../ioc/types';
import { sendTelemetryEvent } from '../telemetry';
import {
PYTHON_LANGUAGE_SERVER_DOWNLOADED,
PYTHON_LANGUAGE_SERVER_EXTRACTED
} from '../telemetry/constants';
import { PlatformData, PlatformName } from './platformData';
import { IDownloadFileService } from './types';
import { PlatformData } from './platformData';
import { IHttpClient, ILanguageServerDownloader, ILanguageServerFolderService } from './types';

// tslint:disable-next-line:no-require-imports no-var-requires
const StreamZip = require('node-stream-zip');

const downloadUriPrefix = 'https://pvsc.blob.core.windows.net/python-language-server';
const downloadBaseFileName = 'Python-Language-Server';
const downloadVersion = 'beta';
const downloadFileExtension = '.nupkg';

export const DownloadLinks = {
[PlatformName.Windows32Bit]: `${downloadUriPrefix}/${downloadBaseFileName}-${PlatformName.Windows32Bit}.${downloadVersion}${downloadFileExtension}`,
[PlatformName.Windows64Bit]: `${downloadUriPrefix}/${downloadBaseFileName}-${PlatformName.Windows64Bit}.${downloadVersion}${downloadFileExtension}`,
[PlatformName.Linux64Bit]: `${downloadUriPrefix}/${downloadBaseFileName}-${PlatformName.Linux64Bit}.${downloadVersion}${downloadFileExtension}`,
[PlatformName.Mac64Bit]: `${downloadUriPrefix}/${downloadBaseFileName}-${PlatformName.Mac64Bit}.${downloadVersion}${downloadFileExtension}`
};

export class LanguageServerDownloader {

export class LanguageServerDownloader implements ILanguageServerDownloader {
private readonly output: IOutputChannel;
private readonly fs: IFileSystem;
constructor(
private readonly output: IOutputChannel,
private readonly fs: IFileSystem,
private readonly platformData: PlatformData,
private requestHandler: IDownloadFileService,
private engineFolder: string
) { }
private readonly engineFolder: string,
private readonly serviceContainer: IServiceContainer
) {
this.output = this.serviceContainer.get<IOutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL);
this.fs = this.serviceContainer.get<IFileSystem>(IFileSystem);

public getDownloadUri() {
const platformString = this.platformData.getPlatformName();
return DownloadLinks[platformString];
}

public async getDownloadUri() {
const lsFolderService = this.serviceContainer.get<ILanguageServerFolderService>(ILanguageServerFolderService);
return lsFolderService.getLatestLanguageServerVersion().then(info => info!.uri);
}

public async downloadLanguageServer(context: IExtensionContext): Promise<void> {
const downloadUri = this.getDownloadUri();
const downloadUri = await this.getDownloadUri();
const timer: StopWatch = new StopWatch();
let success: boolean = true;
let localTempFilePath = '';
Expand Down Expand Up @@ -103,9 +97,8 @@ export class LanguageServerDownloader {
await window.withProgress({
location: ProgressLocation.Window
}, (progress) => {

requestProgress(
this.requestHandler!.downloadFile(uri))
const httpClient = this.serviceContainer.get<IHttpClient>(IHttpClient);
requestProgress(httpClient.downloadFile(uri))
.on('progress', (state) => {
// https://www.npmjs.com/package/request-progress
const received = Math.round(state.size.transferred / 1024);
Expand Down Expand Up @@ -152,15 +145,15 @@ export class LanguageServerDownloader {
if (!await this.fs.directoryExists(installFolder)) {
await this.fs.createDirectory(installFolder);
}
zip.extract(null, installFolder, (err, count) => {
zip.extract(null, installFolder, (err) => {
if (err) {
deferred.reject(err);
} else {
deferred.resolve();
}
zip.close();
});
}).on('extract', (entry, file) => {
}).on('extract', () => {
extractedFiles += 1;
progress.report({ message: `${title}${Math.round(100 * extractedFiles / totalFiles)}%` });
}).on('error', e => {
Expand Down
40 changes: 18 additions & 22 deletions src/client/activation/languageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import { isTestExecution, STANDARD_OUTPUT_CHANNEL } from '../common/constants';
import { IFileSystem, IPlatformService } from '../common/platform/types';
import {
BANNER_NAME_LS_SURVEY, DeprecatedFeatureInfo, IConfigurationService,
IExtensionContext, IFeatureDeprecationManager, ILogger, IOutputChannel,
IPathUtils, IPythonExtensionBanner, IPythonSettings
IDisposableRegistry, IExtensionContext, IFeatureDeprecationManager, ILogger,
IOutputChannel, IPathUtils, IPythonExtensionBanner, IPythonSettings
} from '../common/types';
import { IEnvironmentVariablesProvider } from '../common/variables/types';
import { IServiceContainer } from '../ioc/types';
Expand All @@ -40,13 +40,11 @@ import { LanguageServerDownloader } from './downloader';
import { InterpreterData, InterpreterDataService } from './interpreterDataService';
import { PlatformData } from './platformData';
import { ProgressReporting } from './progress';
import { RequestWithProxy } from './requestWithProxy';
import { IExtensionActivator } from './types';
import { IExtensionActivator, ILanguageServerFolderService } from './types';

const PYTHON = 'python';
const dotNetCommand = 'dotnet';
const languageClientName = 'Python Tools';
const languageServerFolder = 'languageServer';
const loadExtensionCommand = 'python._loadLanguageServerExtension';
const buildSymbolsCmdDeprecatedInfo: DeprecatedFeatureInfo = {
doNotDisplayPromptStateKey: 'SHOW_DEPRECATED_FEATURE_PROMPT_BUILD_WORKSPACE_SYMBOLS',
Expand Down Expand Up @@ -75,8 +73,8 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
private typeshedPaths: string[] = [];
private loadExtensionArgs: {} | undefined;
private surveyBanner: IPythonExtensionBanner;
// tslint:disable-next-line:no-unused-variable
private progressReporting: ProgressReporting | undefined;
private languageServerFolder!: string;
private languageServerFolderService: ILanguageServerFolderService;

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

Expand Down Expand Up @@ -116,6 +115,7 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {

public async activate(): Promise<boolean> {
this.sw.reset();
this.languageServerFolder = await this.languageServerFolderService.getLanguageServerFolderName();
const clientOptions = await this.getAnalysisOptions();
if (!clientOptions) {
return false;
Expand Down Expand Up @@ -155,18 +155,13 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
return true;
}

const mscorlib = path.join(this.context.extensionPath, languageServerFolder, 'mscorlib.dll');
const mscorlib = path.join(this.context.extensionPath, this.languageServerFolder, 'mscorlib.dll');
if (!await this.fs.fileExists(mscorlib)) {
const downloader = new LanguageServerDownloader(
this.output,
this.fs,
this.platformData,
new RequestWithProxy(this.workspace.getConfiguration('http').get('proxy', '')),
languageServerFolder);
const downloader = new LanguageServerDownloader(this.platformData, this.languageServerFolder, this.services);
await downloader.downloadLanguageServer(this.context);
}

const serverModule = path.join(this.context.extensionPath, languageServerFolder, this.platformData.getEngineExecutableName());
const serverModule = path.join(this.context.extensionPath, this.languageServerFolder, this.platformData.getEngineExecutableName());
this.languageClient = this.createSelfContainedLanguageClient(serverModule, clientOptions);
try {
await this.startLanguageClient();
Expand All @@ -181,7 +176,9 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
private async startLanguageClient(): Promise<void> {
this.context.subscriptions.push(this.languageClient!.start());
await this.serverReady();
this.progressReporting = new ProgressReporting(this.languageClient!);
const disposables = this.services.get<Disposable[]>(IDisposableRegistry);
const progressReporting = new ProgressReporting(this.languageClient!);
disposables.push(progressReporting);
}

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

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

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

let searchPaths = interpreterData ? interpreterData.searchPaths.split(path.delimiter) : [];
const settings = this.configuration.getSettings();
Expand Down Expand Up @@ -310,8 +307,7 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
this.getVsCodeExcludeSection('search.exclude', list);
this.getVsCodeExcludeSection('files.exclude', list);
this.getVsCodeExcludeSection('files.watcherExclude', list);
this.getPythonExcludeSection('linting.ignorePatterns', list);
this.getPythonExcludeSection('workspaceSymbols.exclusionPattern', list);
this.getPythonExcludeSection(list);
return list;
}

Expand All @@ -324,7 +320,7 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
}
}

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

private async onSettingsChanged(): Promise<void> {
Expand Down
68 changes: 68 additions & 0 deletions src/client/activation/languageServerFolderService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { inject, injectable } from 'inversify';
import * as path from 'path';
import * as semver from 'semver';
import { EXTENSION_ROOT_DIR } from '../common/constants';
import { NugetPackage } from '../common/nuget/types';
import { IFileSystem } from '../common/platform/types';
import { IConfigurationService, ILogger } from '../common/types';
import { IServiceContainer } from '../ioc/types';
import { FolderVersionPair, ILanguageServerFolderService, ILanguageServerPackageService } from './types';

const languageServerFolder = 'languageServer';

@injectable()
export class LanguageServerFolderService implements ILanguageServerFolderService {
constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { }

public async getLanguageServerFolderName(): Promise<string> {
const currentFolder = await this.getcurrentLanguageServerDirectory();
let serverVersion: NugetPackage | undefined;

const configService = this.serviceContainer.get<IConfigurationService>(IConfigurationService);
if (currentFolder && !configService.getSettings().autoUpdateLanguageServer) {
return path.basename(currentFolder.path);
}

serverVersion = await this.getLatestLanguageServerVersion()
.catch(ex => {
const logger = this.serviceContainer.get<ILogger>(ILogger);
logger.logError('Failed to get latest version of Language Server.', ex);
return undefined;
});

if (currentFolder && (!serverVersion || serverVersion.version.compare(currentFolder.version) <= 0)) {
return path.basename(currentFolder.path);
}

return `${languageServerFolder}.${serverVersion!.version.raw}`;
}
public getLatestLanguageServerVersion(): Promise<NugetPackage | undefined> {
const lsPackageService = this.serviceContainer.get<ILanguageServerPackageService>(ILanguageServerPackageService);
return lsPackageService.getLatestNugetPackageVersion();
}
public async getcurrentLanguageServerDirectory(): Promise<FolderVersionPair | undefined> {
const dirs = await this.getExistingLanguageServerDirectories();
if (dirs.length === 0) {
return;
}
const sortedDirs = dirs.sort((a, b) => a.version.compare(b.version));
return sortedDirs[sortedDirs.length - 1];
}
public async getExistingLanguageServerDirectories(): Promise<FolderVersionPair[]> {
const fs = this.serviceContainer.get<IFileSystem>(IFileSystem);
const subDirs = await fs.getSubDirectories(EXTENSION_ROOT_DIR);
return subDirs
.filter(dir => path.basename(dir).startsWith(languageServerFolder))
.map(dir => { return { path: dir, version: this.getFolderVersion(path.basename(dir)) }; });
}

public getFolderVersion(dirName: string): semver.SemVer {
const suffix = dirName.substring(languageServerFolder.length + 1);
return suffix.length === 0 ? new semver.SemVer('0.0.0') : (semver.parse(suffix, true) || new semver.SemVer('0.0.0'));
}
}
55 changes: 55 additions & 0 deletions src/client/activation/languageServerPackageService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { inject, injectable } from 'inversify';
import { Architecture, OSType } from '../../utils/platform';
import { INugetRepository, INugetService, NugetPackage } from '../common/nuget/types';
import { IPlatformService } from '../common/platform/types';
import { IServiceContainer } from '../ioc/types';
import { PlatformName } from './platformData';
import { ILanguageServerPackageService } from './types';

const downloadBaseFileName = 'Python-Language-Server';
export const maxMajorVersion = 0;
export const PackageNames = {
[PlatformName.Windows32Bit]: `${downloadBaseFileName}-${PlatformName.Windows32Bit}`,
[PlatformName.Windows64Bit]: `${downloadBaseFileName}-${PlatformName.Windows64Bit}`,
[PlatformName.Linux64Bit]: `${downloadBaseFileName}-${PlatformName.Linux64Bit}`,
[PlatformName.Mac64Bit]: `${downloadBaseFileName}-${PlatformName.Mac64Bit}`
};

@injectable()
export class LanguageServerPackageService implements ILanguageServerPackageService {
public maxMajorVersion: number = maxMajorVersion;
constructor(@inject(IServiceContainer) protected readonly serviceContainer: IServiceContainer) { }
public getNugetPackageName(): string {
const plaform = this.serviceContainer.get<IPlatformService>(IPlatformService);
switch (plaform.info.type) {
case OSType.Windows: {
const is64Bit = plaform.info.architecture === Architecture.x64;
return PackageNames[is64Bit ? PlatformName.Windows64Bit : PlatformName.Windows32Bit];
}
case OSType.OSX: {
return PackageNames[PlatformName.Mac64Bit];
}
default: {
return PackageNames[PlatformName.Linux64Bit];
}
}
}

public async getLatestNugetPackageVersion(): Promise<NugetPackage> {
const nugetRepo = this.serviceContainer.get<INugetRepository>(INugetRepository);
const nugetService = this.serviceContainer.get<INugetService>(INugetService);
const packageName = this.getNugetPackageName();
const packages = await nugetRepo.getPackages(packageName);

const validPackages = packages
.filter(item => item.version.major === this.maxMajorVersion)
.filter(item => nugetService.isReleaseVersion(item.version))
.sort((a, b) => a.version.compare(b.version));
return validPackages[validPackages.length - 1];
}
}
Loading

0 comments on commit e77e14c

Please sign in to comment.