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 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
|
||||||
@ -104,3 +108,55 @@ 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 = []
|
||||||
|
|
||||||
|
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
|
# The bot commands implemented in here are present no matter which module is loaded
|
||||||
|
|
||||||
import base64
|
|
||||||
import imp
|
|
||||||
import socket
|
|
||||||
import traceback
|
import traceback
|
||||||
|
import base64
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
|
||||||
from oyoyo.parse import parse_nick
|
from oyoyo.parse import parse_nick
|
||||||
|
|
||||||
@ -14,31 +14,10 @@ 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 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):
|
def on_privmsg(cli, rawnick, chan, msg, notice = False):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -63,7 +42,7 @@ def on_privmsg(cli, rawnick, chan, msg, notice = False):
|
|||||||
if botconfig.DEBUG_MODE:
|
if botconfig.DEBUG_MODE:
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
notify_error(cli, chan, log)
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
for x in decorators.COMMANDS:
|
for x in decorators.COMMANDS:
|
||||||
@ -83,7 +62,7 @@ def on_privmsg(cli, rawnick, chan, msg, notice = False):
|
|||||||
if botconfig.DEBUG_MODE:
|
if botconfig.DEBUG_MODE:
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
notify_error(cli, chan, log)
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
def unhandled(cli, prefix, cmd, *args):
|
def unhandled(cli, prefix, cmd, *args):
|
||||||
@ -94,11 +73,11 @@ def unhandled(cli, prefix, cmd, *args):
|
|||||||
for fn in decorators.HOOKS.get(cmd, []):
|
for fn in decorators.HOOKS.get(cmd, []):
|
||||||
try:
|
try:
|
||||||
fn.func(cli, prefix, *largs)
|
fn.func(cli, prefix, *largs)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
if botconfig.DEBUG_MODE:
|
if botconfig.DEBUG_MODE:
|
||||||
raise e
|
raise
|
||||||
else:
|
else:
|
||||||
notify_error(cli, botconfig.CHANNEL, log)
|
traceback.print_exc()
|
||||||
|
|
||||||
def connect_callback(cli):
|
def connect_callback(cli):
|
||||||
@hook("endofmotd", hookid=294)
|
@hook("endofmotd", hookid=294)
|
||||||
@ -184,6 +163,8 @@ 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")
|
||||||
|
@ -494,17 +494,17 @@ def forced_exit(cli, nick, chan, rest):
|
|||||||
try:
|
try:
|
||||||
stop_game(cli)
|
stop_game(cli)
|
||||||
except Exception:
|
except Exception:
|
||||||
notify_error(cli, chan, errlog)
|
traceback.print_exc()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
reset_modes_timers(cli)
|
reset_modes_timers(cli)
|
||||||
except Exception:
|
except Exception:
|
||||||
notify_error(cli, chan, errlog)
|
traceback.print_exc()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
reset()
|
reset()
|
||||||
except Exception:
|
except Exception:
|
||||||
notify_error(cli, chan, errlog)
|
traceback.print_exc()
|
||||||
|
|
||||||
msg = "{0} quit from {1}"
|
msg = "{0} quit from {1}"
|
||||||
|
|
||||||
@ -516,7 +516,7 @@ def forced_exit(cli, nick, chan, rest):
|
|||||||
nick,
|
nick,
|
||||||
rest.strip()))
|
rest.strip()))
|
||||||
except Exception:
|
except Exception:
|
||||||
notify_error(cli, chan, errlog)
|
traceback.print_exc()
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
|
|
||||||
@ -540,12 +540,12 @@ def restart_program(cli, nick, chan, rest):
|
|||||||
try:
|
try:
|
||||||
stop_game(cli)
|
stop_game(cli)
|
||||||
except Exception:
|
except Exception:
|
||||||
notify_error(cli, chan, errlog)
|
traceback.print_exc()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
reset_modes_timers(cli)
|
reset_modes_timers(cli)
|
||||||
except Exception:
|
except Exception:
|
||||||
notify_error(cli, chan, errlog)
|
traceback.print_exc()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with sqlite3.connect("data.sqlite3", check_same_thread=False) as conn:
|
with sqlite3.connect("data.sqlite3", check_same_thread=False) as conn:
|
||||||
@ -554,12 +554,12 @@ def restart_program(cli, nick, chan, rest):
|
|||||||
if players:
|
if players:
|
||||||
c.execute("UPDATE pre_restart_state SET players = ?", (" ".join(players),))
|
c.execute("UPDATE pre_restart_state SET players = ?", (" ".join(players),))
|
||||||
except Exception:
|
except Exception:
|
||||||
notify_error(cli, chan, errlog)
|
traceback.print_exc()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
reset()
|
reset()
|
||||||
except Exception:
|
except Exception:
|
||||||
notify_error(cli, chan, errlog)
|
traceback.print_exc()
|
||||||
|
|
||||||
msg = "{0} restart from {1}".format(
|
msg = "{0} restart from {1}".format(
|
||||||
"Scheduled" if restart_program.aftergame else "Forced", nick)
|
"Scheduled" if restart_program.aftergame else "Forced", nick)
|
||||||
@ -596,7 +596,7 @@ def restart_program(cli, nick, chan, rest):
|
|||||||
try:
|
try:
|
||||||
cli.quit(msg.format(nick, rest.strip()))
|
cli.quit(msg.format(nick, rest.strip()))
|
||||||
except Exception:
|
except Exception:
|
||||||
notify_error(cli, chan, errlog)
|
traceback.print_exc()
|
||||||
|
|
||||||
@hook("quit")
|
@hook("quit")
|
||||||
def restart_buffer(cli, raw_nick, reason):
|
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"))
|
@cmd("fstop", admin_only=True, phases=("join", "day", "night"))
|
||||||
def reset_game(cli, nick, chan, rest):
|
def reset_game(cli, nick, chan, rest):
|
||||||
"""Forces the game to stop."""
|
"""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))
|
cli.msg(botconfig.CHANNEL, "\u0002{0}\u0002 has forced the game to stop.".format(nick))
|
||||||
if var.PHASE != "join":
|
if var.PHASE != "join":
|
||||||
stop_game(cli)
|
stop_game(cli)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user