From 732d4ff89e8ed9fa872d61ac31cdd7f84a7663a1 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Mon, 11 Jan 2021 15:38:03 -0800 Subject: [PATCH] Make PowerShell 7 default if available and show in choose shell menu (#112768) * make PowerShell 7 default if available and show in choose shell menu * misc feedback * better handle ARM and use pfs everywhere also update pfs to handle AppExecLinks * fix test * move to async * add logging * powershell global tool is in the image apparently * have path test be the same * try/catch the readlink * await exists * fix test * check what arch node is * fix indexes * address daniel's feedback * have getProgramFilesPath return null instead --- src/vs/base/node/pfs.ts | 24 +- src/vs/base/node/powershell.ts | 316 ++++++++++++++++++ src/vs/base/node/shell.ts | 45 ++- src/vs/base/test/node/powershell.test.ts | 67 ++++ src/vs/code/node/shellEnv.ts | 4 +- .../api/node/extHostTerminalService.ts | 12 +- .../browser/terminal.web.contribution.ts | 4 +- .../terminal/common/terminalConfiguration.ts | 31 +- .../electron-browser/terminal.contribution.ts | 3 +- .../terminalInstanceService.ts | 4 +- .../contrib/terminal/node/terminal.ts | 39 ++- 11 files changed, 500 insertions(+), 49 deletions(-) create mode 100644 src/vs/base/node/powershell.ts create mode 100644 src/vs/base/test/node/powershell.test.ts diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index fa07dcb9b7e..ddf8a804965 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -247,6 +247,10 @@ export function renameIgnoreError(oldPath: string, newPath: string): Promise fs.rename(oldPath, newPath, () => resolve())); } +export function readlink(path: string): Promise { + return promisify(fs.readlink)(path); +} + export function unlink(path: string): Promise { return promisify(fs.unlink)(path); } @@ -422,7 +426,15 @@ export async function dirExists(path: string): Promise { return fileStat.isDirectory(); } catch (error) { - return false; + // This catch will be called on some symbolic links on Windows (AppExecLink for example). + // So we try our best to see if it's a Directory. + try { + const fileStat = await stat(await readlink(path)); + + return fileStat.isDirectory(); + } catch { + return false; + } } } @@ -432,7 +444,15 @@ export async function fileExists(path: string): Promise { return fileStat.isFile(); } catch (error) { - return false; + // This catch will be called on some symbolic links on Windows (AppExecLink for example). + // So we try our best to see if it's a File. + try { + const fileStat = await stat(await readlink(path)); + + return fileStat.isFile(); + } catch { + return false; + } } } diff --git a/src/vs/base/node/powershell.ts b/src/vs/base/node/powershell.ts new file mode 100644 index 00000000000..3f50271ecf3 --- /dev/null +++ b/src/vs/base/node/powershell.ts @@ -0,0 +1,316 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as pfs from 'vs/base/node/pfs'; +import * as os from 'os'; +import * as path from 'vs/base/common/path'; +import { env } from 'vs/base/common/process'; + +const WindowsPowerShell64BitLabel = 'Windows PowerShell'; +const WindowsPowerShell32BitLabel = 'Windows PowerShell (x86)'; + +// This is required, since parseInt("7-preview") will return 7. +const IntRegex: RegExp = /^\d+$/; + +const PwshMsixRegex: RegExp = /^Microsoft.PowerShell_.*/; +const PwshPreviewMsixRegex: RegExp = /^Microsoft.PowerShellPreview_.*/; + +// The platform details descriptor for the platform we're on +const isProcess64Bit: boolean = process.arch === 'x64'; +const isOS64Bit: boolean = isProcess64Bit || env.hasOwnProperty('PROCESSOR_ARCHITEW6432'); + +export interface IPowerShellExeDetails { + readonly displayName: string; + readonly exePath: string; +} + +export interface IPossiblePowerShellExe extends IPowerShellExeDetails { + exists(): Promise; +} + +class PossiblePowerShellExe implements IPossiblePowerShellExe { + constructor( + public readonly exePath: string, + public readonly displayName: string, + private knownToExist?: boolean) { } + + public async exists(): Promise { + if (this.knownToExist === undefined) { + this.knownToExist = await pfs.fileExists(this.exePath); + } + return this.knownToExist; + } +} + +function getProgramFilesPath( + { useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): string | null { + + if (!useAlternateBitness) { + // Just use the native system bitness + return env.ProgramFiles || null; + } + + // We might be a 64-bit process looking for 32-bit program files + if (isProcess64Bit) { + return env['ProgramFiles(x86)'] || null; + } + + // We might be a 32-bit process looking for 64-bit program files + if (isOS64Bit) { + return env.ProgramW6432 || null; + } + + // We're a 32-bit process on 32-bit Windows, there is no other Program Files dir + return null; +} + +function getSystem32Path({ useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): string { + const windir: string = env.windir!; + + if (!useAlternateBitness) { + // Just use the native system bitness + return path.join(windir, 'System32'); + } + + // We might be a 64-bit process looking for 32-bit system32 + if (isProcess64Bit) { + return path.join(windir, 'SysWOW64'); + } + + // We might be a 32-bit process looking for 64-bit system32 + if (isOS64Bit) { + return path.join(windir, 'Sysnative'); + } + + // We're on a 32-bit Windows, so no alternate bitness + return path.join(windir, 'System32'); +} + +async function findPSCoreWindowsInstallation( + { useAlternateBitness = false, findPreview = false }: + { useAlternateBitness?: boolean; findPreview?: boolean } = {}): Promise { + + const programFilesPath = getProgramFilesPath({ useAlternateBitness }); + if (!programFilesPath) { + return null; + } + + const powerShellInstallBaseDir = path.join(programFilesPath, 'PowerShell'); + + // Ensure the base directory exists + if (!await pfs.dirExists(powerShellInstallBaseDir)) { + return null; + } + + let highestSeenVersion: number = -1; + let pwshExePath: string | null = null; + for (const item of await pfs.readdir(powerShellInstallBaseDir)) { + + let currentVersion: number = -1; + if (findPreview) { + // We are looking for something like "7-preview" + + // Preview dirs all have dashes in them + const dashIndex = item.indexOf('-'); + if (dashIndex < 0) { + continue; + } + + // Verify that the part before the dash is an integer + // and that the part after the dash is "preview" + const intPart: string = item.substring(0, dashIndex); + if (!IntRegex.test(intPart) || item.substring(dashIndex + 1) !== 'preview') { + continue; + } + + currentVersion = parseInt(intPart, 10); + } else { + // Search for a directory like "6" or "7" + if (!IntRegex.test(item)) { + continue; + } + + currentVersion = parseInt(item, 10); + } + + // Ensure we haven't already seen a higher version + if (currentVersion <= highestSeenVersion) { + continue; + } + + // Now look for the file + const exePath = path.join(powerShellInstallBaseDir, item, 'pwsh.exe'); + if (!await pfs.fileExists(exePath)) { + continue; + } + + pwshExePath = exePath; + highestSeenVersion = currentVersion; + } + + if (!pwshExePath) { + return null; + } + + const bitness: string = programFilesPath.includes('x86') ? ' (x86)' : ''; + const preview: string = findPreview ? ' Preview' : ''; + + return new PossiblePowerShellExe(pwshExePath, `PowerShell${preview}${bitness}`, true); +} + +async function findPSCoreMsix({ findPreview }: { findPreview?: boolean } = {}): Promise { + // We can't proceed if there's no LOCALAPPDATA path + if (!env.LOCALAPPDATA) { + return null; + } + + // Find the base directory for MSIX application exe shortcuts + const msixAppDir = path.join(env.LOCALAPPDATA, 'Microsoft', 'WindowsApps'); + + if (!await pfs.dirExists(msixAppDir)) { + return null; + } + + // Define whether we're looking for the preview or the stable + const { pwshMsixDirRegex, pwshMsixName } = findPreview + ? { pwshMsixDirRegex: PwshPreviewMsixRegex, pwshMsixName: 'PowerShell Preview (Store)' } + : { pwshMsixDirRegex: PwshMsixRegex, pwshMsixName: 'PowerShell (Store)' }; + + // We should find only one such application, so return on the first one + for (const subdir of await pfs.readdir(msixAppDir)) { + if (pwshMsixDirRegex.test(subdir)) { + const pwshMsixPath = path.join(msixAppDir, subdir, 'pwsh.exe'); + return new PossiblePowerShellExe(pwshMsixPath, pwshMsixName); + } + } + + // If we find nothing, return null + return null; +} + +function findPSCoreDotnetGlobalTool(): IPossiblePowerShellExe { + const dotnetGlobalToolExePath: string = path.join(os.homedir(), '.dotnet', 'tools', 'pwsh.exe'); + + return new PossiblePowerShellExe(dotnetGlobalToolExePath, '.NET Core PowerShell Global Tool'); +} + +function findWinPS({ useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): IPossiblePowerShellExe | null { + + // x86 and ARM only have one WinPS on them + if (!isOS64Bit && useAlternateBitness) { + return null; + } + + const systemFolderPath = getSystem32Path({ useAlternateBitness }); + + const winPSPath = path.join(systemFolderPath, 'WindowsPowerShell', 'v1.0', 'powershell.exe'); + + let displayName: string; + if (isProcess64Bit) { + displayName = useAlternateBitness + ? WindowsPowerShell32BitLabel + : WindowsPowerShell64BitLabel; + } else if (isOS64Bit) { + displayName = useAlternateBitness + ? WindowsPowerShell64BitLabel + : WindowsPowerShell32BitLabel; + } else { + // NOTE: ARM Windows devices also have Windows PowerShell x86 on them. There is no + // "ARM Windows PowerShell". + displayName = WindowsPowerShell32BitLabel; + } + + return new PossiblePowerShellExe(winPSPath, displayName, true); +} + +/** + * Iterates through all the possible well-known PowerShell installations on a machine. + * Returned values may not exist, but come with an .exists property + * which will check whether the executable exists. + */ +async function* enumerateDefaultPowerShellInstallations(): AsyncIterable { + // Find PSCore stable first + let pwshExe = await findPSCoreWindowsInstallation(); + if (pwshExe) { + yield pwshExe; + } + + // Windows may have a 32-bit pwsh.exe + pwshExe = await findPSCoreWindowsInstallation({ useAlternateBitness: true }); + if (pwshExe) { + yield pwshExe; + } + + // Also look for the MSIX/UWP installation + pwshExe = await findPSCoreMsix(); + if (pwshExe) { + yield pwshExe; + } + + // Look for the .NET global tool + // Some older versions of PowerShell have a bug in this where startup will fail, + // but this is fixed in newer versions + pwshExe = findPSCoreDotnetGlobalTool(); + if (pwshExe) { + yield pwshExe; + } + + // Look for PSCore preview + pwshExe = await findPSCoreWindowsInstallation({ findPreview: true }); + if (pwshExe) { + yield pwshExe; + } + + // Find a preview MSIX + pwshExe = await findPSCoreMsix({ findPreview: true }); + if (pwshExe) { + yield pwshExe; + } + + // Look for pwsh-preview with the opposite bitness + pwshExe = await findPSCoreWindowsInstallation({ useAlternateBitness: true, findPreview: true }); + if (pwshExe) { + yield pwshExe; + } + + // Finally, get Windows PowerShell + + // Get the natural Windows PowerShell for the process bitness + pwshExe = findWinPS(); + if (pwshExe) { + yield pwshExe; + } + + // Get the alternate bitness Windows PowerShell + pwshExe = findWinPS({ useAlternateBitness: true }); + if (pwshExe) { + yield pwshExe; + } +} + +/** + * Iterates through PowerShell installations on the machine according + * to configuration passed in through the constructor. + * PowerShell items returned by this object are verified + * to exist on the filesystem. + */ +export async function* enumeratePowerShellInstallations(): AsyncIterable { + // Get the default PowerShell installations first + for await (const defaultPwsh of enumerateDefaultPowerShellInstallations()) { + if (await defaultPwsh.exists()) { + yield defaultPwsh; + } + } +} + +/** +* Returns the first available PowerShell executable found in the search order. +*/ +export async function getFirstAvailablePowerShellInstallation(): Promise { + for await (const pwsh of enumeratePowerShellInstallations()) { + return pwsh; + } + return null; +} diff --git a/src/vs/base/node/shell.ts b/src/vs/base/node/shell.ts index 91ed33a10c3..cacc2da4db2 100644 --- a/src/vs/base/node/shell.ts +++ b/src/vs/base/node/shell.ts @@ -5,6 +5,7 @@ import * as os from 'os'; import * as platform from 'vs/base/common/platform'; +import { getFirstAvailablePowerShellInstallation } from 'vs/base/node/powershell'; import * as processes from 'vs/base/node/processes'; /** @@ -12,23 +13,37 @@ import * as processes from 'vs/base/node/processes'; * shell that the terminal uses by default. * @param p The platform to detect the shell of. */ -export function getSystemShell(p: platform.Platform, env = process.env as platform.IProcessEnvironment): string { +export async function getSystemShell(p: platform.Platform, env = process.env as platform.IProcessEnvironment): Promise { if (p === platform.Platform.Windows) { if (platform.isWindows) { - return getSystemShellWindows(env); + return getSystemShellWindows(); } // Don't detect Windows shell when not on Windows return processes.getWindowsShell(env); } + + return getSystemShellUnixLike(p, env); +} + +export function getSystemShellSync(p: platform.Platform, env = process.env as platform.IProcessEnvironment): string { + if (p === platform.Platform.Windows) { + if (platform.isWindows) { + return getSystemShellWindowsSync(env); + } + // Don't detect Windows shell when not on Windows + return processes.getWindowsShell(env); + } + + return getSystemShellUnixLike(p, env); +} + +let _TERMINAL_DEFAULT_SHELL_UNIX_LIKE: string | null = null; +function getSystemShellUnixLike(p: platform.Platform, env: platform.IProcessEnvironment): string { // Only use $SHELL for the current OS if (platform.isLinux && p === platform.Platform.Mac || platform.isMacintosh && p === platform.Platform.Linux) { return '/bin/bash'; } - return getSystemShellUnixLike(env); -} -let _TERMINAL_DEFAULT_SHELL_UNIX_LIKE: string | null = null; -function getSystemShellUnixLike(env: platform.IProcessEnvironment): string { if (!_TERMINAL_DEFAULT_SHELL_UNIX_LIKE) { let unixLikeTerminal: string; if (platform.isWindows) { @@ -59,12 +74,20 @@ function getSystemShellUnixLike(env: platform.IProcessEnvironment): string { } let _TERMINAL_DEFAULT_SHELL_WINDOWS: string | null = null; -function getSystemShellWindows(env: platform.IProcessEnvironment): string { +async function getSystemShellWindows(): Promise { if (!_TERMINAL_DEFAULT_SHELL_WINDOWS) { - const isAtLeastWindows10 = platform.isWindows && parseFloat(os.release()) >= 10; - const is32ProcessOn64Windows = env.hasOwnProperty('PROCESSOR_ARCHITEW6432'); - const powerShellPath = `${env['windir']}\\${is32ProcessOn64Windows ? 'Sysnative' : 'System32'}\\WindowsPowerShell\\v1.0\\powershell.exe`; - _TERMINAL_DEFAULT_SHELL_WINDOWS = isAtLeastWindows10 ? powerShellPath : processes.getWindowsShell(env); + _TERMINAL_DEFAULT_SHELL_WINDOWS = (await getFirstAvailablePowerShellInstallation())!.exePath; } return _TERMINAL_DEFAULT_SHELL_WINDOWS; } + +function getSystemShellWindowsSync(env: platform.IProcessEnvironment): string { + if (_TERMINAL_DEFAULT_SHELL_WINDOWS) { + return _TERMINAL_DEFAULT_SHELL_WINDOWS; + } + + const isAtLeastWindows10 = platform.isWindows && parseFloat(os.release()) >= 10; + const is32ProcessOn64Windows = env.hasOwnProperty('PROCESSOR_ARCHITEW6432'); + const powerShellPath = `${env['windir']}\\${is32ProcessOn64Windows ? 'Sysnative' : 'System32'}\\WindowsPowerShell\\v1.0\\powershell.exe`; + return isAtLeastWindows10 ? powerShellPath : processes.getWindowsShell(env); +} diff --git a/src/vs/base/test/node/powershell.test.ts b/src/vs/base/test/node/powershell.test.ts new file mode 100644 index 00000000000..fa490742401 --- /dev/null +++ b/src/vs/base/test/node/powershell.test.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as assert from 'assert'; +import * as platform from 'vs/base/common/platform'; +import * as fs from 'fs'; +import { enumeratePowerShellInstallations, getFirstAvailablePowerShellInstallation, IPowerShellExeDetails } from 'vs/base/node/powershell'; + +function checkPath(exePath: string) { + // Check to see if the path exists + let pathCheckResult = false; + try { + const stat = fs.statSync(exePath); + pathCheckResult = stat.isFile(); + } catch { + // fs.exists throws on Windows with SymbolicLinks so we + // also use lstat to try and see if the file exists. + try { + pathCheckResult = fs.statSync(fs.readlinkSync(exePath)).isFile(); + } catch { + + } + } + + assert.strictEqual(pathCheckResult, true); +} + +if (platform.isWindows) { + suite('PowerShell finder', () => { + + test('Can find first available PowerShell', async () => { + const pwshExe = await getFirstAvailablePowerShellInstallation(); + const exePath = pwshExe?.exePath; + assert.notStrictEqual(exePath, null); + assert.notStrictEqual(pwshExe?.displayName, null); + + checkPath(exePath!); + }); + + test('Can enumerate PowerShells', async () => { + const pwshs = new Array(); + for await (const p of enumeratePowerShellInstallations()) { + pwshs.push(p); + } + + // In Azure DevOps and GitHub Actions there should be an extra PowerShell since PowerShell 7 comes pre-installed + const minNumberOfPowerShells = process.env.TF_BUILD || process.env.CI ? 3 : 2; + + assert.strictEqual(pwshs.length >= minNumberOfPowerShells, true, 'Found these PowerShells:\n' + pwshs.map(p => `${p.displayName}: ${p.exePath}`).join('\n')); + + for (const pwsh of pwshs) { + checkPath(pwsh.exePath); + } + + const lastIndex = pwshs.length - 1; + checkPath(pwshs[lastIndex].exePath); + assert.strictEqual(pwshs[lastIndex].displayName, 'Windows PowerShell (x86)'); + + if (process.arch === 'x64') { + const secondToLastIndex = pwshs.length - 2; + checkPath(pwshs[secondToLastIndex].exePath); + assert.strictEqual(pwshs[secondToLastIndex].displayName, 'Windows PowerShell'); + } + }); + }); +} diff --git a/src/vs/code/node/shellEnv.ts b/src/vs/code/node/shellEnv.ts index 2ea93f9c5af..9454f1d4730 100644 --- a/src/vs/code/node/shellEnv.ts +++ b/src/vs/code/node/shellEnv.ts @@ -59,7 +59,7 @@ export async function resolveShellEnv(logService: ILogService, args: NativeParse let unixShellEnvPromise: Promise | undefined = undefined; async function doResolveUnixShellEnv(logService: ILogService): Promise { - const promise = new Promise((resolve, reject) => { + const promise = new Promise(async (resolve, reject) => { const runAsNode = process.env['ELECTRON_RUN_AS_NODE']; logService.trace('getUnixShellEnvironment#runAsNode', runAsNode); @@ -79,7 +79,7 @@ async function doResolveUnixShellEnv(logService: ILogService): Promise this._defaultShell = s); + this._updateLastActiveWorkspace(); this._updateVariableResolver(); this._registerListeners(); @@ -78,10 +85,11 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { .inspect(key.substr(key.lastIndexOf('.') + 1)); return this._apiInspectConfigToPlain(setting); }; + return terminalEnvironment.getDefaultShell( fetchSetting, this._isWorkspaceShellAllowed, - getSystemShell(platform.platform), + this._defaultShell ?? getSystemShellSync(platform.platform), process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'), process.env.windir, terminalEnvironment.createVariableResolver(this._lastActiveWorkspace, this._variableResolver), diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.web.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.web.contribution.ts index e2c091ae9cd..6773ae9e4aa 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.web.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.web.contribution.ts @@ -8,12 +8,12 @@ import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/co import { Registry } from 'vs/platform/registry/common/platform'; import { TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; -import { getTerminalShellConfiguration } from 'vs/workbench/contrib/terminal/common/terminalConfiguration'; +import { getNoDefaultTerminalShellConfiguration } from 'vs/workbench/contrib/terminal/common/terminalConfiguration'; // Desktop shell configuration are registered in electron-browser as their default values rely // on process.env const configurationRegistry = Registry.as(Extensions.Configuration); -configurationRegistry.registerConfiguration(getTerminalShellConfiguration()); +configurationRegistry.registerConfiguration(getNoDefaultTerminalShellConfiguration()); // Register standard external terminal keybinding as integrated terminal when in web as the // external terminal is not available diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 759e7c2eb96..d2fe5d11fec 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -401,7 +401,7 @@ export const terminalConfiguration: IConfigurationNode = { } }; -export function getTerminalShellConfiguration(getSystemShell?: (p: Platform) => string): IConfigurationNode { +function getTerminalShellConfigurationStub(linux: string, osx: string, windows: string): IConfigurationNode { return { id: 'terminal', order: 100, @@ -409,29 +409,34 @@ export function getTerminalShellConfiguration(getSystemShell?: (p: Platform) => type: 'object', properties: { 'terminal.integrated.shell.linux': { - markdownDescription: - getSystemShell - ? localize('terminal.integrated.shell.linux', "The path of the shell that the terminal uses on Linux (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", getSystemShell(Platform.Linux)) - : localize('terminal.integrated.shell.linux.noDefault', "The path of the shell that the terminal uses on Linux. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + markdownDescription: linux, type: ['string', 'null'], default: null }, 'terminal.integrated.shell.osx': { - markdownDescription: - getSystemShell - ? localize('terminal.integrated.shell.osx', "The path of the shell that the terminal uses on macOS (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", getSystemShell(Platform.Mac)) - : localize('terminal.integrated.shell.osx.noDefault', "The path of the shell that the terminal uses on macOS. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + markdownDescription: osx, type: ['string', 'null'], default: null }, 'terminal.integrated.shell.windows': { - markdownDescription: - getSystemShell - ? localize('terminal.integrated.shell.windows', "The path of the shell that the terminal uses on Windows (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", getSystemShell(Platform.Windows)) - : localize('terminal.integrated.shell.windows.noDefault', "The path of the shell that the terminal uses on Windows. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + markdownDescription: windows, type: ['string', 'null'], default: null } } }; } + +export function getNoDefaultTerminalShellConfiguration(): IConfigurationNode { + return getTerminalShellConfigurationStub( + localize('terminal.integrated.shell.linux.noDefault', "The path of the shell that the terminal uses on Linux. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + localize('terminal.integrated.shell.osx.noDefault', "The path of the shell that the terminal uses on macOS. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + localize('terminal.integrated.shell.windows.noDefault', "The path of the shell that the terminal uses on Windows. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).")); +} + +export async function getTerminalShellConfiguration(getSystemShell: (p: Platform) => Promise): Promise { + return getTerminalShellConfigurationStub( + localize('terminal.integrated.shell.linux', "The path of the shell that the terminal uses on Linux (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", await getSystemShell(Platform.Linux)), + localize('terminal.integrated.shell.osx', "The path of the shell that the terminal uses on macOS (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", await getSystemShell(Platform.Mac)), + localize('terminal.integrated.shell.windows', "The path of the shell that the terminal uses on Windows (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", await getSystemShell(Platform.Windows))); +} diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts index 0ea7c35c273..0c11faaf0b3 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts @@ -24,4 +24,5 @@ workbenchRegistry.registerWorkbenchContribution(TerminalNativeContribution, Life // Register configurations const configurationRegistry = Registry.as(Extensions.Configuration); -configurationRegistry.registerConfiguration(getTerminalShellConfiguration(getSystemShell)); + +getTerminalShellConfiguration(getSystemShell).then(config => configurationRegistry.registerConfiguration(config)); diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts index 03012b1ca05..2692cf9e171 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts @@ -82,7 +82,7 @@ export class TerminalInstanceService implements ITerminalInstanceService { return this._storageService.getBoolean(IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, StorageScope.WORKSPACE, false); } - public getDefaultShellAndArgs(useAutomationShell: boolean, platformOverride: Platform = platform): Promise<{ shell: string, args: string | string[] }> { + public async getDefaultShellAndArgs(useAutomationShell: boolean, platformOverride: Platform = platform): Promise<{ shell: string, args: string | string[] }> { const isWorkspaceShellAllowed = this._isWorkspaceShellAllowed(); const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(); let lastActiveWorkspace = activeWorkspaceRootUri ? this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri) : undefined; @@ -90,7 +90,7 @@ export class TerminalInstanceService implements ITerminalInstanceService { const shell = getDefaultShell( (key) => this._configurationService.inspect(key), isWorkspaceShellAllowed, - getSystemShell(platformOverride), + await getSystemShell(platformOverride), process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'), process.env.windir, createVariableResolver(lastActiveWorkspace, this._configurationResolverService), diff --git a/src/vs/workbench/contrib/terminal/node/terminal.ts b/src/vs/workbench/contrib/terminal/node/terminal.ts index f8281c244da..ff36c4e6331 100644 --- a/src/vs/workbench/contrib/terminal/node/terminal.ts +++ b/src/vs/workbench/contrib/terminal/node/terminal.ts @@ -5,10 +5,11 @@ import * as os from 'os'; import * as platform from 'vs/base/common/platform'; -import { readFile, fileExists, stat } from 'vs/base/node/pfs'; +import { readFile, fileExists, stat, lstat } from 'vs/base/node/pfs'; import { LinuxDistro, IShellDefinition } from 'vs/workbench/contrib/terminal/common/terminal'; import { coalesce } from 'vs/base/common/arrays'; import { normalize, basename } from 'vs/base/common/path'; +import { enumeratePowerShellInstallations } from 'vs/base/node/powershell'; let detectedDistro = LinuxDistro.Unknown; if (platform.isLinux) { @@ -58,8 +59,6 @@ async function detectAvailableWindowsShells(): Promise { const expectedLocations: { [key: string]: string[] } = { 'Command Prompt': [`${system32Path}\\cmd.exe`], - 'Windows PowerShell': [`${system32Path}\\WindowsPowerShell\\v1.0\\powershell.exe`], - 'PowerShell': [await getShellPathFromRegistry('pwsh')], 'WSL Bash': [`${system32Path}\\${useWSLexe ? 'wsl.exe' : 'bash.exe'}`], 'Git Bash': [ `${process.env['ProgramW6432']}\\Git\\bin\\bash.exe`, @@ -74,6 +73,12 @@ async function detectAvailableWindowsShells(): Promise { // `${process.env['HOMEDRIVE']}\\cygwin\\bin\\bash.exe` // ] }; + + // Add all of the different kinds of PowerShells + for await (const pwshExe of enumeratePowerShellInstallations()) { + expectedLocations[pwshExe.displayName] = [pwshExe.exePath]; + } + const promises: Promise[] = []; Object.keys(expectedLocations).forEach(key => promises.push(validateShellPaths(key, expectedLocations[key]))); const shells = await Promise.all(promises); @@ -107,16 +112,22 @@ async function validateShellPaths(label: string, potentialPaths: string[]): Prom path: current }; } - } catch { /* noop */ } - return validateShellPaths(label, potentialPaths); -} - -async function getShellPathFromRegistry(shellName: string): Promise { - const Registry = await import('vscode-windows-registry'); - try { - const shellPath = Registry.GetStringRegKey('HKEY_LOCAL_MACHINE', `SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\${shellName}.exe`, ''); - return shellPath ? shellPath : ''; - } catch (error) { - return ''; + } catch (e) { + // Also try using lstat as some symbolic links on Windows + // throw 'permission denied' using 'stat' but don't throw + // using 'lstat' + try { + const result = await lstat(normalize(current)); + if (result.isFile() || result.isSymbolicLink()) { + return { + label, + path: current + }; + } + } + catch (e) { + // noop + } } + return validateShellPaths(label, potentialPaths); } -- GitLab