Rework error dialog

- Hide traceback
- Include full add-on info in 'copy debug info' button, like about
screen
- Link to troubleshooting page
- Use non-modal pop-up in the common case, to avoid potential conflicts
with other modals.

Closes #2830
This commit is contained in:
Damien Elmes 2023-11-29 10:25:32 +10:00
parent e5170f341b
commit 063b6f60fd
6 changed files with 121 additions and 104 deletions

View file

@ -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

View file

@ -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;
}

View file

@ -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()

View file

@ -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}<div style='white-space: pre-wrap'>{error}</div>"
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"<b>{addons[0]}</b>"
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

View file

@ -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,
)

View file

@ -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",
}
}