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.",
|
"acc_warning": "Changing accounts during game. Please do not change accounts while playing.",
|
||||||
"tempban_kick": "Temporary ban for warning: {reason}",
|
"tempban_kick": "Temporary ban for warning: {reason}",
|
||||||
"error_log": "An error has occurred and has been logged.",
|
"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:"
|
"_": " vim: set sw=4 expandtab:"
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import fnmatch
|
|
||||||
import socket
|
|
||||||
import traceback
|
import traceback
|
||||||
import threading
|
import threading
|
||||||
import types
|
import string
|
||||||
import sys
|
import random
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
import urllib.request, urllib.parse
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from oyoyo.client import IRCClient
|
from oyoyo.client import IRCClient
|
||||||
@ -12,7 +15,7 @@ from oyoyo.parse import parse_nick
|
|||||||
import botconfig
|
import botconfig
|
||||||
import src.settings as var
|
import src.settings as var
|
||||||
from src.utilities import *
|
from src.utilities import *
|
||||||
from src import channels, logger, errlog, events
|
from src import channels, users, logger, errlog, events
|
||||||
from src.messages import messages
|
from src.messages import messages
|
||||||
|
|
||||||
adminlog = logger.logger("audit.log")
|
adminlog = logger.logger("audit.log")
|
||||||
@ -20,64 +23,161 @@ adminlog = logger.logger("audit.log")
|
|||||||
COMMANDS = defaultdict(list)
|
COMMANDS = defaultdict(list)
|
||||||
HOOKS = defaultdict(list)
|
HOOKS = defaultdict(list)
|
||||||
|
|
||||||
# Error handler decorators
|
# Error handler decorators and context managers
|
||||||
|
|
||||||
_do_nothing = lambda: None
|
|
||||||
|
|
||||||
class _local(threading.local):
|
class _local(threading.local):
|
||||||
level = 0
|
|
||||||
frame_locals = None
|
frame_locals = None
|
||||||
|
handler = None
|
||||||
|
level = 0
|
||||||
|
|
||||||
_local = _local()
|
_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:
|
class handle_error:
|
||||||
|
|
||||||
def __new__(cls, func):
|
def __new__(cls, func=None, *, instance=None):
|
||||||
if isinstance(func, cls): # already decorated
|
if isinstance(func, cls) and instance is func.instance: # already decorated
|
||||||
return func
|
return func
|
||||||
|
|
||||||
self = super().__new__(cls)
|
self = super().__new__(cls)
|
||||||
|
self.instance = instance
|
||||||
self.func = func
|
self.func = func
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __get__(self, instance, owner):
|
def __get__(self, instance, owner):
|
||||||
if instance is not None:
|
if instance is not self.instance:
|
||||||
return types.MethodType(self, instance)
|
return type(self)(self.func, instance=instance)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(*args, **kwargs):
|
||||||
fn = _do_nothing
|
self, *args = args
|
||||||
_local.level += 1
|
if self.instance is not None:
|
||||||
try:
|
args = self.instance, *args
|
||||||
|
with print_traceback():
|
||||||
return self.func(*args, **kwargs)
|
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:
|
class cmd:
|
||||||
def __init__(self, *cmds, raw_nick=False, flag=None, owner_only=False,
|
def __init__(self, *cmds, raw_nick=False, flag=None, owner_only=False,
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
import fnmatch
|
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import fnmatch
|
||||||
import random
|
|
||||||
import re
|
import re
|
||||||
import string
|
|
||||||
import traceback
|
|
||||||
import urllib
|
|
||||||
|
|
||||||
import botconfig
|
import botconfig
|
||||||
import src.settings as var
|
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",
|
"is_owner", "is_admin", "plural", "singular", "list_players",
|
||||||
"list_players_and_roles", "list_participants", "get_role", "get_roles",
|
"list_players_and_roles", "list_participants", "get_role", "get_roles",
|
||||||
"get_reveal_role", "get_templates", "role_order", "break_long_message",
|
"get_reveal_role", "get_templates", "role_order", "break_long_message",
|
||||||
"complete_match", "get_victim", "get_nick", "pastebin_tb",
|
"complete_match", "get_victim", "get_nick", "InvalidModeException"]
|
||||||
"InvalidModeException"]
|
|
||||||
# message either privmsg or notice, depending on user settings
|
# message either privmsg or notice, depending on user settings
|
||||||
def pm(cli, target, message):
|
def pm(cli, target, message):
|
||||||
if is_fake_nick(target) and botconfig.DEBUG_MODE:
|
if is_fake_nick(target) and botconfig.DEBUG_MODE:
|
||||||
@ -463,31 +457,6 @@ def get_nick(cli, nick):
|
|||||||
return None
|
return None
|
||||||
return ul[ull.index(lnick)]
|
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
|
class InvalidModeException(Exception): pass
|
||||||
|
|
||||||
# vim: set sw=4 expandtab:
|
# vim: set sw=4 expandtab:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user