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()