fix(suspend): restart keyboard shortcut backend after resume (#371)

After suspend/resume, evdev devices disconnect and the monitor thread
exits when all file descriptors close. The resume handler only
reinitialized the speech engine, leaving keyboard shortcuts dead.

Restart _setup_keyboard_shortcuts() in _reinit_after_resume() to
re-scan /dev/input and open fresh device file descriptors.
This commit is contained in:
Jatin K Malik
2026-03-29 23:21:55 -07:00
committed by GitHub
parent 4d501f1711
commit 5909def48d
2 changed files with 62 additions and 0 deletions
+9
View File
@@ -476,6 +476,15 @@ class TrayIndicator:
self.speech_engine.reinitialize_after_resume()
except Exception:
logger.error("Failed to reinitialize after resume", exc_info=True)
# evdev devices disconnect on suspend; monitor thread exits when all fds close.
# Full stop+start re-scans /dev/input and opens fresh file descriptors.
try:
logger.info("Restarting keyboard shortcuts after resume")
self._setup_keyboard_shortcuts()
except Exception:
logger.error("Failed to restart keyboard shortcuts after resume", exc_info=True)
return GLib.SOURCE_REMOVE
def _on_quit_clicked(self, widget):
+53
View File
@@ -272,5 +272,58 @@ class TestReinitializeAfterResume(unittest.TestCase):
mgr._update_state.assert_called_once_with(RecognitionState.ERROR)
class TestTrayIndicatorResumeFlow(unittest.TestCase):
"""Tests for TrayIndicator._reinit_after_resume keyboard restart."""
def _make_tray_indicator(self):
from vocalinux.ui.tray_indicator import TrayIndicator
with patch.object(TrayIndicator, "__init__", lambda self: None):
indicator = TrayIndicator.__new__(TrayIndicator)
indicator.speech_engine = MagicMock()
indicator._setup_keyboard_shortcuts = MagicMock()
return indicator
def test_reinit_after_resume_calls_speech_engine_reinit(self):
indicator = self._make_tray_indicator()
indicator._reinit_after_resume()
indicator.speech_engine.reinitialize_after_resume.assert_called_once()
def test_reinit_after_resume_restarts_keyboard_shortcuts(self):
indicator = self._make_tray_indicator()
indicator._reinit_after_resume()
indicator._setup_keyboard_shortcuts.assert_called_once()
def test_keyboard_restart_happens_even_if_speech_reinit_fails(self):
indicator = self._make_tray_indicator()
indicator.speech_engine.reinitialize_after_resume.side_effect = RuntimeError("boom")
indicator._reinit_after_resume()
indicator._setup_keyboard_shortcuts.assert_called_once()
def test_keyboard_restart_failure_does_not_crash(self):
indicator = self._make_tray_indicator()
indicator._setup_keyboard_shortcuts.side_effect = RuntimeError("no devices")
indicator._reinit_after_resume()
indicator.speech_engine.reinitialize_after_resume.assert_called_once()
def test_returns_source_remove(self):
from gi.repository import GLib
indicator = self._make_tray_indicator()
result = indicator._reinit_after_resume()
assert result == GLib.SOURCE_REMOVE
if __name__ == "__main__":
unittest.main()