Skip to content

Commit 23353bb

Browse files
author
Kartik Raj
authored
Improvements to pythonTerminalEnvVarActivation experiment (#21751)
1 parent 40bb62a commit 23353bb

File tree

4 files changed

+118
-90
lines changed

4 files changed

+118
-90
lines changed

src/client/interpreter/activation/service.ts

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import { ICurrentProcess, IDisposable, Resource } from '../../common/types';
1919
import { sleep } from '../../common/utils/async';
2020
import { InMemoryCache } from '../../common/utils/cacheUtils';
2121
import { OSType } from '../../common/utils/platform';
22-
import { IEnvironmentVariablesProvider } from '../../common/variables/types';
23-
import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info';
22+
import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../common/variables/types';
23+
import { EnvironmentType, PythonEnvironment, virtualEnvTypes } from '../../pythonEnvironments/info';
2424
import { sendTelemetryEvent } from '../../telemetry';
2525
import { EventName } from '../../telemetry/constants';
2626
import { IInterpreterService } from '../contracts';
@@ -38,6 +38,7 @@ import { Conda } from '../../pythonEnvironments/common/environmentManagers/conda
3838
import { StopWatch } from '../../common/utils/stopWatch';
3939
import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector';
4040
import { getSearchPathEnvVarNames } from '../../common/utils/exec';
41+
import { cache } from '../../common/utils/decorators';
4142

4243
const ENVIRONMENT_PREFIX = 'e8b39361-0157-4923-80e1-22d70d46dee6';
4344
const CACHE_DURATION = 10 * 60 * 1000;
@@ -154,11 +155,11 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi
154155
}
155156

156157
// Cache only if successful, else keep trying & failing if necessary.
157-
const cache = new InMemoryCache<NodeJS.ProcessEnv | undefined>(CACHE_DURATION);
158+
const memCache = new InMemoryCache<NodeJS.ProcessEnv | undefined>(CACHE_DURATION);
158159
return this.getActivatedEnvironmentVariablesImpl(resource, interpreter, allowExceptions, shell)
159160
.then((vars) => {
160-
cache.data = vars;
161-
this.activatedEnvVariablesCache.set(cacheKey, cache);
161+
memCache.data = vars;
162+
this.activatedEnvVariablesCache.set(cacheKey, memCache);
162163
sendTelemetryEvent(
163164
EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES,
164165
stopWatch.elapsedTime,
@@ -176,6 +177,35 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi
176177
});
177178
}
178179

180+
@cache(-1, true)
181+
public async getProcessEnvironmentVariables(resource: Resource, shell?: string): Promise<EnvironmentVariables> {
182+
// Try to get the process environment variables using Python by printing variables, that can be little different
183+
// from `process.env` and is preferred when calculating diff.
184+
const globalInterpreters = this.interpreterService
185+
.getInterpreters()
186+
.filter((i) => !virtualEnvTypes.includes(i.envType));
187+
const interpreterPath =
188+
globalInterpreters.length > 0 && globalInterpreters[0] ? globalInterpreters[0].path : 'python';
189+
try {
190+
const [args, parse] = internalScripts.printEnvVariables();
191+
args.forEach((arg, i) => {
192+
args[i] = arg.toCommandArgumentForPythonExt();
193+
});
194+
const command = `${interpreterPath} ${args.join(' ')}`;
195+
const processService = await this.processServiceFactory.create(resource);
196+
const result = await processService.shellExec(command, {
197+
shell,
198+
timeout: ENVIRONMENT_TIMEOUT,
199+
maxBuffer: 1000 * 1000,
200+
throwOnStdErr: false,
201+
});
202+
const returnedEnv = this.parseEnvironmentOutput(result.stdout, parse);
203+
return returnedEnv ?? process.env;
204+
} catch (ex) {
205+
return process.env;
206+
}
207+
}
208+
179209
public async getEnvironmentActivationShellCommands(
180210
resource: Resource,
181211
interpreter?: PythonEnvironment,
@@ -231,7 +261,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi
231261
);
232262
traceVerbose(`Activation Commands received ${activationCommands} for shell ${shellInfo.shell}`);
233263
if (!activationCommands || !Array.isArray(activationCommands) || activationCommands.length === 0) {
234-
if (interpreter?.envType === EnvironmentType.Venv) {
264+
if (interpreter && [EnvironmentType.Venv, EnvironmentType.Pyenv].includes(interpreter?.envType)) {
235265
const key = getSearchPathEnvVarNames()[0];
236266
if (env[key]) {
237267
env[key] = `${path.dirname(interpreter.path)}${path.delimiter}${env[key]}`;
@@ -247,7 +277,14 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi
247277
const activationCommand = fixActivationCommands(activationCommands).join(' && ');
248278
// In order to make sure we know where the environment output is,
249279
// put in a dummy echo we can look for
250-
command = `${activationCommand} && echo '${ENVIRONMENT_PREFIX}' && python ${args.join(' ')}`;
280+
const commandSeparator = [TerminalShellType.powershell, TerminalShellType.powershellCore].includes(
281+
shellInfo.shellType,
282+
)
283+
? ';'
284+
: '&&';
285+
command = `${activationCommand} ${commandSeparator} echo '${ENVIRONMENT_PREFIX}' ${commandSeparator} python ${args.join(
286+
' ',
287+
)}`;
251288
}
252289

253290
// Make sure python warnings don't interfere with getting the environment. However

src/client/interpreter/activation/terminalEnvVarCollectionService.ts

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { defaultShells } from './service';
3333
import { IEnvironmentActivationService } from './types';
3434
import { EnvironmentType } from '../../pythonEnvironments/info';
3535
import { getSearchPathEnvVarNames } from '../../common/utils/exec';
36+
import { EnvironmentVariables } from '../../common/variables/types';
3637

3738
@injectable()
3839
export class TerminalEnvVarCollectionService implements IExtensionActivationService {
@@ -45,7 +46,10 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ
4546

4647
private registeredOnce = false;
4748

48-
private previousEnvVars = _normCaseKeys(process.env);
49+
/**
50+
* Carries default environment variables for the currently selected shell.
51+
*/
52+
private processEnvVars: EnvironmentVariables | undefined;
4953

5054
constructor(
5155
@inject(IPlatformService) private readonly platform: IPlatformService,
@@ -90,6 +94,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ
9094
this.applicationEnvironment.onDidChangeShell(
9195
async (shell: string) => {
9296
this.showProgress();
97+
this.processEnvVars = undefined;
9398
// Pass in the shell where known instead of relying on the application environment, because of bug
9499
// on VSCode: https://github.com/microsoft/vscode/issues/160694
95100
await this._applyCollection(undefined, shell).ignoreErrors();
@@ -106,6 +111,9 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ
106111
public async _applyCollection(resource: Resource, shell = this.applicationEnvironment.shell): Promise<void> {
107112
const workspaceFolder = this.getWorkspaceFolder(resource);
108113
const settings = this.configurationService.getSettings(resource);
114+
const envVarCollection = this.getEnvironmentVariableCollection(workspaceFolder);
115+
// Clear any previously set env vars from collection.
116+
envVarCollection.clear();
109117
if (!settings.terminal.activateEnvironment) {
110118
traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath);
111119
return;
@@ -116,7 +124,6 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ
116124
undefined,
117125
shell,
118126
);
119-
const envVarCollection = this.getEnvironmentVariableCollection(workspaceFolder);
120127
if (!env) {
121128
const shellType = identifyShellFromShellPath(shell);
122129
const defaultShell = defaultShells[this.platform.osType];
@@ -126,32 +133,38 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ
126133
await this._applyCollection(resource, defaultShell?.shell);
127134
return;
128135
}
129-
envVarCollection.clear();
130-
this.previousEnvVars = _normCaseKeys(process.env);
136+
this.processEnvVars = undefined;
131137
return;
132138
}
133-
const previousEnv = this.previousEnvVars;
134-
this.previousEnvVars = env;
139+
if (!this.processEnvVars) {
140+
this.processEnvVars = await this.environmentActivationService.getProcessEnvironmentVariables(
141+
resource,
142+
shell,
143+
);
144+
}
145+
const processEnv = this.processEnvVars;
135146
Object.keys(env).forEach((key) => {
147+
if (shouldSkip(key)) {
148+
return;
149+
}
136150
const value = env[key];
137-
const prevValue = previousEnv[key];
151+
const prevValue = processEnv[key];
138152
if (prevValue !== value) {
139153
if (value !== undefined) {
154+
if (key === 'PS1') {
155+
traceVerbose(`Prepending environment variable ${key} in collection with ${value}`);
156+
envVarCollection.prepend(key, value, {
157+
applyAtShellIntegration: true,
158+
applyAtProcessCreation: false,
159+
});
160+
return;
161+
}
140162
traceVerbose(`Setting environment variable ${key} in collection to ${value}`);
141163
envVarCollection.replace(key, value, { applyAtShellIntegration: true });
142-
} else {
143-
traceVerbose(`Clearing environment variable ${key} from collection`);
144-
envVarCollection.delete(key);
145164
}
146165
}
147166
});
148-
Object.keys(previousEnv).forEach((key) => {
149-
// If the previous env var is not in the current env, clear it from collection.
150-
if (!(key in env)) {
151-
traceVerbose(`Clearing environment variable ${key} from collection`);
152-
envVarCollection.delete(key);
153-
}
154-
});
167+
155168
const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath);
156169
const description = new MarkdownString(`${Interpreters.activateTerminalDescription} \`${displayPath}\``);
157170
envVarCollection.description = description;
@@ -224,13 +237,6 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ
224237
}
225238
}
226239

227-
export function _normCaseKeys(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
228-
const result: NodeJS.ProcessEnv = {};
229-
Object.keys(env).forEach((key) => {
230-
// `os.environ` script used to get env vars normalizes keys to upper case:
231-
// https://github.com/python/cpython/issues/101754
232-
// So convert `process.env` keys to upper case to match.
233-
result[key.toUpperCase()] = env[key];
234-
});
235-
return result;
240+
function shouldSkip(env: string) {
241+
return ['_', 'SHLVL'].includes(env);
236242
}

src/client/interpreter/activation/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
'use strict';
55

66
import { Resource } from '../../common/types';
7+
import { EnvironmentVariables } from '../../common/variables/types';
78
import { PythonEnvironment } from '../../pythonEnvironments/info';
89

910
export const IEnvironmentActivationService = Symbol('IEnvironmentActivationService');
1011
export interface IEnvironmentActivationService {
12+
getProcessEnvironmentVariables(resource: Resource, shell?: string): Promise<EnvironmentVariables>;
1113
getActivatedEnvironmentVariables(
1214
resource: Resource,
1315
interpreter?: PythonEnvironment,

0 commit comments

Comments
 (0)