"use strict" ;
require ( "array.prototype.flat" ) . shim ( ) ;
const Promise = require ( "bluebird" ) ;
const util = require ( "util" ) ;
const execFileAsync = util . promisify ( require ( "child_process" ) . execFile ) ;
const debug = require ( "debug" ) ( "cvm:execBinary" ) ;
const asExpression = require ( "as-expression" ) ;
const { rethrowAs , chain } = require ( "error-chain" ) ;
const textParser = require ( "../text-parser" ) ;
const errors = require ( "./errors" ) ;
/* FIXME: How to handle partial result parsing when an error is encountered in the parsing adapter? */
/* FIXME: Test that flag-dash prevention in arguments works */
function keyToFlagName ( key ) {
if ( key . startsWith ( "!" ) ) {
return key . slice ( 1 ) ;
} else if ( key . length === 1 ) {
return ` - ${ key } ` ;
} else {
return ` -- ${ key } ` ;
}
}
function flagValueToArgs ( key , value ) {
if ( value === true ) {
return [ key ] ;
} else if ( Array . isArray ( value ) ) {
return value . map ( ( item ) => {
return flagValueToArgs ( key , item ) ;
} ) . flat ( ) ;
} else {
return [ key , value ] ;
}
}
function flagsToArgs ( flags ) {
return Object . keys ( flags ) . map ( ( key ) => {
let value = flags [ key ] ;
let flagName = keyToFlagName ( key ) ;
return flagValueToArgs ( flagName , value ) ;
} ) . flat ( ) ;
}
function validateArguments ( args ) {
if ( args . some ( ( arg ) => arg == null ) ) {
throw new Error ( "One or more arguments were undefined or null; this is probably a mistake in how you're calling the command" ) ;
} else if ( args . some ( ( arg ) => arg [ 0 ] === "-" ) ) {
throw new Error ( "For security reasons, command arguments cannot start with a dash; use the 'withFlags' method if you want to specify flags" ) ;
}
}
// FIXME: Immutable-builder abstraction
// FIXME: validatem
module . exports = function createBinaryInvocation ( command , args = [ ] ) {
/* FIXME: The below disallows dashes in the args, but not in the command. Is that what we want? */
validateArguments ( args ) ;
return {
_settings : {
asRoot : false ,
expectations : [ ] ,
flags : { } ,
environment : { } ,
expectedExitCodes : [ 0 ] ,
resultMerger : function ( results ) {
return results . reduce ( ( merged , result ) => Object . assign ( merged , result ) , { } ) ;
}
} ,
_withSettings : function ( newSettings ) {
let newObject = Object . assign ( { } , this , {
_settings : Object . assign ( { } , this . _settings , newSettings )
} ) ;
return newObject ;
} ,
_withExpectation : function ( expectation ) {
return this . _withSettings ( {
expectations : this . _settings . expectations . concat ( [ expectation ] )
} ) ;
} ,
asRoot : function ( ) {
return this . _withSettings ( { asRoot : true } ) ;
} ,
withFlags : function ( flags ) {
if ( flags != null ) {
return this . _withSettings ( {
flags : Object . assign ( { } , this . _settings . flags , flags )
} ) ;
} else {
return this ;
}
} ,
withEnvironment : function ( environment ) {
if ( environment != null ) {
return this . _withSettings ( {
environment : Object . assign ( { } , this . _settings . environment , environment )
} ) ;
} else {
return this ;
}
} ,
withModifier : function ( modifierFunction ) {
if ( modifierFunction != null ) {
return modifierFunction ( this ) ;
} else {
return this ;
}
} ,
expectOnStdout : function ( adapter ) {
return this . _withExpectation ( {
channel : "stdout" ,
adapter : adapter
} ) ;
} ,
requireOnStdout : function ( adapter ) {
return this . _withExpectation ( {
channel : "stdout" ,
adapter : adapter ,
required : true
} ) ;
} ,
failOnStdout : function ( adapter ) {
return this . _withExpectation ( {
channel : "stdout" ,
adapter : adapter ,
disallowed : true
} ) ;
} ,
expectOnStderr : function ( adapter ) {
return this . _withExpectation ( {
channel : "stderr" ,
adapter : adapter
} ) ;
} ,
requireOnStderr : function ( adapter ) {
return this . _withExpectation ( {
channel : "stderr" ,
adapter : adapter ,
required : true
} ) ;
} ,
failOnStderr : function ( adapter ) {
return this . _withExpectation ( {
channel : "stderr" ,
adapter : adapter ,
disallowed : true
} ) ;
} ,
failOnAnyStderr : function ( ) {
return this . _withExpectation ( {
channel : "stderr" ,
adapter : null ,
disallowed : true
} ) ;
} ,
then : function ( ) {
throw new Error ( "Attempted to use a command builder as a Promise; you probably forgot to call .execute" ) ;
} ,
execute : function ( ) {
return Promise . try ( ( ) => {
let effectiveCommand = command ;
let effectiveArgs = flagsToArgs ( this . _settings . flags ) . concat ( args ) ;
if ( this . _settings . asRoot ) {
effectiveCommand = "sudo" ;
effectiveArgs = [ command ] . concat ( effectiveArgs ) ;
}
// FIXME: Shouldn't we represent this in its original form, or at least an escaped form? And suffix 'Unsafe' to ensure it's not used in any actual execution code.
let effectiveCompleteCommand = [ effectiveCommand ] . concat ( effectiveArgs ) ;
return Promise . try ( ( ) => {
debug ( ` Running: ${ effectiveCommand } ${ effectiveArgs . map ( ( arg ) => ` " ${ arg } " ` ) . join ( " " ) } ` ) ;
return execFileAsync ( effectiveCommand , effectiveArgs , {
env : Object . assign ( { } , process . env , this . _settings . environment )
} ) ;
} ) . then ( ( { stdout , stderr } ) => {
return { stdout , stderr , exitCode : 0 } ;
} ) . catch ( ( error ) => {
let { stdout , stderr } = error ;
let exitCode = ( typeof error . code === "number" ) ? error . code : null ;
return { stdout , stderr , error , exitCode } ;
} ) . then ( ( { stdout , stderr , error , exitCode } ) => {
try {
let channels = { stdout , stderr } ;
if ( ! this . _settings . expectedExitCodes . includes ( exitCode ) ) {
// FIXME: Can we actually pass `error` to be chained onto here, when there's a case where `error` is undefined? Namely, when requiring a non-zero exit code, but the process exits with 0.
throw chain ( error , errors . NonZeroExitCode , ` Expected exit code to be one of ${ JSON . stringify ( this . _settings . expectedExitCodes ) } , but got ' ${ exitCode } ' ` , {
exitCode : exitCode ,
stdout : stdout ,
stderr : stderr
} ) ;
} else {
let expectationResults = this . _settings . expectations
. map ( ( expectation ) => {
if ( expectation . adapter == null ) {
if ( channels [ expectation . channel ] != null ) {
if ( channels [ expectation . channel ] . length > 0 ) {
throw new errors . UnexpectedOutput ( ` Encountered output on ' ${ expectation . channel } ', but no output was supposed to be produced there ` , {
failedChannel : expectation . channel
} ) ;
} else {
return undefined ;
}
} else {
// FIXME: use @joepie91/unreachable
throw new Error ( ` Encountered expectation for unexpected channel ' ${ expectation . channel } '; this is a bug, please report it ` , {
failedChannel : expectation . channel
} ) ;
}
} else {
let result = asExpression ( ( ) => {
try {
return expectation . adapter . parse ( channels [ expectation . channel ] . toString ( ) ) ;
} catch ( error ) {
// TODO: What if both `required` *and* `disallowed`? Can that ever occur, conceptually speaking?
if ( error instanceof textParser . NoResult ) {
// FIXME: Annotate to make error source clearer?
if ( expectation . required === true ) {
throw error ;
} else {
return undefined ;
}
} else {
throw chain ( error , errors . OutputParsingFailed , ` An error occurred while parsing ' ${ expectation . channel } ' ` , {
failedChannel : expectation . channel
} ) ;
}
}
} ) ;
if ( result !== undefined && ( typeof result !== "object" || Array . isArray ( result ) ) ) {
throw new Error ( ` Output adapters may only return a plain object from their parse method (or nothing at all) ` ) ;
} else if ( result !== undefined && expectation . disallowed === true ) {
// TODO: How to make this error more informative?
throw new errors . UnexpectedOutput ( ` Encountered output on ' ${ expectation . channel } ' that isn't supposed to be there ` , {
failedChannel : expectation . channel
} ) ;
} else {
return result ;
}
}
} )
. filter ( ( result ) => {
return ( result != null ) ;
} ) ;
let mergedResults = ( expectationResults . length > 0 )
? this . _settings . resultMerger ( expectationResults )
: expectationResults [ 0 ] ;
return {
exitCode : exitCode ,
stdout : stdout ,
stderr : stderr ,
result : mergedResults
} ;
}
} catch ( error ) {
// FIXME: Use getAllContext
let message = ( error . failedChannel != null )
? ` Failed while processing ${ error . failedChannel } of command `
: "Failed while processing result of command execution" ;
throw chain ( error , errors . CommandExecutionFailed , message , {
exitCode : exitCode ,
stdout : stdout ,
stderr : stderr
} ) ;
}
} ) . catch ( rethrowAs ( errors . CommandExecutionFailed , ` An error occurred while executing ' ${ command } ' ` , {
command : effectiveCompleteCommand
} ) ) ;
} ) ;
}
} ;
} ;