mirror of
https://github.com/lutris/lutris
synced 2024-10-14 19:53:53 +00:00
Move last-chance exception handling code to its own file.
This keeps the actual exceptions available to everything without hitting circular imports; very few things should need access to the backstops as such. Resolves #5194
This commit is contained in:
parent
6a0dd3f31a
commit
090736df07
149
lutris/exception_backstops.py
Normal file
149
lutris/exception_backstops.py
Normal file
|
@ -0,0 +1,149 @@
|
|||
from functools import wraps
|
||||
from typing import Any, Callable, Iterable
|
||||
|
||||
from gi.repository import Gio, GLib, GObject, Gtk
|
||||
|
||||
from lutris.gui.dialogs import ErrorDialog
|
||||
from lutris.util.log import logger
|
||||
|
||||
|
||||
def watch_game_errors(game_stop_result, game=None):
|
||||
"""Decorator used to catch exceptions and send events instead of propagating them normally.
|
||||
If 'game_stop_result' is not None, and the decorated function returns that, this will
|
||||
send game-stop and make the game stopped as well. This simplifies handling cancellation.
|
||||
Also, if an error occurs and is emitted, the function returns this value, so callers
|
||||
can tell that the function failed.
|
||||
|
||||
If you do not provide a game object directly, it is assumed to be in the first argument to
|
||||
the decorated method (which is 'self', typically).
|
||||
"""
|
||||
captured_game = game
|
||||
|
||||
def inner_decorator(function):
|
||||
@wraps(function)
|
||||
def wrapper(*args, **kwargs):
|
||||
"""Catch all exceptions and emit an event."""
|
||||
game = captured_game if captured_game else args[0]
|
||||
|
||||
try:
|
||||
result = function(*args, **kwargs)
|
||||
if game_stop_result is not None and result == game_stop_result and game.state != game.STATE_STOPPED:
|
||||
game.state = game.STATE_STOPPED
|
||||
game.emit("game-stop")
|
||||
return result
|
||||
except Exception as ex:
|
||||
logger.exception("%s has encountered an error: %s", game, ex, exc_info=ex)
|
||||
if game.state != game.STATE_STOPPED:
|
||||
game.state = game.STATE_STOPPED
|
||||
game.emit("game-stop")
|
||||
game.signal_error(ex)
|
||||
return game_stop_result
|
||||
|
||||
return wrapper
|
||||
|
||||
return inner_decorator
|
||||
|
||||
|
||||
def _get_error_parent(error_objects: Iterable) -> Gtk.Window:
|
||||
"""Obtains a top-level window to use as the parent of an
|
||||
error, by examining s list of objects. Any that are None
|
||||
are skipped; we call get_toplevel() on each object that has
|
||||
this method, and return the first non-None result.
|
||||
|
||||
If this fails, we turn to the application's main window instead."""
|
||||
|
||||
for error_object in error_objects:
|
||||
if not error_object:
|
||||
continue
|
||||
|
||||
if error_object and hasattr(error_object, "get_toplevel"):
|
||||
toplevel = error_object.get_toplevel()
|
||||
if toplevel:
|
||||
return toplevel
|
||||
|
||||
application = Gio.Application.get_default()
|
||||
return application.window if application else None
|
||||
|
||||
|
||||
def _create_error_wrapper(handler: Callable, handler_name: str,
|
||||
error_result: Any,
|
||||
error_method_name: str,
|
||||
connected_object: Any = None):
|
||||
"""Wraps a handler function in an error handler that will log and then report
|
||||
any exceptions, then return the 'error_result'."""
|
||||
|
||||
handler_object = handler.__self__ if hasattr(handler, "__self__") else None
|
||||
|
||||
def error_wrapper(*args, **kwargs):
|
||||
try:
|
||||
return handler(*args, **kwargs)
|
||||
except Exception as ex:
|
||||
logger.exception("Error handling %s: %s", handler_name, ex)
|
||||
|
||||
if handler_object and hasattr(handler_object, error_method_name):
|
||||
error_method = getattr(handler_object, error_method_name)
|
||||
error_method(ex)
|
||||
else:
|
||||
ErrorDialog(ex, parent=_get_error_parent([handler_object, connected_object]))
|
||||
return error_result
|
||||
|
||||
return error_wrapper
|
||||
|
||||
|
||||
def init_exception_backstops():
|
||||
"""This function is called once only, during startup, and replaces ("swizzles") a bunch of
|
||||
callback setup functions in GLib. The callbacks are all wrapped with error handlers that
|
||||
log the error and report it.
|
||||
|
||||
This is important to do because PyGObject will straight up crash if an exception escapes
|
||||
these handlers; it's better to tell the user and try to survive.
|
||||
|
||||
You can provide certain methods to provide error handling, but if you do not you get an
|
||||
ErrorDialog. Put these handling methods on the same object as the callback method itself.
|
||||
|
||||
We take care of these methods:
|
||||
GObject.Object.connect (via on_signal_error(self, error))
|
||||
GObject.add_emission_hook (via on_emission_hook_error(self, error))
|
||||
GLib.idle_add (via on_idle_error(self, error))
|
||||
GLib.timeout_add (via on_timeout_error(self, error))
|
||||
|
||||
Idle and timeout handlers will be disconnected if this happens to avoid repeated error reports,
|
||||
but signals and emission hooks will remain connected.
|
||||
"""
|
||||
|
||||
def _error_handling_connect(self: Gtk.Widget, signal_spec: str, handler, *args, **kwargs):
|
||||
error_wrapper = _create_error_wrapper(handler, f"signal '{signal_spec}'",
|
||||
error_result=None,
|
||||
error_method_name="on_signal_error",
|
||||
connected_object=self)
|
||||
return _original_connect(self, signal_spec, error_wrapper, *args, **kwargs)
|
||||
|
||||
def _error_handling_add_emission_hook(emitting_type, signal_spec, handler, *args, **kwargs):
|
||||
error_wrapper = _create_error_wrapper(handler, f"emission hook '{emitting_type}.{signal_spec}'",
|
||||
error_result=True, # stay attached
|
||||
error_method_name="on_emission_hook_error")
|
||||
return _original_add_emission_hook(emitting_type, signal_spec, error_wrapper, *args, **kwargs)
|
||||
|
||||
def _error_handling_idle_add(handler, *args, **kwargs):
|
||||
error_wrapper = _create_error_wrapper(handler, "idle function",
|
||||
error_result=False, # stop calling idle func
|
||||
error_method_name="on_idle_error")
|
||||
return _original_idle_add(error_wrapper, *args, **kwargs)
|
||||
|
||||
def _error_handling_timeout_add(interval, handler, *args, **kwargs):
|
||||
error_wrapper = _create_error_wrapper(handler, "timeout function",
|
||||
error_result=False, # stop calling timeout fund
|
||||
error_method_name="on_timeout_error")
|
||||
return _original_timeout_add(interval, error_wrapper, *args, **kwargs)
|
||||
|
||||
_original_connect = Gtk.Widget.connect
|
||||
GObject.Object.connect = _error_handling_connect
|
||||
|
||||
_original_add_emission_hook = GObject.add_emission_hook
|
||||
GObject.add_emission_hook = _error_handling_add_emission_hook
|
||||
|
||||
_original_idle_add = GLib.idle_add
|
||||
GLib.idle_add = _error_handling_idle_add
|
||||
|
||||
_original_timeout_add = GLib.timeout_add
|
||||
GLib.timeout_add = _error_handling_timeout_add
|
|
@ -1,12 +1,5 @@
|
|||
"""Exception handling module"""
|
||||
from functools import wraps
|
||||
from gettext import gettext as _
|
||||
from typing import Any, Callable, Iterable
|
||||
|
||||
from gi.repository import Gio, GLib, GObject, Gtk
|
||||
|
||||
from lutris.gui.dialogs import ErrorDialog
|
||||
from lutris.util.log import logger
|
||||
|
||||
|
||||
class LutrisError(Exception):
|
||||
|
@ -66,145 +59,3 @@ class EsyncLimitError(Exception):
|
|||
|
||||
class FsyncUnsupportedError(Exception):
|
||||
"""Raised when FSYNC is enabled, but is not supported by the kernel."""
|
||||
|
||||
|
||||
def watch_game_errors(game_stop_result, game=None):
|
||||
"""Decorator used to catch exceptions and send events instead of propagating them normally.
|
||||
If 'game_stop_result' is not None, and the decorated function returns that, this will
|
||||
send game-stop and make the game stopped as well. This simplifies handling cancellation.
|
||||
Also, if an error occurs and is emitted, the function returns this value, so callers
|
||||
can tell that the function failed.
|
||||
|
||||
If you do not provide a game object directly, it is assumed to be in the first argument to
|
||||
the decorated method (which is 'self', typically).
|
||||
"""
|
||||
captured_game = game
|
||||
|
||||
def inner_decorator(function):
|
||||
@wraps(function)
|
||||
def wrapper(*args, **kwargs):
|
||||
"""Catch all exceptions and emit an event."""
|
||||
game = captured_game if captured_game else args[0]
|
||||
|
||||
try:
|
||||
result = function(*args, **kwargs)
|
||||
if game_stop_result is not None and result == game_stop_result and game.state != game.STATE_STOPPED:
|
||||
game.state = game.STATE_STOPPED
|
||||
game.emit("game-stop")
|
||||
return result
|
||||
except Exception as ex:
|
||||
logger.exception("%s has encountered an error: %s", game, ex, exc_info=ex)
|
||||
if game.state != game.STATE_STOPPED:
|
||||
game.state = game.STATE_STOPPED
|
||||
game.emit("game-stop")
|
||||
game.signal_error(ex)
|
||||
return game_stop_result
|
||||
|
||||
return wrapper
|
||||
|
||||
return inner_decorator
|
||||
|
||||
|
||||
def _get_error_parent(error_objects: Iterable) -> Gtk.Window:
|
||||
"""Obtains a top-level window to use as the parent of an
|
||||
error, by examining s list of objects. Any that are None
|
||||
are skipped; we call get_toplevel() on each object that has
|
||||
this method, and return the first non-None result.
|
||||
|
||||
If this fails, we turn to the application's main window instead."""
|
||||
|
||||
for error_object in error_objects:
|
||||
if not error_object:
|
||||
continue
|
||||
|
||||
if error_object and hasattr(error_object, "get_toplevel"):
|
||||
toplevel = error_object.get_toplevel()
|
||||
if toplevel:
|
||||
return toplevel
|
||||
|
||||
application = Gio.Application.get_default()
|
||||
return application.window if application else None
|
||||
|
||||
|
||||
def _create_error_wrapper(handler: Callable, handler_name: str,
|
||||
error_result: Any,
|
||||
error_method_name: str,
|
||||
connected_object: Any = None):
|
||||
"""Wraps a handler function in an error handler that will log and then report
|
||||
any exceptions, then return the 'error_result'."""
|
||||
|
||||
handler_object = handler.__self__ if hasattr(handler, "__self__") else None
|
||||
|
||||
def error_wrapper(*args, **kwargs):
|
||||
try:
|
||||
return handler(*args, **kwargs)
|
||||
except Exception as ex:
|
||||
logger.exception("Error handling %s: %s", handler_name, ex)
|
||||
|
||||
if handler_object and hasattr(handler_object, error_method_name):
|
||||
error_method = getattr(handler_object, error_method_name)
|
||||
error_method(ex)
|
||||
else:
|
||||
ErrorDialog(ex, parent=_get_error_parent([handler_object, connected_object]))
|
||||
return error_result
|
||||
|
||||
return error_wrapper
|
||||
|
||||
|
||||
def init_exception_backstops():
|
||||
"""This function is called once only, during startup, and replaces ("swizzles") a bunch of
|
||||
callback setup functions in GLib. The callbacks are all wrapped with error handlers that
|
||||
log the error and report it.
|
||||
|
||||
This is important to do because PyGObject will straight up crash if an exception escapes
|
||||
these handlers; it's better to tell the user and try to survive.
|
||||
|
||||
You can provide certain methods to provide error handling, but if you do not you get an
|
||||
ErrorDialog. Put these handling methods on the same object as the callback method itself.
|
||||
|
||||
We take care of these methods:
|
||||
GObject.Object.connect (via on_signal_error(self, error))
|
||||
GObject.add_emission_hook (via on_emission_hook_error(self, error))
|
||||
GLib.idle_add (via on_idle_error(self, error))
|
||||
GLib.timeout_add (via on_timeout_error(self, error))
|
||||
|
||||
Idle and timeout handlers will be disconnected if this happens to avoid repeated error reports,
|
||||
but signals and emission hooks will remain connected.
|
||||
"""
|
||||
|
||||
def _error_handling_connect(self: Gtk.Widget, signal_spec: str, handler, *args, **kwargs):
|
||||
error_wrapper = _create_error_wrapper(handler, f"signal '{signal_spec}'",
|
||||
error_result=None,
|
||||
error_method_name="on_signal_error",
|
||||
connected_object=self)
|
||||
return _original_connect(self, signal_spec, error_wrapper, *args, **kwargs)
|
||||
|
||||
def _error_handling_add_emission_hook(emitting_type, signal_spec, handler, *args, **kwargs):
|
||||
error_wrapper = _create_error_wrapper(handler, f"emission hook '{emitting_type}.{signal_spec}'",
|
||||
error_result=True, # stay attached
|
||||
error_method_name="on_emission_hook_error")
|
||||
return _original_add_emission_hook(emitting_type, signal_spec, error_wrapper, *args, **kwargs)
|
||||
|
||||
def _error_handling_idle_add(handler, *args, **kwargs):
|
||||
error_wrapper = _create_error_wrapper(handler, "idle function",
|
||||
error_result=False, # stop calling idle func
|
||||
error_method_name="on_idle_error")
|
||||
return _original_idle_add(error_wrapper, *args, **kwargs)
|
||||
|
||||
def _error_handling_timeout_add(interval, handler, *args, **kwargs):
|
||||
error_wrapper = _create_error_wrapper(handler, "timeout function",
|
||||
error_result=False, # stop calling timeout fund
|
||||
error_method_name="on_timeout_error")
|
||||
return _original_timeout_add(interval, error_wrapper, *args, **kwargs)
|
||||
|
||||
_original_connect = Gtk.Widget.connect
|
||||
GObject.Object.connect = _error_handling_connect
|
||||
|
||||
_original_add_emission_hook = GObject.add_emission_hook
|
||||
GObject.add_emission_hook = _error_handling_add_emission_hook
|
||||
|
||||
_original_idle_add = GLib.idle_add
|
||||
GLib.idle_add = _error_handling_idle_add
|
||||
|
||||
_original_timeout_add = GLib.timeout_add
|
||||
GLib.timeout_add = _error_handling_timeout_add
|
||||
|
|
|
@ -18,7 +18,8 @@ from lutris.config import LutrisConfig
|
|||
from lutris.database import categories as categories_db
|
||||
from lutris.database import games as games_db
|
||||
from lutris.database import sql
|
||||
from lutris.exceptions import GameConfigError, MissingExecutableError, watch_game_errors
|
||||
from lutris.exception_backstops import watch_game_errors
|
||||
from lutris.exceptions import GameConfigError, MissingExecutableError
|
||||
from lutris.runner_interpreter import export_bash_script, get_launch_parameters
|
||||
from lutris.runners import InvalidRunner, import_runner
|
||||
from lutris.util import audio, discord, extract, jobs, linux, strings, system, xdgshortcuts
|
||||
|
|
|
@ -58,7 +58,7 @@ from lutris.util.savesync import show_save_stats, upload_save, save_check
|
|||
from lutris.services import get_enabled_services
|
||||
from lutris.database.services import ServiceGameCollection
|
||||
from lutris.util.jobs import AsyncCall
|
||||
from lutris.exceptions import init_exception_backstops
|
||||
from lutris.exception_backstops import init_exception_backstops
|
||||
|
||||
from .lutriswindow import LutrisWindow
|
||||
|
||||
|
|
Loading…
Reference in a new issue