| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| var dbg = require('./utils/debug'); |
| var DEBUG = new dbg(); |
| |
| /** |
| * Object which encapsulates a first-class function, the user code for |
| * an action. |
| * |
| * This file (runner.js) must currently live in root directory for nodeJsAction. |
| */ |
| var util = require('util'); |
| var child_process = require('child_process'); |
| var fs = require('fs'); |
| var path = require('path'); |
| const serializeError = require('serialize-error'); |
| |
| 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()); |
| } |
| }); |
| } |
| ); |
| } |
| } |
| |
| module.exports = NodeActionRunner; |