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:
parent
a39ded6053
commit
7c753b2810
@ -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:"
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user