Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move shell detectors into separate classes #6401

Merged
merged 15 commits into from
Jul 8, 2019
11 changes: 9 additions & 2 deletions src/client/common/serviceRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import { IHttpClient, IFileDownloader } from '../common/types';
import { IFileDownloader, IHttpClient } from '../common/types';
import { IServiceManager } from '../ioc/types';
import { ImportTracker } from '../telemetry/importTracker';
import { IImportTracker } from '../telemetry/types';
Expand Down Expand Up @@ -34,6 +34,7 @@ import { ProductInstaller } from './installer/productInstaller';
import { LiveShareApi } from './liveshare/liveshare';
import { Logger } from './logger';
import { BrowserService } from './net/browser';
import { FileDownloader } from './net/fileDownloader';
import { HttpClient } from './net/httpClient';
import { NugetService } from './nuget/nugetService';
import { INugetService } from './nuget/types';
Expand All @@ -52,7 +53,11 @@ import { PipEnvActivationCommandProvider } from './terminal/environmentActivatio
import { PyEnvActivationCommandProvider } from './terminal/environmentActivationProviders/pyenvActivationProvider';
import { TerminalServiceFactory } from './terminal/factory';
import { TerminalHelper } from './terminal/helper';
import { SettingsShellDetector } from './terminal/shellDetectors/settingsShellDetector';
import { TerminalNameShellDetector } from './terminal/shellDetectors/terminalNameShellDetector';
import { UserEnvironmentShellDetector } from './terminal/shellDetectors/userEnvironmentShellDetector';
import {
IShellDetector,
ITerminalActivationCommandProvider,
ITerminalActivationHandler,
ITerminalActivator,
Expand All @@ -79,7 +84,6 @@ import {
} from './types';
import { IMultiStepInputFactory, MultiStepInputFactory } from './utils/multiStepInput';
import { Random } from './utils/random';
import { FileDownloader } from './net/fileDownloader';

export function registerTypes(serviceManager: IServiceManager) {
serviceManager.addSingletonInstance<boolean>(IsWindows, IS_WINDOWS);
Expand Down Expand Up @@ -129,4 +133,7 @@ export function registerTypes(serviceManager: IServiceManager) {
serviceManager.addSingleton<IAsyncDisposableRegistry>(IAsyncDisposableRegistry, AsyncDisposableRegistry);
serviceManager.addSingleton<IMultiStepInputFactory>(IMultiStepInputFactory, MultiStepInputFactory);
serviceManager.addSingleton<IImportTracker>(IImportTracker, ImportTracker);
serviceManager.addSingleton<IShellDetector>(IShellDetector, TerminalNameShellDetector);
serviceManager.addSingleton<IShellDetector>(IShellDetector, SettingsShellDetector);
serviceManager.addSingleton<IShellDetector>(IShellDetector, UserEnvironmentShellDetector);
}
13 changes: 6 additions & 7 deletions src/client/common/terminal/helper.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { inject, injectable, named } from 'inversify';
import { inject, injectable, multiInject, named } from 'inversify';
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, ICurrentProcess, Resource } from '../types';
import { IConfigurationService, Resource } from '../types';
import { OSType } from '../utils/platform';
import { ShellDetector } from './shellDetector';
import { ITerminalActivationCommandProvider, ITerminalHelper, TerminalActivationProviders, TerminalShellType } from './types';
import { IShellDetector, ITerminalActivationCommandProvider, ITerminalHelper, TerminalActivationProviders, TerminalShellType } from './types';

@injectable()
export class TerminalHelper implements ITerminalHelper {
Expand All @@ -28,10 +28,9 @@ export class TerminalHelper implements ITerminalHelper {
@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(ICurrentProcess) private readonly currentProcess: ICurrentProcess,
@inject(IWorkspaceService) private readonly workspace: IWorkspaceService
@multiInject(IShellDetector) shellDetectors: IShellDetector[]
) {
this.shellDetector = new ShellDetector(this.platform, this.currentProcess, this.workspace);
this.shellDetector = new ShellDetector(this.platform, shellDetectors);

}
public createTerminal(title?: string): Terminal {
Expand Down
170 changes: 17 additions & 153 deletions src/client/common/terminal/shellDetector.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,29 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { inject, injectable } from 'inversify';
'use strict';

import { inject, injectable, multiInject } from 'inversify';
import { Terminal } from 'vscode';
import { sendTelemetryEvent } from '../../telemetry';
import { EventName } from '../../telemetry/constants';
import { IWorkspaceService } from '../application/types';
import '../extensions';
import { traceVerbose } from '../logger';
import { IPlatformService } from '../platform/types';
import { ICurrentProcess } from '../types';
import { OSType } from '../utils/platform';
import { TerminalShellType } from './types';

// Types of shells can be found here:
// 1. https://wiki.ubuntu.com/ChangingShells
const IS_GITBASH = /(gitbash.exe$)/i;
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$|cmd$)/i;
const IS_POWERSHELL = /(powershell.exe$|powershell$)/i;
const IS_POWERSHELL_CORE = /(pwsh.exe$|pwsh$)/i;
const IS_FISH = /(fish$)/i;
const IS_CSHELL = /(csh$)/i;
const IS_TCSHELL = /(tcsh$)/i;
const IS_XONSH = /(xonsh$)/i;
import { IShellDetector, ShellIdentificationTelemetry, TerminalShellType } from './types';

const defaultOSShells = {
[OSType.Linux]: TerminalShellType.bash,
[OSType.OSX]: TerminalShellType.bash,
[OSType.Windows]: TerminalShellType.commandPrompt,
[OSType.Unknown]: undefined
[OSType.Unknown]: TerminalShellType.other
};

type ShellIdentificationTelemetry = {
failed: boolean;
terminalProvided: boolean;
shellIdentificationSource: 'terminalName' | 'settings' | 'environment' | 'default';
hasCustomShell: undefined | boolean;
hasShellInEnv: undefined | boolean;
};
const detectableShells = new Map<TerminalShellType, RegExp>();
detectableShells.set(TerminalShellType.powershell, IS_POWERSHELL);
detectableShells.set(TerminalShellType.gitbash, IS_GITBASH);
detectableShells.set(TerminalShellType.bash, IS_BASH);
detectableShells.set(TerminalShellType.wsl, IS_WSL);
detectableShells.set(TerminalShellType.zsh, IS_ZSH);
detectableShells.set(TerminalShellType.ksh, IS_KSH);
detectableShells.set(TerminalShellType.commandPrompt, IS_COMMAND);
detectableShells.set(TerminalShellType.fish, IS_FISH);
detectableShells.set(TerminalShellType.tcshell, IS_TCSHELL);
detectableShells.set(TerminalShellType.cshell, IS_CSHELL);
detectableShells.set(TerminalShellType.powershellCore, IS_POWERSHELL_CORE);
detectableShells.set(TerminalShellType.xonsh, IS_XONSH);

@injectable()
export class ShellDetector {
constructor(@inject(IPlatformService) private readonly platform: IPlatformService,
@inject(ICurrentProcess) private readonly currentProcess: ICurrentProcess,
@inject(IWorkspaceService) private readonly workspace: IWorkspaceService
@multiInject(IShellDetector) private readonly shellDetectors: IShellDetector[]
) { }
/**
* Logic is as follows:
Expand All @@ -75,7 +38,7 @@ export class ShellDetector {
* @memberof TerminalHelper
*/
public identifyTerminalShell(terminal?: Terminal): TerminalShellType {
let shell = TerminalShellType.other;
let shell: TerminalShellType | undefined;
const telemetryProperties: ShellIdentificationTelemetry = {
failed: true,
shellIdentificationSource: 'default',
Expand All @@ -84,19 +47,15 @@ export class ShellDetector {
hasShellInEnv: undefined
};

// Step 1. Determine shell based on the name of the terminal.
if (terminal) {
shell = this.identifyShellByTerminalName(terminal.name, telemetryProperties);
}

// Step 2. Detemrine shell based on user settings.
if (shell === TerminalShellType.other) {
shell = this.identifyShellFromSettings(telemetryProperties);
}
// Sort in order of priority and then identify the shell in terminal.
const shellDetectors = this.shellDetectors.slice();
shellDetectors.sort((a, b) => a.priority < b.priority ? 1 : 0);

// Step 3. Determine shell based on user environment.
if (shell === TerminalShellType.other) {
shell = this.identifyShellFromUserEnv(telemetryProperties);
for (const detector of shellDetectors) {
shell = detector.identify(telemetryProperties, terminal);
if (shell) {
break;
}
}

// This information is useful in determining how well we identify shells on users machines.
Expand All @@ -106,104 +65,9 @@ export class ShellDetector {
traceVerbose(`Shell identified as '${shell}'`);

// If we could not identify the shell, use the defaults.
return shell === TerminalShellType.other ? (defaultOSShells[this.platform.osType] || TerminalShellType.other) : shell;
}
public getTerminalShellPath(): string | undefined {
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 getDefaultPlatformShell(): string {
return getDefaultShell(this.platform, this.currentProcess);
}
public identifyShellByTerminalName(name: string, telemetryProperties: ShellIdentificationTelemetry): TerminalShellType {
const shell = Array.from(detectableShells.keys())
.reduce((matchedShell, shellToDetect) => {
if (matchedShell === TerminalShellType.other && detectableShells.get(shellToDetect)!.test(name)) {
return shellToDetect;
}
return matchedShell;
}, TerminalShellType.other);
traceVerbose(`Terminal name '${name}' identified as shell '${shell}'`);
telemetryProperties.shellIdentificationSource = shell === TerminalShellType.other ? telemetryProperties.shellIdentificationSource : 'terminalName';
return shell;
}
public identifyShellFromSettings(telemetryProperties: ShellIdentificationTelemetry): TerminalShellType {
const shellPath = this.getTerminalShellPath();
telemetryProperties.hasCustomShell = !!shellPath;
const shell = shellPath ? this.identifyShellFromShellPath(shellPath) : TerminalShellType.other;

if (shell !== TerminalShellType.other) {
telemetryProperties.shellIdentificationSource = 'environment';
}
telemetryProperties.shellIdentificationSource = 'settings';
traceVerbose(`Shell path from user settings '${shellPath}'`);
return shell;
}

public identifyShellFromUserEnv(telemetryProperties: ShellIdentificationTelemetry): TerminalShellType {
const shellPath = this.getDefaultPlatformShell();
telemetryProperties.hasShellInEnv = !!shellPath;
const shell = this.identifyShellFromShellPath(shellPath);

if (shell !== TerminalShellType.other) {
telemetryProperties.shellIdentificationSource = 'environment';
if (shell === undefined || shell === TerminalShellType.other) {
shell = defaultOSShells[this.platform.osType];
}
traceVerbose(`Shell path from user env '${shellPath}'`);
return shell;
}
public identifyShellFromShellPath(shellPath: string): TerminalShellType {
const shell = Array.from(detectableShells.keys())
.reduce((matchedShell, shellToDetect) => {
if (matchedShell === TerminalShellType.other && detectableShells.get(shellToDetect)!.test(shellPath)) {
return shellToDetect;
}
return matchedShell;
}, TerminalShellType.other);

traceVerbose(`Shell path '${shellPath}'`);
traceVerbose(`Shell path identified as shell '${shell}'`);
return shell;
}
}

/*
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(platform: IPlatformService, currentProcess: ICurrentProcess): string {
if (platform.osType === OSType.Windows) {
return getTerminalDefaultShellWindows(platform, currentProcess);
}

return currentProcess.env.SHELL && currentProcess.env.SHELL !== '/bin/false' ? currentProcess.env.SHELL : '/bin/bash';
}
function getTerminalDefaultShellWindows(platform: IPlatformService, currentProcess: ICurrentProcess): string {
const isAtLeastWindows10 = parseFloat(platform.osRelease) >= 10;
const is32ProcessOn64Windows = currentProcess.env.hasOwnProperty('PROCESSOR_ARCHITEW6432');
const powerShellPath = `${currentProcess.env.windir}\\${is32ProcessOn64Windows ? 'Sysnative' : 'System32'}\\WindowsPowerShell\\v1.0\\powershell.exe`;
return isAtLeastWindows10 ? powerShellPath : getWindowsShell(currentProcess);
}

function getWindowsShell(currentProcess: ICurrentProcess): string {
return currentProcess.env.comspec || 'cmd.exe';
}
70 changes: 70 additions & 0 deletions src/client/common/terminal/shellDetectors/baseShellDetector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { injectable, unmanaged } from 'inversify';
import { Terminal } from 'vscode';
import { traceVerbose } from '../../logger';
import { IShellDetector, ShellIdentificationTelemetry, TerminalShellType } from '../types';

// tslint:disable: max-classes-per-file

/*
When identifying the shell use the following algorithm:
* 1. Identify shell based on the name of the terminal (if there is one already opened and used).
* 2. Identify shell based on the settings in VSC.
* 3. Identify shell based on users environment variables.
* 4. Use default shells (bash for mac and linux, cmd for windows).
*/

// Types of shells can be found here:
// 1. https://wiki.ubuntu.com/ChangingShells
const IS_GITBASH = /(gitbash.exe$)/i;
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$|cmd$)/i;
const IS_POWERSHELL = /(powershell.exe$|powershell$)/i;
const IS_POWERSHELL_CORE = /(pwsh.exe$|pwsh$)/i;
const IS_FISH = /(fish$)/i;
const IS_CSHELL = /(csh$)/i;
const IS_TCSHELL = /(tcsh$)/i;
const IS_XONSH = /(xonsh$)/i;

const detectableShells = new Map<TerminalShellType, RegExp>();
detectableShells.set(TerminalShellType.powershell, IS_POWERSHELL);
detectableShells.set(TerminalShellType.gitbash, IS_GITBASH);
detectableShells.set(TerminalShellType.bash, IS_BASH);
detectableShells.set(TerminalShellType.wsl, IS_WSL);
detectableShells.set(TerminalShellType.zsh, IS_ZSH);
detectableShells.set(TerminalShellType.ksh, IS_KSH);
detectableShells.set(TerminalShellType.commandPrompt, IS_COMMAND);
detectableShells.set(TerminalShellType.fish, IS_FISH);
detectableShells.set(TerminalShellType.tcshell, IS_TCSHELL);
detectableShells.set(TerminalShellType.cshell, IS_CSHELL);
detectableShells.set(TerminalShellType.powershellCore, IS_POWERSHELL_CORE);
detectableShells.set(TerminalShellType.xonsh, IS_XONSH);

@injectable()
export abstract class BaseShellDetector implements IShellDetector {
constructor(@unmanaged() public readonly priority: number) { }
public abstract identify(telemetryProperties: ShellIdentificationTelemetry, terminal?: Terminal): TerminalShellType | undefined;
public identifyShellFromShellPath(shellPath: string): TerminalShellType {
const shell = Array.from(detectableShells.keys())
.reduce((matchedShell, shellToDetect) => {
if (matchedShell === TerminalShellType.other) {
const pat = detectableShells.get(shellToDetect);
if (pat && pat.test(shellPath)) {
return shellToDetect;
}
}
return matchedShell;
}, TerminalShellType.other);

traceVerbose(`Shell path '${shellPath}'`);
traceVerbose(`Shell path identified as shell '${shell}'`);
return shell;
}
}
Loading