Skip to content

Fix StreamController crashes: None value sorting, GTK thread safety, null guards, and redundant populate() calls#12

Merged
gensyn merged 7 commits intomainfrom
copilot/fix-stream-controller-crash
Mar 4, 2026
Merged

Fix StreamController crashes: None value sorting, GTK thread safety, null guards, and redundant populate() calls#12
gensyn merged 7 commits intomainfrom
copilot/fix-stream-controller-crash

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 2, 2026

Fixes multiple causes of StreamController crashes when using the Home Assistant Plugin.

Changes

None value sorting fixes

_load_domains and _load_entities in BaseCore would crash with TypeError: '<' not supported between instances of 'NoneType' and 'str' when the backend returned None entries in the domains list, or when no entity was configured (causing None to be appended before sorting).

  • _load_domains: Filter None values from the domains list before sorting.
  • _load_entities: Guard the append with entity is not None, and filter None values from the entities list before sorting.
  • _load_actions (perform_action.py): Guard the append with action is not None and filter None values, consistent with the above fixes.
  • _load_attributes (text_action.py): Guard the append with attribute is not None, consistent with the above fixes.

GTK thread safety

The WebSocket runs in a background daemon thread. Its callbacks (_on_connect, _on_disconnect, _on_event_message) directly invoked action methods that manipulate GTK widgets (e.g. populate(), set_sensitive(), set_media()), violating GTK's requirement that all UI operations run on the main thread. This was the primary cause of non-deterministic GTK crashes.

  • home_assistant_backend.py: Added from gi.repository import GLib and replaced all direct callback invocations from the WebSocket background thread with GLib.idle_add().

Null guard in _on_event_message

When HA sends an event for an entity that has been removed (or during a race condition on reload), entity_settings could be None, causing an AttributeError. Additionally, self._entities[domain] could raise KeyError if the domain was no longer present.

  • home_assistant_backend.py: Changed self._entities[domain] subscript access to .get(domain, {}) and added an early return when entity_settings is None.

Fix gtk_list_tile_split fatal GTK4 assertion

_load_domains() and _load_entities() unconditionally called populate() on ComboRow (backed by GtkDropDown) on every invocation. Since on_ready() is registered as a backend reconnect callback and fired via GLib.idle_add() for every action on every reconnect, populate() could be called while the user had a dropdown popup open. GTK4's GtkListItemManager fires a fatal assertion (gtk_list_tile_split: assertion failed: (n_items < tile->n_items)) and aborts the process when its backing model is modified while the dropdown is visible.

  • base_core.py: Added _get_current_domains() and _get_current_entities() helpers (following the same pattern as ShowText._get_current_attributes()), and equality guards in _load_domains() and _load_entities() to skip populate() when the list has not changed.

Other

  • .gitignore: Added __pycache__/ and *.pyc entries.

Tests

  • Added test_load_domains_with_none_in_domains and test_load_entities_with_none_entity for the None-in-list sorting fix.
  • Updated backend tests for _on_connect, _on_disconnect, and _on_event_message to patch GLib.idle_add with side_effect=lambda f, *args: f(*args) so existing callback assertions remain valid.
  • Added test_on_event_message_unknown_entity_no_new_state and test_on_event_message_unknown_entity_with_new_state for the null entity_settings guard.
  • Added test_load_actions_none_action and test_load_attributes_none_attribute for the new None guards.
  • Updated test_load_domains_* and test_load_entities_* to configure get_item_amount on the mock so the equality guard is exercised; added test_load_domains_no_update_needed and test_load_entities_no_update_needed to verify populate() is not called when the list is already correct.
Original prompt

This section details on the original issue you should resolve

<issue_title>Stream Controller crashing with Home Assistant Plugin</issue_title>
<issue_description>Hi,
I just installed StreamController with your Home Assistant Plugin. Some times it works, but after 3-5 interactions StreamController crashes. It happens in different situations. As I thought it is a StreamController issue I contacted them, who told me it may be a problem with his plugin. And there is a problem with a TypeError.

Would you like to have a look at it?

Here is part of the SC.log:

  File "/home/wgleich/.var/app/com.core447.StreamController/data/plugins/HomeAssistantPlugin/actions/cores/base_core/base_core.py", line 140, in _load_domains
    domains.sort()
    │       └ <method 'sort' of 'list' objects>
    └ ['alarm_control_panel', 'automation', 'backup', 'button', 'camera', 'climate', 'cloud', 'conversation', 'counter', 'cover', '...

TypeError: '<' not supported between instances of 'NoneType' and 'str'
2026-02-09 19:06:59.981 | INFO     | src.backend.DeckManagement.DeckController:load_page:752 - Loaded page Gaming on deck DL41L2A45922
2026-02-09 19:07:00.039 | INFO     | src.backend.DeckManagement.DeckController:load_background:571 - Loading background in thread: 139932710463168
2026-02-09 19:07:00.135 | ERROR    | src.backend.DeckManagement.DeckController:run:78 - An error has been caught in function 'run', process 'MainProcess' (3), thread 'MediaPlayerThread' (139932710463168):
Traceback (most recent call last):

  File "/usr/lib/python3.12/threading.py", line 1032, in _bootstrap
    self._bootstrap_inner()
    │    └ <function Thread._bootstrap_inner at 0x7f450eca6700>
    └ <MediaPlayerThread(MediaPlayerThread, started daemon 139932710463168)>
  File "/usr/lib/python3.12/threading.py", line 1075, in _bootstrap_inner
    self.run()
    │    └ <function MediaPlayerThread.run at 0x7f44ee1d7060>
    └ <MediaPlayerThread(MediaPlayerThread, started daemon 139932710463168)>

  File "/app/bin/StreamController/src/backend/DeckManagement/DeckController.py", line 191, in run
    self.perform_media_player_tasks()
    │    └ <function MediaPlayerThread.perform_media_player_tasks at 0x7f44ee1d7740>
    └ <MediaPlayerThread(MediaPlayerThread, started daemon 139932710463168)>

  File "/app/bin/StreamController/src/backend/DeckManagement/DeckController.py", line 276, in perform_media_player_tasks
    task.run()
    │    └ <function MediaPlayerTask.run at 0x7f44ee119c60>
    └ MediaPlayerTask(deck_controller=<src.backend.DeckManagement.DeckController.DeckController object at 0x7f44dc0c06e0>, page=<sr...

> File "/app/bin/StreamController/src/backend/DeckManagement/DeckController.py", line 78, in run
    self._callable(*self.args, **self.kwargs)
    │    │          │    │       │    └ {'update': False}
    │    │          │    │       └ MediaPlayerTask(deck_controller=<src.backend.DeckManagement.DeckController.DeckController object at 0x7f44dc0c06e0>, page=<sr...
    │    │          │    └ (<src.backend.PageManagement.Page.Page object at 0x7f44dc09a8d0>,)
    │    │          └ MediaPlayerTask(deck_controller=<src.backend.DeckManagement.DeckController.DeckController object at 0x7f44dc0c06e0>, page=<sr...
    │    └ <bound method DeckController.load_all_inputs of <src.backend.DeckManagement.DeckController.DeckController object at 0x7f44dc0...
    └ MediaPlayerTask(deck_controller=<src.backend.DeckManagement.DeckController.DeckController object at 0x7f44dc0c06e0>, page=<sr...

  File "/app/bin/StreamController/src/backend/DeckManagement/DeckController.py", line 656, in load_all_inputs
    future.result()
    │      └ <function Future.result at 0x7f450e84a5c0>
    └ <Future at 0x7f44c4790320 state=finished raised TypeError>

  File "/usr/lib/python3.12/concurrent/futures/_base.py", line 449, in result
    return self.__get_result()
           └ None
  File "/usr/lib/python3.12/concurrent/futures/_base.py", line 401, in __get_result
    raise self._exception
          └ None
  File "/usr/lib/python3.12/concurrent/futures/thread.py", line 59, in run
    result = self.fn(*self.args, **self.kwargs)
             │        │            └ None
             │        └ None
             └ None

  File "/app/bin/StreamController/src/backend/DeckManagement/DeckController.py", line 666, in load_input
    controller_input.load_from_input_dict(input_dict, update)
    │                │                    │           └ False
    │                │                    └ {'states': {'0': {'labels': {'center': {'text': 'LED', 'outline_width': 1}}, 'actions': [{'id': 'HomeAssistantPlugin::Perform...
    │                └ <function ControllerKey.load_from_input_dict at 0x7f44ee1cee80>
    └ <src.backend.DeckManagement.DeckController.ControllerKey object at 0x7f44dc0d4b30>

  File "/app/bin/StreamController/src/backend/DeckManagement/DeckController.py", line 2189, in load_from_input_dict
    state.own_actions_update() # Why not threaded? Because this w...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes gensyn/HomeAssistantPlugin#5

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

Copilot AI and others added 2 commits March 2, 2026 16:07
Co-authored-by: gensyn <36128035+gensyn@users.noreply.github.com>
…acts

Co-authored-by: gensyn <36128035+gensyn@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix TypeError causing Stream Controller crash with Home Assistant Plugin Fix TypeError when sorting domains/entities lists containing None values Mar 2, 2026
@gensyn
Copy link
Copy Markdown
Owner

gensyn commented Mar 2, 2026

@copilot This ia great but does not fix most of the errors users are having. Most crashes seem to stem from GTK related issues. Have a deeper look for likely causes, please.

…age and list builders

Co-authored-by: gensyn <36128035+gensyn@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 2, 2026

@copilot This ia great but does not fix most of the errors users are having. Most crashes seem to stem from GTK related issues. Have a deeper look for likely causes, please.

Investigated the codebase more deeply. The GTK crashes stem from three causes:

1. Thread safety (primary cause of GTK crashes) — Commit 811956d
The WebSocket runs in a background daemon thread. Its callbacks (_on_connect, _on_disconnect, _on_event_message) were directly calling action methods that manipulate GTK widgets (e.g. populate(), set_sensitive(), set_media()). GTK is not thread-safe and all UI operations must run on the main thread. Fixed by importing GLib in home_assistant_backend.py and wrapping all action callbacks with GLib.idle_add().

2. Null pointer in _on_event_message — Same commit
When HA sends an event for an entity that has been removed (or during a race condition), entity_settings was None, causing an AttributeError. Also changed self._entities[domain] subscript access to .get(domain, {}) to guard against missing domains. Added an early return when entity_settings is None.

3. None guards in _load_actions and _load_attributes — Same commit
Consistent with the previous _load_domains/_load_entities fix: added is not None guards before appending to actions in perform_action.py and attributes in text_action.py.

Copilot AI changed the title Fix TypeError when sorting domains/entities lists containing None values Fix StreamController crashes: None value sorting, GTK thread safety, and null guards Mar 2, 2026
@gensyn
Copy link
Copy Markdown
Owner

gensyn commented Mar 3, 2026

@copilot The current version crashes for me with the following error:

Gtk:ERROR:../../../gtk/gtklistitemmanager.c:1039:gtk_list_tile_split: assertion failed: (n_items < tile->n_items)
Bail out! Gtk:ERROR:../../../gtk/gtklistitemmanager.c:1039:gtk_list_tile_split: assertion failed: (n_items < tile->n_items)
Aborted (core dumped)
/usr/lib/python3.14/multiprocessing/resource_tracker.py:297: UserWarning: resource_tracker: There appear to be 8 leaked semaphore objects to clean up at shutdown: {'/mp-hkpz4v2t', '/mp-h7983k7x', '/mp-331ujf17', '/mp-z1a9zxel', '/mp-tcl4fnnr', '/mp-gpsj_1s7', '/mp-d8yzmm2s', '/mp-y1cvzzjc'}
  warnings.warn(

…hanged

Co-authored-by: gensyn <36128035+gensyn@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 3, 2026

@copilot The current version crashes for me with the following error:

...

The gtk_list_tile_split assertion is a GTK4 fatal error that fires when a GtkDropDown's backing list model is modified while its popup is open. The root cause was that _load_domains() and _load_entities() in BaseCore unconditionally called populate() every time they ran — including on every backend reconnect (triggered via GLib.idle_add for all registered actions). If a user had a combo dropdown open at that moment, populate() modified the model while GTK's GtkListItemManager had tiles allocated for the visible list, causing the assertion.

Fixed in 2b19e81: added _get_current_domains() and _get_current_entities() helpers (same pattern as the existing _get_current_attributes() in ShowText), and equality guards in _load_domains() / _load_entities() to skip populate() when the list hasn't changed.

Copilot AI changed the title Fix StreamController crashes: None value sorting, GTK thread safety, and null guards Fix StreamController crashes: None value sorting, GTK thread safety, null guards, and redundant populate() calls Mar 3, 2026
@gensyn gensyn marked this pull request as ready for review March 4, 2026 09:51
@gensyn gensyn merged commit 5f3c0e7 into main Mar 4, 2026
4 checks passed
@gensyn gensyn deleted the copilot/fix-stream-controller-crash branch March 9, 2026 15:02
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