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