Skip to content

Commit 542ae64

Browse files
authored
Disable env activation in PowerShell using command prompt (#2892)
* Disable powershell activation using command prompt * Change to cmd prompt and fix code review comments * Change news entry * Fix tests
1 parent a339d2f commit 542ae64

38 files changed

Lines changed: 692 additions & 189 deletions

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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ export enum DiagnosticCodes {
99
NoPythonInterpretersDiagnostic = 'NoPythonInterpretersDiagnostic',
1010
MacInterpreterSelectedAndNoOtherInterpretersDiagnostic = 'MacInterpreterSelectedAndNoOtherInterpretersDiagnostic',
1111
MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic = 'MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic',
12-
InvalidPythonPathInDebuggerDiagnostic = 'InvalidPythonPathInDebuggerDiagnostic'
12+
InvalidPythonPathInDebuggerDiagnostic = 'InvalidPythonPathInDebuggerDiagnostic',
13+
EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic = 'EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic'
1314
}

src/client/application/diagnostics/serviceRegistry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { IServiceManager } from '../../ioc/types';
77
import { EnvironmentPathVariableDiagnosticsService, EnvironmentPathVariableDiagnosticsServiceId } from './checks/envPathVariable';
88
import { InvalidDebuggerTypeDiagnosticsService, InvalidDebuggerTypeDiagnosticsServiceId } from './checks/invalidDebuggerType';
99
import { InvalidPythonPathInDebuggerService, InvalidPythonPathInDebuggerServiceId } from './checks/invalidPythonPathInDebugger';
10+
import { PowerShellActivationHackDiagnosticsService, PowerShellActivationHackDiagnosticsServiceId } from './checks/powerShellActivation';
1011
import { InvalidPythonInterpreterService, InvalidPythonInterpreterServiceId } from './checks/pythonInterpreter';
1112
import { DiagnosticsCommandFactory } from './commands/factory';
1213
import { IDiagnosticsCommandFactory } from './commands/types';
@@ -21,5 +22,6 @@ export function registerTypes(serviceManager: IServiceManager) {
2122
serviceManager.addSingleton<IDiagnosticsService>(IDiagnosticsService, InvalidDebuggerTypeDiagnosticsService, InvalidDebuggerTypeDiagnosticsServiceId);
2223
serviceManager.addSingleton<IDiagnosticsService>(IDiagnosticsService, InvalidPythonInterpreterService, InvalidPythonInterpreterServiceId);
2324
serviceManager.addSingleton<IDiagnosticsService>(IDiagnosticsService, InvalidPythonPathInDebuggerService, InvalidPythonPathInDebuggerServiceId);
25+
serviceManager.addSingleton<IDiagnosticsService>(IDiagnosticsService, PowerShellActivationHackDiagnosticsService, PowerShellActivationHackDiagnosticsServiceId);
2426
serviceManager.addSingleton<IDiagnosticsCommandFactory>(IDiagnosticsCommandFactory, DiagnosticsCommandFactory);
2527
}

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
@@ -28,6 +28,8 @@ import { PersistentStateFactory } from './persistentState';
2828
import { IS_64_BIT, IS_WINDOWS } from './platform/constants';
2929
import { PathUtils } from './platform/pathUtils';
3030
import { CurrentProcess } from './process/currentProcess';
31+
import { TerminalActivator } from './terminal/activator';
32+
import { PowershellTerminalActivationFailedHandler } from './terminal/activator/powershellFailedHandler';
3133
import { Bash } from './terminal/environmentActivationProviders/bash';
3234
import {
3335
CommandPromptAndPowerShell
@@ -37,7 +39,7 @@ import { TerminalServiceFactory } from './terminal/factory';
3739
import { TerminalHelper } from './terminal/helper';
3840
import {
3941
ITerminalActivationCommandProvider,
40-
ITerminalHelper, ITerminalServiceFactory
42+
ITerminalActivationHandler, ITerminalActivator, ITerminalHelper, ITerminalServiceFactory
4143
} from './terminal/types';
4244
import {
4345
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+
}

0 commit comments

Comments
 (0)