import * as child_process from 'child_process' ; import * as os from 'os' ; import * as path from 'path' ; import { EnvironmentMap } from './EnvironmentMap' ; import { FileSystem } from './FileSystem' ; import { PosixModeBits } from './PosixModeBits' ; public export type ExecutableStdioStreamMapping = 'pipe' | 'ignore' | 'inherit' | NodeJS . WritableStream | NodeJS . ReadableStream | number | undefined ; /**
 * Types for  IExecutableSpawnSyncOptions . stdio * and  IExecutableSpawnOptions . stdio public export type ExecutableStdioMapping = 'pipe' | 'ignore' | 'inherit' | ExecutableStdioStreamMapping [ ] ; public export interface IExecutableResolveOptions { /**
     * The current working directory.  If omitted, process.cwd() will be used.
     */ currentWorkingDirectory ? : string ; remarks environment ? : NodeJS . ProcessEnv ; remarks environmentMap ? : EnvironmentMap ; } /**
 * Options for  Executable . spawnSync public export interface IExecutableSpawnSyncOptions extends IExecutableResolveOptions { /**
     * The content to be passed to the child process's stdin.
     *
     * NOTE: If specified, this content replaces any IExecutableSpawnSyncOptions.stdio[0]
     * mapping for stdin.
     */ input ? : string ; /**
     * The stdio mappings for the child process.
     *
     * NOTE: If IExecutableSpawnSyncOptions.input is provided, it will take precedence
     * over the stdin mapping (stdio[0]).
     */ stdio ? : ExecutableStdioMapping ; /**
     * The maximum time the process is allowed to run before it will be terminated.
     */ timeoutMs ? : number ; /**
     * The largest amount of bytes allowed on stdout or stderr for this synchronous operation.
     * If exceeded, the child process will be terminated.  The default is 200 * 1024.
     */ maxBuffer ? : number ; } /**
 * Options for  Executable . spawn public export interface IExecutableSpawnOptions extends IExecutableResolveOptions { /**
     * The stdio mappings for the child process.
     *
     * NOTE: If IExecutableSpawnSyncOptions.input is provided, it will take precedence
     * over the stdin mapping (stdio[0]).
     */ stdio ? : ExecutableStdioMapping ; } interface IExecutableContext { currentWorkingDirectory : string ; environmentMap : EnvironmentMap ; windowsExecutableExtensions : string [ ] ; } interface ICommandLineFixup { path : string ; args : string [ ] ; } remarks public export class Executable { remarks param filename param args param options returns privateRemarks public static spawnSync ( filename , args , options ? ) { if ( ! options ) { options = { } ; } const context = Executable . _getExecutableContext ( options ) ; const resolvedPath = Executable . _tryResolve ( filename , options , context ) ; if ( ! resolvedPath ) { throw new Error ( `The executable file was not found: "${ filename }"` ) ; } const spawnOptions = { cwd : context . currentWorkingDirectory , env : context . environmentMap . toObject ( ) , input : options . input , stdio : options . stdio as child_process . StdioOptions , timeout : options . timeoutMs , maxBuffer : options . maxBuffer , encoding : 'utf8' , shell : false } ; const normalizedCommandLine = Executable . _buildCommandLineFixup ( resolvedPath , args , context ) ; return child_process . spawnSync ( normalizedCommandLine . path , normalizedCommandLine . args , spawnOptions ) ; } remarks param filename param args param options returns public static spawn ( filename , args , options ? ) { if ( ! options ) { options = { } ; } const context = Executable . _getExecutableContext ( options ) ; const resolvedPath = Executable . _tryResolve ( filename , options , context ) ; if ( ! resolvedPath ) { throw new Error ( `The executable file was not found: "${ filename }"` ) ; } const spawnOptions = { cwd : context . currentWorkingDirectory , env : context . environmentMap . toObject ( ) , stdio : options . stdio as child_process . StdioOptions , shell : false } ; const normalizedCommandLine = Executable . _buildCommandLineFixup ( resolvedPath , args , context ) ; return child_process . spawn ( normalizedCommandLine . path , normalizedCommandLine . args , spawnOptions ) ; } private static _buildCommandLineFixup ( resolvedPath , args , context ) { const fileExtension = path . extname ( resolvedPath ) ; if ( os . platform ( ) === 'win32' ) { switch ( fileExtension . toUpperCase ( ) ) { case '.EXE' : case '.COM' : break ; case '.BAT' : case '.CMD' : { Executable . _validateArgsForWindowsShell ( args ) ; let shellPath = context . environmentMap . get ( 'COMSPEC' ) ; if ( ! shellPath || ! Executable . _canExecute ( shellPath , context ) ) { shellPath = Executable . tryResolve ( 'cmd.exe' ) ; } if ( ! shellPath ) { throw new Error ( `Unable to execute "${ path . basename ( resolvedPath ) }" ` + `because CMD.exe was not found in the PATH` ) ; } const shellArgs = [ ] ; shellArgs . push ( '/d' ) ; shellArgs . push ( '/s' ) ; shellArgs . push ( '/c' ) ; shellArgs . push ( Executable . _getEscapedForWindowsShell ( resolvedPath ) ) ; shellArgs . push ( ... args ) ; return { path : shellPath , args : shellArgs } ; } default : throw new Error ( `Cannot execute "${ path . basename ( resolvedPath ) }" because the file type is not supported` ) ; } } return { path : resolvedPath , args : args } ; } remarks param filename param options returns public static tryResolve ( filename , options ? ) { return Executable . _tryResolve ( filename , options || { } , Executable . _getExecutableContext ( options ) ) ; } private static _tryResolve ( filename , options , context ) { const hasPathSeparators = filename . indexOf ( '/' ) >= 0 || ( os . platform ( ) === 'win32' && filename . indexOf ( '\\' ) >= 0 ) ; if ( hasPathSeparators ) { const resolvedPath = path . resolve ( context . currentWorkingDirectory , filename ) ; return Executable . _tryResolveFileExtension ( resolvedPath , context ) ; } else { const pathsToSearch = Executable . _getSearchFolders ( context ) ; for ( const pathToSearch of pathsToSearch ) { const resolvedPath = path . join ( pathToSearch , filename ) ; const result = Executable . _tryResolveFileExtension ( resolvedPath , context ) ; if ( result ) { return result ; } } return undefined ; } } private static _tryResolveFileExtension ( resolvedPath , context ) { if ( Executable . _canExecute ( resolvedPath , context ) ) { return resolvedPath ; } for ( const shellExtension of context . windowsExecutableExtensions ) { const resolvedNameWithExtension = resolvedPath + shellExtension ; if ( Executable . _canExecute ( resolvedNameWithExtension , context ) ) { return resolvedNameWithExtension ; } } return undefined ; } private static _buildEnvironmentMap ( options ) { const environmentMap = new EnvironmentMap ( ) ; if ( options . environment !== undefined && options . environmentMap !== undefined ) { throw new Error ( 'IExecutableResolveOptions.environment and IExecutableResolveOptions.environmentMap' + ' cannot both be specified' ) ; } if ( options . environment !== undefined ) { environmentMap . mergeFromObject ( options . environment ) ; } else if ( options . environmentMap !== undefined ) { environmentMap . mergeFrom ( options . environmentMap ) ; } else { environmentMap . mergeFromObject ( process . env ) ; } return environmentMap ; } /**
     * This is used when searching the shell PATH for an executable, to determine
     * whether a match should be skipped or not.  If it returns true, this does not
     * guarantee that the file can be successfully executed.
     */ private static _canExecute ( filePath , context ) { if ( ! FileSystem . exists ( filePath ) ) { return false ; } if ( os . platform ( ) === 'win32' ) { if ( path . extname ( filePath ) === '' ) { return false ; } } else { try { if ( ( FileSystem . getPosixModeBits ( filePath ) & PosixModeBits . AllExecute ) === 0 ) { return false ; } } catch ( error ) { } } return true ; } /**
     * Returns the list of folders where we will search for an executable,
     * based on the PATH environment variable.
     */ private static _getSearchFolders ( context ) { const pathList = context . environmentMap . get ( 'PATH' ) || '' ; const folders = [ ] ; const seenPaths = new Set < string > ( ) ; for ( const splitPath of pathList . split ( path . delimiter ) ) { const trimmedPath = splitPath . trim ( ) ; if ( trimmedPath !== '' ) { if ( ! seenPaths . has ( trimmedPath ) ) { const resolvedPath = path . resolve ( context . currentWorkingDirectory , trimmedPath ) ; if ( ! seenPaths . has ( resolvedPath ) ) { if ( FileSystem . exists ( resolvedPath ) ) { folders . push ( resolvedPath ) ; } seenPaths . add ( resolvedPath ) ; } seenPaths . add ( trimmedPath ) ; } } } return folders ; } private static _getExecutableContext ( options ) { if ( ! options ) { options = { } ; } const environment = Executable . _buildEnvironmentMap ( options ) ; let currentWorkingDirectory ; if ( options . currentWorkingDirectory ) { currentWorkingDirectory = path . resolve ( options . currentWorkingDirectory ) ; } else { currentWorkingDirectory = process . cwd ( ) ; } const windowsExecutableExtensions = [ ] ; if ( os . platform ( ) === 'win32' ) { const pathExtVariable = environment . get ( 'PATHEXT' ) || '' ; for ( const splitValue of pathExtVariable . split ( ';' ) ) { const trimmed = splitValue . trim ( ) . toLowerCase ( ) ; if ( /^\.[a-z0-9\.]*[a-z0-9]$/i . test ( trimmed ) ) { if ( windowsExecutableExtensions . indexOf ( trimmed ) < 0 ) { windowsExecutableExtensions . push ( trimmed ) ; } } } } return { environmentMap : environment , currentWorkingDirectory , windowsExecutableExtensions } ; } /**
     * Given an input string containing special symbol characters, this inserts the "^" escape
     * character to ensure the symbols are interpreted literally by the Windows shell.
     */ private static _getEscapedForWindowsShell ( text ) { const escapableCharRegExp = /[%\^&|<> ]/g ; return text . replace ( escapableCharRegExp , ( value ) => '^' + value ) ; } /**
     * Checks for characters that are unsafe to pass to a Windows batch file
     * due to the way that cmd.exe implements escaping.
     */ private static _validateArgsForWindowsShell ( args ) { const specialCharRegExp = /[%\^&|<>\r\n]/g ; for ( const arg of args ) { const match = arg . match ( specialCharRegExp ) ; if ( match ) { throw new Error ( `The command line argument ${ JSON . stringify ( arg ) } contains a` + ` special character ${ JSON . stringify ( match [ 0 ] ) } that cannot be escaped for the Windows shell` ) ; } } } }