Source: org/terraswarm/accessor/accessors/web/hosts/node/nodeHost.js

// Node.js swarmlet host.
//
// Copyright (c) 2015-2017 The Regents of the University of California.
// All rights reserved.
//
// Permission is hereby granted, without written agreement and without
// license or royalty fees, to use, copy, modify, and distribute this
// software and its documentation for any purpose, provided that the above
// copyright notice and the following two paragraphs appear in all copies
// of this software.
//
// IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
// ENHANCEMENTS, OR MODIFICATIONS.
//

/** Node.js swarmlet host module. To use this, issue the following
 *  command:
 *
 *    var nodeHost = require(path + '/nodeHost.js');
 *
 *  where path is the path to this nodeHost.js file.
 *
 *  The resulting nodeHost object provides a number of functions,
 *  including:
 *
 *  * getAccessorCode(accessorClass): Return the source code for
 *    an accessor, given its fully-qualified class name, e.g. 'net/REST'.
 *
 *  * getTopLevelAccessors(): Return an array of instantiated
 *    top-level accessors (implemented in commonHost.js).
 *
 *  * installIfMissingThenRequire(npmPackage): Check the require path
 *    for a module and if it is not found, invoke npm install.
 *
 *  * instantiate(accessorName, accessorClass): Instantiate an
 *    accessor with an assigned name (an arbitrary string) and
 *    its fully-qualified class name, e.g. 'net/REST'.
 *
 *  * instantiateTopLevel(): Instantiate and return a top-level
 *    accessor.
 *
 *  * startHostShell(): Start an interactive shell on stdin/stdout
 *    to execute commands. Type 'help' in this shell for a list of
 *    supported commands.
 *
 *  * stopAllAccessors(): Call wrapup on all top-level accessors.
 *
 *  See nodeHostShell.js for an example use of this module.
 *
 *  See [https://accessors.org/accessors/wiki/Main/DuktapeHost](https://accessors.org/wiki/Main/DuktapeHost).
 *
 *  @module @accessors-hosts/nodeHost
 *  @author Edward A. Lee, Chris Shaver, Christopher Brooks
 *  @version $$Id$$
 */

//////////////////////////////////////////////////////////////////////////
// Module dependencies.

var path = require('path');
var fs = require('fs');

// Locally defined modules.
var commonHost = require('@terraswarm/accessors/hosts/common');
var deterministicTemporalSemantics = require('@terraswarm/accessors/hosts/common/modules/deterministicTemporalSemantics');

// This Node host allows trusted accessors, which means that any
// accessor whose class name begins with 'trusted/' can invoke the
// function getTopLevelAccessors().
commonHost.allowTrustedAccessors(true);

//////////////////////////////////////////////////////////////////////////
// Module variables.

/** Module variable giving the paths to search for accessors.
 *  By default this module assumes that accessors are stored in
 *  __dirname/../.., where __dirname is the directory where this
 *  script is located.
 */
var accessorPath = [path.join(__dirname, '..', '..')];

/////////////////////////////////////////////////
// Functions are defined below here.
// Please keep them alphabetical.

/** Return the source code for an accessor from its fully qualified name.
 *
 *  A name can be an absolute pathname, a relative pathname or a fully
 *  qualified accessor name such as 'net/REST'.
 *
 *  If name refers to a file that can be read, then the contents of that file
 *  are returned.  If name does not refer to a file that can be read,
 *  then each element of the accessorPath array is prepended to the name
 *  and a file read is attempted.
 *
 *  If there is no such accessor on the accessor search path, then
 *  an exception is thrown.
 *
 *  @param name Fully qualified accessor name, e.g. 'net/REST'.
 */
function getAccessorCode(name) {
    var code;
    // Append a '.js' to the name, if needed.
    if (name.indexOf('.js') !== name.length - 3) {
        name += '.js';
    }

    // Look for the accessor as a regular file.
    // See https://www.terraswarm.org/accessors/wiki/Main/DeploymentNotes#SSHScript
    try {
        code = fs.readFileSync(name, 'utf8');
        return code;
    } catch (error) {
        // Ignore, we will look search the accessorPath.
    }

    for (var i = 0; i < accessorPath.length; i++) {
        var location = path.join(accessorPath[i], name);
        try {
            code = fs.readFileSync(location, 'utf8');
            //console.log('nodeHost.js: Reading accessor at: ' + location);
            break;
        } catch (error) {
            //console.log('nodeHost.js: getAccessorCode(' + name + '): error:');
            continue;
        }
    }
    if (!code) {
        throw new Error('Accessor ' + name + ' not found on path: ' + accessorPath);
    }
    return code;
}

/** Get a resource.
 *  Below are the types of resources that are handled
 *  all other resources will cause an error.
 *
 *  * $KEYSTORE is replaced with $HOME/.ptKeystore
 *
 *  @param uri A specification for the resource.
 */
getResource = function (uri) {

    // We might want the Node host (and in fact all hosts) to allow access to
    // resources that are given with relative paths. By default, these would
    // get resolved relative to the location of the file defining the swarmlet.
    // This might even work in the Browser host with the same source
    // policy.

    if (uri.startsWith('$KEYSTORE') === true) {
        var home = process.env.HOME;
        if (home === undefined) {
            throw new Error('Could not get $HOME from the environment to expand ' + uri);
        } else {
            uri = uri.replace('$KEYSTORE', home + path.sep + '.ptKeystore');
            code = fs.readFileSync(uri, 'utf8');
            return code;
        }
    }
    throw new Error('getResouce(' + uri + ', ' + timeout + ') only supports $KEYSTORE, not ' +
        uri);
}

/** Check the require path for a module and if it is not found, invoke
 *  npm install.
 *
 *  In node, the module loading system caches the return values of stat()
 *  calls.  Thus, if we require() a missing package, then installing it
 *  will not invalidate the cache and calling require() again will not
 *  find our newly installed cache.  As a workaround, for possibly missing
 *  packages, we search the array of paths contained by module.paths.
 *
 *  @param npmPackage the package to be possibly installed using npm
 *  and then required.
 *  @return the value returned by requiring the package
 */
function installIfMissingThenRequire(npmPackage) {
    // console.log('nodeHost.js: installIfMissingThenRequire(' + npmPackage);
    const paths = module.paths;
    var foundIt = false;
    for (var i = 0; i < paths.length; i++) {
        if (fs.existsSync(paths[i] + '/' + npmPackage)) {
            console.log('nodeHost.js: installIfMissingThenRequire(' + npmPackage + '): found ' + paths[i] + '/' + npmPackage);
            foundIt = true;
            break;
        }
    }
    if (!foundIt) {
        var execSync = require('child_process').execSync;
        var npmOutput;
        try {
            console.log('Invoking npm install ' + npmPackage);
            npmOutput = execSync('npm install ' + npmPackage);
            console.log('Done with npm install ' + npmPackage);
        } catch (error) {
            console.log('npm install ' + npmPackage + ' failed: ' + error + '.  A return code of 1 can typically be ignored because package.json is present.');
        }     
    }
    // console.log('nodeHost.js: installIfMissingThenRequire(): about to do require(' + npmPackage + ')');
    var myPackage = require(npmPackage);
    // console.log('nodeHost.js: installIfMissingThenRequire(): returning: ' + myPackage);
    return myPackage;
}

/** Instantiate and return an accessor.
 *  This will throw an exception if there is no such accessor class on the accessor
 *  search path.
 *  @param accessorName The name to give to the instance.
 *  @param accessorClass Fully qualified accessor class name, e.g. 'net/REST'.
 */
function instantiate(accessorName, accessorClass) {
    // The instantiate() function must be defined in
    // web/hosts/nodeHost/nodeHost.js so that require() knows to look
    // in the web/hosts/nodeHost/node_modules.

    // FIXME: The bindings should be a bindings object where require == a requireLocal
    // function that searches first for local modules.
    var bindings = {
        'getResource': getResource,
        'require': require,
    };
    var instance = commonHost.instantiateAccessor(
        accessorName, accessorClass, getAccessorCode, bindings);
    //console.log('nodeHost.js: Instantiated accessor ' + accessorName + ' with class ' + accessorClass);
    return instance;
}

/** Return an accessor and a list of its required modules for purposes of examining
 *  its non-functional characteristics. This is accomplished by instantiating the accessor,
 *  and ignoring any exceptions from require due to modules that do not exist for this host.
 *  
 *  WARNING: An accessor instantiated by this function may be unusable in a swarmlet!
 *  Use instantiate if you want to run the accessor later.
 *
 *  FIXME: The instantiatedInterface still appears to the swarmlet as an ordinary
 *  instantiated accessor. I think we should delete it immediately after getting
 *  its data.
 *
 *
 *  This will throw an exception if there is no such accessor class on the accessor
 *  search path.
 *  @param accessorName The name to give to the instance.
 *  @param accessorClass Fully qualified accessor class name, e.g. 'net/REST'.
 *  @return An object containing the accessor and a unique array of modules used by the accessor.
 */
function instantiateInterface(accessorName, accessorClass) {

    var requireLog = [];
    var instantiateLog = [];

    //An alternative require-like function that records the modules it attempts to
    //load in requireLog and also will not throw an error if the module does not exist.
    //This is desirable because we may wish to examine the interfaces of accesssors,
    //that cannot be run in the node host. Note an error will still be thrown if
    //the accessor uses a required module in its top level or setup function.
    function loggingRequire(path) {
        requireLog.push(path);
        var response;
        try{
            response = require(path);
        } catch(ignoredError){
            response = {};
        }
        return response;
    }

    //Composite Accessors may instantiate other accessors in their setup functions.
    //This local override intercepts those calls and replaces them with calls to instantiateInterface,
    //so require statements won't be a problem.
    function loggingInstantiate(accessorName, accessorClass) {
        instantiateLog.push(accessorClass);
        var face = instantiateInterface(accessorName, accessorClass);

        //Append instantiated sub-accessor's modules and any sub-sub-accessors to logs.
        Array.prototype.push.apply(requireLog ,face.modules);
        Array.prototype.push.apply(instantiateLog ,face.subAccessors);     
        return face.accessor;
    }

    var bindings = {
        'getResource': getResource,
        'require': loggingRequire,
        'instantiate': loggingInstantiate
    };

    //Instantiating an accessor calls its setup function.
    var instance = commonHost.instantiateAccessor(
        accessorName, accessorClass, getAccessorCode, bindings);
    
    //Make array of required modules unique
    function onlyUnique(value, index, self) { 
        return self.indexOf(value) === index;
    }
    var modules = requireLog.filter( onlyUnique );

    //Make array of instantiated accessors unique
    var subAccessors = instantiateLog.filter( onlyUnique );

    return {"modules" : modules, "accessor": instance, "subAccessors": subAccessors};
}


/** Instantiate and return a top-level accessor.
 *  This will throw an exception if there is no such accessor class on the accessor
 *  search path.
 *  @param accessorName The name to give to the instance.
 *  @param accessorClass Fully qualified accessor class name, e.g. 'net/REST'.
 */
function instantiateTopLevel(accessorName, accessorClass) {
    // FIXME: See if we can get rid of instantiateTopLevel
    return instantiate(accessorName, accessorClass);
}

/** Handle calls to exit, Control-C, errors and uncaught exceptions.
 *  The wrapup() method is invoked for all accessors.  The first
 *  exception is reported and process.exitCode is set to non-zero;
 *  @param options Properties for the call.  Properties include cleanup and exit.
 */
function exitHandler(options, err) {
    // console.log("nodeHost.js: exitHandler(" + options + ", " + err + ") process.exitCode: " +  process.exitCode + ' options: ');
    // console.log(options);
    // var myError = new Error("nodeHost.js: In exitHandler()");
    // console.log(myError.stack);

    var accessor,
        composite,
        i,
        initialThrowable = null;
    if (options.cleanup) {
        try {
            commonHost.stopAllAccessors();
        } catch (wrapupError) {
            console.log("nodeHost.js: wrapup() failed: " + wrapupError);
            if (process.exitCode === undefined) {
                process.exitCode = 1;
            }
        }
        if (initialThrowable !== null) {
            console.log("nodeHost.js: while invoking wrapup() of all accessors, an exception was thrown: " +
                initialThrowable + ":" + initialThrowable.stack);
            if (process.exitCode === undefined) {
                process.exitCode = 1;
            }
        }
    }
    // Use kill -30 to display a stack
    if (options.stack) {
        var util = require('util');
        console.log("nodeHost.js: SIGUSR1 was received.");
        // console.log(util.inspect(this, {depth: 15}));
        for (composite in this.process.mainModule.exports.accessors) {
            for (i in this.process.mainModule.exports.accessors[composite].containedAccessors) {
                accessor = this.process.mainModule.exports.accessors[composite].containedAccessors[i];
                console.log("accessor: " + accessor.accessorName);
                //console.log(util.inspect(accessor, {depth: 2}));
                console.log("accessor.outputs: ");
                console.log(util.inspect(accessor.outputs, {
                    depth: 2
                }));
                for (var output in accessor.outputs) {
                    console.log("accessor.outputs: output: ");
                    console.log(output);
                    console.log("accessor.outputs: outputs[output]: ");
                    console.log(accessor.outputs[output]);
                    var destinations = accessor.outputs[output];
                    for (var destination in destinations.destinations) {
                        console.log("output " + output + ", destination: " + destination);
                    }
                }
            }
        }
        err = new Error("SIGUSR1 was received, here's the stack.");
    }
    if (err) {
        if (err.stack === undefined) {
            if (err !== process.exitCode) {
                console.log("nodeHost.js: err: \"" + err + "\" has no stack.");
            }
        } else {
            console.log("nodeHost.js: Error: " + err.stack);
        }
        if (process.exitCode === undefined) {
            process.exitCode = 1;
        }
    } else if (process.exitCode === undefined) {
        process.exitCode = 0;
    }

    if (process.exitCode !== 0) {
        console.log('nodeHost.js: An error occurred: Node will exit returning ' +
            process.exitCode + '.');
    }

    // If we the exitHandler was called with 'cleanup', then we won't exit here,
    // but will exit later.
    if (options.exit) {
        // console.log(new Error("nodeHost.js: exitHandler(): Calling process.exit("
        //        + process.exitCode
        //        + "): Here is the stack so we know why: ").stack);
        process.exit(process.exitCode);
    }
}

// Indicator of whether the interactive host is already running.
var interactiveHostRunning = false;

/** Return the name of this host.
 *
 *  Return the string "Node".
 *
 *  @return In nodeHost.js, return "Node".
 */ 
function getHostName() {
    return "Node";
};

/** Start an interactive version of this host as a shell.
 *  This will produce a prompt on stdout that accepts JavaScript statements
 *  on stdin and executes them.
 */
function startHostShell() {
    if (interactiveHostRunning) {
        console.log('Interactive host is already running.');
        return;
    }
    interactiveHostRunning = true;
    var readline = require('readline');

    // Support auto completion for common commands.
    function completer(line) {
        var completions = [
            'exit',
            'getTopLevelAccessors()',
            'help',
            'instantiate(',
            'provideInput(',
            'setParameter(',
            'quit',
        ];
        var hits = completions.filter(function (candidate) {
            // FIXME: need a better filter.
            return candidate.indexOf(line) === 0;
        });
        // show all completions if none found
        return [hits.length ? hits : completions, line];
    }
    // FIXME: make options passable to startHost()?
    var rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
        completer: completer,
    });

    var self = this;

    // The next bit of code is quite tricky, and is due to
    // Axel Rauschmayer. See
    // https://dzone.com/articles/implementing-command-line-eval
    // The reason it is tricky is that we want to support statements like
    // "var foo = 10" on the command line, and we want the variable foo
    // to be stored as a property of self-contained object so it can be used
    // in subsequent commands. While this can be done using eval() as an
    // indirect call, which stores foo in the global scope, this is not
    // very clean, because it makes it possible to clobber functions and
    // properties of the global scope.
    //
    // This code uses an Ecma 6 "generator," which is a long running
    // function that pauses at "yield" statements. Each yield statement
    // sends a value that is returned by a call to next() on the generator.
    // And each call to next() can pass in a value that will be returned
    // by the generator.  For a clear explanation of
    // ES6 generators, see https://davidwalsh.name/es6-generators
    // The reason that generators solve the problem is that variable
    // assignments become properties of the generator function evalGenerator
    // rather than properties of the global scope. Since this function
    // never returns, its assigned variables are never forgotten.
    // Also, the functions available on the command line can be provided
    // here in a controlled way, including overriding any global functions,
    // if necessary.
    //
    // Note that one side effect of this strategy is that all context variables
    // here (nodeHost, fs, path, accessorPath, etc.) are available in the shell.
    // These are not available to accessors, however, so this seems OK.
    function* evalGenerator() {
        var command = yield;

        // Define functions available to the command line.
        var getTopLevelAccessors = commonHost.getTopLevelAccessors;
        var stopAllAccessors = commonHost.stopAllAccessors;
        var uniqueName = commonHost.uniqueName;

        while (true) {
            try {
                var result = eval(command);
                command = yield result;
            } catch (e) {
                command = yield e.toString();
            }
        }
    }

    function Evaluator() {
        this.evalGen = evalGenerator();
        this.evalGen.next(); // start
    }
    Evaluator.prototype.evaluate = function (str) {
        return this.evalGen.next(str);
    };
    var evaluator = new Evaluator();

    // Emitted whenever a command is entered on stdin.
    rl.on('line', function (command) {
        // Remove any trailing semicolon.
        command = command.replace(/;$/, '');

        ///////////////
        // exit and quit functions.
        // NOTE: \s is whitespace. The 'i' qualifier means 'case insensitive'.
        // Also, tolerate trailing semicolon.
        if (command.match(/^\s*quit\s*$/i) ||
            command.match(/^\s*exit\s*$/i)) {
            console.log('exit');
            interactiveHostRunning = false;
            rl.close();
            // Invoke process.exit() so that exitHandler() in
            // nodeHost.js gets invoked and calls wrapup.
            process.exit(0);
            return;
        }
        if (command.match(/^\s*help\s*$/i)) {
            var helpFile = path.join(__dirname, 'nodeHostShellHelp.txt');
            var helpText = fs.readFileSync(helpFile, 'utf8');
            console.log(helpText);
            rl.prompt();
            return;
        }

        ///////////////
        // Evaluate anything else.
        try {
            var response = evaluator.evaluate(command);
            console.log(response.value);
        } catch (error) {
            console.log(error);
        }
        rl.prompt();
    });
    // Emitted whenever the input stream receives a ^C.
    rl.on('SIGINT', function () {
        rl.question('Are you sure you want to exit? ', function (answer) {
            if (answer.match(/^y(es)?$/i)) {
                rl.close();
                process.exit(0);
                return;
            } else {
                console.log('Cancelled.');
                rl.prompt();
            }
        });
    });
    // Emitted whenever the input stream is sent to the background with ^Z
    // and then continued with fg. Does not work on Windows.
    rl.on('SIGCONT', function () {
        // `prompt` will automatically resume the stream
        rl.prompt();
    });

    console.log('Welcome to the Node swarmlet host (nsh). Type exit to exit, help for help.');
    rl.setPrompt('nsh> ');
    rl.prompt();

    return evaluator;
}

///////////////////////////////////////////////////////////////////////
// Execution handlers.

// If the node host is exiting, then cleanup, which includes invoking wrapup();
process.on('exit', exitHandler.bind(null, {
    cleanup: true
}));

// Catch the Control-C event, which calls exit, which is caught in the line above.
process.on('SIGINT', exitHandler.bind(null, {
    exit: true
}));

// Catch kill -30 and display a stack.  SIGUSR1 is reserved by node to
// start the debugger, but we use it here anyway.
process.on('SIGUSR1', exitHandler.bind(null, {
    stack: true
}));

// Catch any uncaughtExceptions.  If an uncaughtException is caught, is it still uncaught? :-)
process.on('uncaughtException', exitHandler.bind(null, {
    exit: true
}));

/** Instantiate and invoke a composite accessor.
 *
 *  This function is useful for invoking the Node
 *  host on a composite accessor.
 *  This function calls process.exit() upon termination
 *  of the accessor.
 *
 *  @param args An array of command line arguments.
 *  For the values of the arguments, see the documentation
 *  for commonHost.processCommandLineArguments().
 */
function processCommandLineArguments(args) {
    // We use a simple version of this so that nodeHostInvoke.js and
    // ptolemy/cg/kernel/generic/accessor/accessorInvokeSSH in the
    // Cape Code AccessorSSHCodeGenerator are both very small and not
    // likely to change.  By having one function defined in the host,
    // we avoid code duplication.  nashornHost defines a similar method

    // This script is Node-specific because it uses fs.
    var result = commonHost.processCommandLineArguments(args,
        // Argument to read a file.
        function (filename) {
            // FIXME: What if the encoding is not utf8?
            return fs.readFileSync(filename, 'utf8');
        },
        // Argument to instantiate an accessor.
        instantiateTopLevel,
        // Function to call upon termination.
        function () {
            // Note that in the node host, an exit handler
            // will call wrapup on all accessors.
            process.exit(0);
        }
    );
    if (!result) {
        // No accessors were initialized and the keepalive argument
        // was not given, so there is presumably no more to do.
        console.log('No standalone accessors were instantiated');
        process.exit(0);
    }
}

///////////////////////////////////////////////////////////////////////
// Export the module functions.

// Exported from this module:
exports.getAccessorCode = getAccessorCode;
exports.installIfMissingThenRequire = installIfMissingThenRequire;
exports.instantiate = instantiate;
exports.instantiateInterface = instantiateInterface;
exports.instantiateTopLevel = instantiateTopLevel;
exports.processCommandLineArguments = processCommandLineArguments;
exports.startHostShell = startHostShell;
exports.getHostName = getHostName;

// Exported from commonHost:
exports.Accessor = commonHost.Accessor;
exports.getTopLevelAccessors = commonHost.getTopLevelAccessors;
exports.isReifiableBy = commonHost.isReifiableBy;
exports.stopAllAccessors = commonHost.stopAllAccessors;
exports.uniqueName = commonHost.uniqueName;

// FIXME: Should not be needed:
//Make the Accessor constructor visible so that we may use it in the
//Cape Code Accessor Code Generator.
Accessor = commonHost.Accessor;

/**
 *  Below is the creation of a web server that retrieves all the accessors
 *  monitoring information and returns them as a JSON object.
 *
 *  In order to test this service, you need first to decomment the code below.
 *  After running your swarmlet on a node host, you can request from your
 *  browser a web page with the following URL: http://127.0.0.1:8082/monitor/
 *
 *  A JSON object is provided. It shows for each accessor: its name, its 'type'
 *  and all the monitoring information that is stored.
 */

/*var http = require('http');
var url = require('url');

// Create a server
http.createServer(function (request, response) {

        var reqParts = request.url.split("/");

        console.log(reqParts);

        if (reqParts[1] == "monitor") {
                // HTTP Status: 200 : OK
                // Content Type: text/plain
                response.writeHead(200, {'Content-Type': 'text/html'});
                // Write the content of the file to response body

                //console.log(commonHost.getMonitoringInformation());

                // Retrieve all monitoring information
                var allMonitoringInformation = commonHost.getMonitoringInformation();

                // Parse the elements and send them one by one
                Object.keys(allMonitoringInformation).forEach(function (accName) {
                        var accMonitoringInformation = {};
                        accMonitoringInformation[accName] = allMonitoringInformation[accName];
                        console.log(JSON.stringify(accMonitoringInformation));
                        response.write(JSON.stringify(accMonitoringInformation));
                });
        } else {
                response.writeHead(404, {'Content-Type': 'text/html'});
                response.write("Hello, nothing received information!");
        }
        // Send the response body
        response.end();

}).listen(8082);

// Console will print the message
console.log('Server running at http://127.0.0.1:8082/');
*/