Skip to content

Commit 0eb059b

Browse files
authored
Move shell detectors into separate classes (#6401)
* Move shell detectors into separate classes
1 parent 9d48ebc commit 0eb059b

16 files changed

+613
-384
lines changed

src/client/common/serviceRegistry.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
3-
import { IHttpClient, IFileDownloader } from '../common/types';
3+
import { IFileDownloader, IHttpClient } from '../common/types';
44
import { IServiceManager } from '../ioc/types';
55
import { ImportTracker } from '../telemetry/importTracker';
66
import { IImportTracker } from '../telemetry/types';
@@ -34,6 +34,7 @@ import { ProductInstaller } from './installer/productInstaller';
3434
import { LiveShareApi } from './liveshare/liveshare';
3535
import { Logger } from './logger';
3636
import { BrowserService } from './net/browser';
37+
import { FileDownloader } from './net/fileDownloader';
3738
import { HttpClient } from './net/httpClient';
3839
import { NugetService } from './nuget/nugetService';
3940
import { INugetService } from './nuget/types';
@@ -52,7 +53,11 @@ import { PipEnvActivationCommandProvider } from './terminal/environmentActivatio
5253
import { PyEnvActivationCommandProvider } from './terminal/environmentActivationProviders/pyenvActivationProvider';
5354
import { TerminalServiceFactory } from './terminal/factory';
5455
import { TerminalHelper } from './terminal/helper';
56+
import { SettingsShellDetector } from './terminal/shellDetectors/settingsShellDetector';
57+
import { TerminalNameShellDetector } from './terminal/shellDetectors/terminalNameShellDetector';
58+
import { UserEnvironmentShellDetector } from './terminal/shellDetectors/userEnvironmentShellDetector';
5559
import {
60+
IShellDetector,
5661
ITerminalActivationCommandProvider,
5762
ITerminalActivationHandler,
5863
ITerminalActivator,
@@ -79,7 +84,6 @@ import {
7984
} from './types';
8085
import { IMultiStepInputFactory, MultiStepInputFactory } from './utils/multiStepInput';
8186
import { Random } from './utils/random';
82-
import { FileDownloader } from './net/fileDownloader';
8387

8488
export function registerTypes(serviceManager: IServiceManager) {
8589
serviceManager.addSingletonInstance<boolean>(IsWindows, IS_WINDOWS);
@@ -129,4 +133,7 @@ export function registerTypes(serviceManager: IServiceManager) {
129133
serviceManager.addSingleton<IAsyncDisposableRegistry>(IAsyncDisposableRegistry, AsyncDisposableRegistry);
130134
serviceManager.addSingleton<IMultiStepInputFactory>(IMultiStepInputFactory, MultiStepInputFactory);
131135
serviceManager.addSingleton<IImportTracker>(IImportTracker, ImportTracker);
136+
serviceManager.addSingleton<IShellDetector>(IShellDetector, TerminalNameShellDetector);
137+
serviceManager.addSingleton<IShellDetector>(IShellDetector, SettingsShellDetector);
138+
serviceManager.addSingleton<IShellDetector>(IShellDetector, UserEnvironmentShellDetector);
132139
}

src/client/common/terminal/helper.ts

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

4-
import { inject, injectable, named } from 'inversify';
4+
import { inject, injectable, multiInject, named } from 'inversify';
55
import { Terminal, Uri } from 'vscode';
66
import { ICondaService, IInterpreterService, InterpreterType, PythonInterpreter } from '../../interpreter/contracts';
77
import { sendTelemetryEvent } from '../../telemetry';
88
import { EventName } from '../../telemetry/constants';
9-
import { ITerminalManager, IWorkspaceService } from '../application/types';
9+
import { ITerminalManager } from '../application/types';
1010
import '../extensions';
1111
import { traceDecorators, traceError } from '../logger';
1212
import { IPlatformService } from '../platform/types';
13-
import { IConfigurationService, ICurrentProcess, Resource } from '../types';
13+
import { IConfigurationService, Resource } from '../types';
1414
import { OSType } from '../utils/platform';
1515
import { ShellDetector } from './shellDetector';
16-
import { ITerminalActivationCommandProvider, ITerminalHelper, TerminalActivationProviders, TerminalShellType } from './types';
16+
import { IShellDetector, ITerminalActivationCommandProvider, ITerminalHelper, TerminalActivationProviders, TerminalShellType } from './types';
1717

1818
@injectable()
1919
export class TerminalHelper implements ITerminalHelper {
@@ -28,10 +28,9 @@ export class TerminalHelper implements ITerminalHelper {
2828
@inject(ITerminalActivationCommandProvider) @named(TerminalActivationProviders.commandPromptAndPowerShell) private readonly commandPromptAndPowerShell: ITerminalActivationCommandProvider,
2929
@inject(ITerminalActivationCommandProvider) @named(TerminalActivationProviders.pyenv) private readonly pyenv: ITerminalActivationCommandProvider,
3030
@inject(ITerminalActivationCommandProvider) @named(TerminalActivationProviders.pipenv) private readonly pipenv: ITerminalActivationCommandProvider,
31-
@inject(ICurrentProcess) private readonly currentProcess: ICurrentProcess,
32-
@inject(IWorkspaceService) private readonly workspace: IWorkspaceService
31+
@multiInject(IShellDetector) shellDetectors: IShellDetector[]
3332
) {
34-
this.shellDetector = new ShellDetector(this.platform, this.currentProcess, this.workspace);
33+
this.shellDetector = new ShellDetector(this.platform, shellDetectors);
3534

3635
}
3736
public createTerminal(title?: string): Terminal {
Lines changed: 17 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,29 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
import { inject, injectable } from 'inversify';
4+
'use strict';
5+
6+
import { inject, injectable, multiInject } from 'inversify';
57
import { Terminal } from 'vscode';
68
import { sendTelemetryEvent } from '../../telemetry';
79
import { EventName } from '../../telemetry/constants';
8-
import { IWorkspaceService } from '../application/types';
910
import '../extensions';
1011
import { traceVerbose } from '../logger';
1112
import { IPlatformService } from '../platform/types';
12-
import { ICurrentProcess } from '../types';
1313
import { OSType } from '../utils/platform';
14-
import { TerminalShellType } from './types';
15-
16-
// Types of shells can be found here:
17-
// 1. https://wiki.ubuntu.com/ChangingShells
18-
const IS_GITBASH = /(gitbash.exe$)/i;
19-
const IS_BASH = /(bash.exe$|bash$)/i;
20-
const IS_WSL = /(wsl.exe$)/i;
21-
const IS_ZSH = /(zsh$)/i;
22-
const IS_KSH = /(ksh$)/i;
23-
const IS_COMMAND = /(cmd.exe$|cmd$)/i;
24-
const IS_POWERSHELL = /(powershell.exe$|powershell$)/i;
25-
const IS_POWERSHELL_CORE = /(pwsh.exe$|pwsh$)/i;
26-
const IS_FISH = /(fish$)/i;
27-
const IS_CSHELL = /(csh$)/i;
28-
const IS_TCSHELL = /(tcsh$)/i;
29-
const IS_XONSH = /(xonsh$)/i;
14+
import { IShellDetector, ShellIdentificationTelemetry, TerminalShellType } from './types';
3015

3116
const defaultOSShells = {
3217
[OSType.Linux]: TerminalShellType.bash,
3318
[OSType.OSX]: TerminalShellType.bash,
3419
[OSType.Windows]: TerminalShellType.commandPrompt,
35-
[OSType.Unknown]: undefined
20+
[OSType.Unknown]: TerminalShellType.other
3621
};
3722

38-
type ShellIdentificationTelemetry = {
39-
failed: boolean;
40-
terminalProvided: boolean;
41-
shellIdentificationSource: 'terminalName' | 'settings' | 'environment' | 'default';
42-
hasCustomShell: undefined | boolean;
43-
hasShellInEnv: undefined | boolean;
44-
};
45-
const detectableShells = new Map<TerminalShellType, RegExp>();
46-
detectableShells.set(TerminalShellType.powershell, IS_POWERSHELL);
47-
detectableShells.set(TerminalShellType.gitbash, IS_GITBASH);
48-
detectableShells.set(TerminalShellType.bash, IS_BASH);
49-
detectableShells.set(TerminalShellType.wsl, IS_WSL);
50-
detectableShells.set(TerminalShellType.zsh, IS_ZSH);
51-
detectableShells.set(TerminalShellType.ksh, IS_KSH);
52-
detectableShells.set(TerminalShellType.commandPrompt, IS_COMMAND);
53-
detectableShells.set(TerminalShellType.fish, IS_FISH);
54-
detectableShells.set(TerminalShellType.tcshell, IS_TCSHELL);
55-
detectableShells.set(TerminalShellType.cshell, IS_CSHELL);
56-
detectableShells.set(TerminalShellType.powershellCore, IS_POWERSHELL_CORE);
57-
detectableShells.set(TerminalShellType.xonsh, IS_XONSH);
58-
5923
@injectable()
6024
export class ShellDetector {
6125
constructor(@inject(IPlatformService) private readonly platform: IPlatformService,
62-
@inject(ICurrentProcess) private readonly currentProcess: ICurrentProcess,
63-
@inject(IWorkspaceService) private readonly workspace: IWorkspaceService
26+
@multiInject(IShellDetector) private readonly shellDetectors: IShellDetector[]
6427
) { }
6528
/**
6629
* Logic is as follows:
@@ -75,7 +38,7 @@ export class ShellDetector {
7538
* @memberof TerminalHelper
7639
*/
7740
public identifyTerminalShell(terminal?: Terminal): TerminalShellType {
78-
let shell = TerminalShellType.other;
41+
let shell: TerminalShellType | undefined;
7942
const telemetryProperties: ShellIdentificationTelemetry = {
8043
failed: true,
8144
shellIdentificationSource: 'default',
@@ -84,19 +47,15 @@ export class ShellDetector {
8447
hasShellInEnv: undefined
8548
};
8649

87-
// Step 1. Determine shell based on the name of the terminal.
88-
if (terminal) {
89-
shell = this.identifyShellByTerminalName(terminal.name, telemetryProperties);
90-
}
91-
92-
// Step 2. Detemrine shell based on user settings.
93-
if (shell === TerminalShellType.other) {
94-
shell = this.identifyShellFromSettings(telemetryProperties);
95-
}
50+
// Sort in order of priority and then identify the shell in terminal.
51+
const shellDetectors = this.shellDetectors.slice();
52+
shellDetectors.sort((a, b) => a.priority < b.priority ? 1 : 0);
9653

97-
// Step 3. Determine shell based on user environment.
98-
if (shell === TerminalShellType.other) {
99-
shell = this.identifyShellFromUserEnv(telemetryProperties);
54+
for (const detector of shellDetectors) {
55+
shell = detector.identify(telemetryProperties, terminal);
56+
if (shell) {
57+
break;
58+
}
10059
}
10160

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

10867
// If we could not identify the shell, use the defaults.
109-
return shell === TerminalShellType.other ? (defaultOSShells[this.platform.osType] || TerminalShellType.other) : shell;
110-
}
111-
public getTerminalShellPath(): string | undefined {
112-
const shellConfig = this.workspace.getConfiguration('terminal.integrated.shell');
113-
let osSection = '';
114-
switch (this.platform.osType) {
115-
case OSType.Windows: {
116-
osSection = 'windows';
117-
break;
118-
}
119-
case OSType.OSX: {
120-
osSection = 'osx';
121-
break;
122-
}
123-
case OSType.Linux: {
124-
osSection = 'linux';
125-
break;
126-
}
127-
default: {
128-
return '';
129-
}
130-
}
131-
return shellConfig.get<string>(osSection)!;
132-
}
133-
public getDefaultPlatformShell(): string {
134-
return getDefaultShell(this.platform, this.currentProcess);
135-
}
136-
public identifyShellByTerminalName(name: string, telemetryProperties: ShellIdentificationTelemetry): TerminalShellType {
137-
const shell = Array.from(detectableShells.keys())
138-
.reduce((matchedShell, shellToDetect) => {
139-
if (matchedShell === TerminalShellType.other && detectableShells.get(shellToDetect)!.test(name)) {
140-
return shellToDetect;
141-
}
142-
return matchedShell;
143-
}, TerminalShellType.other);
144-
traceVerbose(`Terminal name '${name}' identified as shell '${shell}'`);
145-
telemetryProperties.shellIdentificationSource = shell === TerminalShellType.other ? telemetryProperties.shellIdentificationSource : 'terminalName';
146-
return shell;
147-
}
148-
public identifyShellFromSettings(telemetryProperties: ShellIdentificationTelemetry): TerminalShellType {
149-
const shellPath = this.getTerminalShellPath();
150-
telemetryProperties.hasCustomShell = !!shellPath;
151-
const shell = shellPath ? this.identifyShellFromShellPath(shellPath) : TerminalShellType.other;
152-
153-
if (shell !== TerminalShellType.other) {
154-
telemetryProperties.shellIdentificationSource = 'environment';
155-
}
156-
telemetryProperties.shellIdentificationSource = 'settings';
157-
traceVerbose(`Shell path from user settings '${shellPath}'`);
158-
return shell;
159-
}
160-
161-
public identifyShellFromUserEnv(telemetryProperties: ShellIdentificationTelemetry): TerminalShellType {
162-
const shellPath = this.getDefaultPlatformShell();
163-
telemetryProperties.hasShellInEnv = !!shellPath;
164-
const shell = this.identifyShellFromShellPath(shellPath);
165-
166-
if (shell !== TerminalShellType.other) {
167-
telemetryProperties.shellIdentificationSource = 'environment';
68+
if (shell === undefined || shell === TerminalShellType.other) {
69+
shell = defaultOSShells[this.platform.osType];
16870
}
169-
traceVerbose(`Shell path from user env '${shellPath}'`);
17071
return shell;
17172
}
172-
public identifyShellFromShellPath(shellPath: string): TerminalShellType {
173-
const shell = Array.from(detectableShells.keys())
174-
.reduce((matchedShell, shellToDetect) => {
175-
if (matchedShell === TerminalShellType.other && detectableShells.get(shellToDetect)!.test(shellPath)) {
176-
return shellToDetect;
177-
}
178-
return matchedShell;
179-
}, TerminalShellType.other);
180-
181-
traceVerbose(`Shell path '${shellPath}'`);
182-
traceVerbose(`Shell path identified as shell '${shell}'`);
183-
return shell;
184-
}
185-
}
186-
187-
/*
188-
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
189-
This is only a fall back to identify the default shell used by VSC.
190-
On Windows, determine the default shell.
191-
On others, default to bash.
192-
*/
193-
function getDefaultShell(platform: IPlatformService, currentProcess: ICurrentProcess): string {
194-
if (platform.osType === OSType.Windows) {
195-
return getTerminalDefaultShellWindows(platform, currentProcess);
196-
}
197-
198-
return currentProcess.env.SHELL && currentProcess.env.SHELL !== '/bin/false' ? currentProcess.env.SHELL : '/bin/bash';
199-
}
200-
function getTerminalDefaultShellWindows(platform: IPlatformService, currentProcess: ICurrentProcess): string {
201-
const isAtLeastWindows10 = parseFloat(platform.osRelease) >= 10;
202-
const is32ProcessOn64Windows = currentProcess.env.hasOwnProperty('PROCESSOR_ARCHITEW6432');
203-
const powerShellPath = `${currentProcess.env.windir}\\${is32ProcessOn64Windows ? 'Sysnative' : 'System32'}\\WindowsPowerShell\\v1.0\\powershell.exe`;
204-
return isAtLeastWindows10 ? powerShellPath : getWindowsShell(currentProcess);
205-
}
206-
207-
function getWindowsShell(currentProcess: ICurrentProcess): string {
208-
return currentProcess.env.comspec || 'cmd.exe';
20973
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { injectable, unmanaged } from 'inversify';
7+
import { Terminal } from 'vscode';
8+
import { traceVerbose } from '../../logger';
9+
import { IShellDetector, ShellIdentificationTelemetry, TerminalShellType } from '../types';
10+
11+
// tslint:disable: max-classes-per-file
12+
13+
/*
14+
When identifying the shell use the following algorithm:
15+
* 1. Identify shell based on the name of the terminal (if there is one already opened and used).
16+
* 2. Identify shell based on the settings in VSC.
17+
* 3. Identify shell based on users environment variables.
18+
* 4. Use default shells (bash for mac and linux, cmd for windows).
19+
*/
20+
21+
// Types of shells can be found here:
22+
// 1. https://wiki.ubuntu.com/ChangingShells
23+
const IS_GITBASH = /(gitbash.exe$)/i;
24+
const IS_BASH = /(bash.exe$|bash$)/i;
25+
const IS_WSL = /(wsl.exe$)/i;
26+
const IS_ZSH = /(zsh$)/i;
27+
const IS_KSH = /(ksh$)/i;
28+
const IS_COMMAND = /(cmd.exe$|cmd$)/i;
29+
const IS_POWERSHELL = /(powershell.exe$|powershell$)/i;
30+
const IS_POWERSHELL_CORE = /(pwsh.exe$|pwsh$)/i;
31+
const IS_FISH = /(fish$)/i;
32+
const IS_CSHELL = /(csh$)/i;
33+
const IS_TCSHELL = /(tcsh$)/i;
34+
const IS_XONSH = /(xonsh$)/i;
35+
36+
const detectableShells = new Map<TerminalShellType, RegExp>();
37+
detectableShells.set(TerminalShellType.powershell, IS_POWERSHELL);
38+
detectableShells.set(TerminalShellType.gitbash, IS_GITBASH);
39+
detectableShells.set(TerminalShellType.bash, IS_BASH);
40+
detectableShells.set(TerminalShellType.wsl, IS_WSL);
41+
detectableShells.set(TerminalShellType.zsh, IS_ZSH);
42+
detectableShells.set(TerminalShellType.ksh, IS_KSH);
43+
detectableShells.set(TerminalShellType.commandPrompt, IS_COMMAND);
44+
detectableShells.set(TerminalShellType.fish, IS_FISH);
45+
detectableShells.set(TerminalShellType.tcshell, IS_TCSHELL);
46+
detectableShells.set(TerminalShellType.cshell, IS_CSHELL);
47+
detectableShells.set(TerminalShellType.powershellCore, IS_POWERSHELL_CORE);
48+
detectableShells.set(TerminalShellType.xonsh, IS_XONSH);
49+
50+
@injectable()
51+
export abstract class BaseShellDetector implements IShellDetector {
52+
constructor(@unmanaged() public readonly priority: number) { }
53+
public abstract identify(telemetryProperties: ShellIdentificationTelemetry, terminal?: Terminal): TerminalShellType | undefined;
54+
public identifyShellFromShellPath(shellPath: string): TerminalShellType {
55+
const shell = Array.from(detectableShells.keys())
56+
.reduce((matchedShell, shellToDetect) => {
57+
if (matchedShell === TerminalShellType.other) {
58+
const pat = detectableShells.get(shellToDetect);
59+
if (pat && pat.test(shellPath)) {
60+
return shellToDetect;
61+
}
62+
}
63+
return matchedShell;
64+
}, TerminalShellType.other);
65+
66+
traceVerbose(`Shell path '${shellPath}'`);
67+
traceVerbose(`Shell path identified as shell '${shell}'`);
68+
return shell;
69+
}
70+
}

0 commit comments

Comments
 (0)