diff --git a/.vscode/launch.json b/.vscode/launch.json index f53cedf458..155db9a086 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -32,7 +32,13 @@ "stopOnEntry": false, "sourceMaps": true, "outFiles": ["${workspaceRoot}/out/test/**/*.js"], - "preLaunchTask": "Build" + "preLaunchTask": "Build", + "skipFiles": [ + "${workspaceFolder}/node_modules/**/*", + "${workspaceFolder}/lib/**/*", + "/private/var/folders/**/*", + "/**/*" + ] }, { "name": "Attach", diff --git a/package-lock.json b/package-lock.json index 51081f0d30..250eaa6423 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,12 +32,65 @@ } } }, + "@sinonjs/commons": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.6.0.tgz", + "integrity": "sha512-w4/WHG7C4WWFyE5geCieFJF6MZkbW4VAriol5KlmQXpAQdxvV0p26sqNZOW6Qyw6Y0l9K4g+cHvvczR2sEEpqg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", + "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", + "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.3.0", + "array-from": "^2.1.1", + "lodash": "^4.17.15" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "@types/mocha": { "version": "5.2.7", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", "dev": true }, + "@types/mock-fs": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.10.0.tgz", + "integrity": "sha512-FQ5alSzmHMmliqcL36JqIA4Yyn9jyJKvRSGV3mvPh108VFatX7naJDzSG4fnFQNZFq9dIx0Dzoe6ddflMB2Xkg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "10.11.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.11.7.tgz", @@ -65,6 +118,12 @@ "integrity": "sha512-1OzrNb4RuAzIT7wHSsgZRlMBlNsJl+do6UblR7JMW4oB7bbR+uBEYtUh7gEc/jM84GGilh68lSOokyM/zNUlBA==", "dev": true }, + "@types/sinon": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.5.0.tgz", + "integrity": "sha512-NyzhuSBy97B/zE58cDw4NyGvByQbAHNP9069KVSgnXt/sc0T6MFRh0InKAeBVHJWdSXG1S3+PxgVIgKo9mTHbw==", + "dev": true + }, "acorn": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", @@ -153,6 +212,12 @@ "sprintf-js": "~1.0.2" } }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -1253,6 +1318,12 @@ "verror": "1.10.0" } }, + "just-extend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", + "dev": true + }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -1278,6 +1349,12 @@ "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==", "dev": true }, + "lolex": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", + "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==", + "dev": true + }, "lru-cache": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", @@ -1422,6 +1499,12 @@ "lodash": "^4.16.4" } }, + "mock-fs": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-4.10.2.tgz", + "integrity": "sha512-ewPQ83O4U8/Gd8I15WoB6vgTTmq5khxBskUWCRvswUqjCfOOTREmxllztQOm+PXMWUxATry+VBWXQJloAyxtbQ==", + "dev": true + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1440,6 +1523,19 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "nise": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.2.tgz", + "integrity": "sha512-/6RhOUlicRCbE9s+94qCUsyE+pKlVJ5AhIv+jEE7ESKwnbXqulKZ1FYU+XAtHHWE9TinYvAxDUJAb912PwPoWA==", + "dev": true, + "requires": { + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^4.1.0", + "path-to-regexp": "^1.7.0" + } + }, "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", @@ -1570,6 +1666,23 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -1836,6 +1949,32 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, + "sinon": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz", + "integrity": "sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.3", + "diff": "^3.5.0", + "lolex": "^4.2.0", + "nise": "^1.5.2", + "supports-color": "^5.5.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "slice-ansi": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", @@ -2065,6 +2204,12 @@ "prelude-ls": "~1.1.2" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "typed-rest-client": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.2.0.tgz", diff --git a/package.json b/package.json index 4da5a73421..c142503f58 100644 --- a/package.json +++ b/package.json @@ -48,14 +48,18 @@ }, "devDependencies": { "@types/mocha": "~5.2.7", + "@types/mock-fs": "~4.10.0", "@types/node": "~10.11.0", - "@types/node-fetch": "^2.5.2", - "@types/rewire": "^2.5.28", - "@types/semver": "^6.2.0", + "@types/node-fetch": "~2.5.2", + "@types/rewire": "~2.5.28", + "@types/semver": "~6.0.2", + "@types/sinon": "~7.5.0", "mocha": "~5.2.0", "mocha-junit-reporter": "~1.23.1", "mocha-multi-reporters": "~1.1.7", + "mock-fs": "~4.10.2", "rewire": "~4.0.1", + "sinon": "~7.5.0", "tslint": "~5.20.0", "typescript": "~3.5.3", "vsce": "~1.68.0", diff --git a/src/logging.ts b/src/logging.ts index 62d26a56a4..5faf83df35 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -114,6 +114,29 @@ export class Logger implements ILogger { }); } + public async writeAndShowErrorWithActions( + message: string, + actions: Array<{ prompt: string; action: () => Promise }>) { + this.writeError(message); + + const fullActions = [ + ...actions, + { prompt: "Show Logs", action: async () => { this.showLogPanel(); } }, + ]; + + const actionKeys: string[] = fullActions.map((action) => action.prompt); + + const choice = await vscode.window.showErrorMessage(message, ...actionKeys); + if (choice) { + for (const action of fullActions) { + if (choice === action.prompt) { + await action.action(); + return; + } + } + } + } + public startNewLog(minimumLogLevel: string = "Normal") { this.MinimumLogLevel = this.logLevelNameToValue(minimumLogLevel.trim()); diff --git a/src/platform.ts b/src/platform.ts index 2cede17892..2ade340989 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -2,19 +2,24 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ -import fs = require("fs"); -import path = require("path"); -import process = require("process"); -import Settings = require("./settings"); +import * as child_process from "child_process"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import * as process from "process"; +import { IPowerShellAdditionalExePathSettings } from "./settings"; -const linuxExePath = "/usr/bin/pwsh"; -const linuxPreviewExePath = "/usr/bin/pwsh-preview"; +const WindowsPowerShell64BitLabel = "Windows PowerShell (x64)"; +const WindowsPowerShell32BitLabel = "Windows PowerShell (x86)"; -const snapExePath = "/snap/bin/pwsh"; -const snapPreviewExePath = "/snap/bin/pwsh-preview"; +const LinuxExePath = "/usr/bin/pwsh"; +const LinuxPreviewExePath = "/usr/bin/pwsh-preview"; -const macOSExePath = "/usr/local/bin/pwsh"; -const macOSPreviewExePath = "/usr/local/bin/pwsh-preview"; +const SnapExePath = "/snap/bin/pwsh"; +const SnapPreviewExePath = "/snap/bin/pwsh-preview"; + +const MacOSExePath = "/usr/local/bin/pwsh"; +const MacOSPreviewExePath = "/usr/local/bin/pwsh-preview"; export enum OperatingSystem { Unknown, @@ -30,8 +35,8 @@ export interface IPlatformDetails { } export interface IPowerShellExeDetails { - versionName: string; - exePath: string; + readonly displayName: string; + readonly exePath: string; } export function getPlatformDetails(): IPlatformDetails { @@ -55,196 +60,436 @@ export function getPlatformDetails(): IPlatformDetails { } /** - * Gets the default instance of PowerShell for the specified platform. - * On Windows, the default version of PowerShell is "Windows PowerShell". - * @param platformDetails Specifies information about the platform - primarily the operating system. - * @param use32Bit On Windows, this boolean determines whether the 32-bit version of Windows PowerShell is returned. - * @returns A string containing the path of the default version of PowerShell. + * Class to lazily find installed PowerShell executables on a machine. + * When given a list of additional PowerShell executables, + * this will also surface those at the end of the list. */ -export function getDefaultPowerShellPath( - platformDetails: IPlatformDetails, - use32Bit: boolean = false): string | null { - - let powerShellExePath; - let psCoreInstallPath; - - // Find the path to the powershell executable based on the current platform - // and the user's desire to run the x86 version of PowerShell - if (platformDetails.operatingSystem === OperatingSystem.Windows) { - if (use32Bit) { - psCoreInstallPath = - (platformDetails.isProcess64Bit ? process.env["ProgramFiles(x86)"] : process.env.ProgramFiles) - + "\\PowerShell"; - } else { - psCoreInstallPath = - (platformDetails.isProcess64Bit ? process.env.ProgramFiles : process.env.ProgramW6432) + "\\PowerShell"; - } - - if (fs.existsSync(psCoreInstallPath)) { - const arch = platformDetails.isProcess64Bit ? "(x64)" : "(x86)"; - const psCorePaths = - fs.readdirSync(psCoreInstallPath) - .map((item) => path.join(psCoreInstallPath, item)) - .filter((item) => { - const exePath = path.join(item, "pwsh.exe"); - return fs.lstatSync(item).isDirectory() && fs.existsSync(exePath); - }) - .map((item) => ({ - versionName: `PowerShell ${path.parse(item).base} ${arch}`, - exePath: path.join(item, "pwsh.exe"), - })); - - if (psCorePaths) { - return powerShellExePath = psCorePaths[0].exePath; +export class PowerShellExeFinder { + // This is required, since parseInt("7-preview") will return 7. + private static IntRegex: RegExp = /^\d+$/; + + private static PwshMsixRegex: RegExp = /^Microsoft.PowerShell_.*/; + + private static PwshPreviewMsixRegex: RegExp = /^Microsoft.PowerShellPreview_.*/; + + // The platform details descriptor for the platform we're on + private readonly platformDetails: IPlatformDetails; + + // Additional configured PowerShells + private readonly additionalPSExeSettings: Iterable; + + private winPS: IPossiblePowerShellExe; + + private alternateBitnessWinPS: IPossiblePowerShellExe; + + /** + * Create a new PowerShellFinder object to discover PowerShell installations. + * @param platformDetails Information about the machine we are running on. + * @param additionalPowerShellExes Additional PowerShell installations as configured in the settings. + */ + constructor( + platformDetails?: IPlatformDetails, + additionalPowerShellExes?: Iterable) { + + this.platformDetails = platformDetails || getPlatformDetails(); + this.additionalPSExeSettings = additionalPowerShellExes || []; + } + + /** + * Returns the first available PowerShell executable found in the search order. + */ + public getFirstAvailablePowerShellInstallation(): IPowerShellExeDetails { + for (const pwsh of this.enumeratePowerShellInstallations()) { + return pwsh; + } + } + + /** + * Get an array of all PowerShell executables found when searching for PowerShell installations. + */ + public getAllAvailablePowerShellInstallations(): IPowerShellExeDetails[] { + return Array.from(this.enumeratePowerShellInstallations()); + } + + /** + * Fixes PowerShell paths when Windows PowerShell is set to the non-native bitness. + * @param configuredPowerShellPath the PowerShell path configured by the user. + */ + public fixWindowsPowerShellPath(configuredPowerShellPath: string): string { + const lowerConfiguredPath = configuredPowerShellPath.toLocaleLowerCase(); + const lowerAltWinPSPath = this.alternateBitnessWinPS.exePath.toLocaleLowerCase(); + + if (lowerConfiguredPath === lowerAltWinPSPath) { + return this.winPS.exePath; + } + + return configuredPowerShellPath; + } + + /** + * 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. + */ + public *enumeratePowerShellInstallations(): Iterable { + // Get the default PowerShell installations first + for (const defaultPwsh of this.enumerateDefaultPowerShellInstallations()) { + if (defaultPwsh && defaultPwsh.exists()) { + yield defaultPwsh; } } - // No PowerShell 6+ detected so use Windows PowerShell. - if (use32Bit) { - return platformDetails.isOS64Bit && platformDetails.isProcess64Bit - ? SysWow64PowerShellPath - : System32PowerShellPath; + // Also show any additionally configured PowerShells + // These may be duplicates of the default installations, but given a different name. + for (const additionalPwsh of this.enumerateAdditionalPowerShellInstallations()) { + if (additionalPwsh && additionalPwsh.exists()) { + yield additionalPwsh; + } } - return !platformDetails.isOS64Bit || platformDetails.isProcess64Bit - ? System32PowerShellPath - : SysnativePowerShellPath; } - if (platformDetails.operatingSystem === OperatingSystem.MacOS) { - // Always default to the stable version of PowerShell (if installed) but handle case of only Preview installed - powerShellExePath = macOSExePath; - if (!fs.existsSync(macOSExePath) && fs.existsSync(macOSPreviewExePath)) { - powerShellExePath = macOSPreviewExePath; + + /** + * 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. + */ + private *enumerateDefaultPowerShellInstallations(): Iterable { + // Find PSCore stable first + yield this.findPSCoreStable(); + + switch (this.platformDetails.operatingSystem) { + case OperatingSystem.Linux: + // On Linux, find the snap + yield this.findPSCoreStableSnap(); + break; + + case OperatingSystem.Windows: + // Windows may have a 32-bit pwsh.exe + yield this.findPSCoreWindowsInstallation({ useAlternateBitness: true }); + + // Also look for the MSIX/UWP installation + yield this.findPSCoreMsix(); + + break; + } + + // TODO: + // Enable this when the global tool has been updated + // to support proper argument passing. + // Currently it cannot take startup arguments to start PSES with. + // + // Look for the .NET global tool + // yield this.pwshDotnetGlobalTool; + + // Look for PSCore preview + yield this.findPSCorePreview(); + + switch (this.platformDetails.operatingSystem) { + // On Linux, there might be a preview snap + case OperatingSystem.Linux: + yield this.findPSCorePreviewSnap(); + break; + + case OperatingSystem.Windows: + // Find a preview MSIX + yield this.findPSCoreMsix({ findPreview: true }); + + // Look for pwsh-preview with the opposite bitness + yield this.findPSCoreWindowsInstallation({ useAlternateBitness: true, findPreview: true }); + + // Finally, get Windows PowerShell + + // Get the natural Windows PowerShell for the process bitness + yield this.findWinPS(); + + // Get the alternate bitness Windows PowerShell + yield this.findWinPS({ useAlternateBitness: true }); + + break; + } } - } else if (platformDetails.operatingSystem === OperatingSystem.Linux) { - // Always default to the stable version of PowerShell (if installed) but handle case of only Preview installed - // as well as the Snaps case - https://snapcraft.io/ - powerShellExePath = linuxExePath; - if (!fs.existsSync(linuxExePath) && fs.existsSync(linuxPreviewExePath)) { - powerShellExePath = linuxPreviewExePath; - } else if (fs.existsSync(snapExePath)) { - powerShellExePath = snapExePath; - } else if (fs.existsSync(snapPreviewExePath)) { - powerShellExePath = snapPreviewExePath; + + /** + * Iterates through the configured additonal PowerShell executable locations, + * without checking for their existence. + */ + private *enumerateAdditionalPowerShellInstallations(): Iterable { + for (const additionalPwshSetting of this.additionalPSExeSettings) { + yield new PossiblePowerShellExe(additionalPwshSetting.exePath, additionalPwshSetting.versionName); } } - return powerShellExePath; -} + private findPSCoreStable(): IPossiblePowerShellExe { + switch (this.platformDetails.operatingSystem) { + case OperatingSystem.Linux: + return new PossiblePowerShellExe(LinuxExePath, "PowerShell"); -export function getWindowsSystemPowerShellPath(systemFolderName: string) { - return `${process.env.windir}\\${systemFolderName}\\WindowsPowerShell\\v1.0\\powershell.exe`; -} + case OperatingSystem.MacOS: + return new PossiblePowerShellExe(MacOSExePath, "PowerShell"); + + case OperatingSystem.Windows: + return this.findPSCoreWindowsInstallation(); + } + } + + private findPSCorePreview(): IPossiblePowerShellExe { + switch (this.platformDetails.operatingSystem) { + case OperatingSystem.Linux: + return new PossiblePowerShellExe(LinuxPreviewExePath, "PowerShell Preview"); -export const System32PowerShellPath = getWindowsSystemPowerShellPath("System32"); -export const SysnativePowerShellPath = getWindowsSystemPowerShellPath("Sysnative"); -export const SysWow64PowerShellPath = getWindowsSystemPowerShellPath("SysWow64"); + case OperatingSystem.MacOS: + return new PossiblePowerShellExe(MacOSPreviewExePath, "PowerShell Preview"); -export const WindowsPowerShell64BitLabel = "Windows PowerShell (x64)"; -export const WindowsPowerShell32BitLabel = "Windows PowerShell (x86)"; + case OperatingSystem.Windows: + return this.findPSCoreWindowsInstallation({ findPreview: true }); + } + } -const powerShell64BitPathOn32Bit = SysnativePowerShellPath.toLocaleLowerCase(); -const powerShell32BitPathOn64Bit = SysWow64PowerShellPath.toLocaleLowerCase(); + private findPSCoreDotnetGlobalTool(): IPossiblePowerShellExe { + const exeName: string = this.platformDetails.operatingSystem === OperatingSystem.Windows + ? "pwsh.exe" + : "pwsh"; -export function fixWindowsPowerShellPath(powerShellExePath: string, platformDetails: IPlatformDetails): string { - const lowerCasedPath = powerShellExePath.toLocaleLowerCase(); + const dotnetGlobalToolExePath: string = path.join(os.homedir(), ".dotnet", "tools", exeName); - if ((platformDetails.isProcess64Bit && (lowerCasedPath === powerShell64BitPathOn32Bit)) || - (!platformDetails.isProcess64Bit && (lowerCasedPath === powerShell32BitPathOn64Bit))) { - return System32PowerShellPath; + return new PossiblePowerShellExe(dotnetGlobalToolExePath, ".NET Core PowerShell Global Tool"); } - // If the path doesn't need to be fixed, return the original - return powerShellExePath; -} + private findPSCoreMsix({ findPreview }: { findPreview?: boolean } = {}): IPossiblePowerShellExe { + // We can't proceed if there's no LOCALAPPDATA path + if (!process.env.LOCALAPPDATA) { + return null; + } -/** - * Gets a list of all available PowerShell instance on the specified platform. - * @param platformDetails Specifies information about the platform - primarily the operating system. - * @param sessionSettings Specifies the user/workspace settings. Additional PowerShell exe paths loaded from settings. - * @returns An array of IPowerShellExeDetails objects with the PowerShell name & exe path for each instance found. - */ -export function getAvailablePowerShellExes( - platformDetails: IPlatformDetails, - sessionSettings: Settings.ISettings | undefined): IPowerShellExeDetails[] { - - let paths: IPowerShellExeDetails[] = []; - - if (platformDetails.operatingSystem === OperatingSystem.Windows) { - if (platformDetails.isProcess64Bit) { - paths.push({ - versionName: WindowsPowerShell64BitLabel, - exePath: System32PowerShellPath, - }); - - paths.push({ - versionName: WindowsPowerShell32BitLabel, - exePath: SysWow64PowerShellPath, - }); - } else { - if (platformDetails.isOS64Bit) { - paths.push({ - versionName: WindowsPowerShell64BitLabel, - exePath: SysnativePowerShellPath, - }); + // Find the base directory for MSIX application exe shortcuts + const msixAppDir = path.join(process.env.LOCALAPPDATA, "Microsoft", "WindowsApps"); + + if (!fs.existsSync(msixAppDir)) { + return null; + } + + // Define whether we're looking for the preview or the stable + const { pwshMsixDirRegex, pwshMsixName } = findPreview + ? { pwshMsixDirRegex: PowerShellExeFinder.PwshPreviewMsixRegex, pwshMsixName: "PowerShell Preview (Store)" } + : { pwshMsixDirRegex: PowerShellExeFinder.PwshMsixRegex, pwshMsixName: "PowerShell (Store)" }; + + // We should find only one such application, so return on the first one + for (const subdir of fs.readdirSync(msixAppDir)) { + if (pwshMsixDirRegex.test(subdir)) { + const pwshMsixPath = path.join(msixAppDir, subdir, "pwsh.exe"); + return new PossiblePowerShellExe(pwshMsixPath, pwshMsixName); } + } - paths.push({ - versionName: WindowsPowerShell32BitLabel, - exePath: System32PowerShellPath, - }); - } - - const psCoreInstallPath = - (!platformDetails.isProcess64Bit ? process.env.ProgramW6432 : process.env.ProgramFiles) + "\\PowerShell"; - - if (fs.existsSync(psCoreInstallPath)) { - const arch = platformDetails.isProcess64Bit ? "(x64)" : "(x86)"; - const psCorePaths = - fs.readdirSync(psCoreInstallPath) - .map((item) => path.join(psCoreInstallPath, item)) - .filter((item) => { - const exePath = path.join(item, "pwsh.exe"); - return fs.lstatSync(item).isDirectory() && fs.existsSync(exePath); - }) - .map((item) => ({ - versionName: `PowerShell ${path.parse(item).base} ${arch}`, - exePath: path.join(item, "pwsh.exe"), - })); - - if (psCorePaths) { - paths = paths.concat(psCorePaths); + // If we find nothing, return null + return null; + } + + private findPSCoreStableSnap(): IPossiblePowerShellExe { + return new PossiblePowerShellExe(SnapExePath, "PowerShell Snap"); + } + + private findPSCorePreviewSnap(): IPossiblePowerShellExe { + return new PossiblePowerShellExe(SnapPreviewExePath, "PowerShell Preview Snap"); + } + + private findPSCoreWindowsInstallation( + { useAlternateBitness = false, findPreview = false }: + { useAlternateBitness?: boolean; findPreview?: boolean } = {}): IPossiblePowerShellExe { + + const programFilesPath: string = this.getProgramFilesPath({ useAlternateBitness }); + + if (!programFilesPath) { + return null; + } + + const powerShellInstallBaseDir = path.join(programFilesPath, "PowerShell"); + + // Ensure the base directory exists + if (!(fs.existsSync(powerShellInstallBaseDir) && fs.lstatSync(powerShellInstallBaseDir).isDirectory())) { + return null; + } + + let highestSeenVersion: number = -1; + let pwshExePath: string = null; + for (const item of fs.readdirSync(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 + const intPart: string = item.substring(0, dashIndex); + if (!PowerShellExeFinder.IntRegex.test(intPart)) { + continue; + } + + // Verify that the part after the dash is "preview" + if (item.substring(dashIndex + 1) !== "preview") { + continue; + } + + currentVersion = parseInt(intPart, 10); + } else { + // Search for a directory like "6" or "7" + if (!PowerShellExeFinder.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 (!fs.existsSync(exePath)) { + continue; } + + pwshExePath = exePath; + highestSeenVersion = currentVersion; + } + + if (!pwshExePath) { + return null; } - } else { - // Handle Linux and macOS case - let exePaths: string[]; - if (platformDetails.operatingSystem === OperatingSystem.Linux) { - exePaths = [ linuxExePath, snapExePath, linuxPreviewExePath, snapPreviewExePath ]; - } else { - exePaths = [ macOSExePath, macOSPreviewExePath ]; + const bitness: string = programFilesPath.includes("x86") + ? "(x86)" + : "(x64)"; + + const preview: string = findPreview ? " Preview" : ""; + + return new PossiblePowerShellExe(pwshExePath, `PowerShell${preview} ${bitness}`); + } + + private findWinPS({ useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): IPossiblePowerShellExe { + + // 32-bit OSes only have one WinPS on them + if (!this.platformDetails.isOS64Bit && useAlternateBitness) { + return null; } - exePaths.forEach((exePath) => { - if (fs.existsSync(exePath)) { - paths.push({ - versionName: "PowerShell" + (/-preview/.test(exePath) ? " Preview" : ""), - exePath, - }); + let winPS = useAlternateBitness ? this.alternateBitnessWinPS : this.winPS; + if (winPS === undefined) { + const systemFolderPath: string = this.getSystem32Path({ useAlternateBitness }); + + const winPSPath = path.join(systemFolderPath, "WindowsPowerShell", "v1.0", "powershell.exe"); + + let displayName: string; + if (this.platformDetails.isProcess64Bit) { + displayName = useAlternateBitness + ? WindowsPowerShell32BitLabel + : WindowsPowerShell64BitLabel; + } else if (this.platformDetails.isOS64Bit) { + displayName = useAlternateBitness + ? WindowsPowerShell64BitLabel + : WindowsPowerShell32BitLabel; + } else { + displayName = WindowsPowerShell32BitLabel; + } + + winPS = new PossiblePowerShellExe(winPSPath, displayName, { knownToExist: true }); + + if (useAlternateBitness) { + this.alternateBitnessWinPS = winPS; + } else { + this.winPS = winPS; } - }); + } + + return winPS; } - // When unit testing, we don't have session settings available to test, so skip reading this setting - if (sessionSettings) { - // Add additional PowerShell paths as configured in settings - for (const additionalPowerShellExePath of sessionSettings.powerShellAdditionalExePaths) { - paths.push({ - versionName: additionalPowerShellExePath.versionName, - exePath: additionalPowerShellExePath.exePath, - }); + private getProgramFilesPath( + { useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): string | null { + + if (!useAlternateBitness) { + // Just use the native system bitness + return process.env.ProgramFiles; } + + // We might be a 64-bit process looking for 32-bit program files + if (this.platformDetails.isProcess64Bit) { + return process.env["ProgramFiles(x86)"]; + } + + // We might be a 32-bit process looking for 64-bit program files + if (this.platformDetails.isOS64Bit) { + return process.env.ProgramW6432; + } + + // We're a 32-bit process on 32-bit Windows, there is no other Program Files dir + return null; } - return paths; + private getSystem32Path({ useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): string | null { + const windir: string = process.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 (this.platformDetails.isProcess64Bit) { + return path.join(windir, "SysWOW64"); + } + + // We might be a 32-bit process looking for 64-bit system32 + if (this.platformDetails.isOS64Bit) { + return path.join(windir, "Sysnative"); + } + + // We're on a 32-bit Windows, so no alternate bitness + return null; + } +} + +export function getWindowsSystemPowerShellPath(systemFolderName: string) { + return path.join( + process.env.windir, + systemFolderName, + "WindowsPowerShell", + "v1.0", + "powershell.exe"); +} + +interface IPossiblePowerShellExe extends IPowerShellExeDetails { + exists(): boolean; +} + +class PossiblePowerShellExe implements IPossiblePowerShellExe { + public readonly exePath: string; + public readonly displayName: string; + + private knownToExist: boolean; + + constructor( + pathToExe: string, + installationName: string, + { knownToExist = false }: { knownToExist?: boolean } = {}) { + + this.exePath = pathToExe; + this.displayName = installationName; + this.knownToExist = knownToExist || undefined; + } + + public exists(): boolean { + if (this.knownToExist === undefined) { + this.knownToExist = fs.existsSync(this.exePath); + } + return this.knownToExist; + } } diff --git a/src/session.ts b/src/session.ts index 6de4882965..4fb4186f92 100644 --- a/src/session.ts +++ b/src/session.ts @@ -5,7 +5,6 @@ import cp = require("child_process"); import fs = require("fs"); import net = require("net"); -import os = require("os"); import path = require("path"); import * as semver from "semver"; import vscode = require("vscode"); @@ -24,8 +23,8 @@ import { import { GitHubReleaseInformation, InvokePowerShellUpdateCheck } from "./features/UpdatePowerShell"; import { - fixWindowsPowerShellPath, getAvailablePowerShellExes, getDefaultPowerShellPath, - getPlatformDetails, IPlatformDetails, OperatingSystem } from "./platform"; + getPlatformDetails, IPlatformDetails, + OperatingSystem, PowerShellExeFinder } from "./platform"; export enum SessionStatus { NeverStarted, @@ -57,6 +56,9 @@ export class SessionManager implements Middleware { private bundledModulesPath: string; private telemetryReporter: TelemetryReporter; + // Initialized by the start() method, since this requires settings + private powershellExeFinder: PowerShellExeFinder; + // When in development mode, VS Code's session ID is a fake // value of "someValue.machineId". Use that to detect dev // mode for now until Microsoft/vscode#10272 gets implemented. @@ -71,6 +73,7 @@ export class SessionManager implements Middleware { private reporter: TelemetryReporter) { this.platformDetails = getPlatformDetails(); + this.HostVersion = version; this.telemetryReporter = reporter; @@ -104,55 +107,78 @@ export class SessionManager implements Middleware { public start() { this.sessionSettings = Settings.load(); + this.log.startNewLog(this.sessionSettings.developer.editorServicesLogLevel); + // Create the PowerShell executable finder now + this.powershellExeFinder = new PowerShellExeFinder( + this.platformDetails, + this.sessionSettings.powerShellAdditionalExePaths); + this.focusConsoleOnExecute = this.sessionSettings.integratedConsole.focusConsoleOnExecute; this.createStatusBarItem(); - this.powerShellExePath = this.getPowerShellExePath(); + try { + this.powerShellExePath = this.getPowerShellExePath(); + } catch (e) { + this.log.writeError(`Error occurred while searching for a PowerShell executable:\n${e}`); + } this.suppressRestartPrompt = false; - if (this.powerShellExePath) { + if (!this.powerShellExePath) { + const message = "Unable to find PowerShell." + + " Do you have PowerShell installed?" + + " You can also configure custom PowerShell installations" + + " with the 'powershell.powerShellAdditionalExePaths' setting."; + + this.log.writeAndShowErrorWithActions(message, [ + { + prompt: "Get PowerShell", + action: async () => { + const getPSUri = vscode.Uri.parse("https://aka.ms/get-powershell-vscode"); + vscode.env.openExternal(getPSUri); + }, + }, + ]); + return; + } - this.bundledModulesPath = path.resolve(__dirname, this.sessionSettings.bundledModulesPath); + this.bundledModulesPath = path.resolve(__dirname, this.sessionSettings.bundledModulesPath); - if (this.inDevelopmentMode) { - const devBundledModulesPath = - path.resolve( - __dirname, - this.sessionSettings.developer.bundledModulesPath); + if (this.inDevelopmentMode) { + const devBundledModulesPath = + path.resolve( + __dirname, + this.sessionSettings.developer.bundledModulesPath); - // Make sure the module's bin path exists - if (fs.existsSync(path.join(devBundledModulesPath, "PowerShellEditorServices/bin"))) { - this.bundledModulesPath = devBundledModulesPath; - } else { - this.log.write( - "\nWARNING: In development mode but PowerShellEditorServices dev module path cannot be " + - `found (or has not been built yet): ${devBundledModulesPath}\n`); - } + // Make sure the module's bin path exists + if (fs.existsSync(path.join(devBundledModulesPath, "PowerShellEditorServices/bin"))) { + this.bundledModulesPath = devBundledModulesPath; + } else { + this.log.write( + "\nWARNING: In development mode but PowerShellEditorServices dev module path cannot be " + + `found (or has not been built yet): ${devBundledModulesPath}\n`); } + } - this.editorServicesArgs = - `-HostName 'Visual Studio Code Host' ` + - `-HostProfileId 'Microsoft.VSCode' ` + - `-HostVersion '${this.HostVersion}' ` + - `-AdditionalModules @('PowerShellEditorServices.VSCode') ` + - `-BundledModulesPath '${PowerShellProcess.escapeSingleQuotes(this.bundledModulesPath)}' ` + - `-EnableConsoleRepl `; + this.editorServicesArgs = + `-HostName 'Visual Studio Code Host' ` + + `-HostProfileId 'Microsoft.VSCode' ` + + `-HostVersion '${this.HostVersion}' ` + + `-AdditionalModules @('PowerShellEditorServices.VSCode') ` + + `-BundledModulesPath '${PowerShellProcess.escapeSingleQuotes(this.bundledModulesPath)}' ` + + `-EnableConsoleRepl `; - if (this.sessionSettings.developer.editorServicesWaitForDebugger) { - this.editorServicesArgs += "-WaitForDebugger "; - } - if (this.sessionSettings.developer.editorServicesLogLevel) { - this.editorServicesArgs += `-LogLevel '${this.sessionSettings.developer.editorServicesLogLevel}' `; - } - - this.startPowerShell(); - } else { - this.setSessionFailure("PowerShell could not be started, click 'Show Logs' for more details."); + if (this.sessionSettings.developer.editorServicesWaitForDebugger) { + this.editorServicesArgs += "-WaitForDebugger "; } + if (this.sessionSettings.developer.editorServicesLogLevel) { + this.editorServicesArgs += `-LogLevel '${this.sessionSettings.developer.editorServicesLogLevel}' `; + } + + this.startPowerShell(); } public stop() { @@ -216,62 +242,16 @@ export class SessionManager implements Middleware { } public getPowerShellExePath(): string { - let powerShellExePath: string; - if (!this.sessionSettings.powerShellExePath && this.sessionSettings.developer.powerShellExePath) { // Show deprecation message with fix action. // We don't need to wait on this to complete // because we can finish gathering the configured // PowerShell path without the fix - vscode - .window - .showWarningMessage( - "The 'powershell.developer.powerShellExePath' setting is deprecated, use " + - "'powershell.powerShellExePath' instead.", - "Fix Automatically") - .then((choice) => { - if (choice) { - this.suppressRestartPrompt = true; - Settings - .change( - "powerShellExePath", - this.sessionSettings.developer.powerShellExePath, - true) - .then(() => { - return Settings.change( - "developer.powerShellExePath", - undefined, - true); - }) - .then(() => { - this.suppressRestartPrompt = false; - }); - } - }); + this.showExePathSettingDeprecationWarning(); } - // If powershell.powerShellDefaultVersion specified, attempt to find the PowerShell exe path - // of the version specified by the setting. - if ((this.sessionStatus === SessionStatus.NeverStarted) && this.sessionSettings.powerShellDefaultVersion) { - const powerShellExePaths = getAvailablePowerShellExes(this.platformDetails, this.sessionSettings); - const powerShellDefaultVersion = - powerShellExePaths.find((item) => item.versionName === this.sessionSettings.powerShellDefaultVersion); - - if (powerShellDefaultVersion) { - powerShellExePath = powerShellDefaultVersion.exePath; - } else { - this.log.writeWarning( - `Could not find powerShellDefaultVersion: '${this.sessionSettings.powerShellDefaultVersion}'`); - } - } - - // Is there a setting override for the PowerShell path? - powerShellExePath = - (powerShellExePath || - this.sessionSettings.powerShellExePath || - this.sessionSettings.developer.powerShellExePath || - "").trim(); + let powerShellExePath: string = this.getConfiguredPowerShellExePath().trim(); // New versions of PS Core uninstall the previous version // so make sure the path stored in the settings exists. @@ -282,54 +262,28 @@ export class SessionManager implements Middleware { powerShellExePath = ""; } - if (this.platformDetails.operatingSystem === OperatingSystem.Windows && - powerShellExePath.length > 0) { - - // Check the path bitness - const fixedPath = - fixWindowsPowerShellPath( - powerShellExePath, - this.platformDetails); - - if (fixedPath !== powerShellExePath) { - const bitness = this.platformDetails.isOS64Bit ? 64 : 32; - // Show deprecation message with fix action. - // We don't need to wait on this to complete - // because we can finish gathering the configured - // PowerShell path without the fix - vscode - .window - .showWarningMessage( - `The specified PowerShell path is incorrect for ${bitness}-bit VS Code, using '${fixedPath}' ` + - "instead.", - "Fix Setting Automatically") - .then((choice) => { - if (choice) { - this.suppressRestartPrompt = true; - Settings - .change( - "powerShellExePath", - this.sessionSettings.developer.powerShellExePath, - true) - .then(() => { - return Settings.change( - "developer.powerShellExePath", - undefined, - true); - }) - .then(() => { - this.suppressRestartPrompt = false; - }); - } - }); - - powerShellExePath = fixedPath; + if (powerShellExePath) { + if (this.platformDetails.operatingSystem === OperatingSystem.Windows) { + // Check the path bitness + const fixedPath = this.powershellExeFinder.fixWindowsPowerShellPath( + powerShellExePath); + + if (fixedPath !== powerShellExePath) { + // Show deprecation message with fix action. + // We don't need to wait on this to complete + // because we can finish gathering the configured + // PowerShell path without the fix + this.showBitnessPathFixWarning(fixedPath); + powerShellExePath = fixedPath; + } } + + return this.resolvePowerShellPath(powerShellExePath); } - return powerShellExePath.length > 0 - ? this.resolvePowerShellPath(powerShellExePath) - : getDefaultPowerShellPath(this.platformDetails, this.sessionSettings.useX86Host); + // No need to resolve this path, since the finder guarantees its existence + const firstPowerShell = this.powershellExeFinder.getFirstAvailablePowerShellInstallation(); + return firstPowerShell && firstPowerShell.exePath || null; } // ----- LanguageClient middleware methods ----- @@ -377,6 +331,66 @@ export class SessionManager implements Middleware { return resolvedCodeLens; } + private async showExePathSettingDeprecationWarning(): Promise { + const choice: string = await vscode.window.showWarningMessage( + "The 'powershell.developer.powerShellExePath' setting is deprecated, use " + + "'powershell.powerShellExePath' instead.", + "Fix Automatically"); + + if (!choice) { + return; + } + + this.suppressRestartPrompt = true; + await Settings.change("powerShellExePath", this.sessionSettings.developer.powerShellExePath, true); + await Settings.change("developer.powerShellExePath", undefined, true); + this.suppressRestartPrompt = false; + } + + private async showBitnessPathFixWarning(fixedPath: string): Promise { + const bitness = this.platformDetails.isOS64Bit ? "64" : "32"; + + const choice = await vscode.window.showWarningMessage( + `The specified PowerShell path is incorrect for ${bitness}-bit VS Code, using '${fixedPath}' instead.`, + "Fix Setting Automatically"); + + if (!choice) { + return; + } + + this.suppressRestartPrompt = true; + await Settings.change("powerShellExePath", this.sessionSettings.developer.powerShellExePath, true); + await Settings.change("developer.powerShellExePath", undefined, true); + this.suppressRestartPrompt = false; + } + + private getConfiguredPowerShellExePath(): string { + // If powershell.powerShellDefaultVersion specified, attempt to find the PowerShell exe path + // of the version specified by the setting. + if (this.sessionSettings.powerShellDefaultVersion && this.sessionStatus === SessionStatus.NeverStarted) { + for (const pwshExe of this.powershellExeFinder.enumeratePowerShellInstallations()) { + if (pwshExe.displayName === this.sessionSettings.powerShellDefaultVersion) { + return pwshExe.exePath; + } + } + + // Default PowerShell version was configured but we didn't find it + this.log.writeWarning( + `Could not find powerShellDefaultVersion: '${this.sessionSettings.powerShellDefaultVersion}'`); + } + + if (this.sessionSettings.powerShellExePath) { + return this.sessionSettings.powerShellExePath; + } + + if (this.sessionSettings.developer.powerShellExePath) { + this.showExePathSettingDeprecationWarning(); + return this.sessionSettings.developer.powerShellExePath; + } + + return ""; + } + private onConfigurationUpdated() { const settings = Settings.load(); @@ -687,8 +701,10 @@ export class SessionManager implements Middleware { // If the path does not exist, show an error if (!utils.checkIfFileExists(resolvedPath)) { - this.setSessionFailure( - "powershell.exe cannot be found or is not accessible at path " + resolvedPath); + const pwshPath = resolvedPath || powerShellExePath; + const pwshExeName = path.basename(pwshPath) || "PowerShell executable"; + + this.setSessionFailure(`${pwshExeName} cannot be found or is not accessible at path '${pwshPath}'`); return null; } @@ -730,8 +746,7 @@ export class SessionManager implements Middleware { private showSessionMenu() { const currentExePath = (this.powerShellExePath || "").toLowerCase(); - const availablePowerShellExes = - getAvailablePowerShellExes(this.platformDetails, this.sessionSettings); + const availablePowerShellExes = this.powershellExeFinder.getAllAvailablePowerShellInstallations(); let sessionText: string; @@ -747,7 +762,7 @@ export class SessionManager implements Middleware { const powerShellSessionName = currentPowerShellExe ? - currentPowerShellExe.versionName : + currentPowerShellExe.displayName : `PowerShell ${this.versionDetails.displayVersion} ` + `(${this.versionDetails.architecture}) ${this.versionDetails.edition} Edition ` + `[${this.versionDetails.version}]`; @@ -768,7 +783,7 @@ export class SessionManager implements Middleware { .filter((item) => item.exePath.toLowerCase() !== currentExePath) .map((item) => { return new SessionMenuItem( - `Switch to: ${item.versionName}`, + `Switch to: ${item.displayName}`, () => { this.changePowerShellExePath(item.exePath); }); }); diff --git a/test/platform.test.ts b/test/platform.test.ts index 398ca7560b..e141795029 100644 --- a/test/platform.test.ts +++ b/test/platform.test.ts @@ -3,119 +3,595 @@ *--------------------------------------------------------*/ import * as assert from "assert"; +import mockFS = require("mock-fs"); +import FileSystem = require("mock-fs/lib/filesystem"); +import * as path from "path"; +import * as sinon from "sinon"; import * as platform from "../src/platform"; -function checkDefaultPowerShellPath(platformDetails, expectedPath) { - test("returns correct default path", () => { - assert.equal( - platform.getDefaultPowerShellPath(platformDetails), - expectedPath); - }); +/** + * Describes a platform on which the PowerShell extension should work, + * including the test conditions (filesystem, environment variables). + */ +interface ITestPlatform { + name: string; + platformDetails: platform.IPlatformDetails; + filesystem: FileSystem.DirectoryItems; + environmentVars?: Record; } -function checkAvailableWindowsPowerShellPaths( - platformDetails: platform.IPlatformDetails, - expectedPaths: platform.IPowerShellExeDetails[]) { - test("correctly enumerates available Windows PowerShell paths", () => { - - // The system may return PowerShell Core paths so only - // enumerate the first list items. - const enumeratedPaths = platform.getAvailablePowerShellExes(platformDetails, undefined); - for (let i; i < expectedPaths.length; i++) { - assert.equal(enumeratedPaths[i], expectedPaths[i]); - } - }); +/** + * A platform where the extension should find a PowerShell, + * including the sequence of PowerShell installations that should be found. + * The expected default PowerShell is the first installation. + */ +interface ITestPlatformSuccessCase extends ITestPlatform { + expectedPowerShellSequence: platform.IPowerShellExeDetails[]; } -function checkFixedWindowsPowerShellpath(platformDetails, inputPath, expectedPath) { - test("fixes incorrect Windows PowerShell Sys* path", () => { - assert.equal( - platform.fixWindowsPowerShellPath(inputPath, platformDetails), - expectedPath); - }); -} +// Platform configurations where we expect to find a set of PowerShells +let successTestCases: ITestPlatformSuccessCase[]; -suite("Platform module", () => { - if (process.platform === "win32") { - suite("64-bit Windows, 64-bit VS Code", () => { - const platformDetails: platform.IPlatformDetails = { +let msixAppDir = null; +let pwshMsixPath = null; +let pwshPreviewMsixPath = null; +if (process.platform === "win32") { + msixAppDir = path.join(process.env.LOCALAPPDATA, "Microsoft", "WindowsApps"); + pwshMsixPath = path.join(msixAppDir, "Microsoft.PowerShell_8wekyb3d8bbwe", "pwsh.exe"); + pwshPreviewMsixPath = path.join(msixAppDir, "Microsoft.PowerShellPreview_8wekyb3d8bbwe", "pwsh.exe"); + + successTestCases = [ + { + name: "Windows 64-bit, 64-bit VSCode (all installations)", + platformDetails: { operatingSystem: platform.OperatingSystem.Windows, isOS64Bit: true, isProcess64Bit: true, - }; - - checkDefaultPowerShellPath( - platformDetails, - platform.System32PowerShellPath); - - checkAvailableWindowsPowerShellPaths( - platformDetails, - [ - { - versionName: platform.WindowsPowerShell64BitLabel, - exePath: platform.System32PowerShellPath, + }, + environmentVars: { + "ProgramFiles": "C:\\Program Files", + "ProgramFiles(x86)": "C:\\Program Files (x86)", + "windir": "C:\\WINDOWS", + }, + expectedPowerShellSequence: [ + { + exePath: "C:\\Program Files\\PowerShell\\6\\pwsh.exe", + displayName: "PowerShell (x64)", + }, + { + exePath: "C:\\Program Files (x86)\\PowerShell\\6\\pwsh.exe", + displayName: "PowerShell (x86)", + }, + { + exePath: pwshMsixPath, + displayName: "PowerShell (Store)", + }, + { + exePath: "C:\\Program Files\\PowerShell\\7-preview\\pwsh.exe", + displayName: "PowerShell Preview (x64)", + }, + { + exePath: pwshPreviewMsixPath, + displayName: "PowerShell Preview (Store)", + }, + { + exePath: "C:\\Program Files (x86)\\PowerShell\\7-preview\\pwsh.exe", + displayName: "PowerShell Preview (x86)", + }, + { + exePath: "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + displayName: "Windows PowerShell (x64)", + }, + { + exePath: "C:\\WINDOWS\\SysWOW64\\WindowsPowerShell\\v1.0\\powershell.exe", + displayName: "Windows PowerShell (x86)", + }, + ], + filesystem: { + "C:\\Program Files\\PowerShell": { + "6": { + "pwsh.exe": "", }, - { - versionName: platform.WindowsPowerShell32BitLabel, - exePath: platform.SysWow64PowerShellPath, + "7-preview": { + "pwsh.exe": "", }, - ]); - - checkFixedWindowsPowerShellpath( - platformDetails, - platform.SysnativePowerShellPath, - platform.System32PowerShellPath); - }); - - suite("64-bit Windows, 32-bit VS Code", () => { - const platformDetails: platform.IPlatformDetails = { + }, + "C:\\Program Files (x86)\\PowerShell": { + "6": { + "pwsh.exe": "", + }, + "7-preview": { + "pwsh.exe": "", + }, + }, + [msixAppDir]: { + "Microsoft.PowerShell_8wekyb3d8bbwe": { + "pwsh.exe": "", + }, + "Microsoft.PowerShellPreview_8wekyb3d8bbwe": { + "pwsh.exe": "", + }, + }, + "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0": { + "powershell.exe": "", + }, + "C:\\WINDOWS\\SysWOW64\\WindowsPowerShell\\v1.0": { + "powershell.exe": "", + }, + }, + }, + { + name: "Windows 64-bit, 64-bit VSCode (only Windows PowerShell)", + platformDetails: { + operatingSystem: platform.OperatingSystem.Windows, + isOS64Bit: true, + isProcess64Bit: true, + }, + environmentVars: { + "ProgramFiles": "C:\\Program Files", + "ProgramFiles(x86)": "C:\\Program Files (x86)", + "windir": "C:\\WINDOWS", + }, + expectedPowerShellSequence: [ + { + exePath: "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + displayName: "Windows PowerShell (x64)", + }, + { + exePath: "C:\\WINDOWS\\SysWOW64\\WindowsPowerShell\\v1.0\\powershell.exe", + displayName: "Windows PowerShell (x86)", + }, + ], + filesystem: { + "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0": { + "powershell.exe": "", + }, + "C:\\WINDOWS\\SysWOW64\\WindowsPowerShell\\v1.0": { + "powershell.exe": "", + }, + }, + }, + { + name: "Windows 64-bit, 32-bit VSCode (all installations)", + platformDetails: { operatingSystem: platform.OperatingSystem.Windows, isOS64Bit: true, isProcess64Bit: false, - }; - - checkDefaultPowerShellPath( - platformDetails, - "C:\\Program Files\\PowerShell\\6\\pwsh.exe"); - - checkAvailableWindowsPowerShellPaths( - platformDetails, - [ - { - versionName: platform.WindowsPowerShell64BitLabel, - exePath: platform.SysnativePowerShellPath, + }, + environmentVars: { + "ProgramFiles": "C:\\Program Files (x86)", + "ProgramFiles(x86)": "C:\\Program Files (x86)", + "windir": "C:\\WINDOWS", + }, + expectedPowerShellSequence: [ + { + exePath: "C:\\Program Files (x86)\\PowerShell\\6\\pwsh.exe", + displayName: "PowerShell (x86)", + }, + { + exePath: "C:\\Program Files\\PowerShell\\6\\pwsh.exe", + displayName: "PowerShell (x64)", + }, + { + exePath: pwshMsixPath, + displayName: "PowerShell (Store)", + }, + { + exePath: "C:\\Program Files (x86)\\PowerShell\\7-preview\\pwsh.exe", + displayName: "PowerShell Preview (x86)", + }, + { + exePath: pwshPreviewMsixPath, + displayName: "PowerShell Preview (Store)", + }, + { + exePath: "C:\\Program Files\\PowerShell\\7-preview\\pwsh.exe", + displayName: "PowerShell Preview (x64)", + }, + { + exePath: "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + displayName: "Windows PowerShell (x86)", + }, + { + exePath: "C:\\WINDOWS\\Sysnative\\WindowsPowerShell\\v1.0\\powershell.exe", + displayName: "Windows PowerShell (x64)", + }, + ], + filesystem: { + "C:\\Program Files\\PowerShell": { + "6": { + "pwsh.exe": "", }, - { - versionName: platform.WindowsPowerShell32BitLabel, - exePath: platform.System32PowerShellPath, + "7-preview": { + "pwsh.exe": "", }, - ]); - - checkFixedWindowsPowerShellpath( - platformDetails, - platform.SysWow64PowerShellPath, - platform.System32PowerShellPath); - }); - - suite("32-bit Windows, 32-bit VS Code", () => { - const platformDetails: platform.IPlatformDetails = { + }, + "C:\\Program Files (x86)\\PowerShell": { + "6": { + "pwsh.exe": "", + }, + "7-preview": { + "pwsh.exe": "", + }, + }, + [msixAppDir]: { + "Microsoft.PowerShell_8wekyb3d8bbwe": { + "pwsh.exe": "", + }, + "Microsoft.PowerShellPreview_8wekyb3d8bbwe": { + "pwsh.exe": "", + }, + }, + "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0": { + "powershell.exe": "", + }, + "C:\\WINDOWS\\Sysnative\\WindowsPowerShell\\v1.0": { + "powershell.exe": "", + }, + }, + }, + { + name: "Windows 64-bit, 32-bit VSCode (Windows PowerShell only)", + platformDetails: { + operatingSystem: platform.OperatingSystem.Windows, + isOS64Bit: true, + isProcess64Bit: false, + }, + environmentVars: { + "ProgramFiles": "C:\\Program Files (x86)", + "ProgramFiles(x86)": "C:\\Program Files (x86)", + "windir": "C:\\WINDOWS", + }, + expectedPowerShellSequence: [ + { + exePath: "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + displayName: "Windows PowerShell (x86)", + }, + { + exePath: "C:\\WINDOWS\\Sysnative\\WindowsPowerShell\\v1.0\\powershell.exe", + displayName: "Windows PowerShell (x64)", + }, + ], + filesystem: { + "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0": { + "powershell.exe": "", + }, + "C:\\WINDOWS\\Sysnative\\WindowsPowerShell\\v1.0": { + "powershell.exe": "", + }, + }, + }, + { + name: "Windows 32-bit, 32-bit VSCode (all installations)", + platformDetails: { + operatingSystem: platform.OperatingSystem.Windows, + isOS64Bit: false, + isProcess64Bit: false, + }, + environmentVars: { + "ProgramFiles": "C:\\Program Files (x86)", + "ProgramFiles(x86)": "C:\\Program Files (x86)", + "windir": "C:\\WINDOWS", + }, + expectedPowerShellSequence: [ + { + exePath: "C:\\Program Files (x86)\\PowerShell\\6\\pwsh.exe", + displayName: "PowerShell (x86)", + }, + { + exePath: pwshMsixPath, + displayName: "PowerShell (Store)", + }, + { + exePath: "C:\\Program Files (x86)\\PowerShell\\7-preview\\pwsh.exe", + displayName: "PowerShell Preview (x86)", + }, + { + exePath: pwshPreviewMsixPath, + displayName: "PowerShell Preview (Store)", + }, + { + exePath: "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + displayName: "Windows PowerShell (x86)", + }, + ], + filesystem: { + "C:\\Program Files (x86)\\PowerShell": { + "6": { + "pwsh.exe": "", + }, + "7-preview": { + "pwsh.exe": "", + }, + }, + [msixAppDir]: { + "Microsoft.PowerShell_8wekyb3d8bbwe": { + "pwsh.exe": "", + }, + "Microsoft.PowerShellPreview_8wekyb3d8bbwe": { + "pwsh.exe": "", + }, + }, + "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0": { + "powershell.exe": "", + }, + }, + }, + { + name: "Windows 32-bit, 32-bit VSCode (Windows PowerShell only)", + platformDetails: { operatingSystem: platform.OperatingSystem.Windows, isOS64Bit: false, isProcess64Bit: false, - }; + }, + environmentVars: { + "ProgramFiles": "C:\\Program Files (x86)", + "ProgramFiles(x86)": "C:\\Program Files (x86)", + "windir": "C:\\WINDOWS", + }, + expectedPowerShellSequence: [ + { + exePath: "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + displayName: "Windows PowerShell (x86)", + }, + ], + filesystem: { + "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0": { + "powershell.exe": "", + }, + }, + }, + ]; +} else { + successTestCases = [ + { + name: "Linux (all installations)", + platformDetails: { + operatingSystem: platform.OperatingSystem.Linux, + isOS64Bit: true, + isProcess64Bit: true, + }, + expectedPowerShellSequence: [ + { exePath: "/usr/bin/pwsh", displayName: "PowerShell" }, + { exePath: "/snap/bin/pwsh", displayName: "PowerShell Snap" }, + { exePath: "/usr/bin/pwsh-preview", displayName: "PowerShell Preview" }, + { exePath: "/snap/bin/pwsh-preview", displayName: "PowerShell Preview Snap" }, + ], + filesystem: { + "/usr/bin": { + "pwsh": "", + "pwsh-preview": "", + }, + "/snap/bin": { + "pwsh": "", + "pwsh-preview": "", + }, + }, + }, + { + name: "MacOS (all installations)", + platformDetails: { + operatingSystem: platform.OperatingSystem.MacOS, + isOS64Bit: true, + isProcess64Bit: true, + }, + expectedPowerShellSequence: [ + { exePath: "/usr/local/bin/pwsh", displayName: "PowerShell" }, + { exePath: "/usr/local/bin/pwsh-preview", displayName: "PowerShell Preview" }, + ], + filesystem: { + "/usr/local/bin": { + "pwsh": "", + "pwsh-preview": "", + }, + }, + }, + { + name: "Linux (stable only)", + platformDetails: { + operatingSystem: platform.OperatingSystem.Linux, + isOS64Bit: true, + isProcess64Bit: true, + }, + expectedPowerShellSequence: [ + { exePath: "/usr/bin/pwsh", displayName: "PowerShell" }, + ], + filesystem: { + "/usr/bin": { + pwsh: "", + }, + }, + }, + { + name: "Linux (stable snap only)", + platformDetails: { + operatingSystem: platform.OperatingSystem.Linux, + isOS64Bit: true, + isProcess64Bit: true, + }, + expectedPowerShellSequence: [ + { exePath: "/snap/bin/pwsh", displayName: "PowerShell Snap" }, + ], + filesystem: { + "/snap/bin": { + pwsh: "", + }, + }, + }, + { + name: "MacOS (stable only)", + platformDetails: { + operatingSystem: platform.OperatingSystem.MacOS, + isOS64Bit: true, + isProcess64Bit: true, + }, + expectedPowerShellSequence: [ + { exePath: "/usr/local/bin/pwsh", displayName: "PowerShell" }, + ], + filesystem: { + "/usr/local/bin": { + pwsh: "", + }, + }, + }, + ]; +} - checkDefaultPowerShellPath( - platformDetails, - "C:\\Program Files\\PowerShell\\6\\pwsh.exe"); +const errorTestCases: ITestPlatform[] = [ + { + name: "Linux (no PowerShell)", + platformDetails: { + operatingSystem: platform.OperatingSystem.Linux, + isOS64Bit: true, + isProcess64Bit: true, + }, + filesystem: {}, + }, + { + name: "MacOS (no PowerShell)", + platformDetails: { + operatingSystem: platform.OperatingSystem.MacOS, + isOS64Bit: true, + isProcess64Bit: true, + }, + filesystem: {}, + }, +]; - checkAvailableWindowsPowerShellPaths( - platformDetails, - [ - { - versionName: platform.WindowsPowerShell32BitLabel, - exePath: platform.System32PowerShellPath, - }, - ]); - }); +function setupTestEnvironment(testPlatform: ITestPlatform) { + mockFS(testPlatform.filesystem); + + if (testPlatform.environmentVars) { + for (const envVar of Object.keys(testPlatform.environmentVars)) { + sinon.stub(process.env, envVar).value(testPlatform.environmentVars[envVar]); + } } +} + +suite("Platform module", () => { + suite("PlatformDetails", () => { + const platformDetails: platform.IPlatformDetails = platform.getPlatformDetails(); + switch (process.platform) { + case "darwin": + assert.strictEqual( + platformDetails.operatingSystem, + platform.OperatingSystem.MacOS, + "Platform details operating system should be MacOS"); + assert.strictEqual( + platformDetails.isProcess64Bit, + true, + "VSCode on darwin should be 64-bit"); + assert.strictEqual( + platformDetails.isOS64Bit, + true, + "Darwin is 64-bit only"); + break; + + case "linux": + assert.strictEqual( + platformDetails.operatingSystem, + platform.OperatingSystem.Linux, + "Platform details operating system should be Linux"); + assert.strictEqual( + platformDetails.isProcess64Bit, + true, + "Only 64-bit VSCode supported on Linux"); + assert.strictEqual( + platformDetails.isOS64Bit, + true, + "Only 64-bit Linux supported by PowerShell"); + return; + + case "win32": + assert.strictEqual( + platformDetails.operatingSystem, + platform.OperatingSystem.Windows, + "Platform details operating system should be Windows"); + assert.strictEqual( + platformDetails.isProcess64Bit, + process.arch === "x64", + "Windows process bitness should match process arch"); + assert.strictEqual( + platformDetails.isOS64Bit, + !!(platformDetails.isProcess64Bit || process.env.ProgramW6432), + "Windows OS arch should match process bitness unless 64-bit env var set"); + return; + + default: + assert.fail("Tests run on unsupported platform"); + } + }); + + suite("Default PowerShell installation", () => { + teardown(() => { + sinon.restore(); + mockFS.restore(); + }); + + for (const testPlatform of successTestCases) { + test(`Default PowerShell path on ${testPlatform.name}`, () => { + setupTestEnvironment(testPlatform); + + const powerShellExeFinder = new platform.PowerShellExeFinder(testPlatform.platformDetails); + + const defaultPowerShell = powerShellExeFinder.getFirstAvailablePowerShellInstallation(); + const expectedPowerShell = testPlatform.expectedPowerShellSequence[0]; + + assert.strictEqual(defaultPowerShell.exePath, expectedPowerShell.exePath); + assert.strictEqual(defaultPowerShell.displayName, expectedPowerShell.displayName); + }); + } + + for (const testPlatform of errorTestCases) { + test(`Extension startup fails gracefully on ${testPlatform.name}`, () => { + setupTestEnvironment(testPlatform); + + const powerShellExeFinder = new platform.PowerShellExeFinder(testPlatform.platformDetails); + + const defaultPowerShell = powerShellExeFinder.getFirstAvailablePowerShellInstallation(); + assert.strictEqual(defaultPowerShell, undefined); + }); + } + }); + + suite("Expected PowerShell installation list", () => { + teardown(() => { + sinon.restore(); + mockFS.restore(); + }); + + for (const testPlatform of successTestCases) { + test(`PowerShell installation list on ${testPlatform.name}`, () => { + setupTestEnvironment(testPlatform); + + const powerShellExeFinder = new platform.PowerShellExeFinder(testPlatform.platformDetails); + + const foundPowerShells = powerShellExeFinder.getAllAvailablePowerShellInstallations(); + + for (let i = 0; i < testPlatform.expectedPowerShellSequence.length; i++) { + const foundPowerShell = foundPowerShells[i]; + const expectedPowerShell = testPlatform.expectedPowerShellSequence[i]; + + assert.strictEqual(foundPowerShell && foundPowerShell.exePath, expectedPowerShell.exePath); + assert.strictEqual(foundPowerShell && foundPowerShell.displayName, expectedPowerShell.displayName); + } + + assert.strictEqual( + foundPowerShells.length, + testPlatform.expectedPowerShellSequence.length, + "Number of expected PowerShells found does not match"); + }); + } + + for (const testPlatform of errorTestCases) { + test(`Extension startup fails gracefully on ${testPlatform.name}`, () => { + setupTestEnvironment(testPlatform); + + const powerShellExeFinder = new platform.PowerShellExeFinder(testPlatform.platformDetails); + + const foundPowerShells = powerShellExeFinder.getAllAvailablePowerShellInstallations(); + assert.strictEqual(foundPowerShells.length, 0); + }); + } + }); });