github.com/fnproject/cli@v0.0.0-20240508150455-e5d88bd86117/lambda/node-4/bootstrap.js (about) 1 /* 2 * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 'use strict'; 18 19 var fs = require('fs'); 20 var net = require('net'); 21 // var http = require('http'); 22 var http_common = require('_http_common'); 23 var events = require('events'); 24 var HTTPParser = process.binding('http_parser').HTTPParser; 25 26 var oldlog = console.log 27 console.log = console.error 28 29 // Some notes on the semantics of the succeed(), fail() and done() methods. 30 // Tests are the source of truth! 31 // First call wins in terms of deciding the result of the function. BUT, 32 // subsequent calls also log. Further, code execution does not stop, even where 33 // for done(), the docs say that the "function terminates". It seems though 34 // that further cycles of the event loop do not run. For example: 35 // index.handler = function(event, context) { 36 // context.fail("FAIL") 37 // process.nextTick(function() { 38 // console.log("This does not get logged") 39 // }) 40 // console.log("This does get logged") 41 // } 42 // on the other hand: 43 // index.handler = function(event, context) { 44 // process.nextTick(function() { 45 // console.log("This also gets logged") 46 // context.fail("FAIL") 47 // }) 48 // console.log("This does get logged") 49 // } 50 // 51 // The same is true for context.succeed() and done() captures the semantics of 52 // both. It seems this is implemented simply by having process.nextTick() cause 53 // process.exit() or similar, because the following: 54 // exports.handler = function(event, context) { 55 // process.nextTick(function() {console.log("This gets logged")}) 56 // process.nextTick(function() {console.log("This also gets logged")}) 57 // context.succeed("END") 58 // process.nextTick(function() {console.log("This does not get logged")}) 59 // }; 60 // 61 // So the context object needs to have some sort of hidden boolean that is only 62 // flipped once, by the first call, and dictates the behavior on the next tick. 63 // 64 // In addition, the response behaviour depends on the invocation type. If we 65 // are to only support the async type, succeed() must return a 202 response 66 // code, not sure how to do this. 67 // 68 // Only the first 256kb, followed by a truncation message, should be logged. 69 // 70 // Also, the error log is always in a json literal 71 // { "errorMessage": "<message>" } 72 var Context = function() { 73 var concluded = false; 74 75 var contextSelf = this; 76 77 // The succeed, fail and done functions are public, but access a private 78 // member (concluded). Hence this ugly nested definition. 79 this.succeed = function(result) { 80 if (concluded) { 81 return 82 } 83 84 // We have to process the result before we can conclude, because otherwise 85 // we have to fail. This means NO EARLY RETURNS from this function without 86 // review! 87 if (result === undefined) { 88 result = null 89 } 90 91 var failed = false; 92 try { 93 // Output result to log 94 oldlog(JSON.stringify(result)); 95 } catch(e) { 96 // Set X-Amz-Function-Error: Unhandled header 97 console.log("Unable to stringify body as json: " + e); 98 failed = true; 99 } 100 101 // FIXME(nikhil): Return 202 or 200 based on invocation type and set response 102 // to result. Should probably be handled externally by the runner/swapi. 103 104 // OK, everything good. 105 concluded = true; 106 process.nextTick(function() { process.exit(failed ? 1 : 0) }) 107 } 108 109 this.fail = function(error) { 110 if (concluded) { 111 return 112 } 113 114 concluded = true 115 process.nextTick(function() { process.exit(1) }) 116 117 if (error === undefined) { 118 error = null 119 } 120 121 // FIXME(nikhil): Truncated log of error, plus non-truncated response body 122 var errstr = "fail() called with argument but a problem was encountered while converting it to a to string"; 123 124 // The semantics of fail() are weird. If the error is something that can be 125 // converted to a string, the log output wraps the string in a JSON literal 126 // with key "errorMessage". If toString() fails, then the output is only 127 // the error string. 128 try { 129 if (error === null) { 130 errstr = null 131 } else { 132 errstr = error.toString() 133 } 134 oldlog(JSON.stringify({"errorMessage": errstr })) 135 } catch(e) { 136 // Set X-Amz-Function-Error: Unhandled header 137 oldlog(errstr) 138 } 139 } 140 141 this.done = function() { 142 var error = arguments[0]; 143 var result = arguments[1]; 144 if (error) { 145 contextSelf.fail(error) 146 } else { 147 contextSelf.succeed(result) 148 } 149 } 150 151 var plannedEnd = Date.now() + (getTimeoutInSeconds() * 1000); 152 this.getRemainingTimeInMillis = function() { 153 return Math.max(plannedEnd - Date.now(), 0); 154 } 155 } 156 157 function getTimeoutInSeconds() { 158 var t = parseInt(getEnv("TASK_TIMEOUT")); 159 if (Number.isNaN(t)) { 160 return 3600; 161 } 162 163 return t; 164 } 165 166 var getEnv = function(name) { 167 return process.env[name] || ""; 168 } 169 170 var makeCtx = function() { 171 var fnname = getEnv("AWS_LAMBDA_FUNCTION_NAME"); 172 // FIXME(nikhil): Generate UUID. 173 var taskID = getEnv("TASK_ID"); 174 175 var mem = getEnv("TASK_MAXRAM").toLowerCase(); 176 var bytes = 300 * 1024 * 1024; 177 178 var scale = { 'b': 1, 'k': 1024, 'm': 1024*1024, 'g': 1024*1024*1024 }; 179 // We don't bother validating too much, if the last character is not a number 180 // and not in the scale table we just return a default value. 181 // We use slice instead of indexing so that we always get an empty string, 182 // instead of undefined. 183 if (mem.slice(-1).match(/[0-9]/)) { 184 var a = parseInt(mem); 185 if (!Number.isNaN(a)) { 186 bytes = a; 187 } 188 } else { 189 var rem = parseInt(mem.slice(0, -1)); 190 if (!Number.isNaN(rem)) { 191 var multiplier = scale[mem.slice(-1)]; 192 if (multiplier) { 193 bytes = rem * multiplier 194 } 195 } 196 } 197 198 var memoryMB = bytes / (1024 * 1024); 199 200 var ctx = new Context(); 201 Object.defineProperties(ctx, { 202 "functionName": { 203 value: fnname, 204 enumerable: true, 205 }, 206 "functionVersion": { 207 value: "$LATEST", 208 enumerable: true, 209 }, 210 "invokedFunctionArn": { 211 // FIXME(nikhil): Should be filled in. 212 value: "", 213 enumerable: true, 214 }, 215 "memoryLimitInMB": { 216 // Sigh, yes it is a string. 217 value: ""+memoryMB, 218 enumerable: true, 219 }, 220 "awsRequestId": { 221 value: taskID, 222 enumerable: true, 223 }, 224 "logGroupName": { 225 // FIXME(nikhil): Should be filled in. 226 value: "", 227 enumerable: true, 228 }, 229 "logStreamName": { 230 // FIXME(nikhil): Should be filled in. 231 value: "", 232 enumerable: true, 233 }, 234 "identity": { 235 // FIXME(nikhil): Should be filled in. 236 value: null, 237 enumerable: true, 238 }, 239 "clientContext": { 240 // FIXME(nikhil): Should be filled in. 241 value: null, 242 enumerable: true, 243 }, 244 }); 245 246 return ctx; 247 } 248 249 var setEnvFromHeader = function () { 250 var headerPrefix = "CONFIG_"; 251 var newEnvVars = {}; 252 for (var key in process.env) { 253 if (key.indexOf(headerPrefix) == 0) { 254 newEnvVars[key.slice(headerPrefix.length)] = process.env[key]; 255 } 256 } 257 258 for (var key in newEnvVars) { 259 process.env[key] = newEnvVars[key]; 260 } 261 } 262 263 // for http hot functions 264 function freeParser(parser){ 265 if (parser) { 266 parser.onIncoming = null; 267 parser.socket = null; 268 http_common.parsers.free(parser); 269 parser = null; 270 } 271 }; 272 273 // parses http requests 274 function parse(socket){ 275 var emitter = new events.EventEmitter(); 276 var parser = http_common.parsers.alloc(); 277 278 parser.reinitialize(HTTPParser.REQUEST); 279 parser.socket = socket; 280 parser.maxHeaderPairs = 2000; 281 282 parser.onIncoming = function(req){ 283 emitter.emit('request', req); 284 }; 285 286 socket.on('data', function(buffer){ 287 var ret = parser.execute(buffer, 0, buffer.length); 288 if(ret instanceof Error){ 289 emitter.emit('error'); 290 freeParser(parser); 291 } 292 }); 293 294 socket.once('close', function(){ 295 freeParser(parser); 296 }); 297 298 return emitter; 299 }; 300 301 function run() { 302 303 setEnvFromHeader(); 304 var path = process.env["PAYLOAD_FILE"]; // if we allow a mounted file, this is used. Could safely be removed. 305 var stream = process.stdin; 306 if (path) { 307 try { 308 stream = fs.createReadStream(path); 309 } catch(e) { 310 console.error("bootstrap: Error opening payload file", e) 311 process.exit(1); 312 } 313 } 314 315 // First, check format (ie: hot functions) 316 var format = process.env["FORMAT"]; 317 if (format == "http"){ 318 // var parser = httpSocketSetup(stream) 319 // init parser 320 var parser = parse(stream); 321 let i = 0; 322 parser.on('request', function(req){ 323 // Got parsed HTTP object 324 // console.error("REQUEST", req) 325 i++; 326 console.error("REQUEST:", i) 327 handleRequest(req); 328 }); 329 330 parser.on('error', function(e){ 331 // Not HTTP data 332 console.error("INVALID HTTP DATA!", e) 333 }); 334 335 } else { 336 // default 337 handleRequest(stream); 338 } 339 } 340 341 function handleRequest(stream) { 342 var input = ""; 343 stream.setEncoding('utf8'); 344 stream.on('data', function(chunk) { 345 input += chunk; 346 }); 347 stream.on('error', function(err) { 348 console.error("bootstrap: Error reading payload stream", err); 349 process.exit(1); 350 }); 351 stream.on('end', function() { 352 var payload = {} 353 try { 354 if (input.length > 0) { 355 payload = JSON.parse(input); 356 } 357 } catch(e) { 358 console.error("bootstrap: Error parsing JSON", e); 359 process.exit(1); 360 } 361 362 handlePayload(payload) 363 }) 364 } 365 366 function handlePayload(payload) { 367 if (process.argv.length > 2) { 368 var handler = process.argv[2]; 369 var parts = handler.split('.'); 370 // FIXME(nikhil): Error checking. 371 var script = parts[0]; 372 var entry = parts[1]; 373 var started = false; 374 try { 375 var mod = require('./'+script); 376 var func = mod[entry]; 377 if (func === undefined) { 378 oldlog("Handler '" + entry + "' missing on module '" + script + "'"); 379 return; 380 } 381 382 if (typeof func !== 'function') { 383 throw "TypeError: " + (typeof func) + " is not a function"; 384 } 385 started = true; 386 var cback 387 // RUN THE FUNCTION: 388 mod[entry](payload, makeCtx(), functionCallback) 389 } catch(e) { 390 if (typeof e === 'string') { 391 oldlog(e) 392 } else { 393 oldlog(e.message) 394 } 395 if (!started) { 396 oldlog("Process exited before completing request\n") 397 } 398 } 399 } else { 400 console.error("bootstrap: No script specified") 401 process.exit(1); 402 } 403 } 404 405 406 407 function functionCallback(err, result) { 408 if (err != null) { 409 // then user returned error and we should respond with error 410 // http://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-mode-exceptions.html 411 oldlog(JSON.stringify({"errorMessage": errstr })) 412 return 413 } 414 if (result != null) { 415 oldlog(JSON.stringify(result)) 416 } 417 } 418 419 run()