Skip to content

Commit fe23c04

Browse files
committed
Cherrypick PR #2892
1 parent 98a2b22 commit fe23c04

38 files changed

+693
-189
lines changed

news/2 Fixes/2827.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Disable activation of conda environments in PowerShell.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { inject, injectable } from 'inversify';
7+
import { DiagnosticSeverity } from 'vscode';
8+
import '../../../common/extensions';
9+
import { error } from '../../../common/logger';
10+
import { useCommandPromptAsDefaultShell } from '../../../common/terminal/commandPrompt';
11+
import { IConfigurationService, ICurrentProcess } from '../../../common/types';
12+
import { IServiceContainer } from '../../../ioc/types';
13+
import { BaseDiagnostic, BaseDiagnosticsService } from '../base';
14+
import { IDiagnosticsCommandFactory } from '../commands/types';
15+
import { DiagnosticCodes } from '../constants';
16+
import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler';
17+
import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types';
18+
19+
const PowershellActivationNotSupportedWithBatchFilesMessage = 'Activation of the selected Python environment is not supported in PowerShell. Consider changing your shell to Command Prompt.';
20+
21+
export class PowershellActivationNotAvailableDiagnostic extends BaseDiagnostic {
22+
constructor() {
23+
super(DiagnosticCodes.EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic,
24+
PowershellActivationNotSupportedWithBatchFilesMessage,
25+
DiagnosticSeverity.Warning, DiagnosticScope.Global);
26+
}
27+
}
28+
29+
export const PowerShellActivationHackDiagnosticsServiceId = 'EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic';
30+
31+
@injectable()
32+
export class PowerShellActivationHackDiagnosticsService extends BaseDiagnosticsService {
33+
protected readonly messageService: IDiagnosticHandlerService<MessageCommandPrompt>;
34+
constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
35+
super([DiagnosticCodes.EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic], serviceContainer);
36+
this.messageService = serviceContainer.get<IDiagnosticHandlerService<MessageCommandPrompt>>(IDiagnosticHandlerService, DiagnosticCommandPromptHandlerServiceId);
37+
}
38+
public async diagnose(): Promise<IDiagnostic[]> {
39+
return [];
40+
}
41+
public async handle(diagnostics: IDiagnostic[]): Promise<void> {
42+
// This class can only handle one type of diagnostic, hence just use first item in list.
43+
if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) {
44+
return;
45+
}
46+
const diagnostic = diagnostics[0];
47+
if (await this.filterService.shouldIgnoreDiagnostic(diagnostic.code)) {
48+
return;
49+
}
50+
const commandFactory = this.serviceContainer.get<IDiagnosticsCommandFactory>(IDiagnosticsCommandFactory);
51+
const currentProcess = this.serviceContainer.get<ICurrentProcess>(ICurrentProcess);
52+
const configurationService = this.serviceContainer.get<IConfigurationService>(IConfigurationService);
53+
const options = [
54+
{
55+
prompt: 'Use Command Prompt',
56+
// tslint:disable-next-line:no-object-literal-type-assertion
57+
command: {
58+
diagnostic, invoke: async (): Promise<void> => {
59+
useCommandPromptAsDefaultShell(currentProcess, configurationService)
60+
.catch(ex => error('Use Command Prompt as default shell', ex));
61+
}
62+
}
63+
},
64+
{
65+
prompt: 'Ignore'
66+
},
67+
{
68+
prompt: 'Always Ignore',
69+
command: commandFactory.createCommand(diagnostic, { type: 'ignore', options: DiagnosticScope.Global })
70+
},
71+
{
72+
prompt: 'More Info',
73+
command: commandFactory.createCommand(diagnostic, { type: 'launch', options: 'https://aka.ms/Niq35h' })
74+
}
75+
];
76+
77+
await this.messageService.handle(diagnostic, { commandPrompts: options });
78+
}
79+
}

src/client/application/diagnostics/constants.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@ export enum DiagnosticCodes {
88
InvalidDebuggerTypeDiagnostic = 'InvalidDebuggerTypeDiagnostic',
99
NoPythonInterpretersDiagnostic = 'NoPythonInterpretersDiagnostic',
1010
MacInterpreterSelectedAndNoOtherInterpretersDiagnostic = 'MacInterpreterSelectedAndNoOtherInterpretersDiagnostic',
11-
MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic = 'MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic'
11+
MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic = 'MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic',
12+
InvalidPythonPathInDebuggerDiagnostic = 'InvalidPythonPathInDebuggerDiagnostic',
13+
EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic = 'EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic'
1214
}

src/client/application/diagnostics/serviceRegistry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { IServiceManager } from '../../ioc/types';
77
import { EnvironmentPathVariableDiagnosticsService, EnvironmentPathVariableDiagnosticsServiceId } from './checks/envPathVariable';
88
import { InvalidDebuggerTypeDiagnosticsService, InvalidDebuggerTypeDiagnosticsServiceId } from './checks/invalidDebuggerType';
9+
import { PowerShellActivationHackDiagnosticsService, PowerShellActivationHackDiagnosticsServiceId } from './checks/powerShellActivation';
910
import { InvalidPythonInterpreterService, InvalidPythonInterpreterServiceId } from './checks/pythonInterpreter';
1011
import { DiagnosticsCommandFactory } from './commands/factory';
1112
import { IDiagnosticsCommandFactory } from './commands/types';
@@ -19,5 +20,6 @@ export function registerTypes(serviceManager: IServiceManager) {
1920
serviceManager.addSingleton<IDiagnosticsService>(IDiagnosticsService, EnvironmentPathVariableDiagnosticsService, EnvironmentPathVariableDiagnosticsServiceId);
2021
serviceManager.addSingleton<IDiagnosticsService>(IDiagnosticsService, InvalidDebuggerTypeDiagnosticsService, InvalidDebuggerTypeDiagnosticsServiceId);
2122
serviceManager.addSingleton<IDiagnosticsService>(IDiagnosticsService, InvalidPythonInterpreterService, InvalidPythonInterpreterServiceId);
23+
serviceManager.addSingleton<IDiagnosticsService>(IDiagnosticsService, PowerShellActivationHackDiagnosticsService, PowerShellActivationHackDiagnosticsServiceId);
2224
serviceManager.addSingleton<IDiagnosticsCommandFactory>(IDiagnosticsCommandFactory, DiagnosticsCommandFactory);
2325
}

src/client/common/configuration/service.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,16 @@ export class ConfigurationService implements IConfigurationService {
1212
return PythonSettings.getInstance(resource);
1313
}
1414

15-
public async updateSettingAsync(setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise<void> {
16-
const settingsInfo = PythonSettings.getSettingsUriAndTarget(resource);
15+
public async updateSectionSetting(section: string, setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise<void> {
16+
const settingsInfo = section === 'python' ?
17+
PythonSettings.getSettingsUriAndTarget(resource) :
18+
{
19+
uri: resource,
20+
target: configTarget ? configTarget : ConfigurationTarget.WorkspaceFolder
21+
};
1722

18-
const pythonConfig = workspace.getConfiguration('python', settingsInfo.uri);
19-
const currentValue = pythonConfig.inspect(setting);
23+
const configSection = workspace.getConfiguration(section, settingsInfo.uri);
24+
const currentValue = configSection.inspect(setting);
2025

2126
if (currentValue !== undefined &&
2227
((settingsInfo.target === ConfigurationTarget.Global && currentValue.globalValue === value) ||
@@ -25,19 +30,23 @@ export class ConfigurationService implements IConfigurationService {
2530
return;
2631
}
2732

28-
await pythonConfig.update(setting, value, settingsInfo.target);
29-
await this.verifySetting(pythonConfig, settingsInfo.target, setting, value);
33+
await configSection.update(setting, value, settingsInfo.target);
34+
await this.verifySetting(configSection, settingsInfo.target, setting, value);
35+
}
36+
37+
public async updateSetting(setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise<void> {
38+
return this.updateSectionSetting('python', setting, value, resource, configTarget);
3039
}
3140

3241
public isTestExecution(): boolean {
3342
return process.env.VSC_PYTHON_CI_TEST === '1';
3443
}
3544

36-
private async verifySetting(pythonConfig: WorkspaceConfiguration, target: ConfigurationTarget, settingName: string, value?: {}): Promise<void> {
45+
private async verifySetting(configSection: WorkspaceConfiguration, target: ConfigurationTarget, settingName: string, value?: {}): Promise<void> {
3746
if (this.isTestExecution()) {
3847
let retries = 0;
3948
do {
40-
const setting = pythonConfig.inspect(settingName);
49+
const setting = configSection.inspect(settingName);
4150
if (!setting && value === undefined) {
4251
break; // Both are unset
4352
}

src/client/common/installer/productInstaller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export class FormatterInstaller extends BaseInstaller {
156156
const formatterName = ProductNames.get(formatter)!;
157157

158158
if (item.endsWith(formatterName)) {
159-
await this.configService.updateSettingAsync('formatting.provider', formatterName, resource);
159+
await this.configService.updateSetting('formatting.provider', formatterName, resource);
160160
return this.install(formatter, resource);
161161
}
162162
}

src/client/common/serviceRegistry.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import { PersistentStateFactory } from './persistentState';
2929
import { IS_64_BIT, IS_WINDOWS } from './platform/constants';
3030
import { PathUtils } from './platform/pathUtils';
3131
import { CurrentProcess } from './process/currentProcess';
32+
import { TerminalActivator } from './terminal/activator';
33+
import { PowershellTerminalActivationFailedHandler } from './terminal/activator/powershellFailedHandler';
3234
import { Bash } from './terminal/environmentActivationProviders/bash';
3335
import {
3436
CommandPromptAndPowerShell
@@ -38,7 +40,7 @@ import { TerminalServiceFactory } from './terminal/factory';
3840
import { TerminalHelper } from './terminal/helper';
3941
import {
4042
ITerminalActivationCommandProvider,
41-
ITerminalHelper, ITerminalServiceFactory
43+
ITerminalActivationHandler, ITerminalActivator, ITerminalHelper, ITerminalServiceFactory
4244
} from './terminal/types';
4345
import {
4446
IBrowserService, IConfigurationService,
@@ -71,6 +73,8 @@ export function registerTypes(serviceManager: IServiceManager) {
7173
serviceManager.addSingleton<IHttpClient>(IHttpClient, HttpClient);
7274
serviceManager.addSingleton<IEditorUtils>(IEditorUtils, EditorUtils);
7375
serviceManager.addSingleton<INugetService>(INugetService, NugetService);
76+
serviceManager.addSingleton<ITerminalActivator>(ITerminalActivator, TerminalActivator);
77+
serviceManager.addSingleton<ITerminalActivationHandler>(ITerminalActivationHandler, PowershellTerminalActivationFailedHandler);
7478

7579
serviceManager.addSingleton<ITerminalHelper>(ITerminalHelper, TerminalHelper);
7680
serviceManager.addSingleton<ITerminalActivationCommandProvider>(
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { Terminal, Uri } from 'vscode';
7+
import { sleep } from '../../../../utils/async';
8+
import { ITerminalActivator, ITerminalHelper, TerminalShellType } from '../types';
9+
10+
export class BaseTerminalActivator implements ITerminalActivator {
11+
private readonly activatedTerminals: Set<Terminal> = new Set<Terminal>();
12+
constructor(private readonly helper: ITerminalHelper) { }
13+
public async activateEnvironmentInTerminal(terminal: Terminal, resource: Uri | undefined, preserveFocus: boolean = true) {
14+
if (this.activatedTerminals.has(terminal)) {
15+
return false;
16+
}
17+
this.activatedTerminals.add(terminal);
18+
const shellPath = this.helper.getTerminalShellPath();
19+
const terminalShellType = !shellPath || shellPath.length === 0 ? TerminalShellType.other : this.helper.identifyTerminalShell(shellPath);
20+
21+
const activationCommamnds = await this.helper.getEnvironmentActivationCommands(terminalShellType, resource);
22+
if (activationCommamnds) {
23+
for (const command of activationCommamnds!) {
24+
terminal.show(preserveFocus);
25+
terminal.sendText(command);
26+
await this.waitForCommandToProcess(terminalShellType);
27+
}
28+
return true;
29+
} else {
30+
return false;
31+
}
32+
}
33+
protected async waitForCommandToProcess(shell: TerminalShellType) {
34+
// Give the command some time to complete.
35+
// Its been observed that sending commands too early will strip some text off in VS Code Terminal.
36+
await sleep(500);
37+
}
38+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { inject, injectable, multiInject } from 'inversify';
7+
import { Terminal, Uri } from 'vscode';
8+
import { ITerminalActivationHandler, ITerminalActivator, ITerminalHelper } from '../types';
9+
import { BaseTerminalActivator } from './base';
10+
11+
@injectable()
12+
export class TerminalActivator implements ITerminalActivator {
13+
protected baseActivator!: ITerminalActivator;
14+
constructor(@inject(ITerminalHelper) readonly helper: ITerminalHelper,
15+
@multiInject(ITerminalActivationHandler) private readonly handlers: ITerminalActivationHandler[]) {
16+
this.initialize();
17+
}
18+
public async activateEnvironmentInTerminal(terminal: Terminal, resource: Uri | undefined, preserveFocus: boolean = true) {
19+
const activated = await this.baseActivator.activateEnvironmentInTerminal(terminal, resource, preserveFocus);
20+
this.handlers.forEach(handler => handler.handleActivation(terminal, resource, preserveFocus, activated).ignoreErrors());
21+
return activated;
22+
}
23+
protected initialize() {
24+
this.baseActivator = new BaseTerminalActivator(this.helper);
25+
}
26+
}
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+
'use strict';
5+
6+
import { inject, injectable, named } from 'inversify';
7+
import { Terminal, Uri } from 'vscode';
8+
import { PowerShellActivationHackDiagnosticsServiceId, PowershellActivationNotAvailableDiagnostic } from '../../../application/diagnostics/checks/powerShellActivation';
9+
import { IDiagnosticsService } from '../../../application/diagnostics/types';
10+
import { IPlatformService } from '../../platform/types';
11+
import { ITerminalActivationHandler, ITerminalHelper, TerminalShellType } from '../types';
12+
13+
@injectable()
14+
export class PowershellTerminalActivationFailedHandler implements ITerminalActivationHandler {
15+
constructor(@inject(ITerminalHelper) private readonly helper: ITerminalHelper,
16+
@inject(IPlatformService) private readonly platformService: IPlatformService,
17+
@inject(IDiagnosticsService) @named(PowerShellActivationHackDiagnosticsServiceId) private readonly diagnosticService: IDiagnosticsService) {
18+
}
19+
public async handleActivation(_terminal: Terminal, resource: Uri | undefined, _preserveFocus: boolean, activated: boolean) {
20+
if (activated || !this.platformService.isWindows) {
21+
return;
22+
}
23+
const shell = this.helper.identifyTerminalShell(this.helper.getTerminalShellPath());
24+
if (shell !== TerminalShellType.powershell && shell !== TerminalShellType.powershellCore) {
25+
return;
26+
}
27+
// Check if we can activate in Command Prompt.
28+
const activationCommands = await this.helper.getEnvironmentActivationCommands(TerminalShellType.commandPrompt, resource);
29+
if (!activationCommands || !Array.isArray(activationCommands) || activationCommands.length === 0) {
30+
return;
31+
}
32+
this.diagnosticService.handle([new PowershellActivationNotAvailableDiagnostic()]).ignoreErrors();
33+
}
34+
35+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import * as path from 'path';
7+
import { ConfigurationTarget } from 'vscode';
8+
import { IConfigurationService, ICurrentProcess } from '../types';
9+
10+
export function getCommandPromptLocation(currentProcess: ICurrentProcess) {
11+
// https://github.com/Microsoft/vscode/blob/master/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts#L218
12+
// Determine the correct System32 path. We want to point to Sysnative
13+
// when the 32-bit version of VS Code is running on a 64-bit machine.
14+
// The reason for this is because PowerShell's important PSReadline
15+
// module doesn't work if this is not the case. See #27915.
16+
const is32ProcessOn64Windows = currentProcess.env.hasOwnProperty('PROCESSOR_ARCHITEW6432');
17+
const system32Path = path.join(currentProcess.env.windir!, is32ProcessOn64Windows ? 'Sysnative' : 'System32');
18+
return path.join(system32Path, 'cmd.exe');
19+
}
20+
export async function useCommandPromptAsDefaultShell(currentProcess: ICurrentProcess, configService: IConfigurationService) {
21+
const cmdPromptLocation = getCommandPromptLocation(currentProcess);
22+
await configService.updateSectionSetting('terminal', 'integrated.shell.windows', cmdPromptLocation, undefined, ConfigurationTarget.Global);
23+
}

src/client/common/terminal/environmentActivationProviders/commandPrompt.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { inject, injectable } from 'inversify';
55
import * as path from 'path';
66
import { IServiceContainer } from '../../../ioc/types';
77
import '../../extensions';
8-
import { IPlatformService } from '../../platform/types';
98
import { TerminalShellType } from '../types';
109
import { BaseActivationCommandProvider } from './baseActivationProvider';
1110

@@ -34,18 +33,7 @@ export class CommandPromptAndPowerShell extends BaseActivationCommandProvider {
3433
// lets not try to run the powershell file from command prompt (user may not have powershell)
3534
return [];
3635
} else {
37-
// This means we're in powershell and we have a .bat file.
38-
if (this.serviceContainer.get<IPlatformService>(IPlatformService).isWindows) {
39-
// On windows, the solution is to go into cmd, then run the batch (.bat) file and go back into powershell.
40-
const powershellExe = targetShell === TerminalShellType.powershell ? 'powershell' : 'pwsh';
41-
const activationCmd = scriptFile.fileToCommandArgument();
42-
return [
43-
`& cmd /k "${activationCmd.replace(/"/g, '""')} & ${powershellExe}"`
44-
];
45-
} else {
46-
// Powershell on non-windows os, we cannot execute the batch file.
47-
return;
48-
}
36+
return;
4937
}
5038
}
5139

src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
'use strict';
5+
46
import { injectable } from 'inversify';
57
import * as path from 'path';
68
import { Uri } from 'vscode';
@@ -105,19 +107,7 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman
105107
envName: string,
106108
targetShell: TerminalShellType
107109
): Promise<string[] | undefined> {
108-
// https://github.com/conda/conda/issues/626
109-
// On windows, the solution is to go into cmd, then run the batch (.bat) file and go back into powershell.
110-
const powershellExe = targetShell === TerminalShellType.powershell ? 'powershell' : 'pwsh';
111-
const activateCmd = await this.getWindowsActivateCommand();
112-
113-
let cmdStyleCmd = `${activateCmd} ${envName.toCommandArgument()}`;
114-
// we need to double-quote any cmd quotes as we will wrap them
115-
// in another layer of quotes for powershell:
116-
cmdStyleCmd = cmdStyleCmd.replace(/"/g, '""');
117-
118-
return [
119-
`& cmd /k "${cmdStyleCmd} & ${powershellExe}"`
120-
];
110+
return;
121111
}
122112

123113
public async getFishCommands(

0 commit comments

Comments
 (0)