Skip to content

Commit

Permalink
Move shell detectors into separate classes (#6401)
Browse files Browse the repository at this point in the history
* Move shell detectors into separate classes
  • Loading branch information
DonJayamanne authored Jul 8, 2019
1 parent 9d48ebc commit 0eb059b
Show file tree
Hide file tree
Showing 16 changed files with 613 additions and 384 deletions.
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

0 comments on commit 0eb059b

Please sign in to comment.