mirror of
https://github.com/serrebidev/BlindRSS.git
synced 2026-08-13 11:59:26 -07:00
Regression: Full article text remains in the read-only preview area #64
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
serrebi/BlindRSS#64
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
In versions between 1.119.0 and 1.120.3 (but that's not certain), a regression has appeared where the full text of an article loaded into the preview area stays there even after the user selects a different article. The issue is intermittent: it does not occur consistently, and no definitive set of steps reliably reproduces it. The problem is only resolved by restarting the application.
Affected Versions
Preconditions
Steps to Reproduce
Tabto move focus to the preview area displaying the extracted full text of the article.Shift+Tabto return focus to the list of articles.Tabto move focus to the preview area for the newly selected article.Actual Result
Expected Result
Additional Notes
Thanks @tseykovets — reproduced the mechanism. Your instinct that it's non-deterministic is right, but there is a hard trigger condition behind it, and it explains why only a restart clears it.
Regression window:
0c4fa73("fix: reduce NVDA lag in YouTube readers", v1.120.3), which added_swap_focused_large_readerand the_reader_displayed_textmemo ingui/mainframe.py.The trigger
Three conditions have to line up, which is why it's intermittent:
LARGE_READER_TEXT_CHARS).Short articles never take this path, which is why no fixed set of steps reproduces it.
What happens
To hand NVDA a fully-populated RichEdit in one focus transition,
_swap_focused_large_readerbuilds a newwx.TextCtrl, swaps it into the sizer, pointsself.content_ctrlat it, and callsnew.SetFocus().That
SetFocus()firesEVT_SET_FOCUSsynchronously →on_content_focus→_schedule_fulltext_load_for_index(idx, force=True). The text was already cached by_fulltext_apply_resultbefore it applied it, and a successful web extraction iscache_source == "web", so_cached_fulltext_is_authoritativepasses and the cache-hit branch calls_set_article_reader_textsynchronously, re-entering the function that is still mid-swap.The memo that would short-circuit that re-entry (
_reader_displayed_text) is assigned only after the swapper returns (mainframe.py:5257-5258), so on re-entry it still holds the previous article's text, the guard misses, and it swaps again. Each turn builds another TextCtrl holding the same multi-megabyte string.I bound the real
_set_article_reader_textand_swap_focused_large_readerto a stub reproducing that call graph. It recurses without bound — it only stopped at depth 61 because I capped it:Why the stale text is permanent
This is the part that answers "only a restart fixes it". The swap mutates shared state before the risky call and rolls nothing back on failure:
The
except Exceptionhandler only destroysnewif new is not self.content_ctrl— andnewisself.content_ctrlby then, so nothing is undone. The old control stays visible and parented, showing the full text it was last given, whileself.content_ctrlpoints at a different control. Every later write — including the "Loading..." reset and the next article's description — goes to the control you can no longer see. The pane is frozen on the previous article's text for the rest of the session. My stub run ends in exactly that state: two visible controls,content_ctrlpointing at the wrong one.Fix direction
_reader_displayed_textbefore invoking the swapper, so the re-entrant call short-circuits, and add an explicit reentrancy guard around the swap.self.content_ctrland the sizer, or do theSetFocus()last with the old control already retired.content_ctrl.SetValue(...)writes (mainframe.py:1579,1627,1684,9593,9609,11100) which leave it describing text the control no longer holds. Those should invalidate it.Note there's no test coverage for
_swap_focused_large_reader—tests/test_reader_performance.pyonly exercises thereader_performance.pyhelpers, not the mainframe swap, which is how this got through.Happy to put up a PR with the fix and a regression test that asserts the swap is non-reentrant and leaves exactly one visible control on failure — say the word.
Fixed and shipped in v1.120.5 — https://github.com/serrebidev/BlindRSS/releases/tag/v1.120.5
Windows installer and portable ZIP are up now; the macOS/Linux assets are attaching from the Actions build. Auto-update will offer it.
Fix is
420ebb4, three changes on top of the diagnosis above:_swap_focused_large_readerrefuses to run re-entrantly, so theSetFocus()→on_content_focus→ cache-hit path can no longer start a second swap inside the first. The guard is released in afinally, so a failed attempt can't wedge every later swap.content_ctrlpointed back at the surviving control, replacement destroyed. This was the part that made it permanent: previously a half-applied swap left the old control visible while writes went somewhere else. The article text still reaches the reader through the normal path when the swap is skipped, since the swap is only a latency optimisation._reader_displayed_textis now recorded before the swap, cleared if a write raises, and invalidated at the ten directcontent_ctrlwrites that used to leave it describing text the control no longer held.New regression tests in
tests/test_reader_swap_reentrancy.pycover non-reentrancy, rollback, guard release, and memo invalidation. Four of the five fail against the old build — the runaway one built 10 replacement controls where 2 is correct — so they pin the actual bug rather than just the fixed behaviour. Full suite: 1970 passed, 7 skipped.One thing worth saying plainly: because you couldn't pin down reliable steps, it would have been easy to file this as unreproducible. The steps you did give were exactly right — the Tab into the preview is what puts focus in the reader when the extraction lands, which is one of the three conditions. The other two are the classic reader and an article whose extracted text runs over 16384 characters, which is why it only ever hit some articles.
Please do reopen if you see any stale text after updating.