Fix X11 display lifecycle leaks in text injection
This commit is contained in:
parent
5f756ea04e
commit
48d7460f57
2 changed files with 94 additions and 12 deletions
|
|
@ -258,23 +258,35 @@ class X11Adapter:
|
||||||
|
|
||||||
def _paste_clipboard(self) -> None:
|
def _paste_clipboard(self) -> None:
|
||||||
dpy = display.Display()
|
dpy = display.Display()
|
||||||
self._send_combo(dpy, ["Control_L", "Shift_L", "v"])
|
try:
|
||||||
|
self._send_combo(dpy, ["Control_L", "Shift_L", "v"])
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
dpy.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _type_text(self, text: str) -> None:
|
def _type_text(self, text: str) -> None:
|
||||||
if not text:
|
if not text:
|
||||||
return
|
return
|
||||||
dpy = display.Display()
|
dpy = display.Display()
|
||||||
for ch in text:
|
try:
|
||||||
if ch == "\n":
|
for ch in text:
|
||||||
self._send_combo(dpy, ["Return"])
|
if ch == "\n":
|
||||||
continue
|
self._send_combo(dpy, ["Return"])
|
||||||
keysym, needs_shift = self._keysym_for_char(ch)
|
continue
|
||||||
if keysym is None:
|
keysym, needs_shift = self._keysym_for_char(ch)
|
||||||
continue
|
if keysym is None:
|
||||||
if needs_shift:
|
continue
|
||||||
self._send_combo(dpy, ["Shift_L", keysym], already_keysym=True)
|
if needs_shift:
|
||||||
else:
|
self._send_combo(dpy, ["Shift_L", keysym], already_keysym=True)
|
||||||
self._send_combo(dpy, [keysym], already_keysym=True)
|
else:
|
||||||
|
self._send_combo(dpy, [keysym], already_keysym=True)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
dpy.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _send_combo(self, dpy: display.Display, keys: Iterable[str | int], already_keysym: bool = False) -> None:
|
def _send_combo(self, dpy: display.Display, keys: Iterable[str | int], already_keysym: bool = False) -> None:
|
||||||
keycodes: list[int] = []
|
keycodes: list[int] = []
|
||||||
|
|
|
||||||
70
tests/test_desktop_x11.py
Normal file
70
tests/test_desktop_x11.py
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SRC = ROOT / "src"
|
||||||
|
if str(SRC) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SRC))
|
||||||
|
|
||||||
|
from desktop_x11 import X11Adapter
|
||||||
|
|
||||||
|
|
||||||
|
class DesktopX11Tests(unittest.TestCase):
|
||||||
|
def _adapter(self) -> X11Adapter:
|
||||||
|
return object.__new__(X11Adapter)
|
||||||
|
|
||||||
|
@patch("desktop_x11.display.Display")
|
||||||
|
def test_paste_clipboard_closes_display_on_success(self, display_cls):
|
||||||
|
adapter = self._adapter()
|
||||||
|
display_obj = MagicMock()
|
||||||
|
display_cls.return_value = display_obj
|
||||||
|
adapter._send_combo = MagicMock()
|
||||||
|
|
||||||
|
adapter._paste_clipboard()
|
||||||
|
|
||||||
|
adapter._send_combo.assert_called_once_with(display_obj, ["Control_L", "Shift_L", "v"])
|
||||||
|
display_obj.close.assert_called_once_with()
|
||||||
|
|
||||||
|
@patch("desktop_x11.display.Display")
|
||||||
|
def test_paste_clipboard_closes_display_on_send_error(self, display_cls):
|
||||||
|
adapter = self._adapter()
|
||||||
|
display_obj = MagicMock()
|
||||||
|
display_cls.return_value = display_obj
|
||||||
|
adapter._send_combo = MagicMock(side_effect=RuntimeError("boom"))
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "boom"):
|
||||||
|
adapter._paste_clipboard()
|
||||||
|
|
||||||
|
display_obj.close.assert_called_once_with()
|
||||||
|
|
||||||
|
@patch("desktop_x11.display.Display")
|
||||||
|
def test_type_text_closes_display_on_success(self, display_cls):
|
||||||
|
adapter = self._adapter()
|
||||||
|
display_obj = MagicMock()
|
||||||
|
display_cls.return_value = display_obj
|
||||||
|
adapter._send_combo = MagicMock()
|
||||||
|
adapter._keysym_for_char = MagicMock(return_value=(42, False))
|
||||||
|
|
||||||
|
adapter._type_text("a")
|
||||||
|
|
||||||
|
adapter._send_combo.assert_called_once_with(display_obj, [42], already_keysym=True)
|
||||||
|
display_obj.close.assert_called_once_with()
|
||||||
|
|
||||||
|
@patch("desktop_x11.display.Display")
|
||||||
|
def test_type_text_closes_display_on_send_error(self, display_cls):
|
||||||
|
adapter = self._adapter()
|
||||||
|
display_obj = MagicMock()
|
||||||
|
display_cls.return_value = display_obj
|
||||||
|
adapter._send_combo = MagicMock(side_effect=RuntimeError("boom"))
|
||||||
|
adapter._keysym_for_char = MagicMock(return_value=(42, False))
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "boom"):
|
||||||
|
adapter._type_text("a")
|
||||||
|
|
||||||
|
display_obj.close.assert_called_once_with()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue