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}
{error}
" - showText(txt, type="html", copyBtn=True) if self.fatal_error_encountered: + _mbox.exec() sys.exit(1) + else: + _mbox.show() def _addonText(self, error: str) -> str: matches = re.findall(r"addons21(/|\\)(.*?)(/|\\)", error) if not matches: - return "" + return tr.errors_may_be_addon() # reverse to list most likely suspect first, dict to deduplicate: addons = [ aqt.mw.addonManager.addonName(i[1]) for i in dict.fromkeys(reversed(matches)) ] - # highlight importance of first add-on: - addons[0] = f"{addons[0]}" addons_str = ", ".join(addons) - return f"{tr.addons_possibly_involved(addons=addons_str)}\n" + return tr.addons_possibly_involved(addons=addons_str) + + +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 addon_debug_info() -> str: + from aqt import mw + + 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"""\ +===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))} +""" + return info diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index 360139447..6e69b7b9d 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -1098,39 +1098,23 @@ def add_ellipsis_to_action_label(*actions: QAction) -> None: def supportText() -> str: import platform - import time from aqt import mw platname = platform.platform() - def schedVer() -> str: - try: - if mw.col.v3_scheduler(): - return "3" - else: - return str(mw.col.sched_ver()) - except: - return "?" - - lc = mw.pm.last_addon_update_check() - lcfmt = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(lc)) - return """\ -Anki {} Python {} Qt {} PyQt {} +Anki {} {} {} +Python {} Qt {} PyQt {} Platform: {} -Flags: frz={} ao={} sv={} -Add-ons, last update check: {} """.format( version_with_build(), + "(src)" if not getattr(sys, "frozen", False) else "", + "(ao)" if mw.addonManager.dirty else "", platform.python_version(), qVersion(), PYQT_VERSION_STR, platname, - getattr(sys, "frozen", False), - mw.addonManager.dirty, - schedVer(), - lcfmt, ) diff --git a/rslib/src/links.rs b/rslib/src/links.rs index 2f39acd6c..90e8e9ae2 100644 --- a/rslib/src/links.rs +++ b/rslib/src/links.rs @@ -41,6 +41,7 @@ pub fn help_page_link_suffix(page: HelpPage) -> &'static str { HelpPage::CardTypeExtraneousCloze => { "templates/errors.html#cloze-filter-outside-cloze-notetype" } + HelpPage::Troubleshooting => "troubleshooting.html", } }