Skip to content

Commit 07dac60

Browse files
author
Kartik Raj
committed
Modify environment info worker to support new environment type
1 parent 10b8965 commit 07dac60

File tree

5 files changed

+194
-97
lines changed

5 files changed

+194
-97
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { PythonVersion } from '.';
5+
import { interpreterInfo as getInterpreterInfoCommand, PythonEnvInfo } from '../../../common/process/internal/scripts';
6+
import { Architecture } from '../../../common/utils/platform';
7+
import { copyPythonExecInfo, PythonExecInfo } from '../../exec';
8+
import { parseVersion } from './pythonVersion';
9+
10+
type PythonEnvInformation = {
11+
arch: Architecture;
12+
executable: {
13+
filename: string;
14+
sysPrefix: string;
15+
mtime: number;
16+
ctime: number;
17+
};
18+
version: PythonVersion;
19+
};
20+
21+
/**
22+
* Compose full interpreter information based on the given data.
23+
*
24+
* The data format corresponds to the output of the `interpreterInfo.py` script.
25+
*
26+
* @param python - the path to the Python executable
27+
* @param raw - the information returned by the `interpreterInfo.py` script
28+
*/
29+
export function extractPythonEnvInfo(python: string, raw: PythonEnvInfo): PythonEnvInformation {
30+
const rawVersion = `${raw.versionInfo.slice(0, 3).join('.')}-${raw.versionInfo[3]}`;
31+
const version = parseVersion(rawVersion);
32+
version.sysVersion = raw.sysVersion;
33+
return {
34+
arch: raw.is64Bit ? Architecture.x64 : Architecture.x86,
35+
executable: {
36+
filename: python,
37+
sysPrefix: raw.sysPrefix,
38+
mtime: -1,
39+
ctime: -1,
40+
},
41+
version: parseVersion(rawVersion),
42+
};
43+
}
44+
45+
type ShellExecResult = {
46+
stdout: string;
47+
stderr?: string;
48+
};
49+
type ShellExecFunc = (command: string, timeout: number) => Promise<ShellExecResult>;
50+
51+
type Logger = {
52+
info(msg: string): void;
53+
error(msg: string): void;
54+
};
55+
56+
/**
57+
* Collect full interpreter information from the given Python executable.
58+
*
59+
* @param python - the information to use when running Python
60+
* @param shellExec - the function to use to exec Python
61+
* @param logger - if provided, used to log failures or other info
62+
*/
63+
export async function getInterpreterInfo(
64+
python: PythonExecInfo,
65+
shellExec: ShellExecFunc,
66+
logger?: Logger,
67+
): Promise<PythonEnvInformation | undefined> {
68+
const [args, parse] = getInterpreterInfoCommand();
69+
const info = copyPythonExecInfo(python, args);
70+
const argv = [info.command, ...info.args];
71+
72+
// Concat these together to make a set of quoted strings
73+
const quoted = argv.reduce((p, c) => (p ? `${p} "${c}"` : `"${c.replace('\\', '\\\\')}"`), '');
74+
75+
// Try shell execing the command, followed by the arguments. This will make node kill the process if it
76+
// takes too long.
77+
// Sometimes the python path isn't valid, timeout if that's the case.
78+
// See these two bugs:
79+
// https://github.com/microsoft/vscode-python/issues/7569
80+
// https://github.com/microsoft/vscode-python/issues/7760
81+
const result = await shellExec(quoted, 15000);
82+
if (result.stderr) {
83+
if (logger) {
84+
logger.error(`Failed to parse interpreter information for ${argv} stderr: ${result.stderr}`);
85+
}
86+
return undefined;
87+
}
88+
const json = parse(result.stdout);
89+
if (logger) {
90+
logger.info(`Found interpreter for ${argv}`);
91+
}
92+
return extractPythonEnvInfo(python.pythonExecutable, json);
93+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { PythonReleaseLevel, PythonVersion } from '.';
5+
import { EMPTY_VERSION, parseBasicVersionInfo } from '../../../common/utils/version';
6+
7+
export function parseVersion(versionStr: string): PythonVersion {
8+
const parsed = parseBasicVersionInfo<PythonVersion>(versionStr);
9+
if (!parsed) {
10+
if (versionStr === '') {
11+
return EMPTY_VERSION as PythonVersion;
12+
}
13+
throw Error(`invalid version ${versionStr}`);
14+
}
15+
const { version, after } = parsed;
16+
const match = after.match(/^(a|b|rc)(\d+)$/);
17+
if (match) {
18+
const [, levelStr, serialStr] = match;
19+
let level: PythonReleaseLevel;
20+
if (levelStr === 'a') {
21+
level = PythonReleaseLevel.Alpha;
22+
} else if (levelStr === 'b') {
23+
level = PythonReleaseLevel.Beta;
24+
} else if (levelStr === 'rc') {
25+
level = PythonReleaseLevel.Candidate;
26+
} else {
27+
throw Error('unreachable!');
28+
}
29+
version.release = {
30+
level,
31+
serial: parseInt(serialStr, 10),
32+
};
33+
}
34+
return version;
35+
}

src/client/pythonEnvironments/info/environmentInfoService.ts

Lines changed: 22 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
// Licensed under the MIT License.
33

44
import { injectable } from 'inversify';
5-
import { EnvironmentType, PythonEnvironment } from '.';
65
import { createWorkerPool, IWorkerPool, QueuePosition } from '../../common/utils/workerPool';
6+
import { PythonEnvInfo } from '../base/info';
7+
import { getInterpreterInfo } from '../base/info/interpreter';
78
import { shellExecute } from '../common/externalDependencies';
89
import { buildPythonExecInfo } from '../exec';
9-
import { getInterpreterInfo } from './interpreter';
1010

1111
export enum EnvironmentInfoServiceQueuePriority {
1212
Default,
@@ -16,39 +16,24 @@ export enum EnvironmentInfoServiceQueuePriority {
1616
export const IEnvironmentInfoService = Symbol('IEnvironmentInfoService');
1717
export interface IEnvironmentInfoService {
1818
getEnvironmentInfo(
19-
interpreterPath: string,
19+
environment: PythonEnvInfo,
2020
priority?: EnvironmentInfoServiceQueuePriority
21-
): Promise<PythonEnvironment | undefined>;
21+
): Promise<PythonEnvInfo | undefined>;
2222
}
2323

24-
async function buildEnvironmentInfo(interpreterPath: string): Promise<PythonEnvironment | undefined> {
25-
const interpreterInfo = await getInterpreterInfo(buildPythonExecInfo(interpreterPath), shellExecute);
24+
async function buildEnvironmentInfo(environment: PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
25+
const interpreterInfo = await getInterpreterInfo(
26+
buildPythonExecInfo(environment.executable.filename),
27+
shellExecute,
28+
);
2629
if (interpreterInfo === undefined || interpreterInfo.version === undefined) {
2730
return undefined;
2831
}
29-
return {
30-
path: interpreterInfo.path,
31-
// Have to do this because the type returned by getInterpreterInfo is SemVer
32-
// But we expect this to be PythonVersion
33-
version: {
34-
raw: interpreterInfo.version.raw,
35-
major: interpreterInfo.version.major,
36-
minor: interpreterInfo.version.minor,
37-
patch: interpreterInfo.version.patch,
38-
build: interpreterInfo.version.build,
39-
prerelease: interpreterInfo.version.prerelease,
40-
},
41-
sysVersion: interpreterInfo.sysVersion,
42-
architecture: interpreterInfo.architecture,
43-
sysPrefix: interpreterInfo.sysPrefix,
44-
pipEnvWorkspaceFolder: interpreterInfo.pipEnvWorkspaceFolder,
45-
companyDisplayName: '',
46-
displayName: '',
47-
envType: EnvironmentType.Unknown, // Code to handle This will be added later.
48-
envName: '',
49-
envPath: '',
50-
cachedEntry: false,
51-
};
32+
environment.version = interpreterInfo.version;
33+
environment.executable.filename = interpreterInfo.executable.filename;
34+
environment.executable.sysPrefix = interpreterInfo.executable.sysPrefix;
35+
environment.arch = interpreterInfo.arch;
36+
return environment;
5237
}
5338

5439
@injectable()
@@ -57,26 +42,27 @@ export class EnvironmentInfoService implements IEnvironmentInfoService {
5742
// path again and again in a given session. This information will likely not change in a given
5843
// session. There are definitely cases where this will change. But a simple reload should address
5944
// those.
60-
private readonly cache: Map<string, PythonEnvironment> = new Map<string, PythonEnvironment>();
45+
private readonly cache: Map<string, PythonEnvInfo> = new Map<string, PythonEnvInfo>();
6146

62-
private readonly workerPool: IWorkerPool<string, PythonEnvironment | undefined>;
47+
private readonly workerPool: IWorkerPool<PythonEnvInfo, PythonEnvInfo | undefined>;
6348

6449
public constructor() {
65-
this.workerPool = createWorkerPool<string, PythonEnvironment | undefined>(buildEnvironmentInfo);
50+
this.workerPool = createWorkerPool<PythonEnvInfo, PythonEnvInfo | undefined>(buildEnvironmentInfo);
6651
}
6752

6853
public async getEnvironmentInfo(
69-
interpreterPath: string,
54+
environment: PythonEnvInfo,
7055
priority?: EnvironmentInfoServiceQueuePriority,
71-
): Promise<PythonEnvironment | undefined> {
56+
): Promise<PythonEnvInfo | undefined> {
57+
const interpreterPath = environment.executable.filename;
7258
const result = this.cache.get(interpreterPath);
7359
if (result !== undefined) {
7460
return result;
7561
}
7662

7763
return (priority === EnvironmentInfoServiceQueuePriority.High
78-
? this.workerPool.addToQueue(interpreterPath, QueuePosition.Front)
79-
: this.workerPool.addToQueue(interpreterPath, QueuePosition.Back)
64+
? this.workerPool.addToQueue(environment, QueuePosition.Front)
65+
: this.workerPool.addToQueue(environment, QueuePosition.Back)
8066
).then((r) => {
8167
if (r !== undefined) {
8268
this.cache.set(interpreterPath, r);

src/test/pythonEnvironments/base/common.ts

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@
33

44
import { createDeferred, flattenIterator, iterable, mapToIterator } from '../../../client/common/utils/async';
55
import { Architecture } from '../../../client/common/utils/platform';
6-
import { EMPTY_VERSION, parseBasicVersionInfo } from '../../../client/common/utils/version';
76
import {
87
PythonEnvInfo,
98
PythonEnvKind,
10-
PythonReleaseLevel,
11-
PythonVersion
129
} from '../../../client/pythonEnvironments/base/info';
10+
import { parseVersion } from '../../../client/pythonEnvironments/base/info/pythonVersion';
1311
import { IPythonEnvsIterator, Locator, PythonLocatorQuery } from '../../../client/pythonEnvironments/base/locator';
1412
import { PythonEnvsChangedEvent } from '../../../client/pythonEnvironments/base/watcher';
1513

@@ -45,36 +43,6 @@ export function createEnv(
4543
};
4644
}
4745

48-
function parseVersion(versionStr: string): PythonVersion {
49-
const parsed = parseBasicVersionInfo<PythonVersion>(versionStr);
50-
if (!parsed) {
51-
if (versionStr === '') {
52-
return EMPTY_VERSION as PythonVersion;
53-
}
54-
throw Error(`invalid version ${versionStr}`);
55-
}
56-
const { version, after } = parsed;
57-
const match = after.match(/^(a|b|rc)(\d+)$/);
58-
if (match) {
59-
const [, levelStr, serialStr ] = match;
60-
let level: PythonReleaseLevel;
61-
if (levelStr === 'a') {
62-
level = PythonReleaseLevel.Alpha;
63-
} else if (levelStr === 'b') {
64-
level = PythonReleaseLevel.Beta;
65-
} else if (levelStr === 'rc') {
66-
level = PythonReleaseLevel.Candidate;
67-
} else {
68-
throw Error('unreachable!');
69-
}
70-
version.release = {
71-
level,
72-
serial: parseInt(serialStr, 10)
73-
};
74-
}
75-
return version;
76-
}
77-
7846
export function createLocatedEnv(
7947
location: string,
8048
versionStr: string,

src/test/pythonEnvironments/info/environmentInfoService.functional.test.ts

Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import * as sinon from 'sinon';
88
import { ImportMock } from 'ts-mock-imports';
99
import { ExecutionResult } from '../../../client/common/process/types';
1010
import { Architecture } from '../../../client/common/utils/platform';
11+
import { PythonEnvInfo, PythonEnvKind } from '../../../client/pythonEnvironments/base/info';
12+
import { parseVersion } from '../../../client/pythonEnvironments/base/info/pythonVersion';
1113
import * as ExternalDep from '../../../client/pythonEnvironments/common/externalDependencies';
12-
import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info';
1314
import {
1415
EnvironmentInfoService,
1516
EnvironmentInfoServiceQueuePriority,
@@ -18,27 +19,39 @@ import {
1819
suite('Environment Info Service', () => {
1920
let stubShellExec: sinon.SinonStub;
2021

21-
function createExpectedEnvInfo(path: string): PythonEnvironment {
22+
function createEnvInfo(executable: string): PythonEnvInfo {
2223
return {
23-
path,
24-
architecture: Architecture.x64,
25-
sysVersion: undefined,
26-
sysPrefix: 'path',
27-
pipEnvWorkspaceFolder: undefined,
28-
version: {
29-
build: [],
30-
major: 3,
31-
minor: 8,
32-
patch: 3,
33-
prerelease: ['final'],
34-
raw: '3.8.3-final',
24+
id: '',
25+
kind: PythonEnvKind.Unknown,
26+
version: parseVersion('0.0.0'),
27+
name: '',
28+
location: '',
29+
arch: Architecture.x64,
30+
executable: {
31+
filename: executable,
32+
sysPrefix: '',
33+
mtime: -1,
34+
ctime: -1,
3535
},
36-
companyDisplayName: '',
37-
displayName: '',
38-
envType: EnvironmentType.Unknown,
39-
envName: '',
40-
envPath: '',
41-
cachedEntry: false,
36+
distro: { org: '' },
37+
};
38+
}
39+
40+
function createExpectedEnvInfo(executable: string): PythonEnvInfo {
41+
return {
42+
id: '',
43+
kind: PythonEnvKind.Unknown,
44+
version: parseVersion('3.8.3-final'),
45+
name: '',
46+
location: '',
47+
arch: Architecture.x64,
48+
executable: {
49+
filename: executable,
50+
sysPrefix: 'path',
51+
mtime: -1,
52+
ctime: -1,
53+
},
54+
distro: { org: '' },
4255
};
4356
}
4457

@@ -59,14 +72,16 @@ suite('Environment Info Service', () => {
5972
});
6073
test('Add items to queue and get results', async () => {
6174
const envService = new EnvironmentInfoService();
62-
const promises: Promise<PythonEnvironment | undefined>[] = [];
63-
const expected: PythonEnvironment[] = [];
75+
const promises: Promise<PythonEnvInfo | undefined>[] = [];
76+
const expected: PythonEnvInfo[] = [];
6477
for (let i = 0; i < 10; i = i + 1) {
6578
const path = `any-path${i}`;
6679
if (i < 5) {
67-
promises.push(envService.getEnvironmentInfo(path));
80+
promises.push(envService.getEnvironmentInfo(createEnvInfo(path)));
6881
} else {
69-
promises.push(envService.getEnvironmentInfo(path, EnvironmentInfoServiceQueuePriority.High));
82+
promises.push(
83+
envService.getEnvironmentInfo(createEnvInfo(path), EnvironmentInfoServiceQueuePriority.High),
84+
);
7085
}
7186
expected.push(createExpectedEnvInfo(path));
7287
}
@@ -82,17 +97,17 @@ suite('Environment Info Service', () => {
8297

8398
test('Add same item to queue', async () => {
8499
const envService = new EnvironmentInfoService();
85-
const promises: Promise<PythonEnvironment | undefined>[] = [];
86-
const expected: PythonEnvironment[] = [];
100+
const promises: Promise<PythonEnvInfo | undefined>[] = [];
101+
const expected: PythonEnvInfo[] = [];
87102

88103
const path = 'any-path';
89104
// Clear call counts
90105
stubShellExec.resetHistory();
91106
// Evaluate once so the result is cached.
92-
await envService.getEnvironmentInfo(path);
107+
await envService.getEnvironmentInfo(createEnvInfo(path));
93108

94109
for (let i = 0; i < 10; i = i + 1) {
95-
promises.push(envService.getEnvironmentInfo(path));
110+
promises.push(envService.getEnvironmentInfo(createEnvInfo(path)));
96111
expected.push(createExpectedEnvInfo(path));
97112
}
98113

0 commit comments

Comments
 (0)