diff --git a/src/AutoSplit.py b/src/AutoSplit.py index c39b101e..0550da6c 100644 --- a/src/AutoSplit.py +++ b/src/AutoSplit.py @@ -1,6 +1,8 @@ #!/usr/bin/python3.9 # -*- coding: utf-8 -*- -from typing import Callable, List, Optional +import traceback +from types import FunctionType, TracebackType +from typing import Callable, List, Optional, Type from copy import copy from PyQt6 import QtCore, QtGui, QtTest, QtWidgets @@ -51,6 +53,8 @@ class AutoSplit(QtWidgets.QMainWindow, design.Ui_MainWindow): undoSplitSignal = QtCore.pyqtSignal() pauseSignal = QtCore.pyqtSignal() afterSettingHotkeySignal = QtCore.pyqtSignal() + # Use this signal when trying to show an error from outside the main thread + showErrorSignal = QtCore.pyqtSignal(FunctionType) def __init__(self, parent=None): super(AutoSplit, self).__init__(parent) @@ -104,7 +108,7 @@ def run(self): try: line = input() except RuntimeError: - # stdin not supported or lost, stop looking for inputs + self.autosplit.showErrorSignal.emit(error_messages.stdinLostError) break # TODO: "AutoSplit Integration" needs to call this and wait instead of outright killing the app. # TODO: See if we can also get LiveSplit to wait on Exit in "AutoSplit Integration" @@ -169,6 +173,7 @@ def run(self): self.resetSignal.connect(self.reset) self.skipSplitSignal.connect(self.skipSplit) self.undoSplitSignal.connect(self.undoSplit) + self.showErrorSignal.connect(lambda errorMessageBox: errorMessageBox()) # live image checkbox self.liveimageCheckBox.clicked.connect(self.checkLiveImage) @@ -1193,22 +1198,50 @@ def exit(): def main(): app = QtWidgets.QApplication(sys.argv) - app.setWindowIcon(QtGui.QIcon(':/resources/icon.ico')) - - main_window = AutoSplit() - main_window.show() - if main_window.actionCheck_for_Updates_on_Open.isChecked(): - checkForUpdates(main_window, check_for_updates_on_open=True) + try: + app.setWindowIcon(QtGui.QIcon(':/resources/icon.ico')) + main_window = AutoSplit() + main_window.show() + # Needs to be after main_window.show() to be shown over + if main_window.actionCheck_for_Updates_on_Open.isChecked(): + checkForUpdates(main_window, check_for_updates_on_open=True) + + # Kickoff the event loop every so often so we can handle KeyboardInterrupt (^C) + timer = QtCore.QTimer() + timer.timeout.connect(lambda: None) + timer.start(500) + + exit_code = app.exec() + except Exception as exception: + # Print error to console if not running in executable + if getattr(sys, 'frozen', False): + error_messages.exceptionTraceback( + "AutoSplit encountered an unrecoverable exception and will now close itself.
" + "Please copy the following message over at
" + "github.com/Toufool/Auto-Split/issues", + exception) + else: + traceback.print_exception(type(exception), exception, exception.__traceback__) + sys.exit(1) - # Kickoff the event loop every so often so we can handle KeyboardInterrupt (^C) - timer = QtCore.QTimer() - timer.timeout.connect(lambda: None) - timer.start(500) # Catch Keyboard Interrupts for a clean close - signal.signal(signal.SIGINT, lambda _, __: sys.exit(app)) + signal.signal(signal.SIGINT, lambda code, _: sys.exit(code)) + + sys.exit(exit_code) - sys.exit(app.exec()) + +def excepthook(exceptionType: Type[BaseException], exception: BaseException, traceback: Optional[TracebackType]): + # Catch Keyboard Interrupts for a clean close + if exceptionType is KeyboardInterrupt: + sys.exit(0) + error_messages.exceptionTraceback( + "AutoSplit encountered an unhandled exception and will try to recover, " + "however, things may not work quite right.
" + "Please copy the following message over at
" + "github.com/Toufool/Auto-Split/issues", + exception) if __name__ == '__main__': + sys.excepthook = excepthook main() diff --git a/src/error_messages.py b/src/error_messages.py index 1a0e40f1..925d6341 100644 --- a/src/error_messages.py +++ b/src/error_messages.py @@ -1,12 +1,20 @@ # Error messages -from PyQt6 import QtWidgets +import traceback +from PyQt6 import QtCore, QtWidgets -def setTextMessage(message: str): - msgBox = QtWidgets.QMessageBox() - msgBox.setWindowTitle('Error') - msgBox.setText(message) - msgBox.exec() +def setTextMessage(message: str, details: str = ''): + messageBox = QtWidgets.QMessageBox() + messageBox.setWindowTitle('Error') + messageBox.setTextFormat(QtCore.Qt.TextFormat.RichText) + messageBox.setText(message) + if details: + messageBox.setDetailedText(details) + for button in messageBox.buttons(): + if messageBox.buttonRole(button) == QtWidgets.QMessageBox.ButtonRole.ActionRole: + button.click() + break + messageBox.exec() def splitImageDirectoryError(): @@ -76,5 +84,16 @@ def noSettingsFileOnOpenError(): def tooManySettingsFilesOnOpenError(): setTextMessage("Too many settings files found. Only one can be loaded on open if placed in the same folder as AutoSplit.exe") + def checkForUpdatesError(): setTextMessage("An error occurred while attempting to check for updates. Please check your connection.") + + +def stdinLostError(): + setTextMessage("stdin not supported or lost, external control like LiveSplit integration will not work.") + + +def exceptionTraceback(message: str, exception: Exception): + setTextMessage( + message, + "\n".join(traceback.format_exception(None, exception, exception.__traceback__))) diff --git a/src/settings_file.py b/src/settings_file.py index 26f15b2d..052bed5c 100644 --- a/src/settings_file.py +++ b/src/settings_file.py @@ -157,10 +157,11 @@ def saveSettingsAs(self: AutoSplit): def loadSettings(self: AutoSplit, load_settings_on_open: bool = False, load_settings_from_livesplit: bool = False): if load_settings_on_open: - settings_files = [] - for file in os.listdir(auto_split_directory): - if file.endswith(".pkl"): - settings_files.append(file) + settings_files = [ + file for file + in os.listdir(auto_split_directory) + if file.endswith(".pkl") + ] # find all .pkls in AutoSplit folder, error if there is none or more than 1 if len(settings_files) < 1: @@ -190,8 +191,7 @@ def loadSettings(self: AutoSplit, load_settings_on_open: bool = False, load_sett settings: List[Union[str, int]] = pickle.load(f) settings_count = len(settings) if settings_count < 18: - if not load_settings_from_livesplit: - error_messages.oldVersionSettingsFileError() + self.showErrorSignal.emit(error_messages.oldVersionSettingsFileError) return # v1.3-1.4 settings. Add default pause_key and auto_start_on_reset_setting if settings_count == 18: @@ -199,8 +199,7 @@ def loadSettings(self: AutoSplit, load_settings_on_open: bool = False, load_sett settings.insert(20, 0) # v1.5 settings elif settings_count != 20: - if not load_settings_from_livesplit: - error_messages.invalidSettingsError() + self.showErrorSignal.emit(error_messages.invalidSettingsError) return self.last_loaded_settings = [ self.split_image_directory, @@ -224,11 +223,7 @@ def loadSettings(self: AutoSplit, load_settings_on_open: bool = False, load_sett self.loop_setting, self.auto_start_on_reset_setting] = settings except (FileNotFoundError, MemoryError, pickle.UnpicklingError): - # HACK / Workaround: Executing the error QMessageBox from the auto-controlled Worker Thread makes it hangs. - # I don't like this solution as we should probably ensure the Worker works nicely with PyQt instead, - # but in the mean time, this will do. - if not load_settings_from_livesplit: - error_messages.invalidSettingsError() + self.showErrorSignal.emit(error_messages.invalidSettingsError) return self.splitimagefolderLineEdit.setText(self.split_image_directory)