diff options
author | Maxim Devaev <[email protected]> | 2024-10-21 17:46:59 +0300 |
---|---|---|
committer | Maxim Devaev <[email protected]> | 2024-10-21 17:46:59 +0300 |
commit | cda32a083faf3e7326cfe317336e473c905c6dfd (patch) | |
tree | 19445e4098d4603f3b2cd296504a648110712af1 | |
parent | b67a2325842a6f407d3935f8445d50cb8bf307f2 (diff) |
new events model
30 files changed, 337 insertions, 167 deletions
diff --git a/kvmd/aiomulti.py b/kvmd/aiomulti.py index 653651cb..a4537204 100644 --- a/kvmd/aiomulti.py +++ b/kvmd/aiomulti.py @@ -59,14 +59,25 @@ def queue_get_last_sync( # pylint: disable=invalid-name # ===== class AioProcessNotifier: def __init__(self) -> None: - self.__queue: "multiprocessing.Queue[None]" = multiprocessing.Queue() - - def notify(self) -> None: - self.__queue.put_nowait(None) - - async def wait(self) -> None: - while not (await queue_get_last(self.__queue, 0.1))[0]: - pass + self.__queue: "multiprocessing.Queue[int]" = multiprocessing.Queue() + + def notify(self, mask: int=0) -> None: + self.__queue.put_nowait(mask) + + async def wait(self) -> int: + while True: + mask = await aiotools.run_async(self.__get) + if mask >= 0: + return mask + + def __get(self) -> int: + try: + mask = self.__queue.get(timeout=0.1) + while not self.__queue.empty(): + mask |= self.__queue.get() + return mask + except queue.Empty: + return -1 # ===== diff --git a/kvmd/aiotools.py b/kvmd/aiotools.py index 5d284a94..b1747f16 100644 --- a/kvmd/aiotools.py +++ b/kvmd/aiotools.py @@ -232,25 +232,26 @@ async def close_writer(writer: asyncio.StreamWriter) -> bool: # ===== class AioNotifier: def __init__(self) -> None: - self.__queue: "asyncio.Queue[None]" = asyncio.Queue() + self.__queue: "asyncio.Queue[int]" = asyncio.Queue() - def notify(self) -> None: - self.__queue.put_nowait(None) + def notify(self, mask: int=0) -> None: + self.__queue.put_nowait(mask) - async def wait(self, timeout: (float | None)=None) -> None: + async def wait(self, timeout: (float | None)=None) -> int: + mask = 0 if timeout is None: - await self.__queue.get() + mask = await self.__queue.get() else: try: - await asyncio.wait_for( + mask = await asyncio.wait_for( asyncio.ensure_future(self.__queue.get()), timeout=timeout, ) except asyncio.TimeoutError: - return # False + return -1 while not self.__queue.empty(): - await self.__queue.get() - # return True + mask |= await self.__queue.get() + return mask # ===== diff --git a/kvmd/apps/kvmd/api/export.py b/kvmd/apps/kvmd/api/export.py index 998c5d81..bb048f53 100644 --- a/kvmd/apps/kvmd/api/export.py +++ b/kvmd/apps/kvmd/api/export.py @@ -55,10 +55,9 @@ class ExportApi: @async_lru.alru_cache(maxsize=1, ttl=5) async def __get_prometheus_metrics(self) -> str: - (atx_state, hw_state, fan_state, gpio_state) = await asyncio.gather(*[ + (atx_state, info_state, gpio_state) = await asyncio.gather(*[ self.__atx.get_state(), - self.__info_manager.get_submanager("hw").get_state(), - self.__info_manager.get_submanager("fan").get_state(), + self.__info_manager.get_state(["hw", "fan"]), self.__user_gpio.get_state(), ]) rows: list[str] = [] @@ -72,8 +71,8 @@ class ExportApi: for key in ["online", "state"]: self.__append_prometheus_rows(rows, ch_state["state"], f"pikvm_gpio_{mode}_{key}_{channel}") - self.__append_prometheus_rows(rows, hw_state["health"], "pikvm_hw") # type: ignore - self.__append_prometheus_rows(rows, fan_state, "pikvm_fan") + self.__append_prometheus_rows(rows, info_state["hw"]["health"], "pikvm_hw") # type: ignore + self.__append_prometheus_rows(rows, info_state["fan"], "pikvm_fan") return "\n".join(rows) diff --git a/kvmd/apps/kvmd/api/info.py b/kvmd/apps/kvmd/api/info.py index a0be01a5..89d45a84 100644 --- a/kvmd/apps/kvmd/api/info.py +++ b/kvmd/apps/kvmd/api/info.py @@ -20,8 +20,6 @@ # ========================================================================== # -import asyncio - from aiohttp.web import Request from aiohttp.web import Response @@ -43,15 +41,11 @@ class InfoApi: @exposed_http("GET", "/info") async def __common_state_handler(self, req: Request) -> Response: fields = self.__valid_info_fields(req) - results = dict(zip(fields, await asyncio.gather(*[ - self.__info_manager.get_submanager(field).get_state() - for field in fields - ]))) - return make_json_response(results) + return make_json_response(await self.__info_manager.get_state(fields)) def __valid_info_fields(self, req: Request) -> list[str]: - subs = self.__info_manager.get_subs() + available = self.__info_manager.get_subs() return sorted(valid_info_fields( - arg=req.query.get("fields", ",".join(subs)), - variants=subs, - ) or subs) + arg=req.query.get("fields", ",".join(available)), + variants=available, + ) or available) diff --git a/kvmd/apps/kvmd/api/redfish.py b/kvmd/apps/kvmd/api/redfish.py index e1822496..3b248685 100644 --- a/kvmd/apps/kvmd/api/redfish.py +++ b/kvmd/apps/kvmd/api/redfish.py @@ -88,12 +88,12 @@ class RedfishApi: @exposed_http("GET", "/redfish/v1/Systems/0") async def __server_handler(self, _: Request) -> Response: - (atx_state, meta_state) = await asyncio.gather(*[ + (atx_state, info_state) = await asyncio.gather(*[ self.__atx.get_state(), - self.__info_manager.get_submanager("meta").get_state(), + self.__info_manager.get_state(["meta"]), ]) try: - host = str(meta_state.get("server", {})["host"]) # type: ignore + host = str(info_state["meta"].get("server", {})["host"]) # type: ignore except Exception: host = "" return make_json_response({ diff --git a/kvmd/apps/kvmd/info/__init__.py b/kvmd/apps/kvmd/info/__init__.py index 983ef6d6..b346c10c 100644 --- a/kvmd/apps/kvmd/info/__init__.py +++ b/kvmd/apps/kvmd/info/__init__.py @@ -20,6 +20,10 @@ # ========================================================================== # +import asyncio + +from typing import AsyncGenerator + from ....yamlconf import Section from .base import BaseInfoSubmanager @@ -34,17 +38,50 @@ from .fan import FanInfoSubmanager # ===== class InfoManager: def __init__(self, config: Section) -> None: - self.__subs = { + self.__subs: dict[str, BaseInfoSubmanager] = { "system": SystemInfoSubmanager(config.kvmd.streamer.cmd), - "auth": AuthInfoSubmanager(config.kvmd.auth.enabled), - "meta": MetaInfoSubmanager(config.kvmd.info.meta), + "auth": AuthInfoSubmanager(config.kvmd.auth.enabled), + "meta": MetaInfoSubmanager(config.kvmd.info.meta), "extras": ExtrasInfoSubmanager(config), - "hw": HwInfoSubmanager(**config.kvmd.info.hw._unpack()), - "fan": FanInfoSubmanager(**config.kvmd.info.fan._unpack()), + "hw": HwInfoSubmanager(**config.kvmd.info.hw._unpack()), + "fan": FanInfoSubmanager(**config.kvmd.info.fan._unpack()), } + self.__queue: "asyncio.Queue[tuple[str, (dict | None)]]" = asyncio.Queue() def get_subs(self) -> set[str]: return set(self.__subs) - def get_submanager(self, name: str) -> BaseInfoSubmanager: - return self.__subs[name] + async def get_state(self, fields: (list[str] | None)=None) -> dict: + fields = (fields or list(self.__subs)) + return dict(zip(fields, await asyncio.gather(*[ + self.__subs[field].get_state() + for field in fields + ]))) + + async def trigger_state(self) -> None: + await asyncio.gather(*[ + sub.trigger_state() + for sub in self.__subs.values() + ]) + + async def poll_state(self) -> AsyncGenerator[dict, None]: + while True: + (field, value) = await self.__queue.get() + yield {field: value} + + async def systask(self) -> None: + tasks = [ + asyncio.create_task(self.__poller(field)) + for field in self.__subs + ] + try: + await asyncio.gather(*tasks) + except Exception: + for task in tasks: + task.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + raise + + async def __poller(self, field: str) -> None: + async for state in self.__subs[field].poll_state(): + self.__queue.put_nowait((field, state)) diff --git a/kvmd/apps/kvmd/info/auth.py b/kvmd/apps/kvmd/info/auth.py index 2077afff..301cffe3 100644 --- a/kvmd/apps/kvmd/info/auth.py +++ b/kvmd/apps/kvmd/info/auth.py @@ -20,6 +20,10 @@ # ========================================================================== # +from typing import AsyncGenerator + +from .... import aiotools + from .base import BaseInfoSubmanager @@ -27,6 +31,15 @@ from .base import BaseInfoSubmanager class AuthInfoSubmanager(BaseInfoSubmanager): def __init__(self, enabled: bool) -> None: self.__enabled = enabled + self.__notifier = aiotools.AioNotifier() async def get_state(self) -> dict: return {"enabled": self.__enabled} + + async def trigger_state(self) -> None: + self.__notifier.notify() + + async def poll_state(self) -> AsyncGenerator[(dict | None), None]: + while True: + await self.__notifier.wait() + yield (await self.get_state()) diff --git a/kvmd/apps/kvmd/info/base.py b/kvmd/apps/kvmd/info/base.py index 87494e70..d090ed34 100644 --- a/kvmd/apps/kvmd/info/base.py +++ b/kvmd/apps/kvmd/info/base.py @@ -20,7 +20,17 @@ # ========================================================================== # +from typing import AsyncGenerator + + # ===== class BaseInfoSubmanager: async def get_state(self) -> (dict | None): raise NotImplementedError + + async def trigger_state(self) -> None: + raise NotImplementedError + + async def poll_state(self) -> AsyncGenerator[(dict | None), None]: + yield None + raise NotImplementedError diff --git a/kvmd/apps/kvmd/info/extras.py b/kvmd/apps/kvmd/info/extras.py index 07225013..a855ad15 100644 --- a/kvmd/apps/kvmd/info/extras.py +++ b/kvmd/apps/kvmd/info/extras.py @@ -24,6 +24,8 @@ import os import re import asyncio +from typing import AsyncGenerator + from ....logging import get_logger from ....yamlconf import Section @@ -41,6 +43,7 @@ from .base import BaseInfoSubmanager class ExtrasInfoSubmanager(BaseInfoSubmanager): def __init__(self, global_config: Section) -> None: self.__global_config = global_config + self.__notifier = aiotools.AioNotifier() async def get_state(self) -> (dict | None): try: @@ -65,6 +68,14 @@ class ExtrasInfoSubmanager(BaseInfoSubmanager): if sui is not None: await aiotools.shield_fg(sui.close()) + async def trigger_state(self) -> None: + self.__notifier.notify() + + async def poll_state(self) -> AsyncGenerator[(dict | None), None]: + while True: + await self.__notifier.wait() + yield (await self.get_state()) + def __get_extras_path(self, *parts: str) -> str: return os.path.join(self.__global_config.kvmd.info.extras, *parts) diff --git a/kvmd/apps/kvmd/info/fan.py b/kvmd/apps/kvmd/info/fan.py index 247aff7d..8f3f69c8 100644 --- a/kvmd/apps/kvmd/info/fan.py +++ b/kvmd/apps/kvmd/info/fan.py @@ -21,7 +21,6 @@ import copy -import asyncio from typing import AsyncGenerator @@ -53,6 +52,8 @@ class FanInfoSubmanager(BaseInfoSubmanager): self.__timeout = timeout self.__state_poll = state_poll + self.__notifier = aiotools.AioNotifier() + async def get_state(self) -> dict: monitored = await self.__get_monitored() return { @@ -60,24 +61,28 @@ class FanInfoSubmanager(BaseInfoSubmanager): "state": ((await self.__get_fan_state() if monitored else None)), } - async def poll_state(self) -> AsyncGenerator[dict, None]: - prev_state: dict = {} + async def trigger_state(self) -> None: + self.__notifier.notify(1) + + async def poll_state(self) -> AsyncGenerator[(dict | None), None]: + prev: dict = {} while True: if self.__unix_path: - pure = state = await self.get_state() + if (await self.__notifier.wait(timeout=self.__state_poll)) > 0: + prev = {} + new = await self.get_state() + pure = copy.deepcopy(new) if pure["state"] is not None: try: - pure = copy.deepcopy(state) pure["state"]["service"]["now_ts"] = 0 except Exception: pass - if pure != prev_state: - yield state - prev_state = pure - await asyncio.sleep(self.__state_poll) + if pure != prev: + prev = pure + yield new else: + await self.__notifier.wait() yield (await self.get_state()) - await aiotools.wait_infinite() # ===== diff --git a/kvmd/apps/kvmd/info/hw.py b/kvmd/apps/kvmd/info/hw.py index 458bc1ec..81cd1af6 100644 --- a/kvmd/apps/kvmd/info/hw.py +++ b/kvmd/apps/kvmd/info/hw.py @@ -22,6 +22,7 @@ import os import asyncio +import copy from typing import Callable from typing import AsyncGenerator @@ -60,6 +61,8 @@ class HwInfoSubmanager(BaseInfoSubmanager): self.__dt_cache: dict[str, str] = {} + self.__notifier = aiotools.AioNotifier() + async def get_state(self) -> dict: ( base, @@ -97,14 +100,18 @@ class HwInfoSubmanager(BaseInfoSubmanager): }, } + async def trigger_state(self) -> None: + self.__notifier.notify(1) + async def poll_state(self) -> AsyncGenerator[dict, None]: - prev_state: dict = {} + prev: dict = {} while True: - state = await self.get_state() - if state != prev_state: - yield state - prev_state = state - await asyncio.sleep(self.__state_poll) + if (await self.__notifier.wait(timeout=self.__state_poll)) > 0: + prev = {} + new = await self.get_state() + if new != prev: + prev = copy.deepcopy(new) + yield new # ===== diff --git a/kvmd/apps/kvmd/info/meta.py b/kvmd/apps/kvmd/info/meta.py index b66b76b1..996e648a 100644 --- a/kvmd/apps/kvmd/info/meta.py +++ b/kvmd/apps/kvmd/info/meta.py @@ -20,6 +20,8 @@ # ========================================================================== # +from typing import AsyncGenerator + from ....logging import get_logger from ....yamlconf.loader import load_yaml_file @@ -33,6 +35,7 @@ from .base import BaseInfoSubmanager class MetaInfoSubmanager(BaseInfoSubmanager): def __init__(self, meta_path: str) -> None: self.__meta_path = meta_path + self.__notifier = aiotools.AioNotifier() async def get_state(self) -> (dict | None): try: @@ -40,3 +43,11 @@ class MetaInfoSubmanager(BaseInfoSubmanager): except Exception: get_logger(0).exception("Can't parse meta") return None + + async def trigger_state(self) -> None: + self.__notifier.notify() + + async def poll_state(self) -> AsyncGenerator[(dict | None), None]: + while True: + await self.__notifier.wait() + yield (await self.get_state()) diff --git a/kvmd/apps/kvmd/info/system.py b/kvmd/apps/kvmd/info/system.py index 462b52c9..d4a450de 100644 --- a/kvmd/apps/kvmd/info/system.py +++ b/kvmd/apps/kvmd/info/system.py @@ -24,8 +24,11 @@ import os import asyncio import platform +from typing import AsyncGenerator + from ....logging import get_logger +from .... import aiotools from .... import aioproc from .... import __version__ @@ -37,6 +40,7 @@ from .base import BaseInfoSubmanager class SystemInfoSubmanager(BaseInfoSubmanager): def __init__(self, streamer_cmd: list[str]) -> None: self.__streamer_cmd = streamer_cmd + self.__notifier = aiotools.AioNotifier() async def get_state(self) -> dict: streamer_info = await self.__get_streamer_info() @@ -50,6 +54,14 @@ class SystemInfoSubmanager(BaseInfoSubmanager): }, } + async def trigger_state(self) -> None: + self.__notifier.notify() + + async def poll_state(self) -> AsyncGenerator[(dict | None), None]: + while True: + await self.__notifier.wait() + yield (await self.get_state()) + # ===== async def __get_streamer_info(self) -> dict: diff --git a/kvmd/apps/kvmd/server.py b/kvmd/apps/kvmd/server.py index 7e3c7d48..50642725 100644 --- a/kvmd/apps/kvmd/server.py +++ b/kvmd/apps/kvmd/server.py @@ -150,6 +150,7 @@ class _Subsystem: class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-instance-attributes __EV_GPIO_STATE = "gpio_state" + __EV_INFO_STATE = "info_state" def __init__( # pylint: disable=too-many-arguments,too-many-locals self, @@ -200,15 +201,12 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins self.__subsystems = [ _Subsystem.make(auth_manager, "Auth manager"), - _Subsystem.make(user_gpio, "User-GPIO", self.__EV_GPIO_STATE), - _Subsystem.make(hid, "HID", "hid_state").add_source("hid_keymaps_state", self.__hid_api.get_keymaps, None, None), - _Subsystem.make(atx, "ATX", "atx_state"), - _Subsystem.make(msd, "MSD", "msd_state"), - _Subsystem.make(streamer, "Streamer", "streamer_state").add_source("streamer_ocr_state", self.__streamer_api.get_ocr, None, None), - *[ - _Subsystem.make(info_manager.get_submanager(sub), f"Info manager ({sub})", f"info_{sub}_state",) - for sub in sorted(info_manager.get_subs()) - ], + _Subsystem.make(user_gpio, "User-GPIO", self.__EV_GPIO_STATE), + _Subsystem.make(hid, "HID", "hid_state").add_source("hid_keymaps_state", self.__hid_api.get_keymaps, None, None), + _Subsystem.make(atx, "ATX", "atx_state"), + _Subsystem.make(msd, "MSD", "msd_state"), + _Subsystem.make(streamer, "Streamer", "streamer_state").add_source("streamer_ocr_state", self.__streamer_api.get_ocr, None, None), + _Subsystem.make(info_manager, "Info manager", self.__EV_INFO_STATE), ] self.__streamer_notifier = aiotools.AioNotifier() @@ -251,6 +249,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins stream = valid_bool(req.query.get("stream", True)) legacy = valid_bool(req.query.get("legacy", True)) async with self._ws_session(req, stream=stream, legacy=legacy) as ws: + await ws.send_event("loop", {}) states = [ (event_type, src.get_state()) for sub in self.__subsystems @@ -269,7 +268,6 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins for src in sub.sources.values(): if src.trigger_state: await src.trigger_state() - await ws.send_event("loop", {}) return (await self._ws_loop(ws)) @exposed_ws("ping") @@ -366,6 +364,8 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins async def __poll_state(self, event_type: str, poller: AsyncGenerator[dict, None]) -> None: if event_type == self.__EV_GPIO_STATE: await self.__poll_gpio_state(poller) + elif event_type == self.__EV_INFO_STATE: + await self.__poll_info_state(poller) else: async for state in poller: await self._broadcast_ws_event(event_type, state) @@ -381,3 +381,9 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins prev["state"]["inputs"].update(state["state"].get("inputs", {})) prev["state"]["outputs"].update(state["state"].get("outputs", {})) await self._broadcast_ws_event(self.__EV_GPIO_STATE, prev["state"], legacy=True) + + async def __poll_info_state(self, poller: AsyncGenerator[dict, None]) -> None: + async for state in poller: + await self._broadcast_ws_event(self.__EV_INFO_STATE, state, legacy=False) + for (key, value) in state.items(): + await self._broadcast_ws_event(f"info_{key}_state", value, legacy=True) diff --git a/kvmd/apps/kvmd/streamer.py b/kvmd/apps/kvmd/streamer.py index c365e19d..c0a56ad1 100644 --- a/kvmd/apps/kvmd/streamer.py +++ b/kvmd/apps/kvmd/streamer.py @@ -26,6 +26,7 @@ import asyncio import asyncio.subprocess import dataclasses import functools +import copy from typing import AsyncGenerator from typing import Any @@ -136,7 +137,7 @@ class _StreamerParams: } def get_limits(self) -> dict: - limits = dict(self.__limits) + limits = copy.deepcopy(self.__limits) if self.__has_resolution: limits[self.__AVAILABLE_RESOLUTIONS] = list(limits[self.__AVAILABLE_RESOLUTIONS]) return limits @@ -323,6 +324,9 @@ class Streamer: # pylint: disable=too-many-instance-attributes "features": self.__params.get_features(), } + async def trigger_state(self) -> None: + self.__notifier.notify(1) + async def poll_state(self) -> AsyncGenerator[dict, None]: def signal_handler(*_: Any) -> None: get_logger(0).info("Got SIGUSR2, checking the stream state ...") @@ -331,21 +335,14 @@ class Streamer: # pylint: disable=too-many-instance-attributes get_logger(0).info("Installing SIGUSR2 streamer handler ...") asyncio.get_event_loop().add_signal_handler(signal.SIGUSR2, signal_handler) - waiter_task: (asyncio.Task | None) = None - prev_state: dict = {} + prev: dict = {} while True: - state = await self.get_state() - if state != prev_state: - yield state - prev_state = state - - if waiter_task is None: - waiter_task = asyncio.create_task(self.__notifier.wait()) - if waiter_task in (await aiotools.wait_first( - asyncio.ensure_future(asyncio.sleep(self.__state_poll)), - waiter_task, - ))[0]: - waiter_task = None + if (await self.__notifier.wait(timeout=self.__state_poll)) > 0: + prev = {} + new = await self.get_state() + if new != prev: + prev = copy.deepcopy(new) + yield new # ===== diff --git a/kvmd/apps/kvmd/ugpio.py b/kvmd/apps/kvmd/ugpio.py index d893d2a0..11c60777 100644 --- a/kvmd/apps/kvmd/ugpio.py +++ b/kvmd/apps/kvmd/ugpio.py @@ -234,7 +234,6 @@ class _GpioOutput: # pylint: disable=too-many-instance-attributes class UserGpio: def __init__(self, config: Section, otg_config: Section) -> None: self.__notifier = aiotools.AioNotifier() - self.__full_state_requested = True self.__drivers = { driver: get_ugpio_driver_class(drv_config.type)( @@ -269,14 +268,12 @@ class UserGpio: } async def trigger_state(self) -> None: - self.__full_state_requested = True - self.__notifier.notify() + self.__notifier.notify(1) async def poll_state(self) -> AsyncGenerator[dict, None]: prev: dict = {"inputs": {}, "outputs": {}} while True: # pylint: disable=too-many-nested-blocks - if self.__full_state_requested: - self.__full_state_requested = False + if (await self.__notifier.wait()) > 0: full = await self.get_state() prev = copy.deepcopy(full["state"]) yield full @@ -285,14 +282,13 @@ class UserGpio: diff: dict = {} for sub in ["inputs", "outputs"]: for ch in new[sub]: - if new[sub][ch] != prev[sub][ch]: + if new[sub][ch] != prev[sub].get(ch): if sub not in diff: diff[sub] = {} diff[sub][ch] = new[sub][ch] if diff: prev = copy.deepcopy(new) yield {"state": diff} - await self.__notifier.wait() async def __get_io_state(self) -> dict: return { diff --git a/kvmd/apps/vnc/server.py b/kvmd/apps/vnc/server.py index f8e97050..a6e89311 100644 --- a/kvmd/apps/vnc/server.py +++ b/kvmd/apps/vnc/server.py @@ -175,17 +175,18 @@ class _Client(RfbClient): # pylint: disable=too-many-instance-attributes self.__kvmd_ws = None async def __process_ws_event(self, event_type: str, event: dict) -> None: - if event_type == "info_meta_state": - try: - host = event["server"]["host"] - except Exception: - host = None - else: - if isinstance(host, str): - name = f"PiKVM: {host}" - if self._encodings.has_rename: - await self._send_rename(name) - self.__shared_params.name = name + if event_type == "info_state": + if "meta" in event: + try: + host = event["meta"]["server"]["host"] + except Exception: + host = None + else: + if isinstance(host, str): + name = f"PiKVM: {host}" + if self._encodings.has_rename: + await self._send_rename(name) + self.__shared_params.name = name elif event_type == "hid_state": if self._encodings.has_leds_state: diff --git a/kvmd/clients/kvmd.py b/kvmd/clients/kvmd.py index ff8581ef..ddf0d024 100644 --- a/kvmd/clients/kvmd.py +++ b/kvmd/clients/kvmd.py @@ -222,7 +222,7 @@ class KvmdClientSession: @contextlib.asynccontextmanager async def ws(self) -> AsyncGenerator[KvmdClientWs, None]: session = self.__ensure_http_session() - async with session.ws_connect(self.__make_url("ws")) as ws: + async with session.ws_connect(self.__make_url("ws"), params={"legacy": 0}) as ws: yield KvmdClientWs(ws) def __ensure_http_session(self) -> aiohttp.ClientSession: @@ -267,16 +267,15 @@ class KvmdClient: ) def __make_http_session(self, user: str, passwd: str) -> aiohttp.ClientSession: - kwargs: dict = { - "headers": { + return aiohttp.ClientSession( + headers={ "X-KVMD-User": user, "X-KVMD-Passwd": passwd, "User-Agent": self.__user_agent, }, - "connector": aiohttp.UnixConnector(path=self.__unix_path), - "timeout": aiohttp.ClientTimeout(total=self.__timeout), - } - return aiohttp.ClientSession(**kwargs) + connector=aiohttp.UnixConnector(path=self.__unix_path), + timeout=aiohttp.ClientTimeout(total=self.__timeout), + ) def __make_url(self, handle: str) -> str: assert not handle.startswith("/"), handle diff --git a/kvmd/plugins/atx/__init__.py b/kvmd/plugins/atx/__init__.py index e9496a65..7545b030 100644 --- a/kvmd/plugins/atx/__init__.py +++ b/kvmd/plugins/atx/__init__.py @@ -48,6 +48,9 @@ class BaseAtx(BasePlugin): async def get_state(self) -> dict: raise NotImplementedError + async def trigger_state(self) -> None: + raise NotImplementedError + async def poll_state(self) -> AsyncGenerator[dict, None]: yield {} raise NotImplementedError diff --git a/kvmd/plugins/atx/disabled.py b/kvmd/plugins/atx/disabled.py index d9abec00..60c2fa5b 100644 --- a/kvmd/plugins/atx/disabled.py +++ b/kvmd/plugins/atx/disabled.py @@ -36,6 +36,9 @@ class AtxDisabledError(AtxOperationError): # ===== class Plugin(BaseAtx): + def __init__(self) -> None: + self.__notifier = aiotools.AioNotifier() + async def get_state(self) -> dict: return { "enabled": False, @@ -46,10 +49,13 @@ class Plugin(BaseAtx): }, } + async def trigger_state(self) -> None: + self.__notifier.notify() + async def poll_state(self) -> AsyncGenerator[dict, None]: while True: + await self.__notifier.wait() yield (await self.get_state()) - await aiotools.wait_infinite() # ===== diff --git a/kvmd/plugins/atx/gpio.py b/kvmd/plugins/atx/gpio.py index 538aafaf..e42b3959 100644 --- a/kvmd/plugins/atx/gpio.py +++ b/kvmd/plugins/atx/gpio.py @@ -21,6 +21,7 @@ import asyncio +import copy from typing import AsyncGenerator @@ -130,14 +131,18 @@ class Plugin(BaseAtx): # pylint: disable=too-many-instance-attributes }, } + async def trigger_state(self) -> None: + self.__notifier.notify(1) + async def poll_state(self) -> AsyncGenerator[dict, None]: - prev_state: dict = {} + prev: dict = {} while True: - state = await self.get_state() - if state != prev_state: - yield state - prev_state = state - await self.__notifier.wait() + if (await self.__notifier.wait()) > 0: + prev = {} + new = await self.get_state() + if new != prev: + prev = copy.deepcopy(new) + yield new async def systask(self) -> None: await self.__reader.poll() diff --git a/kvmd/plugins/hid/__init__.py b/kvmd/plugins/hid/__init__.py index 1c2efeec..3cba01f4 100644 --- a/kvmd/plugins/hid/__init__.py +++ b/kvmd/plugins/hid/__init__.py @@ -63,6 +63,9 @@ class BaseHid(BasePlugin): async def get_state(self) -> dict: raise NotImplementedError + async def trigger_state(self) -> None: + raise NotImplementedError + async def poll_state(self) -> AsyncGenerator[dict, None]: yield {} raise NotImplementedError diff --git a/kvmd/plugins/hid/_mcu/__init__.py b/kvmd/plugins/hid/_mcu/__init__.py index 53665fb2..e058fd6c 100644 --- a/kvmd/plugins/hid/_mcu/__init__.py +++ b/kvmd/plugins/hid/_mcu/__init__.py @@ -23,6 +23,7 @@ import multiprocessing import contextlib import queue +import copy import time from typing import Iterable @@ -232,14 +233,18 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many- **self._get_jiggler_state(), } + async def trigger_state(self) -> None: + self.__notifier.notify(1) + async def poll_state(self) -> AsyncGenerator[dict, None]: - prev_state: dict = {} + prev: dict = {} while True: - state = await self.get_state() - if state != prev_state: - yield state - prev_state = state - await self.__notifier.wait() + if (await self.__notifier.wait()) > 0: + prev = {} + new = await self.get_state() + if new != prev: + prev = copy.deepcopy(new) + yield new async def reset(self) -> None: self.__reset_required_event.set() diff --git a/kvmd/plugins/hid/bt/__init__.py b/kvmd/plugins/hid/bt/__init__.py index ece6b59b..bca8f9a5 100644 --- a/kvmd/plugins/hid/bt/__init__.py +++ b/kvmd/plugins/hid/bt/__init__.py @@ -21,6 +21,7 @@ import multiprocessing +import copy import time from typing import Iterable @@ -158,14 +159,18 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes **self._get_jiggler_state(), } + async def trigger_state(self) -> None: + self.__notifier.notify(1) + async def poll_state(self) -> AsyncGenerator[dict, None]: - prev_state: dict = {} + prev: dict = {} while True: - state = await self.get_state() - if state != prev_state: - yield state - prev_state = state - await self.__notifier.wait() + if (await self.__notifier.wait()) > 0: + prev = {} + new = await self.get_state() + if new != prev: + prev = copy.deepcopy(new) + yield new async def reset(self) -> None: self.clear_events() diff --git a/kvmd/plugins/hid/ch9329/__init__.py b/kvmd/plugins/hid/ch9329/__init__.py index 3245505d..f93be95c 100644 --- a/kvmd/plugins/hid/ch9329/__init__.py +++ b/kvmd/plugins/hid/ch9329/__init__.py @@ -22,6 +22,7 @@ import multiprocessing import queue +import copy import time from typing import Iterable @@ -119,14 +120,18 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst **self._get_jiggler_state(), } + async def trigger_state(self) -> None: + self.__notifier.notify(1) + async def poll_state(self) -> AsyncGenerator[dict, None]: - prev_state: dict = {} + prev: dict = {} while True: - state = await self.get_state() - if state != prev_state: - yield state - prev_state = state - await self.__notifier.wait() + if (await self.__notifier.wait()) > 0: + prev = {} + new = await self.get_state() + if new != prev: + prev = copy.deepcopy(new) + yield new async def reset(self) -> None: self.__reset_required_event.set() diff --git a/kvmd/plugins/hid/otg/__init__.py b/kvmd/plugins/hid/otg/__init__.py index 3516546d..7686ebdd 100644 --- a/kvmd/plugins/hid/otg/__init__.py +++ b/kvmd/plugins/hid/otg/__init__.py @@ -20,6 +20,8 @@ # ========================================================================== # +import copy + from typing import Iterable from typing import AsyncGenerator from typing import Any @@ -150,14 +152,18 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes **self._get_jiggler_state(), } + async def trigger_state(self) -> None: + self.__notifier.notify(1) + async def poll_state(self) -> AsyncGenerator[dict, None]: - prev_state: dict = {} + prev: dict = {} while True: - state = await self.get_state() - if state != prev_state: - yield state - prev_state = state - await self.__notifier.wait() + if (await self.__notifier.wait()) > 0: + prev = {} + new = await self.get_state() + if new != prev: + prev = copy.deepcopy(new) + yield new async def reset(self) -> None: self.__keyboard_proc.send_reset_event() diff --git a/kvmd/plugins/msd/__init__.py b/kvmd/plugins/msd/__init__.py index e0210921..11cd8be5 100644 --- a/kvmd/plugins/msd/__init__.py +++ b/kvmd/plugins/msd/__init__.py @@ -117,6 +117,9 @@ class BaseMsd(BasePlugin): async def get_state(self) -> dict: raise NotImplementedError() + async def trigger_state(self) -> None: + raise NotImplementedError() + async def poll_state(self) -> AsyncGenerator[dict, None]: if self is not None: # XXX: Vulture and pylint hack raise NotImplementedError() diff --git a/kvmd/plugins/msd/disabled.py b/kvmd/plugins/msd/disabled.py index ad49875e..b9f14f6e 100644 --- a/kvmd/plugins/msd/disabled.py +++ b/kvmd/plugins/msd/disabled.py @@ -40,6 +40,9 @@ class MsdDisabledError(MsdOperationError): # ===== class Plugin(BaseMsd): + def __init__(self) -> None: + self.__notifier = aiotools.AioNotifier() + async def get_state(self) -> dict: return { "enabled": False, @@ -49,10 +52,13 @@ class Plugin(BaseMsd): "drive": None, } + async def trigger_state(self) -> None: + self.__notifier.notify() + async def poll_state(self) -> AsyncGenerator[dict, None]: while True: + await self.__notifier.wait() yield (await self.get_state()) - await aiotools.wait_infinite() async def reset(self) -> None: raise MsdDisabledError() diff --git a/kvmd/plugins/msd/otg/__init__.py b/kvmd/plugins/msd/otg/__init__.py index b652cdd0..0bb9f489 100644 --- a/kvmd/plugins/msd/otg/__init__.py +++ b/kvmd/plugins/msd/otg/__init__.py @@ -24,6 +24,7 @@ import asyncio import contextlib import dataclasses import functools +import copy import time from typing import AsyncGenerator @@ -195,14 +196,18 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes "drive": vd, } + async def trigger_state(self) -> None: + self.__notifier.notify(1) + async def poll_state(self) -> AsyncGenerator[dict, None]: - prev_state: dict = {} + prev: dict = {} while True: - state = await self.get_state() - if state != prev_state: - yield state - prev_state = state - await self.__notifier.wait() + if (await self.__notifier.wait()) > 0: + prev = {} + new = await self.get_state() + if new != prev: + prev = copy.deepcopy(new) + yield new async def systask(self) -> None: await self.__watch_inotify() diff --git a/web/share/js/kvm/session.js b/web/share/js/kvm/session.js index 353815da..22cefe89 100644 --- a/web/share/js/kvm/session.js +++ b/web/share/js/kvm/session.js @@ -62,9 +62,21 @@ export function Session() { /************************************************************************/ - var __setAboutInfoMeta = function(state) { + var __setInfoState = function(state) { + for (let key of Object.keys(state)) { + switch (key) { + case "meta": __setInfoStateMeta(state.meta); break; + case "hw": __setInfoStateHw(state.hw); break; + case "fan": __setInfoStateFan(state.fan); break; + case "system": __setInfoStateSystem(state.system); break; + case "extras": __setInfoStateExtras(state.extras); break; + } + } + }; + + var __setInfoStateMeta = function(state) { if (state !== null) { - let text = JSON.stringify(state, undefined, 4).replace(/ /g, " ").replace(/\n/g, "<br>"); + let text = tools.escape(JSON.stringify(state, undefined, 4)).replace(/ /g, " ").replace(/\n/g, "<br>"); $("about-meta").innerHTML = ` <span class="code-comment">// The PiKVM metadata.<br> // You can get this JSON using handle <a target="_blank" href="/api/info?fields=meta">/api/info?fields=meta</a>.<br> @@ -74,10 +86,10 @@ export function Session() { ${text} `; if (state.server && state.server.host) { - $("kvmd-meta-server-host").innerHTML = `Server: ${state.server.host}`; + $("kvmd-meta-server-host").innerText = `Server: ${state.server.host}`; document.title = `PiKVM Session: ${state.server.host}`; } else { - $("kvmd-meta-server-host").innerHTML = ""; + $("kvmd-meta-server-host").innerText = ""; document.title = "PiKVM Session"; } @@ -88,7 +100,7 @@ export function Session() { } }; - var __setAboutInfoHw = function(state) { + var __setInfoStateHw = function(state) { if (state.health.throttling !== null) { let flags = state.health.throttling.parsed_flags; let ignore_past = state.health.throttling.ignore_past; @@ -105,7 +117,7 @@ export function Session() { __renderAboutInfoHardware(); }; - var __setAboutInfoFan = function(state) { + var __setInfoStateFan = function(state) { let failed = false; let failed_past = false; if (state.monitored) { @@ -207,11 +219,11 @@ export function Session() { } }; - var __colored = function(color, text) { - return `<font color="${color}">${text}</font>`; + var __colored = function(color, html) { + return `<font color="${color}">${html}</font>`; }; - var __setAboutInfoSystem = function(state) { + var __setInfoStateSystem = function(state) { $("about-version").innerHTML = ` KVMD: <span class="code-comment">${state.kvmd.version}</span><br> <hr> @@ -221,8 +233,8 @@ export function Session() { ${state.kernel.system} kernel: ${__formatUname(state.kernel)} `; - $("kvmd-version-kvmd").innerHTML = state.kvmd.version; - $("kvmd-version-streamer").innerHTML = state.streamer.version; + $("kvmd-version-kvmd").innerText = state.kvmd.version; + $("kvmd-version-streamer").innerText = state.streamer.version; }; var __formatStreamerFeatures = function(features) { @@ -244,14 +256,14 @@ export function Session() { }; var __formatUl = function(pairs) { - let text = "<ul>"; + let html = ""; for (let pair of pairs) { - text += `<li>${pair[0]}: <span class="code-comment">${pair[1]}</span></li>`; + html += `<li>${pair[0]}: <span class="code-comment">${pair[1]}</span></li>`; } - return text + "</ul>"; + return `<ul>${html}</ul>`; }; - var __setExtras = function(state) { + var __setInfoStateExtras = function(state) { let show_hook = null; let close_hook = null; let has_webterm = (state.webterm && (state.webterm.enabled || state.webterm.started)); @@ -354,11 +366,7 @@ export function Session() { let data = JSON.parse(event.data); switch (data.event_type) { case "pong": __missed_heartbeats = 0; break; - case "info_meta_state": __setAboutInfoMeta(data.event); break; - case "info_hw_state": __setAboutInfoHw(data.event); break; - case "info_fan_state": __setAboutInfoFan(data.event); break; - case "info_system_state": __setAboutInfoSystem(data.event); break; - case "info_extras_state": __setExtras(data.event); break; + case "info_state": __setInfoState(data.event); break; case "gpio_state": __gpio.setState(data.event); break; case "hid_keymaps_state": __hid.setKeymaps(data.event); break; case "hid_state": __hid.setState(data.event); break; |