Skip to content
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

Modify environment info worker to support new type and to work with resolver #13997

Merged
merged 8 commits into from
Sep 22, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions src/client/pythonEnvironments/base/info/interpreter.ts
Original file line number Diff line number Diff line change
@@ -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;
};
karrtikr marked this conversation as resolved.
Show resolved Hide resolved
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;
karrtikr marked this conversation as resolved.
Show resolved Hide resolved
karrtikr marked this conversation as resolved.
Show resolved Hide resolved
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<ShellExecResult>;

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(
Copy link
Author

@karrtikr karrtikr Sep 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of it is copied from src\client\pythonEnvironments\info\interpreter.ts which is to be removed later. All the new types are being put inside base folder.

python: PythonExecInfo,
shellExec: ShellExecFunc,
logger?: Logger,
): Promise<PythonEnvInformation | undefined> {
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);
}
35 changes: 35 additions & 0 deletions src/client/pythonEnvironments/base/info/pythonVersion.ts
Original file line number Diff line number Diff line change
@@ -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<PythonVersion>(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;
}
60 changes: 24 additions & 36 deletions src/client/pythonEnvironments/info/environmentInfoService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -16,39 +16,26 @@ export enum EnvironmentInfoServiceQueuePriority {
export const IEnvironmentInfoService = Symbol('IEnvironmentInfoService');
export interface IEnvironmentInfoService {
getEnvironmentInfo(
interpreterPath: string,
environment: PythonEnvInfo,
priority?: EnvironmentInfoServiceQueuePriority
): Promise<PythonEnvironment | undefined>;
): Promise<PythonEnvInfo | undefined>;
}

async function buildEnvironmentInfo(interpreterPath: string): Promise<PythonEnvironment | undefined> {
const interpreterInfo = await getInterpreterInfo(buildPythonExecInfo(interpreterPath), shellExecute);
async function buildEnvironmentInfo(environment: PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
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,
};
// Deep copy into a new object
karrtikr marked this conversation as resolved.
Show resolved Hide resolved
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;
}

@injectable()
Expand All @@ -57,26 +44,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<string, PythonEnvironment> = new Map<string, PythonEnvironment>();
private readonly cache: Map<string, PythonEnvInfo> = new Map<string, PythonEnvInfo>();

private readonly workerPool: IWorkerPool<string, PythonEnvironment | undefined>;
private readonly workerPool: IWorkerPool<PythonEnvInfo, PythonEnvInfo | undefined>;

public constructor() {
this.workerPool = createWorkerPool<string, PythonEnvironment | undefined>(buildEnvironmentInfo);
this.workerPool = createWorkerPool<PythonEnvInfo, PythonEnvInfo | undefined>(buildEnvironmentInfo);
}

public async getEnvironmentInfo(
interpreterPath: string,
environment: PythonEnvInfo,
priority?: EnvironmentInfoServiceQueuePriority,
): Promise<PythonEnvironment | undefined> {
): Promise<PythonEnvInfo | undefined> {
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);
Expand Down
34 changes: 1 addition & 33 deletions src/test/pythonEnvironments/base/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -45,36 +43,6 @@ export function createEnv(
};
}

function parseVersion(versionStr: string): PythonVersion {
const parsed = parseBasicVersionInfo<PythonVersion>(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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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: '' },
};
}

Expand All @@ -59,14 +72,16 @@ suite('Environment Info Service', () => {
});
test('Add items to queue and get results', async () => {
const envService = new EnvironmentInfoService();
const promises: Promise<PythonEnvironment | undefined>[] = [];
const expected: PythonEnvironment[] = [];
const promises: Promise<PythonEnvInfo | undefined>[] = [];
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));
}
Expand All @@ -82,17 +97,17 @@ suite('Environment Info Service', () => {

test('Add same item to queue', async () => {
const envService = new EnvironmentInfoService();
const promises: Promise<PythonEnvironment | undefined>[] = [];
const expected: PythonEnvironment[] = [];
const promises: Promise<PythonEnvInfo | undefined>[] = [];
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));
}

Expand Down