fix: wrap GTK widget calls in GLib.idle_add() to prevent SIGSEGV on startup by mattmattox · Pull Request #567 · StreamController/StreamController · GitHub
Skip to content

fix: wrap GTK widget calls in GLib.idle_add() to prevent SIGSEGV on startup#567

Merged
Core447 merged 3 commits into
StreamController:mainfrom
mattmattox:fix/gtk-threading-sigsegv
Mar 5, 2026
Merged

fix: wrap GTK widget calls in GLib.idle_add() to prevent SIGSEGV on startup#567
Core447 merged 3 commits into
StreamController:mainfrom
mattmattox:fix/gtk-threading-sigsegv

Conversation

@mattmattox

Copy link
Copy Markdown
Contributor

Summary

Fixes #566 — SIGSEGV ~7 seconds after startup caused by GTK threading violations in MediaPlayerThread.

MediaPlayerThread runs at 30fps and was calling GTK widget methods directly from a background thread. GTK is not thread-safe; this causes C-level memory corruption that Python exception handling cannot catch, resulting in SIGSEGV.

Three calls were missing GLib.idle_add() guards:

File Method Change
src/backend/DeckManagement/DeckController.py ControllerKey.set_ui_key_image() Wrapped buttons[x][y].set_image(image)
src/backend/DeckManagement/DeckController.py ControllerTouchScreen.set_ui_image() Wrapped screenbar.image.set_image(image)
src/windows/mainWindow/DeckPlus/ScreenBar.py ScreenBarImage.set_image() Wrapped sidebar icon_selector.set_image(dial_image)

Note: ScreenBarImage.set_image() already correctly used GLib.idle_add() for the pixbuf update (line 269). Only the sidebar icon selector call at line 284 was missing the guard.

Test plan

  • Launch with a fake deck ("dev": {"n-fake-decks": 1} in settings.json)
  • Assign an animated or video action to a key to trigger MediaPlayerThread image updates
  • Let the app run for 30+ seconds — no SIGSEGV
  • Verify key images still render correctly in the UI
  • On a Stream Deck+, verify the touchscreen image still updates

MediaPlayerThread is a 30fps background thread that was calling GTK
widget methods directly, violating GTK's single-thread requirement.
C-level memory corruption from the race caused a SIGSEGV ~7 seconds
after startup (after the main window maps and image updates begin).

Wrap the three offending calls in GLib.idle_add() to schedule them
on the main loop thread:
- ControllerKey.set_ui_key_image() in DeckController.py
- ControllerTouchScreen.set_ui_image() in DeckController.py
- ScreenBarImage.set_image() in ScreenBar.py (sidebar icon selector)

Fixes StreamController#566
Three more GTK threading violations found after further testing:
- LabelManager.set_action_label() called update_label_editor()
  unconditionally even when reached from plugin rpyc threads
- LayoutManager.update() called update_layout_editor() directly
- BackgroundManager.update() called update_background_editor() directly

All three functions call GTK widget methods (label_editor,
image_editor, background_editor load_for_identifier()) that must
run on the main loop thread.

Wrapping with GLib.idle_add() fixes the remaining SIGSEGV triggered
on page-load events after extended runtime.
@mattmattox

Copy link
Copy Markdown
Contributor Author

Update after real-hardware testing:

After the initial fix, the app survived ~46 minutes before crashing again (vs the original ~7s). The second crash was triggered by a page-load event (likely sleep/resume). A deeper audit found 3 more violations in the same file:

Class Method Line Unsafe Call
LabelManager set_action_label() 1210 update_label_editor() unconditionally — reachable from plugin rpyc background threads
LayoutManager update() 1474 update_layout_editor()image_editor.load_for_identifier()
BackgroundManager update() 1551 update_background_editor()background_editor.load_for_identifier()

All three are now wrapped with GLib.idle_add() in the second commit (7a408ea). Total fix is 6 lines changed across 2 files.

Page.initialize_actions() is called from a background thread during
load_page(). It called load_initial_generative_ui() synchronously,
which invoked ComboRow.set_selected() and other GTK widget methods
from that background thread — corrupting GtkListItemManager state
and causing an abort with:
  Gtk:ERROR:gtklistitemmanager.c:1039:gtk_list_tile_split:
  assertion failed: (n_items < tile->n_items)

Route the actual widget initialization through GLib.idle_add() so
it runs on the GTK main loop thread. The public method schedules the
work; a private helper performs it.
@mattmattox

Copy link
Copy Markdown
Contributor Author

Third round of testing found another violation (commit 3297515):

Page.initialize_actions() is called from a background thread during load_page(). It was calling action.load_initial_generative_ui() synchronously, which walks all GenerativeUI objects and calls load_initial_ui() on each — including ComboRow.set_selected() and other Adw.ComboRow/GTK widget methods. This corrupted the internal GtkListItemManager state and caused:

Gtk:ERROR:../gtk/gtklistitemmanager.c:1039:gtk_list_tile_split:
  assertion failed: (n_items < tile->n_items)
Aborted (core dumped)

Fix: ActionCore.load_initial_generative_ui() now schedules the actual widget initialization via GLib.idle_add() through a private _do_load_initial_generative_ui() helper, ensuring all GenerativeUI widget operations run on the GTK main loop thread.

Full list of fixes so far (7 total):

Commit File Location Violation
76fc3dd DeckController.py set_ui_key_image() KeyButton.set_image() from MediaPlayerThread
76fc3dd DeckController.py set_ui_image() ScreenBarImage.set_image() from MediaPlayerThread
76fc3dd ScreenBar.py set_image() IconSelector.set_image() from MediaPlayerThread
7a408ea DeckController.py LabelManager.set_action_label() update_label_editor() unconditional GTK call
7a408ea DeckController.py LayoutManager.update() update_layout_editor() from background threads
7a408ea DeckController.py BackgroundManager.update() update_background_editor() from background threads
3297515 ActionCore.py load_initial_generative_ui() All GenerativeUI widget init from background threads

@Core447

Core447 commented Mar 5, 2026

Copy link
Copy Markdown
Member

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

fix: SIGSEGV ~7s after startup due to GTK threading violation in MediaPlayerThread

2 participants