From 07dac60f0804fb83a67d7e08e2225c2e72bf9770 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 18 Sep 2020 13:57:39 -0700 Subject: [PATCH 1/8] Modify environment info worker to support new environment type --- .../base/info/interpreter.ts | 93 +++++++++++++++++++ .../base/info/pythonVersion.ts | 35 +++++++ .../info/environmentInfoService.ts | 58 +++++------- src/test/pythonEnvironments/base/common.ts | 34 +------ .../environmentInfoService.functional.test.ts | 71 ++++++++------ 5 files changed, 194 insertions(+), 97 deletions(-) create mode 100644 src/client/pythonEnvironments/base/info/interpreter.ts create mode 100644 src/client/pythonEnvironments/base/info/pythonVersion.ts diff --git a/src/client/pythonEnvironments/base/info/interpreter.ts b/src/client/pythonEnvironments/base/info/interpreter.ts new file mode 100644 index 000000000000..4acb15f5ee00 --- /dev/null +++ b/src/client/pythonEnvironments/base/info/interpreter.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { PythonVersion } from '.'; +import { interpreterInfo as getInterpreterInfoCommand, PythonEnvInfo } from '../../../common/process/internal/scripts'; +import { Architecture } from '../../../common/utils/platform'; +import { copyPythonExecInfo, PythonExecInfo } from '../../exec'; +import { parseVersion } from './pythonVersion'; + +type PythonEnvInformation = { + arch: Architecture; + executable: { + filename: string; + sysPrefix: string; + mtime: number; + ctime: number; + }; + version: PythonVersion; +}; + +/** + * Compose full interpreter information based on the given data. + * + * The data format corresponds to the output of the `interpreterInfo.py` script. + * + * @param python - the path to the Python executable + * @param raw - the information returned by the `interpreterInfo.py` script + */ +export function extractPythonEnvInfo(python: string, raw: PythonEnvInfo): PythonEnvInformation { + const rawVersion = `${raw.versionInfo.slice(0, 3).join('.')}-${raw.versionInfo[3]}`; + const version = parseVersion(rawVersion); + version.sysVersion = raw.sysVersion; + return { + arch: raw.is64Bit ? Architecture.x64 : Architecture.x86, + executable: { + filename: python, + sysPrefix: raw.sysPrefix, + mtime: -1, + ctime: -1, + }, + version: parseVersion(rawVersion), + }; +} + +type ShellExecResult = { + stdout: string; + stderr?: string; +}; +type ShellExecFunc = (command: string, timeout: number) => Promise; + +type Logger = { + info(msg: string): void; + error(msg: string): void; +}; + +/** + * Collect full interpreter information from the given Python executable. + * + * @param python - the information to use when running Python + * @param shellExec - the function to use to exec Python + * @param logger - if provided, used to log failures or other info + */ +export async function getInterpreterInfo( + python: PythonExecInfo, + shellExec: ShellExecFunc, + logger?: Logger, +): Promise { + const [args, parse] = getInterpreterInfoCommand(); + const info = copyPythonExecInfo(python, args); + const argv = [info.command, ...info.args]; + + // Concat these together to make a set of quoted strings + const quoted = argv.reduce((p, c) => (p ? `${p} "${c}"` : `"${c.replace('\\', '\\\\')}"`), ''); + + // Try shell execing the command, followed by the arguments. This will make node kill the process if it + // takes too long. + // Sometimes the python path isn't valid, timeout if that's the case. + // See these two bugs: + // https://github.com/microsoft/vscode-python/issues/7569 + // https://github.com/microsoft/vscode-python/issues/7760 + const result = await shellExec(quoted, 15000); + if (result.stderr) { + if (logger) { + logger.error(`Failed to parse interpreter information for ${argv} stderr: ${result.stderr}`); + } + return undefined; + } + const json = parse(result.stdout); + if (logger) { + logger.info(`Found interpreter for ${argv}`); + } + return extractPythonEnvInfo(python.pythonExecutable, json); +} diff --git a/src/client/pythonEnvironments/base/info/pythonVersion.ts b/src/client/pythonEnvironments/base/info/pythonVersion.ts new file mode 100644 index 000000000000..58248f2c1789 --- /dev/null +++ b/src/client/pythonEnvironments/base/info/pythonVersion.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { PythonReleaseLevel, PythonVersion } from '.'; +import { EMPTY_VERSION, parseBasicVersionInfo } from '../../../common/utils/version'; + +export function parseVersion(versionStr: string): PythonVersion { + const parsed = parseBasicVersionInfo(versionStr); + if (!parsed) { + if (versionStr === '') { + return EMPTY_VERSION as PythonVersion; + } + throw Error(`invalid version ${versionStr}`); + } + const { version, after } = parsed; + const match = after.match(/^(a|b|rc)(\d+)$/); + if (match) { + const [, levelStr, serialStr] = match; + let level: PythonReleaseLevel; + if (levelStr === 'a') { + level = PythonReleaseLevel.Alpha; + } else if (levelStr === 'b') { + level = PythonReleaseLevel.Beta; + } else if (levelStr === 'rc') { + level = PythonReleaseLevel.Candidate; + } else { + throw Error('unreachable!'); + } + version.release = { + level, + serial: parseInt(serialStr, 10), + }; + } + return version; +} diff --git a/src/client/pythonEnvironments/info/environmentInfoService.ts b/src/client/pythonEnvironments/info/environmentInfoService.ts index 12a84f017692..85f86fb1ad0d 100644 --- a/src/client/pythonEnvironments/info/environmentInfoService.ts +++ b/src/client/pythonEnvironments/info/environmentInfoService.ts @@ -2,11 +2,11 @@ // Licensed under the MIT License. import { injectable } from 'inversify'; -import { EnvironmentType, PythonEnvironment } from '.'; import { createWorkerPool, IWorkerPool, QueuePosition } from '../../common/utils/workerPool'; +import { PythonEnvInfo } from '../base/info'; +import { getInterpreterInfo } from '../base/info/interpreter'; import { shellExecute } from '../common/externalDependencies'; import { buildPythonExecInfo } from '../exec'; -import { getInterpreterInfo } from './interpreter'; export enum EnvironmentInfoServiceQueuePriority { Default, @@ -16,39 +16,24 @@ export enum EnvironmentInfoServiceQueuePriority { export const IEnvironmentInfoService = Symbol('IEnvironmentInfoService'); export interface IEnvironmentInfoService { getEnvironmentInfo( - interpreterPath: string, + environment: PythonEnvInfo, priority?: EnvironmentInfoServiceQueuePriority - ): Promise; + ): Promise; } -async function buildEnvironmentInfo(interpreterPath: string): Promise { - const interpreterInfo = await getInterpreterInfo(buildPythonExecInfo(interpreterPath), shellExecute); +async function buildEnvironmentInfo(environment: PythonEnvInfo): Promise { + const interpreterInfo = await getInterpreterInfo( + buildPythonExecInfo(environment.executable.filename), + shellExecute, + ); if (interpreterInfo === undefined || interpreterInfo.version === undefined) { return undefined; } - return { - path: interpreterInfo.path, - // Have to do this because the type returned by getInterpreterInfo is SemVer - // But we expect this to be PythonVersion - version: { - raw: interpreterInfo.version.raw, - major: interpreterInfo.version.major, - minor: interpreterInfo.version.minor, - patch: interpreterInfo.version.patch, - build: interpreterInfo.version.build, - prerelease: interpreterInfo.version.prerelease, - }, - sysVersion: interpreterInfo.sysVersion, - architecture: interpreterInfo.architecture, - sysPrefix: interpreterInfo.sysPrefix, - pipEnvWorkspaceFolder: interpreterInfo.pipEnvWorkspaceFolder, - companyDisplayName: '', - displayName: '', - envType: EnvironmentType.Unknown, // Code to handle This will be added later. - envName: '', - envPath: '', - cachedEntry: false, - }; + environment.version = interpreterInfo.version; + environment.executable.filename = interpreterInfo.executable.filename; + environment.executable.sysPrefix = interpreterInfo.executable.sysPrefix; + environment.arch = interpreterInfo.arch; + return environment; } @injectable() @@ -57,26 +42,27 @@ export class EnvironmentInfoService implements IEnvironmentInfoService { // path again and again in a given session. This information will likely not change in a given // session. There are definitely cases where this will change. But a simple reload should address // those. - private readonly cache: Map = new Map(); + private readonly cache: Map = new Map(); - private readonly workerPool: IWorkerPool; + private readonly workerPool: IWorkerPool; public constructor() { - this.workerPool = createWorkerPool(buildEnvironmentInfo); + this.workerPool = createWorkerPool(buildEnvironmentInfo); } public async getEnvironmentInfo( - interpreterPath: string, + environment: PythonEnvInfo, priority?: EnvironmentInfoServiceQueuePriority, - ): Promise { + ): Promise { + const interpreterPath = environment.executable.filename; const result = this.cache.get(interpreterPath); if (result !== undefined) { return result; } return (priority === EnvironmentInfoServiceQueuePriority.High - ? this.workerPool.addToQueue(interpreterPath, QueuePosition.Front) - : this.workerPool.addToQueue(interpreterPath, QueuePosition.Back) + ? this.workerPool.addToQueue(environment, QueuePosition.Front) + : this.workerPool.addToQueue(environment, QueuePosition.Back) ).then((r) => { if (r !== undefined) { this.cache.set(interpreterPath, r); diff --git a/src/test/pythonEnvironments/base/common.ts b/src/test/pythonEnvironments/base/common.ts index e55e0a64391b..6f56011f1f03 100644 --- a/src/test/pythonEnvironments/base/common.ts +++ b/src/test/pythonEnvironments/base/common.ts @@ -3,13 +3,11 @@ import { createDeferred, flattenIterator, iterable, mapToIterator } from '../../../client/common/utils/async'; import { Architecture } from '../../../client/common/utils/platform'; -import { EMPTY_VERSION, parseBasicVersionInfo } from '../../../client/common/utils/version'; import { PythonEnvInfo, PythonEnvKind, - PythonReleaseLevel, - PythonVersion } from '../../../client/pythonEnvironments/base/info'; +import { parseVersion } from '../../../client/pythonEnvironments/base/info/pythonVersion'; import { IPythonEnvsIterator, Locator, PythonLocatorQuery } from '../../../client/pythonEnvironments/base/locator'; import { PythonEnvsChangedEvent } from '../../../client/pythonEnvironments/base/watcher'; @@ -45,36 +43,6 @@ export function createEnv( }; } -function parseVersion(versionStr: string): PythonVersion { - const parsed = parseBasicVersionInfo(versionStr); - if (!parsed) { - if (versionStr === '') { - return EMPTY_VERSION as PythonVersion; - } - throw Error(`invalid version ${versionStr}`); - } - const { version, after } = parsed; - const match = after.match(/^(a|b|rc)(\d+)$/); - if (match) { - const [, levelStr, serialStr ] = match; - let level: PythonReleaseLevel; - if (levelStr === 'a') { - level = PythonReleaseLevel.Alpha; - } else if (levelStr === 'b') { - level = PythonReleaseLevel.Beta; - } else if (levelStr === 'rc') { - level = PythonReleaseLevel.Candidate; - } else { - throw Error('unreachable!'); - } - version.release = { - level, - serial: parseInt(serialStr, 10) - }; - } - return version; -} - export function createLocatedEnv( location: string, versionStr: string, diff --git a/src/test/pythonEnvironments/info/environmentInfoService.functional.test.ts b/src/test/pythonEnvironments/info/environmentInfoService.functional.test.ts index 9d810f96f2d9..b0803ba064d2 100644 --- a/src/test/pythonEnvironments/info/environmentInfoService.functional.test.ts +++ b/src/test/pythonEnvironments/info/environmentInfoService.functional.test.ts @@ -8,8 +8,9 @@ import * as sinon from 'sinon'; import { ImportMock } from 'ts-mock-imports'; import { ExecutionResult } from '../../../client/common/process/types'; import { Architecture } from '../../../client/common/utils/platform'; +import { PythonEnvInfo, PythonEnvKind } from '../../../client/pythonEnvironments/base/info'; +import { parseVersion } from '../../../client/pythonEnvironments/base/info/pythonVersion'; import * as ExternalDep from '../../../client/pythonEnvironments/common/externalDependencies'; -import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; import { EnvironmentInfoService, EnvironmentInfoServiceQueuePriority, @@ -18,27 +19,39 @@ import { suite('Environment Info Service', () => { let stubShellExec: sinon.SinonStub; - function createExpectedEnvInfo(path: string): PythonEnvironment { + function createEnvInfo(executable: string): PythonEnvInfo { return { - path, - architecture: Architecture.x64, - sysVersion: undefined, - sysPrefix: 'path', - pipEnvWorkspaceFolder: undefined, - version: { - build: [], - major: 3, - minor: 8, - patch: 3, - prerelease: ['final'], - raw: '3.8.3-final', + id: '', + kind: PythonEnvKind.Unknown, + version: parseVersion('0.0.0'), + name: '', + location: '', + arch: Architecture.x64, + executable: { + filename: executable, + sysPrefix: '', + mtime: -1, + ctime: -1, }, - companyDisplayName: '', - displayName: '', - envType: EnvironmentType.Unknown, - envName: '', - envPath: '', - cachedEntry: false, + distro: { org: '' }, + }; + } + + function createExpectedEnvInfo(executable: string): PythonEnvInfo { + return { + id: '', + kind: PythonEnvKind.Unknown, + version: parseVersion('3.8.3-final'), + name: '', + location: '', + arch: Architecture.x64, + executable: { + filename: executable, + sysPrefix: 'path', + mtime: -1, + ctime: -1, + }, + distro: { org: '' }, }; } @@ -59,14 +72,16 @@ suite('Environment Info Service', () => { }); test('Add items to queue and get results', async () => { const envService = new EnvironmentInfoService(); - const promises: Promise[] = []; - const expected: PythonEnvironment[] = []; + const promises: Promise[] = []; + const expected: PythonEnvInfo[] = []; for (let i = 0; i < 10; i = i + 1) { const path = `any-path${i}`; if (i < 5) { - promises.push(envService.getEnvironmentInfo(path)); + promises.push(envService.getEnvironmentInfo(createEnvInfo(path))); } else { - promises.push(envService.getEnvironmentInfo(path, EnvironmentInfoServiceQueuePriority.High)); + promises.push( + envService.getEnvironmentInfo(createEnvInfo(path), EnvironmentInfoServiceQueuePriority.High), + ); } expected.push(createExpectedEnvInfo(path)); } @@ -82,17 +97,17 @@ suite('Environment Info Service', () => { test('Add same item to queue', async () => { const envService = new EnvironmentInfoService(); - const promises: Promise[] = []; - const expected: PythonEnvironment[] = []; + const promises: Promise[] = []; + const expected: PythonEnvInfo[] = []; const path = 'any-path'; // Clear call counts stubShellExec.resetHistory(); // Evaluate once so the result is cached. - await envService.getEnvironmentInfo(path); + await envService.getEnvironmentInfo(createEnvInfo(path)); for (let i = 0; i < 10; i = i + 1) { - promises.push(envService.getEnvironmentInfo(path)); + promises.push(envService.getEnvironmentInfo(createEnvInfo(path))); expected.push(createExpectedEnvInfo(path)); } From b3e4222c9ef368166b8190f13d84e171e1b2849e Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 21 Sep 2020 05:48:04 -0700 Subject: [PATCH 2/8] Do not replace existing fields and return a new object --- .../info/environmentInfoService.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/client/pythonEnvironments/info/environmentInfoService.ts b/src/client/pythonEnvironments/info/environmentInfoService.ts index 85f86fb1ad0d..4d87506628ac 100644 --- a/src/client/pythonEnvironments/info/environmentInfoService.ts +++ b/src/client/pythonEnvironments/info/environmentInfoService.ts @@ -29,11 +29,13 @@ async function buildEnvironmentInfo(environment: PythonEnvInfo): Promise Date: Mon, 21 Sep 2020 06:45:16 -0700 Subject: [PATCH 3/8] Modify cache to carry deferred instead --- .../info/environmentInfoService.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/client/pythonEnvironments/info/environmentInfoService.ts b/src/client/pythonEnvironments/info/environmentInfoService.ts index 4d87506628ac..30ff146ce048 100644 --- a/src/client/pythonEnvironments/info/environmentInfoService.ts +++ b/src/client/pythonEnvironments/info/environmentInfoService.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { injectable } from 'inversify'; +import { createDeferred, Deferred } from '../../common/utils/async'; import { createWorkerPool, IWorkerPool, QueuePosition } from '../../common/utils/workerPool'; import { PythonEnvInfo } from '../base/info'; import { getInterpreterInfo } from '../base/info/interpreter'; @@ -44,7 +45,7 @@ export class EnvironmentInfoService implements IEnvironmentInfoService { // path again and again in a given session. This information will likely not change in a given // session. There are definitely cases where this will change. But a simple reload should address // those. - private readonly cache: Map = new Map(); + private readonly cache: Map> = new Map>(); private readonly workerPool: IWorkerPool; @@ -59,15 +60,19 @@ export class EnvironmentInfoService implements IEnvironmentInfoService { const interpreterPath = environment.executable.filename; const result = this.cache.get(interpreterPath); if (result !== undefined) { - return result; + // Another call for this environment has already been made, return its result + return result.promise; } - + const deferred = createDeferred(); + this.cache.set(interpreterPath, deferred); return (priority === EnvironmentInfoServiceQueuePriority.High ? this.workerPool.addToQueue(environment, QueuePosition.Front) : this.workerPool.addToQueue(environment, QueuePosition.Back) ).then((r) => { if (r !== undefined) { - this.cache.set(interpreterPath, r); + deferred.resolve(r); + } else { + this.cache.delete(interpreterPath); } return r; }); From 87d84e6d83afc86beaea86bdae550a971648d362 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 21 Sep 2020 07:03:47 -0700 Subject: [PATCH 4/8] Change worker to return interpreter information instead --- .../base/info/interpreter.ts | 6 +-- .../info/environmentInfoService.ts | 47 ++++++++----------- .../environmentInfoService.functional.test.ts | 45 ++++-------------- 3 files changed, 32 insertions(+), 66 deletions(-) diff --git a/src/client/pythonEnvironments/base/info/interpreter.ts b/src/client/pythonEnvironments/base/info/interpreter.ts index 4acb15f5ee00..f6daa31ee03b 100644 --- a/src/client/pythonEnvironments/base/info/interpreter.ts +++ b/src/client/pythonEnvironments/base/info/interpreter.ts @@ -7,7 +7,7 @@ import { Architecture } from '../../../common/utils/platform'; import { copyPythonExecInfo, PythonExecInfo } from '../../exec'; import { parseVersion } from './pythonVersion'; -type PythonEnvInformation = { +export type InterpreterInformation = { arch: Architecture; executable: { filename: string; @@ -26,7 +26,7 @@ type PythonEnvInformation = { * @param python - the path to the Python executable * @param raw - the information returned by the `interpreterInfo.py` script */ -export function extractPythonEnvInfo(python: string, raw: PythonEnvInfo): PythonEnvInformation { +export function extractPythonEnvInfo(python: string, raw: PythonEnvInfo): InterpreterInformation { const rawVersion = `${raw.versionInfo.slice(0, 3).join('.')}-${raw.versionInfo[3]}`; const version = parseVersion(rawVersion); version.sysVersion = raw.sysVersion; @@ -64,7 +64,7 @@ export async function getInterpreterInfo( python: PythonExecInfo, shellExec: ShellExecFunc, logger?: Logger, -): Promise { +): Promise { const [args, parse] = getInterpreterInfoCommand(); const info = copyPythonExecInfo(python, args); const argv = [info.command, ...info.args]; diff --git a/src/client/pythonEnvironments/info/environmentInfoService.ts b/src/client/pythonEnvironments/info/environmentInfoService.ts index 30ff146ce048..bca3d5d7a973 100644 --- a/src/client/pythonEnvironments/info/environmentInfoService.ts +++ b/src/client/pythonEnvironments/info/environmentInfoService.ts @@ -4,8 +4,7 @@ import { injectable } from 'inversify'; import { createDeferred, Deferred } from '../../common/utils/async'; import { createWorkerPool, IWorkerPool, QueuePosition } from '../../common/utils/workerPool'; -import { PythonEnvInfo } from '../base/info'; -import { getInterpreterInfo } from '../base/info/interpreter'; +import { getInterpreterInfo, InterpreterInformation } from '../base/info/interpreter'; import { shellExecute } from '../common/externalDependencies'; import { buildPythonExecInfo } from '../exec'; @@ -17,26 +16,17 @@ export enum EnvironmentInfoServiceQueuePriority { export const IEnvironmentInfoService = Symbol('IEnvironmentInfoService'); export interface IEnvironmentInfoService { getEnvironmentInfo( - environment: PythonEnvInfo, + interpreterPath: string, priority?: EnvironmentInfoServiceQueuePriority - ): Promise; + ): Promise; } -async function buildEnvironmentInfo(environment: PythonEnvInfo): Promise { - const interpreterInfo = await getInterpreterInfo( - buildPythonExecInfo(environment.executable.filename), - shellExecute, - ); +async function buildEnvironmentInfo(interpreterPath: string): Promise { + const interpreterInfo = await getInterpreterInfo(buildPythonExecInfo(interpreterPath), shellExecute); if (interpreterInfo === undefined || interpreterInfo.version === undefined) { return undefined; } - // Deep copy into a new object - const resolvedEnv = JSON.parse(JSON.stringify(environment)) as PythonEnvInfo; - resolvedEnv.version = interpreterInfo.version; - resolvedEnv.executable.filename = interpreterInfo.executable.filename; - resolvedEnv.executable.sysPrefix = interpreterInfo.executable.sysPrefix; - resolvedEnv.arch = interpreterInfo.arch; - return resolvedEnv; + return interpreterInfo; } @injectable() @@ -45,33 +35,34 @@ export class EnvironmentInfoService implements IEnvironmentInfoService { // path again and again in a given session. This information will likely not change in a given // session. There are definitely cases where this will change. But a simple reload should address // those. - private readonly cache: Map> = new Map>(); + private readonly cache: Map> = new Map< + string, + Deferred + >(); - private readonly workerPool: IWorkerPool; + private readonly workerPool: IWorkerPool; public constructor() { - this.workerPool = createWorkerPool(buildEnvironmentInfo); + this.workerPool = createWorkerPool(buildEnvironmentInfo); } public async getEnvironmentInfo( - environment: PythonEnvInfo, + interpreterPath: string, priority?: EnvironmentInfoServiceQueuePriority, - ): Promise { - const interpreterPath = environment.executable.filename; + ): Promise { const result = this.cache.get(interpreterPath); if (result !== undefined) { // Another call for this environment has already been made, return its result return result.promise; } - const deferred = createDeferred(); + const deferred = createDeferred(); this.cache.set(interpreterPath, deferred); return (priority === EnvironmentInfoServiceQueuePriority.High - ? this.workerPool.addToQueue(environment, QueuePosition.Front) - : this.workerPool.addToQueue(environment, QueuePosition.Back) + ? this.workerPool.addToQueue(interpreterPath, QueuePosition.Front) + : this.workerPool.addToQueue(interpreterPath, QueuePosition.Back) ).then((r) => { - if (r !== undefined) { - deferred.resolve(r); - } else { + deferred.resolve(r); + if (r === undefined) { this.cache.delete(interpreterPath); } return r; diff --git a/src/test/pythonEnvironments/info/environmentInfoService.functional.test.ts b/src/test/pythonEnvironments/info/environmentInfoService.functional.test.ts index b0803ba064d2..6b0162b18b1b 100644 --- a/src/test/pythonEnvironments/info/environmentInfoService.functional.test.ts +++ b/src/test/pythonEnvironments/info/environmentInfoService.functional.test.ts @@ -8,7 +8,7 @@ import * as sinon from 'sinon'; import { ImportMock } from 'ts-mock-imports'; import { ExecutionResult } from '../../../client/common/process/types'; import { Architecture } from '../../../client/common/utils/platform'; -import { PythonEnvInfo, PythonEnvKind } from '../../../client/pythonEnvironments/base/info'; +import { InterpreterInformation } from '../../../client/pythonEnvironments/base/info/interpreter'; import { parseVersion } from '../../../client/pythonEnvironments/base/info/pythonVersion'; import * as ExternalDep from '../../../client/pythonEnvironments/common/externalDependencies'; import { @@ -19,31 +19,9 @@ import { suite('Environment Info Service', () => { let stubShellExec: sinon.SinonStub; - function createEnvInfo(executable: string): PythonEnvInfo { + function createExpectedEnvInfo(executable: string): InterpreterInformation { return { - id: '', - kind: PythonEnvKind.Unknown, - version: parseVersion('0.0.0'), - name: '', - location: '', - arch: Architecture.x64, - executable: { - filename: executable, - sysPrefix: '', - mtime: -1, - ctime: -1, - }, - distro: { org: '' }, - }; - } - - function createExpectedEnvInfo(executable: string): PythonEnvInfo { - return { - id: '', - kind: PythonEnvKind.Unknown, version: parseVersion('3.8.3-final'), - name: '', - location: '', arch: Architecture.x64, executable: { filename: executable, @@ -51,7 +29,6 @@ suite('Environment Info Service', () => { mtime: -1, ctime: -1, }, - distro: { org: '' }, }; } @@ -72,16 +49,14 @@ suite('Environment Info Service', () => { }); test('Add items to queue and get results', async () => { const envService = new EnvironmentInfoService(); - const promises: Promise[] = []; - const expected: PythonEnvInfo[] = []; + const promises: Promise[] = []; + const expected: InterpreterInformation[] = []; for (let i = 0; i < 10; i = i + 1) { const path = `any-path${i}`; if (i < 5) { - promises.push(envService.getEnvironmentInfo(createEnvInfo(path))); + promises.push(envService.getEnvironmentInfo(path)); } else { - promises.push( - envService.getEnvironmentInfo(createEnvInfo(path), EnvironmentInfoServiceQueuePriority.High), - ); + promises.push(envService.getEnvironmentInfo(path, EnvironmentInfoServiceQueuePriority.High)); } expected.push(createExpectedEnvInfo(path)); } @@ -97,17 +72,17 @@ suite('Environment Info Service', () => { test('Add same item to queue', async () => { const envService = new EnvironmentInfoService(); - const promises: Promise[] = []; - const expected: PythonEnvInfo[] = []; + const promises: Promise[] = []; + const expected: InterpreterInformation[] = []; const path = 'any-path'; // Clear call counts stubShellExec.resetHistory(); // Evaluate once so the result is cached. - await envService.getEnvironmentInfo(createEnvInfo(path)); + await envService.getEnvironmentInfo(path); for (let i = 0; i < 10; i = i + 1) { - promises.push(envService.getEnvironmentInfo(createEnvInfo(path))); + promises.push(envService.getEnvironmentInfo(path)); expected.push(createExpectedEnvInfo(path)); } From 148aaae9dbeae9e8fd4ab24e27da797ad19915f1 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 21 Sep 2020 07:32:46 -0700 Subject: [PATCH 5/8] Handle error and rename --- src/client/pythonEnvironments/base/info/interpreter.ts | 4 ++-- src/client/pythonEnvironments/info/environmentInfoService.ts | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/client/pythonEnvironments/base/info/interpreter.ts b/src/client/pythonEnvironments/base/info/interpreter.ts index f6daa31ee03b..62d29cfa8bd6 100644 --- a/src/client/pythonEnvironments/base/info/interpreter.ts +++ b/src/client/pythonEnvironments/base/info/interpreter.ts @@ -26,7 +26,7 @@ export type InterpreterInformation = { * @param python - the path to the Python executable * @param raw - the information returned by the `interpreterInfo.py` script */ -export function extractPythonEnvInfo(python: string, raw: PythonEnvInfo): InterpreterInformation { +function extractInterpreterInfo(python: string, raw: PythonEnvInfo): InterpreterInformation { const rawVersion = `${raw.versionInfo.slice(0, 3).join('.')}-${raw.versionInfo[3]}`; const version = parseVersion(rawVersion); version.sysVersion = raw.sysVersion; @@ -89,5 +89,5 @@ export async function getInterpreterInfo( if (logger) { logger.info(`Found interpreter for ${argv}`); } - return extractPythonEnvInfo(python.pythonExecutable, json); + return extractInterpreterInfo(python.pythonExecutable, json); } diff --git a/src/client/pythonEnvironments/info/environmentInfoService.ts b/src/client/pythonEnvironments/info/environmentInfoService.ts index bca3d5d7a973..6ab761c86278 100644 --- a/src/client/pythonEnvironments/info/environmentInfoService.ts +++ b/src/client/pythonEnvironments/info/environmentInfoService.ts @@ -22,7 +22,9 @@ export interface IEnvironmentInfoService { } async function buildEnvironmentInfo(interpreterPath: string): Promise { - const interpreterInfo = await getInterpreterInfo(buildPythonExecInfo(interpreterPath), shellExecute); + const interpreterInfo = await getInterpreterInfo(buildPythonExecInfo(interpreterPath), shellExecute).catch( + () => undefined, + ); if (interpreterInfo === undefined || interpreterInfo.version === undefined) { return undefined; } From 6cf5a3e9b7523f5f93eabd0ad605d3fb8f228c05 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 22 Sep 2020 06:10:08 -0700 Subject: [PATCH 6/8] Fix bug with interpreterInfo.py --- pythonFiles/interpreterInfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonFiles/interpreterInfo.py b/pythonFiles/interpreterInfo.py index bb284e729d71..601959b7c2d5 100644 --- a/pythonFiles/interpreterInfo.py +++ b/pythonFiles/interpreterInfo.py @@ -7,7 +7,7 @@ obj = {} obj["versionInfo"] = tuple(sys.version_info) obj["sysPrefix"] = sys.prefix -obj["version"] = sys.version +obj["sysVersion"] = sys.version obj["is64Bit"] = sys.maxsize > 2 ** 32 print(json.dumps(obj)) From eece52a2b40491e37e2b52aa53b43acf59ebf501 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 22 Sep 2020 06:10:38 -0700 Subject: [PATCH 7/8] Code reviews --- .../pythonEnvironments/base/info/interpreter.ts | 16 ++++++---------- .../environmentInfoService.functional.test.ts | 4 ++-- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/client/pythonEnvironments/base/info/interpreter.ts b/src/client/pythonEnvironments/base/info/interpreter.ts index 62d29cfa8bd6..a654ac0126ec 100644 --- a/src/client/pythonEnvironments/base/info/interpreter.ts +++ b/src/client/pythonEnvironments/base/info/interpreter.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { PythonVersion } from '.'; +import { PythonExecutableInfo, PythonVersion } from '.'; import { interpreterInfo as getInterpreterInfoCommand, PythonEnvInfo } from '../../../common/process/internal/scripts'; import { Architecture } from '../../../common/utils/platform'; import { copyPythonExecInfo, PythonExecInfo } from '../../exec'; @@ -9,12 +9,7 @@ import { parseVersion } from './pythonVersion'; export type InterpreterInformation = { arch: Architecture; - executable: { - filename: string; - sysPrefix: string; - mtime: number; - ctime: number; - }; + executable: PythonExecutableInfo; version: PythonVersion; }; @@ -28,8 +23,6 @@ export type InterpreterInformation = { */ function extractInterpreterInfo(python: string, raw: PythonEnvInfo): InterpreterInformation { const rawVersion = `${raw.versionInfo.slice(0, 3).join('.')}-${raw.versionInfo[3]}`; - const version = parseVersion(rawVersion); - version.sysVersion = raw.sysVersion; return { arch: raw.is64Bit ? Architecture.x64 : Architecture.x86, executable: { @@ -38,7 +31,10 @@ function extractInterpreterInfo(python: string, raw: PythonEnvInfo): Interpreter mtime: -1, ctime: -1, }, - version: parseVersion(rawVersion), + version: { + ...parseVersion(rawVersion), + sysVersion: raw.sysVersion, + }, }; } diff --git a/src/test/pythonEnvironments/info/environmentInfoService.functional.test.ts b/src/test/pythonEnvironments/info/environmentInfoService.functional.test.ts index 6b0162b18b1b..e1d64c353305 100644 --- a/src/test/pythonEnvironments/info/environmentInfoService.functional.test.ts +++ b/src/test/pythonEnvironments/info/environmentInfoService.functional.test.ts @@ -21,7 +21,7 @@ suite('Environment Info Service', () => { function createExpectedEnvInfo(executable: string): InterpreterInformation { return { - version: parseVersion('3.8.3-final'), + version: { ...parseVersion('3.8.3-final'), sysVersion: '3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]' }, arch: Architecture.x64, executable: { filename: executable, @@ -39,7 +39,7 @@ suite('Environment Info Service', () => { new Promise>((resolve) => { resolve({ stdout: - '{"versionInfo": [3, 8, 3, "final", 0], "sysPrefix": "path", "version": "3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]", "is64Bit": true}', + '{"versionInfo": [3, 8, 3, "final", 0], "sysPrefix": "path", "sysVersion": "3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]", "is64Bit": true}', }); }), ); From b5ce54a61a9f441a169b4118232636c6408043da Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 22 Sep 2020 11:32:36 -0700 Subject: [PATCH 8/8] Rename old PythonEnvInfo type to InterpreterInfoJson --- src/client/common/process/internal/scripts/index.ts | 8 ++++---- src/client/common/process/pythonDaemon.ts | 6 ++++-- src/client/pythonEnvironments/base/info/interpreter.ts | 5 +++-- src/client/pythonEnvironments/info/interpreter.ts | 4 ++-- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/client/common/process/internal/scripts/index.ts b/src/client/common/process/internal/scripts/index.ts index 74408e81e1e6..30bb3d913d49 100644 --- a/src/client/common/process/internal/scripts/index.ts +++ b/src/client/common/process/internal/scripts/index.ts @@ -41,19 +41,19 @@ export * as vscode_datascience_helpers from './vscode_datascience_helpers'; type ReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final'; type PythonVersionInfo = [number, number, number, ReleaseLevel, number]; -export type PythonEnvInfo = { +export type InterpreterInfoJson = { versionInfo: PythonVersionInfo; sysPrefix: string; sysVersion: string; is64Bit: boolean; }; -export function interpreterInfo(): [string[], (out: string) => PythonEnvInfo] { +export function interpreterInfo(): [string[], (out: string) => InterpreterInfoJson] { const script = path.join(SCRIPTS_DIR, 'interpreterInfo.py'); const args = [ISOLATED, script]; - function parse(out: string): PythonEnvInfo { - let json: PythonEnvInfo; + function parse(out: string): InterpreterInfoJson { + let json: InterpreterInfoJson; try { json = JSON.parse(out); } catch (ex) { diff --git a/src/client/common/process/pythonDaemon.ts b/src/client/common/process/pythonDaemon.ts index b588af7f8053..225d58804609 100644 --- a/src/client/common/process/pythonDaemon.ts +++ b/src/client/common/process/pythonDaemon.ts @@ -11,7 +11,7 @@ import { extractInterpreterInfo } from '../../pythonEnvironments/info/interprete import { traceWarning } from '../logger'; import { IPlatformService } from '../platform/types'; import { BasePythonDaemon } from './baseDaemon'; -import { PythonEnvInfo } from './internal/scripts'; +import { InterpreterInfoJson } from './internal/scripts'; import { IPythonDaemonExecutionService, IPythonExecutionService, @@ -45,7 +45,9 @@ export class PythonDaemonExecutionService extends BasePythonDaemon implements IP public async getInterpreterInformation(): Promise { try { this.throwIfRPCConnectionIsDead(); - const request = new RequestType0('get_interpreter_information'); + const request = new RequestType0( + 'get_interpreter_information' + ); const response = await this.sendRequestWithoutArgs(request); if (response.error) { throw Error(response.error); diff --git a/src/client/pythonEnvironments/base/info/interpreter.ts b/src/client/pythonEnvironments/base/info/interpreter.ts index a654ac0126ec..16d82d3280b5 100644 --- a/src/client/pythonEnvironments/base/info/interpreter.ts +++ b/src/client/pythonEnvironments/base/info/interpreter.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { PythonExecutableInfo, PythonVersion } from '.'; -import { interpreterInfo as getInterpreterInfoCommand, PythonEnvInfo } from '../../../common/process/internal/scripts'; +import { interpreterInfo as getInterpreterInfoCommand, InterpreterInfoJson } from '../../../common/process/internal/scripts'; import { Architecture } from '../../../common/utils/platform'; import { copyPythonExecInfo, PythonExecInfo } from '../../exec'; import { parseVersion } from './pythonVersion'; @@ -21,7 +21,7 @@ export type InterpreterInformation = { * @param python - the path to the Python executable * @param raw - the information returned by the `interpreterInfo.py` script */ -function extractInterpreterInfo(python: string, raw: PythonEnvInfo): InterpreterInformation { +function extractInterpreterInfo(python: string, raw: InterpreterInfoJson): InterpreterInformation { const rawVersion = `${raw.versionInfo.slice(0, 3).join('.')}-${raw.versionInfo[3]}`; return { arch: raw.is64Bit ? Architecture.x64 : Architecture.x86, @@ -46,6 +46,7 @@ type ShellExecFunc = (command: string, timeout: number) => Promise