Simon Priet e69a613a37 feat: Created a mini nodeJS server with NewMan for testing without PostMan GUI.
This will mimic a run in a CD/CI environment or docker container.
2021-09-08 14:01:19 +02:00

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);
});
}
};