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 WindowsPowerShell64BitLabel = "Windows PowerShell (x64)" ; const WindowsPowerShell32BitLabel = "Windows PowerShell (x86)" ; const LinuxExePath = "/usr/bin/pwsh" ; const LinuxPreviewExePath = "/usr/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 , Windows , MacOS , Linux } export interface IPlatformDetails { operatingSystem : OperatingSystem ; isOS64Bit : boolean ; isProcess64Bit : boolean ; } export interface IPowerShellExeDetails { readonly displayName : string ; readonly exePath : string ; } export function getPlatformDetails ( ) { let operatingSystem = OperatingSystem . Unknown ; if ( process . platform === "win32" ) { operatingSystem = OperatingSystem . Windows ; } else if ( process . platform === "darwin" ) { operatingSystem = OperatingSystem . MacOS ; } else if ( process . platform === "linux" ) { operatingSystem = OperatingSystem . Linux ; } const isProcess64Bit = process . arch === "x64" ; return { operatingSystem , isOS64Bit : isProcess64Bit || process . env . hasOwnProperty ( "PROCESSOR_ARCHITEW6432" ) , isProcess64Bit , } ; } /**
 * 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 class PowerShellExeFinder { private static IntRegex : RegExp = /^\d+$/ ; private static PwshMsixRegex : RegExp = /^Microsoft.PowerShell_.*/ ; private static PwshPreviewMsixRegex : RegExp = /^Microsoft.PowerShellPreview_.*/ ; private readonly platformDetails : IPlatformDetails ; private readonly additionalPSExeSettings : Iterable < IPowerShellAdditionalExePathSettings > ; private winPS : IPossiblePowerShellExe ; private alternateBitnessWinPS : IPossiblePowerShellExe ; param platformDetails param additionalPowerShellExes constructor ( platformDetails ? , additionalPowerShellExes ? ) { this . platformDetails = platformDetails || getPlatformDetails ( ) ; this . additionalPSExeSettings = additionalPowerShellExes || [ ] ; } /**
     * Returns the first available PowerShell executable found in the search order.
     */ public getFirstAvailablePowerShellInstallation ( ) { for ( const pwsh of this . enumeratePowerShellInstallations ( ) ) { return pwsh ; } throw new Error ( "No available PowerShell executables on this machine." ) ; } /**
     * Get an array of all PowerShell executables found when searching for PowerShell installations.
     */ public getAllAvailablePowerShellInstallations ( ) { return Array . from ( this . enumeratePowerShellInstallations ( ) ) ; } param configuredPowerShellPath public fixWindowsPowerShellPath ( configuredPowerShellPath ) { const altWinPS = this . findWinPS ( { useAlternateBitness : true } ) ; if ( ! altWinPS ) { return configuredPowerShellPath ; } const lowerAltWinPSPath = altWinPS . exePath . toLocaleLowerCase ( ) ; const lowerConfiguredPath = configuredPowerShellPath . toLocaleLowerCase ( ) ; if ( lowerConfiguredPath === lowerAltWinPSPath ) { return this . findWinPS ( ) . 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 ( ) { for ( const defaultPwsh of this . enumerateDefaultPowerShellInstallations ( ) ) { if ( defaultPwsh && defaultPwsh . exists ( ) ) { yield defaultPwsh ; } } for ( const additionalPwsh of this . enumerateAdditionalPowerShellInstallations ( ) ) { if ( additionalPwsh && additionalPwsh . exists ( ) ) { yield additionalPwsh ; } } } /**
     * 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 ( ) { yield this . findPSCoreStable ( ) ; switch ( this . platformDetails . operatingSystem ) { case OperatingSystem . Linux : yield this . findPSCoreStableSnap ( ) ; break ; case OperatingSystem . Windows : yield this . findPSCoreWindowsInstallation ( { useAlternateBitness : true } ) ; yield this . findPSCoreMsix ( ) ; break ; } yield this . findPSCoreDotnetGlobalTool ( ) ; yield this . findPSCorePreview ( ) ; switch ( this . platformDetails . operatingSystem ) { case OperatingSystem . Linux : yield this . findPSCorePreviewSnap ( ) ; break ; case OperatingSystem . Windows : yield this . findPSCoreMsix ( { findPreview : true } ) ; yield this . findPSCoreWindowsInstallation ( { useAlternateBitness : true , findPreview : true } ) ; yield this . findWinPS ( ) ; yield this . findWinPS ( { useAlternateBitness : true } ) ; break ; } } /**
     * Iterates through the configured additonal PowerShell executable locations,
     * without checking for their existence.
     */ private * enumerateAdditionalPowerShellInstallations ( ) { for ( const additionalPwshSetting of this . additionalPSExeSettings ) { yield new PossiblePowerShellExe ( additionalPwshSetting . exePath , additionalPwshSetting . versionName ) ; } } private findPSCoreStable ( ) { switch ( this . platformDetails . operatingSystem ) { case OperatingSystem . Linux : return new PossiblePowerShellExe ( LinuxExePath , "PowerShell" ) ; case OperatingSystem . MacOS : return new PossiblePowerShellExe ( MacOSExePath , "PowerShell" ) ; case OperatingSystem . Windows : return this . findPSCoreWindowsInstallation ( ) ; case OperatingSystem . Unknown : throw new Error ( "Unable to detect operating system" ) ; } } private findPSCorePreview ( ) { switch ( this . platformDetails . operatingSystem ) { case OperatingSystem . Linux : return new PossiblePowerShellExe ( LinuxPreviewExePath , "PowerShell Preview" ) ; case OperatingSystem . MacOS : return new PossiblePowerShellExe ( MacOSPreviewExePath , "PowerShell Preview" ) ; case OperatingSystem . Windows : return this . findPSCoreWindowsInstallation ( { findPreview : true } ) ; case OperatingSystem . Unknown : throw new Error ( "Unable to detect operating system" ) ; } } private findPSCoreDotnetGlobalTool ( ) { const exeName = this . platformDetails . operatingSystem === OperatingSystem . Windows ? "pwsh.exe" : "pwsh" ; const dotnetGlobalToolExePath = path . join ( os . homedir ( ) , ".dotnet" , "tools" , exeName ) ; return new PossiblePowerShellExe ( dotnetGlobalToolExePath , ".NET Core PowerShell Global Tool" ) ; } private findPSCoreMsix ( { findPreview } = { } ) { if ( ! process . env . LOCALAPPDATA ) { return null ; } const msixAppDir = path . join ( process . env . LOCALAPPDATA , "Microsoft" , "WindowsApps" ) ; if ( ! fs . existsSync ( msixAppDir ) ) { return null ; } const { pwshMsixDirRegex , pwshMsixName } = findPreview ? { pwshMsixDirRegex : PowerShellExeFinder . PwshPreviewMsixRegex , pwshMsixName : "PowerShell Preview (Store)" } : { pwshMsixDirRegex : PowerShellExeFinder . PwshMsixRegex , pwshMsixName : "PowerShell (Store)" } ; for ( const subdir of fs . readdirSync ( msixAppDir ) ) { if ( pwshMsixDirRegex . test ( subdir ) ) { const pwshMsixPath = path . join ( msixAppDir , subdir , "pwsh.exe" ) ; return new PossiblePowerShellExe ( pwshMsixPath , pwshMsixName ) ; } } return null ; } private findPSCoreStableSnap ( ) { return new PossiblePowerShellExe ( SnapExePath , "PowerShell Snap" ) ; } private findPSCorePreviewSnap ( ) { return new PossiblePowerShellExe ( SnapPreviewExePath , "PowerShell Preview Snap" ) ; } private findPSCoreWindowsInstallation ( { useAlternateBitness = false , findPreview = false } = { } ) { const programFilesPath = this . getProgramFilesPath ( { useAlternateBitness } ) ; if ( ! programFilesPath ) { return null ; } const powerShellInstallBaseDir = path . join ( programFilesPath , "PowerShell" ) ; if ( ! ( fs . existsSync ( powerShellInstallBaseDir ) && fs . lstatSync ( powerShellInstallBaseDir ) . isDirectory ( ) ) ) { return null ; } let highestSeenVersion = - 1 ; let pwshExePath = null ; for ( const item of fs . readdirSync ( powerShellInstallBaseDir ) ) { let currentVersion = - 1 ; if ( findPreview ) { const dashIndex = item . indexOf ( "-" ) ; if ( dashIndex < 0 ) { continue ; } const intPart = item . substring ( 0 , dashIndex ) ; if ( ! PowerShellExeFinder . IntRegex . test ( intPart ) ) { continue ; } if ( item . substring ( dashIndex + 1 ) !== "preview" ) { continue ; } currentVersion = parseInt ( intPart , 10 ) ; } else { if ( ! PowerShellExeFinder . IntRegex . test ( item ) ) { continue ; } currentVersion = parseInt ( item , 10 ) ; } if ( currentVersion <= highestSeenVersion ) { continue ; } const exePath = path . join ( powerShellInstallBaseDir , item , "pwsh.exe" ) ; if ( ! fs . existsSync ( exePath ) ) { continue ; } pwshExePath = exePath ; highestSeenVersion = currentVersion ; } if ( ! pwshExePath ) { return null ; } const bitness = programFilesPath . includes ( "x86" ) ? "(x86)" : "(x64)" ; const preview = findPreview ? " Preview" : "" ; return new PossiblePowerShellExe ( pwshExePath , `PowerShell${ preview } ${ bitness }` ) ; } private findWinPS ( { useAlternateBitness = false } = { } ) { if ( ! this . platformDetails . isOS64Bit && useAlternateBitness ) { return null ; } let winPS = useAlternateBitness ? this . alternateBitnessWinPS : this . winPS ; if ( winPS === undefined ) { const systemFolderPath = this . getSystem32Path ( { useAlternateBitness } ) ; const winPSPath = path . join ( systemFolderPath , "WindowsPowerShell" , "v1.0" , "powershell.exe" ) ; let displayName ; 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 ; } private getProgramFilesPath ( { useAlternateBitness = false } = { } ) { if ( ! useAlternateBitness ) { return process . env . ProgramFiles ; } if ( this . platformDetails . isProcess64Bit ) { return process . env [ "ProgramFiles(x86)" ] ; } if ( this . platformDetails . isOS64Bit ) { return process . env . ProgramW6432 ; } return null ; } private getSystem32Path ( { useAlternateBitness = false } = { } ) { const windir = process . env . windir ; if ( ! useAlternateBitness ) { return path . join ( windir , "System32" ) ; } if ( this . platformDetails . isProcess64Bit ) { return path . join ( windir , "SysWOW64" ) ; } if ( this . platformDetails . isOS64Bit ) { return path . join ( windir , "Sysnative" ) ; } return null ; } } export function getWindowsSystemPowerShellPath ( systemFolderName ) { 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 , installationName , { knownToExist = false } = { } ) { this . exePath = pathToExe ; this . displayName = installationName ; this . knownToExist = knownToExist || undefined ; } public exists ( ) { if ( this . knownToExist === undefined ) { this . knownToExist = fs . existsSync ( this . exePath ) ; } return this . knownToExist ; } }