143 lines
4.6 KiB
JavaScript
143 lines
4.6 KiB
JavaScript
const vm = require('vm'),
|
|
Flatted = require('flatted'),
|
|
|
|
{ isString, randomNumber } = require('./utils'),
|
|
|
|
bridgeClientCode = require('./bridge-client'),
|
|
delegateTimers = require('./vm-delegate-timers'),
|
|
|
|
ERROR = 'error',
|
|
UVM_DATA_ = '__uvm_data_',
|
|
UVM_DISPATCH_ = '__uvm_dispatch_',
|
|
|
|
/**
|
|
* Convert array or arguments object to JSON
|
|
*
|
|
* @private
|
|
* @param {Array|Argument} arr
|
|
* @return {String}
|
|
*
|
|
* @note This has been held as reference to avoid being misused if modified in global context;
|
|
*/
|
|
jsonArray = (function (arrayProtoSlice, jsonStringify) {
|
|
return function (arr) {
|
|
return jsonStringify(arrayProtoSlice.call(arr));
|
|
};
|
|
}(Array.prototype.slice, Flatted.stringify)),
|
|
|
|
/**
|
|
* @private
|
|
* @param {String} str
|
|
* @return {Array}
|
|
*/
|
|
unJsonArray = (function (jsonParse) {
|
|
return function (str) {
|
|
return jsonParse(str);
|
|
};
|
|
}(Flatted.parse));
|
|
|
|
/**
|
|
* This function equips an event emitter with communication capability with a VM.
|
|
*
|
|
* @param {EventEmitter} emitter -
|
|
* @param {Object} options -
|
|
* @param {String} options.bootCode -
|
|
* @param {vm~Context=} [options._sandbox] -
|
|
* @param {Function} callback -
|
|
*/
|
|
module.exports = function (emitter, options, callback) {
|
|
let code = bridgeClientCode(options.bootCode),
|
|
context = options._sandbox || vm.createContext(Object.create(null)),
|
|
bridgeDispatch;
|
|
|
|
// inject console on debug mode
|
|
options.debug && (context.console = console);
|
|
|
|
// we need to inject the timers inside vm since VM does not have timers
|
|
if (!options._sandbox) {
|
|
delegateTimers(context);
|
|
}
|
|
|
|
try {
|
|
// inject the emitter via context. it will be referenced by the bridge and then deleted to prevent
|
|
// additional access
|
|
context.__uvm_emit = function (args) {
|
|
/* istanbul ignore if */
|
|
if (!isString(args)) { return; }
|
|
|
|
try { args = unJsonArray(args); }
|
|
catch (err) { /* istanbul ignore next */ emitter.emit(ERROR, err); }
|
|
|
|
emitter.emit(...args);
|
|
};
|
|
|
|
vm.runInContext(code, context, {
|
|
timeout: options.bootTimeout
|
|
});
|
|
|
|
// we keep a reference to the dispatcher so that we can preemptively re inject it in case it is deleted
|
|
// by user scripts
|
|
bridgeDispatch = context.__uvm_dispatch;
|
|
}
|
|
catch (err) {
|
|
return callback(err);
|
|
}
|
|
finally { // set all raw interface methods to null (except the dispatcher since we need it later)
|
|
vm.runInContext(`
|
|
__uvm_emit = null; delete __uvm_emit; __uvm_dispatch = null; delete __uvm_dispatch;
|
|
`, context);
|
|
delete context.__uvm_emit;
|
|
delete context.__uvm_dispatch;
|
|
}
|
|
|
|
// since context is created and emitter is bound, we would now attach the send function
|
|
emitter._dispatch = function () {
|
|
const id = UVM_DATA_ + randomNumber(),
|
|
dispatchId = UVM_DISPATCH_ + id;
|
|
|
|
// trigger event if any dispatch happens post disconnection
|
|
if (!context) {
|
|
return this.emit(ERROR, new Error(`uvm: unable to dispatch "${arguments[0]}" post disconnection.`));
|
|
}
|
|
|
|
try {
|
|
// save the data in context. by this method, we avoid needless string and character encoding or escaping
|
|
// issues. this is slightly prone to race condition issues, but using random numbers we intend to solve it
|
|
context[id] = jsonArray(arguments);
|
|
context[dispatchId] = bridgeDispatch;
|
|
|
|
// restore the dispatcher for immediate use!
|
|
vm.runInContext(`
|
|
(function (dispatch, data) {
|
|
${id} = null; (delete ${id});
|
|
${dispatchId} = null; (delete ${dispatchId});
|
|
dispatch(String(data));
|
|
}(${dispatchId}, ${id}));
|
|
`, context, {
|
|
timeout: options.dispatchTimeout
|
|
});
|
|
}
|
|
// swallow errors since other platforms will not trigger error if execution fails
|
|
catch (e) { this.emit(ERROR, e); }
|
|
finally { // precautionary delete
|
|
if (context) {
|
|
delete context[id];
|
|
delete context[dispatchId];
|
|
}
|
|
}
|
|
};
|
|
|
|
emitter._disconnect = function () {
|
|
/* istanbul ignore if */
|
|
if (!context) { return; }
|
|
|
|
// clear only if the context was created inside this function
|
|
!options._sandbox && Object.keys(context).forEach((prop) => {
|
|
delete context[prop];
|
|
});
|
|
context = null;
|
|
};
|
|
|
|
callback(null, emitter);
|
|
};
|