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