Created
August 23, 2020 19:11
-
-
Save notbrain/7390d17401c4c5d10fedd76a06363df2 to your computer and use it in GitHub Desktop.
Lambda NodeJS 12.18 Default Runtime JavaScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ | |
"use strict"; | |
/** | |
* The runtime has a single beforeExit function which is stored in the global | |
* object with a symbol key. | |
* The symbol is not exported. | |
* The process.beforeExit listener is setup in index.js along with all other | |
* top-level process event listeners. | |
*/ | |
// define a named symbol for the handler function | |
const LISTENER_SYMBOL = Symbol.for("aws.lambda.beforeExit"); | |
const NO_OP_LISTENER = () => {}; | |
// export a setter | |
module.exports = { | |
/** | |
* Call the listener function with no arguments. | |
*/ | |
invoke: () => global[LISTENER_SYMBOL](), | |
/** | |
* Reset the listener to a no-op function. | |
*/ | |
reset: () => (global[LISTENER_SYMBOL] = NO_OP_LISTENER), | |
/** | |
* Set the listener to the provided function. | |
*/ | |
set: listener => (global[LISTENER_SYMBOL] = listener) | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/sh | |
# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. | |
if [ -z "$NODE_PATH" ]; | |
then | |
nodejs_mods="/opt/nodejs/node_modules" | |
nodejs12_mods="/opt/nodejs/node12/node_modules" | |
runtime_mods="/var/runtime/node_modules" | |
task="/var/runtime:/var/task" | |
export NODE_PATH="$nodejs12_mods:$nodejs_mods:$runtime_mods:$task" | |
fi | |
if [ -n "$AWS_LAMBDA_FUNCTION_MEMORY_SIZE" ]; | |
then | |
new_space=$(expr $AWS_LAMBDA_FUNCTION_MEMORY_SIZE / 10) | |
semi_space=$(expr $new_space / 2) | |
old_space=$(expr $AWS_LAMBDA_FUNCTION_MEMORY_SIZE - $new_space) | |
MEMORY_ARGS=( | |
"--max-semi-space-size=$semi_space" | |
"--max-old-space-size=$old_space" | |
) | |
fi | |
if [[ $AWS_DEFAULT_REGION == us-iso* ]] | |
then export NODE_EXTRA_CA_CERTS="/etc/pki/tls/certs/ca-bundle.crt" | |
fi | |
export AWS_EXECUTION_ENV=AWS_Lambda_nodejs12.x | |
exec env \ | |
/var/lang/bin/node \ | |
--expose-gc \ | |
--max-http-header-size 81920 \ | |
"${MEMORY_ARGS[@]}" \ | |
/var/runtime/index.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ | |
"use strict"; | |
let BeforeExitListener = require("./BeforeExitListener.js"); | |
let Errors = require("./Errors"); | |
function _homogeneousError(err) { | |
if (err instanceof Error) { | |
return err; | |
} else { | |
return new Error(err); | |
} | |
} | |
/** | |
* Build the callback function and the part of the context which exposes | |
* the succeed/fail/done callbacks. | |
* @param client {Client} | |
* The RAPID client used to post results/errors. | |
* @param id {string} | |
* The invokeId for the current invocation. | |
* @param scheduleNext {function} | |
* A function which takes no params and immediately schedules the next | |
* iteration of the invoke loop. | |
*/ | |
function _rawCallbackContext(client, id, scheduleNext) { | |
const postError = (err, callback) => { | |
console.error("Invoke Error", Errors.toFormatted(_homogeneousError(err))); | |
client.postInvocationError(err, id, callback); | |
}; | |
const complete = (result, callback) => { | |
client.postInvocationResponse(result, id, callback); | |
}; | |
let waitForEmptyEventLoop = true; | |
let callback = function(err, result) { | |
BeforeExitListener.reset(); | |
if (err !== undefined && err !== null) { | |
postError(err, scheduleNext); | |
} else { | |
complete(result, () => { | |
if (!waitForEmptyEventLoop) { | |
scheduleNext(); | |
} else { | |
BeforeExitListener.set(scheduleNext); | |
} | |
}); | |
} | |
}; | |
let done = (err, result) => { | |
BeforeExitListener.reset(); | |
if (err !== undefined && err !== null) { | |
postError(err, scheduleNext); | |
} else { | |
complete(result, scheduleNext); | |
} | |
}; | |
let succeed = result => { | |
done(null, result); | |
}; | |
let fail = err => { | |
if (err === undefined || err === null) { | |
done("handled"); | |
} else { | |
done(err, null); | |
} | |
}; | |
let callbackContext = { | |
get callbackWaitsForEmptyEventLoop() { | |
return waitForEmptyEventLoop; | |
}, | |
set callbackWaitsForEmptyEventLoop(value) { | |
waitForEmptyEventLoop = value; | |
}, | |
succeed: succeed, | |
fail: fail, | |
done: done | |
}; | |
return [callback, callbackContext]; | |
} | |
/** | |
* Wraps the callback and context so that only the first call to any callback | |
* succeeds. | |
* @param callback {function} | |
* the node-style callback function that was previously generated but not | |
* yet wrapped. | |
* @param callbackContext {object} | |
* The previously generated callbackContext object that contains | |
* getter/setters for the contextWaitsForEmptyeventLoop flag and the | |
* succeed/fail/done functions. | |
* @return [callback, context] | |
*/ | |
function _wrappedCallbackContext(callback, callbackContext) { | |
let finished = false; | |
let onlyAllowFirstCall = function(toWrap) { | |
return function() { | |
if (!finished) { | |
toWrap.apply(null, arguments); | |
finished = true; | |
} | |
}; | |
}; | |
callbackContext.succeed = onlyAllowFirstCall(callbackContext.succeed); | |
callbackContext.fail = onlyAllowFirstCall(callbackContext.fail); | |
callbackContext.done = onlyAllowFirstCall(callbackContext.done); | |
return [onlyAllowFirstCall(callback), callbackContext]; | |
} | |
/** | |
* Construct the base-context object which includes the required flags and | |
* callback methods for the Node programming model. | |
* @param client {Client} | |
* The RAPID client used to post results/errors. | |
* @param id {string} | |
* The invokeId for the current invocation. | |
* @param scheduleNext {function} | |
* A function which takes no params and immediately schedules the next | |
* iteration of the invoke loop. | |
* @return [callback, context] | |
* The same function and context object, but wrapped such that only the | |
* first call to any function will be successful. All subsequent calls are | |
* a no-op. | |
*/ | |
module.exports.build = function(client, id, scheduleNext) { | |
let rawCallbackContext = _rawCallbackContext(client, id, scheduleNext); | |
return _wrappedCallbackContext(...rawCallbackContext); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. | |
* | |
* Defines custom error types throwable by the runtime. | |
*/ | |
"use strict"; | |
const util = require("util"); | |
function _isError(obj) { | |
return ( | |
obj && | |
obj.name && | |
obj.message && | |
obj.stack && | |
typeof obj.name === "string" && | |
typeof obj.message === "string" && | |
typeof obj.stack === "string" | |
); | |
} | |
/** | |
* Attempt to convert an object into a response object. | |
* This method accounts for failures when serializing the error object. | |
*/ | |
function toRapidResponse(error) { | |
try { | |
if (util.types.isNativeError(error) || _isError(error)) { | |
return { | |
errorType: error.name, | |
errorMessage: error.message, | |
trace: error.stack.split("\n") | |
}; | |
} else { | |
return { | |
errorType: typeof error, | |
errorMessage: error.toString(), | |
trace: [] | |
}; | |
} | |
} catch (_err) { | |
return { | |
errorType: "handled", | |
errorMessage: | |
"callback called with Error argument, but there was a problem while retrieving one or more of its message, name, and stack" | |
}; | |
} | |
} | |
module.exports.toRapidResponse = toRapidResponse; | |
/** | |
* Format an error with the expected properties. | |
* For compatability, the error string always starts with a tab. | |
*/ | |
module.exports.toFormatted = error => { | |
try { | |
return ( | |
"\t" + JSON.stringify(error, (_k, v) => _withEnumerableProperties(v)) | |
); | |
} catch (err) { | |
return "\t" + JSON.stringify(toRapidResponse(error)); | |
} | |
}; | |
/** | |
* Error name, message, code, and stack are all members of the superclass, which | |
* means they aren't enumerable and don't normally show up in JSON.stringify. | |
* This method ensures those interesting properties are available along with any | |
* user-provided enumerable properties. | |
*/ | |
function _withEnumerableProperties(error) { | |
if (error instanceof Error) { | |
let ret = Object.assign( | |
{ | |
errorType: error.name, | |
errorMessage: error.message, | |
code: error.code | |
}, | |
error | |
); | |
if (typeof error.stack == "string") { | |
ret.stack = error.stack.split("\n"); | |
} | |
return ret; | |
} else { | |
return error; | |
} | |
} | |
const errorClasses = [ | |
class ImportModuleError extends Error {}, | |
class HandlerNotFound extends Error {}, | |
class MalformedHandlerName extends Error {}, | |
class UserCodeSyntaxError extends Error {}, | |
class UnhandledPromiseRejection extends Error { | |
constructor(reason, promise) { | |
super(reason); | |
this.reason = reason; | |
this.promise = promise; | |
} | |
} | |
]; | |
errorClasses.forEach(e => { | |
module.exports[e.name] = e; | |
e.prototype.name = `Runtime.${e.name}`; | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. | |
* | |
* This module is the bootstrap entrypoint. It establishes the top-level event | |
* listeners and loads the user's code. | |
*/ | |
"use strict"; | |
const RAPIDClient = require("./RAPIDClient.js"); | |
const Runtime = require("./Runtime.js"); | |
const UserFunction = require("./UserFunction.js"); | |
const Errors = require("./Errors.js"); | |
const BeforeExitListener = require("./BeforeExitListener.js"); | |
require("./LogPatch").patchConsole(); | |
const client = new RAPIDClient(process.env.AWS_LAMBDA_RUNTIME_API); | |
let errorCallbacks = { | |
uncaughtException: error => { | |
client.postInitError(error, () => process.exit(129)); | |
}, | |
unhandledRejection: error => { | |
client.postInitError(error, () => process.exit(128)); | |
} | |
}; | |
process.on("uncaughtException", error => { | |
console.error("Uncaught Exception", Errors.toFormatted(error)); | |
errorCallbacks.uncaughtException(error); | |
}); | |
process.on("unhandledRejection", (reason, promise) => { | |
let error = new Errors.UnhandledPromiseRejection(reason, promise); | |
console.error("Unhandled Promise Rejection", Errors.toFormatted(error)); | |
errorCallbacks.unhandledRejection(error); | |
}); | |
BeforeExitListener.reset(); | |
process.on("beforeExit", BeforeExitListener.invoke); | |
const handler = UserFunction.load( | |
process.env.LAMBDA_TASK_ROOT, | |
process.env._HANDLER | |
); | |
new Runtime(client, handler, errorCallbacks).scheduleIteration(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. | |
* | |
* This module defines the InvokeContext and supporting functions. The | |
* InvokeContext is responsible for pulling information from the invoke headers | |
* and for wrapping the Rapid Client object's error and response functions. | |
*/ | |
"use strict"; | |
const assert = require("assert").strict; | |
let { setCurrentRequestId } = require("./LogPatch"); | |
const INVOKE_HEADER = { | |
ClientContext: "lambda-runtime-client-context", | |
CognitoIdentity: "lambda-runtime-cognito-identity", | |
ARN: "lambda-runtime-invoked-function-arn", | |
AWSRequestId: "lambda-runtime-aws-request-id", | |
DeadlineMs: "lambda-runtime-deadline-ms", | |
XRayTrace: "lambda-runtime-trace-id" | |
}; | |
module.exports = class InvokeContext { | |
constructor(headers) { | |
this.headers = _enforceLowercaseKeys(headers); | |
} | |
/** | |
* The invokeId for this request. | |
*/ | |
get invokeId() { | |
let id = this.headers[INVOKE_HEADER.AWSRequestId]; | |
assert.ok(id, "invocation id is missing or invalid"); | |
return id; | |
} | |
/** | |
* Push relevant invoke data into the logging context. | |
*/ | |
updateLoggingContext() { | |
setCurrentRequestId(this.invokeId); | |
} | |
/** | |
* Attach all of the relavant environmental and invocation data to the | |
* provided object. | |
* This method can throw if the headers are malformed and cannot be parsed. | |
* @param callbackContext {Object} | |
* The callbackContext object returned by a call to buildCallbackContext(). | |
* @return {Object} | |
* The user context object with all required data populated from the headers | |
* and environment variables. | |
*/ | |
attachEnvironmentData(callbackContext) { | |
this._forwardXRay(); | |
return Object.assign( | |
callbackContext, | |
this._environmentalData(), | |
this._headerData() | |
); | |
} | |
/** | |
* All parts of the user-facing context object which are provided through | |
* environment variables. | |
*/ | |
_environmentalData() { | |
return { | |
functionVersion: process.env["AWS_LAMBDA_FUNCTION_VERSION"], | |
functionName: process.env["AWS_LAMBDA_FUNCTION_NAME"], | |
memoryLimitInMB: process.env["AWS_LAMBDA_FUNCTION_MEMORY_SIZE"], | |
logGroupName: process.env["AWS_LAMBDA_LOG_GROUP_NAME"], | |
logStreamName: process.env["AWS_LAMBDA_LOG_STREAM_NAME"] | |
}; | |
} | |
/** | |
* All parts of the user-facing context object which are provided through | |
* request headers. | |
*/ | |
_headerData() { | |
const deadline = this.headers[INVOKE_HEADER.DeadlineMs]; | |
return { | |
clientContext: _parseJson( | |
this.headers[INVOKE_HEADER.ClientContext], | |
"ClientContext" | |
), | |
identity: _parseJson( | |
this.headers[INVOKE_HEADER.CognitoIdentity], | |
"CognitoIdentity" | |
), | |
invokedFunctionArn: this.headers[INVOKE_HEADER.ARN], | |
awsRequestId: this.headers[INVOKE_HEADER.AWSRequestId], | |
getRemainingTimeInMillis: function() { | |
return deadline - Date.now(); | |
} | |
}; | |
} | |
/** | |
* Forward the XRay header into the environment variable. | |
*/ | |
_forwardXRay() { | |
if (this.headers[INVOKE_HEADER.XRayTrace]) { | |
process.env["_X_AMZN_TRACE_ID"] = this.headers[INVOKE_HEADER.XRayTrace]; | |
} else { | |
delete process.env["_X_AMZN_TRACE_ID"]; | |
} | |
} | |
}; | |
/** | |
* Parse a JSON string and throw a readable error if something fails. | |
* @param jsonString {string} - the string to attempt to parse | |
* @param name {name} - the name to use when describing the string in an error | |
* @return object - the parsed object | |
* @throws if jsonString cannot be parsed | |
*/ | |
function _parseJson(jsonString, name) { | |
if (jsonString !== undefined) { | |
try { | |
return JSON.parse(jsonString); | |
} catch (err) { | |
throw new Error(`Cannot parse ${name} as json: ${err.toString()}`); | |
} | |
} else { | |
return undefined; | |
} | |
} | |
/** | |
* Construct a copy of an object such that all of its keys are lowercase. | |
*/ | |
function _enforceLowercaseKeys(original) { | |
return Object.keys(original).reduce((enforced, originalKey) => { | |
enforced[originalKey.toLowerCase()] = original[originalKey]; | |
return enforced; | |
}, {}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ | |
"use strict"; | |
const util = require("util"); | |
const fs = require("fs"); | |
const levels = Object.freeze({ | |
INFO: { name: "INFO" }, | |
DEBUG: { name: "DEBUG" }, | |
WARN: { name: "WARN" }, | |
ERROR: { name: "ERROR" }, | |
TRACE: { name: "TRACE" }, | |
FATAL: { name: "FATAL" } | |
}); | |
/* Use a unique symbol to provide global access without risk of name clashes. */ | |
const REQUEST_ID_SYMBOL = Symbol.for("aws.lambda.runtime.requestId"); | |
let _currentRequestId = { | |
get: () => global[REQUEST_ID_SYMBOL], | |
set: id => (global[REQUEST_ID_SYMBOL] = id) | |
}; | |
/** | |
* Write logs to stdout. | |
*/ | |
let _logToStdout = (level, message) => { | |
let time = new Date().toISOString(); | |
let requestId = _currentRequestId.get(); | |
let line = `${time}\t${requestId}\t${level.name}\t${message}`; | |
line = line.replace(/\n/g, "\r"); | |
process.stdout.write(line + "\n"); | |
}; | |
/** | |
* Write logs to filedescriptor. | |
* Implements the logging contract between runtimes and the platform. | |
* Each entry is framed as: | |
* +----------------------+------------------------+-----------------------+ | |
* | Frame Type - 4 bytes | Length (len) - 4 bytes | Message - 'len' bytes | | |
* +----------------------+------------------------+-----------------------+ | |
* The frist 4 bytes are the frame type. For logs this is always 0xa55a0001. | |
* The second 4 bytes are the length of the message. | |
* The remaining bytes ar ethe message itself. Byte order is big-endian. | |
*/ | |
let _logToFd = function(logTarget) { | |
let typeAndLength = Buffer.alloc(8); | |
typeAndLength.writeUInt32BE(0xa55a0001, 0); | |
typeAndLength.writeUInt32BE(0x00000000, 4); | |
return (level, message) => { | |
let time = new Date().toISOString(); | |
let requestId = _currentRequestId.get(); | |
let enrichedMessage = `${time}\t${requestId}\t${level.name}\t${message}\n`; | |
let messageBytes = Buffer.from(enrichedMessage, "utf8"); | |
typeAndLength.writeInt32BE(messageBytes.length, 4); | |
fs.writeSync(logTarget, typeAndLength); | |
fs.writeSync(logTarget, messageBytes); | |
}; | |
}; | |
/** | |
* Replace console functions with a log function. | |
* @param {Function(level, String)} log | |
*/ | |
function _patchConsoleWith(log) { | |
console.log = (msg, ...params) => { | |
log(levels.INFO, util.format(msg, ...params)); | |
}; | |
console.debug = (msg, ...params) => { | |
log(levels.DEBUG, util.format(msg, ...params)); | |
}; | |
console.info = (msg, ...params) => { | |
log(levels.INFO, util.format(msg, ...params)); | |
}; | |
console.warn = (msg, ...params) => { | |
log(levels.WARN, util.format(msg, ...params)); | |
}; | |
console.error = (msg, ...params) => { | |
log(levels.ERROR, util.format(msg, ...params)); | |
}; | |
console.trace = (msg, ...params) => { | |
log(levels.TRACE, util.format(msg, ...params)); | |
}; | |
console.fatal = (msg, ...params) => { | |
log(levels.FATAL, util.format(msg, ...params)); | |
}; | |
} | |
let _patchConsole = () => { | |
if ( | |
process.env["_LAMBDA_TELEMETRY_LOG_FD"] != null && | |
process.env["_LAMBDA_TELEMETRY_LOG_FD"] != undefined | |
) { | |
let logFd = parseInt(process.env["_LAMBDA_TELEMETRY_LOG_FD"]); | |
_patchConsoleWith(_logToFd(logFd)); | |
delete process.env["_LAMBDA_TELEMETRY_LOG_FD"]; | |
} else { | |
_patchConsoleWith(_logToStdout); | |
} | |
}; | |
module.exports = { | |
setCurrentRequestId: _currentRequestId.set, | |
patchConsole: _patchConsole | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. | |
* | |
* This module defines the RAPID client which is responsible for all HTTP | |
* interactions with the RAPID layer. | |
*/ | |
"use strict"; | |
const Errors = require("./Errors"); | |
const XRayError = require("./XRayError"); | |
const ERROR_TYPE_HEADER = "Lambda-Runtime-Function-Error-Type"; | |
/** | |
* Objects of this class are responsible for all interactions with the RAPID | |
* API. | |
*/ | |
module.exports = class RAPIDClient { | |
constructor(hostnamePort, httpClient, nativeClient) { | |
this.http = httpClient || require("http"); | |
this.nativeClient = | |
nativeClient || require("/var/runtime/build/Release/rapid-client.node"); | |
let [hostname, port] = hostnamePort.split(":"); | |
this.hostname = hostname; | |
this.port = parseInt(port, 10); | |
this.agent = new this.http.Agent({ | |
keepAlive: true, | |
maxSockets: 1 | |
}); | |
} | |
/** | |
* Complete and invocation with the provided response. | |
* @param {Object} response | |
* An arbitrary object to convert to JSON and send back as as response. | |
* @param {String} id | |
* The invocation ID. | |
* @param (function()} callback | |
* The callback to run after the POST response ends | |
*/ | |
postInvocationResponse(response, id, callback) { | |
let bodyString = _trySerializeResponse(response); | |
this.nativeClient.done(id, bodyString); | |
callback(); | |
} | |
/** | |
* Post an initialization error to the RAPID API. | |
* @param {Error} error | |
* @param (function()} callback | |
* The callback to run after the POST response ends | |
*/ | |
postInitError(error, callback) { | |
let response = Errors.toRapidResponse(error); | |
this._post( | |
`/2018-06-01/runtime/init/error`, | |
response, | |
{ [ERROR_TYPE_HEADER]: response.errorType }, | |
callback | |
); | |
} | |
/** | |
* Post an invocation error to the RAPID API | |
* @param {Error} error | |
* @param {String} id | |
* The invocation ID for the in-progress invocation. | |
* @param (function()} callback | |
* The callback to run after the POST response ends | |
*/ | |
postInvocationError(error, id, callback) { | |
let response = Errors.toRapidResponse(error); | |
let bodyString = _trySerializeResponse(response); | |
let xrayString = XRayError.formatted(error); | |
this.nativeClient.error(id, bodyString, xrayString); | |
callback(); | |
} | |
/** | |
* Get the next invocation. | |
* @return {PromiseLike.<Object>} | |
* A promise which resolves to an invocation object that contains the body | |
* as json and the header array. e.g. {bodyJson, headers} | |
*/ | |
async nextInvocation() { | |
return this.nativeClient.next(); | |
} | |
/** | |
* HTTP Post to a path. | |
* @param {String} path | |
* @param {Object} body | |
* The body is serialized into JSON before posting. | |
* @param {Object} headers | |
* The http headers | |
* @param (function()} callback | |
* The callback to run after the POST response ends | |
*/ | |
_post(path, body, headers, callback) { | |
let bodyString = _trySerializeResponse(body); | |
let options = { | |
hostname: this.hostname, | |
port: this.port, | |
path: path, | |
method: "POST", | |
headers: Object.assign( | |
{ | |
"Content-Type": "application/json", | |
"Content-Length": Buffer.from(bodyString).length | |
}, | |
headers || {} | |
), | |
agent: this.agent | |
}; | |
let request = this.http.request(options, response => { | |
response.on("end", () => { | |
callback(); | |
}); | |
response.on("error", e => { | |
throw e; | |
}); | |
response.on("data", () => {}); | |
}); | |
request.on("error", e => { | |
throw e; | |
}); | |
request.end(bodyString, "utf-8"); | |
} | |
}; | |
/** | |
* Attempt to serialize an object as json. Capture the failure if it occurs and | |
* throw one that's known to be serializable. | |
*/ | |
function _trySerializeResponse(body) { | |
try { | |
return JSON.stringify(body === undefined ? null : body); | |
} catch (err) { | |
throw new Error("Unable to stringify response body"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
NAME=Node.js | |
VERSION=12.18.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. | |
* | |
* This module defines the top-level Runtime class which controls the | |
* bootstrap's execution flow. | |
*/ | |
"use strict"; | |
const InvokeContext = require("./InvokeContext.js"); | |
const CallbackContext = require("./CallbackContext.js"); | |
const BeforeExitListener = require("./BeforeExitListener.js"); | |
module.exports = class Runtime { | |
constructor(client, handler, errorCallbacks) { | |
this.client = client; | |
this.handler = handler; | |
this.errorCallbacks = errorCallbacks; | |
} | |
/** | |
* Schedule the next loop iteration to start at the beginning of the next time | |
* around the event loop. | |
*/ | |
scheduleIteration() { | |
let that = this; | |
setImmediate(() => { | |
that.handleOnce().then( | |
// Success is a no-op at this level. There are 2 cases: | |
// 1 - The user used one of the callback functions which already | |
// schedules the next iteration. | |
// 2 - The next iteration is not scheduled because the | |
// waitForEmptyEventLoop was set. In this case the beforeExit | |
// handler will automatically start the next iteration. | |
() => {}, | |
// Errors should not reach this level in typical execution. If they do | |
// it's a sign of an issue in the Client or a bug in the runtime. So | |
// dump it to the console and attempt to report it as a Runtime error. | |
err => { | |
console.log(`Unexpected Top Level Error: ${err.toString()}`); | |
this.errorCallbacks.uncaughtException(err); | |
} | |
); | |
}); | |
} | |
/** | |
* Wait for the next invocation, process it, and schedule the next iteration. | |
*/ | |
async handleOnce() { | |
let { bodyJson, headers } = await this.client.nextInvocation(); | |
let invokeContext = new InvokeContext(headers); | |
invokeContext.updateLoggingContext(); | |
let [callback, callbackContext] = CallbackContext.build( | |
this.client, | |
invokeContext.invokeId, | |
this.scheduleIteration.bind(this) | |
); | |
try { | |
this._setErrorCallbacks(invokeContext.invokeId); | |
this._setDefaultExitListener(invokeContext.invokeId); | |
let result = this.handler( | |
JSON.parse(bodyJson), | |
invokeContext.attachEnvironmentData(callbackContext), | |
callback | |
); | |
if (_isPromise(result)) { | |
result | |
.then(callbackContext.succeed, callbackContext.fail) | |
.catch(callbackContext.fail); | |
} | |
} catch (err) { | |
callback(err); | |
} | |
} | |
/** | |
* Replace the error handler callbacks. | |
* @param {String} invokeId | |
*/ | |
_setErrorCallbacks(invokeId) { | |
this.errorCallbacks.uncaughtException = error => { | |
this.client.postInvocationError(error, invokeId, () => { | |
process.exit(129); | |
}); | |
}; | |
this.errorCallbacks.unhandledRejection = error => { | |
this.client.postInvocationError(error, invokeId, () => { | |
process.exit(128); | |
}); | |
}; | |
} | |
/** | |
* Setup the 'beforeExit' listener that is used if the callback is never | |
* called and the handler is not async. | |
* CallbackContext replaces the listener if a callback is invoked. | |
*/ | |
_setDefaultExitListener(invokeId) { | |
BeforeExitListener.set(() => { | |
this.client.postInvocationResponse(null, invokeId, () => | |
this.scheduleIteration() | |
); | |
}); | |
} | |
}; | |
function _isPromise(obj) { | |
return obj && obj.then && typeof obj.then === "function"; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. | |
* | |
* This module defines the functions for loading the user's code as specified | |
* in a handler string. | |
*/ | |
"use strict"; | |
const { | |
HandlerNotFound, | |
MalformedHandlerName, | |
ImportModuleError, | |
UserCodeSyntaxError | |
} = require("./Errors.js"); | |
const path = require("path"); | |
const fs = require("fs"); | |
const FUNCTION_EXPR = /^([^.]*)\.(.*)$/; | |
const RELATIVE_PATH_SUBSTRING = ".."; | |
/** | |
* Break the full handler string into two pieces, the module root and the actual | |
* handler string. | |
* Given './somepath/something/module.nestedobj.handler' this returns | |
* ['./somepath/something', 'module.nestedobj.handler'] | |
*/ | |
function _moduleRootAndHandler(fullHandlerString) { | |
let handlerString = path.basename(fullHandlerString); | |
let moduleRoot = fullHandlerString.substring( | |
0, | |
fullHandlerString.indexOf(handlerString) | |
); | |
return [moduleRoot, handlerString]; | |
} | |
/** | |
* Split the handler string into two pieces: the module name and the path to | |
* the handler function. | |
*/ | |
function _splitHandlerString(handler) { | |
let match = handler.match(FUNCTION_EXPR); | |
if (!match || match.length != 3) { | |
throw new MalformedHandlerName("Bad handler"); | |
} | |
return [match[1], match[2]]; // [module, function-path] | |
} | |
/** | |
* Resolve the user's handler function from the module. | |
*/ | |
function _resolveHandler(object, nestedProperty) { | |
return nestedProperty.split(".").reduce((nested, key) => { | |
return nested && nested[key]; | |
}, object); | |
} | |
/** | |
* Verify that the provided path can be loaded as a file per: | |
* https://nodejs.org/dist/latest-v10.x/docs/api/modules.html#modules_all_together | |
* @param string - the fully resolved file path to the module | |
* @return bool | |
*/ | |
function _canLoadAsFile(modulePath) { | |
return fs.existsSync(modulePath) || fs.existsSync(modulePath + ".js"); | |
} | |
/** | |
* Attempt to load the user's module. | |
* Attempts to directly resolve the module relative to the application root, | |
* then falls back to the more general require(). | |
*/ | |
function _tryRequire(appRoot, moduleRoot, module) { | |
let lambdaStylePath = path.resolve(appRoot, moduleRoot, module); | |
if (_canLoadAsFile(lambdaStylePath)) { | |
return require(lambdaStylePath); | |
} else { | |
// Why not just require(module)? | |
// Because require() is relative to __dirname, not process.cwd(). And the | |
// runtime implementation is not located in /var/task | |
let nodeStylePath = require.resolve(module, { | |
paths: [appRoot, moduleRoot] | |
}); | |
return require(nodeStylePath); | |
} | |
} | |
/** | |
* Load the user's application or throw a descriptive error. | |
* @throws Runtime errors in two cases | |
* 1 - UserCodeSyntaxError if there's a syntax error while loading the module | |
* 2 - ImportModuleError if the module cannot be found | |
*/ | |
function _loadUserApp(appRoot, moduleRoot, module) { | |
try { | |
return _tryRequire(appRoot, moduleRoot, module); | |
} catch (e) { | |
if (e instanceof SyntaxError) { | |
throw new UserCodeSyntaxError(e); | |
} else if (e.code !== undefined && e.code === "MODULE_NOT_FOUND") { | |
throw new ImportModuleError(e); | |
} else { | |
throw e; | |
} | |
} | |
} | |
function _throwIfInvalidHandler(fullHandlerString) { | |
if (fullHandlerString.includes(RELATIVE_PATH_SUBSTRING)) { | |
throw new MalformedHandlerName( | |
`'${fullHandlerString}' is not a valid handler name. Use absolute paths when specifying root directories in handler names.` | |
); | |
} | |
} | |
/** | |
* Load the user's function with the approot and the handler string. | |
* @param appRoot {string} | |
* The path to the application root. | |
* @param handlerString {string} | |
* The user-provided handler function in the form 'module.function'. | |
* @return userFuction {function} | |
* The user's handler function. This function will be passed the event body, | |
* the context object, and the callback function. | |
* @throws In five cases:- | |
* 1 - if the handler string is incorrectly formatted an error is thrown | |
* 2 - if the module referenced by the handler cannot be loaded | |
* 3 - if the function in the handler does not exist in the module | |
* 4 - if a property with the same name, but isn't a function, exists on the | |
* module | |
* 5 - the handler includes illegal character sequences (like relative paths | |
* for traversing up the filesystem '..') | |
* Errors for scenarios known by the runtime, will be wrapped by Runtime.* errors. | |
*/ | |
module.exports.load = function(appRoot, fullHandlerString) { | |
_throwIfInvalidHandler(fullHandlerString); | |
let [moduleRoot, moduleAndHandler] = _moduleRootAndHandler(fullHandlerString); | |
let [module, handlerPath] = _splitHandlerString(moduleAndHandler); | |
let userApp = _loadUserApp(appRoot, moduleRoot, module); | |
let handlerFunc = _resolveHandler(userApp, handlerPath); | |
if (!handlerFunc) { | |
throw new HandlerNotFound( | |
`${fullHandlerString} is undefined or not exported` | |
); | |
} | |
if (typeof handlerFunc !== "function") { | |
throw new HandlerNotFound(`${fullHandlerString} is not a function`); | |
} | |
return handlerFunc; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ | |
"use strict"; | |
module.exports.formatted = err => { | |
try { | |
return JSON.stringify(new XRayFormattedCause(err)); | |
} catch (err) { | |
return ""; | |
} | |
}; | |
/** | |
* prepare an exception blob for sending to AWS X-Ray | |
* adapted from https://code.amazon.com/packages/AWSTracingSDKNode/blobs/c917508ca4fce6a795f95dc30c91b70c6bc6c617/--/core/lib/segments/attributes/captured_exception.js | |
* transform an Error, or Error-like, into an exception parseable by X-Ray's service. | |
* { | |
* "name": "CustomException", | |
* "message": "Something bad happend!", | |
* "stack": [ | |
* "exports.handler (/var/task/node_modules/event_invoke.js:3:502) | |
* ] | |
* } | |
* => | |
* { | |
* "working_directory": "/var/task", | |
* "exceptions": [ | |
* { | |
* "type": "CustomException", | |
* "message": "Something bad happend!", | |
* "stack": [ | |
* { | |
* "path": "/var/task/event_invoke.js", | |
* "line": 502, | |
* "label": "exports.throw_custom_exception" | |
* } | |
* ] | |
* } | |
* ], | |
* "paths": [ | |
* "/var/task/event_invoke.js" | |
* ] | |
* } | |
*/ | |
class XRayFormattedCause { | |
constructor(err) { | |
this.working_directory = process.cwd(); // eslint-disable-line | |
let stack = []; | |
if (err.stack) { | |
let stackLines = err.stack.split("\n"); | |
stackLines.shift(); | |
stackLines.forEach(stackLine => { | |
let line = stackLine.trim().replace(/\(|\)/g, ""); | |
line = line.substring(line.indexOf(" ") + 1); | |
let label = | |
line.lastIndexOf(" ") >= 0 | |
? line.slice(0, line.lastIndexOf(" ")) | |
: null; | |
let path = | |
label == undefined || label == null || label.length === 0 | |
? line | |
: line.slice(line.lastIndexOf(" ") + 1); | |
path = path.split(":"); | |
let entry = { | |
path: path[0], | |
line: parseInt(path[1]), | |
label: label || "anonymous" | |
}; | |
stack.push(entry); | |
}); | |
} | |
this.exceptions = [ | |
{ | |
type: err.name, | |
message: err.message, | |
stack: stack | |
} | |
]; | |
let paths = new Set(); | |
stack.forEach(entry => { | |
paths.add(entry.path); | |
}); | |
this.paths = Array.from(paths); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment