Add proper error handling. Closes #151
All exceptions, if not caught, are unconditionally sent to `sys.stderr`. We only need to use our own custom class with some modifications and we can intercept all errors, even those in threads (which #151 was about). The only real downside from this practice is that `cli` is not accessible to us when the errors happen, so I used a hack to set it on an instance variable. If anyone can find a better solution, please step forward. For the time being, this will have to do. If for some obscure reason this class breaks, `sys.__stderr__` holds the original `sys.stderr`
This commit is contained in:
parent
74555f013a
commit
38b7ef81e2
@ -1,6 +1,10 @@
|
||||
import traceback
|
||||
import argparse
|
||||
import datetime
|
||||
import socket
|
||||
import time
|
||||
import sys
|
||||
import io
|
||||
|
||||
import botconfig
|
||||
import src.settings as var
|
||||
@ -104,3 +108,55 @@ def stream(output, level="normal"):
|
||||
stream_handler(output)
|
||||
elif level == "warning":
|
||||
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 = []
|
||||
|
||||
def write(self, data):
|
||||
assert not (self.cli is None or self.target_logger is None)
|
||||
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.append(data)
|
||||
if data and not data.startswith(("Traceback", " ", "Exception in thread")):
|
||||
self.flush()
|
||||
return length
|
||||
|
||||
def flush(self):
|
||||
self.buffer.flush()
|
||||
|
||||
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("".join(self.data), display=False)
|
||||
else:
|
||||
self.cli.msg(botconfig.DEV_CHANNEL, " ".join((msg, url)))
|
||||
del self.data[:]
|
||||
if var.PHASE in ("join", "day", "night"):
|
||||
from src.decorators import COMMANDS
|
||||
for cmd in COMMANDS["fstop"]:
|
||||
cmd.func(self.cli, "<stderr>", "", "")
|
||||
|
||||
sys.stderr = ErrorHandler(buffer=buffer, encoding=sys.stderr.encoding,
|
||||
errors=sys.stderr.errors, line_buffering=sys.stderr.line_buffering)
|
||||
|
@ -1,9 +1,9 @@
|
||||
# The bot commands implemented in here are present no matter which module is loaded
|
||||
|
||||
import base64
|
||||
import imp
|
||||
import socket
|
||||
import traceback
|
||||
import base64
|
||||
import socket
|
||||
import sys
|
||||
|
||||
from oyoyo.parse import parse_nick
|
||||
|
||||
@ -14,31 +14,10 @@ from src import decorators, logger, wolfgame
|
||||
log = logger("errors.log")
|
||||
alog = logger(None)
|
||||
|
||||
sys.stderr.target_logger = log
|
||||
|
||||
hook = decorators.hook
|
||||
|
||||
def notify_error(cli, chan, target_logger):
|
||||
msg = "An error has occurred and has been logged."
|
||||
|
||||
tb = traceback.format_exc()
|
||||
|
||||
target_logger(tb)
|
||||
|
||||
if (not botconfig.PASTEBIN_ERRORS) or (chan != botconfig.DEV_CHANNEL):
|
||||
# Don't send a duplicate message if DEV_CHANNEL is the current channel.
|
||||
cli.msg(chan, msg)
|
||||
|
||||
if botconfig.PASTEBIN_ERRORS and botconfig.DEV_CHANNEL:
|
||||
try:
|
||||
with socket.socket() as sock:
|
||||
sock.connect(("termbin.com", 9999))
|
||||
sock.send(tb.encode("utf-8", "replace") + b"\n")
|
||||
url = sock.recv(1024).decode("utf-8")
|
||||
except socket.error:
|
||||
target_logger(traceback.format_exc())
|
||||
else:
|
||||
cli.msg(botconfig.DEV_CHANNEL, " ".join((msg, url)))
|
||||
|
||||
|
||||
def on_privmsg(cli, rawnick, chan, msg, notice = False):
|
||||
|
||||
try:
|
||||
@ -63,7 +42,7 @@ def on_privmsg(cli, rawnick, chan, msg, notice = False):
|
||||
if botconfig.DEBUG_MODE:
|
||||
raise
|
||||
else:
|
||||
notify_error(cli, chan, log)
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
for x in decorators.COMMANDS:
|
||||
@ -83,7 +62,7 @@ def on_privmsg(cli, rawnick, chan, msg, notice = False):
|
||||
if botconfig.DEBUG_MODE:
|
||||
raise
|
||||
else:
|
||||
notify_error(cli, chan, log)
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def unhandled(cli, prefix, cmd, *args):
|
||||
@ -94,11 +73,11 @@ def unhandled(cli, prefix, cmd, *args):
|
||||
for fn in decorators.HOOKS.get(cmd, []):
|
||||
try:
|
||||
fn.func(cli, prefix, *largs)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
if botconfig.DEBUG_MODE:
|
||||
raise e
|
||||
raise
|
||||
else:
|
||||
notify_error(cli, botconfig.CHANNEL, log)
|
||||
traceback.print_exc()
|
||||
|
||||
def connect_callback(cli):
|
||||
@hook("endofmotd", hookid=294)
|
||||
@ -184,6 +163,8 @@ def connect_callback(cli):
|
||||
# capability but now claims otherwise.
|
||||
alog("Server refused capabilities: {0}".format(" ".join(caps)))
|
||||
|
||||
if sys.stderr.cli is None:
|
||||
sys.stderr.cli = cli # first connection
|
||||
|
||||
if botconfig.SASL_AUTHENTICATION:
|
||||
@hook("authenticate")
|
||||
|
@ -494,17 +494,17 @@ def forced_exit(cli, nick, chan, rest):
|
||||
try:
|
||||
stop_game(cli)
|
||||
except Exception:
|
||||
notify_error(cli, chan, errlog)
|
||||
traceback.print_exc()
|
||||
|
||||
try:
|
||||
reset_modes_timers(cli)
|
||||
except Exception:
|
||||
notify_error(cli, chan, errlog)
|
||||
traceback.print_exc()
|
||||
|
||||
try:
|
||||
reset()
|
||||
except Exception:
|
||||
notify_error(cli, chan, errlog)
|
||||
traceback.print_exc()
|
||||
|
||||
msg = "{0} quit from {1}"
|
||||
|
||||
@ -516,7 +516,7 @@ def forced_exit(cli, nick, chan, rest):
|
||||
nick,
|
||||
rest.strip()))
|
||||
except Exception:
|
||||
notify_error(cli, chan, errlog)
|
||||
traceback.print_exc()
|
||||
sys.exit()
|
||||
|
||||
|
||||
@ -540,12 +540,12 @@ def restart_program(cli, nick, chan, rest):
|
||||
try:
|
||||
stop_game(cli)
|
||||
except Exception:
|
||||
notify_error(cli, chan, errlog)
|
||||
traceback.print_exc()
|
||||
|
||||
try:
|
||||
reset_modes_timers(cli)
|
||||
except Exception:
|
||||
notify_error(cli, chan, errlog)
|
||||
traceback.print_exc()
|
||||
|
||||
try:
|
||||
with sqlite3.connect("data.sqlite3", check_same_thread=False) as conn:
|
||||
@ -554,12 +554,12 @@ def restart_program(cli, nick, chan, rest):
|
||||
if players:
|
||||
c.execute("UPDATE pre_restart_state SET players = ?", (" ".join(players),))
|
||||
except Exception:
|
||||
notify_error(cli, chan, errlog)
|
||||
traceback.print_exc()
|
||||
|
||||
try:
|
||||
reset()
|
||||
except Exception:
|
||||
notify_error(cli, chan, errlog)
|
||||
traceback.print_exc()
|
||||
|
||||
msg = "{0} restart from {1}".format(
|
||||
"Scheduled" if restart_program.aftergame else "Forced", nick)
|
||||
@ -596,7 +596,7 @@ def restart_program(cli, nick, chan, rest):
|
||||
try:
|
||||
cli.quit(msg.format(nick, rest.strip()))
|
||||
except Exception:
|
||||
notify_error(cli, chan, errlog)
|
||||
traceback.print_exc()
|
||||
|
||||
@hook("quit")
|
||||
def restart_buffer(cli, raw_nick, reason):
|
||||
@ -7508,6 +7508,9 @@ def fwait(cli, nick, chan, rest):
|
||||
@cmd("fstop", admin_only=True, phases=("join", "day", "night"))
|
||||
def reset_game(cli, nick, chan, rest):
|
||||
"""Forces the game to stop."""
|
||||
if nick == "<stderr>":
|
||||
cli.msg(botconfig.CHANNEL, "Game stopped due to error.")
|
||||
else:
|
||||
cli.msg(botconfig.CHANNEL, "\u0002{0}\u0002 has forced the game to stop.".format(nick))
|
||||
if var.PHASE != "join":
|
||||
stop_game(cli)
|
||||
|
Loading…
x
Reference in New Issue
Block a user