diff --git a/aurorastation.dme b/aurorastation.dme index cbf30a086e9..95f2e734dd2 100644 --- a/aurorastation.dme +++ b/aurorastation.dme @@ -2140,6 +2140,8 @@ #include "code\modules\emotes\definitions\slime.dm" #include "code\modules\emotes\definitions\synthetics.dm" #include "code\modules\emotes\definitions\visible.dm" +#include "code\modules\error_handler\error_handler.dm" +#include "code\modules\error_handler\error_viewer.dm" #include "code\modules\events\apc_damage.dm" #include "code\modules\events\blob.dm" #include "code\modules\events\brand_intelligence.dm" diff --git a/code/__DEFINES/admin.dm b/code/__DEFINES/admin.dm index ceedca41140..54eda35f475 100644 --- a/code/__DEFINES/admin.dm +++ b/code/__DEFINES/admin.dm @@ -58,3 +58,4 @@ #define ADMIN_JMP(src) "(JMP)" #define COORD(src) "[src ? src.Admin_Coordinates_Readable() : "nonexistent location"]" +#define AREACOORD(src) "[src ? src.Admin_Coordinates_Readable(TRUE) : "nonexistent location"]" diff --git a/code/__DEFINES/stack_trace.dm b/code/__DEFINES/stack_trace.dm index 969eaecc707..4911b4a0d57 100644 --- a/code/__DEFINES/stack_trace.dm +++ b/code/__DEFINES/stack_trace.dm @@ -1,2 +1,4 @@ /// gives us the stack trace from CRASH() without ending the current proc. #define stack_trace(message) _stack_trace(message, __FILE__, __LINE__) + +#define WORKAROUND_IDENTIFIER "%//%" diff --git a/code/__HELPERS/logging/_logging.dm b/code/__HELPERS/logging/_logging.dm index 0b946ba1b25..0b3fdcbdfd1 100644 --- a/code/__HELPERS/logging/_logging.dm +++ b/code/__HELPERS/logging/_logging.dm @@ -237,3 +237,16 @@ GLOBAL_LIST_INIT(testing_global_profiler, list("_PROFILE_NAME" = "Global")) if(message) entry += "\n[message]" WRITE_LOG(GLOB.config.logfiles["world_subsystems_tgui"], "TGUI: [entry]") + +/proc/loc_name(atom/A) + if(!istype(A)) + return "(INVALID LOCATION)" + + var/turf/T = A + if (!istype(T)) + T = get_turf(A) + + if(istype(T)) + return "([AREACOORD(T)])" + else if(A.loc) + return "(UNKNOWN (?, ?, ?))" diff --git a/code/__HELPERS/stack_trace.dm b/code/__HELPERS/stack_trace.dm index 6dc9bbd5598..453ebd7266c 100644 --- a/code/__HELPERS/stack_trace.dm +++ b/code/__HELPERS/stack_trace.dm @@ -2,4 +2,4 @@ /// Do not call directly, use the [stack_trace] macro instead. /// May also be used by other tooling like from rust. /proc/_stack_trace(message, file, line) - CRASH("[message] ([file]:[line])") + CRASH("[message][WORKAROUND_IDENTIFIER][json_encode(list(file, line))][WORKAROUND_IDENTIFIER]") diff --git a/code/game/world.dm b/code/game/world.dm index 7fa1ef74e12..2fcc0eecc7c 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -268,52 +268,6 @@ var/list/world_api_rate_limit = list() shutdown_logging() ..(reason) -/world/Error(var/exception/e) - var/static/inerror = 0 - - //runtime while processing runtimes - if (inerror) - inerror = 0 - return ..(e) - - inerror = 1 - -// A horrible hack for unit tests but fuck runtiming timers. -// They don't provide any useful information, and as such, are being suppressed. -#ifdef UNIT_TEST - - if (findtextEx(e.name, "Invalid timer:") || findtextEx(e.desc, "Invalid timer:")) - inerror = 0 - return - -#endif // UNIT_TEST - - e.time_stamp() - log_exception(e) - - inerror = 0 - return ..(e) - -// We need this elsewhere! -/exception/var/time_stamped = 0 - -/exception/proc/time_stamp() - if (time_stamped) - return - - //newline at start is because of the "runtime error" byond prints that can't be timestamped. - name = "\n\[[time2text(world.timeofday,"hh:mm:ss")]\][name]" - - //this is done this way rather then replace text to pave the way for processing the runtime reports more thoroughly - // (and because runtimes end with a newline, and we don't want to basically print an empty time stamp) - var/list/split = splittext(desc, "\n") - for (var/i in 1 to split.len) - if (split[i] != "") - split[i] = "\[[time2text(world.timeofday,"hh:mm:ss")]\][split[i]]" - desc = jointext(split, "\n") - - time_stamped = 1 - /proc/load_configuration() GLOB.config = new() GLOB.config.load("config/config.txt") diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 63cdec64a8e..081a00c6ecf 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -1561,6 +1561,17 @@ access_control_topic(href_list["access_control"]) return + else if(href_list["viewruntime"]) + var/datum/error_viewer/error_viewer = locate(href_list["viewruntime"]) + if(!istype(error_viewer)) + to_chat(usr, SPAN_WARNING("That runtime viewer no longer exists."), confidential = TRUE) + return + + if(href_list["viewruntime_backto"]) + error_viewer.show_to(owner, locate(href_list["viewruntime_backto"]), href_list["viewruntime_linear"]) + else + error_viewer.show_to(owner, null, href_list["viewruntime_linear"]) + /mob/living/proc/can_centcom_reply() return 0 diff --git a/code/modules/error_handler/error_handler.dm b/code/modules/error_handler/error_handler.dm new file mode 100644 index 00000000000..7041695fdd5 --- /dev/null +++ b/code/modules/error_handler/error_handler.dm @@ -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 diff --git a/code/modules/error_handler/error_viewer.dm b/code/modules/error_handler/error_viewer.dm new file mode 100644 index 00000000000..f4a6fb0ef3f --- /dev/null +++ b/code/modules/error_handler/error_viewer.dm @@ -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({" + + "}) + 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("<<<", null, linear) + + . += "[make_link("Refresh")]

" + +/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 "[linktext]" + +/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 += "[GLOB.total_runtimes] runtimes, [GLOB.total_runtimes_skipped] skipped

" + if (!linear) + html += "organized | [make_link("linear", null, 1)]
" + 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)]
" + + else + html += "[make_link("organized", null)] | linear
" + for (var/datum/error_viewer/error_entry/error_entry in errors) + html += "[error_entry.make_link(null, src, 1)]
" + + 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 [e.file], line [e.line]: [html_encode(e.name)] [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 = "\[[time_stamp()]] Runtime in [e.file], line [e.line]: [html_encode(e.name)]" + +/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)]
" + + 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 = "\[[time_stamp()]] Uncaught exception: [html_encode(e.name)]" + return + + if(skip_count) + name = "\[[time_stamp()]] Skipped [skip_count] runtimes in [e.file],[e.line]." + is_skip_count = TRUE + return + + name = "\[[time_stamp()]] Runtime in [e.file], line [e.line]: [html_encode(e.name)]" + exc = e + if (istype(desclines)) + for (var/line in desclines) + // There's probably a better way to do this than non-breaking spaces... + desc += "[html_encode(line)]
" + + 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]
[desc]
" + if (usr_ref) + html += "
usr: VV" + html += " PP" + html += " Follow" + if (istype(usr_loc)) + html += "
usr.loc: VV" + html += " JMP" + + browse_to(user, html) + +/datum/error_viewer/error_entry/make_link(linktext, datum/error_viewer/back_to, linear) + return is_skip_count ? name : ..() diff --git a/html/changelogs/fluffyghost-errorhandler.yml b/html/changelogs/fluffyghost-errorhandler.yml new file mode 100644 index 00000000000..21d25693243 --- /dev/null +++ b/html/changelogs/fluffyghost-errorhandler.yml @@ -0,0 +1,58 @@ +################################ +# Example Changelog File +# +# Note: This file, and files beginning with ".", and files that don't end in ".yml" will not be read. If you change this file, you will look really dumb. +# +# Your changelog will be merged with a master changelog. (New stuff added only, and only on the date entry for the day it was merged.) +# When it is, any changes listed below will disappear. +# +# Valid Prefixes: +# bugfix +# - (fixes bugs) +# wip +# - (work in progress) +# qol +# - (quality of life) +# soundadd +# - (adds a sound) +# sounddel +# - (removes a sound) +# rscadd +# - (adds a feature) +# rscdel +# - (removes a feature) +# imageadd +# - (adds an image or sprite) +# imagedel +# - (removes an image or sprite) +# spellcheck +# - (fixes spelling or grammar) +# experiment +# - (experimental change) +# balance +# - (balance changes) +# code_imp +# - (misc internal code change) +# refactor +# - (refactors code) +# config +# - (makes a change to the config files) +# admin +# - (makes changes to administrator tools) +# server +# - (miscellaneous changes to server) +################################# + +# Your name. +author: FluffyGhost + +# Optional: Remove this file after generating master changelog. Useful for PR changelogs that won't get used again. +delete-after: True + +# Any changes you've made. See valid prefix list above. +# INDENT WITH TWO SPACES. NOT TABS. SPACES. +# SCREW THIS UP AND IT WON'T WORK. +# Also, this gets changed to [] after reading. Just remove the brackets when you add new shit. +# Please surround your changes in double quotes ("). It works without them, but if you use certain characters it screws up compiling. The quotes will not show up in the changelog. +changes: + - rscadd: "Ported the TG's error handler system, which allows runtime errors to be collected in a datum, browsed, and all the good stuffs."