Skip to content

Commit

Permalink
Create a new API to retrieve interpreter details with the ability to …
Browse files Browse the repository at this point in the history
…cache the details (microsoft#1567)

Fixes microsoft#1569
  • Loading branch information
DonJayamanne authored May 10, 2018
1 parent f18a5ee commit a4374fe
Show file tree
Hide file tree
Showing 54 changed files with 631 additions and 256 deletions.
1 change: 1 addition & 0 deletions news/3 Code Health/1569.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Create a new API to retrieve interpreter details with the ability to cache the details.
13 changes: 13 additions & 0 deletions pythonFiles/interpreterInfo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import json
import sys

obj = {}
obj["versionInfo"] = sys.version_info[:4]
obj["sysPrefix"] = sys.prefix
obj["version"] = sys.version
obj["is64Bit"] = sys.maxsize > 2**32

print(json.dumps(obj))
2 changes: 1 addition & 1 deletion src/client/activation/interpreterDataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class InterpreterDataService {

public async getInterpreterData(resource?: Uri): Promise<InterpreterData | undefined> {
const executionFactory = this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory);
const execService = await executionFactory.create(resource);
const execService = await executionFactory.create({ resource });

const interpreterPath = await execService.getExecutablePath();
if (interpreterPath.length === 0) {
Expand Down
6 changes: 3 additions & 3 deletions src/client/common/installer/channelManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT License.

import { inject, injectable } from 'inversify';
import { QuickPickItem, Uri } from 'vscode';
import { Uri } from 'vscode';
import { IInterpreterService, InterpreterType } from '../../interpreter/contracts';
import { IServiceContainer } from '../../ioc/types';
import { IApplicationShell } from '../application/types';
Expand Down Expand Up @@ -34,9 +34,9 @@ export class InstallationChannelManager implements IInstallationChannelManager {
label: `Install using ${installer.displayName}`,
description: '',
installer
} as QuickPickItem & { installer: IModuleInstaller };
};
});
const selection = await appShell.showQuickPick(options, { matchOnDescription: true, matchOnDetail: true, placeHolder });
const selection = await appShell.showQuickPick<typeof options[0]>(options, { matchOnDescription: true, matchOnDetail: true, placeHolder });
return selection ? selection.installer : undefined;
}

Expand Down
2 changes: 1 addition & 1 deletion src/client/common/installer/pipInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class PipInstaller extends ModuleInstaller implements IModuleInstaller {
}
private isPipAvailable(resource?: Uri): Promise<boolean> {
const pythonExecutionFactory = this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory);
return pythonExecutionFactory.create(resource)
return pythonExecutionFactory.create({ resource })
.then(proc => proc.isModuleInstalled('pip'))
.catch(() => false);
}
Expand Down
2 changes: 1 addition & 1 deletion src/client/common/installer/productInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ abstract class BaseInstaller {

const isModule = typeof moduleName === 'string' && moduleName.length > 0 && path.basename(executableName) === executableName;
if (isModule) {
const pythonProcess = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create(resource);
const pythonProcess = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create({ resource });
return pythonProcess.isModuleInstalled(executableName);
} else {
const process = await this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create(resource);
Expand Down
29 changes: 21 additions & 8 deletions src/client/common/persistentState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,38 @@ import { Memento } from 'vscode';
import { GLOBAL_MEMENTO, IMemento, IPersistentState, IPersistentStateFactory, WORKSPACE_MEMENTO } from './types';

class PersistentState<T> implements IPersistentState<T>{
constructor(private storage: Memento, private key: string, private defaultValue: T) { }
constructor(private storage: Memento, private key: string, private defaultValue?: T, private expiryDurationMs?: number) { }

public get value(): T {
return this.storage.get<T>(this.key, this.defaultValue);
if (this.expiryDurationMs) {
const cachedData = this.storage.get<{ data?: T; expiry?: number }>(this.key, { data: this.defaultValue! });
if (!cachedData || !cachedData.expiry || cachedData.expiry < Date.now()) {
return this.defaultValue!;
} else {
return cachedData.data!;
}
} else {
return this.storage.get<T>(this.key, this.defaultValue!);
}
}

public async updateValue(newValue: T): Promise<void> {
await this.storage.update(this.key, newValue);
if (this.expiryDurationMs) {
await this.storage.update(this.key, { data: newValue, expiry: Date.now() + this.expiryDurationMs });
} else {
await this.storage.update(this.key, newValue);
}
}
}

@injectable()
export class PersistentStateFactory implements IPersistentStateFactory {
constructor( @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento,
constructor(@inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento,
@inject(IMemento) @named(WORKSPACE_MEMENTO) private workspaceState: Memento) { }
public createGlobalPersistentState<T>(key: string, defaultValue: T): IPersistentState<T> {
return new PersistentState<T>(this.globalState, key, defaultValue);
public createGlobalPersistentState<T>(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState<T> {
return new PersistentState<T>(this.globalState, key, defaultValue, expiryDurationMs);
}
public createWorkspacePersistentState<T>(key: string, defaultValue: T): IPersistentState<T> {
return new PersistentState<T>(this.workspaceState, key, defaultValue);
public createWorkspacePersistentState<T>(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState<T> {
return new PersistentState<T>(this.workspaceState, key, defaultValue, expiryDurationMs);
}
}
13 changes: 13 additions & 0 deletions src/client/common/platform/fileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.
'use strict';

import { createHash } from 'crypto';
import * as fs from 'fs-extra';
import { inject, injectable } from 'inversify';
import * as path from 'path';
Expand Down Expand Up @@ -117,4 +118,16 @@ export class FileSystem implements IFileSystem {
fs.unlink(filename, err => err ? deferred.reject(err) : deferred.resolve());
return deferred.promise;
}
public getFileHash(filePath: string): Promise<string | undefined> {
return new Promise<string | undefined>(resolve => {
fs.lstat(filePath, (err, stats) => {
if (err) {
resolve();
} else {
const actual = createHash('sha512').update(`${stats.ctimeMs}-${stats.mtimeMs}`).digest('hex');
resolve(actual);
}
});
});
}
}
1 change: 1 addition & 0 deletions src/client/common/platform/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ export interface IFileSystem {
getRealPath(path: string): Promise<string>;
copyFile(src: string, dest: string): Promise<void>;
deleteFile(filename: string): Promise<void>;
getFileHash(filePath: string): Promise<string | undefined>;
}
12 changes: 8 additions & 4 deletions src/client/common/process/pythonExecutionFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@
import { inject, injectable } from 'inversify';
import { Uri } from 'vscode';
import { IServiceContainer } from '../../ioc/types';
import { IConfigurationService } from '../types';
import { PythonExecutionService } from './pythonProcess';
import { IProcessServiceFactory, IPythonExecutionFactory, IPythonExecutionService } from './types';
import { ExecutionFactoryCreationOptions, IProcessServiceFactory, IPythonExecutionFactory, IPythonExecutionService } from './types';

@injectable()
export class PythonExecutionFactory implements IPythonExecutionFactory {
private readonly configService: IConfigurationService;
private processServiceFactory: IProcessServiceFactory;
constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {
this.processServiceFactory = serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory);
this.configService = serviceContainer.get<IConfigurationService>(IConfigurationService);
}
public async create(resource?: Uri): Promise<IPythonExecutionService> {
const processService = await this.processServiceFactory.create(resource);
return new PythonExecutionService(this.serviceContainer, processService, resource);
public async create(options: ExecutionFactoryCreationOptions): Promise<IPythonExecutionService> {
const pythonPath = options.pythonPath ? options.pythonPath : this.configService.getSettings(options.resource).pythonPath;
const processService = await this.processServiceFactory.create(options.resource);
return new PythonExecutionService(this.serviceContainer, processService, pythonPath);
}
}
41 changes: 28 additions & 13 deletions src/client/common/process/pythonProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,46 @@
// Licensed under the MIT License.

import { injectable } from 'inversify';
import * as path from 'path';
import { Uri } from 'vscode';
import { IInterpreterVersionService } from '../../interpreter/contracts';
import { IServiceContainer } from '../../ioc/types';
import { EXTENSION_ROOT_DIR } from '../constants';
import { ErrorUtils } from '../errors/errorUtils';
import { ModuleNotInstalledError } from '../errors/moduleNotInstalledError';
import { IFileSystem } from '../platform/types';
import { IConfigurationService } from '../types';
import { ExecutionResult, IProcessService, IPythonExecutionService, ObservableExecutionResult, SpawnOptions } from './types';
import { Architecture, IFileSystem } from '../platform/types';
import { EnvironmentVariables } from '../variables/types';
import { ExecutionResult, InterpreterInfomation, IProcessService, IPythonExecutionService, ObservableExecutionResult, PythonVersionInfo, SpawnOptions } from './types';

@injectable()
export class PythonExecutionService implements IPythonExecutionService {
private readonly configService: IConfigurationService;
private readonly fileSystem: IFileSystem;

constructor(private serviceContainer: IServiceContainer, private readonly procService: IProcessService, private resource?: Uri) {
this.configService = serviceContainer.get<IConfigurationService>(IConfigurationService);
constructor(private serviceContainer: IServiceContainer, private readonly procService: IProcessService, private readonly pythonPath: string) {
this.fileSystem = serviceContainer.get<IFileSystem>(IFileSystem);
}

public async getVersion(): Promise<string> {
const versionService = this.serviceContainer.get<IInterpreterVersionService>(IInterpreterVersionService);
return versionService.getVersion(this.pythonPath, '');
public async getInterpreterInformation(): Promise<InterpreterInfomation | undefined> {
const file = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'interpreterInfo.py');
try {
const [version, jsonValue] = await Promise.all([
this.procService.exec(this.pythonPath, ['--version'], { mergeStdOutErr: true })
.then(output => output.stdout.trim()),
this.procService.exec(this.pythonPath, [file], { mergeStdOutErr: true })
.then(output => output.stdout.trim())
]);

const json = JSON.parse(jsonValue) as { versionInfo: PythonVersionInfo; sysPrefix: string; sysVersion: string; is64Bit: boolean };
return {
architecture: json.is64Bit ? Architecture.x64 : Architecture.x86,
path: this.pythonPath,
version,
sysVersion: json.sysVersion,
version_info: json.versionInfo,
sysPrefix: json.sysPrefix
};
} catch (ex) {
console.error(`Failed to get interpreter information for '${this.pythonPath}'`, ex);
}
}
public async getExecutablePath(): Promise<string> {
// If we've passed the python file, then return the file.
Expand Down Expand Up @@ -65,7 +83,4 @@ export class PythonExecutionService implements IPythonExecutionService {

return result;
}
private get pythonPath(): string {
return this.configService.getSettings(this.resource).pythonPath;
}
}
4 changes: 2 additions & 2 deletions src/client/common/process/pythonToolService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class PythonToolExecutionService implements IPythonToolExecutionService {
throw new Error('Environment variables are not supported');
}
if (executionInfo.moduleName && executionInfo.moduleName.length > 0) {
const pythonExecutionService = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create(resource);
const pythonExecutionService = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create({ resource });
return pythonExecutionService.execModuleObservable(executionInfo.moduleName, executionInfo.args, options);
} else {
const processService = await this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create(resource);
Expand All @@ -27,7 +27,7 @@ export class PythonToolExecutionService implements IPythonToolExecutionService {
throw new Error('Environment variables are not supported');
}
if (executionInfo.moduleName && executionInfo.moduleName.length > 0) {
const pythonExecutionService = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create(resource);
const pythonExecutionService = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create({ resource });
return pythonExecutionService.execModule(executionInfo.moduleName!, executionInfo.args, options);
} else {
const processService = await this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create(resource);
Expand Down
21 changes: 18 additions & 3 deletions src/client/common/process/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { ChildProcess, SpawnOptions as ChildProcessSpawnOptions } from 'child_process';
import { Observable } from 'rxjs/Observable';
import { CancellationToken, Uri } from 'vscode';
import { Architecture } from '../platform/types';
import { ExecutionInfo } from '../types';
import { EnvironmentVariables } from '../variables/types';

Expand Down Expand Up @@ -46,14 +47,28 @@ export interface IProcessServiceFactory {
}

export const IPythonExecutionFactory = Symbol('IPythonExecutionFactory');

export type ExecutionFactoryCreationOptions = {
resource?: Uri;
pythonPath?: string;
};
export interface IPythonExecutionFactory {
create(resource?: Uri): Promise<IPythonExecutionService>;
create(options: ExecutionFactoryCreationOptions): Promise<IPythonExecutionService>;
}

export type ReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final';
// tslint:disable-next-line:interface-name
export type PythonVersionInfo = [number, number, number, ReleaseLevel];
export type InterpreterInfomation = {
path: string;
version: string;
sysVersion: string;
architecture: Architecture;
version_info: PythonVersionInfo;
sysPrefix: string;
};
export const IPythonExecutionService = Symbol('IPythonExecutionService');

export interface IPythonExecutionService {
getInterpreterInformation(): Promise<InterpreterInfomation | undefined>;
getExecutablePath(): Promise<string>;
isModuleInstalled(moduleName: string): Promise<boolean>;

Expand Down
4 changes: 2 additions & 2 deletions src/client/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export interface IPersistentState<T> {
export const IPersistentStateFactory = Symbol('IPersistentStateFactory');

export interface IPersistentStateFactory {
createGlobalPersistentState<T>(key: string, defaultValue: T): IPersistentState<T>;
createWorkspacePersistentState<T>(key: string, defaultValue: T): IPersistentState<T>;
createGlobalPersistentState<T>(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState<T>;
createWorkspacePersistentState<T>(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState<T>;
}

export type ExecutionInfo = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class ConfigurationProviderUtils implements IConfigurationProviderUtils {
}
public async getPyramidStartupScriptFilePath(resource?: Uri): Promise<string | undefined> {
try {
const executionService = await this.executionFactory.create(resource);
const executionService = await this.executionFactory.create({ resource });
const output = await executionService.exec(['-c', 'import pyramid;print(pyramid.__file__)'], { throwOnStdErr: true });
const pserveFilePath = path.join(path.dirname(output.stdout.trim()), 'scripts', PSERVE_SCRIPT_FILE_NAME);
return await this.fs.fileExists(pserveFilePath) ? pserveFilePath : undefined;
Expand Down
19 changes: 11 additions & 8 deletions src/client/interpreter/configuration/pythonPathUpdaterService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { inject, injectable } from 'inversify';
import * as path from 'path';
import { ConfigurationTarget, Uri, window } from 'vscode';
import { InterpreterInfomation, IPythonExecutionFactory } from '../../common/process/types';
import { StopWatch } from '../../common/stopWatch';
import { IServiceContainer } from '../../ioc/types';
import { sendTelemetryEvent } from '../../telemetry';
Expand All @@ -13,9 +14,11 @@ import { IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager } fr
export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManager {
private readonly pythonPathSettingsUpdaterFactory: IPythonPathUpdaterServiceFactory;
private readonly interpreterVersionService: IInterpreterVersionService;
private readonly executionFactory: IPythonExecutionFactory;
constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
this.pythonPathSettingsUpdaterFactory = serviceContainer.get<IPythonPathUpdaterServiceFactory>(IPythonPathUpdaterServiceFactory);
this.interpreterVersionService = serviceContainer.get<IInterpreterVersionService>(IInterpreterVersionService);
this.executionFactory = serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory);
}
public async updatePythonPath(pythonPath: string, configTarget: ConfigurationTarget, trigger: 'ui' | 'shebang' | 'load', wkspace?: Uri): Promise<void> {
const stopWatch = new StopWatch();
Expand All @@ -39,17 +42,17 @@ export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManage
failed, trigger
};
if (!failed) {
const pyVersionPromise = this.interpreterVersionService.getVersion(pythonPath, '')
.then(pyVersion => pyVersion.length === 0 ? undefined : pyVersion);
const processService = await this.executionFactory.create({ pythonPath });
const infoPromise = processService.getInterpreterInformation().catch<InterpreterInfomation>(() => undefined);
const pipVersionPromise = this.interpreterVersionService.getPipVersion(pythonPath)
.then(value => value.length === 0 ? undefined : value)
.catch(() => undefined);
const versions = await Promise.all([pyVersionPromise, pipVersionPromise]);
if (versions[0]) {
telemtryProperties.version = versions[0] as string;
.catch<string>(() => undefined);
const [info, pipVersion] = await Promise.all([infoPromise, pipVersionPromise]);
if (info) {
telemtryProperties.version = info.version;
}
if (versions[1]) {
telemtryProperties.pipVersion = versions[1] as string;
if (pipVersion) {
telemtryProperties.pipVersion = pipVersion;
}
}
sendTelemetryEvent(PYTHON_INTERPRETER, duration, telemtryProperties);
Expand Down
Loading

0 comments on commit a4374fe

Please sign in to comment.