diff --git a/package.json b/package.json index 42bfa345ca..39b2076a5b 100644 --- a/package.json +++ b/package.json @@ -669,6 +669,11 @@ "markdownDescription": "**Deprecated:** Specifies whether you should be prompted to update your version of `PackageManagement` if it's under 1.4.6.", "markdownDeprecationMessage": "**Deprecated:** This prompt has been removed as it's no longer strictly necessary to upgrade the `PackageManagement` module." }, + "powershell.suppressAdditionalExeNotFoundWarning": { + "type": "boolean", + "default": false, + "markdownDescription": "Suppresses the warning message when any of `#powershell.powerShellAdditionalExePaths#` is not found." + }, "powershell.startAsLoginShell.osx": { "type": "boolean", "default": true, diff --git a/src/platform.ts b/src/platform.ts index 4a68ef35f5..b5a790c2e3 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -4,9 +4,10 @@ import * as os from "os"; import * as path from "path"; import * as process from "process"; +import vscode = require("vscode"); import { integer } from "vscode-languageserver-protocol"; import { ILogger } from "./logging"; -import { PowerShellAdditionalExePathSettings } from "./settings"; +import { changeSetting, getSettings, PowerShellAdditionalExePathSettings } from "./settings"; // This uses require so we can rewire it in unit tests! // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-var-requires @@ -149,8 +150,16 @@ export class PowerShellExeFinder { for (const additionalPwsh of this.enumerateAdditionalPowerShellInstallations()) { if (await additionalPwsh.exists()) { yield additionalPwsh; - } else { - void this.logger.writeAndShowWarning(`Additional PowerShell '${additionalPwsh.displayName}' not found at '${additionalPwsh.exePath}'!`); + } else if (!additionalPwsh.suppressWarning) { + const message = `Additional PowerShell '${additionalPwsh.displayName}' not found at '${additionalPwsh.exePath}'!`; + this.logger.writeWarning(message); + + if (!getSettings().suppressAdditionalExeNotFoundWarning) { + const selection = await vscode.window.showWarningMessage(message, "Don't Show Again"); + if (selection !== undefined) { + await changeSetting("suppressAdditionalExeNotFoundWarning", true, true, this.logger); + } + } } } } @@ -223,9 +232,33 @@ export class PowerShellExeFinder { private *enumerateAdditionalPowerShellInstallations(): Iterable { for (const versionName in this.additionalPowerShellExes) { if (Object.prototype.hasOwnProperty.call(this.additionalPowerShellExes, versionName)) { - const exePath = this.additionalPowerShellExes[versionName]; - if (exePath) { - yield new PossiblePowerShellExe(exePath, versionName); + const exePath = utils.stripQuotePair(this.additionalPowerShellExes[versionName]); + if (!exePath) { + continue; + } + + // Always search for what the user gave us first + yield new PossiblePowerShellExe(exePath, versionName); + + // Also search for `pwsh[.exe]` and `powershell[.exe]` if missing + const args: [string, undefined, boolean, boolean] + // Must be a tuple type and is suppressing the warning + = [versionName, undefined, true, true]; + + // Handle Windows where '.exe' and 'powershell' are things + if (this.platformDetails.operatingSystem === OperatingSystem.Windows) { + if (!exePath.endsWith("pwsh.exe") && !exePath.endsWith("powershell.exe")) { + if (exePath.endsWith("pwsh") || exePath.endsWith("powershell")) { + // Add extension if that was missing + yield new PossiblePowerShellExe(exePath + ".exe", ...args); + } + // Also add full exe names (this isn't an else just in case + // the folder was named "pwsh" or "powershell") + yield new PossiblePowerShellExe(path.join(exePath, "pwsh.exe"), ...args); + yield new PossiblePowerShellExe(path.join(exePath, "powershell.exe"), ...args); + } + } else if (!exePath.endsWith("pwsh")) { // Always just 'pwsh' on non-Windows + yield new PossiblePowerShellExe(path.join(exePath, "pwsh"), ...args); } } } @@ -529,6 +562,7 @@ export function getWindowsSystemPowerShellPath(systemFolderName: string): string interface IPossiblePowerShellExe extends IPowerShellExeDetails { exists(): Promise; + readonly suppressWarning: boolean; } class PossiblePowerShellExe implements IPossiblePowerShellExe { @@ -536,7 +570,8 @@ class PossiblePowerShellExe implements IPossiblePowerShellExe { public readonly exePath: string, public readonly displayName: string, private knownToExist?: boolean, - public readonly supportsProperArguments = true) { } + public readonly supportsProperArguments = true, + public readonly suppressWarning = false) { } public async exists(): Promise { if (this.knownToExist === undefined) { diff --git a/src/settings.ts b/src/settings.ts index 923f83c6f8..3e200227dd 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -23,6 +23,7 @@ export class Settings extends PartialSettings { // This setting is no longer used but is here to assist in cleaning up the users settings. powerShellExePath = ""; promptToUpdatePowerShell = true; + suppressAdditionalExeNotFoundWarning = false; startAsLoginShell = new StartAsLoginShellSettings(); startAutomatically = true; enableProfileLoading = true; @@ -216,7 +217,8 @@ let hasPrompted = false; export let chosenWorkspace: vscode.WorkspaceFolder | undefined = undefined; export async function validateCwdSetting(logger: ILogger): Promise { - let cwd: string | undefined = vscode.workspace.getConfiguration(utils.PowerShellLanguageId).get("cwd"); + let cwd: string | undefined = utils.stripQuotePair( + vscode.workspace.getConfiguration(utils.PowerShellLanguageId).get("cwd")); // Only use the cwd setting if it exists. if (cwd !== undefined && await utils.checkIfDirectoryExists(cwd)) { diff --git a/src/utils.ts b/src/utils.ts index 2f8448494c..182612194e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -11,6 +11,20 @@ export function escapeSingleQuotes(p: string): string { return p.replace(new RegExp("'", "g"), "''"); } +export function stripQuotePair(p: string | undefined): string | undefined { + if (p === undefined) { + return p; + } + + // Remove matching surrounding quotes from p (without regex) + if (p.startsWith("'") && p.endsWith("'") + || p.startsWith("\"") && p.endsWith("\"")) { + return p.slice(1, -1); + } + + return p; +} + export function getPipePath(pipeName: string): string { if (os.platform() === "win32") { return "\\\\.\\pipe\\" + pipeName; diff --git a/test/core/platform.test.ts b/test/core/platform.test.ts index 0cdd4012d0..b5c082ec57 100644 --- a/test/core/platform.test.ts +++ b/test/core/platform.test.ts @@ -11,6 +11,7 @@ import * as sinon from "sinon"; import * as platform from "../../src/platform"; import * as fs from "fs"; // NOTE: Necessary for mock-fs. import * as vscode from "vscode"; +import { stripQuotePair } from "../../src/utils"; // We have to rewire the platform module so that mock-fs can be used, as it // overrides the fs module but not the vscode.workspace.fs module. @@ -34,7 +35,8 @@ async function fakeReadDirectory(targetPath: string | vscode.Uri): Promise; +let successAdditionalTestCases: ITestPlatformSuccessCase[]; if (process.platform === "win32") { const msixAppDir = path.join(process.env.LOCALAPPDATA!, "Microsoft", "WindowsApps"); @@ -443,8 +448,105 @@ if (process.platform === "win32") { }, }, ]; + + additionalPowerShellExes = { + "pwsh": "C:\\Users\\test\\pwsh\\pwsh.exe", + "pwsh-no-exe": "C:\\Users\\test\\pwsh\\pwsh", + "pwsh-folder": "C:\\Users\\test\\pwsh\\", + "pwsh-folder-no-slash": "C:\\Users\\test\\pwsh", + "pwsh-single-quotes": "'C:\\Users\\test\\pwsh\\pwsh.exe'", + "pwsh-double-quotes": "\"C:\\Users\\test\\pwsh\\pwsh.exe\"", + }; + + successAdditionalTestCases = [ + { + name: "Windows (Additional PowerShell Executables)", + platformDetails: { + operatingSystem: platform.OperatingSystem.Windows, + isOS64Bit: true, + isProcess64Bit: true, + }, + environmentVars: { + "USERPROFILE": "C:\\Users\\test", + }, + expectedPowerShellSequence: [ + { + exePath: "C:\\Users\\test\\pwsh\\pwsh.exe", + displayName: "pwsh", + supportsProperArguments: true + }, + { + exePath: "C:\\Users\\test\\pwsh\\pwsh", + displayName: "pwsh-no-exe", + supportsProperArguments: true + }, + { + exePath: "C:\\Users\\test\\pwsh\\pwsh.exe", + displayName: "pwsh-no-exe", + supportsProperArguments: true + }, + { + exePath: "C:\\Users\\test\\pwsh\\pwsh\\pwsh.exe", + displayName: "pwsh-no-exe", + supportsProperArguments: true + }, + { + exePath: "C:\\Users\\test\\pwsh\\pwsh\\powershell.exe", + displayName: "pwsh-no-exe", + supportsProperArguments: true + }, + { + exePath: "C:\\Users\\test\\pwsh\\", + displayName: "pwsh-folder", + supportsProperArguments: true + }, + { + exePath: "C:\\Users\\test\\pwsh\\pwsh.exe", + displayName: "pwsh-folder", + supportsProperArguments: true + }, + { + exePath: "C:\\Users\\test\\pwsh\\powershell.exe", + displayName: "pwsh-folder", + supportsProperArguments: true + }, + { + exePath: "C:\\Users\\test\\pwsh", + displayName: "pwsh-folder-no-slash", + supportsProperArguments: true + }, + { + exePath: "C:\\Users\\test\\pwsh.exe", + displayName: "pwsh-folder-no-slash", + supportsProperArguments: true + }, + { + exePath: "C:\\Users\\test\\pwsh\\pwsh.exe", + displayName: "pwsh-folder-no-slash", + supportsProperArguments: true + }, + { + exePath: "C:\\Users\\test\\pwsh\\powershell.exe", + displayName: "pwsh-folder-no-slash", + supportsProperArguments: true + }, + { + exePath: "C:\\Users\\test\\pwsh\\pwsh.exe", + displayName: "pwsh-single-quotes", + supportsProperArguments: true + }, + { + exePath: "C:\\Users\\test\\pwsh\\pwsh.exe", + displayName: "pwsh-double-quotes", + supportsProperArguments: true + }, + ], + filesystem: {}, + } + ]; } else { const pwshDailyDir = path.join(os.homedir(), ".powershell-daily"); + successTestCases = [ { name: "Linux (all installations)", @@ -642,6 +744,66 @@ if (process.platform === "win32") { }, }, ]; + + additionalPowerShellExes = { + "pwsh": "/home/bin/pwsh", + "pwsh-folder": "/home/bin/", + "pwsh-folder-no-slash": "/home/bin", + "pwsh-single-quotes": "'/home/bin/pwsh'", + "pwsh-double-quotes": "\"/home/bin/pwsh\"", + }; + + successAdditionalTestCases = [ + { // Also sufficient for macOS as the behavior is the same + name: "Linux (Additional PowerShell Executables)", + platformDetails: { + operatingSystem: platform.OperatingSystem.Linux, + isOS64Bit: true, + isProcess64Bit: true, + }, + environmentVars: { + "HOME": "/home/test", + }, + expectedPowerShellSequence: [ + { + exePath: "/home/bin/pwsh", + displayName: "pwsh", + supportsProperArguments: true + }, + { + exePath: "/home/bin/", + displayName: "pwsh-folder", + supportsProperArguments: true + }, + { + exePath: "/home/bin/pwsh", + displayName: "pwsh-folder", + supportsProperArguments: true + }, + { + exePath: "/home/bin", + displayName: "pwsh-folder-no-slash", + supportsProperArguments: true + }, + { + exePath: "/home/bin/pwsh", + displayName: "pwsh-folder-no-slash", + supportsProperArguments: true + }, + { + exePath: "/home/bin/pwsh", + displayName: "pwsh-single-quotes", + supportsProperArguments: true + }, + { + exePath: "/home/bin/pwsh", + displayName: "pwsh-double-quotes", + supportsProperArguments: true + }, + ], + filesystem: {}, + } + ]; } const errorTestCases: ITestPlatform[] = [ @@ -846,4 +1008,28 @@ describe("Platform module", function () { }); } }); + + describe("PowerShell executables from 'powerShellAdditionalExePaths' are found", function () { + afterEach(function () { + sinon.restore(); + mockFS.restore(); + }); + + for (const testPlatform of successAdditionalTestCases) { + it(`Guesses for ${testPlatform.name}`, function () { + setupTestEnvironment(testPlatform); + + const powerShellExeFinder = new platformMock.PowerShellExeFinder(testPlatform.platformDetails, additionalPowerShellExes); + + let i = 0; + for (const additionalPwsh of powerShellExeFinder.enumerateAdditionalPowerShellInstallations()) { + const expectedPowerShell = testPlatform.expectedPowerShellSequence[i]; + i++; + + assert.strictEqual(additionalPwsh.exePath, expectedPowerShell.exePath); + assert.strictEqual(additionalPwsh.displayName, expectedPowerShell.displayName); + } + }); + } + }); });