Skip to content

Commit ec568ca

Browse files
author
Kartik Raj
authored
Modify environment info worker to support new type and to work with resolver (#13997)
* Modify environment info worker to support new environment type * Do not replace existing fields and return a new object * Modify cache to carry deferred instead * Change worker to return interpreter information instead * Handle error and rename * Fix bug with interpreterInfo.py * Code reviews * Rename old PythonEnvInfo type to InterpreterInfoJson
1 parent a7176fc commit ec568ca

File tree

9 files changed

+174
-103
lines changed

9 files changed

+174
-103
lines changed

pythonFiles/interpreterInfo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
obj = {}
88
obj["versionInfo"] = tuple(sys.version_info)
99
obj["sysPrefix"] = sys.prefix
10-
obj["version"] = sys.version
10+
obj["sysVersion"] = sys.version
1111
obj["is64Bit"] = sys.maxsize > 2 ** 32
1212

1313
print(json.dumps(obj))

src/client/common/process/internal/scripts/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,19 @@ export * as vscode_datascience_helpers from './vscode_datascience_helpers';
4141

4242
type ReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final';
4343
type PythonVersionInfo = [number, number, number, ReleaseLevel, number];
44-
export type PythonEnvInfo = {
44+
export type InterpreterInfoJson = {
4545
versionInfo: PythonVersionInfo;
4646
sysPrefix: string;
4747
sysVersion: string;
4848
is64Bit: boolean;
4949
};
5050

51-
export function interpreterInfo(): [string[], (out: string) => PythonEnvInfo] {
51+
export function interpreterInfo(): [string[], (out: string) => InterpreterInfoJson] {
5252
const script = path.join(SCRIPTS_DIR, 'interpreterInfo.py');
5353
const args = [ISOLATED, script];
5454

55-
function parse(out: string): PythonEnvInfo {
56-
let json: PythonEnvInfo;
55+
function parse(out: string): InterpreterInfoJson {
56+
let json: InterpreterInfoJson;
5757
try {
5858
json = JSON.parse(out);
5959
} catch (ex) {

src/client/common/process/pythonDaemon.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { extractInterpreterInfo } from '../../pythonEnvironments/info/interprete
1111
import { traceWarning } from '../logger';
1212
import { IPlatformService } from '../platform/types';
1313
import { BasePythonDaemon } from './baseDaemon';
14-
import { PythonEnvInfo } from './internal/scripts';
14+
import { InterpreterInfoJson } from './internal/scripts';
1515
import {
1616
IPythonDaemonExecutionService,
1717
IPythonExecutionService,
@@ -45,7 +45,9 @@ export class PythonDaemonExecutionService extends BasePythonDaemon implements IP
4545
public async getInterpreterInformation(): Promise<InterpreterInformation | undefined> {
4646
try {
4747
this.throwIfRPCConnectionIsDead();
48-
const request = new RequestType0<PythonEnvInfo & ErrorResponse, void, void>('get_interpreter_information');
48+
const request = new RequestType0<InterpreterInfoJson & ErrorResponse, void, void>(
49+
'get_interpreter_information'
50+
);
4951
const response = await this.sendRequestWithoutArgs(request);
5052
if (response.error) {
5153
throw Error(response.error);
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { PythonExecutableInfo, PythonVersion } from '.';
5+
import { interpreterInfo as getInterpreterInfoCommand, InterpreterInfoJson } 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+
export type InterpreterInformation = {
11+
arch: Architecture;
12+
executable: PythonExecutableInfo;
13+
version: PythonVersion;
14+
};
15+
16+
/**
17+
* Compose full interpreter information based on the given data.
18+
*
19+
* The data format corresponds to the output of the `interpreterInfo.py` script.
20+
*
21+
* @param python - the path to the Python executable
22+
* @param raw - the information returned by the `interpreterInfo.py` script
23+
*/
24+
function extractInterpreterInfo(python: string, raw: InterpreterInfoJson): InterpreterInformation {
25+
const rawVersion = `${raw.versionInfo.slice(0, 3).join('.')}-${raw.versionInfo[3]}`;
26+
return {
27+
arch: raw.is64Bit ? Architecture.x64 : Architecture.x86,
28+
executable: {
29+
filename: python,
30+
sysPrefix: raw.sysPrefix,
31+
mtime: -1,
32+
ctime: -1,
33+
},
34+
version: {
35+
...parseVersion(rawVersion),
36+
sysVersion: raw.sysVersion,
37+
},
38+
};
39+
}
40+
41+
type ShellExecResult = {
42+
stdout: string;
43+
stderr?: string;
44+
};
45+
type ShellExecFunc = (command: string, timeout: number) => Promise<ShellExecResult>;
46+
47+
type Logger = {
48+
info(msg: string): void;
49+
50+
error(msg: string): void;
51+
};
52+
53+
/**
54+
* Collect full interpreter information from the given Python executable.
55+
*
56+
* @param python - the information to use when running Python
57+
* @param shellExec - the function to use to exec Python
58+
* @param logger - if provided, used to log failures or other info
59+
*/
60+
export async function getInterpreterInfo(
61+
python: PythonExecInfo,
62+
shellExec: ShellExecFunc,
63+
logger?: Logger,
64+
): Promise<InterpreterInformation | undefined> {
65+
const [args, parse] = getInterpreterInfoCommand();
66+
const info = copyPythonExecInfo(python, args);
67+
const argv = [info.command, ...info.args];
68+
69+
// Concat these together to make a set of quoted strings
70+
const quoted = argv.reduce((p, c) => (p ? `${p} "${c}"` : `"${c.replace('\\', '\\\\')}"`), '');
71+
72+
// Try shell execing the command, followed by the arguments. This will make node kill the process if it
73+
// takes too long.
74+
// Sometimes the python path isn't valid, timeout if that's the case.
75+
// See these two bugs:
76+
// https://github.com/microsoft/vscode-python/issues/7569
77+
// https://github.com/microsoft/vscode-python/issues/7760
78+
const result = await shellExec(quoted, 15000);
79+
if (result.stderr) {
80+
if (logger) {
81+
logger.error(`Failed to parse interpreter information for ${argv} stderr: ${result.stderr}`);
82+
}
83+
return undefined;
84+
}
85+
const json = parse(result.stdout);
86+
if (logger) {
87+
logger.info(`Found interpreter for ${argv}`);
88+
}
89+
return extractInterpreterInfo(python.pythonExecutable, json);
90+
}
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 '.';
5+
import { createDeferred, Deferred } from '../../common/utils/async';
66
import { createWorkerPool, IWorkerPool, QueuePosition } from '../../common/utils/workerPool';
7+
import { getInterpreterInfo, InterpreterInformation } 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,
@@ -18,37 +18,17 @@ export interface IEnvironmentInfoService {
1818
getEnvironmentInfo(
1919
interpreterPath: string,
2020
priority?: EnvironmentInfoServiceQueuePriority
21-
): Promise<PythonEnvironment | undefined>;
21+
): Promise<InterpreterInformation | undefined>;
2222
}
2323

24-
async function buildEnvironmentInfo(interpreterPath: string): Promise<PythonEnvironment | undefined> {
25-
const interpreterInfo = await getInterpreterInfo(buildPythonExecInfo(interpreterPath), shellExecute);
24+
async function buildEnvironmentInfo(interpreterPath: string): Promise<InterpreterInformation | undefined> {
25+
const interpreterInfo = await getInterpreterInfo(buildPythonExecInfo(interpreterPath), shellExecute).catch(
26+
() => undefined,
27+
);
2628
if (interpreterInfo === undefined || interpreterInfo.version === undefined) {
2729
return undefined;
2830
}
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-
};
31+
return interpreterInfo;
5232
}
5333

5434
@injectable()
@@ -57,29 +37,35 @@ export class EnvironmentInfoService implements IEnvironmentInfoService {
5737
// path again and again in a given session. This information will likely not change in a given
5838
// session. There are definitely cases where this will change. But a simple reload should address
5939
// those.
60-
private readonly cache: Map<string, PythonEnvironment> = new Map<string, PythonEnvironment>();
40+
private readonly cache: Map<string, Deferred<InterpreterInformation>> = new Map<
41+
string,
42+
Deferred<InterpreterInformation>
43+
>();
6144

62-
private readonly workerPool: IWorkerPool<string, PythonEnvironment | undefined>;
45+
private readonly workerPool: IWorkerPool<string, InterpreterInformation | undefined>;
6346

6447
public constructor() {
65-
this.workerPool = createWorkerPool<string, PythonEnvironment | undefined>(buildEnvironmentInfo);
48+
this.workerPool = createWorkerPool<string, InterpreterInformation | undefined>(buildEnvironmentInfo);
6649
}
6750

6851
public async getEnvironmentInfo(
6952
interpreterPath: string,
7053
priority?: EnvironmentInfoServiceQueuePriority,
71-
): Promise<PythonEnvironment | undefined> {
54+
): Promise<InterpreterInformation | undefined> {
7255
const result = this.cache.get(interpreterPath);
7356
if (result !== undefined) {
74-
return result;
57+
// Another call for this environment has already been made, return its result
58+
return result.promise;
7559
}
76-
60+
const deferred = createDeferred<InterpreterInformation>();
61+
this.cache.set(interpreterPath, deferred);
7762
return (priority === EnvironmentInfoServiceQueuePriority.High
7863
? this.workerPool.addToQueue(interpreterPath, QueuePosition.Front)
7964
: this.workerPool.addToQueue(interpreterPath, QueuePosition.Back)
8065
).then((r) => {
81-
if (r !== undefined) {
82-
this.cache.set(interpreterPath, r);
66+
deferred.resolve(r);
67+
if (r === undefined) {
68+
this.cache.delete(interpreterPath);
8369
}
8470
return r;
8571
});

src/client/pythonEnvironments/info/interpreter.ts

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

44
import { InterpreterInformation } from '.';
5-
import { interpreterInfo as getInterpreterInfoCommand, PythonEnvInfo } from '../../common/process/internal/scripts';
5+
import { interpreterInfo as getInterpreterInfoCommand, InterpreterInfoJson } from '../../common/process/internal/scripts';
66
import { Architecture } from '../../common/utils/platform';
77
import { copyPythonExecInfo, PythonExecInfo } from '../exec';
88
import { parsePythonVersion } from './pythonVersion';
@@ -15,7 +15,7 @@ import { parsePythonVersion } from './pythonVersion';
1515
* @param python - the path to the Python executable
1616
* @param raw - the information returned by the `interpreterInfo.py` script
1717
*/
18-
export function extractInterpreterInfo(python: string, raw: PythonEnvInfo): InterpreterInformation {
18+
export function extractInterpreterInfo(python: string, raw: InterpreterInfoJson): InterpreterInformation {
1919
const rawVersion = `${raw.versionInfo.slice(0, 3).join('.')}-${raw.versionInfo[3]}`;
2020
return {
2121
architecture: raw.is64Bit ? Architecture.x64 : Architecture.x86,

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,

0 commit comments

Comments
 (0)