Advertisement Create a free account

[Script] Airbag - API Crash Handler

1552604992

Edited 1553168499
Michael G.
Pro
API Scripter
We all try to make our code as stable as possible, but sometimes crashes still happen.  API crashes can easily stall a game, lead to confusion, slow script development, break game immersion, and cause enormous frustration as you struggle to understand what actually went wrong.  Users receive no warning a crash has occurred and must instead figure it out on their own and then navigate to the API page to restart the API.  Even then, all you have is a single console line lacking formatting, making it difficult to draw conclusions regarding the source of the problem. Airbag Airbag is a two-part script that wraps the rest of your codebase, isolating the fragile API from any exceptions thrown by your installed scripts and providing direct insight to the user when a crash occurs as well as the ability to force an API restart. How Roll20's API concatenates all of your scripts into a single enormous file, hence the sometimes-astronomical line numbers when you receive an exception.  Ordinarily, all scripts are self-contained and are themselves compilable, but Airbag's halves are not individually compilable, relying on each other to be valid code.  By sandwiching the rest of your API code in the middle, Airbag functions as an oversized try-catch block.  This means that any exception that occurs within your code will be caught by Airbag's catch block, allowing Airbag to inform the user and prompt them to restart the dead scripts. Operation Should an exception occur while Airbag is installed, Airbag will catch the exception, dump the message and stack trace to the console log and chat log, and finally prompt the GM to restart the API at their leisure with a chat button. Code To run Airbag, you must install both scripts.  AirbagStart MUST be the very first script installed in a game (unfortunately, this means uninstalling and reinstalling all your existing scripts if you already have some).  Similarly, AirbagEnd MUST be the very last script installed.  This allows them to wrap the rest of your scripts.  If you have scripts that are outside the Airbag sandwich, Airbag will not be able to catch the exceptions they throw. AirbagStart  // =============================================================================== // AIRBAG - API Crash Handler // // Version 1.1 // // By: github.com/VoltCruelerz // =============================================================================== // Whether or not the code is operational let codebaseRunning = false ; // Log for Airbag const airLog = ( msg ) => { log ( msg ); sendChat ( "Airbag" , '/w gm ' + msg ); }; // Constant declarations const rezMsg = "[API IS STARTING]" ; const runMsg = "[API OPERATIONAL]" ; // Function shadows const airOn = on ; const airSetTimeout = setTimeout ; const airSetInterval = setInterval ; const airClearTimeout = clearTimeout ; const airClearInterval = clearInterval ; on ( 'chat:message' , ( msg ) => { if ( msg . type !== 'api' ) return ; if ( msg . content !== '!airbag' ) return ; codebase (); }); const handleCrash = ( e ) => { log ( 'Handling Crash...' ); codebaseRunning = false ; const properties = [ 'MSG: ' + e . message + ' \n ' , ' \n ==================== \n ' , 'STK: ' + e . stack + ' \n ' , ' \n ==================== \n ' ]; const errMsg = "[AIRBAG DEPLOYED] \n " + properties + "[Reboot API](!airbag)" ; airLog ( errMsg ); } let codebase = () => { if ( codebaseRunning ) return ; codebaseRunning = true ; airLog ( rezMsg ); // Function shadows const on = ( type , userHandler ) => { let airHandler = (... airArgs ) => { try { userHandler . apply ( null , Array . prototype . slice . call ( airArgs )); } catch ( e ) { handleCrash ( e ); } }; airOn ( type , airHandler ); }; try { AirbagEnd  airLog ( runMsg ); } catch ( e ) { handleCrash ( e ); } }; codebase ();
1552606274
GiGs
Pro
Sheet Author
Interesting idea. Are scripts always loaded in the order they have been installed?
1552610827

Edited 1552617948
Michael G.
Pro
API Scripter
Seem to be.  In my testing, I had 6 scripts running and they executed in the order they were installed despite that order being neither alpha nor anti-alpha.  Setting scripts to active/inactive once installed did not affect the order, so it really does appear to simply iterate over them in installation order.  Theoretically, you could probably change the order up a bit with hoisting, but I do not believe JS has any means by which a standalone-compilable script could escape the Airbag sandwich.
1552659325
The Aaron
Forum Champion
API Scripter
That is a neat idea, particularly the restart part.
Its causing my sandbox to crash
1552682085

Edited 1552682191
Michael G.
Pro
API Scripter
How odd...   I guess search your scripts for .N ?  Maybe something's calling eval?  I can't imagine what else it might be that would do that.  Or add me as a GM to your game, maybe and I can try to take a look?
1552682891
The Aaron
Forum Champion
API Scripter
The only thing in the repo with .N is DLEllipseDrawer: //treehugger.js //Author: Tim Matchen /*Use: Simply draw an ellipse on the dynamic lighting layer and the script replaces the ellipse with a n-sided polygon approximating the ellipse. The default number of sides is 20; this can be adjusted using the command !treehugger n, where n is the desired number of sides. For example, !treehugger 10 would generate 10-sided polygons instead of 20.*/ on("ready",function(){ var gc = globalconfig && globalconfig.dlellipsedrawer; if(isNaN(gc .N ) != 1){ var n = Math.ceil(gc .N ); } else{ var n = 20; log("Invalid input from globalconfig! Using n = 20") } log("Treehugger is up and running!") on("add:path",function(obj){ /* ... */
The Aaron said: The only thing in the repo with .N is DLEllipseDrawer: //treehugger.js //Author: Tim Matchen /*Use: Simply draw an ellipse on the dynamic lighting layer and the script replaces the ellipse with a n-sided polygon approximating the ellipse. The default number of sides is 20; this can be adjusted using the command !treehugger n, where n is the desired number of sides. For example, !treehugger 10 would generate 10-sided polygons instead of 20.*/ on("ready",function(){ var gc = globalconfig && globalconfig.dlellipsedrawer; if(isNaN(gc .N ) != 1){ var n = Math.ceil(gc .N ); } else{ var n = 20; log("Invalid input from globalconfig! Using n = 20") } log("Treehugger is up and running!") on("add:path",function(obj){ /* ... */ Yea that was the culprit thank you
1552684363

Edited 1552689689
Michael G.
Pro
API Scripter
Update : nevermind.  Got PM'd about it. Airbag is fine.  The issue is unrelated. Original post... How would that crash out Airbag though? Having said that, that's such a weird way to check to see if...  Honestly, I don't even know what  that's trying to do. var gc = globalconfig && globalconfig . dlellipsedrawer ;//This will try to get .dlellipsedrawer, but I can't find a definition for // that anywhere. So this'll be falsy. if ( isNaN ( gc . N ) != 1 ){// .N won't exist. var n = Math . ceil ( gc . N );// this is just setting a pointless temp variable to something that'll crash. }
1552760347
Jakob
Pro
Sheet Author
API Scripter
Well, isNaN(undefined) is true, and true == 1, so this thing won't execute ... ah, this is JavaScript art :D:D:D.
1552931420

Edited 1552932433
Ammo
Pro
EllipseDrawer is trying to see if globalconfig.dlelllipsedrawer.N is configured (i.e. not undefined and a valid number) to use as the value of 'n' and otherwise set it to 20.   It crashes because it does not check if globalconfig.dlellipsedrawer actually exists and it does not.  Hence this ends up being '<undefined>.N'  It isn't a temp variable, since var variables aren't local in JavaScript.   I assume the value is used further down as the default value for the command lne argument 'n'.   Sandwich is a cool idea.  Do exceptions thrown in event handlers on(... , ...) bubble up back to the block where the => arrow function is defined in JavaScript?   
1552934765
keithcurtis
Forum Champion
Quoting the Aaron from another thread, because this is just such a useful idea for installation: The Aaron  said: Side note, you can probably modify the first script tab you have installed into the start for api-crash-handler and just append the replaced script to the end before adding the end part of api-crash-handler.  Might be much less effort. =D
1553081575

Edited 1553137492
Michael G.
Pro
API Scripter
keithcurtis said: Quoting the Aaron from another thread, because this is just such a useful idea for installation: The Aaron  said: Side note, you can probably modify the first script tab you have installed into the start for api-crash-handler and just append the replaced script to the end before adding the end part of api-crash-handler.  Might be much less effort. =D That's a good idea!  Thanks!
1553138714

Edited 1553139017
Michael G.
Pro
API Scripter
Ammo said: [snip] Sandwich is a cool idea.  Do exceptions thrown in event handlers on(... , ...) bubble up back to the block where the => arrow function is defined in JavaScript?    After testing, it seems that event-driven functions aren't going to trigger the airbag.  They just directly crash the API because they don't go through the normal execution callstack.  :( About the only way around that would be to implement some sort of event registration system that would pass the data around to any scripts that registered with airbag, but that requires developers to hook into it themselves.  The goal here was to avoid doing something like that, but it looks like it might be the only way... Idk, maybe someone who knows more about js than I can think of some weird js quirk to do it, but I can't, so maybe some future version of airbag will support developers doing something like... airRegister('chat:message', (msg) => { // do stuff that could be unsafe }); The ultimate goal would be to minimize the effort on the author's part to encourage use of Airbag over the stock on() function.
1553139175
The Aaron
Forum Champion
API Scripter
Since your airbag creates a new scope around all the scripts, you could provide your own on() function that shadows the global one and seamlessly passes the registration through with a try/catch decorator wrapping it. 
1553139303
The Aaron
Forum Champion
API Scripter
You’ll probably also want to provide new versions of setTimeout(), setInterval(), _.delay(), and _.defer(). 
1553168617

Edited 1553168867
Michael G.
Pro
API Scripter
Good idea! *8 hours of mostly sleep later* V1.1 should have an operational shadow for on() .  I'll add scheduling next.
1553179433
Ammo
Pro
As long as you are being clear that you are only trying to catch SOME errors, then that's fine.   There are other asynchronous operations that you will not know about.   For example, in the Roll20 API there are asynchronous reads (like gmnotes on journal entries) that you won't be able to wrap.  Also, more advanced scripts can just choose to do things asynchronously, via Promise or otherwise.   It is probably ok that you don't catch these, because developers that use them are probably capable of deciding to catch their own errors if they want to.  Just clarifying that this can't ever be all of them. I am more worried about the idea of restart.   If you restart a script in the same script host, won't you get duplicate handlers for all the events?   Won't every event now get handled twice (and then three times, etc?)     I applaud what you are trying to do, and have at it as long as it is fun. :)   But a long term solution to this problem would be to petition Roll20 for a built-in GM command like "!roll20_api_restart" that actually tears down the script host (the software running the scripts) and starts it back up again, like restarting from the console.  
1553180830
The Aaron
Forum Champion
API Scripter
That double subscribe is a great point. Maybe aiming more for notification is a better goal. 
1553192637

Edited 1553193489
Michael G.
Pro
API Scripter
Yeah, double registration looks like a problem in the current version. Of course, I can easily just apply what I was planning for scheduling: Airbag having an internal list of scripts that are attempting to register with the original functions and then only Airbag would have the real subscription to the function.  Then when a crash happens, it tears down its internal list and re-executes the codebase. And yeah, this has largely just been fun for me.  I use js sparingly at work, so this is providing an opportunity to dig around in the deeper parts of it.