Skip to content

fix(desktop): throttle devicechange-driven media enumeration - #4171

Open
ludo-trentuna wants to merge 1 commit into
block:mainfrom
ludo-trentuna:fix/desktop-media-fd-leak-throttle
Open

ludo-trentuna wants to merge 1 commit into
block:mainfrom
ludo-trentuna:fix/desktop-media-fd-leak-throttle

Conversation

@ludo-trentuna

Copy link
Copy Markdown

Summary

On Linux, Buzz Desktop crashes on its own a few seconds after launch on machines where the webcam fails to enumerate: GLib-ERROR **: Creating pipes for GWakeup: Too many open files aborts the WebKitWebProcess.

This PR breaks the frontend half of the feedback loop that drives the crash: useAudioDevices listened to devicechange with no throttle, so each failed media probe re-triggered enumerateDevices().

Root cause

  1. HuddleProvider is mounted at the app root (desktop/src/app/AppShell.tsx), so useAudioDevices calls navigator.mediaDevices.enumerateDevices() on mount and registers a devicechange listener (desktop/src/features/huddle/lib/useAudioDevices.ts).
  2. WebKitGTK (media stream enabled at boot by fix(desktop): enable webview media capture on Linux #2771) probes devices through libcamera.
  3. On affected hardware, libcamera cannot start the camera manager — Failed to start camera manager: No such device (reproduced with an integrated UVC camera, 2 interfaces, 4 /dev/video* nodes).
  4. WebKit retries the probe in a tight loop; every cycle spawns a new camera_manager process and leaks file descriptors in the web process.
  5. Each cycle emits devicechange, and the frontend listener called enumerateDevices() again without any throttle — amplifying the loop.
  6. FD exhaustion → EMFILE → GLib abort in WebKitWebProcess → the whole app dies.

Measured on the affected machine (idle app, no user interaction, 40 s):

t+ FDs in WebKitWebProcess
5 s 144
10 s 176
15 s 229
20 s 257
25 s 295
30 s 335
35 s 375
40 s 403

~6.5 leaked FDs/s (linear, never released), plus ~16 camera_manager spawns per second (new PID on every log line). ulimit -n was 524288, so this is a genuine leak, not a low system limit.

Fix

Throttle devicechange-driven refreshes to at most one per 2 s, and back off for 30 s after 3 consecutive failures. This breaks the feedback loop while keeping hotplug responsiveness (a healthy device still gets picked up on the next refresh).

Scope / honesty

The frontend throttle stops the amplification, but the internal WebKitGTK/libcamera probe loop and FD leak remain — on a broken-camera machine the app would still crash, just later. The real fix belongs upstream in WebKitGTK (stop retrying / close FDs when the camera manager fails to start) and should be reported to bugs.webkit.org. This PR is the Buzz-side mitigation.

Duplicate search

Test plan

  • Affected machine: app no longer crashes at boot; camera spam in stderr drops to ≤1 probe per 2 s and stops after repeated failures (30 s backoff).
  • Healthy machine: device list still populates; hotplug still works (devicechange triggers a refresh, throttled).
  • pnpm lint / tsc --noEmit pass.

WebKitGTK's libcamera backend can retry a failing camera manager in a
tight loop, firing devicechange on every cycle. Each cycle spawns a new
libcamera manager and leaks file descriptors in the web process; on
machines where the camera fails to enumerate (libcamera: 'No such
device'), this escalates until EMFILE aborts the app ('Too many open
files', GLib: 'Creating pipes for GWakeup').

useAudioDevices listened to devicechange with no throttle, so the
frontend amplified the loop: enumerate -> probe -> fail -> devicechange
-> enumerate...

Throttle refreshes to at most one per 2s, and back off for 30s after 3
consecutive failures. Breaks the feedback loop while keeping hotplug
responsiveness.

Signed-off-by: Ludo <ludo@trentuna.com>
@jandado

jandado commented Sep 24, 2026

Copy link
Copy Markdown

Hi @ludo-trentuna — I independently hit this crash (v0.5.24, AppImage) and
spent time on the diagnosis; sharing what I found since it affects the fix
shape, and your PR has been quiet for a while and now conflicts with main
(useAudioDevices.ts moved hosts: HuddleProvider now mounts from
AppHuddleShell.tsx).

Two findings from a falsification experiment on the affected machine:

  1. With the distro's libcamera GStreamer plugin hidden (registry rebuilt,
    verified via gst-inspect-1.0 libcamera), your log spam disappears but the
    WebKitWebProcess still leaks FDs: 117 → 634 within minutes of
    event-driven re-enumeration, ending in EMFILE and a dead web process. So
    the leak is per-enumeration-cycle in WebKitGTK itself, independent of
    libcamera — libcamera is just the loud, heavy amplifier that makes the
    loop visible.
  2. That means a throttle reduces the bleed but keeps the loop alive: every
    throttled refresh still leaks FDs, and the enumeration → monitor restart →
    devicechange re-announcement cycle never reaches a terminal state. On a
    broken-camera machine the crash is postponed, not prevented (your own
    "Scope / honesty" section says the same).

I have a fix PR incoming that removes the amplifier instead: enumeration
becomes demand-driven (no mount-time enumeration, no devicechange listener
anywhere that calls enumerateDevices(); the mic picker refreshes on open and
once after getUserMedia succeeds), plus a regression test that fails if
event-driven enumeration returns, and a hardware-validated 2×2 matrix (direct
vs firejail launch × pre-fix vs fixed build). Full diagnosis + validation data
in docs/linux-media-device-enumeration-loop.md in that PR.

No duplication intended — your report and FD measurements (144 → 403 over 40 s,
~6.5 FDs/s) match what I measured, and this comment exists mainly so the
maintainers can compare the two approaches in one place. If maintainers prefer
to build on this PR instead, the throttle constants would just need to become
"a terminal demand-driven model" rather than bounded polling — happy to help
with that rebase either way.

Upstream note for whoever picks this up: the per-enumeration re-announcement +
FD leak is WebKitGTK behavior worth filing at bugs.webkit.org
(GStreamerCaptureDeviceManager starts a fresh GstDeviceMonitor per
enumerateDevices(); its startup re-announces already-known devices because
the gst_bus_set_flushing flush window misses providers that add devices
asynchronously).

@jandado

jandado commented Sep 24, 2026

Copy link
Copy Markdown

Update to the upstream note above: filed as WebKitGTK Bug 325151 — https://bugs.webkit.org/show_bug.cgi?id=325151 (full diagnosis, a runnable repro attachment, and suspected fix directions; happy to test patches on the affected machine).

This branch has not been deployed

No deployments
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