171 lines
6.3 KiB
JavaScript
171 lines
6.3 KiB
JavaScript
const _ = require('lodash'),
|
|
uuid = require('./vendor/uuid'),
|
|
UniversalVM = require('uvm'),
|
|
PostmanEvent = require('postman-collection').Event,
|
|
teleportJS = require('teleport-javascript'),
|
|
bootcode = require('./bootcode'),
|
|
|
|
TO_WAIT_BUFFER = 500, // time to wait for sandbox to declare timeout
|
|
EXECUTION_TIMEOUT_ERROR_MESSAGE = 'sandbox not responding',
|
|
BRIDGE_DISCONNECTING_ERROR_MESSAGE = 'sandbox: execution interrupted, bridge disconnecting.';
|
|
|
|
class PostmanSandbox extends UniversalVM {
|
|
constructor () {
|
|
super();
|
|
|
|
this._executing = {};
|
|
}
|
|
|
|
initialize (options, callback) {
|
|
// ensure options is an object and is shallow cloned
|
|
options = _.assign({}, options);
|
|
this.debug = Boolean(options.debug);
|
|
|
|
// set the dispatch timeout of UVM based on what is set in options unless original options sends the same
|
|
_.isFinite(options.timeout) && (options.dispatchTimeout = this.executionTimeout = options.timeout);
|
|
|
|
super.connect(options, (err, context) => {
|
|
if (err) { return callback(err); }
|
|
context.ping((err) => {
|
|
// eslint-disable-next-line callback-return
|
|
callback(err, context);
|
|
context = null;
|
|
});
|
|
});
|
|
}
|
|
|
|
ping (callback) {
|
|
const packet = uuid(),
|
|
start = Date.now();
|
|
|
|
this.once('pong', (echo) => {
|
|
callback((echo !== packet ? new Error('sandbox: ping packet mismatch') : null), Date.now() - start, packet);
|
|
});
|
|
|
|
this.dispatch('ping', packet);
|
|
}
|
|
|
|
/**
|
|
* @param {Event|String} target - can optionally be the code to execute
|
|
* @param {Object} options -
|
|
* @param {String} options.id -
|
|
* @param {Boolean} options.debug -
|
|
* @param {Number} options.timeout -
|
|
* @param {Object} options.cursor -
|
|
* @param {Object} options.context -
|
|
* @param {Boolean} options.serializeLogs -
|
|
* @param {Function} callback -
|
|
*/
|
|
execute (target, options, callback) {
|
|
if (_.isFunction(options) && !callback) {
|
|
callback = options;
|
|
options = null;
|
|
}
|
|
|
|
!_.isObject(options) && (options = {});
|
|
!_.isFunction(callback) && (callback = _.noop);
|
|
|
|
// if the target is simple code, we make a generic event out of it
|
|
if (_.isString(target) || _.isArray(target)) {
|
|
target = new PostmanEvent({ script: target });
|
|
}
|
|
// if target is not a code and instead is not something that can be cast to an event, it is definitely an error
|
|
else if (!_.isObject(target)) {
|
|
return callback(new Error('sandbox: no target provided for execution'));
|
|
}
|
|
|
|
const id = _.isString(options.id) ? options.id : uuid(),
|
|
executionEventName = 'execution.result.' + id,
|
|
consoleEventName = 'execution.console.' + id,
|
|
executionTimeout = _.get(options, 'timeout', this.executionTimeout),
|
|
cursor = _.clone(_.get(options, 'cursor', {})), // clone the cursor as it travels through IPC for mutation
|
|
debugMode = _.has(options, 'debug') ? options.debug : this.debug;
|
|
|
|
let waiting;
|
|
|
|
// set the execution id in cursor
|
|
cursor.execution = id;
|
|
|
|
// set execution timeout and store the interrupt in a global object (so that we can clear during dispose)
|
|
// force trigger of the `execution.${id}` event so that the normal error flow is taken
|
|
this._executing[id] = _.isFinite(executionTimeout) ? (waiting = setTimeout(() => {
|
|
waiting = null;
|
|
this.emit.bind(executionEventName, new Error(EXECUTION_TIMEOUT_ERROR_MESSAGE));
|
|
}, executionTimeout + TO_WAIT_BUFFER)) : null;
|
|
|
|
// @todo decide how the results will return in a more managed fashion
|
|
// listen to this once, so that subsequent calls are simply dropped. especially during timeout and other
|
|
// errors
|
|
this.once(executionEventName, (err, result) => {
|
|
waiting && (waiting = clearTimeout(waiting)); // clear timeout interrupt
|
|
if (Object.hasOwnProperty.call(this._executing, id)) { // clear any pending timeouts
|
|
this._executing[id] && clearTimeout(this._executing[id]);
|
|
delete this._executing[id];
|
|
}
|
|
|
|
this.emit('execution', err, id, result);
|
|
callback(err, result);
|
|
});
|
|
|
|
this.on(consoleEventName, (cursor, level, args) => {
|
|
if (_.get(options, 'serializeLogs')) {
|
|
return this.emit('console', cursor, level, args);
|
|
}
|
|
|
|
args = teleportJS.parse(args);
|
|
args.unshift('console', cursor, level);
|
|
|
|
// eslint-disable-next-line prefer-spread
|
|
this.emit.apply(this, args);
|
|
});
|
|
|
|
// send the code to the sandbox to be intercepted and executed
|
|
this.dispatch('execute', id, target, _.get(options, 'context', {}), {
|
|
cursor: cursor,
|
|
debug: debugMode,
|
|
timeout: executionTimeout,
|
|
legacy: _.get(options, 'legacy')
|
|
});
|
|
}
|
|
|
|
dispose () {
|
|
_.forEach(this._executing, (irq, id) => {
|
|
irq && clearTimeout(irq);
|
|
|
|
// send an abort event to the sandbox so that it can do cleanups
|
|
this.dispatch('execution.abort.' + id);
|
|
|
|
// even though sandbox could bubble the result event upon receiving abort, that would reduce
|
|
// stability of the system in case sandbox was unresponsive.
|
|
this.emit('execution.result.' + id, new Error(BRIDGE_DISCONNECTING_ERROR_MESSAGE));
|
|
});
|
|
|
|
this.disconnect();
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
/**
|
|
* Creates a new instance of sandbox from the options that have been provided
|
|
*
|
|
* @param {Object=} [options] -
|
|
* @param {Function} callback -
|
|
*/
|
|
createContext (options, callback) {
|
|
if (_.isFunction(options) && !callback) {
|
|
callback = options;
|
|
options = {};
|
|
}
|
|
|
|
options = _.clone(options);
|
|
bootcode((err, code) => {
|
|
if (err) { return callback(err); }
|
|
if (!code) { return callback(new Error('sandbox: bootcode missing!')); }
|
|
|
|
options.bootCode = code; // assign the code in options
|
|
|
|
new PostmanSandbox().initialize(options, callback);
|
|
});
|
|
}
|
|
};
|