-
-
Save chriscoyier/b805fe91cc832fc97a3af671051e501f to your computer and use it in GitHub Desktop.
var esprima = require('esprima'); | |
function instrument(code) { | |
var LOOP_CHECK = 'if (window.CP.shouldStopExecution(%d)){break;}'; | |
var LOOP_EXIT = "\nwindow.CP.exitedLoop(%d);\n"; | |
var loopId = 1; | |
var patches = []; | |
esprima.parse(code, { | |
range: true, | |
tolerant: false, | |
sourceType: "script", | |
jsx: true | |
}, function (node) { | |
switch (node.type) { | |
case 'DoWhileStatement': | |
case 'ForStatement': | |
case 'ForInStatement': | |
case 'ForOfStatement': | |
case 'WhileStatement': | |
var start = 1 + node.body.range[0]; | |
var end = node.body.range[1]; | |
var prolog = LOOP_CHECK.replace('%d', loopId); | |
var epilog = ''; | |
if (node.body.type !== 'BlockStatement') { | |
// `while(1) doThat()` becomes `while(1) {doThat()}` | |
prolog = '{' + prolog; | |
epilog = '}'; | |
--start; | |
} | |
patches.push({ pos: start, str: prolog }); | |
patches.push({ pos: end, str: epilog }); | |
patches.push({ pos: node.range[1], str: LOOP_EXIT.replace('%d', loopId) }); | |
++loopId; | |
break; | |
default: | |
break; | |
} | |
}); | |
patches.sort(function (a, b) { | |
return b.pos - a.pos; | |
}).forEach(function (patch) { | |
code = code.slice(0, patch.pos) + patch.str + code.slice(patch.pos); | |
}); | |
return code; | |
} | |
exports.handler = function(event, context) { | |
try { | |
var js = event.markup || ""; | |
if (js === "") { | |
context.succeed({ | |
"markup": "" | |
}); | |
} else { | |
context.succeed({ | |
"markup": instrument(event.markup) | |
}); | |
} | |
} catch(e) { | |
var line = 1; | |
try { | |
line = e.lineNumber; | |
} catch(err) { | |
// go on with line number 1 | |
} | |
context.succeed({ | |
"error": e.description, | |
"line": line | |
}); | |
} | |
}; |
"use strict"; | |
if (typeof (window.CP) !== "object") { | |
window.CP = {}; | |
} | |
window.CP.PenTimer = { | |
// If we successfully run for X seconds no need to continue | |
// to monitor because we know the program isn't locked | |
programNoLongerBeingMonitored: false, | |
timeOfFirstCallToShouldStopLoop: 0, | |
_loopExits: {}, | |
// Keep track of how long program spends in single loop w/o an exit | |
_loopTimers: {}, | |
// Give the program time to get started | |
START_MONITORING_AFTER: 2000, | |
// takes into account START_MONITORING_AFTER val | |
STOP_ALL_MONITORING_TIMEOUT: 5000, | |
// tested against pen: xbwYNm, it loops over 200k real loop | |
MAX_TIME_IN_LOOP_WO_EXIT: 2200, | |
exitedLoop: function(loopID) { | |
this._loopExits[loopID] = true; | |
}, | |
shouldStopLoop: function(loopID) { | |
// Once we kill a loop, kill them all, we have an infinite loop and | |
// it must be fixed prior to running again. | |
if (this.programKilledSoStopMonitoring) { | |
return true; | |
} | |
// Program exceeded monitor time, we're in the clear | |
if (this.programNoLongerBeingMonitored) { | |
return false; | |
} | |
// If the loopExit already called return | |
// It's possible for the program to break out | |
if (this._loopExits[loopID]) { | |
return false; | |
} | |
var now = this._getTime(); | |
if (this.timeOfFirstCallToShouldStopLoop === 0) { | |
this.timeOfFirstCallToShouldStopLoop = now; | |
// first call to shouldStopLoop so just exit already | |
return false; | |
} | |
var programRunningTime = now - this.timeOfFirstCallToShouldStopLoop; | |
// Allow program to run unmolested (yup that's the right word) | |
// while it starts up | |
if (programRunningTime < this.START_MONITORING_AFTER) { | |
return false; | |
} | |
// Once the program's run for a satisfactory amount of time | |
// we assume it won't lock up and we can simply continue w/o | |
// checking for infinite loops | |
if (programRunningTime > this.STOP_ALL_MONITORING_TIMEOUT) { | |
this.programNoLongerBeingMonitored = true; | |
return false; | |
} | |
// Second level shit around new hotness logic | |
try { | |
this._checkOnInfiniteLoop(loopID, now); | |
} catch(e) { | |
this._sendErrorMessageToEditor(); | |
this.programKilledSoStopMonitoring = true; | |
return true; | |
} | |
return false; | |
}, | |
_sendErrorMessageToEditor: function() { | |
try { | |
if (this._shouldPostMessage()) { | |
var data = { | |
action: "infinite-loop", | |
line: this._findAroundLineNumber() | |
}; | |
parent.postMessage(JSON.stringify(data), "*"); | |
} else { | |
this._throwAnErrorToStopPen(); | |
} | |
} catch(error) { | |
this._throwAnErrorToStopPen(); | |
} | |
}, | |
_shouldPostMessage: function() { | |
return document.location.href.match(/boomerang/); | |
}, | |
_throwAnErrorToStopPen: function() { | |
throw "We found an infinite loop in your Pen. We've stopped the Pen from running. Please correct it or contact\ support@codepen.io."; | |
}, | |
_findAroundLineNumber: function() { | |
var err = new Error(); | |
var lineNumber = 0; | |
if (err.stack) { | |
// match only against JS in boomerang | |
var m = err.stack.match(/boomerang\S+:(\d+):\d+/); | |
if (m) { | |
lineNumber = m[1]; | |
} | |
} | |
return lineNumber; | |
}, | |
_checkOnInfiniteLoop: function(loopID, now) { | |
if (!this._loopTimers[loopID]) { | |
this._loopTimers[loopID] = now; | |
// We just started the timer for this loop. exit early | |
return false; | |
} | |
var loopRunningTime = now - this._loopTimers[loopID]; | |
if (loopRunningTime > this.MAX_TIME_IN_LOOP_WO_EXIT) { | |
throw "Infinite Loop found on loop: " + loopID; | |
} | |
}, | |
_getTime: function() { | |
return +new Date(); | |
} | |
}; | |
window.CP.shouldStopExecution = function(loopID) { | |
var shouldStop = window.CP.PenTimer.shouldStopLoop(loopID); | |
if( shouldStop === true ) { | |
console.warn("[CodePen]: An infinite loop (or a loop taking too long) was detected, so we stopped its execution. Sorry!"); | |
} | |
return shouldStop; | |
}; | |
window.CP.exitedLoop = function(loopID) { | |
window.CP.PenTimer.exitedLoop(loopID); | |
}; |
@quezo Good to know it could be useful! Let me know if you encounter any problems.
@ariya - I'm working on implementing it right now!
I've found a little weirdness. Check this original JavaScript:
for (var x = 0; i < 10; x++) {
for (var y = 0; y < 10; y++) {
if (x === y) {
} else if (x === 3) {
}
}
}
It comes back fairly malformed:
for (var x = 0; i < 10; x++) {if (window.CP.shouldStopExecutionif (window.CP.shouldStopExecution(1)) break;(2)) break;
for (var y = 0; y < 10; y++) {
if (x ===
window.CP.exitedLoop(1);
y) {
} else if (x === 3) {
}
}
}
window.CP.exitedLoop(2);
Is it the nesting of the for
loops tripping it up?
@chriscoyier seems that I did not paste the correct version, sorry for that. Try it again, it should be good now.
P.S.: Time to turn it into a simple git repo?
@chriscoyier, @ariya FYI, this currently doesn't seem to work well for asynchronous loops, a simple example.: https://codepen.io/noseratio/pen/jRgYMQ.
for (var i = 0; i < 100 && !cancelled; i++) {
contentDiv.innerText = `Page: ${i}`;
await sleep(100);
};
It gets terminated after about ~40 iterations with this error: Uncaught (in promise) We found an infinite loop in your Pen. We've stopped the Pen from running. Please correct it or contact support@codepen.io.
@ariya This is excellent. This change makes the loop detection compatible with any version of JavaScript. Thank you for sharing this solution.