Skip to content

Instantly share code, notes, and snippets.

@Eunomiac
Forked from VoltCruelerz/AirbagStart.js
Last active July 5, 2021 03:51
Show Gist options
  • Save Eunomiac/6cab31a2eacbf99d3fc2edfc40272e1d to your computer and use it in GitHub Desktop.
Save Eunomiac/6cab31a2eacbf99d3fc2edfc40272e1d to your computer and use it in GitHub Desktop.
Airbag - Roll20 API Crash Handler
/* eslint-disable */
let MarkStart = () => true;
let MarkStop = () => true;
let airIndex = new Error();
/* TO TEMPORARILY DISABLE AIRBAG:
//
// * - Disable the 'AirbagSTOP.js' script in your game's API Scripts page.
// * - Comment out everything below by deleting this end comment marker ⇒ */
// - To reactivate, reverse the above steps.
// ===============================================================================
// AIRBAG - API Crash Handler
//
// Version 1.3.2
//
// By: github.com/VoltCruelerz
//
// Forked by github.com/Eunomiac:
// - chat message is now HTML formatted for easier reading/clarity
// - log message parsed for easier reading
// - ALL line references in the stack trace are converted to local
// ===============================================================================
// ===============================================================================
// Whether or not the code is operational
let codebaseRunning = false;
// System States
const AlertFlag = (content) => `<div style="display: block ; margin: -25px 0px 0px -42px ; height: auto ; min-height: 30px ; min-width: 270px ; width: auto ; text-align: center ; outline: 2px solid rgba( 0 , 0 , 0 , 1 ) ; border: none; padding: 0px ; position: relative;"><span style="display: block;height: 32px;line-height: 34px;width: auto;margin: 0px;padding: 0px 5px;text-align: left;color: rgba( 255 , 255 , 255 , 1 );font-family: Voltaire, Tahoma, sans-serif; font-weight: normal;font-variant: small-caps;font-size: 30px;background-color: rgba( 80 , 80 , 80 , 1 );outline: none;text-shadow: none;box-shadow: none;">${content}</span></div>`;
const rezMsg = AlertFlag("Initializing API ...");
const runMsg = AlertFlag("AirBag Initialized.");
const RandStr = (length = 10) => _.sample("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""), length).join("");
// Log for Airbag
const airLog = (logMsg, chatMsg, dbMsg) => {
log(logMsg);
sendChat(RandStr(3), `/w gm ${chatMsg || logMsg}`);
};
// HTML Styles for Reporting
const HTML = {
ChatBox: content => `<div style="${[
"display: block;",
"width: auto;",
"padding: 5px 5px;",
"margin: -29px 0px 0px -42px;",
"border: 3px outset rgb(255, 0, 0);",
"background-color: rgb(120, 0, 0);",
"position: relative;"
].join(" ")}">${content}</div>`,
TitleBox: content => `<div style="${[
"display: block;",
"height: auto;",
"width: 90%;",
"line-height: 23px;",
"margin: 0px 5%;",
"font-family: Voltaire, Tahoma, sans-serif;",
"font-variant: small-caps;",
"font-size: 16px;",
"text-align: center;",
"text-align-last: center;",
"background-color: rgb(80, 80, 80);",
"color: rgb(255, 255, 255);",
"position: relative;"
].join(" ")}">${content}</div>`,
StackBox: content => `<div style="${[
"display: block;",
"width: auto;",
"padding: 5px 0px 5px 3px;",
"font-family: Input, 'Trebuchet MS', sans-serif;",
"font-size: 9px;",
"background-color: rgb(255, 255, 255);",
"border: 2px solid rgb(0,0,0);",
"line-height: 14px;",
"position: relative;"
].join(" ")}">${content}</div>`,
MessageBox: content => `<div style="${[
"display: block;",
"width: auto;",
"padding: 5px 0px 5px 3px;",
"font-family: Voltaire, Tahoma, sans-serif;",
"font-size: 12px;",
"text-align: center;",
"text-align-last: center;",
"background-color: rgb(200, 200, 200);",
"border: 2px solid rgb(0,0,0);",
"line-height: 14px;",
"color: black;",
"position: relative;"
].join(" ")}">${content}</div>`
}
// ===========================================================================
// Line number Handling
// ===========================================================================
// Denote the occupation ranges of the installed scripts that Airbag insulates
let scriptRanges = [],
scriptNames = []
// Line Number Parser
const GetScriptLine = (traceable, markMode) => {
const match = (traceable && traceable.stack || "").match(/apiscript.js:(\d+)/g) || ["", ""];
if (markMode) {
return parseInt(match[1].split(":")[1]) || 0;
}
return parseInt(match[0].split(":")[1]) || 0;
};
// The last range entry that was added
let lastStart = {
Name: "AirbagStart",
StartLine: GetScriptLine(airIndex, false),
StopLine: -1
};
// Manual insert since Airbag can't use its own MarkStart function
scriptRanges.push(lastStart);
// Available for dependent scripts.
MarkStart = (scriptName) => {
scriptNames.push(scriptName)
let index = new Error();
let line = GetScriptLine(index, true);
lastStart = {
Name: scriptName,
StartLine: line,
StopLine: -1
};
scriptRanges.push(lastStart);
};
// Available for dependent scripts
MarkStop = (scriptName) => {
let index = new Error();
let line = GetScriptLine(index, true);
lastStart.StopLine = line;
};
// Prints the list of scripts and their line number ranges
// (called at end of AirbagStop)
const printScriptRanges = () => {
scriptRanges.forEach((range) => {
const msg = range.StopLine > 0
? `[${range.StartLine}, ${range.StopLine}]`
: `[${range.StartLine}, ???]`;
log("Airbag Handling " + range.Name + ": " + msg);
});
};
// Converts a global line number to a local one
const ConvertGlobalLineToLocal = (gline) => {
let prevRange = {
Name: "PREV_RANGE",
StartLine: -1,
StopLine: -1
};
for(var i = 0; i < scriptRanges.length; i++) {
let curRange = scriptRanges[i];
log(`[${i}] Converting Global Line ${gline} to Local: searching in "${prevRange.Name}" after line ${prevRange.StartLine}`)
// Checking equals in both directions because of minification
if (gline >= prevRange.StartLine && gline <= curRange.StartLine) {
if (prevRange.StartLine === prevRange.StopLine) {
log(`Airbag has detected a minified file for ${prevRange.Name}. Line estimation may be inaccurate.`);
return {
Name: false,
Line: false
}
return {
Name: JSON.stringify(prevRange),
Line: JSON.stringify(prevRange.StartLine)
}
}
let localLine = gline - prevRange.StartLine + 1;
// Check to see if there was an unmarked script in between
// If an author didn't flag the end of the file, blame them
if (prevRange.StopLine === -1 || prevRange.StopLine >= gline) {
//log(`Global[${gline}] => ${prevRange.Name}[${localLine}]`);
return {
Name: prevRange.Name,
Line: localLine
};
} else {
//log(`Global[${gline}] => UNKNOWN SCRIPT between ${prevRange.Name} and ${curRange.Name} at "local" line ${localLine}`);
return {
Name: prevRange.Name,
Line: localLine
};
}
}
prevRange = curRange;
}
return {
Name: "Unknown Script",
Line: -1
};
};
// ===========================================================================
// Function shadows
// ===========================================================================
const airOn = on;
const airSetTimeout = setTimeout;
const airSetInterval = setInterval;
const airClearTimeout = clearTimeout;
const airClearInterval = clearInterval;
// On Registrations
let onRegistrations = [];// Erased after Airbag deployed
let onRegisteredTypes = [];// NOT erased after Airbag deployed (if it was, we'd get double-registrations)
// Builds a handler for the specified type
const getAirbagOnHandler = (eventType) => {
// The master handler that wraps the registrations of the scripts
const airbagOnHandler = (...handlerArgs) => {
let type = eventType;
try{
// Iterate over the registrations. If they match the type,
// execute them.
onRegistrations.forEach((registration) => {
if (registration.Type === type) {
registration.UserHandler.apply(null, Array.prototype.slice.call(handlerArgs));
}
});
} catch (e) {
handleCrash(e);
}
};
return airbagOnHandler;
}
// Delay registrations
let airDelays = [];
let airIntervals = [];
// Attempt to perform the delayed function
const airDelayHandler = (func, params) => {
if (!codebaseRunning) return;
try {
(func || (() => null)).apply(null, Array.prototype.slice.call(params));
} catch(e) {
handleCrash(e);
}
};
// Airbag codebase reboot command handler
on("chat:message", (msg) => {
if (msg.type !== "api") return;
if (msg.content !== "!airbag") return;
codebase();
});
// Halt codebase()'s operations, cancel async tasks, and alert user
const handleCrash = (obj) => {
log("Handling Crash...");
const {src, stackLines, filteredStackLines} = processStack(obj) || {src: false, stackLines: false, filteredStackLines: false}
if (src === false)
return false
const rawErrorMsg = _.compact([
"[AIRBAG",
src.Line > 0 ? `at ${src.Name}:${src.Line}]` : "]",
`▌${obj.message}▐`,
`STACK: ${stackLines.join(" ").replace(/\s@@/gu, " ").replace(/[^\s\(]+underscore\.js:/gu, "_:").replace(/\(\s+/gu, "(")}`
]).join(" ").replace(/\n/gu, "")
const styledErrorMsg = HTML.ChatBox(_.compact([
HTML.TitleBox(`AIRBAG DEPLOYED${src.Line > 0 ? ` at<br>${src.Name}: ${src.Line}` : ""}`),
HTML.MessageBox(obj.message),
HTML.StackBox(filteredStackLines.join("\n")).
replace(/\n/gu, "<br>").
replace(/<br>@@/gu, "").
replace(/<br>at\b/gu, "<br><i><span style='color: rgb(150, 150, 150); display: inline-block; width: auto;'> @ </span></i>").
replace(/[^\s\(]+underscore\.js:(\d*?):/gu, "<span style='color: rgb(0,0,255);'>_</span>@@<span style='color: rgb(0,0,255);'>$1</span>@@").
replace(/[^\s\(]+firebase-node\.js:(\d*?):/gu, "<span style='color: rgb(0,195,0);'>firebase</span>@@<span style='color: rgb(0,195,0);'>$1</span>@@").
replace(/\/home\/node\/d20-api-server\/api\.js/gu, "API").
replace(/(\(?)([^:\.\s]*?):(\d*?):/gu, "$1<b><span style='color: rgb(255,0,0);'>$2</span>:<span style='color: rgb(255,0,0);'>$3</span></b>:").
replace(/@@/gu, ":").
replace(/relative;">\s*?<br>/gu, "relative;\">"),
HTML.TitleBox("[ REBOOT API ](!airbag)")
]).join(""))
const debugErrorMsg = _.compact([
`<b>[AIRBAG ${src.Line > 0 ? `at ${src.Name}:${src.Line}]` : "]"}</b>`,
`▌${obj.message}▐`,
"<b>STACK:</b>",
stackLines.join("<br>").replace(/<br>@@/gu, "")
]).join("<br>").replace(/\n/gu, "")
airLog(rawErrorMsg, styledErrorMsg, debugErrorMsg)
},
processStack = (obj) => {
let globalLine = GetScriptLine(obj, false);
let src = ConvertGlobalLineToLocal(globalLine);
if (!src.Name)
return false
let stackLines = _.map((obj && obj.stack || "").split(/\n|apiscript\.js/gu), v => {
if (v.startsWith(":")) {
const globalLineNum = parseInt(v.match(/^:(\d+):/u)[1]) || 0,
localLine = ConvertGlobalLineToLocal(globalLineNum)
if (!localLine.Name)
return false
return v.replace(/^:\d+:/gu, `@@${localLine.Name}:${localLine.Line}:`).trim()
}
return v.trim()
})
const concatLines = stackLines.join("\n").replace(/\n/gu, "<br>").replace(/<br>@@/gu, "").split("<br>"),
filteredStackLines = []
for (const line of concatLines) {
let lineCheck = false
if (!line.startsWith("at"))
lineCheck = true
else
for (const scriptName of scriptNames.filter(x => x !== "AirbagStop"))
if (line.toLowerCase().includes(scriptName.toLowerCase())) {
lineCheck = true
break
}
if (lineCheck)
filteredStackLines.push(line)
}
filteredStackLines.shift()
filteredStackLines[0] = `<br>${filteredStackLines[0]}`
return {globalLine, src, stackLines, filteredStackLines}
}
// ===========================================================================
// Code Base
// ===========================================================================
let codebase = (errorMsg) => {
if (codebaseRunning) return;
codebaseRunning = true;
if (errorMsg)
handleCrash(errorMsg)
// airLog(rezMsg);
// ===========================================================================
// Function shadows
// ===========================================================================
// ===========================================================================
// on(type, handler)
// DESC: Register in Airbag's internal list that the script wanted to subscribe
// to the event in question.
const on = (type, userHandler) => {
// Add a new registration
let registration = {
Type: type,
UserHandler: userHandler
};
onRegistrations.push(registration);
// Airbag registers for the events on behalf of the script calling on() if
// it hasn't already registered to this type.
if (!onRegisteredTypes[type]) {
log("Airbag is Registering on " + type + " for the first time.");
onRegisteredTypes[type] = true;
let handler = getAirbagOnHandler(type);
airOn(type, handler);
}
};
// ===========================================================================
// setTimeout(func, delay, ...params)
// DESC: Schedule Airbag's timeout handler
const setTimeout = (func, delay, ...params) => {
let delayRef = airSetTimeout(airDelayHandler, delay, func, params);
airDelays.push(delayRef);
return delayRef;
};
// ===========================================================================
// setTimeout(func, delay, ...params)
// DESC: remove from Airbag's memory, then clear the schedule
const clearTimeout = (delayRef) => {
airDelays = airDelays.filter(item => item !== delayRef);
airClearTimeout(delayRef);
};
try {
MarkStop("AirbagStart");
/* eslint-enable */
MarkStart('AirbagStop');
airLog(runMsg);
} catch (e) {
handleCrash(e);
}
};
codebase();
MarkStop('AirbagStop');
printScriptRanges();
@Eunomiac
Copy link
Author

Eunomiac commented Aug 9, 2019

Two suggested enhancements that polish up the Airbag crash report (I've made some tweaks since posting this fork; the image and description below applies to the most current revision):

  1. Converts ALL line numbers in the stack report to local (using ConvertGlobalLineToLocal), replacing all instances of "apiscript.js" with the script's local name and line number, as well as shortening the full path references to underscore.js and firebase-node.js (to "_" and "firebase", respectively).
  2. HTML formatting for the stack report and crash message, condensing it into a single chat message with smaller font size. (I also created a separate report message for logging; no HTML code gets sent to the console!)

Issues

  • Occasionally, the stack message is in a form that doesn't play perfectly well with the formatting substitutions I've made. However, this is rare, only has a limited effect on the formatted output, and invariably involves references to "under the hood" scripts that most API scripters won't worry about anyways (much like references to underscore.js or firebase-node.js)

Chat Message:
image

Console Output
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment