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:
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user