From db5117ce1ab3b1393419d4fcc9c03678094f2f30 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 8 Dec 2021 21:11:37 +1000 Subject: [PATCH] fix webviews sometimes failing to load, by enabling cache Fixes #983 This has been a long-standing issue that was infrequent enough on developer machines that we weren't able to get to the bottom of it before now. As luck would have it, the new ARM build had just the right timing for this to occur every few invocations, and I was able to narrow it down to the call that turns off the cache. We don't really want the cache, so this is not a great solution. But I ran into trouble when trying to figure out a better solution: - Calling setHttpCacheType() earlier (eg immediately after creating the page) fails. - It even fails if you attempt to change the setting in the shared default profile before any webviews are loaded: ``` def setupMainWindow(self) -> None: QWebEngineProfile.defaultProfile().setHttpCacheType(QWebEngineProfile.HttpCacheType.NoCache) ``` - Creating a profile separately, and passing it into QWebEnginePage() does work. But it introduces a warning each time a webview is deallocated, and I fear it may lead to crashes: ``` Release of profile requested but WebEnginePage still not deleted. Expect troubles ! ``` I tried various combinations of parents for the profile and page, and turning web._page into an unretained property, but could not figure it out. Some Googling pulls up a bunch of other people who seem to have hit similar issues with PyQt. If anyone has any ideas, they'd be welcome; in the mean time, I guess we're better off using up some of the user's disk space than sometimes failing to load. The profile-in-advance code I tried is below: ``` diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index 1c96112d8..4f3e91284 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -23,9 +23,49 @@ serverbaseurl = re.compile(r"^.+:\/\/[^\/]+") BridgeCommandHandler = Callable[[str], Any] +def _create_profile(parent: QObject) -> QWebEngineProfile: + qwebchannel = ":/qtwebchannel/qwebchannel.js" + jsfile = QFile(qwebchannel) + if not jsfile.open(QIODevice.OpenModeFlag.ReadOnly): + print(f"Error opening '{qwebchannel}': {jsfile.error()}", file=sys.stderr) + jstext = bytes(cast(bytes, jsfile.readAll())).decode("utf-8") + jsfile.close() + + script = QWebEngineScript() + script.setSourceCode( + jstext + + """ + var pycmd, bridgeCommand; + new QWebChannel(qt.webChannelTransport, function(channel) { + bridgeCommand = pycmd = function (arg, cb) { + var resultCB = function (res) { + // pass result back to user-provided callback + if (cb) { + cb(JSON.parse(res)); + } + } + + channel.objects.py.cmd(arg, resultCB); + return false; + } + pycmd("domDone"); + }); + """ + ) + script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld) + script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady) + script.setRunsOnSubFrames(False) + + profile = QWebEngineProfile(parent) + profile.setHttpCacheType(QWebEngineProfile.HttpCacheType.NoCache) + profile.scripts().insert(script) + return profile + + class AnkiWebPage(QWebEnginePage): - def __init__(self, onBridgeCmd: BridgeCommandHandler) -> None: - QWebEnginePage.__init__(self) + def __init__(self, onBridgeCmd: BridgeCommandHandler, parent: QObject) -> None: + profile = _create_profile(parent) + QWebEnginePage.__init__(self, profile, parent) self._onBridgeCmd = onBridgeCmd self._setupBridge() self.open_links_externally = True @@ -46,39 +86,6 @@ class AnkiWebPage(QWebEnginePage): self._channel.registerObject("py", self._bridge) self.setWebChannel(self._channel) - qwebchannel = ":/qtwebchannel/qwebchannel.js" - jsfile = QFile(qwebchannel) - if not jsfile.open(QIODevice.OpenModeFlag.ReadOnly): - print(f"Error opening '{qwebchannel}': {jsfile.error()}", file=sys.stderr) - jstext = bytes(cast(bytes, jsfile.readAll())).decode("utf-8") - jsfile.close() - - script = QWebEngineScript() - script.setSourceCode( - jstext - + """ - var pycmd, bridgeCommand; - new QWebChannel(qt.webChannelTransport, function(channel) { - bridgeCommand = pycmd = function (arg, cb) { - var resultCB = function (res) { - // pass result back to user-provided callback - if (cb) { - cb(JSON.parse(res)); - } - } - - channel.objects.py.cmd(arg, resultCB); - return false; - } - pycmd("domDone"); - }); - """ - ) - script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld) - script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady) - script.setRunsOnSubFrames(False) - self.profile().scripts().insert(script) - def javaScriptConsoleMessage( self, level: QWebEnginePage.JavaScriptConsoleMessageLevel, @@ -228,7 +235,7 @@ class AnkiWebView(QWebEngineView): ) -> None: QWebEngineView.__init__(self, parent=parent) self.set_title(title) - self._page = AnkiWebPage(self._onBridgeCmd) + self._page = AnkiWebPage(self._onBridgeCmd, self) self._page.setBackgroundColor( self.get_window_bg_color(theme_manager.night_mode) @@ -242,7 +249,6 @@ class AnkiWebView(QWebEngineView): self.requiresCol = True self.setPage(self._page) - self._page.profile().setHttpCacheType(QWebEngineProfile.HttpCacheType.NoCache) self.resetHandlers() self.allowDrops = False self._filterSet = False ``` --- qt/aqt/webview.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index 1c96112d8..5ced290eb 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -242,7 +242,6 @@ class AnkiWebView(QWebEngineView): self.requiresCol = True self.setPage(self._page) - self._page.profile().setHttpCacheType(QWebEngineProfile.HttpCacheType.NoCache) self.resetHandlers() self.allowDrops = False self._filterSet = False