Skip to content

fix(viewer): respawn the webview when it dies mid-D-Bus call instead of crashing - #3012

Merged
vpetersson merged 2 commits into
masterfrom
fix/viewer-dbus-noreply-crash
Jun 7, 2026
Merged

fix(viewer): respawn the webview when it dies mid-D-Bus call instead of crashing#3012
vpetersson merged 2 commits into
masterfrom
fix/viewer-dbus-noreply-crash

Conversation

@vpetersson

@vpetersson vpetersson commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Issues Fixed

Sentry event 58040ab3c2844b07a5d0707a77ba4122 (release 2026.6.2): GError: GDBus.Error:org.freedesktop.DBus.Error.NoReply: Message recipient disconnected from message bus without replying raised from view_image(STANDBY_SCREEN) at viewer startup, crashing the viewer into a container restart loop.

Description

The flaky armv7 Qt5 init crash (malloc(): unaligned tcache chunk detected) that load_browser() already retries past can also strike after the D-Bus handshake. In the Sentry event's breadcrumbs the webview came up on spawn attempt 3/30, then died ~0.4 s later while loadImage was in flight — the death surfaced as a GError out of call_sync instead of browser.is_alive() == False, escaped main(), and defeated the whole spawn-retry machinery.

  • New _send_to_webview() wrapper around loadImage/loadPage: on a webview-gone D-Bus error (NoReply, ServiceUnknown, NameHasNoOwner), reap the dead process, respawn with the existing inline budget, and retry the call once. Other errors (including Disconnected, which means our session bus died) still propagate so the container restart remains the last resort.
  • load_browser() now resets current_browser_url: a fresh webview displays nothing, so the previous process's URL must not short-circuit the value comparison in view_image/view_webpage. Previously a webview that crashed mid-asset with an unchanged URL (e.g. a single-asset playlist) respawned to a permanently blank screen.
  • 6 new unit tests covering the respawn-and-retry path for both image and webpage assets, error-code selectivity, retry-failure propagation, and the URL reset.

Checklist

  • I have performed a self-review of my own code.
  • New and existing unit tests pass locally and on CI with my changes.
  • I have done an end-to-end test for Raspberry Pi devices.
  • I have tested my changes for x86 devices.
  • I added a documentation for the changes I have made (when necessary).

🤖 Generated with Claude Code

…of crashing

- The armv7 Qt5 heap-corruption crash that load_browser() already
  retries past can also strike after the D-Bus handshake; the death
  then surfaces as a GError (NoReply) out of the in-flight
  loadImage/loadPage call, escapes main(), and turns one process
  crash into a container restart loop (Sentry 58040ab3)
- Wrap loadImage/loadPage in _send_to_webview(): on a webview-gone
  D-Bus error, reap the dead process, respawn with the inline
  budget, and retry the call once; anything else still raises
- Reset current_browser_url in load_browser() so a respawned webview
  always gets its asset re-sent, even when the URL is unchanged
  (a crashed single-asset playlist previously respawned to a blank
  screen forever)

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@vpetersson
vpetersson requested a review from a team as a code owner June 7, 2026 08:40
@vpetersson vpetersson self-assigned this Jun 7, 2026
@vpetersson
vpetersson requested a review from Copilot June 7, 2026 08:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the viewer’s interaction with the AnthiasViewer D-Bus webview so that if the webview process dies during an in-flight D-Bus call (e.g., NoReply), the viewer respawns the webview and retries the call once instead of crashing into a container restart loop.

Changes:

  • Added _send_to_webview() + _is_webview_gone_error() to detect “webview gone” D-Bus failures and perform a single respawn-and-retry for loadImage / loadPage.
  • Updated load_browser() to reset current_browser_url on every spawn so a fresh webview won’t remain blank due to URL short-circuiting.
  • Added unit tests covering respawn/retry behavior, selective error handling, retry failure propagation, and the URL reset.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/anthias_viewer/__init__.py Adds respawn-and-retry wrapper for mid-call D-Bus failures and resets cached URL on webview respawn.
tests/test_viewer.py Adds test coverage for the new respawn/retry logic and current_browser_url reset behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Resolves SonarCloud S5332 security hotspots on the PR

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@sonarqubecloud

sonarqubecloud Bot commented Jun 7, 2026

Copy link
Copy Markdown

@vpetersson
vpetersson merged commit 50aa201 into master Jun 7, 2026
9 checks passed
@vpetersson

Copy link
Copy Markdown
Contributor Author

On-device validation (real Pi 3 Model B+, armhf Qt5 image)

Ran the full integrated stack on a Raspberry Pi 3 Model B+ (788 MB, aarch64 kernel, armhf viewer image, server/redis from the running compose project) with this branch's src/ bind-mounted into the viewer container. The board reproduced the field failure signature on its own: malloc(): unaligned tcache chunk detected spawn crashes with the webview coming up on attempt 3/30 — the Sentry event's breadcrumbs verbatim.

Scenario A — webview dies mid-D-Bus-call (the Sentry crash): SIGSTOP the webview so the next loadImage blocks in flight, then SIGKILL. The viewer logged the exact field error and recovered instead of crashing:

09:27:11.341 AnthiasViewer died mid D-Bus call; respawning and retrying once:
             GDBus.Error:org.freedesktop.DBus.Error.NoReply: Message recipient
             disconnected from message bus without replying (4)
09:27:11.341 Loading browser...

On 2026.6.2 this GError escapes main() with zero respawn attempts. (This round the inline 3-attempt budget happened to be exhausted by further init crashes → the designed WebviewLaunchError → container-restart fallback re-rolled with the 30-attempt budget and recovered. On an earlier run of the same scenario the retry succeeded in-process with ~1.4 s total recovery.)

Scenario B — blank-screen fix (unchanged-URL resend): single-asset playlist, clean SIGKILL between rotations. In-process respawn on attempt 2/3, and the fresh webview's stdout proves the unchanged URL was re-sent:

09:32:37.122 Loading browser...
09:32:39.948 AnthiasViewer started on attempt 2/3
09:32:39.981 Current url is http://anthias-server:8080/static/img/anthias-loading.png
webview stdout: Anthias service start → Loading image from: ".../anthias-loading.png"

Without the current_browser_url reset the respawned webview never receives loadImage and stays blank.

Also observed across the session: several natural post-handshake webview deaths were absorbed by the new path (in-process respawns and designed budget-exhaustion container restarts), with the playlist resuming every time.

Caveat: the unit is headless (no HDMI connected → no /dev/fb0 under KMS), so the webview ran under QT_QPA_PLATFORM=offscreen instead of linuxfb. The QPA only affects rendering; the D-Bus lifecycle and recovery paths under test are identical, and the armv7 init heap-corruption reproduced under offscreen as well.

🤖 Generated with Claude Code

vpetersson added a commit that referenced this pull request Jun 7, 2026
…3020)

- The armv7 Qt5 init crash can strike in the gap between the D-Bus
  handshake (which made load_browser() return) and setup()'s
  bus.get — the anthias.viewer name is released again, pydbus raises
  ServiceUnknown, and the GError escaped main() into a container
  restart loop (Sentry ANTHIAS-3)
- Apply the same webview-gone detection and respawn-then-retry-once
  contract as _send_to_webview (#3012), with the generous startup
  budget since nothing is on screen yet
- Unrelated D-Bus errors (e.g. Disconnected) still propagate so the
  container restart handles what a respawn can't fix
- Add regression tests for both paths

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
vpetersson added a commit that referenced this pull request Jun 8, 2026
…+stop (#3031)

- MPVMediaPlayer.play/stop called playVideo/stopVideo directly and, on
  a webview-gone D-Bus error (NoReply — the webview crashed mid-call),
  logged ERROR and gave up: a Sentry event for a self-healing
  condition, and the video stayed dead until the next rotation
  respawned the webview (Sentry ANTHIAS-1A)
- Route both through the same _send_to_webview wrapper the image/page
  paths use (#3012): reap + respawn + retry once on webview death,
  injected via set_send_to_webview() in setup() alongside the bus
- Genuinely unexpected errors still propagate to play/stop's existing
  log+clear-state handling; with no wrapper injected (tests/standalone)
  calls run directly, preserving prior behaviour
- Add tests for the routing, respawn-recovery, re-raise, and the
  no-injection fallback

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants