Paradise error handler (#10571)

* Ports the Paradise error handler.

Made by @Krausus

Initial code port, it compiles!

* It now looks cool!

All kinds of fancy CSS and HTML things.

* Stuff

Fixes Comic's comments.

Ports some more changes from Paradise.

The src from the machinery process is NOT included. I consider stack
traces more valuable.
This commit is contained in:
PJB3005
2016-06-23 02:56:49 +02:00
committed by clusterfack
parent 07998874a6
commit df30c3f4b7
11 changed files with 365 additions and 9 deletions

View File

@@ -0,0 +1 @@
#define ERROR_USEFUL_LEN 2

View File

@@ -135,3 +135,27 @@
/proc/log_startup_progress(var/message)
to_chat(world, "<span class='danger'>[message]</span>")
world.log << message
/proc/datum_info_line(var/datum/D)
if (!istype(D))
return
if (!istype(D, /mob))
return "[D] ([D.type])"
var/mob/M = D
return "[M] ([M.ckey]) ([M.type])"
/proc/atom_loc_line(var/atom/A)
if (!istype(A))
return
var/turf/T = get_turf(A)
if (isturf(T))
return "[T] ([T.x], [T.y], [T.z]) ([T.type])"
else if (A.loc)
return "[A.loc] (nullspace) ([A.loc.type])"
else
return "(nullspace)"

View File

@@ -1382,10 +1382,6 @@ proc/rotate_icon(file, state, step = 1, aa = FALSE)
B.fingerprintshidden = A.fingerprintshidden
B.fingerprintslast = A.fingerprintslast
/world/Error(exception/e)
print_runtime(e)
..()
//Checks if any of the atoms in the turf are dense
//Returns 1 is anything is dense, 0 otherwise
/turf/proc/has_dense_content()

View File

@@ -174,6 +174,12 @@
var/enable_roundstart_away_missions = 0
// Error handler config options.
var/error_cooldown = 600 // The "cooldown" time for each occurrence of a unique error
var/error_limit = 9 // How many occurrences before the next will silence them
var/error_silence_time = 6000 // How long a unique error will be silenced for
var/error_msg_delay = 50 // How long to wait between messaging admins about occurrences of a unique error
/datum/configuration/New()
. = ..()
var/list/L = typesof(/datum/game_mode) - /datum/game_mode
@@ -544,6 +550,15 @@
enable_roundstart_away_missions = 1
if("enable_wages")
roundstart_enable_wages = 1
if("error_cooldown")
error_cooldown = value
if("error_limit")
error_limit = value
if("error_silence_time")
error_silence_time = value
if("error_msg_delay")
error_msg_delay = value
else
diary << "Unknown setting in configuration: '[name]'"

View File

@@ -198,7 +198,8 @@ var/list/admin_verbs_debug = list(
/client/proc/cmd_admin_dump_macprofile,
#endif
/client/proc/debugNatureMapGenerator,
/client/proc/callatomproc
/client/proc/callatomproc,
/client/proc/view_runtimes
)
var/list/admin_verbs_possess = list(
/proc/possess,

View File

@@ -4129,3 +4129,15 @@
if(href_list["econ_panel"])
var/choice = href_list["econ_panel"]
EconomyPanel(choice, href_list)
else if (href_list["viewruntime"])
var/datum/error_viewer/error_viewer = locate(href_list["viewruntime"])
if (!istype(error_viewer))
to_chat(owner, "<span class='warning'>That runtime viewer no longer exists.</span>")
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"])

View File

@@ -1823,3 +1823,13 @@ client/proc/check_convertables()
spiral_block(epicenter,max_range,1,1)
else
spiral_block(epicenter,max_range,0,1)
/client/proc/view_runtimes()
set category = "Debug"
set name = "View Runtimes"
set desc = "Open the Runtime Viewer"
if (!check_rights(R_DEBUG))
return
error_cache.show_to(src)

View File

@@ -0,0 +1,95 @@
/var/list/error_last_seen = 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
/var/list/error_cooldown = list()
/var/total_runtimes = 0
/var/total_runtimes_skipped = 0
// The ifdef needs to be down here, since the error viewer references total_runtimes
#ifdef DEBUG
/world/Error(var/exception/e)
if (!istype(e)) // Something threw an unusual exception
world.log << "\[[time_stamp()]] Uncaught exception: [e]"
return ..()
if (!global.error_last_seen) // A runtime is occurring too early in start-up initialization
return ..()
global.total_runtimes++
var/erroruid = "[e.file][e.line]"
var/last_seen = global.error_last_seen[erroruid]
var/cooldown = global.error_cooldown[erroruid] || 0
if (last_seen == null) // A new error!
global.error_last_seen[erroruid] = world.time
last_seen = world.time
if (cooldown < 0)
global.error_cooldown[erroruid]-- // Used to keep track of skip count for this error
global.total_runtimes_skipped++
return // Error is currently silenced, skip handling it
// Handle cooldowns and silencing spammy errors
var/silencing = 0
// Each occurrence of a unique error adds to its "cooldown" time...
cooldown = max(0, cooldown - (world.time - last_seen)) + config.error_cooldown
// ... which is used to silence an error if it occurs too often, too fast
if (cooldown > config.error_cooldown * config.error_limit)
cooldown = -1
silencing = 1
spawn (0)
usr = null
sleep(config.error_silence_time)
var/skipcount = abs(global.error_cooldown[erroruid]) - 1
global.error_cooldown[erroruid] = 0
if (skipcount > 0)
world.log << "\[[time_stamp()]] Skipped [skipcount] runtimes in [e.file],[e.line]."
error_cache.log_error(e, skip_count = skipcount)
global.error_last_seen[erroruid] = world.time
global.error_cooldown[erroruid] = cooldown
// The detailed error info needs some tweaking to make it look nice
var/list/usrinfo = null
var/locinfo
if (istype(usr))
usrinfo = list(" usr: [datum_info_line(usr)]")
locinfo = atom_loc_line(usr)
if (locinfo)
usrinfo += " usr.loc: [locinfo]"
// The proceeding mess will almost definitely break if error messages are ever changed
// I apologize in advance
var/list/splitlines = splittext(e.desc, "\n")
var/list/desclines = list()
if (splitlines.len > ERROR_USEFUL_LEN) // If there aren't at least three lines, there's no info
for (var/line in splitlines)
if (length(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) != " ")
desclines += (" " + line) // Pad any unpadded lines, so they look pretty
else
desclines += line
if (usrinfo) // If this isn't null, it hasn't been added yet
desclines.Add(usrinfo)
if (silencing)
desclines += " (This error will now be silenced for [config.error_silence_time / 600] minutes)"
// Now to actually output the error info...
world.log << "\[[time_stamp()]] Runtime in [e.file],[e.line]: [e]"
for (var/line in desclines)
world.log << line
if (global.error_cache)
global.error_cache.log_error(e, desclines)
#endif

View File

@@ -0,0 +1,189 @@
// 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 DEBUG
/var/datum/error_viewer/error_cache/error_cache = new()
#else
// If debugging is disabled, there's nothing useful to log, so don't bother.
/var/datum/error_viewer/error_cache/error_cache = null
#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(var/client/user, var/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(var/datum/error_viewer/back_to, var/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(var/user, var/datum/error_viewer/back_to, var/linear)
// Specific to each child type
return
/datum/error_viewer/proc/make_link(var/linktext, var/datum/error_viewer/back_to, var/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(var/user, var/datum/error_viewer/back_to, var/linear)
var/html = build_header()
html += "<b>[global.total_runtimes]</b> runtimes, <b>[global.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(var/exception/e, var/list/desclines, var/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)]")
error_source.next_message_at = world.time + config.error_msg_delay
/datum/error_viewer/error_source
var/list/errors = list()
var/next_message_at = 0
/datum/error_viewer/error_source/New(var/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(var/user, var/datum/error_viewer/back_to, var/linear)
if (!istype(back_to))
back_to = 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(var/exception/e, var/list/desclines, var/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(var/user, var/datum/error_viewer/back_to, var/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(var/linktext, var/datum/error_viewer/back_to, var/linear)
return is_skip_count ? name : ..()

View File

@@ -31,7 +31,7 @@ JOBS_HAVE_MINIMAL_ACCESS
## log OOC channel
LOG_OOC
## log client Say
## log client Say
LOG_SAY
## log admin actions
@@ -77,7 +77,7 @@ LOG_PDA
# KICK_INACTIVE
## probablities for game modes chosen in "secret" and "random" modes
##
##
## default probablity is 1, increase to make that mode more likely to be picked
## set to 0 to disable that mode
## Cult mode is in alpha test, enable at your own risk
@@ -180,7 +180,7 @@ GUEST_BAN
##Remove the # mark infront of this to forbid admins from posssessing the singularity.
#FORBID_SINGULO_POSSESSION
## Remove the # to show a popup 'reply to' window to every non-admin that recieves an adminPM.
## Remove the # to show a popup 'reply to' window to every non-admin that recieves an adminPM.
## The intention is to make adminPMs more visible. (although I fnd popups annoying so this defaults to off)
#POPUP_ADMIN_PM
@@ -270,7 +270,7 @@ RESPAWN_AS_MOMMI
## where the music files are. It's set in config with MEDIA_SECRET_KEY
## and MUST be the same as the media server's.
##
## If you're using older code that doesn't require a password, just
## If you're using older code that doesn't require a password, just
## leave this blank.
##
## Do NOT share this, it's like a password.
@@ -295,6 +295,16 @@ SKIP_MINIMAP_GENERATION
## Uncomment to genereate an away mission at the beginning of each round
#ENABLE_ROUNDSTART_AWAY_MISSIONS
## Error handling related options.
## The "cooldown" time for each occurrence of a unique error
# ERROR_COOLDOWN 600
## How many occurrences before the next will silence them
# ERROR_LIMIT 9
## How long a unique error will be silenced for
# ERROR_SILENCE_TIME 6000
## How long to wait between messaging admins about occurrences of a unique error
# ERROR_MSG_DELAY 50
## ENABLE_WAGES
## Comment out to disable wages being enabled by default.
ENABLE_WAGES

View File

@@ -18,6 +18,7 @@
#include "code\stylesheet.dm"
#include "code\world.dm"
#include "code\__DEFINES\atom_locking.dm"
#include "code\__DEFINES\error_hander.dm"
#include "code\__DEFINES\lighting.dm"
#include "code\__HELPERS\_macros.dm"
#include "code\__HELPERS\cmp.dm"
@@ -1081,6 +1082,8 @@
#include "code\modules\Economy\utils.dm"
#include "code\modules\Economy\Wage-Process.dm"
#include "code\modules\Economy\Wages.dm"
#include "code\modules\error_handler\error_handler.dm"
#include "code\modules\error_handler\error_viewer.dm"
#include "code\modules\events\alien_infestation.dm"
#include "code\modules\events\blob.dm"
#include "code\modules\events\bluespaceanomaly.dm"