summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--kvmd/apps/kvmd/api/hid.py37
-rw-r--r--kvmd/apps/kvmd/server.py2
-rw-r--r--kvmd/apps/vnc/server.py9
-rw-r--r--kvmd/clients/kvmd.py12
-rw-r--r--kvmd/keyprint.py103
-rw-r--r--kvmd/plugins/hid/__init__.py10
-rw-r--r--kvmd/plugins/hid/otg/__init__.py10
-rw-r--r--kvmd/plugins/hid/otg/device.py4
-rw-r--r--kvmd/plugins/hid/otg/keyboard.py2
-rw-r--r--kvmd/plugins/hid/otg/mouse.py2
-rw-r--r--kvmd/plugins/hid/serial.py24
-rw-r--r--web/kvm/index.html1
-rw-r--r--web/share/js/kvm/hid.js92
13 files changed, 196 insertions, 112 deletions
diff --git a/kvmd/apps/kvmd/api/hid.py b/kvmd/apps/kvmd/api/hid.py
index 9d18b2d5..5283751a 100644
--- a/kvmd/apps/kvmd/api/hid.py
+++ b/kvmd/apps/kvmd/api/hid.py
@@ -20,6 +20,8 @@
# ========================================================================== #
+import asyncio
+
from typing import Dict
from aiohttp.web import Request
@@ -29,12 +31,15 @@ from aiohttp.web import WebSocketResponse
from ....plugins.hid import BaseHid
from ....validators.basic import valid_bool
+from ....validators.basic import valid_number
from ....validators.kvm import valid_hid_key
from ....validators.kvm import valid_hid_mouse_move
from ....validators.kvm import valid_hid_mouse_button
from ....validators.kvm import valid_hid_mouse_wheel
+from .... import keyprint
+
from ..http import exposed_http
from ..http import exposed_ws
from ..http import make_json_response
@@ -45,6 +50,8 @@ class HidApi:
def __init__(self, hid: BaseHid) -> None:
self.__hid = hid
+ self.__key_lock = asyncio.Lock()
+
# =====
@exposed_http("GET", "/hid")
@@ -56,16 +63,28 @@ class HidApi:
await self.__hid.reset()
return make_json_response()
+ @exposed_http("POST", "/hid/print")
+ async def __print_handler(self, request: Request) -> Response:
+ text = await request.text()
+ limit = int(valid_number(request.query.get("limit", "1024"), min=0, type=int))
+ if limit > 0:
+ text = text[:limit]
+ async with self.__key_lock:
+ for (key, state) in keyprint.text_to_keys(text):
+ self.__hid.send_key_event(key, state)
+ return make_json_response()
+
# =====
@exposed_ws("key")
async def __ws_key_handler(self, _: WebSocketResponse, event: Dict) -> None:
- try:
- key = valid_hid_key(event["key"])
- state = valid_bool(event["state"])
- except Exception:
- return
- await self.__hid.send_key_event(key, state)
+ async with self.__key_lock:
+ try:
+ key = valid_hid_key(event["key"])
+ state = valid_bool(event["state"])
+ except Exception:
+ return
+ self.__hid.send_key_event(key, state)
@exposed_ws("mouse_button")
async def __ws_mouse_button_handler(self, _: WebSocketResponse, event: Dict) -> None:
@@ -74,7 +93,7 @@ class HidApi:
state = valid_bool(event["state"])
except Exception:
return
- await self.__hid.send_mouse_button_event(button, state)
+ self.__hid.send_mouse_button_event(button, state)
@exposed_ws("mouse_move")
async def __ws_mouse_move_handler(self, _: WebSocketResponse, event: Dict) -> None:
@@ -83,7 +102,7 @@ class HidApi:
to_y = valid_hid_mouse_move(event["to"]["y"])
except Exception:
return
- await self.__hid.send_mouse_move_event(to_x, to_y)
+ self.__hid.send_mouse_move_event(to_x, to_y)
@exposed_ws("mouse_wheel")
async def __ws_mouse_wheel_handler(self, _: WebSocketResponse, event: Dict) -> None:
@@ -92,4 +111,4 @@ class HidApi:
delta_y = valid_hid_mouse_wheel(event["delta"]["y"])
except Exception:
return
- await self.__hid.send_mouse_wheel_event(delta_x, delta_y)
+ self.__hid.send_mouse_wheel_event(delta_x, delta_y)
diff --git a/kvmd/apps/kvmd/server.py b/kvmd/apps/kvmd/server.py
index a965757c..3a309651 100644
--- a/kvmd/apps/kvmd/server.py
+++ b/kvmd/apps/kvmd/server.py
@@ -391,7 +391,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
async def __remove_socket(self, ws: aiohttp.web.WebSocketResponse) -> None:
async with self.__sockets_lock:
- await self.__hid.clear_events()
+ self.__hid.clear_events()
try:
self.__sockets.remove(ws)
remote: Optional[str] = (ws._req.remote if ws._req is not None else None) # pylint: disable=protected-access
diff --git a/kvmd/apps/vnc/server.py b/kvmd/apps/vnc/server.py
index e2ac697c..0b9a26f6 100644
--- a/kvmd/apps/vnc/server.py
+++ b/kvmd/apps/vnc/server.py
@@ -278,7 +278,14 @@ class _Client(RfbClient): # pylint: disable=too-many-instance-attributes
self.__mouse_move = move
async def _on_cut_event(self, text: str) -> None:
- pass # print("CutEvent", text) # TODO
+ assert self.__authorized.done()
+ (user, passwd) = self.__authorized.result()
+ logger = get_logger(0)
+ logger.info("[main] Client %s: Printing %d characters ...", self._remote, len(text))
+ try:
+ await self.__kvmd.hid.print(user, passwd, text, 0)
+ except Exception:
+ logger.exception("[main] Client %s: Can't print characters", self._remote)
async def _on_set_encodings(self) -> None:
assert self.__authorized.done()
diff --git a/kvmd/clients/kvmd.py b/kvmd/clients/kvmd.py
index 0b33eb42..678f104e 100644
--- a/kvmd/clients/kvmd.py
+++ b/kvmd/clients/kvmd.py
@@ -89,6 +89,17 @@ class _StreamerClientPart(_BaseClientPart):
aiotools.raise_not_200(response)
+class _HidClientPart(_BaseClientPart):
+ async def print(self, user: str, passwd: str, text: str, limit: int) -> None:
+ async with self._make_session(user, passwd) as session:
+ async with session.post(
+ url=self._make_url("hid/print"),
+ params={"limit": limit},
+ data=text,
+ ) as response:
+ aiotools.raise_not_200(response)
+
+
class _AtxClientPart(_BaseClientPart):
async def get_state(self, user: str, passwd: str) -> Dict:
async with self._make_session(user, passwd) as session:
@@ -134,6 +145,7 @@ class KvmdClient(_BaseClientPart):
self.auth = _AuthClientPart(**kwargs)
self.streamer = _StreamerClientPart(**kwargs)
+ self.hid = _HidClientPart(**kwargs)
self.atx = _AtxClientPart(**kwargs)
@contextlib.asynccontextmanager
diff --git a/kvmd/keyprint.py b/kvmd/keyprint.py
new file mode 100644
index 00000000..bee7916a
--- /dev/null
+++ b/kvmd/keyprint.py
@@ -0,0 +1,103 @@
+# ========================================================================== #
+# #
+# KVMD - The main Pi-KVM daemon. #
+# #
+# Copyright (C) 2018 Maxim Devaev <[email protected]> #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <https://www.gnu.org/licenses/>. #
+# #
+# ========================================================================== #
+
+
+import string
+
+from typing import Tuple
+from typing import Generator
+
+from . import keymap
+
+
+# =====
+_LOWER_CHARS = {
+ "\n": "Enter",
+ "\t": "Tab",
+ " ": "Space",
+ "`": "Backquote",
+ "\\": "Backslash",
+ "[": "BracketLeft",
+ "]": "BracketLeft",
+ ",": "Comma",
+ ".": "Period",
+ "-": "Minus",
+ "'": "Quote",
+ ";": "Semicolon",
+ "/": "Slash",
+ "=": "Equal",
+ **{str(number): f"Digit{number}" for number in range(0, 10)},
+ **{ch: f"Key{ch.upper()}" for ch in string.ascii_lowercase},
+}
+assert not set(_LOWER_CHARS.values()).difference(keymap.KEYMAP)
+
+_UPPER_CHARS = {
+ "~": "Backquote",
+ "|": "Backslash",
+ "{": "BracketLeft",
+ "}": "BracketRight",
+ "<": "Comma",
+ ">": "Period",
+ "!": "Digit1",
+ "@": "Digit2",
+ "#": "Digit3",
+ "$": "Digit4",
+ "%": "Digit5",
+ "^": "Digit6",
+ "&": "Digit7",
+ "*": "Digit8",
+ "(": "Digit9",
+ ")": "Digit0",
+ "_": "Minus",
+ "\"": "Quote",
+ ":": "Semicolon",
+ "?": "Slash",
+ "+": "Equal",
+ **{ch: f"Key{ch}" for ch in string.ascii_uppercase},
+}
+assert not set(_UPPER_CHARS.values()).difference(keymap.KEYMAP)
+
+
+# =====
+def text_to_keys(text: str, shift_key: str="ShiftLeft") -> Generator[Tuple[str, bool], None, None]:
+ assert shift_key in ["ShiftLeft", "ShiftRight"]
+
+ shifted = False
+ for ch in text:
+ upper = False
+ key = _LOWER_CHARS.get(ch)
+ if key is None:
+ if (key := _UPPER_CHARS.get(ch)) is None:
+ continue
+ upper = True
+
+ if upper and not shifted:
+ yield (shift_key, True)
+ shifted = True
+ elif not upper and shifted:
+ yield (shift_key, False)
+ shifted = False
+
+ yield (key, True)
+ yield (key, False)
+
+ if shifted:
+ yield (shift_key, False)
diff --git a/kvmd/plugins/hid/__init__.py b/kvmd/plugins/hid/__init__.py
index bc391b03..5227d673 100644
--- a/kvmd/plugins/hid/__init__.py
+++ b/kvmd/plugins/hid/__init__.py
@@ -48,19 +48,19 @@ class BaseHid(BasePlugin):
# =====
- async def send_key_event(self, key: str, state: bool) -> None:
+ def send_key_event(self, key: str, state: bool) -> None:
raise NotImplementedError
- async def send_mouse_button_event(self, button: str, state: bool) -> None:
+ def send_mouse_button_event(self, button: str, state: bool) -> None:
raise NotImplementedError
- async def send_mouse_move_event(self, to_x: int, to_y: int) -> None:
+ def send_mouse_move_event(self, to_x: int, to_y: int) -> None:
raise NotImplementedError
- async def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None:
+ def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None:
raise NotImplementedError
- async def clear_events(self) -> None:
+ def clear_events(self) -> None:
raise NotImplementedError
diff --git a/kvmd/plugins/hid/otg/__init__.py b/kvmd/plugins/hid/otg/__init__.py
index 0684a674..3dc49c72 100644
--- a/kvmd/plugins/hid/otg/__init__.py
+++ b/kvmd/plugins/hid/otg/__init__.py
@@ -113,18 +113,18 @@ class Plugin(BaseHid):
# =====
- async def send_key_event(self, key: str, state: bool) -> None:
+ def send_key_event(self, key: str, state: bool) -> None:
self.__keyboard_proc.send_key_event(key, state)
- async def send_mouse_button_event(self, button: str, state: bool) -> None:
+ def send_mouse_button_event(self, button: str, state: bool) -> None:
self.__mouse_proc.send_button_event(button, state)
- async def send_mouse_move_event(self, to_x: int, to_y: int) -> None:
+ def send_mouse_move_event(self, to_x: int, to_y: int) -> None:
self.__mouse_proc.send_move_event(to_x, to_y)
- async def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None:
+ def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None:
self.__mouse_proc.send_wheel_event(delta_x, delta_y)
- async def clear_events(self) -> None:
+ def clear_events(self) -> None:
self.__keyboard_proc.send_clear_event()
self.__mouse_proc.send_clear_event()
diff --git a/kvmd/plugins/hid/otg/device.py b/kvmd/plugins/hid/otg/device.py
index 2986ce4e..d1a2c86c 100644
--- a/kvmd/plugins/hid/otg/device.py
+++ b/kvmd/plugins/hid/otg/device.py
@@ -126,6 +126,10 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
def _queue_event(self, event: BaseEvent) -> None:
self.__events_queue.put_nowait(event)
+ def _clear_queue(self) -> None:
+ while not self.__events_queue.empty():
+ self.__events_queue.get_nowait()
+
def _ensure_write(self, report: bytes, reopen: bool=False, close: bool=False) -> bool:
if reopen:
self.__close_device()
diff --git a/kvmd/plugins/hid/otg/keyboard.py b/kvmd/plugins/hid/otg/keyboard.py
index fb964d8a..cb83232b 100644
--- a/kvmd/plugins/hid/otg/keyboard.py
+++ b/kvmd/plugins/hid/otg/keyboard.py
@@ -81,9 +81,11 @@ class KeyboardProcess(BaseDeviceProcess):
self._ensure_write(b"\x00" * 8, close=True) # Release all keys and modifiers
def send_clear_event(self) -> None:
+ self._clear_queue()
self._queue_event(_ClearEvent())
def send_reset_event(self) -> None:
+ self._clear_queue()
self._queue_event(_ResetEvent())
def send_key_event(self, key: str, state: bool) -> None:
diff --git a/kvmd/plugins/hid/otg/mouse.py b/kvmd/plugins/hid/otg/mouse.py
index a31d7ae5..3c41eb40 100644
--- a/kvmd/plugins/hid/otg/mouse.py
+++ b/kvmd/plugins/hid/otg/mouse.py
@@ -79,9 +79,11 @@ class MouseProcess(BaseDeviceProcess):
self._ensure_write(report, close=True) # Release all buttons
def send_clear_event(self) -> None:
+ self._clear_queue()
self._queue_event(_ClearEvent())
def send_reset_event(self) -> None:
+ self._clear_queue()
self._queue_event(_ResetEvent())
def send_button_event(self, button: str, state: bool) -> None:
diff --git a/kvmd/plugins/hid/serial.py b/kvmd/plugins/hid/serial.py
index 1725483b..f2ece19d 100644
--- a/kvmd/plugins/hid/serial.py
+++ b/kvmd/plugins/hid/serial.py
@@ -252,22 +252,24 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
# =====
- async def send_key_event(self, key: str, state: bool) -> None:
- await self.__queue_event(_KeyEvent(key, state))
+ def send_key_event(self, key: str, state: bool) -> None:
+ self.__queue_event(_KeyEvent(key, state))
- async def send_mouse_button_event(self, button: str, state: bool) -> None:
- await self.__queue_event(_MouseButtonEvent(button, state))
+ def send_mouse_button_event(self, button: str, state: bool) -> None:
+ self.__queue_event(_MouseButtonEvent(button, state))
- async def send_mouse_move_event(self, to_x: int, to_y: int) -> None:
- await self.__queue_event(_MouseMoveEvent(to_x, to_y))
+ def send_mouse_move_event(self, to_x: int, to_y: int) -> None:
+ self.__queue_event(_MouseMoveEvent(to_x, to_y))
- async def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None:
- await self.__queue_event(_MouseWheelEvent(delta_x, delta_y))
+ def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None:
+ self.__queue_event(_MouseWheelEvent(delta_x, delta_y))
- async def clear_events(self) -> None:
- await self.__queue_event(_ClearEvent())
+ def clear_events(self) -> None:
+ while not self.__events_queue.empty():
+ self.__events_queue.get_nowait()
+ self.__queue_event(_ClearEvent())
- async def __queue_event(self, event: _BaseEvent) -> None:
+ def __queue_event(self, event: _BaseEvent) -> None:
if not self.__stop_event.is_set():
self.__events_queue.put_nowait(event)
diff --git a/web/kvm/index.html b/web/kvm/index.html
index bd4ea1d4..1bcbe55b 100644
--- a/web/kvm/index.html
+++ b/web/kvm/index.html
@@ -301,7 +301,6 @@
<li class="menu-right-items">
<a class="menu-item" href="#">
- <img data-dont-hide-menu id="hid-pak-led" class="led-gray" src="../share/svg/led-gear.svg" />
Shortcuts &#8628;
</a>
<div data-dont-hide-menu class="menu-item-content">
diff --git a/web/share/js/kvm/hid.js b/web/share/js/kvm/hid.js
index 4868b0a5..8efb72c2 100644
--- a/web/share/js/kvm/hid.js
+++ b/web/share/js/kvm/hid.js
@@ -35,11 +35,6 @@ export function Hid() {
/************************************************************************/
- var __ws = null;
-
- var __chars_to_codes = {};
- var __codes_delay = 50;
-
var __keyboard = new Keyboard();
var __mouse = new Mouse();
@@ -73,8 +68,6 @@ export function Hid() {
window.addEventListener("pagehide", __releaseAll);
window.addEventListener("blur", __releaseAll);
- __chars_to_codes = __buildCharsToCodes();
-
tools.setOnClick($("hid-pak-button"), __clickPasteAsKeysButton);
tools.setOnClick($("hid-reset-button"), __clickResetButton);
@@ -89,7 +82,6 @@ export function Hid() {
wm.switchEnabled($("hid-pak-text"), ws);
wm.switchEnabled($("hid-pak-button"), ws);
wm.switchEnabled($("hid-reset-button"), ws);
- __ws = ws;
__keyboard.setSocket(ws);
__mouse.setSocket(ws);
};
@@ -125,68 +117,16 @@ export function Hid() {
} else {
resolve(null);
}
- }, __codes_delay);
+ }, 50);
iterate();
});
};
- var __buildCharsToCodes = function() {
- let chars_to_codes = {
- "\n": ["Enter"],
- "\t": ["Tab"],
- " ": ["Space"],
- "`": ["Backquote"], "~": ["ShiftLeft", "Backquote"],
- "\\": ["Backslash"], "|": ["ShiftLeft", "Backslash"],
- "[": ["BracketLeft"], "{": ["ShiftLeft", "BracketLeft"],
- "]": ["BracketLeft"], "}": ["ShiftLeft", "BracketRight"],
- ",": ["Comma"], "<": ["ShiftLeft", "Comma"],
- ".": ["Period"], ">": ["ShiftLeft", "Period"],
- "1": ["Digit1"], "!": ["ShiftLeft", "Digit1"],
- "2": ["Digit2"], "@": ["ShiftLeft", "Digit2"],
- "3": ["Digit3"], "#": ["ShiftLeft", "Digit3"],
- "4": ["Digit4"], "$": ["ShiftLeft", "Digit4"],
- "5": ["Digit5"], "%": ["ShiftLeft", "Digit5"],
- "6": ["Digit6"], "^": ["ShiftLeft", "Digit6"],
- "7": ["Digit7"], "&": ["ShiftLeft", "Digit7"],
- "8": ["Digit8"], "*": ["ShiftLeft", "Digit8"],
- "9": ["Digit9"], "(": ["ShiftLeft", "Digit9"],
- "0": ["Digit0"], ")": ["ShiftLeft", "Digit0"],
- "-": ["Minus"], "_": ["ShiftLeft", "Minus"],
- "'": ["Quote"], "\"": ["ShiftLeft", "Quote"],
- ";": ["Semicolon"], ":": ["ShiftLeft", "Semicolon"],
- "/": ["Slash"], "?": ["ShiftLeft", "Slash"],
- "=": ["Equal"], "+": ["ShiftLeft", "Equal"],
- };
-
- for (let ch = "a".charCodeAt(0); ch <= "z".charCodeAt(0); ++ch) {
- let low = String.fromCharCode(ch);
- let up = low.toUpperCase();
- let code = "Key" + up;
- chars_to_codes[low] = [code];
- chars_to_codes[up] = ["ShiftLeft", code];
- }
-
- return chars_to_codes;
- };
-
var __clickPasteAsKeysButton = function() {
let text = $("hid-pak-text").value.replace(/[^\x00-\x7F]/g, ""); // eslint-disable-line no-control-regex
if (text) {
- let clipboard_codes = [];
- let codes_count = 0;
- for (let ch of text) {
- let codes = __chars_to_codes[ch];
- if (codes) {
- codes_count += codes.length;
- clipboard_codes.push(codes);
- }
- }
- let time = __codes_delay * codes_count * 2 / 1000;
-
let confirm_msg = `
- You are going to automatically type ${codes_count} characters from the system clipboard.
- It will take ${time} seconds.<br>
- <br>
+ You're goint to paste ${text.length} characters.<br>
Are you sure you want to continue?
`;
@@ -194,27 +134,21 @@ export function Hid() {
if (ok) {
wm.switchEnabled($("hid-pak-text"), false);
wm.switchEnabled($("hid-pak-button"), false);
- $("hid-pak-led").className = "led-yellow-rotating-fast";
- $("hid-pak-led").title = "Autotyping...";
tools.debug("HID: paste-as-keys:", text);
- let index = 0;
- let iterate = function() {
- __emitShortcut(clipboard_codes[index]).then(function() {
- ++index;
- if (index < clipboard_codes.length && __ws) {
- iterate();
- } else {
- $("hid-pak-text").value = "";
- wm.switchEnabled($("hid-pak-text"), true);
- wm.switchEnabled($("hid-pak-button"), true);
- $("hid-pak-led").className = "led-gray";
- $("hid-pak-led").title = "";
+ let http = tools.makeRequest("POST", "/api/hid/print?limit=0", function() {
+ if (http.readyState === 4) {
+ wm.switchEnabled($("hid-pak-text"), true);
+ wm.switchEnabled($("hid-pak-button"), true);
+ $("hid-pak-text").value = "";
+ if (http.status === 413) {
+ wm.error("Too many text for paste!");
+ } else if (http.status !== 200) {
+ wm.error("HID paste error:<br>", http.responseText);
}
- });
- };
- iterate();
+ }
+ }, text, "text/plain");
} else {
$("hid-pak-text").value = "";
}