Skip to content

Commit f18a5ee

Browse files
author
Mikhail Arkhipov
authored
PTVS engine update + handling of the interpreter change (microsoft#1613)
* Undo changes * Test fixes * Increase timeout * Remove double event listening * Remove test * Revert "Remove test" This reverts commit e240c3f. * Revert "Remove double event listening" This reverts commit af573be. * microsoft#1096 The if statement is automatically formatted incorrectly * Merge fix * Add more tests * More tests * Typo * Test * Also better handle multiline arguments * Add a couple missing periods [skip ci] * Undo changes * Test fixes * Increase timeout * Remove double event listening * Remove test * Revert "Remove test" This reverts commit e240c3f. * Revert "Remove double event listening" This reverts commit af573be. * Merge fix * microsoft#1257 On type formatting errors for args and kwargs * Handle f-strings * Stop importing from test code * microsoft#1308 Single line statements leading to an indentation on the next line * microsoft#726 editing python after inline if statement invalid indent * Undo change * Move constant * Harden LS startup error checks * microsoft#1364 Intellisense doesn't work after specific const string * Telemetry for the analysis enging * PR feedback * Fix typo * Test baseline update * Jedi 0.12 * Priority to goto_defition * News * Replace unzip * Linux flavors + test * Grammar check * Grammar test * Test baselines * Add news * Pin dependency [skip ci] * Specify markdown as preferable format * Improve function argument detection * Specify markdown * Pythia setting * Baseline updates * Baseline update * Improve startup * Handle missing interpreter better * Handle interpreter change * Delete old file * Fix LS startup time reporting * Remove Async suffix from IFileSystem
1 parent 2324754 commit f18a5ee

40 files changed

+316
-198
lines changed

package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,6 +1232,18 @@
12321232
"description": "Whether to install Python modules globally when not using an environment.",
12331233
"scope": "resource"
12341234
},
1235+
"python.pythiaEnabled": {
1236+
"type": "boolean",
1237+
"default": true,
1238+
"description": "Enables AI-driven additions to the completion list. Does not apply to Jedi.",
1239+
"scope": "resource"
1240+
},
1241+
"python.jediEnabled": {
1242+
"type": "boolean",
1243+
"default": true,
1244+
"description": "Enables Jedi as IntelliSense engine instead of Microsoft Python Analysis Engine.",
1245+
"scope": "resource"
1246+
},
12351247
"python.jediMemoryLimit": {
12361248
"type": "number",
12371249
"default": 0,

src/client/activation/analysis.ts

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { IProcessServiceFactory } from '../common/process/types';
1313
import { StopWatch } from '../common/stopWatch';
1414
import { IConfigurationService, IOutputChannel, IPythonSettings } from '../common/types';
1515
import { IEnvironmentVariablesProvider } from '../common/variables/types';
16+
import { IInterpreterService } from '../interpreter/contracts';
1617
import { IServiceContainer } from '../ioc/types';
1718
import {
1819
PYTHON_ANALYSIS_ENGINE_DOWNLOADED,
@@ -35,11 +36,11 @@ class LanguageServerStartupErrorHandler implements ErrorHandler {
3536
constructor(private readonly deferred: Deferred<void>) { }
3637
public error(error: Error, message: Message, count: number): ErrorAction {
3738
this.deferred.reject(error);
38-
return ErrorAction.Shutdown;
39+
return ErrorAction.Continue;
3940
}
4041
public closed(): CloseAction {
4142
this.deferred.reject();
42-
return CloseAction.DoNotRestart;
43+
return CloseAction.Restart;
4344
}
4445
}
4546

@@ -50,39 +51,66 @@ export class AnalysisExtensionActivator implements IExtensionActivator {
5051
private readonly fs: IFileSystem;
5152
private readonly sw = new StopWatch();
5253
private readonly platformData: PlatformData;
54+
private readonly interpreterService: IInterpreterService;
55+
private readonly disposables: Disposable[] = [];
5356
private languageClient: LanguageClient | undefined;
57+
private context: ExtensionContext | undefined;
58+
private interpreterHash: string = '';
5459

5560
constructor(private readonly services: IServiceContainer, pythonSettings: IPythonSettings) {
5661
this.configuration = this.services.get<IConfigurationService>(IConfigurationService);
5762
this.appShell = this.services.get<IApplicationShell>(IApplicationShell);
5863
this.output = this.services.get<OutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL);
5964
this.fs = this.services.get<IFileSystem>(IFileSystem);
6065
this.platformData = new PlatformData(services.get<IPlatformService>(IPlatformService), this.fs);
66+
this.interpreterService = this.services.get<IInterpreterService>(IInterpreterService);
6167
}
6268

6369
public async activate(context: ExtensionContext): Promise<boolean> {
70+
this.sw.reset();
71+
this.context = context;
6472
const clientOptions = await this.getAnalysisOptions(context);
6573
if (!clientOptions) {
6674
return false;
6775
}
76+
this.disposables.push(this.interpreterService.onDidChangeInterpreter(() => this.restartLanguageServer()));
6877
return this.startLanguageServer(context, clientOptions);
6978
}
7079

7180
public async deactivate(): Promise<void> {
7281
if (this.languageClient) {
7382
await this.languageClient.stop();
7483
}
84+
for (const d of this.disposables) {
85+
d.dispose();
86+
}
87+
}
88+
89+
private async restartLanguageServer(): Promise<void> {
90+
if (!this.context) {
91+
return;
92+
}
93+
const ids = new InterpreterDataService(this.context, this.services);
94+
const idata = await ids.getInterpreterData();
95+
if (!idata || idata.hash !== this.interpreterHash) {
96+
this.interpreterHash = idata ? idata.hash : '';
97+
await this.deactivate();
98+
await this.activate(this.context);
99+
}
75100
}
76101

77102
private async startLanguageServer(context: ExtensionContext, clientOptions: LanguageClientOptions): Promise<boolean> {
78103
// Determine if we are running MSIL/Universal via dotnet or self-contained app.
79104
const mscorlib = path.join(context.extensionPath, analysisEngineFolder, 'mscorlib.dll');
105+
const downloader = new AnalysisEngineDownloader(this.services, analysisEngineFolder);
80106
let downloadPackage = false;
81107

82108
const reporter = getTelemetryReporter();
83109
reporter.sendTelemetryEvent(PYTHON_ANALYSIS_ENGINE_ENABLED);
84110

85-
if (!await this.fs.fileExistsAsync(mscorlib)) {
111+
await this.checkPythiaModel(context, downloader);
112+
113+
if (!await this.fs.fileExists(mscorlib)) {
86114
// Depends on .NET Runtime or SDK
87115
this.languageClient = this.createSimpleLanguageClient(context, clientOptions);
88116
try {
@@ -100,7 +128,7 @@ export class AnalysisExtensionActivator implements IExtensionActivator {
100128
}
101129

102130
if (downloadPackage) {
103-
const downloader = new AnalysisEngineDownloader(this.services, analysisEngineFolder);
131+
this.appShell.showWarningMessage('.NET Runtime is not found, platform-specific Python Analysis Engine will be downloaded.');
104132
await downloader.downloadAnalysisEngine(context);
105133
reporter.sendTelemetryEvent(PYTHON_ANALYSIS_ENGINE_DOWNLOADED);
106134
}
@@ -128,7 +156,9 @@ export class AnalysisExtensionActivator implements IExtensionActivator {
128156
disposable = lc.start();
129157
lc.onReady()
130158
.then(() => deferred.resolve())
131-
.catch(deferred.reject);
159+
.catch((reason) => {
160+
deferred.reject(reason);
161+
});
132162
await deferred.promise;
133163

134164
this.output.appendLine(`Language server ready: ${this.sw.elapsedTime} ms`);
@@ -172,20 +202,19 @@ export class AnalysisExtensionActivator implements IExtensionActivator {
172202
const interpreterData = await interpreterDataService.getInterpreterData();
173203
if (!interpreterData) {
174204
const appShell = this.services.get<IApplicationShell>(IApplicationShell);
175-
appShell.showErrorMessage('Unable to determine path to Python interpreter.');
176-
return;
205+
appShell.showWarningMessage('Unable to determine path to Python interpreter. IntelliSense will be limited.');
177206
}
178207

179-
// tslint:disable-next-line:no-string-literal
180-
properties['InterpreterPath'] = interpreterData.path;
181-
// tslint:disable-next-line:no-string-literal
182-
properties['Version'] = interpreterData.version;
183-
// tslint:disable-next-line:no-string-literal
184-
properties['PrefixPath'] = interpreterData.prefix;
185-
// tslint:disable-next-line:no-string-literal
186-
properties['DatabasePath'] = path.join(context.extensionPath, analysisEngineFolder);
208+
if (interpreterData) {
209+
// tslint:disable-next-line:no-string-literal
210+
properties['InterpreterPath'] = interpreterData.path;
211+
// tslint:disable-next-line:no-string-literal
212+
properties['Version'] = interpreterData.version;
213+
// tslint:disable-next-line:no-string-literal
214+
properties['PrefixPath'] = interpreterData.prefix;
215+
}
187216

188-
let searchPaths = interpreterData.searchPaths;
217+
let searchPaths = interpreterData ? interpreterData.searchPaths : '';
189218
const settings = this.configuration.getSettings();
190219
if (settings.autoComplete) {
191220
const extraPaths = settings.autoComplete.extraPaths;
@@ -194,12 +223,15 @@ export class AnalysisExtensionActivator implements IExtensionActivator {
194223
}
195224
}
196225

226+
// tslint:disable-next-line:no-string-literal
227+
properties['DatabasePath'] = path.join(context.extensionPath, analysisEngineFolder);
228+
197229
const envProvider = this.services.get<IEnvironmentVariablesProvider>(IEnvironmentVariablesProvider);
198230
const pythonPath = (await envProvider.getEnvironmentVariables()).PYTHONPATH;
231+
this.interpreterHash = interpreterData ? interpreterData.hash : '';
199232

200233
// tslint:disable-next-line:no-string-literal
201234
properties['SearchPaths'] = `${searchPaths};${pythonPath ? pythonPath : ''}`;
202-
203235
const selector: string[] = [PYTHON];
204236

205237
// Options to control the language client
@@ -215,12 +247,14 @@ export class AnalysisExtensionActivator implements IExtensionActivator {
215247
properties
216248
},
217249
displayOptions: {
250+
preferredFormat: 1, // Markdown
218251
trimDocumentationLines: false,
219252
maxDocumentationLineLength: 0,
220253
trimDocumentationText: false,
221254
maxDocumentationTextLength: 0
222255
},
223256
asyncStartup: true,
257+
pythiaEnabled: settings.pythiaEnabled,
224258
testEnvironment: isTestExecution()
225259
}
226260
};
@@ -231,4 +265,11 @@ export class AnalysisExtensionActivator implements IExtensionActivator {
231265
const result = await ps.exec('dotnet', ['--version']).catch(() => { return { stdout: '' }; });
232266
return result.stdout.trim().startsWith('2.');
233267
}
268+
269+
private async checkPythiaModel(context: ExtensionContext, downloader: AnalysisEngineDownloader): Promise<void> {
270+
const settings = this.configuration.getSettings();
271+
if (settings.pythiaEnabled) {
272+
await downloader.downloadPythiaModel(context);
273+
}
274+
}
234275
}

src/client/activation/downloader.ts

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
import * as fs from 'fs';
4+
import * as fileSystem from 'fs';
55
import * as path from 'path';
66
import * as request from 'request';
77
import * as requestProgress from 'request-progress';
88
import { ExtensionContext, OutputChannel, ProgressLocation, window } from 'vscode';
99
import { STANDARD_OUTPUT_CHANNEL } from '../common/constants';
10-
import { noop } from '../common/core.utils';
1110
import { createDeferred, createTemporaryFile } from '../common/helpers';
1211
import { IFileSystem, IPlatformService } from '../common/platform/types';
1312
import { IOutputChannel } from '../common/types';
@@ -22,49 +21,78 @@ const downloadUriPrefix = 'https://pvsc.blob.core.windows.net/python-analysis';
2221
const downloadBaseFileName = 'python-analysis-vscode';
2322
const downloadVersion = '0.1.0';
2423
const downloadFileExtension = '.nupkg';
24+
const pythiaModelName = 'model-sequence.json.gz';
2525

2626
export class AnalysisEngineDownloader {
2727
private readonly output: OutputChannel;
2828
private readonly platform: IPlatformService;
2929
private readonly platformData: PlatformData;
30+
private readonly fs: IFileSystem;
3031

3132
constructor(private readonly services: IServiceContainer, private engineFolder: string) {
3233
this.output = this.services.get<OutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL);
34+
this.fs = this.services.get<IFileSystem>(IFileSystem);
3335
this.platform = this.services.get<IPlatformService>(IPlatformService);
34-
this.platformData = new PlatformData(this.platform, this.services.get<IFileSystem>(IFileSystem));
36+
this.platformData = new PlatformData(this.platform, this.fs);
3537
}
3638

3739
public async downloadAnalysisEngine(context: ExtensionContext): Promise<void> {
38-
const localTempFilePath = await this.downloadFile();
40+
const platformString = await this.platformData.getPlatformName();
41+
const enginePackageFileName = `${downloadBaseFileName}-${platformString}.${downloadVersion}${downloadFileExtension}`;
42+
43+
let localTempFilePath = '';
3944
try {
40-
await this.verifyDownload(localTempFilePath);
45+
localTempFilePath = await this.downloadFile(downloadUriPrefix, enginePackageFileName, 'Downloading Python Analysis Engine... ');
46+
await this.verifyDownload(localTempFilePath, platformString);
4147
await this.unpackArchive(context.extensionPath, localTempFilePath);
4248
} catch (err) {
4349
this.output.appendLine('failed.');
4450
this.output.appendLine(err);
4551
throw new Error(err);
4652
} finally {
47-
fs.unlink(localTempFilePath, noop);
53+
if (localTempFilePath.length > 0) {
54+
await this.fs.deleteFile(localTempFilePath);
55+
}
4856
}
4957
}
5058

51-
private async downloadFile(): Promise<string> {
52-
const platformString = await this.platformData.getPlatformName();
53-
const remoteFileName = `${downloadBaseFileName}-${platformString}.${downloadVersion}${downloadFileExtension}`;
54-
const uri = `${downloadUriPrefix}/${remoteFileName}`;
59+
public async downloadPythiaModel(context: ExtensionContext): Promise<void> {
60+
const modelFolder = path.join(context.extensionPath, 'analysis', 'Pythia', 'model');
61+
const localPath = path.join(modelFolder, pythiaModelName);
62+
if (await this.fs.fileExists(localPath)) {
63+
return;
64+
}
65+
66+
let localTempFilePath = '';
67+
try {
68+
localTempFilePath = await this.downloadFile(downloadUriPrefix, pythiaModelName, 'Downloading IntelliSense Model File... ');
69+
await this.fs.createDirectory(modelFolder);
70+
await this.fs.copyFile(localTempFilePath, localPath);
71+
} catch (err) {
72+
this.output.appendLine('failed.');
73+
this.output.appendLine(err);
74+
throw new Error(err);
75+
} finally {
76+
if (localTempFilePath.length > 0) {
77+
await this.fs.deleteFile(localTempFilePath);
78+
}
79+
}
80+
}
81+
82+
private async downloadFile(location: string, fileName: string, title: string): Promise<string> {
83+
const uri = `${location}/${fileName}`;
5584
this.output.append(`Downloading ${uri}... `);
5685
const tempFile = await createTemporaryFile(downloadFileExtension);
5786

5887
const deferred = createDeferred();
59-
const fileStream = fs.createWriteStream(tempFile.filePath);
88+
const fileStream = fileSystem.createWriteStream(tempFile.filePath);
6089
fileStream.on('finish', () => {
6190
fileStream.close();
6291
}).on('error', (err) => {
6392
tempFile.cleanupCallback();
6493
deferred.reject(err);
6594
});
6695

67-
const title = 'Downloading Python Analysis Engine... ';
6896
await window.withProgress({
6997
location: ProgressLocation.Window,
7098
title
@@ -94,11 +122,11 @@ export class AnalysisEngineDownloader {
94122
return tempFile.filePath;
95123
}
96124

97-
private async verifyDownload(filePath: string): Promise<void> {
125+
private async verifyDownload(filePath: string, platformString: string): Promise<void> {
98126
this.output.appendLine('');
99127
this.output.append('Verifying download... ');
100128
const verifier = new HashVerifier();
101-
if (!await verifier.verifyHash(filePath, await this.platformData.getExpectedHash())) {
129+
if (!await verifier.verifyHash(filePath, platformString, await this.platformData.getExpectedHash())) {
102130
throw new Error('Hash of the downloaded file does not match.');
103131
}
104132
this.output.append('valid.');
@@ -123,10 +151,10 @@ export class AnalysisEngineDownloader {
123151

124152
let totalFiles = 0;
125153
let extractedFiles = 0;
126-
zip.on('ready', () => {
154+
zip.on('ready', async () => {
127155
totalFiles = zip.entriesCount;
128-
if (!fs.existsSync(installFolder)) {
129-
fs.mkdirSync(installFolder);
156+
if (!await this.fs.directoryExists(installFolder)) {
157+
await this.fs.createDirectory(installFolder);
130158
}
131159
zip.extract(null, installFolder, (err, count) => {
132160
if (err) {
@@ -147,7 +175,7 @@ export class AnalysisEngineDownloader {
147175
// Set file to executable
148176
if (!this.platform.isWindows) {
149177
const executablePath = path.join(installFolder, this.platformData.getEngineExecutableName());
150-
fs.chmodSync(executablePath, '0764'); // -rwxrw-r--
178+
fileSystem.chmodSync(executablePath, '0764'); // -rwxrw-r--
151179
}
152180
}
153181
}

src/client/activation/hashVerifier.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as fs from 'fs';
66
import { createDeferred } from '../common/helpers';
77

88
export class HashVerifier {
9-
public async verifyHash(filePath: string, expectedDigest: string): Promise<boolean> {
9+
public async verifyHash(filePath: string, platformString: string, expectedDigest: string): Promise<boolean> {
1010
const readStream = fs.createReadStream(filePath);
1111
const deferred = createDeferred();
1212
const hash = createHash('sha512');
@@ -23,6 +23,6 @@ export class HashVerifier {
2323
readStream.pipe(hash);
2424
await deferred.promise;
2525
const actual = hash.read();
26-
return expectedDigest === '' ? true : actual === expectedDigest;
26+
return expectedDigest === platformString ? true : actual === expectedDigest;
2727
}
2828
}

0 commit comments

Comments
 (0)