@litexa/core
Version:
Litexa, a programming language for writing Alexa skills
216 lines (177 loc) • 6.74 kB
text/coffeescript
LoggingChannel = require '../loggingChannel'
{ spawn } = require('child_process')
###
# Utility function to call a SMAPI command via the `ask api` CLI.
# @param askProfile ... required ASK profile name
# @param command ... required ASK API command
# @param params ... optional flags to send with the command
# @param logChannel ... optional caller's LoggingChannel (derived from for SMAPI logs)
###
# only need to fetch once per session
version =
major: null
minor: null
patch: null
module.exports = {
version: version
prepare: (logger) ->
module.exports.getVersion logger
getVersion: (logger) ->
if version.major != null
return Promise.resolve version
cmd = 'ask'
args = [ '--version' ]
@spawnPromise(cmd, args)
.then (data) ->
parts = data.stdout.split '.'
version.major = parseInt parts[0] ? "0"
version.minor = parseInt parts[1] ? "0"
version.patch = parseInt parts[2] ? "0"
logger.log "ask-cli version #{version.major}.#{version.minor}.#{version.patch}"
return version
call: (args) ->
askProfile = args.askProfile
command = args.command
params = args.params
logger = if args.logChannel then args.logChannel.derive('smapi') else new LoggingChannel({logPrefix: 'smapi'})
unless command
throw new Error "SMAPI called without a command. Please provide one."
if version.major == null
await @getVersion logger
cmd = 'ask'
args = []
if version.major < 2
args.push 'api'
args.push command
else
args.push 'smapi'
args.push command
unless askProfile
throw new Error "SMAPI called with command '#{command}' is missing an ASK profile. Please
make sure you've inserted a valid askProfile in your litexa.config file."
args.push '--profile'
args.push askProfile
for k, v of params
args.push "--#{k}"
args.push "#{v}"
logger.verbose "ask #{args.join ' '}"
@spawnPromise(cmd, args)
.then (data) ->
badCommand = data.stdout.toLowerCase().indexOf("command not recognized") >= 0
badCommand = badCommand || data.stderr.toLowerCase().indexOf("command not recognized") >= 0
if badCommand
throw new Error "SMAPI called with command '#{command}', which was reported as an invalid
ask-cli command. Please ensure you have the latest version installed and configured
correctly."
logger.verbose "SMAPI #{command} stdout: #{data.stdout}"
logger.verbose "SMAPI stderr: #{data.stderr}"
if version.major >= 2
if data.errorCode != 0
if data.stderr
throw data.stderr
else
throw "SMAPI command '#{command}' failed, without an error message"
if data.stderr
do ->
# we can filter this informational one out, as it can be confusing in the litexa output
return if data.stderr.match /This is an asynchronous operation/
logger.warning data.stderr
responseKey = "\nResponse:\n"
responsePos = data.stdout.indexOf responseKey
if responsePos >= 0
responsePos += responseKey.length
response = JSON.parse( data.stdout[responsePos...] )
logger.verbose "SMAPI statusCode #{response.statusCode}"
data.stdout = JSON.stringify response.body, null, 2
if version.major < 2
# errors pre V2 were fatal, assume the process failed
if data.stderr
if data.stderr.indexOf('ETag') >= 0
# some v1 commands returned an ETag here, don't need it but it's not an error
else
throw data.stderr
Promise.resolve data.stdout
.catch (err) ->
if typeof(err) != 'string'
if err.message and err.message.match /\s*Cannot resolve profile/i
throw new Error "ASK profile '#{askProfile}' not found. Make sure the profile exists and
was correctly configured with ask init."
else
return Promise.reject(err)
code = undefined
message = undefined
name = ''
# else, err was a string which means it's the SMAPI call's stderr output
if version.major < 2
try
lines = err.split '\n'
for line in lines
k = line.split(':')[0] ? ''
v = (line.replace k, '')[1..].trim()
k = k.trim()
if k.toLowerCase().indexOf('error code') == 0
code = parseInt v
else if k == '"message"'
message = v.trim()
catch err2
logger.error "failed to extract failure code and message from SMAPI call: #{err2}"
else
# starting v2, the error may be a service response JSON error, or it may be a local one
prefix = "[Error]: "
offset = err.indexOf prefix
if offset >= 0
try
err = err[offset + prefix.length ..]
parsed = JSON.parse err
code = parsed.detail?.statusCode ? parsed.statusCode
message = parsed.message
response = parsed.detail?.response ? parsed.response
if response?
message = JSON.stringify response, null, 2
name = parsed.detail?.name ? parsed.name
catch err2
logger.error "failed to extract failure code and message from SMAPI call: #{err2}"
unless message
message = "Unknown SMAPI error during command '#{command}': #{err}"
Promise.reject { code, message, name }
spawnPromise: (cmd, args) ->
return new Promise (resolve, reject) =>
spawnedProcess = spawn(cmd, args, {shell:true})
stdout = null
stderr = ''
spawnedProcess.on('error', (err) ->
if err.code == 'ENOENT'
throw new Error "Unable to run 'ask'. Is the ask-cli installed and configured
correctly?"
else
throw err
)
spawnedProcess.stdout.on('data', (data) ->
if stdout == null
if typeof(data) == 'object'
# observed a binary response here, if so accumulate bytes instead
stdout = data
return
else
stdout = ''
if typeof(data) == 'object'
stdout = Buffer.concat [ stdout, data ]
else
stdout += data
)
spawnedProcess.stderr.on('data', (data) ->
stderr += data
)
resolver = (errorCode) ->
if stdout == null
stdout = ''
if typeof(stdout) == 'object'
stdout = stdout.toString('utf8')
resolve {
errorCode,
stdout,
stderr
}
spawnedProcess.on('exit', resolver)
spawnedProcess.on('close', resolver)
}