function NodeActionRunner()

in knative-build/runtimes/javascript/runner.js [33:203]


function NodeActionRunner() {
    DEBUG.functionStart();
    // Use this ref inside closures etc.
    var thisRunner = this;

    this.userScriptMain = undefined;

    // This structure is reset for every action invocation. It contains two fields:
    //   - completed; indicating whether the action has already signaled completion
    //   - next; a callback to be invoked with the result of the action.
    // Note that { error: ... } results are still results.
    var callback = {
        completed : undefined,
        next      : function (result) { return; }
    };

    DEBUG.dumpObject(callback,"callback");

    this.init = function(message) {
        DEBUG.functionStart("NodeActionRunner");
        function assertMainIsFunction() {
            DEBUG.dumpObject(thisRunner.userScriptMain,"this.Runner.userScriptMain");
            if (typeof thisRunner.userScriptMain !== 'function') {
                DEBUG.functionEndError("ERROR: Action entrypoint '" + message.main + "' is not a function.");
                throw "Action entrypoint '" + message.main + "' is not a function.";
            }
            DEBUG.functionEnd();
        }

        // Loading the user code.
        DEBUG.dumpObject(message.binary, "message.binary");
        if (message.binary) {
            // The code is a base64-encoded zip file.
            return unzipInTmpDir(message.code).then(function (moduleDir) {
                if(!fs.existsSync(path.join(moduleDir, 'package.json')) &&
                    !fs.existsSync(path.join(moduleDir, 'index.js'))) {
                    DEBUG.functionEndError("Promise.reject(): Zipped actions must contain either package.json or index.js at the root.");
                    return Promise.reject('Zipped actions must contain either package.json or index.js at the root.')
                }

                try {
                    // Set the executable directory to the project dir
                    process.chdir(moduleDir);
                    thisRunner.userScriptMain = eval('require("' + moduleDir + '").' + message.main);
                    assertMainIsFunction();
                    // The value 'true' has no special meaning here;
                    // the successful state is fully reflected in the
                    // successful resolution of the promise.
                    DEBUG.functionEndSuccess("return true;");
                    return true;
                } catch (e) {
                    DEBUG.functionEndError("Promise.reject(): " + e.message);
                    return Promise.reject(e);
                }
            }).catch(function (error) {
                DEBUG.functionEndError("Promise.reject(): " + error.message);
                return Promise.reject(error);
            });
        } else {
            // The code is a plain old JS file.
            try {
                thisRunner.userScriptMain = eval('(function(){' + message.code + '\nreturn ' + message.main + '})()');
                DEBUG.dumpObject(thisRunner.userScriptMain,"thisRunner.userScriptMain");
                assertMainIsFunction();
                // See comment above about 'true'; it has no specific meaning.
                DEBUG.functionEndSuccess("Promise.resolve(true)");
                return Promise.resolve(true);
            } catch (e) {
                DEBUG.functionEndError("Promise.reject(): " + e.message);
                return Promise.reject(e);
            }
        }
    };

    // Returns a Promise with the result of the user code invocation.
    // The Promise is rejected iff the user code throws.
    this.run = function(args) {
        DEBUG.functionStart();
        return new Promise(
            function (resolve, reject) {
                callback.completed = undefined;
                callback.next = resolve;

                try {
                    DEBUG.dumpObject(args, "Calling: thisRunner.userScriptMain(args): args", "run");
                    var result = thisRunner.userScriptMain(args);
                    DEBUG.dumpObject(result,"Returned: thisRunner.userScriptMain(args): result", "run");
                } catch (e) {
                    DEBUG.functionEndError("ERROR: Promise.reject(): " + e.message);
                    reject(e);
                }

                // Non-promises/undefined instantly resolve.
                Promise.resolve(result).then(function (resolvedResult) {
                    // This happens, e.g. if you just have "return;"
                    if (typeof resolvedResult === "undefined") {
                        resolvedResult = {};
                    }
                    DEBUG.functionEndSuccess("Promise.Resolve(): result=" + resolvedResult, "run");
                    resolve(resolvedResult);
                }).catch(function (error) {
                    // A rejected Promise from the user code maps into a
                    // successful promise wrapping a whisk-encoded error.

                    // Special case if the user just called `reject()`.
                    if (!error) {
                        DEBUG.functionEndError("ERROR: reject()", "run");
                        resolve({ error: {}});
                    } else {
                        DEBUG.functionEndError("ERROR: " + error.message, "run");
                        resolve({ error: serializeError(error) });
                    }
                });
            }
        );
    };

    // Helper function to copy a base64-encoded zip file to a temporary location,
    // decompress it into temporary directory, and return the name of that directory.
    // Note that this makes heavy use of shell commands because:
    //   1) Node 0.12 doesn't have many of the useful fs functions.
    //   2) We know in which environment we're running.
    function unzipInTmpDir(base64) {
        DEBUG.functionStart();
        var mkTempCmd = "mktemp -d XXXXXXXX";
        return exec(mkTempCmd).then(function (tmpDir1) {
            return new Promise(
                function (resolve, reject) {
                    var zipFile = path.join(tmpDir1, "action.zip");
                    fs.writeFile(zipFile, base64, "base64", function (err) {
                        if (err) {
                            DEBUG.functionEndError("Promise.reject: " + err.message);
                            reject("There was an error reading the action archive.");
                        }
                        DEBUG.functionEndSuccess("Promise.resolve: zipFile=" + zipFile);
                        resolve(zipFile);
                    });
                }
            );
        }).then(function (zipFile) {
            return exec(mkTempCmd).then(function (tmpDir2) {
                return exec("unzip -qq " + zipFile + " -d " + tmpDir2).then(function (res) {
                    DEBUG.functionEndSuccess("Promise.resolve: tmpDir2=" + tmpDir2);
                    return path.resolve(tmpDir2);
                }).catch(function (error) {
                    DEBUG.functionEndError("Promise.reject: " + error.message);
                    return Promise.reject("There was an error uncompressing the action archive.");
                });
            });
        });
    }

    // Helper function to run shell commands.
    function exec(cmd) {
        DEBUG.functionStart();
        return new Promise(
            function (resolve, reject) {
                child_process.exec(cmd, function (error, stdout, stderr) {
                    DEBUG.dumpObject(cmd,"cmd");
                    if (error) {
                        DEBUG.functionEndError("Promise.reject: " + error.message);
                        reject(stderr.trim());
                    } else {
                        DEBUG.functionEndSuccess("Promise.resolve");
                        resolve(stdout.trim());
                    }
                });
            }
        );
    }
}