diff --git a/wuttapos/app.py b/wuttapos/app.py index f419f01..a06393e 100644 --- a/wuttapos/app.py +++ b/wuttapos/app.py @@ -25,7 +25,13 @@ WuttaPOS app """ import json +import logging import os +import socket +import sys +import threading +from collections import OrderedDict +from traceback import format_exception from rattail import app as base from rattail.config import make_config @@ -37,6 +43,9 @@ import wuttapos from wuttapos.util import get_pos_batch_handler +log = logging.getLogger(__name__) + + class WuttaAppHandler(base.AppHandler): """ Custom app handler for WuttaPOS @@ -55,6 +64,61 @@ def main(page: ft.Page): config = make_config() app = config.get_app() model = app.model + handler = get_pos_batch_handler(config) + + # nb. as of python 3.10 the original hook is accessible, we needn't save the ref + # cf. https://docs.python.org/3/library/threading.html#threading.__excepthook__ + orig_thread_hook = threading.excepthook + + hostname = socket.gethostname() + email_context = OrderedDict([ + ('hostname', hostname), + ('ipaddress', socket.gethostbyname(hostname)), + ('terminal', handler.get_terminal_id() or '??'), + ]) + + def send_error_email(traceback): + extra_context = OrderedDict(email_context) + + try: + uuid = page.session.get('user_uuid') + if uuid: + session = app.make_session() + user = session.get(model.User, uuid) + extra_context['username'] = user.username + batch = handler.get_current_batch(user, create=False) + if batch: + extra_context['batchid'] = batch.id_str + session.close() + else: + extra_context['username'] = 'n/a' + + app.send_email('uncaught_exception', { + 'extra_context': extra_context, + 'traceback': traceback, + }) + + except: + log.exception("failed to send error email") + + def sys_exc_hook(exc_type, exc_value, exc_traceback): + traceback = ''.join(format_exception(exc_type, exc_value, exc_traceback)).strip() + send_error_email(traceback) + sys.__excepthook__(exc_type, exc_value, exc_traceback) + + def thread_exc_hook(args): + traceback = ''.join(format_exception(args.exc_type, args.exc_value, args.exc_traceback)).strip() + send_error_email(traceback) + orig_thread_hook(args) + + # custom exception hook for main process + sys.excepthook = sys_exc_hook + + # custom exception hook for threads (requires python 3.8) + # cf. https://docs.python.org/3/library/threading.html#threading.excepthook + v = sys.version_info + if v.major >= 3 and v.minor >= 8: + threading.excepthook = thread_exc_hook page.title = f"WuttaPOS v{wuttapos.__version__}" page.window_full_screen = True @@ -137,7 +201,6 @@ def main(page: ft.Page): if user: page.session.set('user_uuid', uuid) page.session.set('user_display', str(user)) - handler = get_pos_batch_handler(config) batch = handler.get_current_batch(user, create=False) if batch: page.session.set('txn_display', batch.id_str) diff --git a/wuttapos/views/pos.py b/wuttapos/views/pos.py index 4370789..a667a28 100644 --- a/wuttapos/views/pos.py +++ b/wuttapos/views/pos.py @@ -657,7 +657,7 @@ class POSView(WuttaView): meta_button_width = meta_button_height * 2 meta_font_size = tenkey_font_size - def meta_button(text, on_click=None, bgcolor='blue'): + def meta_button(text, on_click=None, bgcolor='blue', data=None): return ft.Container(content=ft.Text(text, size=meta_font_size, weight=ft.FontWeight.BOLD), height=meta_button_height, @@ -666,7 +666,8 @@ class POSView(WuttaView): alignment=ft.alignment.center, border=ft.border.all(1, 'black'), border_radius=ft.border_radius.all(5), - bgcolor=bgcolor) + bgcolor=bgcolor, + data=data) self.meta_menu = ft.Container( content=ft.Column( @@ -695,7 +696,8 @@ class POSView(WuttaView): ft.Row( [ meta_button("TODO", bgcolor='blue', on_click=self.not_supported), - meta_button("TODO", bgcolor='blue', on_click=self.not_supported), + meta_button("TODO", bgcolor='blue', on_click=self.not_supported, + data={'error': True}), ], spacing=0, ), @@ -867,6 +869,11 @@ class POSView(WuttaView): self.page.update() def not_supported(self, e=None, feature=None): + + # test error handler + if e.control.data and e.control.data.get('error'): + raise ValueError("NOT YET SUPPORTED") + text = "NOT YET SUPPORTED" if not feature and e: feature = e.control.content.value