Skip to content

Commit

Permalink
Support Promise execution in the sandbox.
Browse files Browse the repository at this point in the history
Promise.prototype.then arguments are executed in a microtask queue,
pushed to the end of the current event loop tick. The async
lifetime tracking in the sandbox did not factor this into
consideration. Certain completion functions needed to be deferred
with setImmediate(), which will add to the task queue and
execute after the current microtask queue drains.

Calls to global timers also needed to be wrapped in a scoped context.
Due to the setImmediate() delay, the bridge may disconnect when the
context is disposed before the completion timer fires. Disconnecting
the bridge removes all globals. This prevents reference errors from
happening on the final tick.

Original behavior of execution.return.async remains.  It will only
be set to `true` if there's a pending timer that hasn't been
previously cleared.
  • Loading branch information
kevinswiber committed Nov 10, 2022
1 parent 3f95cbf commit d84220c
Show file tree
Hide file tree
Showing 4 changed files with 354 additions and 19 deletions.
16 changes: 10 additions & 6 deletions lib/sandbox/execute-context.js
Expand Up @@ -56,13 +56,17 @@ module.exports = function (scope, code, execution, console, timers, pmapi, onAss
// call this hook to perform any post script execution tasks
legacy.finish(scope, pmapi, onAssertion);

// if timers are running, we do not need to proceed with any logic of completing execution. instead we wait
// for timer completion callback to fire
if (execution.return.async) {
return err && timers.error(err); // but if we had error, we pass it to async error handler
function complete () {
// if timers are running, we do not need to proceed with any logic of completing execution. instead we wait
// for timer completion callback to fire
if (execution.return.async) {
return err && timers.error(err); // but if we had error, we pass it to async error handler
}

// at this stage, the script is a synchronous script, we simply forward whatever has come our way
timers.terminate(err);
}

// at this stage, the script is a synchronous script, we simply forward whatever has come our way
timers.terminate(err);
timers.wrapped.setImmediate(complete);
});
};
6 changes: 4 additions & 2 deletions lib/sandbox/execute.js
Expand Up @@ -143,6 +143,8 @@ module.exports = function (bridge, glob) {
let waiting,
timers;

execution.return.async = false;

// create the controlled timers
timers = new PostmanTimers(null, function (err) {
if (err) { // propagate the error out of sandbox
Expand All @@ -153,7 +155,7 @@ module.exports = function (bridge, glob) {
execution.return.async = true;
}, function (err, dnd) {
// clear timeout tracking timer
waiting && (waiting = clearTimeout(waiting));
waiting && (waiting = timers.wrapped.clearTimeout(waiting));

// do not allow any more timers
if (timers) {
Expand All @@ -180,7 +182,7 @@ module.exports = function (bridge, glob) {

// if a timeout is set, we must ensure that all pending timers are cleared and an execution timeout event is
// triggered.
_.isFinite(options.timeout) && (waiting = setTimeout(function () {
_.isFinite(options.timeout) && (waiting = timers.wrapped.setTimeout(function () {
timers.terminate(new Error('sandbox: ' +
(execution.return.async ? 'asynchronous' : 'synchronous') + ' script execution timeout'));
}, options.timeout));
Expand Down
22 changes: 11 additions & 11 deletions lib/sandbox/timers.js
Expand Up @@ -114,7 +114,6 @@ function Timerz (delegations, onError, onAnyTimerStart, onAllTimerEnd) {
total = 0, // accumulator to keep track of total timers
pending = 0, // counters to keep track of running timers
sealed = false, // flag that stops all new timer additions
wentAsync = false,
computeTimerEvents;

// do special handling to enable emulation of immediate timers in hosts that lacks them
Expand Down Expand Up @@ -158,11 +157,15 @@ function Timerz (delegations, onError, onAnyTimerStart, onAllTimerEnd) {
computeTimerEvents = function (increment, clearing) {
increment && (pending += increment);

if (pending === 0 && computeTimerEvents.started) {
!clearing && (typeof onAllTimerEnd === FUNCTION) && onAllTimerEnd();
computeTimerEvents.started = false;
function maybeEndAllTimers () {
if (pending === 0 && computeTimerEvents.started) {
!clearing && (typeof onAllTimerEnd === FUNCTION) && onAllTimerEnd();
computeTimerEvents.started = false;
}
}

return;
if (pending === 0 && computeTimerEvents.started) {
timers.setImmediate(maybeEndAllTimers);
}

if (pending > 0 && !computeTimerEvents.started) {
Expand All @@ -187,8 +190,6 @@ function Timerz (delegations, onError, onAnyTimerStart, onAllTimerEnd) {
args = arrayProtoSlice.call(arguments);

args[0] = function () {
wentAsync = true; // mark that we did go async once. this will ensure we do not pass erroneous events

// call the actual callback with a dummy context
try { callback.apply(dummyContext, staticTimerFunctions[name] ? arguments : null); }
catch (e) { onError && onError(e); }
Expand All @@ -207,9 +208,6 @@ function Timerz (delegations, onError, onAnyTimerStart, onAllTimerEnd) {
}
};

// for static timers
staticTimerFunctions[name] && (wentAsync = true);

// call the underlying timer function and keep a track of its irq
running[id] = timers[('set' + name)].apply(this, args);
args = null; // precaution
Expand Down Expand Up @@ -241,7 +239,7 @@ function Timerz (delegations, onError, onAnyTimerStart, onAllTimerEnd) {
catch (e) { onError(e); }

// decrement counters and call the clearing timer function
computeTimerEvents(-1, !wentAsync);
computeTimerEvents(-1);

args = underLyingId = null; // just a precaution
};
Expand Down Expand Up @@ -277,6 +275,8 @@ function Timerz (delegations, onError, onAnyTimerStart, onAllTimerEnd) {
return pending;
};

this.wrapped = timers;

/**
* @memberof Timerz.prototype
*/
Expand Down

0 comments on commit d84220c

Please sign in to comment.