-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PTVS engine update + handling of the interpreter change #1613
Changes from 70 commits
a764bc9
9d1b2cc
a91291a
bf266af
7bc6bd6
8ce8b48
e3a549e
92e8c1e
b540a1d
7b0573e
facb106
f113881
3e76718
6e85dc6
99e037c
3caeab7
eeb1f11
f5f78c7
88744da
65dde44
c513f71
ccb3886
106f4db
9e5cb43
e1da6a6
e78f0fb
725cf71
5cd6d45
27613db
8061a20
17dc292
65964b9
4bf5a4c
6f7212c
ddbd295
ffd1d3f
d4afb6c
12186b8
ca90529
a7267b5
cfee109
1285789
d1ff1d9
1bd1651
a69b6fd
f916ace
28ca25f
ad9a3c9
ff8dd35
26726f8
d7806ca
0b3f316
28a8950
1804617
f000e5d
a06fd79
ef7c5c7
f4e88c0
d420c34
b819d57
abff213
d140b3a
4b394d9
a397b11
e54eaf8
3b8ddd5
9a4500d
41f9624
3627b85
486d11d
cf5cf9c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ import { IProcessService } from '../common/process/types'; | |
import { StopWatch } from '../common/stopWatch'; | ||
import { IConfigurationService, IOutputChannel, IPythonSettings } from '../common/types'; | ||
import { IEnvironmentVariablesProvider } from '../common/variables/types'; | ||
import { IInterpreterService } from '../interpreter/contracts'; | ||
import { IServiceContainer } from '../ioc/types'; | ||
import { | ||
PYTHON_ANALYSIS_ENGINE_DOWNLOADED, | ||
|
@@ -35,11 +36,11 @@ class LanguageServerStartupErrorHandler implements ErrorHandler { | |
constructor(private readonly deferred: Deferred<void>) { } | ||
public error(error: Error, message: Message, count: number): ErrorAction { | ||
this.deferred.reject(error); | ||
return ErrorAction.Shutdown; | ||
return ErrorAction.Continue; | ||
} | ||
public closed(): CloseAction { | ||
this.deferred.reject(); | ||
return CloseAction.DoNotRestart; | ||
return CloseAction.Restart; | ||
} | ||
} | ||
|
||
|
@@ -50,39 +51,66 @@ export class AnalysisExtensionActivator implements IExtensionActivator { | |
private readonly fs: IFileSystem; | ||
private readonly sw = new StopWatch(); | ||
private readonly platformData: PlatformData; | ||
private readonly interpreterService: IInterpreterService; | ||
private readonly disposables: Disposable[] = []; | ||
private languageClient: LanguageClient | undefined; | ||
private context: ExtensionContext | undefined; | ||
private interpreterHash: string = ''; | ||
|
||
constructor(private readonly services: IServiceContainer, pythonSettings: IPythonSettings) { | ||
this.configuration = this.services.get<IConfigurationService>(IConfigurationService); | ||
this.appShell = this.services.get<IApplicationShell>(IApplicationShell); | ||
this.output = this.services.get<OutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL); | ||
this.fs = this.services.get<IFileSystem>(IFileSystem); | ||
this.platformData = new PlatformData(services.get<IPlatformService>(IPlatformService), this.fs); | ||
this.interpreterService = this.services.get<IInterpreterService>(IInterpreterService); | ||
} | ||
|
||
public async activate(context: ExtensionContext): Promise<boolean> { | ||
this.sw.reset(); | ||
this.context = context; | ||
const clientOptions = await this.getAnalysisOptions(context); | ||
if (!clientOptions) { | ||
return false; | ||
} | ||
this.disposables.push(this.interpreterService.onDidChangeInterpreter(() => this.restartLanguageServer())); | ||
return this.startLanguageServer(context, clientOptions); | ||
} | ||
|
||
public async deactivate(): Promise<void> { | ||
if (this.languageClient) { | ||
await this.languageClient.stop(); | ||
} | ||
for (const d of this.disposables) { | ||
d.dispose(); | ||
} | ||
} | ||
|
||
private async restartLanguageServer(): Promise<void> { | ||
if (!this.context) { | ||
return; | ||
} | ||
const ids = new InterpreterDataService(this.context, this.services); | ||
const idata = await ids.getInterpreterData(); | ||
if (!idata || idata.hash !== this.interpreterHash) { | ||
this.interpreterHash = idata ? idata.hash : ''; | ||
await this.deactivate(); | ||
await this.activate(this.context); | ||
} | ||
} | ||
|
||
private async startLanguageServer(context: ExtensionContext, clientOptions: LanguageClientOptions): Promise<boolean> { | ||
// Determine if we are running MSIL/Universal via dotnet or self-contained app. | ||
const mscorlib = path.join(context.extensionPath, analysisEngineFolder, 'mscorlib.dll'); | ||
const downloader = new AnalysisEngineDownloader(this.services, analysisEngineFolder); | ||
let downloadPackage = false; | ||
|
||
const reporter = getTelemetryReporter(); | ||
reporter.sendTelemetryEvent(PYTHON_ANALYSIS_ENGINE_ENABLED); | ||
|
||
if (!await this.fs.fileExistsAsync(mscorlib)) { | ||
await this.checkPythiaModel(context, downloader); | ||
|
||
if (!await this.fs.fileExists(mscorlib)) { | ||
// Depends on .NET Runtime or SDK | ||
this.languageClient = this.createSimpleLanguageClient(context, clientOptions); | ||
try { | ||
|
@@ -100,7 +128,7 @@ export class AnalysisExtensionActivator implements IExtensionActivator { | |
} | ||
|
||
if (downloadPackage) { | ||
const downloader = new AnalysisEngineDownloader(this.services, analysisEngineFolder); | ||
this.appShell.showWarningMessage('.NET Runtime is not found, platform-specific Python Analysis Engine will be downloaded.'); | ||
await downloader.downloadAnalysisEngine(context); | ||
reporter.sendTelemetryEvent(PYTHON_ANALYSIS_ENGINE_DOWNLOADED); | ||
} | ||
|
@@ -128,7 +156,9 @@ export class AnalysisExtensionActivator implements IExtensionActivator { | |
disposable = lc.start(); | ||
lc.onReady() | ||
.then(() => deferred.resolve()) | ||
.catch(deferred.reject); | ||
.catch((reason) => { | ||
deferred.reject(reason); | ||
}); | ||
await deferred.promise; | ||
|
||
this.output.appendLine(`Language server ready: ${this.sw.elapsedTime} ms`); | ||
|
@@ -172,20 +202,19 @@ export class AnalysisExtensionActivator implements IExtensionActivator { | |
const interpreterData = await interpreterDataService.getInterpreterData(); | ||
if (!interpreterData) { | ||
const appShell = this.services.get<IApplicationShell>(IApplicationShell); | ||
appShell.showErrorMessage('Unable to determine path to Python interpreter.'); | ||
return; | ||
appShell.showWarningMessage('Unable to determine path to Python interpreter. IntelliSense will be limited.'); | ||
} | ||
|
||
// tslint:disable-next-line:no-string-literal | ||
properties['InterpreterPath'] = interpreterData.path; | ||
// tslint:disable-next-line:no-string-literal | ||
properties['Version'] = interpreterData.version; | ||
// tslint:disable-next-line:no-string-literal | ||
properties['PrefixPath'] = interpreterData.prefix; | ||
// tslint:disable-next-line:no-string-literal | ||
properties['DatabasePath'] = path.join(context.extensionPath, analysisEngineFolder); | ||
if (interpreterData) { | ||
// tslint:disable-next-line:no-string-literal | ||
properties['InterpreterPath'] = interpreterData.path; | ||
// tslint:disable-next-line:no-string-literal | ||
properties['Version'] = interpreterData.version; | ||
// tslint:disable-next-line:no-string-literal | ||
properties['PrefixPath'] = interpreterData.prefix; | ||
} | ||
|
||
let searchPaths = interpreterData.searchPaths; | ||
let searchPaths = interpreterData ? interpreterData.searchPaths : ''; | ||
const settings = this.configuration.getSettings(); | ||
if (settings.autoComplete) { | ||
const extraPaths = settings.autoComplete.extraPaths; | ||
|
@@ -194,12 +223,15 @@ export class AnalysisExtensionActivator implements IExtensionActivator { | |
} | ||
} | ||
|
||
// tslint:disable-next-line:no-string-literal | ||
properties['DatabasePath'] = path.join(context.extensionPath, analysisEngineFolder); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to change, but you can avoid the linter messages if you define properties as follows: |
||
|
||
const envProvider = this.services.get<IEnvironmentVariablesProvider>(IEnvironmentVariablesProvider); | ||
const pythonPath = (await envProvider.getEnvironmentVariables()).PYTHONPATH; | ||
this.interpreterHash = interpreterData ? interpreterData.hash : ''; | ||
|
||
// tslint:disable-next-line:no-string-literal | ||
properties['SearchPaths'] = `${searchPaths};${pythonPath ? pythonPath : ''}`; | ||
|
||
const selector: string[] = [PYTHON]; | ||
|
||
// Options to control the language client | ||
|
@@ -215,12 +247,14 @@ export class AnalysisExtensionActivator implements IExtensionActivator { | |
properties | ||
}, | ||
displayOptions: { | ||
preferredFormat: 1, // Markdown | ||
trimDocumentationLines: false, | ||
maxDocumentationLineLength: 0, | ||
trimDocumentationText: false, | ||
maxDocumentationTextLength: 0 | ||
}, | ||
asyncStartup: true, | ||
pythiaEnabled: settings.pythiaEnabled, | ||
testEnvironment: isTestExecution() | ||
} | ||
}; | ||
|
@@ -231,4 +265,11 @@ export class AnalysisExtensionActivator implements IExtensionActivator { | |
const result = await ps.exec('dotnet', ['--version']).catch(() => { return { stdout: '' }; }); | ||
return result.stdout.trim().startsWith('2.'); | ||
} | ||
|
||
private async checkPythiaModel(context: ExtensionContext, downloader: AnalysisEngineDownloader): Promise<void> { | ||
const settings = this.configuration.getSettings(); | ||
if (settings.pythiaEnabled) { | ||
await downloader.downloadPythiaModel(context); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,12 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
import * as fs from 'fs'; | ||
import * as fileSystem from 'fs'; | ||
import * as path from 'path'; | ||
import * as request from 'request'; | ||
import * as requestProgress from 'request-progress'; | ||
import { ExtensionContext, OutputChannel, ProgressLocation, window } from 'vscode'; | ||
import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; | ||
import { noop } from '../common/core.utils'; | ||
import { createDeferred, createTemporaryFile } from '../common/helpers'; | ||
import { IFileSystem, IPlatformService } from '../common/platform/types'; | ||
import { IOutputChannel } from '../common/types'; | ||
|
@@ -22,49 +21,78 @@ const downloadUriPrefix = 'https://pvsc.blob.core.windows.net/python-analysis'; | |
const downloadBaseFileName = 'python-analysis-vscode'; | ||
const downloadVersion = '0.1.0'; | ||
const downloadFileExtension = '.nupkg'; | ||
const pythiaModelName = 'model-sequence.json.gz'; | ||
|
||
export class AnalysisEngineDownloader { | ||
private readonly output: OutputChannel; | ||
private readonly platform: IPlatformService; | ||
private readonly platformData: PlatformData; | ||
private readonly fs: IFileSystem; | ||
|
||
constructor(private readonly services: IServiceContainer, private engineFolder: string) { | ||
this.output = this.services.get<OutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL); | ||
this.fs = this.services.get<IFileSystem>(IFileSystem); | ||
this.platform = this.services.get<IPlatformService>(IPlatformService); | ||
this.platformData = new PlatformData(this.platform, this.services.get<IFileSystem>(IFileSystem)); | ||
this.platformData = new PlatformData(this.platform, this.fs); | ||
} | ||
|
||
public async downloadAnalysisEngine(context: ExtensionContext): Promise<void> { | ||
const localTempFilePath = await this.downloadFile(); | ||
const platformString = await this.platformData.getPlatformName(); | ||
const enginePackageFileName = `${downloadBaseFileName}-${platformString}.${downloadVersion}${downloadFileExtension}`; | ||
|
||
let localTempFilePath = ''; | ||
try { | ||
await this.verifyDownload(localTempFilePath); | ||
localTempFilePath = await this.downloadFile(downloadUriPrefix, enginePackageFileName, 'Downloading Python Analysis Engine... '); | ||
await this.verifyDownload(localTempFilePath, platformString); | ||
await this.unpackArchive(context.extensionPath, localTempFilePath); | ||
} catch (err) { | ||
this.output.appendLine('failed.'); | ||
this.output.appendLine(err); | ||
throw new Error(err); | ||
} finally { | ||
fs.unlink(localTempFilePath, noop); | ||
if (localTempFilePath.length > 0) { | ||
await this.fs.deleteFile(localTempFilePath); | ||
} | ||
} | ||
} | ||
|
||
private async downloadFile(): Promise<string> { | ||
const platformString = await this.platformData.getPlatformName(); | ||
const remoteFileName = `${downloadBaseFileName}-${platformString}.${downloadVersion}${downloadFileExtension}`; | ||
const uri = `${downloadUriPrefix}/${remoteFileName}`; | ||
public async downloadPythiaModel(context: ExtensionContext): Promise<void> { | ||
const modelFolder = path.join(context.extensionPath, 'analysis', 'Pythia', 'model'); | ||
const localPath = path.join(modelFolder, pythiaModelName); | ||
if (await this.fs.fileExists(localPath)) { | ||
return; | ||
} | ||
|
||
let localTempFilePath = ''; | ||
try { | ||
localTempFilePath = await this.downloadFile(downloadUriPrefix, pythiaModelName, 'Downloading IntelliSense Model File... '); | ||
await this.fs.createDirectory(modelFolder); | ||
await this.fs.copyFile(localTempFilePath, localPath); | ||
} catch (err) { | ||
this.output.appendLine('failed.'); | ||
this.output.appendLine(err); | ||
throw new Error(err); | ||
} finally { | ||
if (localTempFilePath.length > 0) { | ||
await this.fs.deleteFile(localTempFilePath); | ||
} | ||
} | ||
} | ||
|
||
private async downloadFile(location: string, fileName: string, title: string): Promise<string> { | ||
const uri = `${location}/${fileName}`; | ||
this.output.append(`Downloading ${uri}... `); | ||
const tempFile = await createTemporaryFile(downloadFileExtension); | ||
|
||
const deferred = createDeferred(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This |
||
const fileStream = fs.createWriteStream(tempFile.filePath); | ||
const fileStream = fileSystem.createWriteStream(tempFile.filePath); | ||
fileStream.on('finish', () => { | ||
fileStream.close(); | ||
}).on('error', (err) => { | ||
tempFile.cleanupCallback(); | ||
deferred.reject(err); | ||
}); | ||
|
||
const title = 'Downloading Python Analysis Engine... '; | ||
await window.withProgress({ | ||
location: ProgressLocation.Window, | ||
title | ||
|
@@ -94,11 +122,11 @@ export class AnalysisEngineDownloader { | |
return tempFile.filePath; | ||
} | ||
|
||
private async verifyDownload(filePath: string): Promise<void> { | ||
private async verifyDownload(filePath: string, platformString: string): Promise<void> { | ||
this.output.appendLine(''); | ||
this.output.append('Verifying download... '); | ||
const verifier = new HashVerifier(); | ||
if (!await verifier.verifyHash(filePath, await this.platformData.getExpectedHash())) { | ||
if (!await verifier.verifyHash(filePath, platformString, await this.platformData.getExpectedHash())) { | ||
throw new Error('Hash of the downloaded file does not match.'); | ||
} | ||
this.output.append('valid.'); | ||
|
@@ -123,10 +151,10 @@ export class AnalysisEngineDownloader { | |
|
||
let totalFiles = 0; | ||
let extractedFiles = 0; | ||
zip.on('ready', () => { | ||
zip.on('ready', async () => { | ||
totalFiles = zip.entriesCount; | ||
if (!fs.existsSync(installFolder)) { | ||
fs.mkdirSync(installFolder); | ||
if (!await this.fs.directoryExists(installFolder)) { | ||
await this.fs.createDirectory(installFolder); | ||
} | ||
zip.extract(null, installFolder, (err, count) => { | ||
if (err) { | ||
|
@@ -147,7 +175,7 @@ export class AnalysisEngineDownloader { | |
// Set file to executable | ||
if (!this.platform.isWindows) { | ||
const executablePath = path.join(installFolder, this.platformData.getEngineExecutableName()); | ||
fs.chmodSync(executablePath, '0764'); // -rwxrw-r-- | ||
fileSystem.chmodSync(executablePath, '0764'); // -rwxrw-r-- | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be an information message, instead of a warning?