Fix AT-SPI queue flush deadlock

This commit is contained in:
Storm Dragon
2026-07-30 21:30:02 -04:00
parent c39f3bda47
commit ddea36c211
2 changed files with 119 additions and 28 deletions
@@ -186,6 +186,64 @@ class EventManagerCompositorContextRegressionTests(unittest.TestCase):
self.assertEqual(list(self.manager._eventQueue.queue), [currentEvent])
def test_flush_classifies_outside_queue_lock_and_preserves_nested_event(self) -> None:
staleEvent = FakeEvent("object:children-changed:add", source="stale")
currentEvent = FakeEvent("object:children-changed:add", source="current")
nestedEvent = FakeEvent("object:state-changed:focused", source="nested", detail1=1)
self.manager._eventQueue.put(staleEvent)
self.manager._eventQueue.put(currentEvent)
self.manager._ignore = mock.Mock(return_value=False)
self.manager._prioritizeSelfHostedFocusedEvent = mock.Mock(return_value=False)
self.manager._queuePrintln = mock.Mock()
self.manager._inFlood = mock.Mock(return_value=False)
self.manager._shouldSuspendEventsFor = mock.Mock(return_value=False)
self.manager._dequeue = mock.Mock(return_value=False)
script = types.SimpleNamespace(eventCache={})
def classify_event(event) -> bool:
self.assertFalse(self.manager._gidleLock.locked())
if event is staleEvent:
self.manager._enqueue(nestedEvent)
return True
return False
self.manager._event_is_from_stale_context = mock.Mock(side_effect=classify_event)
self.manager._should_preserve_during_suppression = mock.Mock(return_value=False)
with (
mock.patch.object(event_manager.AXObject, "get_application", return_value=object()),
mock.patch.object(event_manager.GLib, "idle_add", return_value=1),
mock.patch.object(
event_manager.cthulhu.cthulhuApp.scriptManager,
"get_script",
return_value=script,
),
mock.patch.object(
event_manager.AXUtilities,
"get_application_toolkit_name",
return_value="VCL",
),
):
self.manager._flush_stale_atspi_events()
self.assertEqual(list(self.manager._eventQueue.queue), [currentEvent, nestedEvent])
self.manager._dequeue.assert_not_called()
def test_flush_restores_retained_and_unexamined_events_after_exception(self) -> None:
firstEvent = FakeEvent("object:children-changed:add", source="first")
secondEvent = FakeEvent("object:children-changed:add", source="second")
self.manager._eventQueue.put(firstEvent)
self.manager._eventQueue.put(secondEvent)
self.manager._event_is_from_stale_context = mock.Mock(
side_effect=RuntimeError("property lookup failed"),
)
with self.assertRaisesRegex(RuntimeError, "property lookup failed"):
self.manager._flush_stale_atspi_events()
self.assertEqual(list(self.manager._eventQueue.queue), [firstEvent, secondEvent])
self.assertFalse(self.manager._flushingStaleAtspiEvents)
def test_stale_background_event_does_not_activate_script_during_suppression(self) -> None:
script = mock.Mock()
script.isActivatableEvent.return_value = True