Never touch the WebView2 from a background thread on a book rename (BL-16749) - #8232
Never touch the WebView2 from a background thread on a book rename (BL-16749)#8232StephenMcConnel wants to merge 3 commits into
Conversation
…L-16749) Importing a .bloomSource as a derivative could fail with "CoreWebView2 can only be accessed from the UI thread", leaving the new book on disk but the import reported to the user as failed. The import runs off the UI thread: the importBloomSource endpoint is registered handleOnUiThread: false and the work itself runs behind the collection tab's progress dialog. Bringing the new book up to date saves it, which renames its folder to the book's title and raises BookRenamedEvent inline, on that worker thread. EditingView answers that event by refreshing the page list, which runs javascript in the workspace's WebView2 - and the readiness guard in RunJavascriptFireAndForget reads _webview.CoreWebView2, which the WinForms wrapper cannot always hand to a thread other than the one that created the control. The guard meant to keep that call safe was therefore the crash site. Whether the read throws depends on the apartment the ICoreWebView2Controller RCW ended up in, which is why it fails on some machines and not others: on the reporter's machine (a native ARM64 build) the cross-apartment QueryInterface fails E_NOINTERFACE, while the same import completes on x64 here. It also takes a selected book, which is what gates the two WebView2 calls in the handler - so a first import into a collection with nothing selected succeeds, and the next one, now that the import has selected a book, does not. Two changes, so neither the browser nor its callers depend on that lottery: - RunJavascriptFireAndForget re-posts itself to the control's own thread when Invoke is required, before anything reads CoreWebView2. The method is fire-and-forget by contract, so marshalling costs the caller nothing, and this also covers its other callers on book-update, import and publish paths. - EditingView's BookRenamedEvent handler moves out of the constructor into HandleBookRenamed and is marshalled as a whole by HandleBookRenamedOnUiThread. It writes Settings.Default and drives WinForms as well, so the whole handler wants the UI thread rather than each callee defending itself. With no window open (unit tests, command-line tools) it runs inline, as CollectionModel.SelectBookOnUiThread does. Verified in the running app: the rename arrives on the worker thread and the handler body then executes on the UI thread, and the import completes. Adds a test that renames a book from a worker thread and checks that the rename and its notification both come through. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
| Filename | Overview |
|---|---|
| src/BloomExe/Edit/EditingView.cs | Moves the rename-event response onto the UI thread before updating settings, page-list controls, and WebView2 state. |
| src/BloomExe/WebView2Browser.cs | Marshals fire-and-forget JavaScript calls before accessing CoreWebView2 and handles expected disposal races while posting. |
| src/BloomTests/Book/BookStorageTests.cs | Adds an NUnit test verifying rename and inline notification behavior when SetBookName runs on a worker thread. |
Reviews (2): Last reviewed commit: "Say what the worker-thread rename test d..." | Re-trigger Greptile
Devin flagged the synchronous form.Invoke as worth a look. It is deliberate -- it keeps the handler's old ordering, so the settings write and the page-list refresh still finish before the rename returns -- and it can only deadlock if the UI thread is blocked waiting on the worker, which no current path does. Recording that reasoning where the next reader will ask the question. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…749) Greptile pointed out that the test's comment claimed more than the test does: it said the test showed a subscriber that marshals to the UI thread still runs with no window, but the subscriber here is just a lambda in the test. Describe what it actually checks -- the folder and file are renamed and the subscriber fires once on the worker thread -- and say plainly that the WebView2 half needs the running app. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
[Claude Opus 5 (1M context) from Steve McConnel's machine during preflight] Consulted Devin on 2026-08-25 18:00 UTC up to commit No bugs, and nothing flagged for investigation. Two informational notes, neither mirrored as a thread: one confirms the new thread guard in |
|
[Claude Opus 5 (1M context) from Steve McConnel's machine during preflight] Answering Greptile's review summary, which said the new test's documentation overstates what it covers. Fair point, and fixed in |
Importing a
.bloomSourceas a derivative could fail withCoreWebView2 can only be accessed from the UI thread, leaving the new book on disk but telling the user the import failed.The import runs off the UI thread (
importBloomSourceis registeredhandleOnUiThread: false, and the work runs behind the collection tab's progress dialog). Bringing the new book up to date saves it, which renames its folder to the book's title and raisesBookRenamedEventinline, on that worker thread.EditingViewanswers by refreshing the page list, which runs javascript in the workspace's WebView2 — and the readiness guard inRunJavascriptFireAndForgetreads_webview.CoreWebView2, which the WinForms wrapper cannot always hand to a thread other than the one that created the control. The guard meant to keep that call safe was the crash site.Why only some machines: whether that read throws depends on the apartment the
ICoreWebView2ControllerRCW ended up in. On the reporter's machine (a native ARM64 build) the cross-apartmentQueryInterfacefailsE_NOINTERFACE; the same import completes on x64 here. It also takes a selected book, which is what gates the two WebView2 calls in the handler — so a first import into a collection with nothing selected succeeds, and the next one (the import having selected a book) does not.Two changes, so neither the browser nor its callers depend on that lottery:
WebView2Browser.RunJavascriptFireAndForgetre-posts itself to the control's own thread whenInvokeis required, before anything readsCoreWebView2. The method is fire-and-forget by contract, so marshalling costs the caller nothing, and this also covers its other callers on book-update, import and publish paths.EditingView'sBookRenamedEventhandler moves out of the constructor intoHandleBookRenamed, marshalled as a whole byHandleBookRenamedOnUiThread. It writesSettings.Defaultand drives WinForms too, so the whole handler wants the UI thread. With no window open (unit tests, command-line tools) it runs inline, asCollectionModel.SelectBookOnUiThreaddoes.Verified in the running app: the rename arrives on the worker thread and the handler body then executes on the UI thread, and the import completes. Adds a test that renames a book from a worker thread and checks the rename and its notification both come through.
Note: the branch name carries BL-16646 (an earlier ticket worked in this worktree); this change fixes BL-16749.
Ref: https://issues.bloomlibrary.org/youtrack/issue/BL-16749
Devin review
This change is