vnc: qemu ext keys

This commit is contained in:
Devaev Maxim
2020-10-08 15:26:37 +03:00
parent f1910f7c8e
commit a0b920a9d6
7 changed files with 114 additions and 28 deletions

View File

@@ -134,6 +134,9 @@ class RfbClient(RfbClientStream): # pylint: disable=too-many-instance-attribute
async def _on_key_event(self, code: int, state: bool) -> None: async def _on_key_event(self, code: int, state: bool) -> None:
raise NotImplementedError raise NotImplementedError
async def _on_ext_key_event(self, code: int, state: bool) -> None:
raise NotImplementedError
async def _on_pointer_event(self, buttons: Dict[str, bool], wheel: Dict[str, int], move: Dict[str, int]) -> None: async def _on_pointer_event(self, buttons: Dict[str, bool], wheel: Dict[str, int], move: Dict[str, int]) -> None:
raise NotImplementedError raise NotImplementedError
@@ -360,6 +363,7 @@ class RfbClient(RfbClientStream): # pylint: disable=too-many-instance-attribute
4: self.__handle_key_event, 4: self.__handle_key_event,
5: self.__handle_pointer_event, 5: self.__handle_pointer_event,
6: self.__handle_client_cut_text, 6: self.__handle_client_cut_text,
255: self.__handle_qemu_event,
} }
while True: while True:
msg_type = await self._read_number("B") msg_type = await self._read_number("B")
@@ -380,9 +384,12 @@ class RfbClient(RfbClientStream): # pylint: disable=too-many-instance-attribute
if encodings_count > 1024: if encodings_count > 1024:
raise RfbError(f"Too many encodings: {encodings_count}") raise RfbError(f"Too many encodings: {encodings_count}")
self._encodings = RfbClientEncodings(frozenset(await self._read_struct("l" * encodings_count))) self._encodings = RfbClientEncodings(frozenset(await self._read_struct("l" * encodings_count)))
get_logger(0).info("[main] %s: Features: resize=%d; rename=%d; leds=%d", get_logger(0).info("[main] %s: Features: resize=%d, rename=%d, leds=%d, extkeys=%d",
self._remote, self._encodings.has_resize, self._encodings.has_rename, self._encodings.has_leds_state) self._remote, self._encodings.has_resize, self._encodings.has_rename,
self._encodings.has_leds_state, self._encodings.has_ext_keys)
self.__check_tight_jpeg() self.__check_tight_jpeg()
if self._encodings.has_ext_keys: # Preferred method
await self._write_fb_update(0, 0, RfbEncodings.EXT_KEYS, drain=True)
await self._on_set_encodings() await self._on_set_encodings()
async def __handle_fb_update_request(self) -> None: async def __handle_fb_update_request(self) -> None:
@@ -417,6 +424,17 @@ class RfbClient(RfbClientStream): # pylint: disable=too-many-instance-attribute
text = await self._read_text(length) text = await self._read_text(length)
await self._on_cut_event(text) await self._on_cut_event(text)
async def __handle_qemu_event(self) -> None:
(sub_type, state, code) = await self._read_struct("B H xxxx L")
if sub_type != 0:
raise RfbError(f"Invalid QEMU sub-message type: {sub_type}")
if code == 0xB7:
# For backwards compatibility servers SHOULD accept 0xB7 as a synonym for 0x54 (PrintScreen)
code = 0x54
if code & 0x80:
code = (0xE0 << 8) | (code & ~0x80)
await self._on_ext_key_event(code, bool(state))
def __check_tight_jpeg(self) -> None: def __check_tight_jpeg(self) -> None:
# JpegCompression may only be used when the client has advertized # JpegCompression may only be used when the client has advertized
# a quality level using the JPEG Quality Level Pseudo-encoding # a quality level using the JPEG Quality Level Pseudo-encoding

View File

@@ -30,7 +30,8 @@ from typing import Any
class RfbEncodings: class RfbEncodings:
RESIZE = -223 # DesktopSize Pseudo-encoding RESIZE = -223 # DesktopSize Pseudo-encoding
RENAME = -307 # DesktopName Pseudo-encoding RENAME = -307 # DesktopName Pseudo-encoding
LEDS_STATE = -261 # LED State Pseudo-encoding LEDS_STATE = -261 # QEMU LED State Pseudo-encoding
EXT_KEYS = -258 # QEMU Extended Key Events Pseudo-encoding
TIGHT = 7 TIGHT = 7
TIGHT_JPEG_QUALITIES = dict(zip( # JPEG Quality Level Pseudo-encoding TIGHT_JPEG_QUALITIES = dict(zip( # JPEG Quality Level Pseudo-encoding
@@ -46,6 +47,7 @@ class RfbClientEncodings:
has_resize: bool = dataclasses.field(default=False) has_resize: bool = dataclasses.field(default=False)
has_rename: bool = dataclasses.field(default=False) has_rename: bool = dataclasses.field(default=False)
has_leds_state: bool = dataclasses.field(default=False) has_leds_state: bool = dataclasses.field(default=False)
has_ext_keys: bool = dataclasses.field(default=False)
has_tight: bool = dataclasses.field(default=False) has_tight: bool = dataclasses.field(default=False)
tight_jpeg_quality: int = dataclasses.field(default=0) tight_jpeg_quality: int = dataclasses.field(default=0)
@@ -54,6 +56,7 @@ class RfbClientEncodings:
self.__set("has_resize", (RfbEncodings.RESIZE in self.encodings)) self.__set("has_resize", (RfbEncodings.RESIZE in self.encodings))
self.__set("has_rename", (RfbEncodings.RENAME in self.encodings)) self.__set("has_rename", (RfbEncodings.RENAME in self.encodings))
self.__set("has_leds_state", (RfbEncodings.LEDS_STATE in self.encodings)) self.__set("has_leds_state", (RfbEncodings.LEDS_STATE in self.encodings))
self.__set("has_ext_keys", (RfbEncodings.EXT_KEYS in self.encodings))
self.__set("has_tight", (RfbEncodings.TIGHT in self.encodings)) self.__set("has_tight", (RfbEncodings.TIGHT in self.encodings))
self.__set("tight_jpeg_quality", self.__get_tight_jpeg_quality()) self.__set("tight_jpeg_quality", self.__get_tight_jpeg_quality())

View File

@@ -27,14 +27,18 @@ import dataclasses
import contextlib import contextlib
from typing import Dict from typing import Dict
from typing import Union
from typing import Optional from typing import Optional
import aiohttp import aiohttp
from ...logging import get_logger from ...logging import get_logger
from ...keyboard.keysym import switch_symmap_modifiers from ...keyboard.keysym import SymmapModifiers
from ...keyboard.keysym import build_symmap from ...keyboard.keysym import build_symmap
from ...keyboard.mappings import WebModifiers
from ...keyboard.mappings import X11Modifiers
from ...keyboard.mappings import AT1_TO_WEB
from ...clients.kvmd import KvmdClientWs from ...clients.kvmd import KvmdClientWs
from ...clients.kvmd import KvmdClientSession from ...clients.kvmd import KvmdClientSession
@@ -240,7 +244,7 @@ class _Client(RfbClient): # pylint: disable=too-many-instance-attributes
# ===== # =====
async def _on_key_event(self, code: int, state: bool) -> None: async def _on_key_event(self, code: int, state: bool) -> None:
(is_modifier, self.__modifiers) = switch_symmap_modifiers(self.__modifiers, code, state) is_modifier = self.__switch_modifiers(code, state)
if self.__kvmd_ws: if self.__kvmd_ws:
web_keys = self.__symmap.get(code) web_keys = self.__symmap.get(code)
if web_keys: if web_keys:
@@ -253,6 +257,29 @@ class _Client(RfbClient): # pylint: disable=too-many-instance-attributes
if web_key is not None: if web_key is not None:
await self.__kvmd_ws.send_key_event(web_key, state) await self.__kvmd_ws.send_key_event(web_key, state)
async def _on_ext_key_event(self, code: int, state: bool) -> None:
web_key = AT1_TO_WEB.get(code)
if web_key is not None:
self.__switch_modifiers(web_key, state) # Предполагаем, что модификаторы всегда известны
if self.__kvmd_ws:
await self.__kvmd_ws.send_key_event(web_key, state)
def __switch_modifiers(self, key: Union[int, str], state: bool) -> bool:
mod = 0
if key in X11Modifiers.SHIFTS or key in WebModifiers.SHIFTS:
mod = SymmapModifiers.SHIFT
elif key == X11Modifiers.ALTGR or key == WebModifiers.ALT_RIGHT:
mod = SymmapModifiers.ALTGR
elif key in X11Modifiers.CTRLS or key in WebModifiers.CTRLS:
mod = SymmapModifiers.CTRL
if mod == 0:
return False
if state:
self.__modifiers |= mod
else:
self.__modifiers &= ~mod
return True
async def _on_pointer_event(self, buttons: Dict[str, bool], wheel: Dict[str, int], move: Dict[str, int]) -> None: async def _on_pointer_event(self, buttons: Dict[str, bool], wheel: Dict[str, int], move: Dict[str, int]) -> None:
if self.__kvmd_ws: if self.__kvmd_ws:
for (button, state) in buttons.items(): for (button, state) in buttons.items():

View File

@@ -23,7 +23,6 @@
import pkgutil import pkgutil
import functools import functools
from typing import Tuple
from typing import List from typing import List
from typing import Dict from typing import Dict
@@ -32,6 +31,7 @@ import Xlib.keysymdef
from ..logging import get_logger from ..logging import get_logger
from .mappings import At1Key from .mappings import At1Key
from .mappings import WebModifiers
from .mappings import X11_TO_AT1 from .mappings import X11_TO_AT1
from .mappings import AT1_TO_WEB from .mappings import AT1_TO_WEB
@@ -43,23 +43,6 @@ class SymmapModifiers:
CTRL: int = 0x4 CTRL: int = 0x4
def switch_symmap_modifiers(modifiers: int, code: int, state: bool) -> Tuple[bool, int]:
mod = 0
if code == 65505 or code == 65506: # XK_Shift_L, XK_Shift_R
mod = SymmapModifiers.SHIFT
elif code == 65027: # AltGR aka XK_ISO_Level3_Shift
mod = SymmapModifiers.ALTGR
elif code == 65507 or code == 65508: # XK_Control_L, XK_Control_R
mod = SymmapModifiers.CTRL
if mod == 0:
return (False, modifiers)
if state:
modifiers |= mod
else:
modifiers &= ~mod
return (True, modifiers)
def build_symmap(path: str) -> Dict[int, Dict[int, str]]: def build_symmap(path: str) -> Dict[int, Dict[int, str]]:
# https://github.com/qemu/qemu/blob/95a9457fd44ad97c518858a4e1586a5498f9773c/ui/keymaps.c # https://github.com/qemu/qemu/blob/95a9457fd44ad97c518858a4e1586a5498f9773c/ui/keymaps.c
logger = get_logger() logger = get_logger()
@@ -74,9 +57,9 @@ def build_symmap(path: str) -> Dict[int, Dict[int, str]]:
web_name = AT1_TO_WEB.get(key.code) web_name = AT1_TO_WEB.get(key.code)
if web_name is not None: if web_name is not None:
if ( if (
(web_name in ["ShiftLeft", "ShiftRight"] and key.shift) # pylint: disable=too-many-boolean-expressions (web_name in WebModifiers.SHIFTS and key.shift) # pylint: disable=too-many-boolean-expressions
or (web_name in ["AltLeft", "AltRight"] and key.altgr) or (web_name in WebModifiers.ALTS and key.altgr)
or (web_name in ["ControlLeft", "ControlRight"] and key.ctrl) or (web_name in WebModifiers.CTRLS and key.ctrl)
): ):
logger.error("Invalid modifier key at mapping %s: %s / %s", src, web_name, key) logger.error("Invalid modifier key at mapping %s: %s / %s", src, web_name, key)
continue continue

View File

@@ -153,6 +153,33 @@ KEYMAP: Dict[str, Key] = {
} }
# =====
class WebModifiers:
SHIFT_LEFT = "ShiftLeft"
SHIFT_RIGHT = "ShiftRight"
SHIFTS = set([SHIFT_LEFT, SHIFT_RIGHT])
ALT_LEFT = "AltLeft"
ALT_RIGHT = "AltRight"
ALTS = set([ALT_LEFT, ALT_RIGHT])
CTRL_LEFT = "ControlLeft"
CTRL_RIGHT = "ControlRight"
CTRLS = set([CTRL_RIGHT, CTRL_RIGHT])
class X11Modifiers:
SHIFT_LEFT = 65505
SHIFT_RIGHT = 65506
SHIFTS = set([SHIFT_LEFT, SHIFT_RIGHT])
ALTGR = 65027 # XK_ISO_Level3_Shift
CTRL_LEFT = 65507
CTRL_RIGHT = 65508
CTRLS = set([CTRL_LEFT, CTRL_RIGHT])
# ===== # =====
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class At1Key: class At1Key:

View File

@@ -50,6 +50,33 @@ KEYMAP: Dict[str, Key] = {
} }
# =====
class WebModifiers:
SHIFT_LEFT = "ShiftLeft"
SHIFT_RIGHT = "ShiftRight"
SHIFTS = set([SHIFT_LEFT, SHIFT_RIGHT])
ALT_LEFT = "AltLeft"
ALT_RIGHT = "AltRight"
ALTS = set([ALT_LEFT, ALT_RIGHT])
CTRL_LEFT = "ControlLeft"
CTRL_RIGHT = "ControlRight"
CTRLS = set([CTRL_RIGHT, CTRL_RIGHT])
class X11Modifiers:
SHIFT_LEFT = 65505
SHIFT_RIGHT = 65506
SHIFTS = set([SHIFT_LEFT, SHIFT_RIGHT])
ALTGR = 65027 # XK_ISO_Level3_Shift
CTRL_LEFT = 65507
CTRL_RIGHT = 65508
CTRLS = set([CTRL_LEFT, CTRL_RIGHT])
# ===== # =====
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class At1Key: class At1Key:

View File

@@ -25,16 +25,17 @@ from typing import Dict
from typing import Generator from typing import Generator
from .keysym import SymmapModifiers from .keysym import SymmapModifiers
from .mappings import WebModifiers
# ===== # =====
def text_to_web_keys( def text_to_web_keys(
text: str, text: str,
symmap: Dict[int, Dict[int, str]], symmap: Dict[int, Dict[int, str]],
shift_key: str="ShiftLeft", shift_key: str=WebModifiers.SHIFT_LEFT,
) -> Generator[Tuple[str, bool], None, None]: ) -> Generator[Tuple[str, bool], None, None]:
assert shift_key in ["ShiftLeft", "ShiftRight"] assert shift_key in WebModifiers.SHIFTS
shifted = False shifted = False
for ch in text: for ch in text: