Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,4 @@
]
}
]
}
}
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# Changelog

## 2019.5.1 (30 May 2019)
## 2019.5.2 (4 June 2019)

### Fixes

1. Changes to identificaction of `shell` for the activation of environments in the terminal.
([#5743](https://github.com/microsoft/vscode-python/issues/5743))


## 2019.5.17517 (30 May 2019)

### Fixes

Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "python",
"displayName": "Python",
"description": "Linting, Debugging (multi-threaded, remote), Intellisense, code formatting, refactoring, unit tests, snippets, and more.",
"version": "2019.5.1",
"version": "2019.5.2",
"languageServerVersion": "0.2.82",
"publisher": "ms-python",
"author": {
Expand Down
3 changes: 1 addition & 2 deletions src/client/common/terminal/activator/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ export class BaseTerminalActivator implements ITerminalActivator {
}
const deferred = createDeferred<boolean>();
this.activatedTerminals.set(terminal, deferred.promise);
const shellPath = this.helper.getTerminalShellPath();
const terminalShellType = !shellPath || shellPath.length === 0 ? TerminalShellType.other : this.helper.identifyTerminalShell(shellPath);
const terminalShellType = this.helper.identifyTerminalShell(terminal);

const activationCommamnds = await this.helper.getEnvironmentActivationCommands(terminalShellType, resource);
let activated = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ export class PowershellTerminalActivationFailedHandler implements ITerminalActiv
@inject(IPlatformService) private readonly platformService: IPlatformService,
@inject(IDiagnosticsService) @named(PowerShellActivationHackDiagnosticsServiceId) private readonly diagnosticService: IDiagnosticsService) {
}
public async handleActivation(_terminal: Terminal, resource: Resource, _preserveFocus: boolean, activated: boolean) {
public async handleActivation(terminal: Terminal, resource: Resource, _preserveFocus: boolean, activated: boolean) {
if (activated || !this.platformService.isWindows) {
return;
}
const shell = this.helper.identifyTerminalShell(this.helper.getTerminalShellPath());
const shell = this.helper.identifyTerminalShell(terminal);
if (shell !== TerminalShellType.powershell && shell !== TerminalShellType.powershellCore) {
return;
}
Expand Down
93 changes: 64 additions & 29 deletions src/client/common/terminal/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
// Licensed under the MIT License.

import { inject, injectable, named } from 'inversify';
import * as os from 'os';
import { Terminal, Uri } from 'vscode';
import { ICondaService, IInterpreterService, InterpreterType, PythonInterpreter } from '../../interpreter/contracts';
import { sendTelemetryEvent } from '../../telemetry';
import { EventName } from '../../telemetry/constants';
import { ITerminalManager, IWorkspaceService } from '../application/types';
import { ITerminalManager } from '../application/types';
import '../extensions';
import { traceDecorators, traceError } from '../logger';
import { IPlatformService } from '../platform/types';
import { IConfigurationService, Resource } from '../types';
import { IConfigurationService, ICurrentProcess, Resource } from '../types';
import { OSType } from '../utils/platform';
import { ITerminalActivationCommandProvider, ITerminalHelper, TerminalActivationProviders, TerminalShellType } from './types';

Expand All @@ -21,7 +22,7 @@ const IS_BASH = /(bash.exe$|bash$)/i;
const IS_WSL = /(wsl.exe$)/i;
const IS_ZSH = /(zsh$)/i;
const IS_KSH = /(ksh$)/i;
const IS_COMMAND = /cmd.exe$/i;
const IS_COMMAND = /(cmd.exe$|cmd$)/i;
const IS_POWERSHELL = /(powershell.exe$|powershell$)/i;
const IS_POWERSHELL_CORE = /(pwsh.exe$|pwsh$)/i;
const IS_FISH = /(fish$)/i;
Expand All @@ -41,15 +42,15 @@ export class TerminalHelper implements ITerminalHelper {
private readonly detectableShells: Map<TerminalShellType, RegExp>;
constructor(@inject(IPlatformService) private readonly platform: IPlatformService,
@inject(ITerminalManager) private readonly terminalManager: ITerminalManager,
@inject(IWorkspaceService) private readonly workspace: IWorkspaceService,
@inject(ICondaService) private readonly condaService: ICondaService,
@inject(IInterpreterService) private readonly interpreterService: IInterpreterService,
@inject(IConfigurationService) private readonly configurationService: IConfigurationService,
@inject(ITerminalActivationCommandProvider) @named(TerminalActivationProviders.conda) private readonly conda: ITerminalActivationCommandProvider,
@inject(ITerminalActivationCommandProvider) @named(TerminalActivationProviders.bashCShellFish) private readonly bashCShellFish: ITerminalActivationCommandProvider,
@inject(ITerminalActivationCommandProvider) @named(TerminalActivationProviders.commandPromptAndPowerShell) private readonly commandPromptAndPowerShell: ITerminalActivationCommandProvider,
@inject(ITerminalActivationCommandProvider) @named(TerminalActivationProviders.pyenv) private readonly pyenv: ITerminalActivationCommandProvider,
@inject(ITerminalActivationCommandProvider) @named(TerminalActivationProviders.pipenv) private readonly pipenv: ITerminalActivationCommandProvider
@inject(ITerminalActivationCommandProvider) @named(TerminalActivationProviders.pipenv) private readonly pipenv: ITerminalActivationCommandProvider,
@inject(IConfigurationService) private readonly currentProcess: ICurrentProcess
) {
this.detectableShells = new Map<TerminalShellType, RegExp>();
this.detectableShells.set(TerminalShellType.powershell, IS_POWERSHELL);
Expand All @@ -68,37 +69,44 @@ export class TerminalHelper implements ITerminalHelper {
public createTerminal(title?: string): Terminal {
return this.terminalManager.createTerminal({ name: title });
}
public identifyTerminalShell(shellPath: string): TerminalShellType {
public identifyTerminalShell(terminal?: Terminal): TerminalShellType {
let shell = TerminalShellType.other;
let usingDefaultShell = false;
const terminalProvided = !!terminal;
// Determine shell based on the name of the terminal.
// See solution here https://github.com/microsoft/vscode/issues/74233#issuecomment-497527337
if (terminal) {
shell = this.identifyTerminalShellByName(terminal.name);
}

// If still unable to identify, then use fall back to determine path to the default shell.
if (shell === TerminalShellType.other) {
const shellPath = getDefaultShell(this.platform.osType, this.currentProcess);
shell = Array.from(this.detectableShells.keys())
.reduce((matchedShell, shellToDetect) => {
if (matchedShell === TerminalShellType.other && this.detectableShells.get(shellToDetect)!.test(shellPath)) {
return shellToDetect;
}
return matchedShell;
}, TerminalShellType.other);

// We have restored to using the default shell.
usingDefaultShell = shell !== TerminalShellType.other;
}
const properties = { failed: shell === TerminalShellType.other, usingDefaultShell, terminalProvided };
sendTelemetryEvent(EventName.TERMINAL_SHELL_IDENTIFICATION, undefined, properties);
return shell;
}
public identifyTerminalShellByName(name: string): TerminalShellType {
return Array.from(this.detectableShells.keys())
.reduce((matchedShell, shellToDetect) => {
if (matchedShell === TerminalShellType.other && this.detectableShells.get(shellToDetect)!.test(shellPath)) {
if (matchedShell === TerminalShellType.other && this.detectableShells.get(shellToDetect)!.test(name)) {
return shellToDetect;
}
return matchedShell;
}, TerminalShellType.other);
}
public getTerminalShellPath(): string {
const shellConfig = this.workspace.getConfiguration('terminal.integrated.shell');
let osSection = '';
switch (this.platform.osType) {
case OSType.Windows: {
osSection = 'windows';
break;
}
case OSType.OSX: {
osSection = 'osx';
break;
}
case OSType.Linux: {
osSection = 'linux';
break;
}
default: {
return '';
}
}
return shellConfig.get<string>(osSection)!;
}

public buildCommandForTerminal(terminalShellType: TerminalShellType, command: string, args: string[]) {
const isPowershell = terminalShellType === TerminalShellType.powershell || terminalShellType === TerminalShellType.powershellCore;
const commandPrefix = isPowershell ? '& ' : '';
Expand Down Expand Up @@ -173,3 +181,30 @@ export class TerminalHelper implements ITerminalHelper {
}
}
}

/*
The following code is based on VS Code from https://github.com/microsoft/vscode/blob/5c65d9bfa4c56538150d7f3066318e0db2c6151f/src/vs/workbench/contrib/terminal/node/terminal.ts#L12-L55
This is only a fall back to identify the default shell used by VSC.
On Windows, determine the default shell.
On others, default to bash.
*/
function getDefaultShell(osType: OSType, currentProcess: ICurrentProcess): string {
if (osType === OSType.Windows) {
return getTerminalDefaultShellWindows(osType, currentProcess);
}
return '/bin/bash';
}
let _TERMINAL_DEFAULT_SHELL_WINDOWS: string | null = null;
function getTerminalDefaultShellWindows(osType: OSType, currentProcess: ICurrentProcess): string {
if (!_TERMINAL_DEFAULT_SHELL_WINDOWS) {
const isAtLeastWindows10 = osType === OSType.Windows && parseFloat(os.release()) >= 10;
const is32ProcessOn64Windows = process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432');
const powerShellPath = `${process.env.windir}\\${is32ProcessOn64Windows ? 'Sysnative' : 'System32'}\\WindowsPowerShell\\v1.0\\powershell.exe`;
_TERMINAL_DEFAULT_SHELL_WINDOWS = isAtLeastWindows10 ? powerShellPath : getWindowsShell(currentProcess);
}
return _TERMINAL_DEFAULT_SHELL_WINDOWS;
}

function getWindowsShell(currentProcess: ICurrentProcess): string {
return currentProcess.env.comspec || 'cmd.exe';
}
3 changes: 1 addition & 2 deletions src/client/common/terminal/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ export class TerminalService implements ITerminalService, Disposable {
if (this.terminal) {
return;
}
const shellPath = this.terminalHelper.getTerminalShellPath();
this.terminalShellType = !shellPath || shellPath.length === 0 ? TerminalShellType.other : this.terminalHelper.identifyTerminalShell(shellPath);
this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal);
this.terminal = this.terminalManager.createTerminal({ name: this.title });

// Sometimes the terminal takes some time to start up before it can start accepting input.
Expand Down
3 changes: 1 addition & 2 deletions src/client/common/terminal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ export const ITerminalHelper = Symbol('ITerminalHelper');

export interface ITerminalHelper {
createTerminal(title?: string): Terminal;
identifyTerminalShell(shellPath: string): TerminalShellType;
getTerminalShellPath(): string;
identifyTerminalShell(terminal?: Terminal): TerminalShellType;
buildCommandForTerminal(terminalShellType: TerminalShellType, command: string, args: string[]): string;
getEnvironmentActivationCommands(terminalShellType: TerminalShellType, resource?: Uri): Promise<string[] | undefined>;
getEnvironmentActivationShellCommands(resource: Resource, interpreter?: PythonInterpreter): Promise<string[] | undefined>;
Expand Down
2 changes: 1 addition & 1 deletion src/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ async function getActivationTelemetryProps(serviceContainer: IServiceContainer):
// be able to partially populate as much as possible instead
// (through granular try-catch statements).
const terminalHelper = serviceContainer.get<ITerminalHelper>(ITerminalHelper);
const terminalShellType = terminalHelper.identifyTerminalShell(terminalHelper.getTerminalShellPath());
const terminalShellType = terminalHelper.identifyTerminalShell();
const condaLocator = serviceContainer.get<ICondaService>(ICondaService);
const interpreterService = serviceContainer.get<IInterpreterService>(IInterpreterService);
const workspaceService = serviceContainer.get<IWorkspaceService>(IWorkspaceService);
Expand Down
1 change: 1 addition & 0 deletions src/client/telemetry/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export enum EventName {
PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES = 'PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES',
PYTHON_INTERPRETER_ACTIVATION_FOR_RUNNING_CODE = 'PYTHON_INTERPRETER_ACTIVATION_FOR_RUNNING_CODE',
PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL = 'PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL',
TERMINAL_SHELL_IDENTIFICATION = 'TERMINAL_SHELL_IDENTIFICATION',
PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT = 'PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT',
ENVFILE_VARIABLE_SUBSTITUTION = 'ENVFILE_VARIABLE_SUBSTITUTION',
WORKSPACE_SYMBOLS_BUILD = 'WORKSPACE_SYMBOLS.BUILD',
Expand Down
7 changes: 7 additions & 0 deletions src/client/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,4 +368,11 @@ export interface IEventNamePropertyMapping {
[EventName.UNITTEST_NAVIGATE_TEST_FUNCTION]: { focus_code: boolean };
[EventName.UNITTEST_NAVIGATE_TEST_SUITE]: { focus_code: boolean };
[EventName.UNITTEST_EXPLORER_WORK_SPACE_COUNT]: { count: number };
/*
Telemetry event sent to provide information on whether we have successfully identify the type of shell used.
failed - If true, indicates we have failed to identify the shell. Note this impacts impacts ability to activate environments in the terminal & code.
usingDefaultShell - If true, this indicates we failed to identify the shell and we have reverted to using the default shell on user machine.
terminalProvided - If true, we used the terminal provided to detec the shell. If not provided, we use the default shell on user machine.
*/
[EventName.TERMINAL_SHELL_IDENTIFICATION]: { failed: boolean; usingDefaultShell: boolean; terminalProvided: boolean };
}
7 changes: 4 additions & 3 deletions src/test/common/terminals/activation.conda.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import { anything, instance, mock, when } from 'ts-mockito';
import * as TypeMoq from 'typemoq';
import { Disposable } from 'vscode';
import { TerminalManager } from '../../../client/common/application/terminalManager';
import { WorkspaceService } from '../../../client/common/application/workspace';
import '../../../client/common/extensions';
import {
IFileSystem, IPlatformService
} from '../../../client/common/platform/types';
import { CurrentProcess } from '../../../client/common/process/currentProcess';
import {
IProcessService, IProcessServiceFactory
} from '../../../client/common/process/types';
Expand Down Expand Up @@ -84,15 +84,16 @@ suite('Terminal Environment Activation conda', () => {
pythonSettings.setup(s => s.terminal).returns(() => terminalSettings.object);

terminalHelper = new TerminalHelper(platformService.object,
instance(mock(TerminalManager)), instance(mock(WorkspaceService)),
instance(mock(TerminalManager)),
condaService.object,
instance(mock(InterpreterService)),
configService.object,
new CondaActivationCommandProvider(condaService.object, platformService.object, configService.object),
instance(bash),
mock(CommandPromptAndPowerShell),
mock(PyEnvActivationCommandProvider),
mock(PipEnvActivationCommandProvider));
mock(PipEnvActivationCommandProvider),
instance(mock(CurrentProcess)));

});
teardown(() => {
Expand Down
3 changes: 0 additions & 3 deletions src/test/common/terminals/activator/base.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ suite('Terminal Base Activator', () => {
const titleSuffix = `(${item.commandCount} activation command, and preserve focus in terminal is ${item.preserveFocus})`;
const activationCommands = item.commandCount === 1 ? ['CMD1'] : ['CMD1', 'CMD2'];
test(`Terminal is activated ${titleSuffix}`, async () => {
helper.setup(h => h.getTerminalShellPath()).returns(() => '');
helper.setup(h => h.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(activationCommands));
const terminal = TypeMoq.Mock.ofType<Terminal>();

Expand All @@ -50,7 +49,6 @@ suite('Terminal Base Activator', () => {
terminal.verifyAll();
});
test(`Terminal is activated only once ${titleSuffix}`, async () => {
helper.setup(h => h.getTerminalShellPath()).returns(() => '');
helper.setup(h => h.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(activationCommands));
const terminal = TypeMoq.Mock.ofType<Terminal>();

Expand All @@ -72,7 +70,6 @@ suite('Terminal Base Activator', () => {
terminal.verifyAll();
});
test(`Terminal is activated only once ${titleSuffix} (even when not waiting)`, async () => {
helper.setup(h => h.getTerminalShellPath()).returns(() => '');
helper.setup(h => h.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(activationCommands));
const terminal = TypeMoq.Mock.ofType<Terminal>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,12 @@ suite('Terminal Activation Powershell Failed Handler', () => {

async function testDiagnostics(mustHandleDiagnostics: boolean, isWindows: boolean, activatedSuccessfully: boolean, shellType: TerminalShellType, cmdPromptHasActivationCommands: boolean) {
platform.setup(p => p.isWindows).returns(() => isWindows);
helper
.setup(p => p.getTerminalShellPath())
.returns(() => '');
// .verifiable(TypeMoq.Times.atMostOnce());
helper
.setup(p => p.identifyTerminalShell(TypeMoq.It.isAny()))
.returns(() => shellType);
// .verifiable(TypeMoq.Times.atMostOnce());c
const cmdPromptCommands = cmdPromptHasActivationCommands ? ['a'] : [];
helper.setup(h => h.getEnvironmentActivationCommands(TypeMoq.It.isValue(TerminalShellType.commandPrompt), TypeMoq.It.isAny()))
.returns(() => Promise.resolve(cmdPromptCommands));
// .verifiable(TypeMoq.Times.atMostOnce());

diagnosticService
.setup(d => d.handle(TypeMoq.It.isAny()))
Expand Down
Loading