Skip to content

Commit 069bced

Browse files
author
Kartik Raj
authored
Ensure command to get executable is run only once per session (microsoft#19606)
1 parent 3c84012 commit 069bced

4 files changed

Lines changed: 32 additions & 24 deletions

File tree

src/client/common/process/pythonEnvironment.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import { buildPythonExecInfo, PythonExecInfo } from '../../pythonEnvironments/ex
88
import { InterpreterInformation } from '../../pythonEnvironments/info';
99
import { getExecutablePath } from '../../pythonEnvironments/info/executable';
1010
import { getInterpreterInfo } from '../../pythonEnvironments/info/interpreter';
11+
import { isTestExecution } from '../constants';
1112
import { IFileSystem } from '../platform/types';
1213
import * as internalPython from './internal/python';
1314
import { ExecutionResult, IProcessService, IPythonEnvironment, ShellOptions, SpawnOptions } from './types';
1415

16+
const cachedExecutablePath: Map<string, Promise<string | undefined>> = new Map<string, Promise<string | undefined>>();
17+
1518
class PythonEnvironment implements IPythonEnvironment {
16-
private cachedExecutablePath: Map<string, Promise<string>> = new Map<string, Promise<string>>();
1719
private cachedInterpreterInformation: InterpreterInformation | undefined | null = null;
1820

1921
constructor(
@@ -45,20 +47,20 @@ class PythonEnvironment implements IPythonEnvironment {
4547
return this.cachedInterpreterInformation;
4648
}
4749

48-
public async getExecutablePath(): Promise<string> {
50+
public async getExecutablePath(): Promise<string | undefined> {
4951
// If we've passed the python file, then return the file.
5052
// This is because on mac if using the interpreter /usr/bin/python2.7 we can get a different value for the path
5153
if (await this.deps.isValidExecutable(this.pythonPath)) {
5254
return this.pythonPath;
5355
}
54-
const result = this.cachedExecutablePath.get(this.pythonPath);
55-
if (result !== undefined) {
56+
const result = cachedExecutablePath.get(this.pythonPath);
57+
if (result !== undefined && !isTestExecution()) {
5658
// Another call for this environment has already been made, return its result
5759
return result;
5860
}
5961
const python = this.getExecutionInfo();
6062
const promise = getExecutablePath(python, this.deps.shellExec);
61-
this.cachedExecutablePath.set(this.pythonPath, promise);
63+
cachedExecutablePath.set(this.pythonPath, promise);
6264
return promise;
6365
}
6466

src/client/common/process/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export const IPythonExecutionService = Symbol('IPythonExecutionService');
8989

9090
export interface IPythonExecutionService {
9191
getInterpreterInformation(): Promise<InterpreterInformation | undefined>;
92-
getExecutablePath(): Promise<string>;
92+
getExecutablePath(): Promise<string | undefined>;
9393
isModuleInstalled(moduleName: string): Promise<boolean>;
9494
getModuleVersion(moduleName: string): Promise<string | undefined>;
9595
getExecutionInfo(pythonArgs?: string[]): PythonExecInfo;
@@ -105,7 +105,7 @@ export interface IPythonExecutionService {
105105
export interface IPythonEnvironment {
106106
getInterpreterInformation(): Promise<InterpreterInformation | undefined>;
107107
getExecutionObservableInfo(pythonArgs?: string[], pythonExecutable?: string): PythonExecInfo;
108-
getExecutablePath(): Promise<string>;
108+
getExecutablePath(): Promise<string | undefined>;
109109
isModuleInstalled(moduleName: string): Promise<boolean>;
110110
getModuleVersion(moduleName: string): Promise<string | undefined>;
111111
getExecutionInfo(pythonArgs?: string[], pythonExecutable?: string): PythonExecInfo;

src/client/pythonEnvironments/info/executable.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import { getExecutable } from '../../common/process/internal/python';
55
import { ShellExecFunc } from '../../common/process/types';
6+
import { traceError } from '../../logging';
67
import { copyPythonExecInfo, PythonExecInfo } from '../exec';
78

89
/**
@@ -17,19 +18,24 @@ export async function getExecutablePath(
1718
python: PythonExecInfo,
1819
shellExec: ShellExecFunc,
1920
timeout?: number,
20-
): Promise<string> {
21-
const [args, parse] = getExecutable();
22-
const info = copyPythonExecInfo(python, args);
23-
const argv = [info.command, ...info.args];
24-
// Concat these together to make a set of quoted strings
25-
const quoted = argv.reduce(
26-
(p, c) => (p ? `${p} ${c.toCommandArgumentForPythonExt()}` : `${c.toCommandArgumentForPythonExt()}`),
27-
'',
28-
);
29-
const result = await shellExec(quoted, { timeout: timeout ?? 15000 });
30-
const executable = parse(result.stdout.trim());
31-
if (executable === '') {
32-
throw new Error(`${quoted} resulted in empty stdout`);
21+
): Promise<string | undefined> {
22+
try {
23+
const [args, parse] = getExecutable();
24+
const info = copyPythonExecInfo(python, args);
25+
const argv = [info.command, ...info.args];
26+
// Concat these together to make a set of quoted strings
27+
const quoted = argv.reduce(
28+
(p, c) => (p ? `${p} ${c.toCommandArgumentForPythonExt()}` : `${c.toCommandArgumentForPythonExt()}`),
29+
'',
30+
);
31+
const result = await shellExec(quoted, { timeout: timeout ?? 15000 });
32+
const executable = parse(result.stdout.trim());
33+
if (executable === '') {
34+
throw new Error(`${quoted} resulted in empty stdout`);
35+
}
36+
return executable;
37+
} catch (ex) {
38+
traceError(ex);
39+
return undefined;
3340
}
34-
return executable;
3541
}

src/test/common/process/pythonEnvironment.unit.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,17 +202,17 @@ suite('PythonEnvironment', () => {
202202
expect(result).to.equal(executablePath, "getExecutablePath() sbould not return pythonPath if it's not a file");
203203
});
204204

205-
test('getExecutablePath should throw if the result of exec() writes to stderr', async () => {
205+
test('getExecutablePath should return `undefined` if the result of exec() writes to stderr', async () => {
206206
const stderr = 'bar';
207207
fileSystem.setup((f) => f.pathExists(pythonPath)).returns(() => Promise.resolve(false));
208208
processService
209209
.setup((p) => p.shellExec(`${pythonPath} -c "import sys;print(sys.executable)"`, TypeMoq.It.isAny()))
210210
.returns(() => Promise.reject(new StdErrError(stderr)));
211211
const env = createPythonEnv(pythonPath, processService.object, fileSystem.object);
212212

213-
const result = env.getExecutablePath();
213+
const result = await env.getExecutablePath();
214214

215-
await expect(result).to.eventually.be.rejectedWith(stderr);
215+
expect(result).to.be.equal(undefined);
216216
});
217217

218218
test('isModuleInstalled should call processService.exec()', async () => {

0 commit comments

Comments
 (0)