Improve the error handler

Also, thanks to @nyuszika7h for the uuid idea. I took his early draft and implemented it properly as part of the refactoring I did.
This commit is contained in:
Vgr E. Barry 2016-11-06 21:43:01 -05:00
parent a39ded6053
commit 7c753b2810
3 changed files with 148 additions and 78 deletions

View File

@ -863,6 +863,7 @@
"acc_warning": "Changing accounts during game. Please do not change accounts while playing.",
"tempban_kick": "Temporary ban for warning: {reason}",
"error_log": "An error has occurred and has been logged.",
"error_pastebin": "(Unable to pastebin traceback; please check the console)",
"_": " vim: set sw=4 expandtab:"
}

View File

@ -1,9 +1,12 @@
import fnmatch
import socket
import traceback
import threading
import types
import sys
import string
import random
import json
import re
import urllib.request, urllib.parse
from collections import defaultdict
from oyoyo.client import IRCClient
@ -12,7 +15,7 @@ from oyoyo.parse import parse_nick
import botconfig
import src.settings as var
from src.utilities import *
from src import channels, logger, errlog, events
from src import channels, users, logger, errlog, events
from src.messages import messages
adminlog = logger.logger("audit.log")
@ -20,64 +23,161 @@ adminlog = logger.logger("audit.log")
COMMANDS = defaultdict(list)
HOOKS = defaultdict(list)
# Error handler decorators
_do_nothing = lambda: None
# Error handler decorators and context managers
class _local(threading.local):
level = 0
frame_locals = None
handler = None
level = 0
_local = _local()
# This is a mapping of stringified tracebacks to (link, uuid) tuples
# That way, we don't have to call in to the website everytime we have
# another error. If you ever need to delete pastes, do the following:
# $ curl -x DELETE https://ptpb.pw/<uuid>
_tracebacks = {}
class chain_exceptions:
def __init__(self, exc, *, suppress_context=False):
self.exc = exc
self.suppress_context = suppress_context
def __enter__(self):
return self
def __exit__(self, exc, value, tb):
if exc is value is tb is None:
return False
value.__context__ = self.exc
value.__suppress_context__ = self.suppress_context
self.exc = value
return True
@property
def traceback(self):
return "".join(traceback.format_exception(type(self.exc), self.exc, self.exc.__traceback__))
class print_traceback:
def __enter__(self):
_local.level += 1
return self
def __exit__(self, exc_type, exc_value, tb):
if exc_type is exc_value is tb is None:
_local.level -= 1
return False
if not issubclass(exc_type, Exception):
_local.level -= 1
return False
if _local.frame_locals is None:
while tb.tb_next is not None:
tb = tb.tb_next
_local.frame_locals = tb.tb_frame.f_locals
if _local.level > 1:
_local.level -= 1
return False # the outermost caller should handle this
if _local.handler is None:
_local.handler = chain_exceptions(exc_value)
variables = ["", None, "Local variables from innermost frame:", ""]
with _local.handler:
for name, value in _local.frame_locals.items():
variables.append("{0} = {1!r}".format(name, value))
if len(variables) > 4:
variables.append("\n")
else:
variables[2] = "No local variables found in innermost frame."
variables[1] = _local.handler.traceback
if not botconfig.PASTEBIN_ERRORS or channels.Main is not channels.Dev:
channels.Main.send(messages["error_log"])
if botconfig.PASTEBIN_ERRORS and channels.Dev is not None:
message = [messages["error_log"]]
link_uuid = _tracebacks.get("\n".join(variables))
if link_uuid is None:
bot_id = re.sub(r"[^A-Za-z0-9-]", "-", users.Bot.nick)
bot_id = re.sub(r"--+", "-", bot_id).strip("-")
rand_id = "".join(random.sample(string.ascii_letters + string.digits, 8))
api_url = "https://ptpb.pw/~{0}-error-{1}".format(bot_id, rand_id)
data = None
with _local.handler:
req = urllib.request.Request(api_url, urllib.parse.urlencode({
"c": "\n".join(variables), # contents
"s": 86400 # expiry (seconds)
}).encode("utf-8", "replace"))
req.add_header("Accept", "application/json")
resp = urllib.request.urlopen(req)
data = json.loads(resp.read().decode("utf-8"))
if data is None: # couldn't fetch the link
message.append(messages["error_pastebin"])
variables[1] = _local.handler.traceback # an error happened; update the stored traceback
else:
link, uuid = _tracebacks["\n".join(variables)] = (data["url"] + "/pytb", data.get("uuid"))
message.append(link)
if uuid is None: # if there's no uuid, the paste already exists and we don't have it
message.append("(Already reported by another instance)")
else:
message.append("(uuid: {0})".format(uuid))
else:
link, uuid = link_uuid
message.append(link)
if uuid is None:
message.append("(Previously reported)")
else:
message.append("(uuid: {0}-...)".format(uuid[:8]))
channels.Dev.send(" ".join(message), prefix=botconfig.DEV_PREFIX)
errlog("\n".join(variables))
_local.level -= 1
if not _local.level: # outermost caller; we're done here
_local.frame_locals = None
_local.handler = None
return True # a true return value tells the interpreter to swallow the exception
class handle_error:
def __new__(cls, func):
if isinstance(func, cls): # already decorated
def __new__(cls, func=None, *, instance=None):
if isinstance(func, cls) and instance is func.instance: # already decorated
return func
self = super().__new__(cls)
self.instance = instance
self.func = func
return self
def __get__(self, instance, owner):
if instance is not None:
return types.MethodType(self, instance)
if instance is not self.instance:
return type(self)(self.func, instance=instance)
return self
def __call__(self, *args, **kwargs):
fn = _do_nothing
_local.level += 1
try:
def __call__(*args, **kwargs):
self, *args = args
if self.instance is not None:
args = self.instance, *args
with print_traceback():
return self.func(*args, **kwargs)
except Exception:
if _local.frame_locals is None:
exc_type, exc_value, tb = sys.exc_info()
while tb.tb_next is not None:
tb = tb.tb_next
_local.frame_locals = tb.tb_frame.f_locals
if _local.level > 1:
raise # the outermost caller should handle this
fn = lambda: errlog("\n{0}\n\n".format(data))
data = traceback.format_exc()
variables = ["\nLocal variables from innermost frame:\n"]
for name, value in _local.frame_locals.items():
variables.append("{0} = {1!r}".format(name, value))
data += "\n".join(variables)
if not botconfig.PASTEBIN_ERRORS or channels.Main is not channels.Dev:
channels.Main.send(messages["error_log"])
if botconfig.PASTEBIN_ERRORS and channels.Dev is not None:
pastebin_tb(channels.Dev, messages["error_log"], data, prefix=botconfig.DEV_PREFIX)
finally:
fn()
_local.level -= 1
if not _local.level: # outermost caller; we're done here
_local.frame_locals = None
class cmd:
def __init__(self, *cmds, raw_nick=False, flag=None, owner_only=False,

View File

@ -1,11 +1,6 @@
import fnmatch
import itertools
import json
import random
import fnmatch
import re
import string
import traceback
import urllib
import botconfig
import src.settings as var
@ -20,8 +15,7 @@ __all__ = ["pm", "is_fake_nick", "mass_mode", "mass_privmsg", "reply",
"is_owner", "is_admin", "plural", "singular", "list_players",
"list_players_and_roles", "list_participants", "get_role", "get_roles",
"get_reveal_role", "get_templates", "role_order", "break_long_message",
"complete_match", "get_victim", "get_nick", "pastebin_tb",
"InvalidModeException"]
"complete_match", "get_victim", "get_nick", "InvalidModeException"]
# message either privmsg or notice, depending on user settings
def pm(cli, target, message):
if is_fake_nick(target) and botconfig.DEBUG_MODE:
@ -463,31 +457,6 @@ def get_nick(cli, nick):
return None
return ul[ull.index(lnick)]
def pastebin_tb(context, msg, exc, prefix):
try:
bot_id = re.sub(r"[^A-Za-z0-9-]", "-", botconfig.NICK)
bot_id = re.sub(r"--+", "-", bot_id)
bot_id = re.sub(r"^-+|-+$", "", bot_id)
rand_id = "".join(random.sample(string.ascii_letters + string.digits, 8))
api_url = "https://ptpb.pw/~{0}-error-{1}".format(bot_id, rand_id)
req = urllib.request.Request(api_url, urllib.parse.urlencode({
"c": exc, # contents
"s": 86400 # expiry (seconds)
}).encode("utf-8", "replace"))
req.add_header("Accept", "application/json")
resp = urllib.request.urlopen(req)
data = json.loads(resp.read().decode("utf-8"))
url = data["url"] + "/pytb"
except Exception:
# Exception is already printed before calling this function, don't print twice
context.send(msg + " (Unable to pastebin traceback; please check the console.)", prefix=prefix)
else:
context.send(" ".join((msg, url)), prefix=prefix)
class InvalidModeException(Exception): pass
# vim: set sw=4 expandtab: