Properly fix the error handler

This commit is contained in:
Vgr E. Barry 2015-11-04 12:41:47 -05:00
parent 154589a748
commit b28d4bf6e3
4 changed files with 63 additions and 104 deletions

View File

@ -1,10 +1,6 @@
import traceback
import argparse import argparse
import datetime import datetime
import socket
import time import time
import sys
import io
import botconfig import botconfig
import src.settings as var import src.settings as var
@ -117,78 +113,3 @@ def stream(output, level="normal"):
stream_handler(output) stream_handler(output)
elif level == "warning": elif level == "warning":
stream_handler(output) stream_handler(output)
# Error handler
buffer = io.BufferedWriter(io.FileIO(file=sys.stderr.fileno(), mode="wb", closefd=False))
class ErrorHandler(io.TextIOWrapper):
"""Handle tracebacks sent to sys.stderr."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.cli = None
self.target_logger = None
self.data = None
def write(self, data):
if self.closed:
raise ValueError("write to closed file")
if not isinstance(data, str):
raise ValueError("can't write %s to text stream" % data.__class__.__name__)
length = len(data)
b = data.encode("utf-8", "replace")
self.buffer.write(b)
self.data = data
self.flush()
return length
def flush(self):
# Probably a syntax error on startup, so these aren't defined yet
# If we do nothing, the error magically is printed to the console
if self.cli is None or self.target_logger is None:
return
self.buffer.flush()
if self.data is None:
return
exc = self.data.rstrip().splitlines()[-1].partition(":")[0]
import builtins
if "." in exc:
import importlib
module, dot, name = exc.rpartition(".")
try:
module = importlib.import_module(module)
except ImportError:
exc = Exception
else:
exc = getattr(module, name.strip())
elif hasattr(builtins, exc):
exc = getattr(builtins, exc)
if not isinstance(exc, type) or not issubclass(exc, Exception):
self.data = None
return # not an actual exception
msg = "An error has occurred and has been logged."
if not botconfig.PASTEBIN_ERRORS or botconfig.CHANNEL != botconfig.DEV_CHANNEL:
self.cli.msg(botconfig.CHANNEL, msg)
if botconfig.PASTEBIN_ERRORS and botconfig.DEV_CHANNEL:
try:
with socket.socket() as sock:
sock.connect(("termbin.com", 9999))
sock.send(b"".join(s.encode("utf-8", "replace") for s in self.data) + b"\n")
url = sock.recv(1024).decode("utf-8")
except socket.error:
self.target_logger(self.data, display=False)
else:
self.cli.msg(botconfig.DEV_CHANNEL, " ".join((msg, url)))
self.data = None
sys.stderr = ErrorHandler(buffer=buffer, encoding=sys.stderr.encoding,
errors=sys.stderr.errors, line_buffering=sys.stderr.line_buffering)

View File

@ -1,6 +1,9 @@
import traceback
import fnmatch import fnmatch
import socket
from collections import defaultdict from collections import defaultdict
from oyoyo.client import IRCClient
from oyoyo.parse import parse_nick from oyoyo.parse import parse_nick
import botconfig import botconfig
@ -9,10 +12,53 @@ from src.utilities import *
from src import logger from src import logger
adminlog = logger("audit.log") adminlog = logger("audit.log")
errlog = logger("errors.log")
COMMANDS = defaultdict(list) COMMANDS = defaultdict(list)
HOOKS = defaultdict(list) HOOKS = defaultdict(list)
# Error handler decorators
class handle_error:
def __init__(self, func):
self.func = func
self.instance = None
self.owner = object
def __get__(self, instance, owner):
self.instance = instance
self.owner = owner
return self
def __call__(self, *args, **kwargs):
try:
self.func.__get__(self.instance, self.owner)(*args, **kwargs)
except Exception:
traceback.print_exc() # no matter what, we want it to print
if kwargs.get("cli"): # client
cli = kwargs["cli"]
else:
for cli in args:
if isinstance(cli, IRCClient):
break
else:
cli = None
if cli is not None:
msg = "An error has occurred and has been logged."
if not botconfig.PASTEBIN_ERRORS or botconfig.CHANNEL != botconfig.DEV_CHANNEL:
cli.msg(botconfig.CHANNEL, msg)
if botconfig.PASTEBIN_ERRORS and botconfig.DEV_CHANNEL:
try:
with socket.socket() as sock:
sock.connect(("termbin.com", 9999))
sock.send(traceback.format_exc().encode("utf-8", "replace") + b"\n")
url = sock.recv(1024).decode("utf-8")
except socket.error:
pass
else:
cli.msg(botconfig.DEV_CHANNEL, " ".join((msg, url)))
class cmd: class cmd:
def __init__(self, *cmds, raw_nick=False, admin_only=False, owner_only=False, def __init__(self, *cmds, raw_nick=False, admin_only=False, owner_only=False,
chan=True, pm=False, playing=False, silenced=False, phases=(), roles=()): chan=True, pm=False, playing=False, silenced=False, phases=(), roles=()):
@ -49,6 +95,7 @@ class cmd:
self.__doc__ = self.func.__doc__ self.__doc__ = self.func.__doc__
return self return self
@handle_error
def caller(self, *args): def caller(self, *args):
largs = list(args) largs = list(args)
@ -190,6 +237,10 @@ class hook:
self.__doc__ = self.func.__doc__ self.__doc__ = self.func.__doc__
return self return self
@handle_error
def caller(self, *args, **kwargs):
return self.func(*args, **kwargs)
@staticmethod @staticmethod
def unhook(hookid): def unhook(hookid):
for each in list(HOOKS): for each in list(HOOKS):

View File

@ -14,8 +14,6 @@ from src import decorators, logger, wolfgame
log = logger("errors.log") log = logger("errors.log")
alog = logger(None) alog = logger(None)
sys.stderr.target_logger = log
hook = decorators.hook hook = decorators.hook
def on_privmsg(cli, rawnick, chan, msg, notice = False): def on_privmsg(cli, rawnick, chan, msg, notice = False):
@ -48,12 +46,7 @@ def on_privmsg(cli, rawnick, chan, msg, notice = False):
chan = parse_nick(rawnick)[0] chan = parse_nick(rawnick)[0]
for fn in decorators.COMMANDS[""]: for fn in decorators.COMMANDS[""]:
try:
fn.caller(cli, rawnick, chan, msg) fn.caller(cli, rawnick, chan, msg)
except Exception:
sys.stderr.write(traceback.format_exc())
if botconfig.DEBUG_MODE:
raise
for x in list(decorators.COMMANDS.keys()): for x in list(decorators.COMMANDS.keys()):
@ -67,12 +60,7 @@ def on_privmsg(cli, rawnick, chan, msg, notice = False):
continue continue
if not h or h[0] == " ": if not h or h[0] == " ":
for fn in decorators.COMMANDS.get(x, []): for fn in decorators.COMMANDS.get(x, []):
try:
fn.caller(cli, rawnick, chan, h.lstrip()) fn.caller(cli, rawnick, chan, h.lstrip())
except Exception:
sys.stderr.write(traceback.format_exc())
if botconfig.DEBUG_MODE:
raise
def unhandled(cli, prefix, cmd, *args): def unhandled(cli, prefix, cmd, *args):
@ -81,12 +69,7 @@ def unhandled(cli, prefix, cmd, *args):
for i,arg in enumerate(largs): for i,arg in enumerate(largs):
if isinstance(arg, bytes): largs[i] = arg.decode('ascii') if isinstance(arg, bytes): largs[i] = arg.decode('ascii')
for fn in decorators.HOOKS.get(cmd, []): for fn in decorators.HOOKS.get(cmd, []):
try: fn.caller(cli, prefix, *largs)
fn.func(cli, prefix, *largs)
except Exception:
sys.stderr.write(traceback.format_exc())
if botconfig.DEBUG_MODE:
raise
def connect_callback(cli): def connect_callback(cli):
@hook("endofmotd", hookid=294) @hook("endofmotd", hookid=294)
@ -172,9 +155,6 @@ def connect_callback(cli):
# capability but now claims otherwise. # capability but now claims otherwise.
alog("Server refused capabilities: {0}".format(" ".join(caps))) alog("Server refused capabilities: {0}".format(" ".join(caps)))
if sys.stderr.cli is None:
sys.stderr.cli = cli # first connection
if botconfig.SASL_AUTHENTICATION: if botconfig.SASL_AUTHENTICATION:
@hook("authenticate") @hook("authenticate")
def auth_plus(cli, something, plus): def auth_plus(cli, something, plus):

View File

@ -60,6 +60,7 @@ is_owner = var.is_owner
cmd = decorators.cmd cmd = decorators.cmd
hook = decorators.hook hook = decorators.hook
handle_error = decorators.handle_error
COMMANDS = decorators.COMMANDS COMMANDS = decorators.COMMANDS
# Game Logic Begins: # Game Logic Begins:
@ -918,6 +919,7 @@ def toggle_altpinged_status(nick, value, old=None):
var.PING_IF_NUMS[old].discard(host) var.PING_IF_NUMS[old].discard(host)
var.PING_IF_NUMS[old].discard(hostmask) var.PING_IF_NUMS[old].discard(hostmask)
@handle_error
def join_timer_handler(cli): def join_timer_handler(cli):
with var.WARNING_LOCK: with var.WARNING_LOCK:
var.PINGING_IFS = True var.PINGING_IFS = True
@ -1267,6 +1269,7 @@ def join_player(cli, player, chan, who = None, forced = False):
return True return True
@handle_error
def kill_join(cli, chan): def kill_join(cli, chan):
pl = var.list_players() pl = var.list_players()
pl.sort(key=lambda x: x.lower()) pl.sort(key=lambda x: x.lower())
@ -1995,6 +1998,7 @@ def stats(cli, nick, chan, rest):
var.PHASE) var.PHASE)
reply(cli, nick, chan, stats_mssg) reply(cli, nick, chan, stats_mssg)
@handle_error
def hurry_up(cli, gameid, change): def hurry_up(cli, gameid, change):
if var.PHASE != "day": return if var.PHASE != "day": return
if gameid: if gameid:
@ -2764,6 +2768,7 @@ def chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs,
stop_game(cli, winner, additional_winners=event.data["additional_winners"]) stop_game(cli, winner, additional_winners=event.data["additional_winners"])
return True return True
@handle_error
def del_player(cli, nick, forced_death=False, devoice=True, end_game=True, death_triggers=True, killer_role="", deadlist=[], original="", cmode=[], deadchat=[], ismain=True): def del_player(cli, nick, forced_death=False, devoice=True, end_game=True, death_triggers=True, killer_role="", deadlist=[], original="", cmode=[], deadchat=[], ismain=True):
""" """
Returns: False if one side won. Returns: False if one side won.
@ -3212,7 +3217,7 @@ def del_player(cli, nick, forced_death=False, devoice=True, end_game=True, death
return ret return ret
@handle_error
def reaper(cli, gameid): def reaper(cli, gameid):
# check to see if idlers need to be killed. # check to see if idlers need to be killed.
var.IDLE_WARNED = set() var.IDLE_WARNED = set()
@ -3414,6 +3419,7 @@ def fgoat(cli, nick, chan, rest):
cli.msg(chan, "\u0002{0}\u0002's goat walks by and {1} \u0002{2}\u0002.".format(nick, goatact, togoat)) cli.msg(chan, "\u0002{0}\u0002's goat walks by and {1} \u0002{2}\u0002.".format(nick, goatact, togoat))
@handle_error
def return_to_village(cli, chan, nick, show_message): def return_to_village(cli, chan, nick, show_message):
with var.GRAVEYARD_LOCK: with var.GRAVEYARD_LOCK:
if nick in var.DISCONNECTED.keys(): if nick in var.DISCONNECTED.keys():
@ -3849,6 +3855,7 @@ def begin_day(cli):
event = Event("begin_day", {}) event = Event("begin_day", {})
event.dispatch(cli, var) event.dispatch(cli, var)
@handle_error
def night_warn(cli, gameid): def night_warn(cli, gameid):
if gameid != var.NIGHT_ID: if gameid != var.NIGHT_ID:
return return
@ -7459,7 +7466,7 @@ def cgamemode(cli, arg):
else: else:
cli.msg(chan, "Mode \u0002{0}\u0002 not found.".format(modeargs[0])) cli.msg(chan, "Mode \u0002{0}\u0002 not found.".format(modeargs[0]))
@handle_error
def expire_start_votes(cli, chan): def expire_start_votes(cli, chan):
# Should never happen as the timer is removed on game start, but just to be safe # Should never happen as the timer is removed on game start, but just to be safe
if var.PHASE != 'join': if var.PHASE != 'join':