import * as fs from 'fs' ; import * as os from 'os' ; import * as cp from 'child_process' ; import * as paths from 'path' ; import * as ps from './powershell' ; const ENOENT = 'ENOENT' ; export interface ITerminalProfile { profileName : string ; path : string ; args ? : string | string [ ] | undefined ; env ? : ITerminalEnvironment ; } export interface ITerminalEnvironment { [ key ] : string | null | undefined ; } export const enum ProfileSource { GitBash = 'Git Bash' , Pwsh = 'PowerShell' } export interface ITerminalExecutable { path : string | string [ ] ; args ? : string | string [ ] | undefined ; env ? : ITerminalEnvironment ; } export interface ITerminalProfileSource { source : ProfileSource ; args ? : string | string [ ] | undefined ; env ? : ITerminalEnvironment ; } export type ITerminalProfileObject = ITerminalExecutable | ITerminalProfileSource | null ; export function exists ( filename ) { return new Promise ( ( resolve , reject ) => { fs . access ( filename , fs . constants . F_OK , ( err ) => { if ( err ) { if ( err . code === ENOENT ) { return resolve ( false ) ; } else { return reject ( err ) ; } } else { resolve ( true ) ; } } ) ; } ) ; } let profileSources ; export function getWindowsBuildNumber ( ) { const osVersion = ( /(\d+)\.(\d+)\.(\d+)/g ) . exec ( os . release ( ) ) ; let buildNumber = 0 ; if ( osVersion && osVersion . length === 4 ) { buildNumber = parseInt ( osVersion [ 3 ] ) ; } return buildNumber ; } export function getCaseInsensitive ( target , key ) { const lowercaseKey = key . toLowerCase ( ) ; const equivalentKey = Object . keys ( target ) . find ( k => k . toLowerCase ( ) === lowercaseKey ) ; return equivalentKey ? target [ equivalentKey ] : target [ key ] ; } export async function findExecutable ( command , cwd ? , pathsToCheck ? , env = process . env as ITerminalEnvironment ) { if ( paths . isAbsolute ( command ) ) { return await exists ( command ) ? command : undefined ; } if ( cwd === undefined ) { cwd = process . cwd ( ) ; } const dir = paths . dirname ( command ) ; if ( dir !== '.' ) { const fullPath = paths . join ( cwd , command ) ; return await exists ( fullPath ) ? fullPath : undefined ; } const envPath = getCaseInsensitive ( env , 'PATH' ) ; if ( pathsToCheck === undefined && typeof envPath === 'string' ) { pathsToCheck = envPath . split ( paths . delimiter ) ; } if ( pathsToCheck === undefined || pathsToCheck . length === 0 ) { const fullPath = paths . join ( cwd , command ) ; return await exists ( fullPath ) ? fullPath : undefined ; } for ( let pathEntry of pathsToCheck ) { let fullPath ; if ( paths . isAbsolute ( pathEntry ) ) { fullPath = paths . join ( pathEntry , command ) ; } else { fullPath = paths . join ( cwd , pathEntry , command ) ; } if ( await exists ( fullPath ) ) { return fullPath ; } if ( process . platform === 'win32' ) { let withExtension = fullPath + '.com' ; if ( await exists ( withExtension ) ) { return withExtension ; } withExtension = fullPath + '.exe' ; if ( await exists ( withExtension ) ) { return withExtension ; } } } const fullPath = paths . join ( cwd , command ) ; return await exists ( fullPath ) ? fullPath : undefined ; } export function detectAvailableProfiles ( ) { if ( process . platform === 'win32' ) { return detectAvailableWindowsProfiles ( ) ; } return detectAvailableUnixProfiles ( ) ; } async function detectAvailableWindowsProfiles ( ) { const is32ProcessOn64Windows = process . env . hasOwnProperty ( 'PROCESSOR_ARCHITEW6432' ) ; const system32Path = `${ process . env [ 'windir' ] }\\${ is32ProcessOn64Windows ? 'Sysnative' : 'System32' }` ; let useWSLexe = false ; if ( getWindowsBuildNumber ( ) >= 16299 ) { useWSLexe = true ; } await initializeWindowsProfiles ( ) ; const detectedProfiles = new Map ( ) ; detectedProfiles . set ( 'PowerShell' , { source : ProfileSource . Pwsh , } ) ; detectedProfiles . set ( 'Windows PowerShell' , { path : `${ system32Path }\\WindowsPowerShell\\v1.0\\powershell.exe` , } ) ; detectedProfiles . set ( 'Git Bash' , { source : ProfileSource . GitBash } ) ; detectedProfiles . set ( 'Cygwin' , { path : [ `${ process . env [ 'HOMEDRIVE' ] }\\cygwin64\\bin\\bash.exe` , `${ process . env [ 'HOMEDRIVE' ] }\\cygwin\\bin\\bash.exe` ] , args : [ '--login' ] , } ) ; detectedProfiles . set ( 'Command Prompt' , { path : `${ system32Path }\\cmd.exe` , } ) ; const resultProfiles = await transformToTerminalProfiles ( detectedProfiles . entries ( ) ) ; try { const result = await getWslProfiles ( `${ system32Path }\\${ useWSLexe ? 'wsl.exe' : 'bash.exe' }` , useWSLexe ) ; if ( result ) { resultProfiles . push ( ... result ) ; } } catch ( e ) { console . info ( 'WSL is not installed, so could not detect WSL profiles' ) ; } return resultProfiles ; } async function transformToTerminalProfiles ( entries ) { const resultProfiles = [ ] ; for ( const [ profileName , profile ] of entries ) { if ( profile === null ) { continue ; } let originalPaths ; let args ; if ( 'source' in profile ) { const source = profileSources ?. get ( profile . source ) ; if ( ! source ) { continue ; } originalPaths = source . paths ; args = profile . args || source . args ; } else { originalPaths = Array . isArray ( profile . path ) ? profile . path : [ profile . path ] ; args = process . platform === 'win32' ? profile . args : Array . isArray ( profile . args ) ? profile . args : undefined ; } const paths = originalPaths . slice ( ) ; const validatedProfile = await validateProfilePaths ( profileName , paths , args , profile . env ) ; if ( validatedProfile ) { resultProfiles . push ( validatedProfile ) ; } } return resultProfiles ; } async function initializeWindowsProfiles ( ) { if ( profileSources ) { return ; } profileSources = new Map ( ) ; profileSources . set ( 'Git Bash' , { profileName : 'Git Bash' , paths : [ `${ process . env [ 'ProgramW6432' ] }\\Git\\bin\\bash.exe` , `${ process . env [ 'ProgramW6432' ] }\\Git\\usr\\bin\\bash.exe` , `${ process . env [ 'ProgramFiles' ] }\\Git\\bin\\bash.exe` , `${ process . env [ 'ProgramFiles' ] }\\Git\\usr\\bin\\bash.exe` , `${ process . env [ 'LocalAppData' ] }\\Programs\\Git\\bin\\bash.exe` ] , args : [ '--login' ] } ) ; profileSources . set ( 'PowerShell' , { profileName : 'PowerShell' , paths : await getPowershellPaths ( ) , } ) ; } async function getPowershellPaths ( ) { const paths = [ ] ; for await ( const pwshExe of ps . enumeratePowerShellInstallations ( ) ) { paths . push ( pwshExe . exePath ) ; } return paths ; } async function getWslProfiles ( wslPath , useWslProfiles ? ) { const profiles = [ ] ; if ( useWslProfiles ) { const distroOutput = await new Promise < string > ( ( resolve , reject ) => { cp . exec ( 'wsl.exe -l -q' , { encoding : 'utf16le' } , ( err , stdout ) => { if ( err ) { return reject ( 'Problem occurred when getting wsl distros' ) ; } resolve ( stdout ) ; } ) ; } ) ; if ( distroOutput ) { const regex = new RegExp ( /[\r?\n]/ ) ; const distroNames = distroOutput . split ( regex ) . filter ( t => t . trim ( ) . length > 0 && t !== '' ) ; for ( let distroName of distroNames ) { if ( distroName === '' ) { continue ; } if ( distroName . startsWith ( 'docker-desktop' ) ) { continue ; } const profile = { profileName : `${ distroName } (WSL)` , path : wslPath , args : [ `-d` , `${ distroName }` ] } ; profiles . push ( profile ) ; } return profiles ; } } return [ ] ; } async function detectAvailableUnixProfiles ( ) { const detectedProfiles = new Map ( ) ; const contents = await fs . promises . readFile ( '/etc/shells' , 'utf8' ) ; const profiles = contents . split ( '\n' ) . filter ( e => e . trim ( ) . indexOf ( '#' ) !== 0 && e . trim ( ) . length > 0 ) ; const counts = new Map ( ) ; for ( const profile of profiles ) { let profileName = paths . basename ( profile ) ; let count = counts . get ( profileName ) || 0 ; count ++ ; if ( count > 1 ) { profileName = `${ profileName } (${ count })` ; } counts . set ( profileName , count ) ; detectedProfiles . set ( profileName , { path : profile } ) ; } return await transformToTerminalProfiles ( detectedProfiles . entries ( ) ) ; } function applyConfigProfilesToMap ( configProfiles , profilesMap ) { if ( ! configProfiles ) { return ; } for ( const [ profileName , value ] of Object . entries ( configProfiles ) ) { if ( value === null || ( ! ( 'path' in value ) && ! ( 'source' in value ) ) ) { profilesMap . delete ( profileName ) ; } else { profilesMap . set ( profileName , value ) ; } } } async function validateProfilePaths ( profileName , potentialPaths , args ? , env ? ) { if ( potentialPaths . length === 0 ) { return Promise . resolve ( undefined ) ; } const path = potentialPaths . shift ( ) ! ; if ( path === '' ) { return validateProfilePaths ( profileName , potentialPaths , args , env ) ; } const profile = { profileName , path , args , env } ; if ( paths . basename ( path ) === path ) { const envPaths = process . env . PATH ? process . env . PATH . split ( paths . delimiter ) : undefined ; const executable = await findExecutable ( path , undefined , envPaths , undefined ) ; if ( ! executable ) { return validateProfilePaths ( profileName , potentialPaths , args ) ; } return profile ; } const result = await exists ( paths . normalize ( path ) ) ; if ( result ) { return profile ; } return validateProfilePaths ( profileName , potentialPaths , args , env ) ; } export interface IFsProvider { existsFile ( path ) : Promise < boolean > ; readFile ( path , options ) : Promise < string > ; } interface IPotentialTerminalProfile { profileName : string ; paths : string [ ] ; args ? : string [ ] ; }