Ported the TG's error handler (#19782)

Ported the TG's error handler system, which allows runtime errors to be
collected in a datum, browsed, and all the good stuffs
This commit is contained in:
Fluffy
2024-08-22 22:46:49 +02:00
committed by GitHub
parent 69dad41c14
commit c86be958de
10 changed files with 427 additions and 47 deletions
+143
View File
@@ -0,0 +1,143 @@
GLOBAL_VAR_INIT(total_runtimes, GLOB.total_runtimes || 0)
GLOBAL_VAR_INIT(total_runtimes_skipped, 0)
#ifdef USE_CUSTOM_ERROR_HANDLER
#define ERROR_USEFUL_LEN 2
/world/Error(exception/E, datum/e_src)
GLOB.total_runtimes++
if(!istype(E)) //Something threw an unusual exception
log_world("uncaught runtime error: [E]")
return ..()
//this is snowflake because of a byond bug (ID:2306577), do not attempt to call non-builtin procs in this if
if(copytext(E.name, 1, 32) == "Maximum recursion level reached")//32 == length() of that string + 1
//log to world while intentionally triggering the byond bug.
log_world("runtime error: [E.name]\n[E.desc]")
//if we got to here without silently ending, the byond bug has been fixed.
log_world("The bug with recursion runtimes has been fixed. Please remove the snowflake check from world/Error in [__FILE__]:[__LINE__]")
return //this will never happen.
else if(copytext(E.name, 1, 18) == "Out of resources!")//18 == length() of that string + 1
log_world("BYOND out of memory. Restarting ([E?.file]:[E?.line])")
TgsEndProcess()
. = ..()
Reboot(reason = 1)
return
var/static/regex/stack_workaround = regex("[WORKAROUND_IDENTIFIER](.+?)[WORKAROUND_IDENTIFIER]")
var/static/list/error_last_seen = list()
var/static/list/error_cooldown = list() /* Error_cooldown items will either be positive(cooldown time) or negative(silenced error)
If negative, starts at -1, and goes down by 1 each time that error gets skipped*/
if(!error_last_seen) // A runtime is occurring too early in start-up initialization
return ..()
if(stack_workaround.Find(E.name))
var/list/data = json_decode(stack_workaround.group[1])
E.file = data[1]
E.line = data[2]
E.name = stack_workaround.Replace(E.name, "")
var/erroruid = "[E.file][E.line]"
var/last_seen = error_last_seen[erroruid]
var/cooldown = error_cooldown[erroruid] || 0
if(last_seen == null)
error_last_seen[erroruid] = world.time
last_seen = world.time
if(cooldown < 0)
error_cooldown[erroruid]-- //Used to keep track of skip count for this error
GLOB.total_runtimes_skipped++
return //Error is currently silenced, skip handling it
//Handle cooldowns and silencing spammy errors
var/silencing = FALSE
// We can runtime before config is initialized because BYOND initialize objs/map before a bunch of other stuff happens.
// This is a bunch of workaround code for that. Hooray!
var/configured_error_cooldown = 600 //CHANGED FROM TG, THAT DO NOT SET THE VARS HERE, SEE BELOW
var/configured_error_limit = 50 //CHANGED FROM TG, THAT DO NOT SET THE VARS HERE, SEE BELOW
var/configured_error_silence_time = 6000 //CHANGED FROM TG, THAT DO NOT SET THE VARS HERE, SEE BELOW
//THIS WILL MATTER WHEN WE PORT THE NEW CONFIG SYSTEM
// if(config?.entries)
// configured_error_cooldown = CONFIG_GET(number/error_cooldown)
// configured_error_limit = CONFIG_GET(number/error_limit)
// configured_error_silence_time = CONFIG_GET(number/error_silence_time)
// else
// var/datum/config_entry/CE = /datum/config_entry/number/error_cooldown
// configured_error_cooldown = initial(CE.default)
// CE = /datum/config_entry/number/error_limit
// configured_error_limit = initial(CE.default)
// CE = /datum/config_entry/number/error_silence_time
// configured_error_silence_time = initial(CE.default)
//Each occurence of a unique error adds to its cooldown time...
cooldown = max(0, cooldown - (world.time - last_seen)) + configured_error_cooldown
// ... which is used to silence an error if it occurs too often, too fast
if(cooldown > configured_error_cooldown * configured_error_limit)
cooldown = -1
silencing = TRUE
spawn(0)
usr = null
sleep(configured_error_silence_time)
var/skipcount = abs(error_cooldown[erroruid]) - 1
error_cooldown[erroruid] = 0
if(skipcount > 0)
SEND_TEXT(world.log, "\[[time_stamp()]] Skipped [skipcount] runtimes in [E.file],[E.line].")
GLOB.error_cache.log_error(E, skip_count = skipcount)
error_last_seen[erroruid] = world.time
error_cooldown[erroruid] = cooldown
var/list/usrinfo = null
var/locinfo
if(istype(usr))
usrinfo = list(" usr: [key_name(usr)]")
locinfo = loc_name(usr)
if(locinfo)
usrinfo += " usr.loc: [locinfo]"
// The proceeding mess will almost definitely break if error messages are ever changed
var/list/splitlines = splittext(E.desc, "\n")
var/list/desclines = list()
if(LAZYLEN(splitlines) > ERROR_USEFUL_LEN) // If there aren't at least three lines, there's no info
for(var/line in splitlines)
if(LAZYLEN(line) < 3 || findtext(line, "source file:") || findtext(line, "usr.loc:"))
continue
if(findtext(line, "usr:"))
if(usrinfo)
desclines.Add(usrinfo)
usrinfo = null
continue // Our usr info is better, replace it
if(copytext(line, 1, 3) != " ")//3 == length(" ") + 1
desclines += (" " + line) // Pad any unpadded lines, so they look pretty
else
desclines += line
if(usrinfo) //If this info isn't null, it hasn't been added yet
desclines.Add(usrinfo)
if(silencing)
desclines += " (This error will now be silenced for [DisplayTimeText(configured_error_silence_time)])"
if(GLOB.error_cache)
GLOB.error_cache.log_error(E, desclines)
var/main_line = "\[[time_stamp()]] Runtime in [E.file],[E.line]: [E]"
SEND_TEXT(world.log, main_line)
for(var/line in desclines)
SEND_TEXT(world.log, line)
// We might want to use this one day, but until it is that day, in `code\unit_tests\ss_test.dm` there is a shadowing of the /Error proc
// #ifdef UNIT_TESTS
// if(GLOB.current_test)
// //good day, sir
// GLOB.current_test.Fail("[main_line]\n[desclines.Join("\n")]", file = E.file, line = E.line)
// #endif
// This writes the regular format (unwrapping newlines and inserting timestamps as needed).
log_runtime("runtime error: [E.name]\n[E.desc]")
#undef ERROR_USEFUL_LEN
#endif
+196
View File
@@ -0,0 +1,196 @@
// Error viewing datums, responsible for storing error info, notifying admins
// when errors occur, and showing them to admins on demand.
// There are 3 different types used here:
//
// - error_cache keeps track of all error sources, as well as all individually
// logged errors. Only one instance of this datum should ever exist, and it's
// right here:
#ifdef USE_CUSTOM_ERROR_HANDLER
GLOBAL_DATUM_INIT(error_cache, /datum/error_viewer/error_cache, new)
#else
// If debugging is disabled, there's nothing useful to log, so don't bother.
GLOBAL_DATUM(error_cache, /datum/error_viewer/error_cache)
#endif
// - error_source datums exist for each line (of code) that generates an error,
// and keep track of all errors generated by that line.
//
// - error_entry datums exist for each logged error, and keep track of all
// relevant info about that error.
// Common vars and procs are kept at the error_viewer level
/datum/error_viewer
var/name = ""
/datum/error_viewer/proc/browse_to(client/user, html)
var/datum/browser/browser = new(user.mob, "error_viewer", null, 600, 400)
browser.set_content(html)
browser.add_head_content({"
<style>
.runtime
{
background-color: #171717;
border: solid 1px #202020;
font-family: "Courier New";
padding-left: 10px;
color: #CCCCCC;
}
.runtime_line
{
margin-bottom: 10px;
display: inline-block;
}
</style>
"})
browser.open()
/datum/error_viewer/proc/build_header(datum/error_viewer/back_to, linear)
// Common starter HTML for show_to
. = ""
if (istype(back_to))
. += back_to.make_link("<b>&lt;&lt;&lt;</b>", null, linear)
. += "[make_link("Refresh")]<br><br>"
/datum/error_viewer/proc/show_to(user, datum/error_viewer/back_to, linear)
// Specific to each child type
return
/datum/error_viewer/proc/make_link(linktext, datum/error_viewer/back_to, linear)
var/back_to_param = ""
if (!linktext)
linktext = name
if (istype(back_to))
back_to_param = ";viewruntime_backto=[REF(back_to)]"
if (linear)
back_to_param += ";viewruntime_linear=1"
return "<a href='?_src_=holder;viewruntime=[REF(src)][back_to_param]'>[linktext]</a>"
/datum/error_viewer/error_cache
var/list/errors = list()
var/list/error_sources = list()
var/list/errors_silenced = list()
/datum/error_viewer/error_cache/show_to(user, datum/error_viewer/back_to, linear)
var/html = build_header()
html += "<b>[GLOB.total_runtimes]</b> runtimes, <b>[GLOB.total_runtimes_skipped]</b> skipped<br><br>"
if (!linear)
html += "organized | [make_link("linear", null, 1)]<hr>"
var/datum/error_viewer/error_source/error_source
for (var/erroruid in error_sources)
error_source = error_sources[erroruid]
html += "[error_source.make_link(null, src)]<br>"
else
html += "[make_link("organized", null)] | linear<hr>"
for (var/datum/error_viewer/error_entry/error_entry in errors)
html += "[error_entry.make_link(null, src, 1)]<br>"
browse_to(user, html)
/datum/error_viewer/error_cache/proc/log_error(exception/e, list/desclines, skip_count)
if (!istype(e))
return // Abnormal exception, don't even bother
var/erroruid = "[e.file][e.line]"
var/datum/error_viewer/error_source/error_source = error_sources[erroruid]
if (!error_source)
error_source = new(e)
error_sources[erroruid] = error_source
var/datum/error_viewer/error_entry/error_entry = new(e, desclines, skip_count)
error_entry.error_source = error_source
errors += error_entry
error_source.errors += error_entry
if (skip_count)
return // Skip notifying admins about skipped errors.
// Show the error to admins with debug messages turned on, but only if one
// from the same source hasn't been shown too recently
if (error_source.next_message_at <= world.time)
var/const/viewtext = "\[view]" // Nesting these in other brackets went poorly
//log_debug("Runtime in <b>[e.file]</b>, line <b>[e.line]</b>: <b>[html_encode(e.name)]</b> [error_entry.make_link(viewtext)]")
var/err_msg_delay = 50 //CHANGED FROM TG, THAT DO NOT SET THE VARS HERE, SEE BELOW
//THIS WILL MATTER WHEN WE PORT THE NEW CONFIG SYSTEM
// if(config?.loaded)
// err_msg_delay = CONFIG_GET(number/error_msg_delay)
// else
// var/datum/config_entry/CE = /datum/config_entry/number/error_msg_delay
// err_msg_delay = initial(CE.default)
error_source.next_message_at = world.time + err_msg_delay
/datum/error_viewer/error_source
var/list/errors = list()
var/next_message_at = 0
/datum/error_viewer/error_source/New(exception/e)
if (!istype(e))
name = "\[[time_stamp()]] Uncaught exceptions"
return
name = "<b>\[[time_stamp()]]</b> Runtime in <b>[e.file]</b>, line <b>[e.line]</b>: <b>[html_encode(e.name)]</b>"
/datum/error_viewer/error_source/show_to(user, datum/error_viewer/back_to, linear)
if (!istype(back_to))
back_to = GLOB.error_cache
var/html = build_header(back_to)
for (var/datum/error_viewer/error_entry/error_entry in errors)
html += "[error_entry.make_link(null, src)]<br>"
browse_to(user, html)
/datum/error_viewer/error_entry
var/datum/error_viewer/error_source/error_source
var/exception/exc
var/desc = ""
var/usr_ref
var/turf/usr_loc
var/is_skip_count
/datum/error_viewer/error_entry/New(exception/e, list/desclines, skip_count)
if (!istype(e))
name = "<b>\[[time_stamp()]]</b> Uncaught exception: <b>[html_encode(e.name)]</b>"
return
if(skip_count)
name = "\[[time_stamp()]] Skipped [skip_count] runtimes in [e.file],[e.line]."
is_skip_count = TRUE
return
name = "<b>\[[time_stamp()]]</b> Runtime in <b>[e.file]</b>, line <b>[e.line]</b>: <b>[html_encode(e.name)]</b>"
exc = e
if (istype(desclines))
for (var/line in desclines)
// There's probably a better way to do this than non-breaking spaces...
desc += "<span class='runtime_line'>[html_encode(line)]</span><br>"
if (usr)
usr_ref = "[REF(usr)]"
usr_loc = get_turf(usr)
/datum/error_viewer/error_entry/show_to(user, datum/error_viewer/back_to, linear)
if (!istype(back_to))
back_to = error_source
var/html = build_header(back_to, linear)
html += "[name]<div class='runtime'>[desc]</div>"
if (usr_ref)
html += "<br><b>usr</b>: <a href='?_src_=vars;Vars=[usr_ref]'>VV</a>"
html += " <a href='?_src_=holder;adminplayeropts=[usr_ref]'>PP</a>"
html += " <a href='?_src_=holder;adminplayerobservefollow=[usr_ref]'>Follow</a>"
if (istype(usr_loc))
html += "<br><b>usr.loc</b>: <a href='?_src_=vars;Vars=[REF(usr_loc)]'>VV</a>"
html += " <a href='?_src_=holder;adminplayerobservecoodjump=1;X=[usr_loc.x];Y=[usr_loc.y];Z=[usr_loc.z]'>JMP</a>"
browse_to(user, html)
/datum/error_viewer/error_entry/make_link(linktext, datum/error_viewer/back_to, linear)
return is_skip_count ? name : ..()