Skip to content

Commit e9edfc0

Browse files
author
Kartik Raj
authored
Detect if VSCode is launched from an activated environment and select it (#20526)
Closes #10668 In case of base conda environments, show a prompt to select it instead, as getting configs is required in that case which can take time.
1 parent bebf05d commit e9edfc0

15 files changed

+801
-14
lines changed

src/client/common/persistentState.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { inject, injectable, named } from 'inversify';
77
import { Memento } from 'vscode';
88
import { IExtensionSingleActivationService } from '../activation/types';
9-
import { traceError } from '../logging';
9+
import { traceError, traceVerbose, traceWarn } from '../logging';
1010
import { ICommandManager } from './application/types';
1111
import { Commands } from './constants';
1212
import {
@@ -41,13 +41,24 @@ export class PersistentState<T> implements IPersistentState<T> {
4141
}
4242
}
4343

44-
public async updateValue(newValue: T): Promise<void> {
44+
public async updateValue(newValue: T, retryOnce = true): Promise<void> {
4545
try {
4646
if (this.expiryDurationMs) {
4747
await this.storage.update(this.key, { data: newValue, expiry: Date.now() + this.expiryDurationMs });
4848
} else {
4949
await this.storage.update(this.key, newValue);
5050
}
51+
if (retryOnce && JSON.stringify(this.value) != JSON.stringify(newValue)) {
52+
// Due to a VSCode bug sometimes the changes are not reflected in the storage, atleast not immediately.
53+
// It is noticed however that if we reset the storage first and then update it, it works.
54+
// https://github.com/microsoft/vscode/issues/171827
55+
traceVerbose('Storage update failed for key', this.key, ' retrying by resetting first');
56+
await this.updateValue(undefined as any, false);
57+
await this.updateValue(newValue, false);
58+
if (JSON.stringify(this.value) != JSON.stringify(newValue)) {
59+
traceWarn('Retry failed, storage update failed for key', this.key);
60+
}
61+
}
5162
} catch (ex) {
5263
traceError('Error while updating storage for key:', this.key, ex);
5364
}

src/client/common/process/pythonExecutionFactory.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { inject, injectable } from 'inversify';
44

55
import { IEnvironmentActivationService } from '../../interpreter/activation/types';
6-
import { IComponentAdapter } from '../../interpreter/contracts';
6+
import { IActivatedEnvironmentLaunch, IComponentAdapter } from '../../interpreter/contracts';
77
import { IServiceContainer } from '../../ioc/types';
88
import { sendTelemetryEvent } from '../../telemetry';
99
import { EventName } from '../../telemetry/constants';
@@ -52,6 +52,10 @@ export class PythonExecutionFactory implements IPythonExecutionFactory {
5252
public async create(options: ExecutionFactoryCreationOptions): Promise<IPythonExecutionService> {
5353
let { pythonPath } = options;
5454
if (!pythonPath || pythonPath === 'python') {
55+
const activatedEnvLaunch = this.serviceContainer.get<IActivatedEnvironmentLaunch>(
56+
IActivatedEnvironmentLaunch,
57+
);
58+
await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv();
5559
// If python path wasn't passed in, we need to auto select it and then read it
5660
// from the configuration.
5761
const interpreterPath = this.interpreterPathExpHelper.get(options.resource);

src/client/common/utils/localize.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ export namespace Interpreters {
191191
export const condaInheritEnvMessage = l10n.t(
192192
'We noticed you\'re using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we recommend that you let the Python extension change "terminal.integrated.inheritEnv" to false in your user settings.',
193193
);
194+
export const activatedCondaEnvLaunch = l10n.t(
195+
'We noticed VS Code was launched from an activated conda environment, would you like to select it?',
196+
);
194197
export const environmentPromptMessage = l10n.t(
195198
'We noticed a new environment has been created. Do you want to select it for the workspace folder?',
196199
);

src/client/interpreter/contracts.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,8 @@ export type WorkspacePythonPath = {
122122
folderUri: Uri;
123123
configTarget: ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder;
124124
};
125+
126+
export const IActivatedEnvironmentLaunch = Symbol('IActivatedEnvironmentLaunch');
127+
export interface IActivatedEnvironmentLaunch {
128+
selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection?: boolean): Promise<string | undefined>;
129+
}

src/client/interpreter/interpreterService.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import { IServiceContainer } from '../ioc/types';
2323
import { PythonEnvironment } from '../pythonEnvironments/info';
2424
import {
25+
IActivatedEnvironmentLaunch,
2526
IComponentAdapter,
2627
IInterpreterDisplay,
2728
IInterpreterService,
@@ -179,7 +180,13 @@ export class InterpreterService implements Disposable, IInterpreterService {
179180
}
180181

181182
public async getActiveInterpreter(resource?: Uri): Promise<PythonEnvironment | undefined> {
182-
let path = this.configService.getSettings(resource).pythonPath;
183+
const activatedEnvLaunch = this.serviceContainer.get<IActivatedEnvironmentLaunch>(IActivatedEnvironmentLaunch);
184+
let path = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(true);
185+
// This is being set as interpreter in background, after which it'll show up in `.pythonPath` config.
186+
// However we need not wait on the update to take place, as we can use the value directly.
187+
if (!path) {
188+
path = this.configService.getSettings(resource).pythonPath;
189+
}
183190
if (pathUtils.basename(path) === path) {
184191
// Value can be `python`, `python3`, `python3.9` etc.
185192
// Note the following triggers autoselection if no interpreter is explictly

src/client/interpreter/serviceRegistry.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ import {
2525
IPythonPathUpdaterServiceFactory,
2626
IPythonPathUpdaterServiceManager,
2727
} from './configuration/types';
28-
import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService } from './contracts';
28+
import { IActivatedEnvironmentLaunch, IInterpreterDisplay, IInterpreterHelper, IInterpreterService } from './contracts';
2929
import { InterpreterDisplay } from './display';
3030
import { InterpreterLocatorProgressStatubarHandler } from './display/progressDisplay';
3131
import { InterpreterHelper } from './helpers';
3232
import { InterpreterService } from './interpreterService';
33+
import { ActivatedEnvironmentLaunch } from './virtualEnvs/activatedEnvLaunch';
3334
import { CondaInheritEnvPrompt } from './virtualEnvs/condaInheritEnvPrompt';
3435
import { VirtualEnvironmentPrompt } from './virtualEnvs/virtualEnvPrompt';
3536

@@ -90,6 +91,7 @@ export function registerInterpreterTypes(serviceManager: IServiceManager): void
9091
);
9192

9293
serviceManager.addSingleton<IExtensionActivationService>(IExtensionActivationService, CondaInheritEnvPrompt);
94+
serviceManager.addSingleton<IActivatedEnvironmentLaunch>(IActivatedEnvironmentLaunch, ActivatedEnvironmentLaunch);
9395
}
9496

9597
export function registerTypes(serviceManager: IServiceManager): void {
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { inject, injectable, optional } from 'inversify';
5+
import { ConfigurationTarget } from 'vscode';
6+
import * as path from 'path';
7+
import { IApplicationShell, IWorkspaceService } from '../../common/application/types';
8+
import { IProcessServiceFactory } from '../../common/process/types';
9+
import { sleep } from '../../common/utils/async';
10+
import { cache } from '../../common/utils/decorators';
11+
import { Common, Interpreters } from '../../common/utils/localize';
12+
import { traceError, traceLog, traceWarn } from '../../logging';
13+
import { Conda } from '../../pythonEnvironments/common/environmentManagers/conda';
14+
import { sendTelemetryEvent } from '../../telemetry';
15+
import { EventName } from '../../telemetry/constants';
16+
import { IPythonPathUpdaterServiceManager } from '../configuration/types';
17+
import { IActivatedEnvironmentLaunch, IInterpreterService } from '../contracts';
18+
19+
@injectable()
20+
export class ActivatedEnvironmentLaunch implements IActivatedEnvironmentLaunch {
21+
public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true };
22+
23+
private inMemorySelection: string | undefined;
24+
25+
constructor(
26+
@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService,
27+
@inject(IApplicationShell) private readonly appShell: IApplicationShell,
28+
@inject(IPythonPathUpdaterServiceManager)
29+
private readonly pythonPathUpdaterService: IPythonPathUpdaterServiceManager,
30+
@inject(IInterpreterService) private readonly interpreterService: IInterpreterService,
31+
@inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory,
32+
@optional() public wasSelected: boolean = false,
33+
) {}
34+
35+
@cache(-1, true)
36+
public async _promptIfApplicable(): Promise<void> {
37+
const baseCondaPrefix = getPrefixOfActivatedCondaEnv();
38+
if (!baseCondaPrefix) {
39+
return;
40+
}
41+
const info = await this.interpreterService.getInterpreterDetails(baseCondaPrefix);
42+
if (info?.envName !== 'base') {
43+
// Only show prompt for base conda environments, as we need to check config for such envs which can be slow.
44+
return;
45+
}
46+
const conda = await Conda.getConda();
47+
if (!conda) {
48+
traceWarn('Conda not found even though activated environment vars are set');
49+
return;
50+
}
51+
const service = await this.processServiceFactory.create();
52+
const autoActivateBaseConfig = await service
53+
.shellExec(`${conda.shellCommand} config --get auto_activate_base`)
54+
.catch((ex) => {
55+
traceError(ex);
56+
return { stdout: '' };
57+
});
58+
if (autoActivateBaseConfig.stdout.trim().toLowerCase().endsWith('false')) {
59+
await this.promptAndUpdate(baseCondaPrefix);
60+
}
61+
}
62+
63+
private async promptAndUpdate(prefix: string) {
64+
this.wasSelected = true;
65+
const prompts = [Common.bannerLabelYes, Common.bannerLabelNo];
66+
const telemetrySelections: ['Yes', 'No'] = ['Yes', 'No'];
67+
const selection = await this.appShell.showInformationMessage(Interpreters.activatedCondaEnvLaunch, ...prompts);
68+
sendTelemetryEvent(EventName.ACTIVATED_CONDA_ENV_LAUNCH, undefined, {
69+
selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined,
70+
});
71+
if (!selection) {
72+
return;
73+
}
74+
if (selection === prompts[0]) {
75+
await this.setInterpeterInStorage(prefix);
76+
}
77+
}
78+
79+
public async selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection = false): Promise<string | undefined> {
80+
if (this.wasSelected) {
81+
return this.inMemorySelection;
82+
}
83+
return this._selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection);
84+
}
85+
86+
@cache(-1, true)
87+
private async _selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection = false): Promise<string | undefined> {
88+
if (this.workspaceService.workspaceFile) {
89+
// Assuming multiroot workspaces cannot be directly launched via `code .` command.
90+
return undefined;
91+
}
92+
const prefix = await this.getPrefixOfSelectedActivatedEnv();
93+
if (!prefix) {
94+
this._promptIfApplicable().ignoreErrors();
95+
return undefined;
96+
}
97+
this.wasSelected = true;
98+
this.inMemorySelection = prefix;
99+
traceLog(
100+
`VS Code was launched from an activated environment: '${path.basename(
101+
prefix,
102+
)}', selecting it as the interpreter for workspace.`,
103+
);
104+
if (doNotBlockOnSelection) {
105+
this.setInterpeterInStorage(prefix).ignoreErrors();
106+
} else {
107+
await this.setInterpeterInStorage(prefix);
108+
await sleep(1); // Yield control so config service can update itself.
109+
}
110+
this.inMemorySelection = undefined; // Once we have set the prefix in storage, clear the in memory selection.
111+
return prefix;
112+
}
113+
114+
private async setInterpeterInStorage(prefix: string) {
115+
const { workspaceFolders } = this.workspaceService;
116+
if (!workspaceFolders || workspaceFolders.length === 0) {
117+
await this.pythonPathUpdaterService.updatePythonPath(prefix, ConfigurationTarget.Global, 'load');
118+
} else {
119+
await this.pythonPathUpdaterService.updatePythonPath(
120+
prefix,
121+
ConfigurationTarget.WorkspaceFolder,
122+
'load',
123+
workspaceFolders[0].uri,
124+
);
125+
}
126+
}
127+
128+
private async getPrefixOfSelectedActivatedEnv(): Promise<string | undefined> {
129+
const virtualEnvVar = process.env.VIRTUAL_ENV;
130+
if (virtualEnvVar !== undefined && virtualEnvVar.length > 0) {
131+
return virtualEnvVar;
132+
}
133+
const condaPrefixVar = getPrefixOfActivatedCondaEnv();
134+
if (!condaPrefixVar) {
135+
return undefined;
136+
}
137+
const info = await this.interpreterService.getInterpreterDetails(condaPrefixVar);
138+
if (info?.envName !== 'base') {
139+
return condaPrefixVar;
140+
}
141+
// Ignoring base conda environments, as they could be automatically set by conda.
142+
if (process.env.CONDA_AUTO_ACTIVATE_BASE !== undefined) {
143+
if (process.env.CONDA_AUTO_ACTIVATE_BASE.toLowerCase() === 'false') {
144+
return condaPrefixVar;
145+
}
146+
}
147+
return undefined;
148+
}
149+
}
150+
151+
function getPrefixOfActivatedCondaEnv() {
152+
const condaPrefixVar = process.env.CONDA_PREFIX;
153+
if (condaPrefixVar && condaPrefixVar.length > 0) {
154+
const condaShlvl = process.env.CONDA_SHLVL;
155+
if (condaShlvl !== undefined && condaShlvl.length > 0 && condaShlvl > '0') {
156+
return condaPrefixVar;
157+
}
158+
}
159+
return undefined;
160+
}

src/client/telemetry/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export enum EventName {
3030
PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT = 'PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT',
3131
PYTHON_NOT_INSTALLED_PROMPT = 'PYTHON_NOT_INSTALLED_PROMPT',
3232
CONDA_INHERIT_ENV_PROMPT = 'CONDA_INHERIT_ENV_PROMPT',
33+
ACTIVATED_CONDA_ENV_LAUNCH = 'ACTIVATED_CONDA_ENV_LAUNCH',
3334
ENVFILE_VARIABLE_SUBSTITUTION = 'ENVFILE_VARIABLE_SUBSTITUTION',
3435
ENVFILE_WORKSPACE = 'ENVFILE_WORKSPACE',
3536
EXECUTION_CODE = 'EXECUTION_CODE',

src/client/telemetry/index.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,8 +1299,9 @@ export interface IEventNamePropertyMapping {
12991299
environmentsWithoutPython?: number;
13001300
};
13011301
/**
1302-
* Telemetry event sent with details when user clicks the prompt with the following message
1303-
* `Prompt message` :- 'We noticed you're using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we suggest the "terminal.integrated.inheritEnv" setting to be changed to false. Would you like to update this setting?'
1302+
* Telemetry event sent with details when user clicks the prompt with the following message:
1303+
*
1304+
* 'We noticed you're using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we suggest the "terminal.integrated.inheritEnv" setting to be changed to false. Would you like to update this setting?'
13041305
*/
13051306
/* __GDPR__
13061307
"conda_inherit_env_prompt" : {
@@ -1315,6 +1316,23 @@ export interface IEventNamePropertyMapping {
13151316
*/
13161317
selection: 'Yes' | 'No' | 'More Info' | undefined;
13171318
};
1319+
/**
1320+
* Telemetry event sent with details when user clicks the prompt with the following message:
1321+
*
1322+
* 'We noticed VS Code was launched from an activated conda environment, would you like to select it?'
1323+
*/
1324+
/* __GDPR__
1325+
"activated_conda_env_launch" : {
1326+
"selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karrtikr" }
1327+
}
1328+
*/
1329+
[EventName.ACTIVATED_CONDA_ENV_LAUNCH]: {
1330+
/**
1331+
* `Yes` When 'Yes' option is selected
1332+
* `No` When 'No' option is selected
1333+
*/
1334+
selection: 'Yes' | 'No' | undefined;
1335+
};
13181336
/**
13191337
* Telemetry event sent with details when user clicks a button in the virtual environment prompt.
13201338
* `Prompt message` :- 'We noticed a new virtual environment has been created. Do you want to select it for the workspace folder?'

src/test/common/installer.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,14 @@ import { MockModuleInstaller } from '../mocks/moduleInstaller';
100100
import { MockProcessService } from '../mocks/proc';
101101
import { UnitTestIocContainer } from '../testing/serviceRegistry';
102102
import { closeActiveWindows, initializeTest, IS_MULTI_ROOT_TEST, TEST_TIMEOUT } from '../initialize';
103+
import { IActivatedEnvironmentLaunch } from '../../client/interpreter/contracts';
104+
import { ActivatedEnvironmentLaunch } from '../../client/interpreter/virtualEnvs/activatedEnvLaunch';
105+
import {
106+
IPythonPathUpdaterServiceFactory,
107+
IPythonPathUpdaterServiceManager,
108+
} from '../../client/interpreter/configuration/types';
109+
import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService';
110+
import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory';
103111

104112
suite('Installer', () => {
105113
let ioc: UnitTestIocContainer;
@@ -169,6 +177,18 @@ suite('Installer', () => {
169177
TestFrameworkProductPathService,
170178
ProductType.TestFramework,
171179
);
180+
ioc.serviceManager.addSingleton<IActivatedEnvironmentLaunch>(
181+
IActivatedEnvironmentLaunch,
182+
ActivatedEnvironmentLaunch,
183+
);
184+
ioc.serviceManager.addSingleton<IPythonPathUpdaterServiceManager>(
185+
IPythonPathUpdaterServiceManager,
186+
PythonPathUpdaterService,
187+
);
188+
ioc.serviceManager.addSingleton<IPythonPathUpdaterServiceFactory>(
189+
IPythonPathUpdaterServiceFactory,
190+
PythonPathUpdaterServiceFactory,
191+
);
172192
ioc.serviceManager.addSingleton<IActiveResourceService>(IActiveResourceService, ActiveResourceService);
173193
ioc.serviceManager.addSingleton<IInterpreterPathService>(IInterpreterPathService, InterpreterPathService);
174194
ioc.serviceManager.addSingleton<IExtensions>(IExtensions, Extensions);

0 commit comments

Comments
 (0)