diff options
-rw-r--r-- | PKGBUILD | 1 | ||||
-rw-r--r-- | configs/kvmd/v1-hdmi.yaml | 8 | ||||
-rw-r--r-- | configs/kvmd/v1-vga.yaml | 8 | ||||
-rw-r--r-- | configs/nginx/nginx.conf | 10 | ||||
-rw-r--r-- | kvmd/__init__.py | 7 | ||||
-rw-r--r-- | kvmd/logging.py | 46 | ||||
-rw-r--r-- | kvmd/server.py | 56 | ||||
-rw-r--r-- | requirements.txt | 1 | ||||
-rw-r--r-- | testenv/Dockerfile | 1 | ||||
-rw-r--r-- | testenv/kvmd.yaml | 6 | ||||
-rw-r--r-- | testenv/requirements.txt | 1 | ||||
-rw-r--r-- | web/index.html | 4 |
12 files changed, 133 insertions, 16 deletions
@@ -18,6 +18,7 @@ depends=( python-raspberry-gpio python-pyserial python-setproctitle + python-systemd ) makedepends=(python-setuptools) source=("$url/archive/v$pkgver.tar.gz") diff --git a/configs/kvmd/v1-hdmi.yaml b/configs/kvmd/v1-hdmi.yaml index 3ab7031d..2ca573e1 100644 --- a/configs/kvmd/v1-hdmi.yaml +++ b/configs/kvmd/v1-hdmi.yaml @@ -4,6 +4,11 @@ kvmd: port: 8081 heartbeat: 3.0 + log: + services: + - kvmd.service + - kvmd-tc358743.service + hid: pinout: reset: 4 @@ -66,8 +71,7 @@ logging: console: (): logging.Formatter style: "{" - datefmt: "%H:%M:%S" - format: "[{asctime}] {name:20.20} {levelname:>7} --- {message}" + format: "{name:20.20} {levelname:>7} --- {message}" handlers: console: diff --git a/configs/kvmd/v1-vga.yaml b/configs/kvmd/v1-vga.yaml index 36e20fbf..48b8e511 100644 --- a/configs/kvmd/v1-vga.yaml +++ b/configs/kvmd/v1-vga.yaml @@ -4,6 +4,11 @@ kvmd: port: 8081 heartbeat: 3.0 + log: + services: + - kvmd.service + - kvmd-tc358743.service + hid: pinout: reset: 4 @@ -69,8 +74,7 @@ logging: console: (): logging.Formatter style: "{" - datefmt: "%H:%M:%S" - format: "[{asctime}] {name:20.20} {levelname:>7} --- {message}" + format: "{name:20.20} {levelname:>7} --- {message}" handlers: console: diff --git a/configs/nginx/nginx.conf b/configs/nginx/nginx.conf index a06ced1b..e5fd03c4 100644 --- a/configs/nginx/nginx.conf +++ b/configs/nginx/nginx.conf @@ -109,6 +109,16 @@ http { proxy_request_buffering off; } + location /kvmd/log { + rewrite /kvmd/log /log break; + proxy_pass http://kvmd; + include /etc/nginx/proxy-params.conf; + proxy_read_timeout 7d; + postpone_output 0; + proxy_buffering off; + proxy_ignore_headers X-Accel-Buffering; + } + location /kvmd { rewrite /kvmd/?(.*) /$1 break; proxy_pass http://kvmd; diff --git a/kvmd/__init__.py b/kvmd/__init__.py index 77c49ad0..93f9e2de 100644 --- a/kvmd/__init__.py +++ b/kvmd/__init__.py @@ -2,6 +2,7 @@ import asyncio from .application import init from .logging import get_logger +from .logging import Log from .hid import Hid from .atx import Atx @@ -22,6 +23,11 @@ def main() -> None: with gpio.bcm(): loop = asyncio.get_event_loop() + log = Log( + services=list(config["log"]["services"]), + loop=loop, + ) + hid = Hid( reset=int(config["hid"]["pinout"]["reset"]), device_path=str(config["hid"]["device"]), @@ -60,6 +66,7 @@ def main() -> None: ) Server( + log=log, hid=hid, atx=atx, msd=msd, diff --git a/kvmd/logging.py b/kvmd/logging.py index 838e13d1..0efb9cb4 100644 --- a/kvmd/logging.py +++ b/kvmd/logging.py @@ -1,5 +1,13 @@ import sys +import asyncio import logging +import time + +from typing import List +from typing import Dict +from typing import AsyncGenerator + +import systemd.journal # ===== @@ -13,3 +21,41 @@ def get_logger(depth: int=1) -> logging.Logger: break name = frames[depth].f_globals["__name__"] return logging.getLogger(name) + + +class Log: + def __init__( + self, + services: List[str], + loop: asyncio.AbstractEventLoop, + ) -> None: + + self.__services = services + self.__loop = loop + + async def log(self, seek: int, follow: bool) -> AsyncGenerator[Dict, None]: + reader = systemd.journal.Reader() + reader.this_boot() + reader.this_machine() + reader.log_level(systemd.journal.LOG_DEBUG) + for service in self.__services: + reader.add_match(_SYSTEMD_UNIT=service) + if seek > 0: + reader.seek_realtime(float(time.time() - seek)) + + for entry in reader: + yield self.__entry_to_record(entry) + + while follow: + entry = reader.get_next() + if entry: + yield self.__entry_to_record(entry) + else: + await asyncio.sleep(1) + + def __entry_to_record(self, entry: Dict) -> Dict[str, Dict]: + return { + "dt": entry["__REALTIME_TIMESTAMP"], + "service": entry["_SYSTEMD_UNIT"], + "msg": entry["MESSAGE"].rstrip(), + } diff --git a/kvmd/server.py b/kvmd/server.py index 4d038fdc..7738eaba 100644 --- a/kvmd/server.py +++ b/kvmd/server.py @@ -25,6 +25,7 @@ from .msd import MassStorageDevice from .streamer import Streamer from .logging import get_logger +from .logging import Log # ===== @@ -68,6 +69,28 @@ class BadRequest(Exception): pass +def _valid_bool(name: str, flag: Optional[str]) -> bool: + flag = str(flag).strip().lower() + if flag in ["1", "true", "yes"]: + return True + elif flag in ["0", "false", "no"]: + return False + raise BadRequest("Invalid param '%s'" % (name)) + + +def _valid_int(name: str, value: Optional[str], min_value: Optional[int]=None, max_value: Optional[int]=None) -> int: + try: + value_int = int(value) # type: ignore + if ( + (min_value is not None and value_int < min_value) + or (max_value is not None and value_int > max_value) + ): + raise ValueError() + return value_int + except Exception: + raise BadRequest("Invalid param %r" % (name)) + + def _wrap_exceptions_for_web(msg: str) -> Callable: def make_wrapper(method: Callable) -> Callable: async def wrap(self: "Server", request: aiohttp.web.Request) -> aiohttp.web.Response: @@ -82,8 +105,9 @@ def _wrap_exceptions_for_web(msg: str) -> Callable: class Server: # pylint: disable=too-many-instance-attributes - def __init__( + def __init__( # pylint: disable=too-many-arguments self, + log: Log, hid: Hid, atx: Atx, msd: MassStorageDevice, @@ -97,6 +121,7 @@ class Server: # pylint: disable=too-many-instance-attributes loop: asyncio.AbstractEventLoop, ) -> None: + self.__log = log self.__hid = hid self.__atx = atx self.__msd = msd @@ -125,6 +150,7 @@ class Server: # pylint: disable=too-many-instance-attributes app = aiohttp.web.Application(loop=self.__loop) app.router.add_get("/info", self.__info_handler) + app.router.add_get("/log", self.__log_handler) app.router.add_get("/ws", self.__ws_handler) @@ -154,7 +180,7 @@ class Server: # pylint: disable=too-many-instance-attributes aiohttp.web.run_app(app, host=host, port=port, print=self.__run_app_print) - # ===== INFO + # ===== SYSTEM async def __info_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response: return _json({ @@ -165,6 +191,20 @@ class Server: # pylint: disable=too-many-instance-attributes "streamer": self.__streamer.get_app(), }) + @_wrap_exceptions_for_web("Log error") + async def __log_handler(self, request: aiohttp.web.Request) -> aiohttp.web.StreamResponse: + seek = _valid_int("seek", request.query.get("seek", "0"), 0) + follow = _valid_bool("follow", request.query.get("follow", "false")) + response = aiohttp.web.StreamResponse(status=200, reason="OK", headers={"Content-Type": "text/plain"}) + await response.prepare(request) + async for record in self.__log.log(seek, follow): + await response.write(("[%s %s] --- %s" % ( + record["dt"].strftime("%Y-%m-%d %H:%M:%S"), + record["service"], + record["msg"], + )).encode("utf-8") + b"\r\n") + return response + # ===== WEBSOCKET async def __ws_handler(self, request: aiohttp.web.Request) -> aiohttp.web.WebSocketResponse: @@ -243,7 +283,7 @@ class Server: # pylint: disable=too-many-instance-attributes "reset": self.__atx.click_reset, }.get(button) if not clicker: - raise BadRequest("Missing or invalid 'button=%s'" % (button)) + raise BadRequest("Invalid param 'button'") await self.__broadcast_event("atx_click", button=button) # type: ignore await clicker() await self.__broadcast_event("atx_click", button=None) # type: ignore @@ -266,7 +306,7 @@ class Server: # pylint: disable=too-many-instance-attributes state = self.__msd.get_state() await self.__broadcast_event("msd_state", **state) else: - raise BadRequest("Missing or invalid 'to=%s'" % (to)) + raise BadRequest("Invalid param 'to'") return _json(state) @_wrap_exceptions_for_web("Can't write data to mass-storage device") @@ -314,13 +354,7 @@ class Server: # pylint: disable=too-many-instance-attributes async def __streamer_set_params_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response: quality = request.query.get("quality") if quality: - try: - quality_int = int(quality) - if not (1 <= quality_int <= 100): - raise ValueError() - except Exception: - raise BadRequest("Invalid quality %r" % (quality)) - self.__streamer_quality = quality_int + self.__streamer_quality = _valid_int("quality", quality, 1, 100) return _json() async def __streamer_reset_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response: diff --git a/requirements.txt b/requirements.txt index 6ed9b5aa..6ca74376 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ pyudev pyyaml pyserial setproctitle +systemd-python diff --git a/testenv/Dockerfile b/testenv/Dockerfile index 6a16cf77..ce00df0a 100644 --- a/testenv/Dockerfile +++ b/testenv/Dockerfile @@ -36,6 +36,7 @@ RUN pacman -Syy \ && user-packer -S --noconfirm \ python \ python-pip \ + python-systemd \ nginx-mainline \ ustreamer \ socat \ diff --git a/testenv/kvmd.yaml b/testenv/kvmd.yaml index df418139..cb981f0a 100644 --- a/testenv/kvmd.yaml +++ b/testenv/kvmd.yaml @@ -4,11 +4,15 @@ kvmd: port: 8081 heartbeat: 3.0 + log: + services: + - kvmd.service + hid: pinout: reset: 4 - device: /dev/ttyAMA0 + device: /dev/ttyS10 speed: 115200 reset_delay: 0.1 diff --git a/testenv/requirements.txt b/testenv/requirements.txt index de538a71..54109c60 100644 --- a/testenv/requirements.txt +++ b/testenv/requirements.txt @@ -5,5 +5,6 @@ pyudev pyyaml pyserial setproctitle +systemd-python bumpversion tox diff --git a/web/index.html b/web/index.html index 6b9979bb..8e49aed7 100644 --- a/web/index.html +++ b/web/index.html @@ -112,6 +112,10 @@ <button disabled data-force-hide-menu id="hid-reset-button">• Reset keyboard & mouse</button> <button disabled data-force-hide-menu id="msd-reset-button">• Reset mass storage</button> </div> + <hr> + <div class="ctl-dropdown-content-buttons"> + <button data-force-hide-menu onclick="window.open('kvmd/log?seek=3600&follow=1', '_blank');">• View log</button> + </div> </div> </div> </li> |