diff --git a/ftl/qt/errors.ftl b/ftl/qt/errors.ftl index e438d3b09..1b4e69236 100644 --- a/ftl/qt/errors.ftl +++ b/ftl/qt/errors.ftl @@ -1,4 +1,10 @@ -errors-support-site = [support site](https://help.ankiweb.net) +errors-standard-popup2 = + Anki encountered a problem. Please follow the troubleshooting steps. +errors-may-be-addon = The problem may be caused by an add-on. +errors-troubleshooting-button = Troubleshooting +errors-copy-debug-info-button = Copy Debug Info +errors-copied-to-clipboard = Copied to clipboard errors-standard-popup = # Error diff --git a/proto/anki/links.proto b/proto/anki/links.proto index 4d862adb9..dc3bd0552 100644 --- a/proto/anki/links.proto +++ b/proto/anki/links.proto @@ -42,6 +42,7 @@ message HelpPageLinkRequest { CARD_TYPE_MISSING_CLOZE = 20; CARD_TYPE_EXTRANEOUS_CLOZE = 21; CARD_TYPE_TEMPLATE_ERROR = 22; + TROUBLESHOOTING = 23; } HelpPage page = 1; } diff --git a/qt/aqt/about.py b/qt/aqt/about.py index 41be7d4a3..2b66fe734 100644 --- a/qt/aqt/about.py +++ b/qt/aqt/about.py @@ -2,12 +2,11 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import platform -import time import aqt.forms from anki.lang import without_unicode_isolation from anki.utils import version_with_build -from aqt.addons import AddonManager, AddonMeta +from aqt.errors import addon_debug_info from aqt.qt import * from aqt.utils import disable_help_button, supportText, tooltip, tr @@ -33,61 +32,15 @@ def show(mw: aqt.AnkiQt) -> QDialog: abt = aqt.forms.about.Ui_About() abt.setupUi(dialog) - # Copy debug info - ###################################################################### - - def addon_fmt(addmgr: AddonManager, addon: AddonMeta) -> str: - if addon.installed_at: - installed = time.strftime( - "%Y-%m-%dT%H:%M", time.localtime(addon.installed_at) - ) - else: - installed = "0" - if addon.provided_name: - name = addon.provided_name - else: - name = "''" - user = addmgr.getConfig(addon.dir_name) - default = addmgr.addonConfigDefaults(addon.dir_name) - if user == default: - modified = "''" - else: - modified = "mod" - return f"{name} ['{addon.dir_name}', {installed}, '{addon.human_version}', {modified}]" - - def onCopy() -> None: - addmgr = mw.addonManager - active = [] - activeids = [] - inactive = [] - for addon in addmgr.all_addon_meta(): - if addon.enabled: - active.append(addon_fmt(addmgr, addon)) - if addon.ankiweb_id(): - activeids.append(addon.dir_name) - else: - inactive.append(addon_fmt(addmgr, addon)) - newline = "\n" - info = f""" -{supportText()} - -===Add-ons (active)=== -(add-on provided name [Add-on folder, installed at, version, is config changed]) -{newline.join(sorted(active))} - -===IDs of active AnkiWeb add-ons=== -{" ".join(activeids)} - -===Add-ons (inactive)=== -(add-on provided name [Add-on folder, installed at, version, is config changed]) -{newline.join(sorted(inactive))} -""" - info = f" {' '.join(info.splitlines(True))}" - QApplication.clipboard().setText(info) + def on_copy() -> None: + txt = supportText() + if mw.addonManager.dirty: + txt += "\n" + addon_debug_info() + QApplication.clipboard().setText(txt) tooltip(tr.about_copied_to_clipboard(), parent=dialog) btn = QPushButton(tr.about_copy_debug_info()) - qconnect(btn.clicked, onCopy) + qconnect(btn.clicked, on_copy) abt.buttonBox.addButton(btn, QDialogButtonBox.ButtonRole.ActionRole) abt.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setFocus() diff --git a/qt/aqt/errors.py b/qt/aqt/errors.py index 47f89a631..608d1de5d 100644 --- a/qt/aqt/errors.py +++ b/qt/aqt/errors.py @@ -3,18 +3,18 @@ from __future__ import annotations -import html import re -import sys -import traceback +import time from typing import TYPE_CHECKING, Optional, TextIO, cast from markdown import markdown import aqt +from anki.collection import HelpPage from anki.errors import BackendError, Interrupted +from aqt.addons import AddonManager, AddonMeta from aqt.qt import * -from aqt.utils import showText, showWarning, supportText, tr +from aqt.utils import openHelp, showWarning, supportText, tooltip, tr if TYPE_CHECKING: from aqt.main import AnkiQt @@ -150,13 +150,14 @@ def is_chromium_cert_error(error: str) -> bool: if not os.environ.get("DEBUG"): def excepthook(etype, val, tb) -> None: # type: ignore - sys.stderr.write( - "Caught exception:\n%s\n" - % ("".join(traceback.format_exception(etype, val, tb))) - ) + sys.stderr.write("%s\n" % ("".join(traceback.format_exception(etype, val, tb)))) sys.excepthook = excepthook +# so we can be non-modal/non-blocking, without Python deallocating the message +# box ahead of time +_mbox: QMessageBox | None = None + class ErrorHandler(QObject): "Catch stderr and write into buffer." @@ -206,7 +207,7 @@ class ErrorHandler(QObject): if self.fatal_error_encountered: # suppress follow-up errors caused by the poisoned lock return - error = html.escape(self.pool) + error = self.pool self.pool = "" self.mw.progress.clear() if "AbortSchemaModification" in error: @@ -230,40 +231,111 @@ class ErrorHandler(QObject): if is_chromium_cert_error(error): return + debug_text = supportText() + "\n" + error + if "PanicException" in error: self.fatal_error_encountered = True - txt = markdown( - "**A fatal error occurred, and Anki must close. Please report this message on the forums.**" - ) - error = f"{supportText() + self._addonText(error)}\n{error}" - elif self.mw.addonManager.dirty: - # Older translations include a link to the old discussions site; rewrite it to a newer one - message = tr.errors_addons_active_popup().replace( - "https://help.ankiweb.net/discussions/add-ons/", - "https://forums.ankiweb.net/c/add-ons/11", - ) - txt = markdown(message) - error = f"{supportText() + self._addonText(error)}\n{error}" + # ensure no collection-related timers like backup fire + self.mw.col = None + user_text = "A fatal error occurred, and Anki must close. Please report this message on the forums." else: - txt = markdown(tr.errors_standard_popup()) - error = f"{supportText()}\n{error}" + user_text = tr.errors_standard_popup2() + if self.mw.addonManager.dirty: + user_text += "\n\n" + self._addonText(error) + debug_text += addon_debug_info() + + def show_troubleshooting(): + openHelp(HelpPage.TROUBLESHOOTING) + + def copy_debug_info(): + QApplication.clipboard().setText(debug_text) + tooltip(tr.errors_copied_to_clipboard(), parent=_mbox) + + global _mbox + _mbox = QMessageBox() + _mbox.setWindowTitle("Anki") + _mbox.setText(user_text) + _mbox.setIcon(QMessageBox.Icon.Warning) + _mbox.setTextFormat(Qt.TextFormat.PlainText) + + troubleshooting = _mbox.addButton( + tr.errors_troubleshooting_button(), QMessageBox.ButtonRole.ActionRole + ) + debug_info = _mbox.addButton( + tr.errors_copy_debug_info_button(), QMessageBox.ButtonRole.ActionRole + ) + cancel = _mbox.addButton(QMessageBox.StandardButton.Cancel) + cancel.setText(tr.actions_close()) + + troubleshooting.disconnect() + troubleshooting.clicked.connect(show_troubleshooting) + debug_info.disconnect() + debug_info.clicked.connect(copy_debug_info) - # show dialog - txt = f"{txt}