From abedace4b3173fca98eee1ac0b778a45dc93c5b5 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Mon, 19 Aug 2024 00:43:32 +0300 Subject: [PATCH 01/88] enable v4p by default --- PKGBUILD | 2 +- configs/kvmd/main/v4plus-hdmi-rpi4.yaml | 6 ++++-- configs/os/services/kvmd-pass.service | 15 --------------- configs/os/services/kvmd-tc358743.service | 2 +- kvmd.install | 4 ++++ scripts/kvmd-udev-restart-pass | 3 --- 6 files changed, 10 insertions(+), 22 deletions(-) delete mode 100644 configs/os/services/kvmd-pass.service diff --git a/PKGBUILD b/PKGBUILD index 25873e5b..9998f276 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -206,7 +206,7 @@ for _variant in "${_variants[@]}"; do cd \"kvmd-\$pkgver\" pkgdesc=\"PiKVM platform configs - $_platform for $_board\" - depends=(kvmd=$pkgver-$pkgrel \"linux-rpi-pikvm>=6.6.21-3\") + depends=(kvmd=$pkgver-$pkgrel \"linux-rpi-pikvm>=6.6.45-1\" \"raspberrypi-bootloader-pikvm>=20240818-1\") backup=( etc/sysctl.d/99-kvmd.conf diff --git a/configs/kvmd/main/v4plus-hdmi-rpi4.yaml b/configs/kvmd/main/v4plus-hdmi-rpi4.yaml index c1e6faff..c59be781 100644 --- a/configs/kvmd/main/v4plus-hdmi-rpi4.yaml +++ b/configs/kvmd/main/v4plus-hdmi-rpi4.yaml @@ -31,6 +31,7 @@ kvmd: type: otg streamer: + forever: true h264_bitrate: default: 5000 cmd: @@ -38,10 +39,10 @@ kvmd: - "--device=/dev/kvmd-video" - "--persistent" - "--dv-timings" - - "--format=uyvy" + - "--format=rgb24" - "--format-swap-rgb" - "--buffers=8" - - "--encoder=m2m-image" + - "--encoder=cpu" - "--workers=3" - "--quality={quality}" - "--desired-fps={desired_fps}" @@ -59,6 +60,7 @@ kvmd: - "--h264-sink-mode=0660" - "--h264-bitrate={h264_bitrate}" - "--h264-gop={h264_gop}" + - "--v4p" gpio: drivers: diff --git a/configs/os/services/kvmd-pass.service b/configs/os/services/kvmd-pass.service deleted file mode 100644 index 5d55b5fe..00000000 --- a/configs/os/services/kvmd-pass.service +++ /dev/null @@ -1,15 +0,0 @@ -[Unit] -Description=PiKVM - Video Passthrough on V4 Plus -Wants=dev-kvmd\x2dvideo.device -After=dev-kvmd\x2dvideo.device systemd-modules-load.service - -[Service] -Type=simple -Restart=always -RestartSec=3 - -ExecStart=/usr/bin/ustreamer-v4p --unix-follow /run/kvmd/ustreamer.sock -TimeoutStopSec=10 - -[Install] -WantedBy=multi-user.target diff --git a/configs/os/services/kvmd-tc358743.service b/configs/os/services/kvmd-tc358743.service index c8b8bbf9..c4f13300 100644 --- a/configs/os/services/kvmd-tc358743.service +++ b/configs/os/services/kvmd-tc358743.service @@ -2,7 +2,7 @@ Description=PiKVM - EDID loader for TC358743 Wants=dev-kvmd\x2dvideo.device After=dev-kvmd\x2dvideo.device systemd-modules-load.service -Before=kvmd.service kvmd-pass.service +Before=kvmd.service [Service] Type=oneshot diff --git a/kvmd.install b/kvmd.install index 55bd999d..ca0593f7 100644 --- a/kvmd.install +++ b/kvmd.install @@ -92,6 +92,10 @@ disable_overscan=1 EOF fi + if [[ "$(vercmp "$2" 4.4)" -lt 0 ]]; then + systemctl disable kvmd-pass || true + fi + # Some update deletes /etc/motd, WTF # shellcheck disable=SC2015,SC2166 [ ! -f /etc/motd -a -f /etc/motd.pacsave ] && mv /etc/motd.pacsave /etc/motd || true diff --git a/scripts/kvmd-udev-restart-pass b/scripts/kvmd-udev-restart-pass index 760267eb..4b77897a 100755 --- a/scripts/kvmd-udev-restart-pass +++ b/scripts/kvmd-udev-restart-pass @@ -42,9 +42,6 @@ test -n "$port" if [ "$port" = "HDMI-A-1" ]; then status=$(head -n 1 "/sys/class/drm/$card-$port/status") if [ "$status" = "connected" ]; then - if systemctl is-enabled -q kvmd-pass; then - systemctl restart kvmd-pass || true - fi for pid in $(pgrep -f '^kvmd/streamer: ' || true); do kill "$pid" || true done From c9405efa0535fda9ab6693ea93a477dbded28069 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Mon, 19 Aug 2024 01:06:00 +0300 Subject: [PATCH 02/88] lint fix --- kvmd/apps/kvmd/streamer.py | 2 +- kvmd/htclient.py | 2 +- kvmd/plugins/auth/http.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/kvmd/apps/kvmd/streamer.py b/kvmd/apps/kvmd/streamer.py index 26904d40..e5c406d9 100644 --- a/kvmd/apps/kvmd/streamer.py +++ b/kvmd/apps/kvmd/streamer.py @@ -357,7 +357,7 @@ class Streamer: # pylint: disable=too-many-instance-attributes try: async with session.get( self.__make_url("snapshot"), - timeout=self.__snapshot_timeout, + timeout=aiohttp.ClientTimeout(total=self.__snapshot_timeout), ) as response: htclient.raise_not_200(response) diff --git a/kvmd/htclient.py b/kvmd/htclient.py index 724eddcd..9d9ae82c 100644 --- a/kvmd/htclient.py +++ b/kvmd/htclient.py @@ -79,6 +79,6 @@ async def download( ), } async with aiohttp.ClientSession(**kwargs) as session: - async with session.get(url, verify_ssl=verify) as response: + async with session.get(url, verify_ssl=verify) as response: # type: ignore raise_not_200(response) yield response diff --git a/kvmd/plugins/auth/http.py b/kvmd/plugins/auth/http.py index 54308279..520f64dc 100644 --- a/kvmd/plugins/auth/http.py +++ b/kvmd/plugins/auth/http.py @@ -75,7 +75,7 @@ class Plugin(BaseAuthService): async with session.request( method="POST", url=self.__url, - timeout=self.__timeout, + timeout=aiohttp.ClientTimeout(total=self.__timeout), json={ "user": user, "passwd": passwd, From 06b69d3dde2ffe8d3236aff18c6bc4f00ae005de Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Mon, 19 Aug 2024 01:06:34 +0300 Subject: [PATCH 03/88] =?UTF-8?q?Bump=20version:=204.3=20=E2=86=92=204.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- PKGBUILD | 2 +- kvmd/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 89b70044..9190745b 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 4.3 +current_version = 4.4 parse = (?P\d+)\.(?P\d+)(\.(?P\d+)(\-(?P[a-z]+))?)? serialize = {major}.{minor} diff --git a/PKGBUILD b/PKGBUILD index 9998f276..fdfd38e3 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -39,7 +39,7 @@ for _variant in "${_variants[@]}"; do pkgname+=(kvmd-platform-$_platform-$_board) done pkgbase=kvmd -pkgver=4.3 +pkgver=4.4 pkgrel=1 pkgdesc="The main PiKVM daemon" url="https://github.com/pikvm/kvmd" diff --git a/kvmd/__init__.py b/kvmd/__init__.py index 11f3622d..13502b25 100644 --- a/kvmd/__init__.py +++ b/kvmd/__init__.py @@ -20,4 +20,4 @@ # ========================================================================== # -__version__ = "4.3" +__version__ = "4.4" diff --git a/setup.py b/setup.py index 61863d72..f44d5230 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def main() -> None: setup( name="kvmd", - version="4.3", + version="4.4", url="https://github.com/pikvm/kvmd", license="GPLv3", author="Maxim Devaev", From 39422f37ac421c94a010400f741fb8ad4c73c64f Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 20 Aug 2024 05:43:47 +0300 Subject: [PATCH 04/88] sticky pst --- configs/os/sysusers.conf | 1 + kvmd.install | 8 +++++++- kvmd/fstab.py | 4 +++- kvmd/helpers/remount/__init__.py | 28 +++++++++++++++++++++++++++- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/configs/os/sysusers.conf b/configs/os/sysusers.conf index 74ab9069..0359974d 100644 --- a/configs/os/sysusers.conf +++ b/configs/os/sysusers.conf @@ -19,6 +19,7 @@ m kvmd gpio m kvmd uucp m kvmd spi m kvmd systemd-journal +m kvmd kvmd-pst m kvmd-pst kvmd diff --git a/kvmd.install b/kvmd.install index ca0593f7..469fba8c 100644 --- a/kvmd.install +++ b/kvmd.install @@ -27,7 +27,8 @@ post_upgrade() { done chown kvmd /var/lib/kvmd/msd 2>/dev/null || true - chown kvmd-pst /var/lib/kvmd/pst 2>/dev/null || true + chown kvmd-pst:kvmd-pst /var/lib/kvmd/pst 2>/dev/null || true + chmod 1775 /var/lib/kvmd/pst 2>/dev/null || true if [ ! -e /etc/kvmd/nginx/ssl/server.crt ]; then echo "==> Generating KVMD-Nginx certificate ..." @@ -96,6 +97,11 @@ EOF systemctl disable kvmd-pass || true fi + if [[ "$(vercmp "$2" 4.5)" -lt 0 ]]; then + sed -i 's/X-kvmd\.pst-user=kvmd-pst/X-kvmd.pst-user=kvmd-pst,X-kvmd.pst-group=kvmd-pst/g' /etc/fstab + touch -t 200701011000 /etc/fstab + fi + # Some update deletes /etc/motd, WTF # shellcheck disable=SC2015,SC2166 [ ! -f /etc/motd -a -f /etc/motd.pacsave ] && mv /etc/motd.pacsave /etc/motd || true diff --git a/kvmd/fstab.py b/kvmd/fstab.py index 4ab3163c..5a603d06 100644 --- a/kvmd/fstab.py +++ b/kvmd/fstab.py @@ -33,6 +33,7 @@ class Partition: mount_path: str root_path: str user: str + group: str # ===== @@ -60,12 +61,13 @@ def _find_partitions(part_type: str, single: bool) -> list[Partition]: if line and not line.startswith("#"): fields = line.split() if len(fields) == 6: - options = dict(re.findall(r"X-kvmd\.%s-(root|user)(?:=([^,]+))?" % (part_type), fields[3])) + options = dict(re.findall(r"X-kvmd\.%s-(root|user|group)(?:=([^,]+))?" % (part_type), fields[3])) if options: parts.append(Partition( mount_path=os.path.normpath(fields[1]), root_path=os.path.normpath(options.get("root", "") or fields[1]), user=options.get("user", ""), + group=options.get("group", ""), )) if single: break diff --git a/kvmd/helpers/remount/__init__.py b/kvmd/helpers/remount/__init__.py index e41bbbfd..716b9c72 100644 --- a/kvmd/helpers/remount/__init__.py +++ b/kvmd/helpers/remount/__init__.py @@ -23,6 +23,7 @@ import sys import os import pwd +import grp import shutil import subprocess @@ -87,11 +88,28 @@ def _chown(path: str, user: str) -> None: if pwd.getpwuid(os.stat(path).st_uid).pw_name != user: _log(f"CHOWN --- {user} - {path}") try: - shutil.chown(path, user) + shutil.chown(path, user=user) except Exception as err: raise SystemExit(f"Can't change ownership: {err}") +def _chgrp(path: str, group: str) -> None: + if grp.getgrgid(os.stat(path).st_gid).gr_name != group: + _log(f"CHGRP --- {group} - {path}") + try: + shutil.chown(path, group=group) + except Exception as err: + raise SystemExit(f"Can't change group: {err}") + + +def _chmod(path: str, mode: int) -> None: + _log(f"CHMOD --- 0o{mode:o} - {path}") + try: + os.chmod(path, mode) + except Exception as err: + raise SystemExit(f"Can't change permissions: {err}") + + # ===== def _fix_msd(part: Partition) -> None: # First images migration @@ -112,13 +130,21 @@ def _fix_msd(part: Partition) -> None: if part.user: _chown(part.root_path, part.user) + if part.group: + _chgrp(part.root_path, part.group) def _fix_pst(part: Partition) -> None: path = os.path.join(part.root_path, "data") _mkdir(path) if part.user: + _chown(part.root_path, part.user) _chown(path, part.user) + if part.group: + _chown(part.root_path, part.group) + _chgrp(path, part.group) + if part.user and part.group: + _chmod(part.root_path, 0o1775) # ===== From a55948bf8e381eebedf4206f70969805d2dc805b Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 20 Aug 2024 05:45:00 +0300 Subject: [PATCH 05/88] =?UTF-8?q?Bump=20version:=204.4=20=E2=86=92=204.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- PKGBUILD | 2 +- kvmd/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 9190745b..c1682f23 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 4.4 +current_version = 4.5 parse = (?P\d+)\.(?P\d+)(\.(?P\d+)(\-(?P[a-z]+))?)? serialize = {major}.{minor} diff --git a/PKGBUILD b/PKGBUILD index fdfd38e3..932fbdff 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -39,7 +39,7 @@ for _variant in "${_variants[@]}"; do pkgname+=(kvmd-platform-$_platform-$_board) done pkgbase=kvmd -pkgver=4.4 +pkgver=4.5 pkgrel=1 pkgdesc="The main PiKVM daemon" url="https://github.com/pikvm/kvmd" diff --git a/kvmd/__init__.py b/kvmd/__init__.py index 13502b25..1a9a8cc5 100644 --- a/kvmd/__init__.py +++ b/kvmd/__init__.py @@ -20,4 +20,4 @@ # ========================================================================== # -__version__ = "4.4" +__version__ = "4.5" diff --git a/setup.py b/setup.py index f44d5230..a3502e75 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def main() -> None: setup( name="kvmd", - version="4.4", + version="4.5", url="https://github.com/pikvm/kvmd", license="GPLv3", author="Maxim Devaev", From 721a80ef03ffe43c832f1942d057c6250472c149 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 20 Aug 2024 07:13:52 +0300 Subject: [PATCH 06/88] fixed pst chgrp and chmod --- kvmd/helpers/remount/__init__.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/kvmd/helpers/remount/__init__.py b/kvmd/helpers/remount/__init__.py index 716b9c72..b8e71e4f 100644 --- a/kvmd/helpers/remount/__init__.py +++ b/kvmd/helpers/remount/__init__.py @@ -22,6 +22,7 @@ import sys import os +import stat import pwd import grp import shutil @@ -103,11 +104,12 @@ def _chgrp(path: str, group: str) -> None: def _chmod(path: str, mode: int) -> None: - _log(f"CHMOD --- 0o{mode:o} - {path}") - try: - os.chmod(path, mode) - except Exception as err: - raise SystemExit(f"Can't change permissions: {err}") + if stat.S_IMODE(os.stat(path).st_mode) != mode: + _log(f"CHMOD --- 0o{mode:o} - {path}") + try: + os.chmod(path, mode) + except Exception as err: + raise SystemExit(f"Can't change permissions: {err}") # ===== @@ -141,7 +143,7 @@ def _fix_pst(part: Partition) -> None: _chown(part.root_path, part.user) _chown(path, part.user) if part.group: - _chown(part.root_path, part.group) + _chgrp(part.root_path, part.group) _chgrp(path, part.group) if part.user and part.group: _chmod(part.root_path, 0o1775) From e6b775089f78f0afc2271deefdec2d43da0a8610 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 20 Aug 2024 07:15:03 +0300 Subject: [PATCH 07/88] =?UTF-8?q?Bump=20version:=204.5=20=E2=86=92=204.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- PKGBUILD | 2 +- kvmd/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index c1682f23..d13892d4 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 4.5 +current_version = 4.6 parse = (?P\d+)\.(?P\d+)(\.(?P\d+)(\-(?P[a-z]+))?)? serialize = {major}.{minor} diff --git a/PKGBUILD b/PKGBUILD index 932fbdff..a5ec59f1 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -39,7 +39,7 @@ for _variant in "${_variants[@]}"; do pkgname+=(kvmd-platform-$_platform-$_board) done pkgbase=kvmd -pkgver=4.5 +pkgver=4.6 pkgrel=1 pkgdesc="The main PiKVM daemon" url="https://github.com/pikvm/kvmd" diff --git a/kvmd/__init__.py b/kvmd/__init__.py index 1a9a8cc5..4c23e3ef 100644 --- a/kvmd/__init__.py +++ b/kvmd/__init__.py @@ -20,4 +20,4 @@ # ========================================================================== # -__version__ = "4.5" +__version__ = "4.6" diff --git a/setup.py b/setup.py index a3502e75..1482ea23 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def main() -> None: setup( name="kvmd", - version="4.5", + version="4.6", url="https://github.com/pikvm/kvmd", license="GPLv3", author="Maxim Devaev", From 4772c2b6c3f2b490d6f635112431f6395579daf2 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sat, 24 Aug 2024 23:05:49 +0300 Subject: [PATCH 08/88] Since 1.28.1, v4l2-ctl deprecated --fix-edid-checksums and made thid behaviour default --- configs/os/services/kvmd-tc358743.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configs/os/services/kvmd-tc358743.service b/configs/os/services/kvmd-tc358743.service index c4f13300..494da726 100644 --- a/configs/os/services/kvmd-tc358743.service +++ b/configs/os/services/kvmd-tc358743.service @@ -6,7 +6,7 @@ Before=kvmd.service [Service] Type=oneshot -ExecStart=/usr/bin/v4l2-ctl --device=/dev/kvmd-video --set-edid=file=/etc/kvmd/tc358743-edid.hex --fix-edid-checksums --info-edid +ExecStart=/usr/bin/v4l2-ctl --device=/dev/kvmd-video --set-edid=file=/etc/kvmd/tc358743-edid.hex --info-edid ExecStop=/usr/bin/v4l2-ctl --device=/dev/kvmd-video --clear-edid RemainAfterExit=true From 8569ed406a0d5f1c5da100fb58c49208fd6e1887 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sat, 24 Aug 2024 23:07:05 +0300 Subject: [PATCH 09/88] =?UTF-8?q?Bump=20version:=204.6=20=E2=86=92=204.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- PKGBUILD | 2 +- kvmd/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index d13892d4..5a13ea08 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 4.6 +current_version = 4.7 parse = (?P\d+)\.(?P\d+)(\.(?P\d+)(\-(?P[a-z]+))?)? serialize = {major}.{minor} diff --git a/PKGBUILD b/PKGBUILD index a5ec59f1..0046f0c4 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -39,7 +39,7 @@ for _variant in "${_variants[@]}"; do pkgname+=(kvmd-platform-$_platform-$_board) done pkgbase=kvmd -pkgver=4.6 +pkgver=4.7 pkgrel=1 pkgdesc="The main PiKVM daemon" url="https://github.com/pikvm/kvmd" diff --git a/kvmd/__init__.py b/kvmd/__init__.py index 4c23e3ef..cdc07a33 100644 --- a/kvmd/__init__.py +++ b/kvmd/__init__.py @@ -20,4 +20,4 @@ # ========================================================================== # -__version__ = "4.6" +__version__ = "4.7" diff --git a/setup.py b/setup.py index 1482ea23..8b816d08 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def main() -> None: setup( name="kvmd", - version="4.6", + version="4.7", url="https://github.com/pikvm/kvmd", license="GPLv3", author="Maxim Devaev", From 3837e1a1c8d64cccb12c4811a3975949ee904327 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sun, 25 Aug 2024 01:24:12 +0300 Subject: [PATCH 10/88] Simplified inotify API --- kvmd/inotify.py | 15 +++++++++++++++ kvmd/plugins/msd/otg/__init__.py | 10 +++++----- kvmd/plugins/ugpio/otgconf.py | 7 +++---- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/kvmd/inotify.py b/kvmd/inotify.py index 700247ef..c70ec465 100644 --- a/kvmd/inotify.py +++ b/kvmd/inotify.py @@ -142,6 +142,14 @@ class InotifyMask: | MOVED_TO ) + # Helper for typicals events when we need to restart watcher + ALL_RESTART_EVENTS = ( + DELETE_SELF + | MOVE_SELF + | UNMOUNT + | ISDIR + ) + # Special flags for watch() # DONT_FOLLOW = 0x02000000 # Don't follow a symbolic link # EXCL_UNLINK = 0x04000000 # Exclude events on unlinked objects @@ -172,6 +180,10 @@ class InotifyEvent: name: str path: str + @property + def restart(self) -> bool: + return bool(self.mask & InotifyMask.ALL_RESTART_EVENTS) + def __repr__(self) -> str: return ( f" None: + await self.watch(InotifyMask.ALL_MODIFY_EVENTS, *paths) + async def watch(self, mask: int, *paths: str) -> None: for path in paths: path = os.path.normpath(path) diff --git a/kvmd/plugins/msd/otg/__init__.py b/kvmd/plugins/msd/otg/__init__.py index 18f2a006..b652cdd0 100644 --- a/kvmd/plugins/msd/otg/__init__.py +++ b/kvmd/plugins/msd/otg/__init__.py @@ -30,7 +30,6 @@ from typing import AsyncGenerator from ....logging import get_logger -from ....inotify import InotifyMask from ....inotify import Inotify from ....yamlconf import Option @@ -415,8 +414,8 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes await asyncio.sleep(5) with Inotify() as inotify: - await inotify.watch(InotifyMask.ALL_MODIFY_EVENTS, *self.__storage.get_watchable_paths()) - await inotify.watch(InotifyMask.ALL_MODIFY_EVENTS, *self.__drive.get_watchable_paths()) + await inotify.watch_all_modify(*self.__storage.get_watchable_paths()) + await inotify.watch_all_modify(*self.__drive.get_watchable_paths()) # После установки вотчеров еще раз проверяем стейт, чтобы ничего не потерять await self.__reload_state() @@ -426,8 +425,9 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes need_reload_state = False for event in (await inotify.get_series(timeout=1)): need_reload_state = True - if event.mask & (InotifyMask.DELETE_SELF | InotifyMask.MOVE_SELF | InotifyMask.UNMOUNT | InotifyMask.ISDIR): - # Если выгрузили OTG, изменили каталоги, что-то отмонтировали или делают еще какую-то странную фигню + if event.restart: + # Если выгрузили OTG, изменили каталоги, что-то отмонтировали или делают еще какую-то странную фигню. + # Проверяется маска InotifyMask.ALL_RESTART_EVENTS logger.info("Got a big inotify event: %s; reinitializing MSD ...", event) need_restart = True break diff --git a/kvmd/plugins/ugpio/otgconf.py b/kvmd/plugins/ugpio/otgconf.py index 4b85a3f4..dc255b02 100644 --- a/kvmd/plugins/ugpio/otgconf.py +++ b/kvmd/plugins/ugpio/otgconf.py @@ -28,7 +28,6 @@ from typing import Any from ...logging import get_logger -from ...inotify import InotifyMask from ...inotify import Inotify from ... import aiotools @@ -82,15 +81,15 @@ class Plugin(BaseUserGpioDriver): await asyncio.sleep(5) with Inotify() as inotify: - await inotify.watch(InotifyMask.ALL_MODIFY_EVENTS, os.path.dirname(self.__udc_path)) - await inotify.watch(InotifyMask.ALL_MODIFY_EVENTS, self.__profile_path) + await inotify.watch_all_modify(os.path.dirname(self.__udc_path)) + await inotify.watch_all_modify(self.__profile_path) self._notifier.notify() while True: need_restart = False need_notify = False for event in (await inotify.get_series(timeout=1)): need_notify = True - if event.mask & (InotifyMask.DELETE_SELF | InotifyMask.MOVE_SELF | InotifyMask.UNMOUNT): + if event.restart: logger.warning("Got fatal inotify event: %s; reinitializing OTG-bind ...", event) need_restart = True break From 0c213add4a8493b262a1cb508abf59a20cb4f459 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 27 Aug 2024 01:48:30 +0300 Subject: [PATCH 11/88] pst: changed data root to /var/lib/kvmd/pst --- kvmd/apps/pst/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kvmd/apps/pst/server.py b/kvmd/apps/pst/server.py index a22025fe..359faf9a 100644 --- a/kvmd/apps/pst/server.py +++ b/kvmd/apps/pst/server.py @@ -50,7 +50,7 @@ class PstServer(HttpServer): # pylint: disable=too-many-arguments,too-many-inst super().__init__() - self.__data_path = os.path.join(fstab.find_pst().root_path, "data") + self.__data_path = fstab.find_pst().root_path self.__ro_retries_delay = ro_retries_delay self.__ro_cleanup_delay = ro_cleanup_delay self.__remount_cmd = remount_cmd From 308911191a839a24272b623160bb1628b8ce1880 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 27 Aug 2024 01:48:52 +0300 Subject: [PATCH 12/88] testenv: restored eslint --- testenv/linters/eslintrc.js | 57 +++++++++++++++++++++++++++++++++++ testenv/linters/eslintrc.yaml | 36 ---------------------- testenv/tox.ini | 5 ++- 3 files changed, 59 insertions(+), 39 deletions(-) create mode 100644 testenv/linters/eslintrc.js delete mode 100644 testenv/linters/eslintrc.yaml diff --git a/testenv/linters/eslintrc.js b/testenv/linters/eslintrc.js new file mode 100644 index 00000000..f1923cb7 --- /dev/null +++ b/testenv/linters/eslintrc.js @@ -0,0 +1,57 @@ +const js = require("/usr/lib/node_modules/eslint/node_modules/@eslint/js/src/index.js"); +const globals = require("/usr/lib/node_modules/eslint/node_modules/@eslint/eslintrc/node_modules/globals/index.js"); +const parser = require("/usr/lib/node_modules/@babel/eslint-parser/lib/index.cjs"); + +module.exports = [ + js.configs.recommended, + + { + files: ["**/*.js"], + languageOptions: { + globals: globals.browser, + ecmaVersion: 2015, + parser: parser, + parserOptions: { + ecmaVersion: 2025, + sourceType: "module", + allowImportExportEverywhere: true, + requireConfigFile: false, + }, + }, + }, + + { + rules: { + indent: [ + "error", + "tab", + {SwitchCase: 1}, + ], + "linebreak-style": [ + "error", + "unix", + ], + quotes: [ + "error", + "double", + ], + "quote-props": [ + "error", + "always", + ], + "semi": [ + "error", + "always", + ], + "comma-dangle": [ + "error", + "always-multiline", + ], + "no-unused-vars": [ + "error", + {vars: "local", args: "after-used"}, + ], + }, + }, + +]; diff --git a/testenv/linters/eslintrc.yaml b/testenv/linters/eslintrc.yaml deleted file mode 100644 index 90268506..00000000 --- a/testenv/linters/eslintrc.yaml +++ /dev/null @@ -1,36 +0,0 @@ -env: - browser: true - es6: true - -extends: "eslint:recommended" - -parser: "/usr/lib/node_modules/@babel/eslint-parser" -parserOptions: - ecmaVersion: 6 - sourceType: module - allowImportExportEverywhere: true - requireConfigFile: false - -rules: - indent: - - error - - tab - - SwitchCase: 1 - linebreak-style: - - error - - unix - quotes: - - error - - double - quote-props: - - error - - always - semi: - - error - - always - comma-dangle: - - error - - always-multiline - no-unused-vars: - - error - - {vars: local, args: after-used} diff --git a/testenv/tox.ini b/testenv/tox.ini index 0d1ed571..5ca65f5d 100644 --- a/testenv/tox.ini +++ b/testenv/tox.ini @@ -1,6 +1,5 @@ [tox] -envlist = flake8, pylint, mypy, vulture, pytest, htmlhint, shellcheck -#envlist = flake8, pylint, mypy, vulture, pytest, eslint, htmlhint, shellcheck +envlist = flake8, pylint, mypy, vulture, pytest, eslint, htmlhint, shellcheck skipsdist = true [testenv] @@ -55,7 +54,7 @@ deps = [testenv:eslint] allowlist_externals = eslint -commands = eslint --cache-location=/tmp --config=testenv/linters/eslintrc.yaml --color --ext .js web/share/js +commands = eslint --cache-location=/tmp --config=testenv/linters/eslintrc.js --color web/share/js [testenv:htmlhint] allowlist_externals = htmlhint From 99fcbdda052940f450d397f3c3577be25c0fe52d Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 27 Aug 2024 01:49:17 +0300 Subject: [PATCH 13/88] lint fix --- web/share/js/login/main.js | 2 +- web/share/js/wm.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/share/js/login/main.js b/web/share/js/login/main.js index 41ed6141..4ffa276a 100644 --- a/web/share/js/login/main.js +++ b/web/share/js/login/main.js @@ -59,7 +59,7 @@ function __login() { } else { let error = ""; if (http.status === 400) { - try { error = JSON.parse(http.responseText)["result"]["error"]; } catch (_) { /* Nah */ } + try { error = JSON.parse(http.responseText)["result"]["error"]; } catch { /* Nah */ } } if (error === "ValidatorError") { wm.error("Invalid characters in credentials").then(__tryAgain); diff --git a/web/share/js/wm.js b/web/share/js/wm.js index 31c7dac0..11f65faa 100644 --- a/web/share/js/wm.js +++ b/web/share/js/wm.js @@ -165,7 +165,7 @@ function __WindowManager() { try { err = (document.execCommand("copy") ? null : "Unknown error"); - } catch (err) { // eslint-disable-line no-empty + } catch (err) { // eslint-disable-line no-unused-vars } // Remove the added textarea again: From 9dc2af0356346072981d6c570394a8e1ac03f684 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 27 Aug 2024 15:51:07 +0300 Subject: [PATCH 14/88] kvmd-edidconf: removed --fix-edid-checksums --- kvmd/apps/edidconf/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/kvmd/apps/edidconf/__init__.py b/kvmd/apps/edidconf/__init__.py index 66136cb3..aabc8a8f 100644 --- a/kvmd/apps/edidconf/__init__.py +++ b/kvmd/apps/edidconf/__init__.py @@ -400,7 +400,6 @@ def main(argv: (list[str] | None)=None) -> None: # pylint: disable=too-many-bra "/usr/bin/v4l2-ctl", f"--device={options.device_path}", f"--set-edid=file={orig_edid_path}", - "--fix-edid-checksums", "--info-edid", ], stdout=sys.stderr, check=True) except subprocess.CalledProcessError as err: From cc66fbf1dfe134f97567358b7c795efac162d1b8 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 27 Aug 2024 15:51:43 +0300 Subject: [PATCH 15/88] =?UTF-8?q?Bump=20version:=204.7=20=E2=86=92=204.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- PKGBUILD | 2 +- kvmd/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 5a13ea08..f44b6425 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 4.7 +current_version = 4.8 parse = (?P\d+)\.(?P\d+)(\.(?P\d+)(\-(?P[a-z]+))?)? serialize = {major}.{minor} diff --git a/PKGBUILD b/PKGBUILD index 0046f0c4..c1c590fb 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -39,7 +39,7 @@ for _variant in "${_variants[@]}"; do pkgname+=(kvmd-platform-$_platform-$_board) done pkgbase=kvmd -pkgver=4.7 +pkgver=4.8 pkgrel=1 pkgdesc="The main PiKVM daemon" url="https://github.com/pikvm/kvmd" diff --git a/kvmd/__init__.py b/kvmd/__init__.py index cdc07a33..d7266b58 100644 --- a/kvmd/__init__.py +++ b/kvmd/__init__.py @@ -20,4 +20,4 @@ # ========================================================================== # -__version__ = "4.7" +__version__ = "4.8" diff --git a/setup.py b/setup.py index 8b816d08..73f08f0d 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def main() -> None: setup( name="kvmd", - version="4.7", + version="4.8", url="https://github.com/pikvm/kvmd", license="GPLv3", author="Maxim Devaev", From 5045d8b3d70cc558908e8da479ee2664e7ab9c65 Mon Sep 17 00:00:00 2001 From: czo Date: Fri, 30 Aug 2024 18:30:31 +0200 Subject: [PATCH 16/88] silence the systemd/dbus exception if there are no matching services (#182) --- kvmd/apps/kvmd/sysunit.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kvmd/apps/kvmd/sysunit.py b/kvmd/apps/kvmd/sysunit.py index b967aa58..11207230 100644 --- a/kvmd/apps/kvmd/sysunit.py +++ b/kvmd/apps/kvmd/sysunit.py @@ -75,6 +75,10 @@ class SystemdUnitInfo: async def close(self) -> None: try: if self.__bus is not None: + try: + await self.__manager.call_get_default_target() + except: + pass self.__bus.disconnect() await self.__bus.wait_for_disconnect() except Exception: From fb9d860cf2594797aefff817c04e8c0f26712bf6 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Fri, 30 Aug 2024 19:52:11 +0300 Subject: [PATCH 17/88] pikvm/kvmd#182: improved dbus_next fix --- kvmd/apps/kvmd/sysunit.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/kvmd/apps/kvmd/sysunit.py b/kvmd/apps/kvmd/sysunit.py index 11207230..eea397ed 100644 --- a/kvmd/apps/kvmd/sysunit.py +++ b/kvmd/apps/kvmd/sysunit.py @@ -35,6 +35,7 @@ class SystemdUnitInfo: self.__bus: (dbus_next.aio.MessageBus | None) = None self.__intr: (dbus_next.introspection.Node | None) = None self.__manager: (dbus_next.aio.proxy_object.ProxyInterface | None) = None + self.__requested = False async def get_status(self, name: str) -> tuple[bool, bool]: assert self.__bus is not None @@ -49,6 +50,7 @@ class SystemdUnitInfo: unit = self.__bus.get_proxy_object("org.freedesktop.systemd1", unit_p, self.__intr) unit_props = unit.get_interface("org.freedesktop.DBus.Properties") started = ((await unit_props.call_get("org.freedesktop.systemd1.Unit", "ActiveState")).value == "active") # type: ignore + self.__requested = True except dbus_next.errors.DBusError as err: if err.type != "org.freedesktop.systemd1.NoSuchUnit": raise @@ -76,11 +78,12 @@ class SystemdUnitInfo: try: if self.__bus is not None: try: - await self.__manager.call_get_default_target() - except: - pass - self.__bus.disconnect() - await self.__bus.wait_for_disconnect() + # XXX: Workaround for dbus_next bug: https://github.com/pikvm/kvmd/pull/182 + if not self.__requested: + await self.__manager.call_get_default_target() # type: ignore + finally: + self.__bus.disconnect() + await self.__bus.wait_for_disconnect() except Exception: pass self.__manager = None From 5c3ac4c9c100fe381591f8756fca3bd56b9ca9f7 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Wed, 4 Sep 2024 03:03:48 +0300 Subject: [PATCH 18/88] pikvm/kvmd#170: alternative implementation --- scripts/kvmd-bootconfig | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/scripts/kvmd-bootconfig b/scripts/kvmd-bootconfig index b4dc2280..09e95fc0 100755 --- a/scripts/kvmd-bootconfig +++ b/scripts/kvmd-bootconfig @@ -55,6 +55,39 @@ rw # ========== First boot configuration ========== +make_avahi_service() { + local _serial + _serial=$(tr -d '\0' < /proc/device-tree/serial-number || echo "0000000000000000") + local _model + _model=$(tr -d '\0' < /proc/device-tree/model || echo "Unknown model") + mkdir -p /etc/avahi/services + cat < /etc/avahi/services/pikvm.service + + + + pikvm-$_serial.local + + _pikvm._tcp + 443 + path=/ + protocol=https + description=PiKVM Web Server + serial=$_serial + model=$_model + + + _https._tcp + 443 + path=/ + protocol=https + description=PiKVM Web Server + serial=$_serial + model=$_model + + +end_of_file +} + if [ -n "$FIRSTBOOT$FIRST_BOOT" ]; then ( \ (umount /etc/machine-id || true) \ @@ -90,6 +123,8 @@ if [ -n "$FIRSTBOOT$FIRST_BOOT" ]; then unset disk part npart label fi + make_avahi_service + # fc-cache is required for installed X server # shellcheck disable=SC2015 which fc-cache && fc-cache || true From af9023e8aab4b6b9e2c466793ee52d0c833ba02d Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Wed, 4 Sep 2024 04:39:56 +0300 Subject: [PATCH 19/88] kvmd-bootconfig: provide ENABLE_AVAHI --- scripts/kvmd-bootconfig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/kvmd-bootconfig b/scripts/kvmd-bootconfig index 09e95fc0..d97f10fa 100755 --- a/scripts/kvmd-bootconfig +++ b/scripts/kvmd-bootconfig @@ -131,6 +131,14 @@ if [ -n "$FIRSTBOOT$FIRST_BOOT" ]; then fi +# ========== Avahi ========== + +if [ -n "$ENABLE_AVAHI" ]; then + systemctl enable avahi-daemon || true + touch /boot/pikvm-reboot.txt +fi + + # ========== OTG serial ========== if [ -n "$ENABLE_OTG_SERIAL" ]; then From 5f26fa40728be4aa075c86a1a238381900e6b27b Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Wed, 4 Sep 2024 04:42:17 +0300 Subject: [PATCH 20/88] added avahi to deps --- PKGBUILD | 3 +++ 1 file changed, 3 insertions(+) diff --git a/PKGBUILD b/PKGBUILD index c1c590fb..85d224b0 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -120,6 +120,9 @@ depends=( # pgrep for kvmd-udev-restart-pass procps-ng + # Avahi for the service discovery, disabled by default, see kvmd-bootconfig + avahi + # Misc hostapd ) From 864a2af45e75be12db8f94687f8754f148121f1c Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Wed, 4 Sep 2024 04:47:43 +0300 Subject: [PATCH 21/88] kvmd-bootconfig: ensure avahi service on ENABLE_AVAHI --- scripts/kvmd-bootconfig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/kvmd-bootconfig b/scripts/kvmd-bootconfig index d97f10fa..06f41dbb 100755 --- a/scripts/kvmd-bootconfig +++ b/scripts/kvmd-bootconfig @@ -53,7 +53,7 @@ source <(dos2unix < /boot/pikvm.txt) rw -# ========== First boot configuration ========== +# ========== First boot and/or Avahi configuration ========== make_avahi_service() { local _serial @@ -130,10 +130,10 @@ if [ -n "$FIRSTBOOT$FIRST_BOOT" ]; then which fc-cache && fc-cache || true fi - -# ========== Avahi ========== - if [ -n "$ENABLE_AVAHI" ]; then + if [ ! -f /etc/avahi/services/pikvm.service ]; then + make_avahi_service + fi systemctl enable avahi-daemon || true touch /boot/pikvm-reboot.txt fi From 572a75d27b76e845a459bbfe3fef362231f04ddc Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Wed, 4 Sep 2024 14:08:00 +0300 Subject: [PATCH 22/88] kvmd-gencert: US is a new default --- scripts/kvmd-gencert | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/kvmd-gencert b/scripts/kvmd-gencert index 7bd95cf2..a199daf8 100755 --- a/scripts/kvmd-gencert +++ b/scripts/kvmd-gencert @@ -55,7 +55,7 @@ cd "$path" # - https://msol.io/blog/tech/create-a-self-signed-ecc-certificate openssl ecparam -out server.key -name prime256v1 -genkey openssl req -new -x509 -sha256 -nodes -key server.key -out server.crt -days 3650 \ - -subj "/C=RU/ST=Moscow/L=Moscow/O=PiKVM/OU=PiKVM/CN=localhost" + -subj "/C=US/O=PiKVM/OU=PiKVM/CN=localhost" chown "root:kvmd-$target" "$path"/* chmod 440 "$path/server.key" From 80aa9de4cc6b2de10f6ef19044585bc39e94904b Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Wed, 4 Sep 2024 18:49:21 +0300 Subject: [PATCH 23/88] =?UTF-8?q?Bump=20version:=204.8=20=E2=86=92=204.9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- PKGBUILD | 2 +- kvmd/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index f44b6425..35a8db1a 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 4.8 +current_version = 4.9 parse = (?P\d+)\.(?P\d+)(\.(?P\d+)(\-(?P[a-z]+))?)? serialize = {major}.{minor} diff --git a/PKGBUILD b/PKGBUILD index 85d224b0..6ac761e8 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -39,7 +39,7 @@ for _variant in "${_variants[@]}"; do pkgname+=(kvmd-platform-$_platform-$_board) done pkgbase=kvmd -pkgver=4.8 +pkgver=4.9 pkgrel=1 pkgdesc="The main PiKVM daemon" url="https://github.com/pikvm/kvmd" diff --git a/kvmd/__init__.py b/kvmd/__init__.py index d7266b58..f9734015 100644 --- a/kvmd/__init__.py +++ b/kvmd/__init__.py @@ -20,4 +20,4 @@ # ========================================================================== # -__version__ = "4.8" +__version__ = "4.9" diff --git a/setup.py b/setup.py index 73f08f0d..a7776b16 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def main() -> None: setup( name="kvmd", - version="4.8", + version="4.9", url="https://github.com/pikvm/kvmd", license="GPLv3", author="Maxim Devaev", From bc22a280223b3f314b440eeb40f9894c887da8ba Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Wed, 4 Sep 2024 21:52:20 +0300 Subject: [PATCH 24/88] removed avahi from deps --- PKGBUILD | 3 --- 1 file changed, 3 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 6ac761e8..7f510abd 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -120,9 +120,6 @@ depends=( # pgrep for kvmd-udev-restart-pass procps-ng - # Avahi for the service discovery, disabled by default, see kvmd-bootconfig - avahi - # Misc hostapd ) From 508d5fe606977d0447bd6bc1dc2048191b7f242f Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Wed, 4 Sep 2024 21:53:01 +0300 Subject: [PATCH 25/88] =?UTF-8?q?Bump=20version:=204.9=20=E2=86=92=204.10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- PKGBUILD | 2 +- kvmd/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 35a8db1a..4cb77adb 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 4.9 +current_version = 4.10 parse = (?P\d+)\.(?P\d+)(\.(?P\d+)(\-(?P[a-z]+))?)? serialize = {major}.{minor} diff --git a/PKGBUILD b/PKGBUILD index 7f510abd..9a3ca473 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -39,7 +39,7 @@ for _variant in "${_variants[@]}"; do pkgname+=(kvmd-platform-$_platform-$_board) done pkgbase=kvmd -pkgver=4.9 +pkgver=4.10 pkgrel=1 pkgdesc="The main PiKVM daemon" url="https://github.com/pikvm/kvmd" diff --git a/kvmd/__init__.py b/kvmd/__init__.py index f9734015..d3ab0bd7 100644 --- a/kvmd/__init__.py +++ b/kvmd/__init__.py @@ -20,4 +20,4 @@ # ========================================================================== # -__version__ = "4.9" +__version__ = "4.10" diff --git a/setup.py b/setup.py index a7776b16..0d7590c2 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def main() -> None: setup( name="kvmd", - version="4.9", + version="4.10", url="https://github.com/pikvm/kvmd", license="GPLv3", author="Maxim Devaev", From aa1ca3b32953498a427f6e0c36f2f46014394324 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sun, 8 Sep 2024 01:35:11 +0300 Subject: [PATCH 26/88] Serial number to uppercase, more info in Avahi --- kvmd/apps/kvmd/info/hw.py | 9 +++++---- scripts/kvmd-bootconfig | 23 ++++++++++++++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/kvmd/apps/kvmd/info/hw.py b/kvmd/apps/kvmd/info/hw.py index 1ff61145..2222ace3 100644 --- a/kvmd/apps/kvmd/info/hw.py +++ b/kvmd/apps/kvmd/info/hw.py @@ -70,8 +70,8 @@ class HwInfoSubmanager(BaseInfoSubmanager): cpu_temp, mem, ) = await asyncio.gather( - self.__read_dt_file("model"), - self.__read_dt_file("serial-number"), + self.__read_dt_file("model", upper=False), + self.__read_dt_file("serial-number", upper=True), self.__read_platform_file(), self.__get_throttling(), self.__get_cpu_percent(), @@ -108,11 +108,12 @@ class HwInfoSubmanager(BaseInfoSubmanager): # ===== - async def __read_dt_file(self, name: str) -> (str | None): + async def __read_dt_file(self, name: str, upper: bool) -> (str | None): if name not in self.__dt_cache: path = os.path.join(f"{env.PROCFS_PREFIX}/proc/device-tree", name) try: - self.__dt_cache[name] = (await aiotools.read_file(path)).strip(" \t\r\n\0") + value = (await aiotools.read_file(path)).strip(" \t\r\n\0") + self.__dt_cache[name] = (value.upper() if upper else value) except Exception as err: get_logger(0).error("Can't read DT %s from %s: %s", name, path, err) return None diff --git a/scripts/kvmd-bootconfig b/scripts/kvmd-bootconfig index 06f41dbb..c2fc5c6f 100755 --- a/scripts/kvmd-bootconfig +++ b/scripts/kvmd-bootconfig @@ -50,16 +50,21 @@ fi # shellcheck disable=SC1090 source <(dos2unix < /boot/pikvm.txt) +# shellcheck disable=SC1091 +source /usr/share/kvmd/platform || true + rw # ========== First boot and/or Avahi configuration ========== make_avahi_service() { + local _base local _serial - _serial=$(tr -d '\0' < /proc/device-tree/serial-number || echo "0000000000000000") - local _model - _model=$(tr -d '\0' < /proc/device-tree/model || echo "Unknown model") + local _platform + _base=$(tr -d '\0' < /proc/device-tree/model || echo "Unknown base") + _serial=$( (cat /proc/device-tree/serial-number || echo "0000000000000000") | tr -d '\0' | tr '[:lower:]' '[:upper:]') + _platform="$PIKVM_MODEL-$PIKVM_VIDEO-$PIKVM_BOARD" mkdir -p /etc/avahi/services cat < /etc/avahi/services/pikvm.service @@ -72,8 +77,12 @@ make_avahi_service() { path=/ protocol=https description=PiKVM Web Server + model=$PIKVM_MODEL + video=$PIKVM_VIDEO + board=$PIKVM_BOARD + base=$_base serial=$_serial - model=$_model + platform=$_platform _https._tcp @@ -81,8 +90,12 @@ make_avahi_service() { path=/ protocol=https description=PiKVM Web Server + model=$PIKVM_MODEL + video=$PIKVM_VIDEO + board=$PIKVM_BOARD + base=$_base serial=$_serial - model=$_model + model=$_platform end_of_file From 8113c5748b7f0e9cbae91f7b90c4e9f9590e63cd Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sun, 8 Sep 2024 01:57:30 +0300 Subject: [PATCH 27/88] new sponsors --- web/kvm/index.html | 5 +++++ web/kvm/window-about.pug | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/web/kvm/index.html b/web/kvm/index.html index f96f09ff..cee8261d 100644 --- a/web/kvm/index.html +++ b/web/kvm/index.html @@ -2124,6 +2124,7 @@
  • Cameron Tacklind
  • Carl Mercier
  • Carl-Fredrik Johansson
  • +
  • Carlos Eduardo Porter Herrera
  • Carlos Garcia
  • Carlos Manuel Torres
  • cbad536
  • @@ -2205,6 +2206,7 @@
  • Dominik Klonowski
  • Donald Hays
  • Edmon Abdul Nur
  • +
  • Edward Wang
  • Egan Ford
  • Elani Ferri
  • Elliot Woo
  • @@ -2379,6 +2381,7 @@
  • LeeNX
  • Leon Siegl
  • Leonard Feineis
  • +
  • Lewis Wild
  • Liran
  • Liviu Dimitriu
  • Lizardo Hernandez
  • @@ -2592,6 +2595,7 @@
  • Tango_Echo_Alpha
  • Tarlak Desaydrone
  • TechBear
  • +
  • techlobo
  • Ted
  • Tejun Heo
  • TheSnowedOne
  • @@ -2613,6 +2617,7 @@
  • Tomas Kuchta
  • Tomáš hrubý
  • Torsten Droste
  • +
  • Torsten Knoll
  • Tracy Fitch
  • Tristan Schoening
  • Truman Kilen
  • diff --git a/web/kvm/window-about.pug b/web/kvm/window-about.pug index 4bf66e72..42503c6c 100644 --- a/web/kvm/window-about.pug +++ b/web/kvm/window-about.pug @@ -141,6 +141,7 @@ div(id="about-window" class="window") li Cameron Tacklind li Carl Mercier li Carl-Fredrik Johansson + li Carlos Eduardo Porter Herrera li Carlos Garcia li Carlos Manuel Torres li cbad536 @@ -222,6 +223,7 @@ div(id="about-window" class="window") li Dominik Klonowski li Donald Hays li Edmon Abdul Nur + li Edward Wang li Egan Ford li Elani Ferri li Elliot Woo @@ -396,6 +398,7 @@ div(id="about-window" class="window") li LeeNX li Leon Siegl li Leonard Feineis + li Lewis Wild li Liran li Liviu Dimitriu li Lizardo Hernandez @@ -609,6 +612,7 @@ div(id="about-window" class="window") li Tango_Echo_Alpha li Tarlak Desaydrone li TechBear + li techlobo li Ted li Tejun Heo li TheSnowedOne @@ -630,6 +634,7 @@ div(id="about-window" class="window") li Tomas Kuchta li Tomáš hrubý li Torsten Droste + li Torsten Knoll li Tracy Fitch li Tristan Schoening li Truman Kilen From bbbc908af19a515a35d55ce9936fdd9fb2b770e8 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sun, 8 Sep 2024 01:59:50 +0300 Subject: [PATCH 28/88] =?UTF-8?q?Bump=20version:=204.10=20=E2=86=92=204.11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- PKGBUILD | 2 +- kvmd/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 4cb77adb..3eed40c1 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 4.10 +current_version = 4.11 parse = (?P\d+)\.(?P\d+)(\.(?P\d+)(\-(?P[a-z]+))?)? serialize = {major}.{minor} diff --git a/PKGBUILD b/PKGBUILD index 9a3ca473..99336259 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -39,7 +39,7 @@ for _variant in "${_variants[@]}"; do pkgname+=(kvmd-platform-$_platform-$_board) done pkgbase=kvmd -pkgver=4.10 +pkgver=4.11 pkgrel=1 pkgdesc="The main PiKVM daemon" url="https://github.com/pikvm/kvmd" diff --git a/kvmd/__init__.py b/kvmd/__init__.py index d3ab0bd7..6ef8dc45 100644 --- a/kvmd/__init__.py +++ b/kvmd/__init__.py @@ -20,4 +20,4 @@ # ========================================================================== # -__version__ = "4.10" +__version__ = "4.11" diff --git a/setup.py b/setup.py index 0d7590c2..1e5a8fef 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def main() -> None: setup( name="kvmd", - version="4.10", + version="4.11", url="https://github.com/pikvm/kvmd", license="GPLv3", author="Maxim Devaev", From 0bb35806ffd06c9bbc5905c9776811b619e93234 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Wed, 11 Sep 2024 00:48:47 +0300 Subject: [PATCH 29/88] Janus: Fixed OPUS mono audio in Chrome --- web/share/js/kvm/stream_janus.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/share/js/kvm/stream_janus.js b/web/share/js/kvm/stream_janus.js index d8218b81..0400267b 100644 --- a/web/share/js/kvm/stream_janus.js +++ b/web/share/js/kvm/stream_janus.js @@ -248,6 +248,13 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _ // Janus 0.x "media": {"audioSend": false, "videoSend": false, "data": false}, + // Chrome is playing OPUS as mono without this hack + // - https://issues.webrtc.org/issues/41481053 - IT'S NOT FIXED! + // - https://github.com/ossrs/srs/pull/2683/files + "customizeSdp": function(jsep) { + jsep.sdp = jsep.sdp.replace("useinbandfec=1", "useinbandfec=1;stereo=1"); + }, + "success": function(jsep) { __logInfo("Got SDP:", jsep); __sendStart(jsep); From 2123799e51df54088a200bb882d5d2127a86c312 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Wed, 11 Sep 2024 01:14:28 +0300 Subject: [PATCH 30/88] required ustreamer 6.16 --- PKGBUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PKGBUILD b/PKGBUILD index 99336259..a95432d0 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -91,7 +91,7 @@ depends=( certbot platform-io-access raspberrypi-utils - "ustreamer>=6.11" + "ustreamer>=6.16" # Systemd UDEV bug "systemd>=248.3-2" From 40393acf67ff9421d94604284d37a560f8f7e711 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Wed, 11 Sep 2024 01:16:16 +0300 Subject: [PATCH 31/88] =?UTF-8?q?Bump=20version:=204.11=20=E2=86=92=204.12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- PKGBUILD | 2 +- kvmd/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 3eed40c1..03436b0d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 4.11 +current_version = 4.12 parse = (?P\d+)\.(?P\d+)(\.(?P\d+)(\-(?P[a-z]+))?)? serialize = {major}.{minor} diff --git a/PKGBUILD b/PKGBUILD index a95432d0..8a0dc3b6 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -39,7 +39,7 @@ for _variant in "${_variants[@]}"; do pkgname+=(kvmd-platform-$_platform-$_board) done pkgbase=kvmd -pkgver=4.11 +pkgver=4.12 pkgrel=1 pkgdesc="The main PiKVM daemon" url="https://github.com/pikvm/kvmd" diff --git a/kvmd/__init__.py b/kvmd/__init__.py index 6ef8dc45..09b378bd 100644 --- a/kvmd/__init__.py +++ b/kvmd/__init__.py @@ -20,4 +20,4 @@ # ========================================================================== # -__version__ = "4.11" +__version__ = "4.12" diff --git a/setup.py b/setup.py index 1e5a8fef..ee3670d0 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def main() -> None: setup( name="kvmd", - version="4.11", + version="4.12", url="https://github.com/pikvm/kvmd", license="GPLv3", author="Maxim Devaev", From 56da910ebe9dc0ec496b8a0e7e9b89c5d531f7a4 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Wed, 11 Sep 2024 20:22:49 +0300 Subject: [PATCH 32/88] moved kvmd-oled to this repo --- PKGBUILD | 3 + configs/os/services/kvmd-oled-reboot.service | 12 + .../os/services/kvmd-oled-shutdown.service | 14 + configs/os/services/kvmd-oled.service | 15 + kvmd/apps/oled/__init__.py | 279 ++++++++++++++++++ kvmd/apps/oled/__main__.py | 24 ++ kvmd/apps/oled/fonts/ProggySquare.ttf | Bin 0 -> 41588 bytes kvmd/apps/oled/pics/hello.ppm | Bin 0 -> 12348 bytes kvmd/apps/oled/pics/pikvm.ppm | Bin 0 -> 12348 bytes setup.py | 3 + testenv/requirements.txt | 1 + 11 files changed, 351 insertions(+) create mode 100644 configs/os/services/kvmd-oled-reboot.service create mode 100644 configs/os/services/kvmd-oled-shutdown.service create mode 100644 configs/os/services/kvmd-oled.service create mode 100644 kvmd/apps/oled/__init__.py create mode 100644 kvmd/apps/oled/__main__.py create mode 100644 kvmd/apps/oled/fonts/ProggySquare.ttf create mode 100644 kvmd/apps/oled/pics/hello.ppm create mode 100644 kvmd/apps/oled/pics/pikvm.ppm diff --git a/PKGBUILD b/PKGBUILD index 8a0dc3b6..cddddea1 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -77,6 +77,8 @@ depends=( python-ldap python-zstandard python-mako + python-luma-oled + python-pyusb "libgpiod>=2.1" freetype2 "v4l-utils>=1.22.1-1" @@ -131,6 +133,7 @@ conflicts=( python-aiohttp-pikvm platformio avrdude-pikvm + kvmd-oled ) makedepends=( python-setuptools diff --git a/configs/os/services/kvmd-oled-reboot.service b/configs/os/services/kvmd-oled-reboot.service new file mode 100644 index 00000000..23bbca15 --- /dev/null +++ b/configs/os/services/kvmd-oled-reboot.service @@ -0,0 +1,12 @@ +[Unit] +Description=PiKVM - Display reboot message on the OLED +DefaultDependencies=no + +[Service] +Type=oneshot +ExecStart=/bin/bash -c "kill -USR1 `systemctl show -P MainPID kvmd-oled`" +ExecStop=/bin/true +RemainAfterExit=yes + +[Install] +WantedBy=reboot.target diff --git a/configs/os/services/kvmd-oled-shutdown.service b/configs/os/services/kvmd-oled-shutdown.service new file mode 100644 index 00000000..61d94c51 --- /dev/null +++ b/configs/os/services/kvmd-oled-shutdown.service @@ -0,0 +1,14 @@ +[Unit] +Description=PiKVM - Display shutdown message on the OLED +Conflicts=reboot.target +Before=shutdown.target poweroff.target halt.target +DefaultDependencies=no + +[Service] +Type=oneshot +ExecStart=/bin/bash -c "kill -USR2 `systemctl show -P MainPID kvmd-oled`" +ExecStop=/bin/true +RemainAfterExit=yes + +[Install] +WantedBy=shutdown.target diff --git a/configs/os/services/kvmd-oled.service b/configs/os/services/kvmd-oled.service new file mode 100644 index 00000000..ea0d4850 --- /dev/null +++ b/configs/os/services/kvmd-oled.service @@ -0,0 +1,15 @@ +[Unit] +Description=PiKVM - A small OLED daemon +After=systemd-modules-load.service +ConditionPathExists=/dev/i2c-1 + +[Service] +Type=simple +Restart=always +RestartSec=3 +ExecStartPre=/usr/bin/kvmd-oled --interval=3 --clear-on-exit --image=@hello.ppm +ExecStart=/usr/bin/kvmd-oled +TimeoutStopSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/kvmd/apps/oled/__init__.py b/kvmd/apps/oled/__init__.py new file mode 100644 index 00000000..9fc4acb8 --- /dev/null +++ b/kvmd/apps/oled/__init__.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 +# ========================================================================== # +# # +# KVMD-OLED - A small OLED daemon for PiKVM. # +# # +# Copyright (C) 2018 Maxim Devaev # +# # +# 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 . # +# # +# ========================================================================== # + + +import sys +import os +import socket +import signal +import itertools +import logging +import datetime +import time + +import netifaces +import psutil +import usb.core + +from luma.core import cmdline as luma_cmdline +from luma.core.device import device as luma_device +from luma.core.render import canvas as luma_canvas + +from PIL import Image +from PIL import ImageFont + + +# ===== +_logger = logging.getLogger("oled") + + +# ===== +def _get_ip() -> tuple[str, str]: + try: + gws = netifaces.gateways() + if "default" in gws: + for proto in [socket.AF_INET, socket.AF_INET6]: + if proto in gws["default"]: + iface = gws["default"][proto][1] + addrs = netifaces.ifaddresses(iface) + return (iface, addrs[proto][0]["addr"]) + + for iface in netifaces.interfaces(): + if not iface.startswith(("lo", "docker")): + addrs = netifaces.ifaddresses(iface) + for proto in [socket.AF_INET, socket.AF_INET6]: + if proto in addrs: + return (iface, addrs[proto][0]["addr"]) + except Exception: + # _logger.exception("Can't get iface/IP") + pass + return ("", "") + + +def _get_uptime() -> str: + uptime = datetime.timedelta(seconds=int(time.time() - psutil.boot_time())) + pl = {"days": uptime.days} + (pl["hours"], rem) = divmod(uptime.seconds, 3600) + (pl["mins"], pl["secs"]) = divmod(rem, 60) + return "{days}d {hours}h {mins}m".format(**pl) + + +def _get_temp(fahrenheit: bool) -> str: + try: + with open("/sys/class/thermal/thermal_zone0/temp") as temp_file: + temp = int((temp_file.read().strip())) / 1000 + if fahrenheit: + temp = temp * 9 / 5 + 32 + return f"{temp:.1f}\u00b0F" + return f"{temp:.1f}\u00b0C" + except Exception: + # _logger.exception("Can't read temp") + return "" + + +def _get_cpu() -> str: + st = psutil.cpu_times_percent() + user = st.user - st.guest + nice = st.nice - st.guest_nice + idle_all = st.idle + st.iowait + system_all = st.system + st.irq + st.softirq + virtual = st.guest + st.guest_nice + total = max(1, user + nice + system_all + idle_all + st.steal + virtual) + percent = int( + st.nice / total * 100 + + st.user / total * 100 + + system_all / total * 100 + + (st.steal + st.guest) / total * 100 + ) + return f"{percent}%" + + +def _get_mem() -> str: + return f"{int(psutil.virtual_memory().percent)}%" + + +# ===== +class Screen: + def __init__( + self, + device: luma_device, + font: ImageFont.FreeTypeFont, + font_spacing: int, + offset: tuple[int, int], + ) -> None: + + self.__device = device + self.__font = font + self.__font_spacing = font_spacing + self.__offset = offset + + def draw_text(self, text: str, offset_x: int=0) -> None: + with luma_canvas(self.__device) as draw: + offset = list(self.__offset) + offset[0] += offset_x + draw.multiline_text(offset, text, font=self.__font, spacing=self.__font_spacing, fill="white") + + def draw_image(self, image_path: str) -> None: + with luma_canvas(self.__device) as draw: + draw.bitmap(self.__offset, Image.open(image_path).convert("1"), fill="white") + + +def _detect_geometry() -> dict: + with open("/proc/device-tree/model") as file: + is_cm4 = ("Compute Module 4" in file.read()) + has_usb = bool(list(usb.core.find(find_all=True))) + if is_cm4 and has_usb: + return {"height": 64, "rotate": 2} + return {"height": 32, "rotate": 0} + + +def _get_data_path(subdir: str, name: str) -> str: + if not name.startswith("@"): + return name # Just a regular system path + name = name[1:] + module_path = sys.modules[__name__].__file__ + assert module_path is not None + return os.path.join(os.path.dirname(module_path), subdir, name) + + +# ===== +def main() -> None: # pylint: disable=too-many-locals,too-many-branches,too-many-statements + logging.basicConfig(level=logging.INFO, format="%(message)s") + logging.getLogger("PIL").setLevel(logging.ERROR) + + parser = luma_cmdline.create_parser(description="Display FQDN and IP on the OLED") + parser.set_defaults(**_detect_geometry()) + + parser.add_argument("--font", default="@ProggySquare.ttf", type=(lambda arg: _get_data_path("fonts", arg)), help="Font path") + parser.add_argument("--font-size", default=16, type=int, help="Font size") + parser.add_argument("--font-spacing", default=2, type=int, help="Font line spacing") + parser.add_argument("--offset-x", default=0, type=int, help="Horizontal offset") + parser.add_argument("--offset-y", default=0, type=int, help="Vertical offset") + parser.add_argument("--interval", default=5, type=int, help="Screens interval") + parser.add_argument("--image", default="", type=(lambda arg: _get_data_path("pics", arg)), help="Display some image, wait a single interval and exit") + parser.add_argument("--text", default="", help="Display some text, wait a single interval and exit") + parser.add_argument("--pipe", action="store_true", help="Read and display lines from stdin until EOF, wait a single interval and exit") + parser.add_argument("--clear-on-exit", action="store_true", help="Clear display on exit") + parser.add_argument("--contrast", default=64, type=int, help="Set OLED contrast, values from 0 to 255") + parser.add_argument("--fahrenheit", action="store_true", help="Display temperature in Fahrenheit instead of Celsius") + options = parser.parse_args(sys.argv[1:]) + if options.config: + config = luma_cmdline.load_config(options.config) + options = parser.parse_args(config + sys.argv[1:]) + + device = luma_cmdline.create_device(options) + device.cleanup = (lambda _: None) + screen = Screen( + device=device, + font=ImageFont.truetype(options.font, options.font_size), + font_spacing=options.font_spacing, + offset=(options.offset_x, options.offset_y), + ) + + if options.display not in luma_cmdline.get_display_types()["emulator"]: + _logger.info("Iface: %s", options.interface) + _logger.info("Display: %s", options.display) + _logger.info("Size: %dx%d", device.width, device.height) + options.contrast = min(max(options.contrast, 0), 255) + _logger.info("Contrast: %d", options.contrast) + device.contrast(options.contrast) + + try: + if options.image: + screen.draw_image(options.image) + time.sleep(options.interval) + + elif options.text: + screen.draw_text(options.text.replace("\\n", "\n")) + time.sleep(options.interval) + + elif options.pipe: + text = "" + for line in sys.stdin: + text += line + if "\0" in text: + screen.draw_text(text.replace("\0", "")) + text = "" + time.sleep(options.interval) + + else: + stop_reason: (str | None) = None + + def sigusr_handler(signum: int, _) -> None: # type: ignore + nonlocal stop_reason + if signum in (signal.SIGINT, signal.SIGTERM): + stop_reason = "" + elif signum == signal.SIGUSR1: + stop_reason = "Rebooting...\nPlease wait" + elif signum == signal.SIGUSR2: + stop_reason = "Halted" + + for signum in [signal.SIGTERM, signal.SIGINT, signal.SIGUSR1, signal.SIGUSR2]: + signal.signal(signum, sigusr_handler) + + hb = itertools.cycle(r"/-\|") # Heartbeat + swim = 0 + + def draw(text: str) -> None: + nonlocal swim + count = 0 + while (count < max(options.interval, 1) * 2) and stop_reason is None: + screen.draw_text( + text=text.replace("__hb__", next(hb)), + offset_x=(3 if swim < 0 else 0), + ) + count += 1 + if swim >= 1200: + swim = -1200 + else: + swim += 1 + time.sleep(0.5) + + if device.height >= 64: + while stop_reason is None: + (iface, ip) = _get_ip() + text = f"{socket.getfqdn()}\n{ip}\niface: {iface}\ntemp: {_get_temp(options.fahrenheit)}" + text += f"\ncpu: {_get_cpu()} mem: {_get_mem()}\n(__hb__) {_get_uptime()}" + draw(text) + else: + summary = True + while stop_reason is None: + if summary: + text = f"{socket.getfqdn()}\n(__hb__) {_get_uptime()}\ntemp: {_get_temp(options.fahrenheit)}" + else: + (iface, ip) = _get_ip() + text = "%s\n(__hb__) iface: %s\ncpu: %s mem: %s" % (ip, iface, _get_cpu(), _get_mem()) + draw(text) + summary = (not summary) + + if stop_reason is not None: + if len(stop_reason) > 0: + options.clear_on_exit = False + screen.draw_text(stop_reason) + while len(stop_reason) > 0: + time.sleep(0.1) + + except (SystemExit, KeyboardInterrupt): + pass + + if options.clear_on_exit: + screen.draw_text("") diff --git a/kvmd/apps/oled/__main__.py b/kvmd/apps/oled/__main__.py new file mode 100644 index 00000000..4827fc49 --- /dev/null +++ b/kvmd/apps/oled/__main__.py @@ -0,0 +1,24 @@ +# ========================================================================== # +# # +# KVMD - The main PiKVM daemon. # +# # +# Copyright (C) 2018-2024 Maxim Devaev # +# # +# 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 . # +# # +# ========================================================================== # + + +from . import main +main() diff --git a/kvmd/apps/oled/fonts/ProggySquare.ttf b/kvmd/apps/oled/fonts/ProggySquare.ttf new file mode 100644 index 0000000000000000000000000000000000000000..9118ece5b2d16328e78c38ba14386bd681574781 GIT binary patch literal 41588 zcmeHQeXt%?d4Kla%@+v?$t58qA>5Dz0)*t=d~q)X2m}ZUMH^B|EmZ=91WiZ`0Rt$$ zqIPIntzxHjnASn-FxE+1>vX0LQyqs=wboi|)pi(b`$vaSO4U+y97joizvrCi>^b}H zzPskH6)}86w#YsJ;2F1G^6m zELiz~$lz0`+j-Z6M}|?4E&mw~1AF&Bbk8Gioc6HDj9lbHKiRiu_uc>U!F^xGxvyZ~ zvJVwAo*meZYjIvy?K^Pf=sP-(Ae(&{mrvh+@UGn-yYY*Ui%dh^Kz?BN(L>oM1=nY5 zP%(Vp?gM-N?Q1V=5P1^)Wgk6s@ctuDAAk1`ME>w8T=VrqhxZ)1cOY8;Jg=brOd&1U zzjx<>&%9~g)*s2>OjL;c)!w(ghv{dZ_{3L5PRL*<@1Qs+12}TSHTkwzKaZ*oi#YO5 z@ixGkECZj0wR!-(ZIjcGe-Js{20PgYNXWon4;(|q@47VmTiM{c$!EEC<>S@+kKB)Y z$?$P^qrAh;iDa-d`G1+-4Y)-*%;Vl2_piuVjj!i}zqF0;f9OV$pBI+f+r^0}qx^0;wPi{gDNn9qmj3Awfe^jw|%8B9QCr)VN zcWDk?{QEckRa%`**JP?5-qnt>;JL}D} zesbFW(_WapY4+1|=FGWc&KFMKa{Aj(e|GMgx%bVJd5_Nf?isH=<8$*z=086FTMM=> zcw%VD(8EK|oq7J5N6-Arh0_;azi|J;M;HF}po^X@qBxs_uppIS9@)$&zaR^74c*s9O1`u6I4^?9rB zUj6Lq7tX)q{4ZUw=7J;m_mK;JxMu5`k6$?R!VMQbdExPkuD$5^MK4~w^WsBm7p%Q( z?QLtHzGUtt4_PY}1RIcW!=W%XwRlZF%|9H(dJc*tW4xjGr-nbo@UqyWz4YF8lH2w_pC;*0o!o z*!t5eZoT5UE7x54{wrU->b|SKzU`WA&s;tC>cdxm>zZq?IeyK5ZePCr_U*s7{ncyt zUHgUW&cE*1b+25%^ZG}xe`&`}JC5J5;D*CDJpY=TUh^{<4x_@L%#hhKPln_ySt{qs zO1VHTmi4kxE|tsWD%mbOV&XF5D4~i-0PNyJL%X*0D zMtLISQq)6E-X)!^r&Wf4%dD4DjPtQntMY7X^~}RMh1S{WKreqT%yeE&5>%?n(u6^z zJI7`%HaSx|ed@AG(YSAvS;~6LGmR;swCtSeGS3q{1cd~2@d(^Vj4Y-qT^v2(eG3vK zX7B(5g}y2k=26sYI+1>e%+3(~!)J7A^gHM>??KA!UOqi1b9I_REQL-MQprJh@2wq< zq^}Z=fK6>B?a(mgX>|o8;DDY$w-8{)KD4lbzz<5)q;67X-*G)9>3UItnmX%1RMESu z5}DNlLy#x?vVJW!`KX+&XWkV9U*(2L9z~`xzR@eIokUgAyd(7%n{rWedVsH(Mvc`e zqg@LRGE{-fUFEgUT|K~XJ@gy;^nJ07U@qx=Z?$eFoQSp4xL~OgZ#v-V&|)s^r*`u` zqp+ps7)UY2(l9?@X{;S_-p*TU_fUS&U6^O^!(EYtGpbz^@E9U<)i*ZL88n~R$XnvC zvO>ddDG-v~ULPI1eW4Xx#GX)w7GrQ(_sm}?S|YZcs12Q^rUrl-{=2$XAgQk>1t;(X z+@{rrk*VPf>~yNo7UX>8WhbxmZ*g2-DRJ`9wA9iVpXJ$(_+foP`cz~Lcpb0KN(xU# zBa)Ff z&!vlI8C)+V~HH#!u*yAw1iy_u^9Z@F%bJC^QgabM69Y61D;Q}q`pTe=Vtu(|_d zj_=KNlN`|>>?V^zAWG=4KDowHX*cXYmaWN&sqezipEys7HnpQr*7yoEgX+rut#6~v zQlmYj{*EM>dry)X)3Lj}rq7rzvpAC@%cdWZPU`sLNvrh_JTFVYbMU*S@1`}apabNy zVY8HVjE7jZxUQ~DyEB=gec-FrMYP%6&`pfVXq_SZj5hS+Q7z?c+q8i#Zhl-Rmnz=$ z7CjU){2llH(7puueqphJdMz(|Fr9jjva{ zl^l{jLU}J(&GL_t`QXVZIW@|e6MHZ9I=?J<1#sJJI`C*B*C^Z=i|Id zM=ehSK3H86_&ItJ3PL3KA7>DK^Z7_$ODYBXEH|k*$!H;Os`AE*m@h`N@sroZdF2HM z1dtmgs9?z@IHg|6Clh1=^kWiZhtbhbN;(Wu}7%UQKV7RakH?QFKHLww~BUS z$r=Tu+ALQ44*nuwcj9v6V4p68u_B(uw)2PXorKnN6r-k`ddfn~+n~BjnQe5D&tiR3Mf4a&&8>zbCGO6aVlceCL34# zuEIlHCa2i8%lf7U5#}Y0thQLB4Nv(VUf0M?9&#rpVl3~hw>EKVGA*8k0(8LLpTC#6 zc*g{c$UBwm%(mv1_<7=t`B;@VylzYmh@(s{QK3Fdu{XSHFy!#qI8&GUm5ZXQT#9@} z4*srx-c<1-t|RXf-jq95PnS!%y0YwfgXa`=v7^e3{j*=Q0ZU_B!Pwwz)V&H2$A&!& z0RU#;T~w$Xa`E$?r6LO>N{wPmE2-%S-aEk)7s;J261&QhI+SC_CKj92JQp#2<-4!3 z&gy+)-V+JSF%Qb;Rd@ZcA3I?eAlE-iQKWVNWBQ%5czsqIGmh2a?SO!4RGGGb>j}4=H%8nMPtBF<4GedZp)Q}^Vs6ld(L7L$k=P@xa8Hz# zcWx+CN63C#a1yvU|AY%_MlJWIf~HXTc>tZ_D4o94wfUr5W#N!)Oyw&S90 zC&QMq0uN`^%4%gXqMCH7QfCDP8+sZr?G{_@dk*nl{J0ZFG%$7aW?p;t~o+k+I3~6p|`!Zz}y~Z=J_7;z78AfO?y>4>Ts%MwS&o+ zstMm0>52Jo_+ovpl0{69S+2@Mr|E8eK0PWainE1O zQhNNmL^b-Z@`!Dh*bKj6i}`KO7Mj4g_;jFC(=Yu=GVYU4><9gaNQvbto%IB^kuTyT z`qA2Xne%Ik#Hfv>W2pSOfQ)UN#E7MkPEQ}}VMELM@IfYvrZh&4wVqUcx})M2Y6P~m z(48_a8zn6z=OR4HDjgMdH17ce-NP2KwH}kY)}3N(mM1RTO}AJk|5#?bhIPSYIj{!> zCT~mo;%Cf1;hqkNT<%0xY&h{_&l96$T^)yB7`G}gbe-A3|{HF2`xcwcrD<73`1 zlDD>(+nC_M;P9VyJ-BR_==`kbZ^rl=g5Uv(!21|OroJYs3%&cMFl2#S9Hmi}fZ@rDtC1R?8*q-$%4a5-;n=F1T74rx^Kpn_Lwy%CbY6?&RnqBFHw zevQMedSS3A-k0Y4HA&18+kxo|ZyKUgRrYc>;JN$VD7;zM{i}Z~9(04fkY?-IdbN6< zE7z6V#?hV-92n!6#L07iv}7f%fzGJ4%ejO*{y+o2;v7 ztkO)j!%WfHM<9>{yA4&j_ZBoi_ny!mLg5HKm zN01?UClW>DR*bY?x`cS+ef)PDTFN#4c1NX*DL2wyC0~BAES_D}}4Zc5tt;M)f ztU2G}Ms-6bj9P0(r8*0+gqGh+I5uIL)mv5 z`e~pI3WpDv74dqP#yuul>E5A_Vxm}Px29B2{U^;91!?fJSbjnXkR9};!J{QmLMn$G4v3Kk0@dxW8V$&T< zse?pG97qjXs_x}iqS;taOmX@je7r%MFU1f$exY!Wwhi-T*tQLkb*Ki z#-nCj{UJvph^&Xuq@* zjOIC=P_c%n8e;Yl+e>06MSSK*Y}|M%${J&DE+N(k{!IN;F61V^P>ZeccLcq@s~N^+ z(*u*a=?hcVncgt9b8)`h&f3KHWuCy*#lqjYF&af=Kv$CLh`|%iA+tdtd6p)yO*wzy zpF1XNJVi{b_7uyF_FYw0`*H(-fUSkEEysTa&!?VmoyJjrg7c*1leR;C-?q^fDLdMb z*%r&Tn{0cXx9b{g5>kfssZCPgE>o&GiB`!zz(342bNaIK*w!_xi_!C1Y`O4QY`7ig zngj5k(R?w)NM-}jd(-uJ{A&HYGz0&uq81{gJnGIldvkDg2=(JAL%@b@>e<~?i_r#j z#l-YD=p0-rbS)Q^FZC01u-5N`h0&i{Wma63n&z2}n5I(`b%aIC1nnyyfyxI{nzYbE zB?E`4r;qR(>N$$oRAm@*maz#PhuX2@1X^oM3x@cZRwyg(ywXYEHN_E)ynlkn3o&r$ z$6{a?1Es+=p1-5t#IM=6)Wh{vT0#`OA6yL*4E8(%O6>R&d=0V15bkH%)hp#a&B>y7 zK~WFALJP!c*s#V_@WyDYdIun}jxv_mN=$}q!@TXrFJlcLde~qRZ3U>keP~1~)m-B> z6&px#?=-Zf=f+}v)zFuziYYMk<?!r4Dzj(S-_%Tove=YQ7uaYwqcj z)xvCMN`7;vwVIra=S;nKfby&Uqo|6{35~0%<}6izs`2#{J;B#dGs>3Qs~l^TDvWpC z&Vg4Q(W=s^SF2lRD(ahk^P=7O!Dkd|+4QOzcH?C@>vA;v{T!V!bf_ zrdPb~Y0sRBzaLXMv#yewQ5REd`8Ms@#v{kHnITqWq2Pq@WL)R+sGQ{D)~PWg(&Ju| zG6~0NEMW?$LP8~(QDDNB?KZ0KsWCA4nN#v9e6A``NxMpZ181?F(zg!Z8tP|G7~6Gh z?-s4srSWyA>$j>I4_mAl$H!(nv=z(qeB9RZJRkdf z=3RU&&?~OBpu`0hpCIR#TNpZuiy)EY*!|W=qnZn#~LuQ;HIa<%!0#?Bk5@$eERQ9OWtOjAszbeYnhvE%BCrV zPCDE$QGe7hI!EKSn?9qXOj^^T+R+BP$-z>i1$qM2p~~0O;g(Y-K+V*fgW?%#d--q_ zB)_3^!8VNlO{vL8&7x27vn3{!@$jn#Rt2ieg-5+RKW{?Yu!($_3|8-`0N# zpVc{?qGGjKYYe`)4t4C4tEl36qlmt&y!xcxO%HU&<{C(IpKFHR{#*L99l9p2-kBET z5e9Et|5i3!@0Qz+90$pt9FRGq1P1vRy#O2&WTS=YVXGs&I z$?Fv$w=yz{wdHowf44+W9{V`Ump%LnKbF~fOC4^;t>PaG>6@0`#ly>o>V~;P6$3Dg z@oy{je3!xgHfu6|5gjS8H;|C3Bn`ElH|ps*)Ke5FyzpA0J{emDXIysa~&<_}|sqd4eiL!B)q5irj zU6e{|DNVI?Cj0hfmCjMa)OJl8kxd8t4ptD9I~UDXvebWTvy<2Lmqb}r{YBc^u19`@ z3C0zE2QwRfEq)g;*b(D}o|KxohklvKg-2|?>8%>uwdclO1^0GA2YcbxV^j`4q^iI2 z?~7f0u47O<=9vGB>sEj_Rb^{iXK$rN2#uinnsY#Mfu<^*siCPR_UodxjxN>%yTyFs zvn6uk_%_!zY*(!bdL|s~o9>l%kkZs;g|VNkl8Mn2%SM$mc)=1GDwSLmT%{HV*qZ4R zyXp|6z0*;WvsOy=4BBdUg1z5^D*rNCDNEq_Q zd}2P#f4lBmeB--V*RS|HV=-SWPi;0I$-3C?;(R~~AZ|X7j1O^{!0$(iBY-ViZiT#? zeS6{u4P#Sk_?g-m<>;t7s#4pC9?PMI9QYFZMtwW{A-ZY8ZRZpGsvCe+a^LYN)Hel_ z@`Z-7Deb1B-sT)N2_30TG|Qqn_DOt46M8$JP$7U(=Qd&z+ItTl=S-pZlQ0j|Z%{VN zBYfw62zF{wKm8MV&asK9$)PTd<-==upP(#MDCexYt;dPd;_7fon~V(OZvjcgZ%*rd zGvA3R-YY&R{N3&&^3LKHJ24%WZ!garOoWMPJn zNBM0Pc;~hZUoLpJ&kwl2hNz{%s6LPK)9|Ld{>?yw0Xq0aX4MQ!|C!4=T6xp zd*uPyFT3TiY>*NB+k|hryW^;AzVo5sU5D?z|H$t9hIbsk_rQTYhj;GT`@sI)hj;C| zZTR(v5ANOj&@FF$VE5rYyLN3D8QHWR|3^2=o$?TrcNcDXFI4adZhjxiJG6X24xrP+ zIJ--3L+$I4KL`+eam_9AR)8UtJ$fG07V_a=ws5>e#$ZZg|AJXjlZb86f!tz0M9gF!dQ zYvi?Zqud19-;CdVyk34;c7iRtbd852Z?vUNtitM~>Wwt6? zot>Xukgdrs%r43<&emp^Wb3l^*+@2;ZOAreo3hQ>mh94OEE~@*%P!BhW>;iaW>;m~ zva98|{IR@8-YcJ#-{?y zA#az@$VcJP9z{I-E_q45FVD<<;J$nBI(YYbPK$fe;yp=mbR;dN^p2+Vj;8dEru2@c^p2+Vj;8dEru2@c z^p2+Vj;8dEru2@c^p2+Vj@~oq z;_3C_`SkYg{PF($dHVEwdiVbNH2pf?{kZvdI~^}BUtS#FOs5am)Az4Gzr47)JzS!H zbvzuet`5&uet-Ud*aKX-J>(CW0P`Pzg@?>^&dW2;cy4>l_a>14w}bI-?}4%3O#o42 z?lrXhTXa3faC-tFc^N5of>0$JU}pl%A0`2?Eh|WVJ5P`G9J?|>!kQ}WOmakCy8|pw zz@rQCdj5T^zk&cC;O5Jp^rVzbtcodRh8T9lmFqw2g#{HH4G`oq}yeNkj;16V#M(4x_nRu>*)_sKg01nwC zkZe^;!>|%Gt*Jcwg5b8l#W|R$63|wp3xow)D-r-K`QYPLTnQvw)zUVs1Wjuy&%PkI z?QC_+0X@M0^s6h&mvFrtWs+|wE5zi()^bvdc_lODxl$(EZ2+Eri!GVV=cymnlqA}~ zxVXezDJ%3PPcpcbq%?V&8J*Ks$@p{|)@cBpaFW=ken?6u9dpr{4ldn95|lM~Y9;V& z^E5M1W->FS|8f$BI^HM^4!Mrkv!yB*F}~L$=M3Q!jCTr z;y|(uRV#rWjo&&-@;SFAUXNKkaKW)-t)Z2y{nWwI%17Vko95&ls^IZ@r4AGOaN-32j)k- zyoRlCdbCUcDa}Sf&4#L#C0Ayj@mK{)2|v)Wes3VX%{Z92K!yW&0%b%X$xF%BYaVGT z01Yr(=?4f2FrTuvqRV1N-Ks60@RFz8mXRtDpDN^X3Z;u3OFuwPfI)%x#2mL)T+=cU apG^kC1Qb!uGcz7qn99d(1L^CJ5B~)eyVKGD literal 0 HcmV?d00001 diff --git a/kvmd/apps/oled/pics/pikvm.ppm b/kvmd/apps/oled/pics/pikvm.ppm new file mode 100644 index 0000000000000000000000000000000000000000..fafaa5114cc79c636742beb3078b2f1847f03f2d GIT binary patch literal 12348 zcmeI2S!k745Xbu*@YN?Df(0LhhMJNR5J4;=BB_cfB8WCN7DcQkRZ$f84XubPYSjmE ztGM8X;#$RB#C_lQec!kChXa3xbH00X?@gpc^F4(5X8E7<%{gag&fK*5G4I>_pUTUA z`J?RD{L+E>En1ee$?soQUeV+C-uZ|gin+O_Mm zj8sEF*^Q;8rGo|y`ZJqn_UzezeR}@>^laX|`TY6w%a<=t|Iv)qu3bBZW7}&5?%lhW z-s+2#wr$&X+qP{FA3l^Hs!mkue|fS?mo6Ia)vH(HePtvn<=gyBGKbp?__wToF-_D+2xpL*?$&)!&*}CP{ty=~~ zBFNUZ=IY{zIt4%^^f)&lf#)>QqzUFS}R#+z9SphQ^d~L#T;X z4<0<=ym#x?EuQ@4%a_d|hYuf)rEpH3JSqS0-McqYCv&Fl1OPb*nOoI#%@r$FR8&+X zegy@Lxi>MBa$g|k$o~r#EJy^&oT)P4+O=z$TYagT-Me>}Z)eV&8Gq*L)vKid(Wg(J zSSKqEC(!`OJ6Rc`+`4t^UcGu%YE`FB9W(>AjvYHDmI^Qt1C>m5IB|j?$wMUU(4m8*5G`A_+_!JvhYugH zrEI{%Ug~MG8(ogGXU`gw*2ktzo2>Q3LFRjD#ksj~|YfDN`Caa3B@hnKNg+yU1Cb zh+x7E8#YvPW6PE;IN1Wu{Q2`eEIlY{ZxGl3ER)Y}3^8rmG}Z_lj)ty^X3ieS>ZTt| zFMb_5bf_kFDhDh{~a8(V~T!^zGZ1b1gqWY~8xGZr!>joi}fu7tt<`2hEwYW5*7G$HfnUR8k{+ z#Ljt8|FOj0PMtb=s6~qwX;8L@7eTYDCFQmlCt1r@G?WHX{7_Bak*p#Xg)QY8aIVNCx=&y&jv^c3J?h3CQ^3r3PnXle$FITFJ*IL zOMR=vAt@_ju>6gGcOXqj%1X(~2)}kyrc4Q>Ill%`F{n3y<*KDw8IkIOoYfqB3|YK* zvA1PETf{qc>Qv*#jlD>;KeKdEysN2s0Po+wzm!D~tn@7)h;KyCo;@YyECkXG8#YLq zRi{0`@vr0&-GcxpCwi&TJ`~#o64hcuI3zxP?c$h|L)p?kS6aPifWG3Tn}qWT)QQ&+ zjlu;)-hfa|LXE_$FE3>R;a822?nThVKwojZ3Hpr$GvGNYtiSH;hnm@r|24+4M(fz*(xNHwkq4uxtkY7AQ=g>kj= zq;CYJi(yg|N<{6))lwG_~jYFGPCe52S57f%a$|R*x zA4vI)Thcg62g;Sc&YCsLSG({RnTZ=N}LT%t|JTXPk8PlxD z#o`-5`(4sjty%@rtX_!^e&z4S!tY#caCRU2%o*33P zZrsQ~^ez$yB9YlA+h4-<(xpp-^O$PffY0Lks7~G3tXZ=_CwOIB65;&>xIa=P=hhYH zQa}Uqsw%eM?$_|8vvU2u(H0|1zaild0ub(F6Yh DXZ!bU literal 0 HcmV?d00001 diff --git a/setup.py b/setup.py index ee3670d0..0a115237 100755 --- a/setup.py +++ b/setup.py @@ -101,6 +101,7 @@ def main() -> None: "kvmd.apps.ngxmkconf", "kvmd.apps.janus", "kvmd.apps.watchdog", + "kvmd.apps.oled", "kvmd.helpers", "kvmd.helpers.remount", "kvmd.helpers.swapfiles", @@ -108,6 +109,7 @@ def main() -> None: package_data={ "kvmd.apps.vnc": ["fonts/*.ttf"], + "kvmd.apps.oled": ["fonts/*.ttf", "pics/*.ppm"], }, entry_points={ @@ -127,6 +129,7 @@ def main() -> None: "kvmd-nginx-mkconf = kvmd.apps.ngxmkconf:main", "kvmd-janus = kvmd.apps.janus:main", "kvmd-watchdog = kvmd.apps.watchdog:main", + "kvmd-oled = kvmd.apps.oled:main", "kvmd-helper-pst-remount = kvmd.helpers.remount:main", "kvmd-helper-otgmsd-remount = kvmd.helpers.remount:main", "kvmd-helper-swapfiles = kvmd.helpers.swapfiles:main", diff --git a/testenv/requirements.txt b/testenv/requirements.txt index b35a712a..3f848431 100644 --- a/testenv/requirements.txt +++ b/testenv/requirements.txt @@ -4,3 +4,4 @@ spidev pyrad types-PyYAML types-aiofiles +luma.oled From 489601bb96162a29c1de2ebde5e4e62ef53993a7 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Wed, 11 Sep 2024 20:23:24 +0300 Subject: [PATCH 33/88] =?UTF-8?q?Bump=20version:=204.12=20=E2=86=92=204.13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- PKGBUILD | 2 +- kvmd/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 03436b0d..803d5272 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 4.12 +current_version = 4.13 parse = (?P\d+)\.(?P\d+)(\.(?P\d+)(\-(?P[a-z]+))?)? serialize = {major}.{minor} diff --git a/PKGBUILD b/PKGBUILD index cddddea1..2366cd35 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -39,7 +39,7 @@ for _variant in "${_variants[@]}"; do pkgname+=(kvmd-platform-$_platform-$_board) done pkgbase=kvmd -pkgver=4.12 +pkgver=4.13 pkgrel=1 pkgdesc="The main PiKVM daemon" url="https://github.com/pikvm/kvmd" diff --git a/kvmd/__init__.py b/kvmd/__init__.py index 09b378bd..7e574a69 100644 --- a/kvmd/__init__.py +++ b/kvmd/__init__.py @@ -20,4 +20,4 @@ # ========================================================================== # -__version__ = "4.12" +__version__ = "4.13" diff --git a/setup.py b/setup.py index 0a115237..b06a31f3 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def main() -> None: setup( name="kvmd", - version="4.12", + version="4.13", url="https://github.com/pikvm/kvmd", license="GPLv3", author="Maxim Devaev", From 445e2e04e2cdc06794e1341a6ad9153bcff18bec Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Thu, 12 Sep 2024 17:05:35 +0300 Subject: [PATCH 34/88] oled: sensors class --- kvmd/apps/oled/__init__.py | 88 +++---------------------- kvmd/apps/oled/sensors.py | 128 +++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 78 deletions(-) create mode 100644 kvmd/apps/oled/sensors.py diff --git a/kvmd/apps/oled/__init__.py b/kvmd/apps/oled/__init__.py index 9fc4acb8..753933ff 100644 --- a/kvmd/apps/oled/__init__.py +++ b/kvmd/apps/oled/__init__.py @@ -3,7 +3,7 @@ # # # KVMD-OLED - A small OLED daemon for PiKVM. # # # -# Copyright (C) 2018 Maxim Devaev # +# Copyright (C) 2018-2024 Maxim Devaev # # # # 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 # @@ -23,15 +23,11 @@ import sys import os -import socket import signal import itertools import logging -import datetime import time -import netifaces -import psutil import usb.core from luma.core import cmdline as luma_cmdline @@ -41,76 +37,13 @@ from luma.core.render import canvas as luma_canvas from PIL import Image from PIL import ImageFont +from .sensors import Sensors + # ===== _logger = logging.getLogger("oled") -# ===== -def _get_ip() -> tuple[str, str]: - try: - gws = netifaces.gateways() - if "default" in gws: - for proto in [socket.AF_INET, socket.AF_INET6]: - if proto in gws["default"]: - iface = gws["default"][proto][1] - addrs = netifaces.ifaddresses(iface) - return (iface, addrs[proto][0]["addr"]) - - for iface in netifaces.interfaces(): - if not iface.startswith(("lo", "docker")): - addrs = netifaces.ifaddresses(iface) - for proto in [socket.AF_INET, socket.AF_INET6]: - if proto in addrs: - return (iface, addrs[proto][0]["addr"]) - except Exception: - # _logger.exception("Can't get iface/IP") - pass - return ("", "") - - -def _get_uptime() -> str: - uptime = datetime.timedelta(seconds=int(time.time() - psutil.boot_time())) - pl = {"days": uptime.days} - (pl["hours"], rem) = divmod(uptime.seconds, 3600) - (pl["mins"], pl["secs"]) = divmod(rem, 60) - return "{days}d {hours}h {mins}m".format(**pl) - - -def _get_temp(fahrenheit: bool) -> str: - try: - with open("/sys/class/thermal/thermal_zone0/temp") as temp_file: - temp = int((temp_file.read().strip())) / 1000 - if fahrenheit: - temp = temp * 9 / 5 + 32 - return f"{temp:.1f}\u00b0F" - return f"{temp:.1f}\u00b0C" - except Exception: - # _logger.exception("Can't read temp") - return "" - - -def _get_cpu() -> str: - st = psutil.cpu_times_percent() - user = st.user - st.guest - nice = st.nice - st.guest_nice - idle_all = st.idle + st.iowait - system_all = st.system + st.irq + st.softirq - virtual = st.guest + st.guest_nice - total = max(1, user + nice + system_all + idle_all + st.steal + virtual) - percent = int( - st.nice / total * 100 - + st.user / total * 100 - + system_all / total * 100 - + (st.steal + st.guest) / total * 100 - ) - return f"{percent}%" - - -def _get_mem() -> str: - return f"{int(psutil.virtual_memory().percent)}%" - - # ===== class Screen: def __init__( @@ -248,21 +181,20 @@ def main() -> None: # pylint: disable=too-many-locals,too-many-branches,too-man swim += 1 time.sleep(0.5) + sensors = Sensors(options.fahrenheit) + if device.height >= 64: while stop_reason is None: - (iface, ip) = _get_ip() - text = f"{socket.getfqdn()}\n{ip}\niface: {iface}\ntemp: {_get_temp(options.fahrenheit)}" - text += f"\ncpu: {_get_cpu()} mem: {_get_mem()}\n(__hb__) {_get_uptime()}" - draw(text) + text = "{fqdn}\n{ip}\niface: {iface}\ntemp: {temp}\ncpu: {cpu} mem: {mem}\n(__hb__) {uptime}" + draw(sensors.render(text)) else: summary = True while stop_reason is None: if summary: - text = f"{socket.getfqdn()}\n(__hb__) {_get_uptime()}\ntemp: {_get_temp(options.fahrenheit)}" + text = "{fqdn}\n(__hb__) {uptime}\ntemp: {temp}" else: - (iface, ip) = _get_ip() - text = "%s\n(__hb__) iface: %s\ncpu: %s mem: %s" % (ip, iface, _get_cpu(), _get_mem()) - draw(text) + text = "{ip}\n(__hb__) iface: {iface}\ncpu: {cpu} mem: {mem}" + draw(sensors.render(text)) summary = (not summary) if stop_reason is not None: diff --git a/kvmd/apps/oled/sensors.py b/kvmd/apps/oled/sensors.py new file mode 100644 index 00000000..9ae5cd8d --- /dev/null +++ b/kvmd/apps/oled/sensors.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +# ========================================================================== # +# # +# KVMD-OLED - A small OLED daemon for PiKVM. # +# # +# Copyright (C) 2018-2024 Maxim Devaev # +# # +# 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 . # +# # +# ========================================================================== # + + +import socket +import functools +import datetime +import time + +import netifaces +import psutil + + +# ===== +class Sensors: + def __init__(self, fahrenheit: bool) -> None: + self.__fahrenheit = fahrenheit + self.__sensors = { + "fqdn": socket.getfqdn, + "iface": self.__get_iface, + "ip": self.__get_ip, + "uptime": self.__get_uptime, + "temp": self.__get_temp, + "cpu": self.__get_cpu, + "mem": self.__get_mem, + } + + def render(self, text: str) -> str: + return text.format_map(self) + + def __getitem__(self, key: str) -> str: + return self.__sensors[key]() # type: ignore + + # ===== + + def __get_iface(self) -> str: + print("get_iface") + return self.__get_netconf(round(time.monotonic() / 0.3))[0] + + def __get_ip(self) -> str: + print("get_ip") + return self.__get_netconf(round(time.monotonic() / 0.3))[1] + + @functools.lru_cache(maxsize=1) + def __get_netconf(self, ts: int) -> tuple[str, str]: + _ = ts + try: + gws = netifaces.gateways() + if "default" in gws: + for proto in [socket.AF_INET, socket.AF_INET6]: + if proto in gws["default"]: + iface = gws["default"][proto][1] + addrs = netifaces.ifaddresses(iface) + return (iface, addrs[proto][0]["addr"]) + + for iface in netifaces.interfaces(): + if not iface.startswith(("lo", "docker")): + addrs = netifaces.ifaddresses(iface) + for proto in [socket.AF_INET, socket.AF_INET6]: + if proto in addrs: + return (iface, addrs[proto][0]["addr"]) + except Exception: + # _logger.exception("Can't get iface/IP") + pass + return ("", "") + + # ===== + + def __get_uptime(self) -> str: + uptime = datetime.timedelta(seconds=int(time.time() - psutil.boot_time())) + pl = {"days": uptime.days} + (pl["hours"], rem) = divmod(uptime.seconds, 3600) + (pl["mins"], pl["secs"]) = divmod(rem, 60) + return "{days}d {hours}h {mins}m".format(**pl) + + # ===== + + def __get_temp(self) -> str: + try: + with open("/sys/class/thermal/thermal_zone0/temp") as file: + temp = int(file.read().strip()) / 1000 + if self.__fahrenheit: + temp = temp * 9 / 5 + 32 + return f"{temp:.1f}\u00b0F" + return f"{temp:.1f}\u00b0C" + except Exception: + # _logger.exception("Can't read temp") + return "" + + # ===== + + def __get_cpu(self) -> str: + st = psutil.cpu_times_percent() + user = st.user - st.guest + nice = st.nice - st.guest_nice + idle_all = st.idle + st.iowait + system_all = st.system + st.irq + st.softirq + virtual = st.guest + st.guest_nice + total = max(1, user + nice + system_all + idle_all + st.steal + virtual) + percent = int( + st.nice / total * 100 + + st.user / total * 100 + + system_all / total * 100 + + (st.steal + st.guest) / total * 100 + ) + return f"{percent}%" + + def __get_mem(self) -> str: + return f"{int(psutil.virtual_memory().percent)}%" From 4bc2ca3c904b3138cc2385dab1481739d0cee5c8 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Fri, 13 Sep 2024 19:33:49 +0300 Subject: [PATCH 35/88] refactoring --- kvmd/apps/oled/__init__.py | 29 +------------------- kvmd/apps/oled/screen.py | 54 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 28 deletions(-) create mode 100644 kvmd/apps/oled/screen.py diff --git a/kvmd/apps/oled/__init__.py b/kvmd/apps/oled/__init__.py index 753933ff..db96ca2c 100644 --- a/kvmd/apps/oled/__init__.py +++ b/kvmd/apps/oled/__init__.py @@ -31,12 +31,10 @@ import time import usb.core from luma.core import cmdline as luma_cmdline -from luma.core.device import device as luma_device -from luma.core.render import canvas as luma_canvas -from PIL import Image from PIL import ImageFont +from .screen import Screen from .sensors import Sensors @@ -45,31 +43,6 @@ _logger = logging.getLogger("oled") # ===== -class Screen: - def __init__( - self, - device: luma_device, - font: ImageFont.FreeTypeFont, - font_spacing: int, - offset: tuple[int, int], - ) -> None: - - self.__device = device - self.__font = font - self.__font_spacing = font_spacing - self.__offset = offset - - def draw_text(self, text: str, offset_x: int=0) -> None: - with luma_canvas(self.__device) as draw: - offset = list(self.__offset) - offset[0] += offset_x - draw.multiline_text(offset, text, font=self.__font, spacing=self.__font_spacing, fill="white") - - def draw_image(self, image_path: str) -> None: - with luma_canvas(self.__device) as draw: - draw.bitmap(self.__offset, Image.open(image_path).convert("1"), fill="white") - - def _detect_geometry() -> dict: with open("/proc/device-tree/model") as file: is_cm4 = ("Compute Module 4" in file.read()) diff --git a/kvmd/apps/oled/screen.py b/kvmd/apps/oled/screen.py new file mode 100644 index 00000000..0e3301bb --- /dev/null +++ b/kvmd/apps/oled/screen.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# ========================================================================== # +# # +# KVMD-OLED - A small OLED daemon for PiKVM. # +# # +# Copyright (C) 2018-2024 Maxim Devaev # +# # +# 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 . # +# # +# ========================================================================== # + + +from luma.core.device import device as luma_device +from luma.core.render import canvas as luma_canvas + +from PIL import Image +from PIL import ImageFont + + +# ===== +class Screen: + def __init__( + self, + device: luma_device, + font: ImageFont.FreeTypeFont, + font_spacing: int, + offset: tuple[int, int], + ) -> None: + + self.__device = device + self.__font = font + self.__font_spacing = font_spacing + self.__offset = offset + + def draw_text(self, text: str, offset_x: int=0) -> None: + with luma_canvas(self.__device) as draw: + offset = list(self.__offset) + offset[0] += offset_x + draw.multiline_text(offset, text, font=self.__font, spacing=self.__font_spacing, fill="white") + + def draw_image(self, image_path: str) -> None: + with luma_canvas(self.__device) as draw: + draw.bitmap(self.__offset, Image.open(image_path).convert("1"), fill="white") From bd127c3fd3224f62d95f66e20af3c2aee483a7ca Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Fri, 13 Sep 2024 19:34:39 +0300 Subject: [PATCH 36/88] =?UTF-8?q?Bump=20version:=204.13=20=E2=86=92=204.14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- PKGBUILD | 2 +- kvmd/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 803d5272..7afb6e0d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 4.13 +current_version = 4.14 parse = (?P\d+)\.(?P\d+)(\.(?P\d+)(\-(?P[a-z]+))?)? serialize = {major}.{minor} diff --git a/PKGBUILD b/PKGBUILD index 2366cd35..1aaa394d 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -39,7 +39,7 @@ for _variant in "${_variants[@]}"; do pkgname+=(kvmd-platform-$_platform-$_board) done pkgbase=kvmd -pkgver=4.13 +pkgver=4.14 pkgrel=1 pkgdesc="The main PiKVM daemon" url="https://github.com/pikvm/kvmd" diff --git a/kvmd/__init__.py b/kvmd/__init__.py index 7e574a69..02bb5e02 100644 --- a/kvmd/__init__.py +++ b/kvmd/__init__.py @@ -20,4 +20,4 @@ # ========================================================================== # -__version__ = "4.13" +__version__ = "4.14" diff --git a/setup.py b/setup.py index b06a31f3..50f40975 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def main() -> None: setup( name="kvmd", - version="4.13", + version="4.14", url="https://github.com/pikvm/kvmd", license="GPLv3", author="Maxim Devaev", From 6ccd91a8d1c06cdc501b2b79014174e9b3c0c3f9 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Fri, 13 Sep 2024 22:07:59 +0300 Subject: [PATCH 37/88] removed print() --- kvmd/apps/oled/sensors.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/kvmd/apps/oled/sensors.py b/kvmd/apps/oled/sensors.py index 9ae5cd8d..26d7a1c5 100644 --- a/kvmd/apps/oled/sensors.py +++ b/kvmd/apps/oled/sensors.py @@ -53,11 +53,9 @@ class Sensors: # ===== def __get_iface(self) -> str: - print("get_iface") return self.__get_netconf(round(time.monotonic() / 0.3))[0] def __get_ip(self) -> str: - print("get_ip") return self.__get_netconf(round(time.monotonic() / 0.3))[1] @functools.lru_cache(maxsize=1) From b779c1853072297ca273ba3895141e821c0e722c Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Fri, 13 Sep 2024 22:08:43 +0300 Subject: [PATCH 38/88] =?UTF-8?q?Bump=20version:=204.14=20=E2=86=92=204.15?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- PKGBUILD | 2 +- kvmd/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 7afb6e0d..dd6fec45 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 4.14 +current_version = 4.15 parse = (?P\d+)\.(?P\d+)(\.(?P\d+)(\-(?P[a-z]+))?)? serialize = {major}.{minor} diff --git a/PKGBUILD b/PKGBUILD index 1aaa394d..96f8d0ea 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -39,7 +39,7 @@ for _variant in "${_variants[@]}"; do pkgname+=(kvmd-platform-$_platform-$_board) done pkgbase=kvmd -pkgver=4.14 +pkgver=4.15 pkgrel=1 pkgdesc="The main PiKVM daemon" url="https://github.com/pikvm/kvmd" diff --git a/kvmd/__init__.py b/kvmd/__init__.py index 02bb5e02..a2b40a13 100644 --- a/kvmd/__init__.py +++ b/kvmd/__init__.py @@ -20,4 +20,4 @@ # ========================================================================== # -__version__ = "4.14" +__version__ = "4.15" diff --git a/setup.py b/setup.py index 50f40975..14d9545c 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def main() -> None: setup( name="kvmd", - version="4.14", + version="4.15", url="https://github.com/pikvm/kvmd", license="GPLv3", author="Maxim Devaev", From c57334f214be1c31f52d486639d26c4fa1bd038f Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Mon, 16 Sep 2024 23:07:38 +0300 Subject: [PATCH 39/88] refactoring --- kvmd/apps/janus/runner.py | 19 +++----- kvmd/apps/janus/stun.py | 99 +++++++++++++++++++++++--------------- testenv/linters/flake8.ini | 3 +- 3 files changed, 68 insertions(+), 53 deletions(-) diff --git a/kvmd/apps/janus/runner.py b/kvmd/apps/janus/runner.py index e08fade0..cb46562a 100644 --- a/kvmd/apps/janus/runner.py +++ b/kvmd/apps/janus/runner.py @@ -11,15 +11,16 @@ from ... import aioproc from ...logging import get_logger +from .stun import StunNatType from .stun import Stun # ===== @dataclasses.dataclass(frozen=True) class _Netcfg: - nat_type: str = dataclasses.field(default="") - src_ip: str = dataclasses.field(default="") - ext_ip: str = dataclasses.field(default="") + nat_type: StunNatType = dataclasses.field(default=StunNatType.ERROR) + src_ip: str = dataclasses.field(default="") + ext_ip: str = dataclasses.field(default="") stun_host: str = dataclasses.field(default="") stun_port: int = dataclasses.field(default=0) @@ -92,8 +93,9 @@ class JanusRunner: # pylint: disable=too-many-instance-attributes async def __get_netcfg(self) -> _Netcfg: src_ip = (self.__get_default_ip() or "0.0.0.0") - (stun, (nat_type, ext_ip)) = await self.__get_stun_info(src_ip) - return _Netcfg(nat_type, src_ip, ext_ip, stun.host, stun.port) + info = await self.__stun.get_info(src_ip, 0) + # В текущей реализации _Netcfg() это копия StunInfo() + return _Netcfg(**dataclasses.asdict(info)) def __get_default_ip(self) -> str: try: @@ -115,13 +117,6 @@ class JanusRunner: # pylint: disable=too-many-instance-attributes get_logger().error("Can't get default IP: %s", tools.efmt(err)) return "" - async def __get_stun_info(self, src_ip: str) -> tuple[Stun, tuple[str, str]]: - try: - return (self.__stun, (await self.__stun.get_info(src_ip, 0))) - except Exception as err: - get_logger().error("Can't get STUN info: %s", tools.efmt(err)) - return (self.__stun, ("", "")) - # ===== @aiotools.atomic_fg diff --git a/kvmd/apps/janus/stun.py b/kvmd/apps/janus/stun.py index 5fea9da6..7c37dc77 100644 --- a/kvmd/apps/janus/stun.py +++ b/kvmd/apps/janus/stun.py @@ -4,6 +4,7 @@ import ipaddress import struct import secrets import dataclasses +import enum from ... import tools from ... import aiotools @@ -12,29 +13,39 @@ from ...logging import get_logger # ===== +class StunNatType(enum.Enum): + ERROR = "" + BLOCKED = "Blocked" + OPEN_INTERNET = "Open Internet" + SYMMETRIC_UDP_FW = "Symmetric UDP Firewall" + FULL_CONE_NAT = "Full Cone NAT" + RESTRICTED_NAT = "Restricted NAT" + RESTRICTED_PORT_NAT = "Restricted Port NAT" + SYMMETRIC_NAT = "Symmetric NAT" + CHANGED_ADDR_ERROR = "Error when testing on Changed-IP and Port" + + @dataclasses.dataclass(frozen=True) -class StunAddress: - ip: str +class StunInfo: + nat_type: StunNatType + src_ip: str + ext_ip: str + stun_host: str + stun_port: int + + +@dataclasses.dataclass(frozen=True) +class _StunAddress: + ip: str port: int @dataclasses.dataclass(frozen=True) -class StunResponse: - ok: bool - ext: (StunAddress | None) = dataclasses.field(default=None) - src: (StunAddress | None) = dataclasses.field(default=None) - changed: (StunAddress | None) = dataclasses.field(default=None) - - -class StunNatType: - BLOCKED = "Blocked" - OPEN_INTERNET = "Open Internet" - SYMMETRIC_UDP_FW = "Symmetric UDP Firewall" - FULL_CONE_NAT = "Full Cone NAT" - RESTRICTED_NAT = "Restricted NAT" - RESTRICTED_PORT_NAT = "Restricted Port NAT" - SYMMETRIC_NAT = "Symmetric NAT" - CHANGED_ADDR_ERROR = "Error when testing on Changed-IP and Port" +class _StunResponse: + ok: bool + ext: (_StunAddress | None) = dataclasses.field(default=None) + src: (_StunAddress | None) = dataclasses.field(default=None) + changed: (_StunAddress | None) = dataclasses.field(default=None) # ===== @@ -50,33 +61,44 @@ class Stun: retries_delay: float, ) -> None: - self.host = host - self.port = port + self.__host = host + self.__port = port self.__timeout = timeout self.__retries = retries self.__retries_delay = retries_delay self.__sock: (socket.socket | None) = None - async def get_info(self, src_ip: str, src_port: int) -> tuple[str, str]: + async def get_info(self, src_ip: str, src_port: int) -> StunInfo: (family, _, _, _, addr) = socket.getaddrinfo(src_ip, src_port, type=socket.SOCK_DGRAM)[0] + nat_type = StunNatType.ERROR + ext_ip = "" try: with socket.socket(family, socket.SOCK_DGRAM) as self.__sock: self.__sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.__sock.settimeout(self.__timeout) self.__sock.bind(addr) (nat_type, response) = await self.__get_nat_type(src_ip) - return (nat_type, (response.ext.ip if response.ext is not None else "")) + ext_ip = (response.ext.ip if response.ext is not None else "") + except Exception as err: + get_logger(0).error("Can't get STUN info: %s", tools.efmt(err)) finally: self.__sock = None + return StunInfo( + nat_type=nat_type, + src_ip=src_ip, + ext_ip=ext_ip, + stun_host=self.__host, + stun_port=self.__port, + ) - async def __get_nat_type(self, src_ip: str) -> tuple[str, StunResponse]: # pylint: disable=too-many-return-statements - first = await self.__make_request("First probe") + async def __get_nat_type(self, src_ip: str) -> tuple[StunNatType, _StunResponse]: # pylint: disable=too-many-return-statements + first = await self.__make_request("First probe", self.__host, b"") if not first.ok: return (StunNatType.BLOCKED, first) request = struct.pack(">HHI", 0x0003, 0x0004, 0x00000006) # Change-Request - response = await self.__make_request("Change request [ext_ip == src_ip]", request) + response = await self.__make_request("Change request [ext_ip == src_ip]", self.__host, request) if first.ext is not None and first.ext.ip == src_ip: if response.ok: @@ -88,20 +110,20 @@ class Stun: if first.changed is None: raise RuntimeError(f"Changed addr is None: {first}") - response = await self.__make_request("Change request [ext_ip != src_ip]", addr=first.changed) + response = await self.__make_request("Change request [ext_ip != src_ip]", first.changed, b"") if not response.ok: return (StunNatType.CHANGED_ADDR_ERROR, response) if response.ext == first.ext: request = struct.pack(">HHI", 0x0003, 0x0004, 0x00000002) - response = await self.__make_request("Change port", request, addr=first.changed.ip) + response = await self.__make_request("Change port", first.changed.ip, request) if response.ok: return (StunNatType.RESTRICTED_NAT, response) return (StunNatType.RESTRICTED_PORT_NAT, response) return (StunNatType.SYMMETRIC_NAT, response) - async def __make_request(self, ctx: str, request: bytes=b"", addr: (StunAddress | str | None)=None) -> StunResponse: + async def __make_request(self, ctx: str, addr: (_StunAddress | str), request: bytes) -> _StunResponse: # TODO: Support IPv6 and RFC 5389 # The first 4 bytes of the response are the Type (2) and Length (2) # The 5th byte is Reserved @@ -111,13 +133,10 @@ class Stun: # More info at: https://tools.ietf.org/html/rfc3489#section-11.2.1 # And at: https://tools.ietf.org/html/rfc5389#section-15.1 - if isinstance(addr, StunAddress): + if isinstance(addr, _StunAddress): addr_t = (addr.ip, addr.port) - elif isinstance(addr, str): - addr_t = (addr, self.port) - else: - assert addr is None - addr_t = (self.host, self.port) + else: # str + addr_t = (addr, self.__port) # https://datatracker.ietf.org/doc/html/rfc5389#section-6 trans_id = b"\x21\x12\xA4\x42" + secrets.token_bytes(12) @@ -130,9 +149,9 @@ class Stun: if error: get_logger(0).error("%s: Can't perform STUN request after %d retries; last error: %s", ctx, self.__retries, error) - return StunResponse(ok=False) + return _StunResponse(ok=False) - parsed: dict[str, StunAddress] = {} + parsed: dict[str, _StunAddress] = {} offset = 0 remaining = len(response) while remaining > 0: @@ -148,7 +167,7 @@ class Stun: parsed[field] = self.__parse_address(response[offset:], (trans_id if attr_type == 0x0020 else b"")) offset += attr_len remaining -= (4 + attr_len) - return StunResponse(ok=True, **parsed) + return _StunResponse(ok=True, **parsed) async def __inner_make_request(self, trans_id: bytes, request: bytes, addr: tuple[str, int]) -> tuple[bytes, str]: assert self.__sock is not None @@ -172,13 +191,13 @@ class Stun: return (response[20 : 20 + payload_len], "") # noqa: E203 - def __parse_address(self, data: bytes, trans_id: bytes) -> StunAddress: + def __parse_address(self, data: bytes, trans_id: bytes) -> _StunAddress: family = data[1] port = struct.unpack(">H", self.__trans_xor(data[2:4], trans_id))[0] if family == 0x01: - return StunAddress(str(ipaddress.IPv4Address(self.__trans_xor(data[4:8], trans_id))), port) + return _StunAddress(str(ipaddress.IPv4Address(self.__trans_xor(data[4:8], trans_id))), port) elif family == 0x02: - return StunAddress(str(ipaddress.IPv6Address(self.__trans_xor(data[4:20], trans_id))), port) + return _StunAddress(str(ipaddress.IPv6Address(self.__trans_xor(data[4:20], trans_id))), port) raise RuntimeError(f"Unknown family; received: {family}") def __trans_xor(self, data: bytes, trans_id: bytes) -> bytes: diff --git a/testenv/linters/flake8.ini b/testenv/linters/flake8.ini index e37c1dfa..768e0b89 100644 --- a/testenv/linters/flake8.ini +++ b/testenv/linters/flake8.ini @@ -1,8 +1,9 @@ [flake8] inline-quotes = double max-line-length = 160 -ignore = W503, E227, E241, E252, Q003 +ignore = W503, E221, E227, E241, E252, Q003 # W503 line break before binary operator +# E221 multiple spaces before operator # E227 missing whitespace around bitwise or shift operator # E241 multiple spaces after # E252 missing whitespace around parameter equals From b3e836e553bdf262bde38e57a60f828cae8d0ea5 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 17 Sep 2024 17:53:55 +0300 Subject: [PATCH 40/88] pikvm/pikvm#1386: Setup STUN by IP --- kvmd/apps/janus/runner.py | 4 ++-- kvmd/apps/janus/stun.py | 39 ++++++++++++++++++++++++++++++++------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/kvmd/apps/janus/runner.py b/kvmd/apps/janus/runner.py index cb46562a..c5ba26f2 100644 --- a/kvmd/apps/janus/runner.py +++ b/kvmd/apps/janus/runner.py @@ -21,7 +21,7 @@ class _Netcfg: nat_type: StunNatType = dataclasses.field(default=StunNatType.ERROR) src_ip: str = dataclasses.field(default="") ext_ip: str = dataclasses.field(default="") - stun_host: str = dataclasses.field(default="") + stun_ip: str = dataclasses.field(default="") stun_port: int = dataclasses.field(default=0) @@ -157,7 +157,7 @@ class JanusRunner: # pylint: disable=too-many-instance-attributes async def __start_janus_proc(self, netcfg: _Netcfg) -> None: assert self.__janus_proc is None placeholders = { - "o_stun_server": f"--stun-server={netcfg.stun_host}:{netcfg.stun_port}", + "o_stun_server": f"--stun-server={netcfg.stun_ip}:{netcfg.stun_port}", **{ key: str(value) for (key, value) in dataclasses.asdict(netcfg).items() diff --git a/kvmd/apps/janus/stun.py b/kvmd/apps/janus/stun.py index 7c37dc77..65f8b0a7 100644 --- a/kvmd/apps/janus/stun.py +++ b/kvmd/apps/janus/stun.py @@ -30,7 +30,7 @@ class StunInfo: nat_type: StunNatType src_ip: str ext_ip: str - stun_host: str + stun_ip: str stun_port: int @@ -67,38 +67,63 @@ class Stun: self.__retries = retries self.__retries_delay = retries_delay + self.__stun_ip = "" self.__sock: (socket.socket | None) = None async def get_info(self, src_ip: str, src_port: int) -> StunInfo: - (family, _, _, _, addr) = socket.getaddrinfo(src_ip, src_port, type=socket.SOCK_DGRAM)[0] nat_type = StunNatType.ERROR ext_ip = "" try: - with socket.socket(family, socket.SOCK_DGRAM) as self.__sock: + (src_fam, _, _, _, src_addr) = (await self.__retried_getaddrinfo_udp(src_ip, src_port))[0] + + stun_ips = [ + stun_addr[0] + for (stun_fam, _, _, _, stun_addr) in (await self.__retried_getaddrinfo_udp(self.__host, self.__port)) + if stun_fam == src_fam + ] + if not stun_ips: + raise RuntimeError(f"Can't resolve {src_fam.name} address for STUN") + if not self.__stun_ip or self.__stun_ip not in stun_ips: + # On new IP, changed family, etc. + self.__stun_ip = stun_ips[0] + + with socket.socket(src_fam, socket.SOCK_DGRAM) as self.__sock: self.__sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.__sock.settimeout(self.__timeout) - self.__sock.bind(addr) + self.__sock.bind(src_addr) (nat_type, response) = await self.__get_nat_type(src_ip) ext_ip = (response.ext.ip if response.ext is not None else "") except Exception as err: get_logger(0).error("Can't get STUN info: %s", tools.efmt(err)) finally: self.__sock = None + return StunInfo( nat_type=nat_type, src_ip=src_ip, ext_ip=ext_ip, - stun_host=self.__host, + stun_ip=self.__stun_ip, stun_port=self.__port, ) + async def __retried_getaddrinfo_udp(self, host: str, port: int) -> list: + retries = self.__retries + while True: + try: + return socket.getaddrinfo(host, port, type=socket.SOCK_DGRAM) + except Exception: + retries -= 1 + if retries == 0: + raise + await asyncio.sleep(self.__retries_delay) + async def __get_nat_type(self, src_ip: str) -> tuple[StunNatType, _StunResponse]: # pylint: disable=too-many-return-statements - first = await self.__make_request("First probe", self.__host, b"") + first = await self.__make_request("First probe", self.__stun_ip, b"") if not first.ok: return (StunNatType.BLOCKED, first) request = struct.pack(">HHI", 0x0003, 0x0004, 0x00000006) # Change-Request - response = await self.__make_request("Change request [ext_ip == src_ip]", self.__host, request) + response = await self.__make_request("Change request [ext_ip == src_ip]", self.__stun_ip, request) if first.ext is not None and first.ext.ip == src_ip: if response.ok: From f03ac695bd42fcf7f0e90787cb95333c8610fdab Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 17 Sep 2024 17:58:31 +0300 Subject: [PATCH 41/88] refactoring --- kvmd/apps/janus/stun.py | 64 ++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/kvmd/apps/janus/stun.py b/kvmd/apps/janus/stun.py index 65f8b0a7..d597192b 100644 --- a/kvmd/apps/janus/stun.py +++ b/kvmd/apps/janus/stun.py @@ -91,8 +91,8 @@ class Stun: self.__sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.__sock.settimeout(self.__timeout) self.__sock.bind(src_addr) - (nat_type, response) = await self.__get_nat_type(src_ip) - ext_ip = (response.ext.ip if response.ext is not None else "") + (nat_type, resp) = await self.__get_nat_type(src_ip) + ext_ip = (resp.ext.ip if resp.ext is not None else "") except Exception as err: get_logger(0).error("Can't get STUN info: %s", tools.efmt(err)) finally: @@ -122,33 +122,33 @@ class Stun: if not first.ok: return (StunNatType.BLOCKED, first) - request = struct.pack(">HHI", 0x0003, 0x0004, 0x00000006) # Change-Request - response = await self.__make_request("Change request [ext_ip == src_ip]", self.__stun_ip, request) + req = struct.pack(">HHI", 0x0003, 0x0004, 0x00000006) # Change-Request + resp = await self.__make_request("Change request [ext_ip == src_ip]", self.__stun_ip, req) if first.ext is not None and first.ext.ip == src_ip: - if response.ok: - return (StunNatType.OPEN_INTERNET, response) - return (StunNatType.SYMMETRIC_UDP_FW, response) + if resp.ok: + return (StunNatType.OPEN_INTERNET, resp) + return (StunNatType.SYMMETRIC_UDP_FW, resp) - if response.ok: - return (StunNatType.FULL_CONE_NAT, response) + if resp.ok: + return (StunNatType.FULL_CONE_NAT, resp) if first.changed is None: raise RuntimeError(f"Changed addr is None: {first}") - response = await self.__make_request("Change request [ext_ip != src_ip]", first.changed, b"") - if not response.ok: - return (StunNatType.CHANGED_ADDR_ERROR, response) + resp = await self.__make_request("Change request [ext_ip != src_ip]", first.changed, b"") + if not resp.ok: + return (StunNatType.CHANGED_ADDR_ERROR, resp) - if response.ext == first.ext: - request = struct.pack(">HHI", 0x0003, 0x0004, 0x00000002) - response = await self.__make_request("Change port", first.changed.ip, request) - if response.ok: - return (StunNatType.RESTRICTED_NAT, response) - return (StunNatType.RESTRICTED_PORT_NAT, response) + if resp.ext == first.ext: + req = struct.pack(">HHI", 0x0003, 0x0004, 0x00000002) + resp = await self.__make_request("Change port", first.changed.ip, req) + if resp.ok: + return (StunNatType.RESTRICTED_NAT, resp) + return (StunNatType.RESTRICTED_PORT_NAT, resp) - return (StunNatType.SYMMETRIC_NAT, response) + return (StunNatType.SYMMETRIC_NAT, resp) - async def __make_request(self, ctx: str, addr: (_StunAddress | str), request: bytes) -> _StunResponse: + async def __make_request(self, ctx: str, addr: (_StunAddress | str), req: bytes) -> _StunResponse: # TODO: Support IPv6 and RFC 5389 # The first 4 bytes of the response are the Type (2) and Length (2) # The 5th byte is Reserved @@ -165,9 +165,9 @@ class Stun: # https://datatracker.ietf.org/doc/html/rfc5389#section-6 trans_id = b"\x21\x12\xA4\x42" + secrets.token_bytes(12) - (response, error) = (b"", "") + (resp, error) = (b"", "") for _ in range(self.__retries): - (response, error) = await self.__inner_make_request(trans_id, request, addr_t) + (resp, error) = await self.__inner_make_request(trans_id, req, addr_t) if not error: break await asyncio.sleep(self.__retries_delay) @@ -178,9 +178,9 @@ class Stun: parsed: dict[str, _StunAddress] = {} offset = 0 - remaining = len(response) + remaining = len(resp) while remaining > 0: - (attr_type, attr_len) = struct.unpack(">HH", response[offset : offset + 4]) # noqa: E203 + (attr_type, attr_len) = struct.unpack(">HH", resp[offset : offset + 4]) # noqa: E203 offset += 4 field = { 0x0001: "ext", # MAPPED-ADDRESS @@ -189,32 +189,32 @@ class Stun: 0x0005: "changed", # CHANGED-ADDRESS }.get(attr_type) if field is not None: - parsed[field] = self.__parse_address(response[offset:], (trans_id if attr_type == 0x0020 else b"")) + parsed[field] = self.__parse_address(resp[offset:], (trans_id if attr_type == 0x0020 else b"")) offset += attr_len remaining -= (4 + attr_len) return _StunResponse(ok=True, **parsed) - async def __inner_make_request(self, trans_id: bytes, request: bytes, addr: tuple[str, int]) -> tuple[bytes, str]: + async def __inner_make_request(self, trans_id: bytes, req: bytes, addr: tuple[str, int]) -> tuple[bytes, str]: assert self.__sock is not None - request = struct.pack(">HH", 0x0001, len(request)) + trans_id + request # Bind Request + req = struct.pack(">HH", 0x0001, len(req)) + trans_id + req # Bind Request try: - await aiotools.run_async(self.__sock.sendto, request, addr) + await aiotools.run_async(self.__sock.sendto, req, addr) except Exception as err: return (b"", f"Send error: {tools.efmt(err)}") try: - response = (await aiotools.run_async(self.__sock.recvfrom, 2048))[0] + resp = (await aiotools.run_async(self.__sock.recvfrom, 2048))[0] except Exception as err: return (b"", f"Recv error: {tools.efmt(err)}") - (response_type, payload_len) = struct.unpack(">HH", response[:4]) + (response_type, payload_len) = struct.unpack(">HH", resp[:4]) if response_type != 0x0101: return (b"", f"Invalid response type: {response_type:#06x}") - if trans_id != response[4:20]: + if trans_id != resp[4:20]: return (b"", "Transaction ID mismatch") - return (response[20 : 20 + payload_len], "") # noqa: E203 + return (resp[20 : 20 + payload_len], "") # noqa: E203 def __parse_address(self, data: bytes, trans_id: bytes) -> _StunAddress: family = data[1] From 45270a09d7b5076bac96887a1e36d752882e3adf Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 17 Sep 2024 17:59:19 +0300 Subject: [PATCH 42/88] =?UTF-8?q?Bump=20version:=204.15=20=E2=86=92=204.16?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- PKGBUILD | 2 +- kvmd/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index dd6fec45..b306e063 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 4.15 +current_version = 4.16 parse = (?P\d+)\.(?P\d+)(\.(?P\d+)(\-(?P[a-z]+))?)? serialize = {major}.{minor} diff --git a/PKGBUILD b/PKGBUILD index 96f8d0ea..87a0cca7 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -39,7 +39,7 @@ for _variant in "${_variants[@]}"; do pkgname+=(kvmd-platform-$_platform-$_board) done pkgbase=kvmd -pkgver=4.15 +pkgver=4.16 pkgrel=1 pkgdesc="The main PiKVM daemon" url="https://github.com/pikvm/kvmd" diff --git a/kvmd/__init__.py b/kvmd/__init__.py index a2b40a13..a7ce5b0b 100644 --- a/kvmd/__init__.py +++ b/kvmd/__init__.py @@ -20,4 +20,4 @@ # ========================================================================== # -__version__ = "4.15" +__version__ = "4.16" diff --git a/setup.py b/setup.py index 14d9545c..da2c4a34 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def main() -> None: setup( name="kvmd", - version="4.15", + version="4.16", url="https://github.com/pikvm/kvmd", license="GPLv3", author="Maxim Devaev", From 7a53f1445619fc471c2823e7081de8b6039b938e Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Wed, 18 Sep 2024 04:37:43 +0300 Subject: [PATCH 43/88] refactoring --- kvmd/aiogp.py | 12 ++-- kvmd/aiohelpers.py | 4 +- kvmd/aiotools.py | 6 +- kvmd/apps/__init__.py | 8 +-- kvmd/apps/edidconf/__init__.py | 4 +- kvmd/apps/htpasswd/__init__.py | 4 +- kvmd/apps/ipmi/server.py | 9 +-- kvmd/apps/janus/runner.py | 4 +- kvmd/apps/janus/stun.py | 18 +++--- kvmd/apps/kvmd/api/atx.py | 12 ++-- kvmd/apps/kvmd/api/auth.py | 26 ++++---- kvmd/apps/kvmd/api/hid.py | 54 ++++++++--------- kvmd/apps/kvmd/api/info.py | 8 +-- kvmd/apps/kvmd/api/log.py | 8 +-- kvmd/apps/kvmd/api/msd.py | 62 +++++++++---------- kvmd/apps/kvmd/api/redfish.py | 4 +- kvmd/apps/kvmd/api/streamer.py | 28 ++++----- kvmd/apps/kvmd/api/ugpio.py | 16 ++--- kvmd/apps/kvmd/info/extras.py | 8 +-- kvmd/apps/kvmd/info/fan.py | 8 +-- kvmd/apps/kvmd/info/hw.py | 20 +++---- kvmd/apps/kvmd/ocr.py | 4 +- kvmd/apps/kvmd/server.py | 14 ++--- kvmd/apps/kvmd/streamer.py | 8 +-- kvmd/apps/kvmd/sysunit.py | 4 +- kvmd/apps/otg/__init__.py | 4 +- kvmd/apps/otgmsd/__init__.py | 6 +- kvmd/apps/otgnet/__init__.py | 4 +- kvmd/apps/pst/server.py | 8 +-- kvmd/apps/pstrun/__init__.py | 4 +- kvmd/apps/vnc/rfb/__init__.py | 8 +-- kvmd/apps/vnc/rfb/errors.py | 4 +- kvmd/apps/vnc/rfb/stream.py | 24 ++++---- kvmd/apps/vnc/server.py | 12 ++-- kvmd/apps/vnc/vncauth.py | 4 +- kvmd/apps/watchdog/__init__.py | 12 ++-- kvmd/clients/kvmd.py | 8 +-- kvmd/clients/streamer.py | 14 ++--- kvmd/helpers/remount/__init__.py | 32 +++++----- kvmd/htclient.py | 30 +++++----- kvmd/htserver.py | 80 ++++++++++++------------- kvmd/inotify.py | 4 +- kvmd/keyboard/keysym.py | 4 +- kvmd/network.py | 6 +- kvmd/plugins/atx/gpio.py | 16 ++--- kvmd/plugins/auth/http.py | 4 +- kvmd/plugins/auth/ldap.py | 8 +-- kvmd/plugins/auth/radius.py | 8 +-- kvmd/plugins/hid/_mcu/__init__.py | 46 +++++++------- kvmd/plugins/hid/_mcu/gpio.py | 22 +++---- kvmd/plugins/hid/_mcu/proto.py | 18 +++--- kvmd/plugins/hid/bt/server.py | 20 +++---- kvmd/plugins/hid/ch9329/__init__.py | 4 +- kvmd/plugins/hid/otg/device.py | 28 ++++----- kvmd/plugins/hid/serial.py | 8 +-- kvmd/plugins/hid/spi.py | 20 +++---- kvmd/plugins/msd/otg/drive.py | 4 +- kvmd/plugins/ugpio/anelpwr.py | 18 +++--- kvmd/plugins/ugpio/cmd.py | 4 +- kvmd/plugins/ugpio/cmdret.py | 4 +- kvmd/plugins/ugpio/extron.py | 4 +- kvmd/plugins/ugpio/ezcoo.py | 4 +- kvmd/plugins/ugpio/gpio.py | 18 +++--- kvmd/plugins/ugpio/hidrelay.py | 12 ++-- kvmd/plugins/ugpio/hue.py | 16 ++--- kvmd/plugins/ugpio/ipmi.py | 8 +-- kvmd/plugins/ugpio/locator.py | 14 ++--- kvmd/plugins/ugpio/noyito.py | 12 ++-- kvmd/plugins/ugpio/pway.py | 4 +- kvmd/plugins/ugpio/pwm.py | 8 +-- kvmd/plugins/ugpio/tesmart.py | 12 ++-- kvmd/plugins/ugpio/xh_hk4401.py | 4 +- kvmd/tools.py | 4 +- kvmd/validators/net.py | 4 +- kvmd/validators/os.py | 4 +- kvmd/yamlconf/__init__.py | 8 +-- kvmd/yamlconf/loader.py | 4 +- testenv/tests/plugins/auth/test_http.py | 6 +- web/share/js/kvm/msd.js | 4 +- web/share/js/kvm/recorder.js | 4 +- web/share/js/kvm/session.js | 4 +- web/share/js/kvm/stream_janus.js | 4 +- web/share/js/wm.js | 18 +++--- 83 files changed, 517 insertions(+), 516 deletions(-) diff --git a/kvmd/aiogp.py b/kvmd/aiogp.py index 7ebe4ade..3ee0ea2b 100644 --- a/kvmd/aiogp.py +++ b/kvmd/aiogp.py @@ -83,9 +83,9 @@ class AioReader: # pylint: disable=too-many-instance-attributes self.__path, consumer=self.__consumer, config={tuple(pins): gpiod.LineSettings(edge_detection=gpiod.line.Edge.BOTH)}, - ) as line_request: + ) as line_req: - line_request.wait_edge_events(0.1) + line_req.wait_edge_events(0.1) self.__values = { pin: _DebouncedValue( initial=bool(value.value), @@ -93,14 +93,14 @@ class AioReader: # pylint: disable=too-many-instance-attributes notifier=self.__notifier, loop=self.__loop, ) - for (pin, value) in zip(pins, line_request.get_values(pins)) + for (pin, value) in zip(pins, line_req.get_values(pins)) } self.__loop.call_soon_threadsafe(self.__notifier.notify) while not self.__stop_event.is_set(): - if line_request.wait_edge_events(1): + if line_req.wait_edge_events(1): new: dict[int, bool] = {} - for event in line_request.read_edge_events(): + for event in line_req.read_edge_events(): (pin, value) = self.__parse_event(event) new[pin] = value for (pin, value) in new.items(): @@ -110,7 +110,7 @@ class AioReader: # pylint: disable=too-many-instance-attributes # Размер буфера ядра - 16 эвентов на линии. При превышении этого числа, # новые эвенты потеряются. Это не баг, это фича, как мне объяснили в LKML. # Штош. Будем с этим жить и синхронизировать состояния при таймауте. - for (pin, value) in zip(pins, line_request.get_values(pins)): + for (pin, value) in zip(pins, line_req.get_values(pins)): self.__values[pin].set(bool(value.value)) # type: ignore def __parse_event(self, event: gpiod.EdgeEvent) -> tuple[int, bool]: diff --git a/kvmd/aiohelpers.py b/kvmd/aiohelpers.py index c3b257d1..70fac32d 100644 --- a/kvmd/aiohelpers.py +++ b/kvmd/aiohelpers.py @@ -42,7 +42,7 @@ async def remount(name: str, base_cmd: list[str], rw: bool) -> bool: if proc.returncode != 0: assert proc.returncode is not None raise subprocess.CalledProcessError(proc.returncode, cmd) - except Exception as err: - logger.error("Can't remount %s storage: %s", name, tools.efmt(err)) + except Exception as ex: + logger.error("Can't remount %s storage: %s", name, tools.efmt(ex)) return False return True diff --git a/kvmd/aiotools.py b/kvmd/aiotools.py index c859e176..5d284a94 100644 --- a/kvmd/aiotools.py +++ b/kvmd/aiotools.py @@ -112,9 +112,9 @@ def shield_fg(aw: Awaitable): # type: ignore if inner.cancelled(): outer.forced_cancel() else: - err = inner.exception() - if err is not None: - outer.set_exception(err) + ex = inner.exception() + if ex is not None: + outer.set_exception(ex) else: outer.set_result(inner.result()) diff --git a/kvmd/apps/__init__.py b/kvmd/apps/__init__.py index 035b3cc4..dd1b8ff9 100644 --- a/kvmd/apps/__init__.py +++ b/kvmd/apps/__init__.py @@ -171,8 +171,8 @@ def _init_config(config_path: str, override_options: list[str], **load_flags: bo config_path = os.path.expanduser(config_path) try: raw_config: dict = load_yaml_file(config_path) - except Exception as err: - raise SystemExit(f"ConfigError: Can't read config file {config_path!r}:\n{tools.efmt(err)}") + except Exception as ex: + raise SystemExit(f"ConfigError: Can't read config file {config_path!r}:\n{tools.efmt(ex)}") if not isinstance(raw_config, dict): raise SystemExit(f"ConfigError: Top-level of the file {config_path!r} must be a dictionary") @@ -187,8 +187,8 @@ def _init_config(config_path: str, override_options: list[str], **load_flags: bo config = make_config(raw_config, scheme) return config - except (ConfigError, UnknownPluginError) as err: - raise SystemExit(f"ConfigError: {err}") + except (ConfigError, UnknownPluginError) as ex: + raise SystemExit(f"ConfigError: {ex}") def _patch_raw(raw_config: dict) -> None: # pylint: disable=too-many-branches diff --git a/kvmd/apps/edidconf/__init__.py b/kvmd/apps/edidconf/__init__.py index aabc8a8f..d213a871 100644 --- a/kvmd/apps/edidconf/__init__.py +++ b/kvmd/apps/edidconf/__init__.py @@ -402,5 +402,5 @@ def main(argv: (list[str] | None)=None) -> None: # pylint: disable=too-many-bra f"--set-edid=file={orig_edid_path}", "--info-edid", ], stdout=sys.stderr, check=True) - except subprocess.CalledProcessError as err: - raise SystemExit(str(err)) + except subprocess.CalledProcessError as ex: + raise SystemExit(str(ex)) diff --git a/kvmd/apps/htpasswd/__init__.py b/kvmd/apps/htpasswd/__init__.py index fd006ee2..9e857abc 100644 --- a/kvmd/apps/htpasswd/__init__.py +++ b/kvmd/apps/htpasswd/__init__.py @@ -155,5 +155,5 @@ def main(argv: (list[str] | None)=None) -> None: options = parser.parse_args(argv[1:]) try: options.cmd(config, options) - except ValidatorError as err: - raise SystemExit(str(err)) + except ValidatorError as ex: + raise SystemExit(str(ex)) diff --git a/kvmd/apps/ipmi/server.py b/kvmd/apps/ipmi/server.py index c6e35fae..2fc897f9 100644 --- a/kvmd/apps/ipmi/server.py +++ b/kvmd/apps/ipmi/server.py @@ -101,6 +101,7 @@ class IpmiServer(BaseIpmiServer): # pylint: disable=too-many-instance-attribute # ===== def handle_raw_request(self, request: dict, session: IpmiServerSession) -> None: + # Parameter 'request' has been renamed to 'req' in overriding method handler = { (6, 1): (lambda _, session: self.send_device_id(session)), # Get device ID (6, 7): self.__get_power_state_handler, # Power state @@ -145,13 +146,13 @@ class IpmiServer(BaseIpmiServer): # pylint: disable=too-many-instance-attribute data = [int(result["leds"]["power"]), 0, 0] session.send_ipmi_response(data=data) - def __chassis_control_handler(self, request: dict, session: IpmiServerSession) -> None: + def __chassis_control_handler(self, req: dict, session: IpmiServerSession) -> None: action = { 0: "off_hard", 1: "on", 3: "reset_hard", 5: "off", - }.get(request["data"][0], "") + }.get(req["data"][0], "") if action: if not self.__make_request(session, f"atx.switch_power({action})", "atx.switch_power", action=action): code = 0xC0 # Try again later @@ -171,8 +172,8 @@ class IpmiServer(BaseIpmiServer): # pylint: disable=too-many-instance-attribute async with self.__kvmd.make_session(credentials.kvmd_user, credentials.kvmd_passwd) as kvmd_session: func = functools.reduce(getattr, func_path.split("."), kvmd_session) return (await func(**kwargs)) - except (aiohttp.ClientError, asyncio.TimeoutError) as err: - logger.error("[%s]: Can't perform request %s: %s", session.sockaddr[0], name, err) + except (aiohttp.ClientError, asyncio.TimeoutError) as ex: + logger.error("[%s]: Can't perform request %s: %s", session.sockaddr[0], name, ex) raise return aiotools.run_sync(runner()) diff --git a/kvmd/apps/janus/runner.py b/kvmd/apps/janus/runner.py index c5ba26f2..9e426021 100644 --- a/kvmd/apps/janus/runner.py +++ b/kvmd/apps/janus/runner.py @@ -113,8 +113,8 @@ class JanusRunner: # pylint: disable=too-many-instance-attributes for proto in [socket.AF_INET, socket.AF_INET6]: if proto in addrs: return addrs[proto][0]["addr"] - except Exception as err: - get_logger().error("Can't get default IP: %s", tools.efmt(err)) + except Exception as ex: + get_logger().error("Can't get default IP: %s", tools.efmt(ex)) return "" # ===== diff --git a/kvmd/apps/janus/stun.py b/kvmd/apps/janus/stun.py index d597192b..41cd86e7 100644 --- a/kvmd/apps/janus/stun.py +++ b/kvmd/apps/janus/stun.py @@ -93,8 +93,8 @@ class Stun: self.__sock.bind(src_addr) (nat_type, resp) = await self.__get_nat_type(src_ip) ext_ip = (resp.ext.ip if resp.ext is not None else "") - except Exception as err: - get_logger(0).error("Can't get STUN info: %s", tools.efmt(err)) + except Exception as ex: + get_logger(0).error("Can't get STUN info: %s", tools.efmt(ex)) finally: self.__sock = None @@ -201,16 +201,16 @@ class Stun: try: await aiotools.run_async(self.__sock.sendto, req, addr) - except Exception as err: - return (b"", f"Send error: {tools.efmt(err)}") + except Exception as ex: + return (b"", f"Send error: {tools.efmt(ex)}") try: resp = (await aiotools.run_async(self.__sock.recvfrom, 2048))[0] - except Exception as err: - return (b"", f"Recv error: {tools.efmt(err)}") + except Exception as ex: + return (b"", f"Recv error: {tools.efmt(ex)}") - (response_type, payload_len) = struct.unpack(">HH", resp[:4]) - if response_type != 0x0101: - return (b"", f"Invalid response type: {response_type:#06x}") + (resp_type, payload_len) = struct.unpack(">HH", resp[:4]) + if resp_type != 0x0101: + return (b"", f"Invalid response type: {resp_type:#06x}") if trans_id != resp[4:20]: return (b"", "Transaction ID mismatch") diff --git a/kvmd/apps/kvmd/api/atx.py b/kvmd/apps/kvmd/api/atx.py index b7f74c99..df952b7b 100644 --- a/kvmd/apps/kvmd/api/atx.py +++ b/kvmd/apps/kvmd/api/atx.py @@ -45,9 +45,9 @@ class AtxApi: return make_json_response(await self.__atx.get_state()) @exposed_http("POST", "/atx/power") - async def __power_handler(self, request: Request) -> Response: - action = valid_atx_power_action(request.query.get("action")) - wait = valid_bool(request.query.get("wait", False)) + async def __power_handler(self, req: Request) -> Response: + action = valid_atx_power_action(req.query.get("action")) + wait = valid_bool(req.query.get("wait", False)) await ({ "on": self.__atx.power_on, "off": self.__atx.power_off, @@ -57,9 +57,9 @@ class AtxApi: return make_json_response() @exposed_http("POST", "/atx/click") - async def __click_handler(self, request: Request) -> Response: - button = valid_atx_button(request.query.get("button")) - wait = valid_bool(request.query.get("wait", False)) + async def __click_handler(self, req: Request) -> Response: + button = valid_atx_button(req.query.get("button")) + wait = valid_bool(req.query.get("wait", False)) await ({ "power": self.__atx.click_power, "power_long": self.__atx.click_power_long, diff --git a/kvmd/apps/kvmd/api/auth.py b/kvmd/apps/kvmd/api/auth.py index 0c7d5484..dee4a85d 100644 --- a/kvmd/apps/kvmd/api/auth.py +++ b/kvmd/apps/kvmd/api/auth.py @@ -43,34 +43,34 @@ from ..auth import AuthManager _COOKIE_AUTH_TOKEN = "auth_token" -async def check_request_auth(auth_manager: AuthManager, exposed: HttpExposed, request: Request) -> None: +async def check_request_auth(auth_manager: AuthManager, exposed: HttpExposed, req: Request) -> None: if auth_manager.is_auth_required(exposed): - user = request.headers.get("X-KVMD-User", "") + user = req.headers.get("X-KVMD-User", "") if user: user = valid_user(user) - passwd = request.headers.get("X-KVMD-Passwd", "") - set_request_auth_info(request, f"{user} (xhdr)") + passwd = req.headers.get("X-KVMD-Passwd", "") + set_request_auth_info(req, f"{user} (xhdr)") if not (await auth_manager.authorize(user, valid_passwd(passwd))): raise ForbiddenError() return - token = request.cookies.get(_COOKIE_AUTH_TOKEN, "") + token = req.cookies.get(_COOKIE_AUTH_TOKEN, "") if token: user = auth_manager.check(valid_auth_token(token)) # type: ignore if not user: - set_request_auth_info(request, "- (token)") + set_request_auth_info(req, "- (token)") raise ForbiddenError() - set_request_auth_info(request, f"{user} (token)") + set_request_auth_info(req, f"{user} (token)") return - basic_auth = request.headers.get("Authorization", "") + basic_auth = req.headers.get("Authorization", "") if basic_auth and basic_auth[:6].lower() == "basic ": try: (user, passwd) = base64.b64decode(basic_auth[6:]).decode("utf-8").split(":") except Exception: raise UnauthorizedError() user = valid_user(user) - set_request_auth_info(request, f"{user} (basic)") + set_request_auth_info(req, f"{user} (basic)") if not (await auth_manager.authorize(user, valid_passwd(passwd))): raise ForbiddenError() return @@ -85,9 +85,9 @@ class AuthApi: # ===== @exposed_http("POST", "/auth/login", auth_required=False) - async def __login_handler(self, request: Request) -> Response: + async def __login_handler(self, req: Request) -> Response: if self.__auth_manager.is_auth_enabled(): - credentials = await request.post() + credentials = await req.post() token = await self.__auth_manager.login( user=valid_user(credentials.get("user", "")), passwd=valid_passwd(credentials.get("passwd", "")), @@ -98,9 +98,9 @@ class AuthApi: return make_json_response() @exposed_http("POST", "/auth/logout") - async def __logout_handler(self, request: Request) -> Response: + async def __logout_handler(self, req: Request) -> Response: if self.__auth_manager.is_auth_enabled(): - token = valid_auth_token(request.cookies.get(_COOKIE_AUTH_TOKEN, "")) + token = valid_auth_token(req.cookies.get(_COOKIE_AUTH_TOKEN, "")) self.__auth_manager.logout(token) return make_json_response() diff --git a/kvmd/apps/kvmd/api/hid.py b/kvmd/apps/kvmd/api/hid.py index 326daf44..107fc858 100644 --- a/kvmd/apps/kvmd/api/hid.py +++ b/kvmd/apps/kvmd/api/hid.py @@ -85,22 +85,22 @@ class HidApi: return make_json_response(await self.__hid.get_state()) @exposed_http("POST", "/hid/set_params") - async def __set_params_handler(self, request: Request) -> Response: + async def __set_params_handler(self, req: Request) -> Response: params = { - key: validator(request.query.get(key)) + key: validator(req.query.get(key)) for (key, validator) in [ ("keyboard_output", valid_hid_keyboard_output), ("mouse_output", valid_hid_mouse_output), ("jiggler", valid_bool), ] - if request.query.get(key) is not None + if req.query.get(key) is not None } self.__hid.set_params(**params) # type: ignore return make_json_response() @exposed_http("POST", "/hid/set_connected") - async def __set_connected_handler(self, request: Request) -> Response: - self.__hid.set_connected(valid_bool(request.query.get("connected"))) + async def __set_connected_handler(self, req: Request) -> Response: + self.__hid.set_connected(valid_bool(req.query.get("connected"))) return make_json_response() @exposed_http("POST", "/hid/reset") @@ -128,12 +128,12 @@ class HidApi: return make_json_response(await self.get_keymaps()) @exposed_http("POST", "/hid/print") - async def __print_handler(self, request: Request) -> Response: - text = await request.text() - limit = int(valid_int_f0(request.query.get("limit", 1024))) + async def __print_handler(self, req: Request) -> Response: + text = await req.text() + limit = int(valid_int_f0(req.query.get("limit", 1024))) if limit > 0: text = text[:limit] - symmap = self.__ensure_symmap(request.query.get("keymap", self.__default_keymap_name)) + symmap = self.__ensure_symmap(req.query.get("keymap", self.__default_keymap_name)) self.__hid.send_key_events(text_to_web_keys(text, symmap)) return make_json_response() @@ -257,21 +257,21 @@ class HidApi: # ===== @exposed_http("POST", "/hid/events/send_key") - async def __events_send_key_handler(self, request: Request) -> Response: - key = valid_hid_key(request.query.get("key")) + async def __events_send_key_handler(self, req: Request) -> Response: + key = valid_hid_key(req.query.get("key")) if key not in self.__ignore_keys: - if "state" in request.query: - state = valid_bool(request.query["state"]) + if "state" in req.query: + state = valid_bool(req.query["state"]) self.__hid.send_key_events([(key, state)]) else: self.__hid.send_key_events([(key, True), (key, False)]) return make_json_response() @exposed_http("POST", "/hid/events/send_mouse_button") - async def __events_send_mouse_button_handler(self, request: Request) -> Response: - button = valid_hid_mouse_button(request.query.get("button")) - if "state" in request.query: - state = valid_bool(request.query["state"]) + async def __events_send_mouse_button_handler(self, req: Request) -> Response: + button = valid_hid_mouse_button(req.query.get("button")) + if "state" in req.query: + state = valid_bool(req.query["state"]) self.__hid.send_mouse_button_event(button, state) else: self.__hid.send_mouse_button_event(button, True) @@ -279,23 +279,23 @@ class HidApi: return make_json_response() @exposed_http("POST", "/hid/events/send_mouse_move") - async def __events_send_mouse_move_handler(self, request: Request) -> Response: - to_x = valid_hid_mouse_move(request.query.get("to_x")) - to_y = valid_hid_mouse_move(request.query.get("to_y")) + async def __events_send_mouse_move_handler(self, req: Request) -> Response: + to_x = valid_hid_mouse_move(req.query.get("to_x")) + to_y = valid_hid_mouse_move(req.query.get("to_y")) self.__send_mouse_move_event(to_x, to_y) return make_json_response() @exposed_http("POST", "/hid/events/send_mouse_relative") - async def __events_send_mouse_relative_handler(self, request: Request) -> Response: - return self.__process_http_delta_event(request, self.__hid.send_mouse_relative_event) + async def __events_send_mouse_relative_handler(self, req: Request) -> Response: + return self.__process_http_delta_event(req, self.__hid.send_mouse_relative_event) @exposed_http("POST", "/hid/events/send_mouse_wheel") - async def __events_send_mouse_wheel_handler(self, request: Request) -> Response: - return self.__process_http_delta_event(request, self.__hid.send_mouse_wheel_event) + async def __events_send_mouse_wheel_handler(self, req: Request) -> Response: + return self.__process_http_delta_event(req, self.__hid.send_mouse_wheel_event) - def __process_http_delta_event(self, request: Request, handler: Callable[[int, int], None]) -> Response: - delta_x = valid_hid_mouse_delta(request.query.get("delta_x")) - delta_y = valid_hid_mouse_delta(request.query.get("delta_y")) + def __process_http_delta_event(self, req: Request, handler: Callable[[int, int], None]) -> Response: + delta_x = valid_hid_mouse_delta(req.query.get("delta_x")) + delta_y = valid_hid_mouse_delta(req.query.get("delta_y")) handler(delta_x, delta_y) return make_json_response() diff --git a/kvmd/apps/kvmd/api/info.py b/kvmd/apps/kvmd/api/info.py index 6a7fbf4e..a0be01a5 100644 --- a/kvmd/apps/kvmd/api/info.py +++ b/kvmd/apps/kvmd/api/info.py @@ -41,17 +41,17 @@ class InfoApi: # ===== @exposed_http("GET", "/info") - async def __common_state_handler(self, request: Request) -> Response: - fields = self.__valid_info_fields(request) + 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) - def __valid_info_fields(self, request: Request) -> list[str]: + def __valid_info_fields(self, req: Request) -> list[str]: subs = self.__info_manager.get_subs() return sorted(valid_info_fields( - arg=request.query.get("fields", ",".join(subs)), + arg=req.query.get("fields", ",".join(subs)), variants=subs, ) or subs) diff --git a/kvmd/apps/kvmd/api/log.py b/kvmd/apps/kvmd/api/log.py index 5bead7f7..c82d6bd9 100644 --- a/kvmd/apps/kvmd/api/log.py +++ b/kvmd/apps/kvmd/api/log.py @@ -47,12 +47,12 @@ class LogApi: # ===== @exposed_http("GET", "/log") - async def __log_handler(self, request: Request) -> StreamResponse: + async def __log_handler(self, req: Request) -> StreamResponse: if self.__log_reader is None: raise LogReaderDisabledError() - seek = valid_log_seek(request.query.get("seek", 0)) - follow = valid_bool(request.query.get("follow", False)) - response = await start_streaming(request, "text/plain") + seek = valid_log_seek(req.query.get("seek", 0)) + follow = valid_bool(req.query.get("follow", False)) + response = await start_streaming(req, "text/plain") async for record in self.__log_reader.poll_log(seek, follow): await response.write(("[%s %s] --- %s" % ( record["dt"].strftime("%Y-%m-%d %H:%M:%S"), diff --git a/kvmd/apps/kvmd/api/msd.py b/kvmd/apps/kvmd/api/msd.py index dcb6ef62..2fa2eb9b 100644 --- a/kvmd/apps/kvmd/api/msd.py +++ b/kvmd/apps/kvmd/api/msd.py @@ -66,29 +66,29 @@ class MsdApi: return make_json_response(await self.__msd.get_state()) @exposed_http("POST", "/msd/set_params") - async def __set_params_handler(self, request: Request) -> Response: + async def __set_params_handler(self, req: Request) -> Response: params = { - key: validator(request.query.get(param)) + key: validator(req.query.get(param)) for (param, key, validator) in [ ("image", "name", (lambda arg: str(arg).strip() and valid_msd_image_name(arg))), ("cdrom", "cdrom", valid_bool), ("rw", "rw", valid_bool), ] - if request.query.get(param) is not None + if req.query.get(param) is not None } await self.__msd.set_params(**params) # type: ignore return make_json_response() @exposed_http("POST", "/msd/set_connected") - async def __set_connected_handler(self, request: Request) -> Response: - await self.__msd.set_connected(valid_bool(request.query.get("connected"))) + async def __set_connected_handler(self, req: Request) -> Response: + await self.__msd.set_connected(valid_bool(req.query.get("connected"))) return make_json_response() # ===== @exposed_http("GET", "/msd/read") - async def __read_handler(self, request: Request) -> StreamResponse: - name = valid_msd_image_name(request.query.get("image")) + async def __read_handler(self, req: Request) -> StreamResponse: + name = valid_msd_image_name(req.query.get("image")) compressors = { "": ("", None), "none": ("", None), @@ -96,7 +96,7 @@ class MsdApi: "zstd": (".zst", (lambda: zstandard.ZstdCompressor().compressobj())), # pylint: disable=unnecessary-lambda } (suffix, make_compressor) = compressors[check_string_in_list( - arg=request.query.get("compress", ""), + arg=req.query.get("compress", ""), name="Compression mode", variants=set(compressors), )] @@ -127,7 +127,7 @@ class MsdApi: src = compressed() size = -1 - response = await start_streaming(request, "application/octet-stream", size, name + suffix) + response = await start_streaming(req, "application/octet-stream", size, name + suffix) async for chunk in src: await response.write(chunk) return response @@ -135,28 +135,28 @@ class MsdApi: # ===== @exposed_http("POST", "/msd/write") - async def __write_handler(self, request: Request) -> Response: - unsafe_prefix = request.query.get("prefix", "") + "/" - name = valid_msd_image_name(unsafe_prefix + request.query.get("image", "")) - size = valid_int_f0(request.content_length) - remove_incomplete = self.__get_remove_incomplete(request) + async def __write_handler(self, req: Request) -> Response: + unsafe_prefix = req.query.get("prefix", "") + "/" + name = valid_msd_image_name(unsafe_prefix + req.query.get("image", "")) + size = valid_int_f0(req.content_length) + remove_incomplete = self.__get_remove_incomplete(req) written = 0 async with self.__msd.write_image(name, size, remove_incomplete) as writer: chunk_size = writer.get_chunk_size() while True: - chunk = await request.content.read(chunk_size) + chunk = await req.content.read(chunk_size) if not chunk: break written = await writer.write_chunk(chunk) return make_json_response(self.__make_write_info(name, size, written)) @exposed_http("POST", "/msd/write_remote") - async def __write_remote_handler(self, request: Request) -> (Response | StreamResponse): # pylint: disable=too-many-locals - unsafe_prefix = request.query.get("prefix", "") + "/" - url = valid_url(request.query.get("url")) - insecure = valid_bool(request.query.get("insecure", False)) - timeout = valid_float_f01(request.query.get("timeout", 10.0)) - remove_incomplete = self.__get_remove_incomplete(request) + async def __write_remote_handler(self, req: Request) -> (Response | StreamResponse): # pylint: disable=too-many-locals + unsafe_prefix = req.query.get("prefix", "") + "/" + url = valid_url(req.query.get("url")) + insecure = valid_bool(req.query.get("insecure", False)) + timeout = valid_float_f01(req.query.get("timeout", 10.0)) + remove_incomplete = self.__get_remove_incomplete(req) name = "" size = written = 0 @@ -174,7 +174,7 @@ class MsdApi: read_timeout=(7 * 24 * 3600), ) as remote: - name = str(request.query.get("image", "")).strip() + name = str(req.query.get("image", "")).strip() if len(name) == 0: name = htclient.get_filename(remote) name = valid_msd_image_name(unsafe_prefix + name) @@ -184,7 +184,7 @@ class MsdApi: get_logger(0).info("Downloading image %r as %r to MSD ...", url, name) async with self.__msd.write_image(name, size, remove_incomplete) as writer: chunk_size = writer.get_chunk_size() - response = await start_streaming(request, "application/x-ndjson") + response = await start_streaming(req, "application/x-ndjson") await stream_write_info() last_report_ts = 0 async for chunk in remote.content.iter_chunked(chunk_size): @@ -197,16 +197,16 @@ class MsdApi: await stream_write_info() return response - except Exception as err: + except Exception as ex: if response is not None: await stream_write_info() - await stream_json_exception(response, err) - elif isinstance(err, aiohttp.ClientError): - return make_json_exception(err, 400) + await stream_json_exception(response, ex) + elif isinstance(ex, aiohttp.ClientError): + return make_json_exception(ex, 400) raise - def __get_remove_incomplete(self, request: Request) -> (bool | None): - flag: (str | None) = request.query.get("remove_incomplete") + def __get_remove_incomplete(self, req: Request) -> (bool | None): + flag: (str | None) = req.query.get("remove_incomplete") return (valid_bool(flag) if flag is not None else None) def __make_write_info(self, name: str, size: int, written: int) -> dict: @@ -215,8 +215,8 @@ class MsdApi: # ===== @exposed_http("POST", "/msd/remove") - async def __remove_handler(self, request: Request) -> Response: - await self.__msd.remove(valid_msd_image_name(request.query.get("image"))) + async def __remove_handler(self, req: Request) -> Response: + await self.__msd.remove(valid_msd_image_name(req.query.get("image"))) return make_json_response() @exposed_http("POST", "/msd/reset") diff --git a/kvmd/apps/kvmd/api/redfish.py b/kvmd/apps/kvmd/api/redfish.py index 3aaa1812..e1822496 100644 --- a/kvmd/apps/kvmd/api/redfish.py +++ b/kvmd/apps/kvmd/api/redfish.py @@ -111,10 +111,10 @@ class RedfishApi: }, wrap_result=False) @exposed_http("POST", "/redfish/v1/Systems/0/Actions/ComputerSystem.Reset") - async def __power_handler(self, request: Request) -> Response: + async def __power_handler(self, req: Request) -> Response: try: action = check_string_in_list( - arg=(await request.json())["ResetType"], + arg=(await req.json()).get("ResetType"), name="Redfish ResetType", variants=set(self.__actions), lower=False, diff --git a/kvmd/apps/kvmd/api/streamer.py b/kvmd/apps/kvmd/api/streamer.py index f384eb17..fc8e01de 100644 --- a/kvmd/apps/kvmd/api/streamer.py +++ b/kvmd/apps/kvmd/api/streamer.py @@ -52,36 +52,36 @@ class StreamerApi: return make_json_response(await self.__streamer.get_state()) @exposed_http("GET", "/streamer/snapshot") - async def __take_snapshot_handler(self, request: Request) -> Response: + async def __take_snapshot_handler(self, req: Request) -> Response: snapshot = await self.__streamer.take_snapshot( - save=valid_bool(request.query.get("save", False)), - load=valid_bool(request.query.get("load", False)), - allow_offline=valid_bool(request.query.get("allow_offline", False)), + save=valid_bool(req.query.get("save", False)), + load=valid_bool(req.query.get("load", False)), + allow_offline=valid_bool(req.query.get("allow_offline", False)), ) if snapshot: - if valid_bool(request.query.get("ocr", False)): + if valid_bool(req.query.get("ocr", False)): langs = self.__ocr.get_available_langs() return Response( body=(await self.__ocr.recognize( data=snapshot.data, langs=valid_string_list( - arg=str(request.query.get("ocr_langs", "")).strip(), + arg=str(req.query.get("ocr_langs", "")).strip(), subval=(lambda lang: check_string_in_list(lang, "OCR lang", langs)), name="OCR langs list", ), - left=int(valid_number(request.query.get("ocr_left", -1))), - top=int(valid_number(request.query.get("ocr_top", -1))), - right=int(valid_number(request.query.get("ocr_right", -1))), - bottom=int(valid_number(request.query.get("ocr_bottom", -1))), + left=int(valid_number(req.query.get("ocr_left", -1))), + top=int(valid_number(req.query.get("ocr_top", -1))), + right=int(valid_number(req.query.get("ocr_right", -1))), + bottom=int(valid_number(req.query.get("ocr_bottom", -1))), )), headers=dict(snapshot.headers), content_type="text/plain", ) - elif valid_bool(request.query.get("preview", False)): + elif valid_bool(req.query.get("preview", False)): data = await snapshot.make_preview( - max_width=valid_int_f0(request.query.get("preview_max_width", 0)), - max_height=valid_int_f0(request.query.get("preview_max_height", 0)), - quality=valid_stream_quality(request.query.get("preview_quality", 80)), + max_width=valid_int_f0(req.query.get("preview_max_width", 0)), + max_height=valid_int_f0(req.query.get("preview_max_height", 0)), + quality=valid_stream_quality(req.query.get("preview_quality", 80)), ) else: data = snapshot.data diff --git a/kvmd/apps/kvmd/api/ugpio.py b/kvmd/apps/kvmd/api/ugpio.py index 3e5f7ca3..ddaefefa 100644 --- a/kvmd/apps/kvmd/api/ugpio.py +++ b/kvmd/apps/kvmd/api/ugpio.py @@ -48,17 +48,17 @@ class UserGpioApi: }) @exposed_http("POST", "/gpio/switch") - async def __switch_handler(self, request: Request) -> Response: - channel = valid_ugpio_channel(request.query.get("channel")) - state = valid_bool(request.query.get("state")) - wait = valid_bool(request.query.get("wait", False)) + async def __switch_handler(self, req: Request) -> Response: + channel = valid_ugpio_channel(req.query.get("channel")) + state = valid_bool(req.query.get("state")) + wait = valid_bool(req.query.get("wait", False)) await self.__user_gpio.switch(channel, state, wait) return make_json_response() @exposed_http("POST", "/gpio/pulse") - async def __pulse_handler(self, request: Request) -> Response: - channel = valid_ugpio_channel(request.query.get("channel")) - delay = valid_float_f0(request.query.get("delay", 0.0)) - wait = valid_bool(request.query.get("wait", False)) + async def __pulse_handler(self, req: Request) -> Response: + channel = valid_ugpio_channel(req.query.get("channel")) + delay = valid_float_f0(req.query.get("delay", 0.0)) + wait = valid_bool(req.query.get("wait", False)) await self.__user_gpio.pulse(channel, delay, wait) return make_json_response() diff --git a/kvmd/apps/kvmd/info/extras.py b/kvmd/apps/kvmd/info/extras.py index a5f803bc..07225013 100644 --- a/kvmd/apps/kvmd/info/extras.py +++ b/kvmd/apps/kvmd/info/extras.py @@ -46,8 +46,8 @@ class ExtrasInfoSubmanager(BaseInfoSubmanager): try: sui = sysunit.SystemdUnitInfo() await sui.open() - except Exception as err: - get_logger(0).error("Can't open systemd bus to get extras state: %s", tools.efmt(err)) + except Exception as ex: + get_logger(0).error("Can't open systemd bus to get extras state: %s", tools.efmt(ex)) sui = None try: extras: dict[str, dict] = {} @@ -85,8 +85,8 @@ class ExtrasInfoSubmanager(BaseInfoSubmanager): if sui is not None: try: (extra["enabled"], extra["started"]) = await sui.get_status(daemon) - except Exception as err: - get_logger(0).error("Can't get info about the service %r: %s", daemon, tools.efmt(err)) + except Exception as ex: + get_logger(0).error("Can't get info about the service %r: %s", daemon, tools.efmt(ex)) def __rewrite_app_port(self, extra: dict) -> None: port_path = extra.get("port", "") diff --git a/kvmd/apps/kvmd/info/fan.py b/kvmd/apps/kvmd/info/fan.py index 48f4f6d5..247aff7d 100644 --- a/kvmd/apps/kvmd/info/fan.py +++ b/kvmd/apps/kvmd/info/fan.py @@ -87,8 +87,8 @@ class FanInfoSubmanager(BaseInfoSubmanager): async with sysunit.SystemdUnitInfo() as sui: status = await sui.get_status(self.__daemon) return (status[0] or status[1]) - except Exception as err: - get_logger(0).error("Can't get info about the service %r: %s", self.__daemon, tools.efmt(err)) + except Exception as ex: + get_logger(0).error("Can't get info about the service %r: %s", self.__daemon, tools.efmt(ex)) return False async def __get_fan_state(self) -> (dict | None): @@ -97,8 +97,8 @@ class FanInfoSubmanager(BaseInfoSubmanager): async with session.get("http://localhost/state") as response: htclient.raise_not_200(response) return (await response.json())["result"] - except Exception as err: - get_logger(0).error("Can't read fan state: %s", err) + except Exception as ex: + get_logger(0).error("Can't read fan state: %s", ex) return None def __make_http_session(self) -> aiohttp.ClientSession: diff --git a/kvmd/apps/kvmd/info/hw.py b/kvmd/apps/kvmd/info/hw.py index 2222ace3..458bc1ec 100644 --- a/kvmd/apps/kvmd/info/hw.py +++ b/kvmd/apps/kvmd/info/hw.py @@ -114,8 +114,8 @@ class HwInfoSubmanager(BaseInfoSubmanager): try: value = (await aiotools.read_file(path)).strip(" \t\r\n\0") self.__dt_cache[name] = (value.upper() if upper else value) - except Exception as err: - get_logger(0).error("Can't read DT %s from %s: %s", name, path, err) + except Exception as ex: + get_logger(0).error("Can't read DT %s from %s: %s", name, path, ex) return None return self.__dt_cache[name] @@ -141,8 +141,8 @@ class HwInfoSubmanager(BaseInfoSubmanager): temp_path = f"{env.SYSFS_PREFIX}/sys/class/thermal/thermal_zone0/temp" try: return int((await aiotools.read_file(temp_path)).strip()) / 1000 - except Exception as err: - get_logger(0).error("Can't read CPU temp from %s: %s", temp_path, err) + except Exception as ex: + get_logger(0).error("Can't read CPU temp from %s: %s", temp_path, ex) return None async def __get_cpu_percent(self) -> (float | None): @@ -160,8 +160,8 @@ class HwInfoSubmanager(BaseInfoSubmanager): + system_all / total * 100 + (st.steal + st.guest) / total * 100 ) - except Exception as err: - get_logger(0).error("Can't get CPU percent: %s", err) + except Exception as ex: + get_logger(0).error("Can't get CPU percent: %s", ex) return None async def __get_mem(self) -> dict: @@ -172,8 +172,8 @@ class HwInfoSubmanager(BaseInfoSubmanager): "total": st.total, "available": st.available, } - except Exception as err: - get_logger(0).error("Can't get memory info: %s", err) + except Exception as ex: + get_logger(0).error("Can't get memory info: %s", ex) return { "percent": None, "total": None, @@ -216,6 +216,6 @@ class HwInfoSubmanager(BaseInfoSubmanager): return None try: return parser(text) - except Exception as err: - get_logger(0).error("Can't parse [ %s ] output: %r: %s", tools.cmdfmt(cmd), text, tools.efmt(err)) + except Exception as ex: + get_logger(0).error("Can't parse [ %s ] output: %r: %s", tools.cmdfmt(cmd), text, tools.efmt(ex)) return None diff --git a/kvmd/apps/kvmd/ocr.py b/kvmd/apps/kvmd/ocr.py index bd30afa6..0a632d9d 100644 --- a/kvmd/apps/kvmd/ocr.py +++ b/kvmd/apps/kvmd/ocr.py @@ -76,8 +76,8 @@ def _load_libtesseract() -> (ctypes.CDLL | None): setattr(func, "restype", restype) setattr(func, "argtypes", argtypes) return lib - except Exception as err: - warnings.warn(f"Can't load libtesseract: {err}", RuntimeWarning) + except Exception as ex: + warnings.warn(f"Can't load libtesseract: {ex}", RuntimeWarning) return None diff --git a/kvmd/apps/kvmd/server.py b/kvmd/apps/kvmd/server.py index 44f33f89..366958ce 100644 --- a/kvmd/apps/kvmd/server.py +++ b/kvmd/apps/kvmd/server.py @@ -213,7 +213,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins # ===== STREAMER CONTROLLER @exposed_http("POST", "/streamer/set_params") - async def __streamer_set_params_handler(self, request: Request) -> Response: + async def __streamer_set_params_handler(self, req: Request) -> Response: current_params = self.__streamer.get_params() for (name, validator, exc_cls) in [ ("quality", valid_stream_quality, StreamerQualityNotSupported), @@ -222,7 +222,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins ("h264_bitrate", valid_stream_h264_bitrate, StreamerH264NotSupported), ("h264_gop", valid_stream_h264_gop, StreamerH264NotSupported), ]: - value = request.query.get(name) + value = req.query.get(name) if value: if name not in current_params: assert exc_cls is not None, name @@ -242,9 +242,9 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins # ===== WEBSOCKET @exposed_http("GET", "/ws") - async def __ws_handler(self, request: Request) -> WebSocketResponse: - stream = valid_bool(request.query.get("stream", True)) - async with self._ws_session(request, stream=stream) as ws: + async def __ws_handler(self, req: Request) -> WebSocketResponse: + stream = valid_bool(req.query.get("stream", True)) + async with self._ws_session(req, stream=stream) as ws: states = [ (event_type, src.get_state()) for sub in self.__subsystems @@ -275,8 +275,8 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins aioproc.rename_process("main") super().run(**kwargs) - async def _check_request_auth(self, exposed: HttpExposed, request: Request) -> None: - await check_request_auth(self.__auth_manager, exposed, request) + async def _check_request_auth(self, exposed: HttpExposed, req: Request) -> None: + await check_request_auth(self.__auth_manager, exposed, req) async def _init_app(self) -> None: aiotools.create_deadly_task("Stream controller", self.__stream_controller()) diff --git a/kvmd/apps/kvmd/streamer.py b/kvmd/apps/kvmd/streamer.py index e5c406d9..c365e19d 100644 --- a/kvmd/apps/kvmd/streamer.py +++ b/kvmd/apps/kvmd/streamer.py @@ -386,8 +386,8 @@ class Streamer: # pylint: disable=too-many-instance-attributes return snapshot logger.error("Stream is offline, no signal or so") - except (aiohttp.ClientConnectionError, aiohttp.ServerConnectionError) as err: - logger.error("Can't connect to streamer: %s", tools.efmt(err)) + except (aiohttp.ClientConnectionError, aiohttp.ServerConnectionError) as ex: + logger.error("Can't connect to streamer: %s", tools.efmt(ex)) except Exception: logger.exception("Invalid streamer response from /snapshot") return None @@ -473,8 +473,8 @@ class Streamer: # pylint: disable=too-many-instance-attributes logger.info("%s: %s", name, tools.cmdfmt(cmd)) try: await aioproc.log_process(cmd, logger, prefix=name) - except Exception as err: - logger.exception("Can't execute command: %s", err) + except Exception as ex: + logger.exception("Can't execute command: %s", ex) async def __start_streamer_proc(self) -> None: assert self.__streamer_proc is None diff --git a/kvmd/apps/kvmd/sysunit.py b/kvmd/apps/kvmd/sysunit.py index eea397ed..bcb41c34 100644 --- a/kvmd/apps/kvmd/sysunit.py +++ b/kvmd/apps/kvmd/sysunit.py @@ -51,8 +51,8 @@ class SystemdUnitInfo: unit_props = unit.get_interface("org.freedesktop.DBus.Properties") started = ((await unit_props.call_get("org.freedesktop.systemd1.Unit", "ActiveState")).value == "active") # type: ignore self.__requested = True - except dbus_next.errors.DBusError as err: - if err.type != "org.freedesktop.systemd1.NoSuchUnit": + except dbus_next.errors.DBusError as ex: + if ex.type != "org.freedesktop.systemd1.NoSuchUnit": raise started = False enabled = ((await self.__manager.call_get_unit_file_state(name)) in [ # type: ignore diff --git a/kvmd/apps/otg/__init__.py b/kvmd/apps/otg/__init__.py index 1da23c67..b683c5fd 100644 --- a/kvmd/apps/otg/__init__.py +++ b/kvmd/apps/otg/__init__.py @@ -346,5 +346,5 @@ def main(argv: (list[str] | None)=None) -> None: options = parser.parse_args(argv[1:]) try: options.cmd(config) - except ValidatorError as err: - raise SystemExit(str(err)) + except ValidatorError as ex: + raise SystemExit(str(ex)) diff --git a/kvmd/apps/otgmsd/__init__.py b/kvmd/apps/otgmsd/__init__.py index abe2400c..7d5bfdbc 100644 --- a/kvmd/apps/otgmsd/__init__.py +++ b/kvmd/apps/otgmsd/__init__.py @@ -47,9 +47,9 @@ def _set_param(gadget: str, instance: int, param: str, value: str) -> None: try: with open(_get_param_path(gadget, instance, param), "w") as file: file.write(value + "\n") - except OSError as err: - if err.errno == errno.EBUSY: - raise SystemExit(f"Can't change {param!r} value because device is locked: {err}") + except OSError as ex: + if ex.errno == errno.EBUSY: + raise SystemExit(f"Can't change {param!r} value because device is locked: {ex}") raise diff --git a/kvmd/apps/otgnet/__init__.py b/kvmd/apps/otgnet/__init__.py index 3519d98b..35c0bc45 100644 --- a/kvmd/apps/otgnet/__init__.py +++ b/kvmd/apps/otgnet/__init__.py @@ -133,8 +133,8 @@ class _Service: # pylint: disable=too-many-instance-attributes logger.info("CMD: %s", tools.cmdfmt(cmd)) try: return (not (await aioproc.log_process(cmd, logger)).returncode) - except Exception as err: - logger.exception("Can't execute command: %s", err) + except Exception as ex: + logger.exception("Can't execute command: %s", ex) return False # ===== diff --git a/kvmd/apps/pst/server.py b/kvmd/apps/pst/server.py index 359faf9a..79bbf7c8 100644 --- a/kvmd/apps/pst/server.py +++ b/kvmd/apps/pst/server.py @@ -60,8 +60,8 @@ class PstServer(HttpServer): # pylint: disable=too-many-arguments,too-many-inst # ===== WEBSOCKET @exposed_http("GET", "/ws") - async def __ws_handler(self, request: Request) -> WebSocketResponse: - async with self._ws_session(request) as ws: + async def __ws_handler(self, req: Request) -> WebSocketResponse: + async with self._ws_session(req) as ws: await ws.send_event("loop", {}) return (await self._ws_loop(ws)) @@ -128,9 +128,9 @@ class PstServer(HttpServer): # pylint: disable=too-many-arguments,too-many-inst def __is_write_available(self) -> bool: try: return (not (os.statvfs(self.__data_path).f_flag & os.ST_RDONLY)) - except Exception as err: + except Exception as ex: get_logger(0).info("Can't get filesystem state of PST (%s): %s", - self.__data_path, tools.efmt(err)) + self.__data_path, tools.efmt(ex)) return False async def __remount_storage(self, rw: bool) -> bool: diff --git a/kvmd/apps/pstrun/__init__.py b/kvmd/apps/pstrun/__init__.py index f7dd537d..33e1396e 100644 --- a/kvmd/apps/pstrun/__init__.py +++ b/kvmd/apps/pstrun/__init__.py @@ -46,8 +46,8 @@ def _preexec() -> None: if os.isatty(0): try: os.tcsetpgrp(0, os.getpgid(0)) - except Exception as err: - get_logger(0).info("Can't perform tcsetpgrp(0): %s", tools.efmt(err)) + except Exception as ex: + get_logger(0).info("Can't perform tcsetpgrp(0): %s", tools.efmt(ex)) async def _run_process(cmd: list[str], data_path: str) -> asyncio.subprocess.Process: # pylint: disable=no-member diff --git a/kvmd/apps/vnc/rfb/__init__.py b/kvmd/apps/vnc/rfb/__init__.py index ac4e0354..c145b4b3 100644 --- a/kvmd/apps/vnc/rfb/__init__.py +++ b/kvmd/apps/vnc/rfb/__init__.py @@ -120,10 +120,10 @@ class RfbClient(RfbClientStream): # pylint: disable=too-many-instance-attribute except asyncio.CancelledError: logger.info("%s [%s]: Cancelling subtask ...", self._remote, name) raise - except RfbConnectionError as err: - logger.info("%s [%s]: Gone: %s", self._remote, name, err) - except (RfbError, ssl.SSLError) as err: - logger.error("%s [%s]: Error: %s", self._remote, name, err) + except RfbConnectionError as ex: + logger.info("%s [%s]: Gone: %s", self._remote, name, ex) + except (RfbError, ssl.SSLError) as ex: + logger.error("%s [%s]: Error: %s", self._remote, name, ex) except Exception: logger.exception("%s [%s]: Unhandled exception", self._remote, name) diff --git a/kvmd/apps/vnc/rfb/errors.py b/kvmd/apps/vnc/rfb/errors.py index 1cf68818..caa4d085 100644 --- a/kvmd/apps/vnc/rfb/errors.py +++ b/kvmd/apps/vnc/rfb/errors.py @@ -29,5 +29,5 @@ class RfbError(Exception): class RfbConnectionError(RfbError): - def __init__(self, msg: str, err: Exception) -> None: - super().__init__(f"{msg}: {tools.efmt(err)}") + def __init__(self, msg: str, ex: Exception) -> None: + super().__init__(f"{msg}: {tools.efmt(ex)}") diff --git a/kvmd/apps/vnc/rfb/stream.py b/kvmd/apps/vnc/rfb/stream.py index 44998617..dc3ceb1b 100644 --- a/kvmd/apps/vnc/rfb/stream.py +++ b/kvmd/apps/vnc/rfb/stream.py @@ -51,22 +51,22 @@ class RfbClientStream: else: fmt = f">{fmt}" return struct.unpack(fmt, await self.__reader.readexactly(struct.calcsize(fmt)))[0] - except (ConnectionError, asyncio.IncompleteReadError) as err: - raise RfbConnectionError(f"Can't read {msg}", err) + except (ConnectionError, asyncio.IncompleteReadError) as ex: + raise RfbConnectionError(f"Can't read {msg}", ex) async def _read_struct(self, msg: str, fmt: str) -> tuple[int, ...]: assert len(fmt) > 1 try: fmt = f">{fmt}" return struct.unpack(fmt, (await self.__reader.readexactly(struct.calcsize(fmt)))) - except (ConnectionError, asyncio.IncompleteReadError) as err: - raise RfbConnectionError(f"Can't read {msg}", err) + except (ConnectionError, asyncio.IncompleteReadError) as ex: + raise RfbConnectionError(f"Can't read {msg}", ex) async def _read_text(self, msg: str, length: int) -> str: try: return (await self.__reader.readexactly(length)).decode("utf-8", errors="ignore") - except (ConnectionError, asyncio.IncompleteReadError) as err: - raise RfbConnectionError(f"Can't read {msg}", err) + except (ConnectionError, asyncio.IncompleteReadError) as ex: + raise RfbConnectionError(f"Can't read {msg}", ex) # ===== @@ -84,8 +84,8 @@ class RfbClientStream: self.__writer.write(struct.pack(f">{fmt}", *values)) if drain: await self.__writer.drain() - except ConnectionError as err: - raise RfbConnectionError(f"Can't write {msg}", err) + except ConnectionError as ex: + raise RfbConnectionError(f"Can't write {msg}", ex) async def _write_reason(self, msg: str, text: str, drain: bool=True) -> None: encoded = text.encode("utf-8", errors="ignore") @@ -94,8 +94,8 @@ class RfbClientStream: self.__writer.write(encoded) if drain: await self.__writer.drain() - except ConnectionError as err: - raise RfbConnectionError(f"Can't write {msg}", err) + except ConnectionError as ex: + raise RfbConnectionError(f"Can't write {msg}", ex) async def _write_fb_update(self, msg: str, width: int, height: int, encoding: int, drain: bool=True) -> None: await self._write_struct( @@ -123,8 +123,8 @@ class RfbClientStream: server_side=True, ssl_handshake_timeout=ssl_timeout, ) - except ConnectionError as err: - raise RfbConnectionError("Can't start TLS", err) + except ConnectionError as ex: + raise RfbConnectionError("Can't start TLS", ex) ssl_reader.set_transport(transport) # type: ignore ssl_writer = asyncio.StreamWriter( diff --git a/kvmd/apps/vnc/server.py b/kvmd/apps/vnc/server.py index 6ec3960e..f8e97050 100644 --- a/kvmd/apps/vnc/server.py +++ b/kvmd/apps/vnc/server.py @@ -210,12 +210,12 @@ class _Client(RfbClient): # pylint: disable=too-many-instance-attributes await self.__queue_frame(frame) else: await self.__queue_frame("No signal") - except StreamerError as err: - if isinstance(err, StreamerPermError): + except StreamerError as ex: + if isinstance(ex, StreamerPermError): streamer = self.__get_default_streamer() - logger.info("%s [streamer]: Permanent error: %s; switching to %s ...", self._remote, err, streamer) + logger.info("%s [streamer]: Permanent error: %s; switching to %s ...", self._remote, ex, streamer) else: - logger.info("%s [streamer]: Waiting for stream: %s", self._remote, err) + logger.info("%s [streamer]: Waiting for stream: %s", self._remote, ex) await self.__queue_frame("Waiting for stream ...") await asyncio.sleep(1) @@ -481,8 +481,8 @@ class VncServer: # pylint: disable=too-many-instance-attributes try: async with kvmd.make_session("", "") as kvmd_session: none_auth_only = await kvmd_session.auth.check() - except (aiohttp.ClientError, asyncio.TimeoutError) as err: - logger.error("%s [entry]: Can't check KVMD auth mode: %s", remote, tools.efmt(err)) + except (aiohttp.ClientError, asyncio.TimeoutError) as ex: + logger.error("%s [entry]: Can't check KVMD auth mode: %s", remote, tools.efmt(ex)) return await _Client( diff --git a/kvmd/apps/vnc/vncauth.py b/kvmd/apps/vnc/vncauth.py index ebda9ef4..46c1a77d 100644 --- a/kvmd/apps/vnc/vncauth.py +++ b/kvmd/apps/vnc/vncauth.py @@ -54,8 +54,8 @@ class VncAuthManager: if self.__enabled: try: return (await self.__inner_read_credentials(), True) - except VncAuthError as err: - get_logger(0).error(str(err)) + except VncAuthError as ex: + get_logger(0).error(str(ex)) except Exception: get_logger(0).exception("Unhandled exception while reading VNCAuth passwd file") return ({}, (not self.__enabled)) diff --git a/kvmd/apps/watchdog/__init__.py b/kvmd/apps/watchdog/__init__.py index 37981d0b..c2b03730 100644 --- a/kvmd/apps/watchdog/__init__.py +++ b/kvmd/apps/watchdog/__init__.py @@ -56,8 +56,8 @@ def _write_int(rtc: int, key: str, value: int) -> None: def _reset_alarm(rtc: int, timeout: int) -> None: try: now = _read_int(rtc, "since_epoch") - except OSError as err: - if err.errno != errno.EINVAL: + except OSError as ex: + if ex.errno != errno.EINVAL: raise raise RtcIsNotAvailableError("Can't read since_epoch right now") if now == 0: @@ -65,8 +65,8 @@ def _reset_alarm(rtc: int, timeout: int) -> None: try: for wake in [0, now + timeout]: _write_int(rtc, "wakealarm", wake) - except OSError as err: - if err.errno != errno.EIO: + except OSError as ex: + if ex.errno != errno.EIO: raise raise RtcIsNotAvailableError("IO error, probably the supercapacitor is not charged") @@ -80,9 +80,9 @@ def _cmd_run(config: Section) -> None: while True: try: _reset_alarm(config.rtc, config.timeout) - except RtcIsNotAvailableError as err: + except RtcIsNotAvailableError as ex: if not fail: - logger.error("RTC%d is not available now: %s; waiting ...", config.rtc, err) + logger.error("RTC%d is not available now: %s; waiting ...", config.rtc, ex) fail = True else: if fail: diff --git a/kvmd/clients/kvmd.py b/kvmd/clients/kvmd.py index 569fb9d0..ff8581ef 100644 --- a/kvmd/clients/kvmd.py +++ b/kvmd/clients/kvmd.py @@ -66,8 +66,8 @@ class _AuthApiPart(_BaseApiPart): async with session.get(self._make_url("auth/check")) as response: htclient.raise_not_200(response) return True - except aiohttp.ClientResponseError as err: - if err.status in [400, 401, 403]: + except aiohttp.ClientResponseError as ex: + if ex.status in [400, 401, 403]: return False raise @@ -128,8 +128,8 @@ class _AtxApiPart(_BaseApiPart): ) as response: htclient.raise_not_200(response) return True - except aiohttp.ClientResponseError as err: - if err.status == 409: + except aiohttp.ClientResponseError as ex: + if ex.status == 409: return False raise diff --git a/kvmd/clients/streamer.py b/kvmd/clients/streamer.py index 1d8cd601..fdc855bb 100644 --- a/kvmd/clients/streamer.py +++ b/kvmd/clients/streamer.py @@ -72,10 +72,10 @@ class BaseStreamerClient: def _http_handle_errors() -> Generator[None, None, None]: try: yield - except Exception as err: # Тут бывают и ассерты, и KeyError, и прочая херня - if isinstance(err, StreamerTempError): + except Exception as ex: # Тут бывают и ассерты, и KeyError, и прочая херня + if isinstance(ex, StreamerTempError): raise - raise StreamerTempError(tools.efmt(err)) + raise StreamerTempError(tools.efmt(ex)) class HttpStreamerClient(BaseStreamerClient): @@ -167,10 +167,10 @@ def _memsink_handle_errors() -> Generator[None, None, None]: yield except StreamerPermError: raise - except FileNotFoundError as err: - raise StreamerTempError(tools.efmt(err)) - except Exception as err: - raise StreamerPermError(tools.efmt(err)) + except FileNotFoundError as ex: + raise StreamerTempError(tools.efmt(ex)) + except Exception as ex: + raise StreamerPermError(tools.efmt(ex)) class MemsinkStreamerClient(BaseStreamerClient): diff --git a/kvmd/helpers/remount/__init__.py b/kvmd/helpers/remount/__init__.py index b8e71e4f..54eb7731 100644 --- a/kvmd/helpers/remount/__init__.py +++ b/kvmd/helpers/remount/__init__.py @@ -46,8 +46,8 @@ def _remount(path: str, rw: bool) -> None: _log(f"Remounting {path} to {mode.upper()}-mode ...") try: subprocess.check_call(["/bin/mount", "--options", f"remount,{mode}", path]) - except subprocess.CalledProcessError as err: - raise SystemExit(f"Can't remount: {err}") + except subprocess.CalledProcessError as ex: + raise SystemExit(f"Can't remount: {ex}") def _mkdir(path: str) -> None: @@ -55,8 +55,8 @@ def _mkdir(path: str) -> None: _log(f"MKDIR --- {path}") try: os.mkdir(path) - except Exception as err: - raise SystemExit(f"Can't create directory: {err}") + except Exception as ex: + raise SystemExit(f"Can't create directory: {ex}") def _rmtree(path: str) -> None: @@ -64,8 +64,8 @@ def _rmtree(path: str) -> None: _log(f"RMALL --- {path}") try: shutil.rmtree(path) - except Exception as err: - raise SystemExit(f"Can't remove directory: {err}") + except Exception as ex: + raise SystemExit(f"Can't remove directory: {ex}") def _rm(path: str) -> None: @@ -73,16 +73,16 @@ def _rm(path: str) -> None: _log(f"RM --- {path}") try: os.remove(path) - except Exception as err: - raise SystemExit(f"Can't remove file: {err}") + except Exception as ex: + raise SystemExit(f"Can't remove file: {ex}") def _move(src: str, dest: str) -> None: _log(f"MOVE --- {src} --> {dest}") try: os.rename(src, dest) - except Exception as err: - raise SystemExit(f"Can't move file: {err}") + except Exception as ex: + raise SystemExit(f"Can't move file: {ex}") def _chown(path: str, user: str) -> None: @@ -90,8 +90,8 @@ def _chown(path: str, user: str) -> None: _log(f"CHOWN --- {user} - {path}") try: shutil.chown(path, user=user) - except Exception as err: - raise SystemExit(f"Can't change ownership: {err}") + except Exception as ex: + raise SystemExit(f"Can't change ownership: {ex}") def _chgrp(path: str, group: str) -> None: @@ -99,8 +99,8 @@ def _chgrp(path: str, group: str) -> None: _log(f"CHGRP --- {group} - {path}") try: shutil.chown(path, group=group) - except Exception as err: - raise SystemExit(f"Can't change group: {err}") + except Exception as ex: + raise SystemExit(f"Can't change group: {ex}") def _chmod(path: str, mode: int) -> None: @@ -108,8 +108,8 @@ def _chmod(path: str, mode: int) -> None: _log(f"CHMOD --- 0o{mode:o} - {path}") try: os.chmod(path, mode) - except Exception as err: - raise SystemExit(f"Can't change permissions: {err}") + except Exception as ex: + raise SystemExit(f"Can't change permissions: {ex}") # ===== diff --git a/kvmd/htclient.py b/kvmd/htclient.py index 9d9ae82c..5978b189 100644 --- a/kvmd/htclient.py +++ b/kvmd/htclient.py @@ -36,27 +36,27 @@ def make_user_agent(app: str) -> str: return f"{app}/{__version__}" -def raise_not_200(response: aiohttp.ClientResponse) -> None: - if response.status != 200: - assert response.reason is not None - response.release() +def raise_not_200(resp: aiohttp.ClientResponse) -> None: + if resp.status != 200: + assert resp.reason is not None + resp.release() raise aiohttp.ClientResponseError( - response.request_info, - response.history, - status=response.status, - message=response.reason, - headers=response.headers, + resp.request_info, + resp.history, + status=resp.status, + message=resp.reason, + headers=resp.headers, ) -def get_filename(response: aiohttp.ClientResponse) -> str: +def get_filename(resp: aiohttp.ClientResponse) -> str: try: - disp = response.headers["Content-Disposition"] + disp = resp.headers["Content-Disposition"] parsed = aiohttp.multipart.parse_content_disposition(disp) return str(parsed[1]["filename"]) except Exception: try: - return os.path.basename(response.url.path) + return os.path.basename(resp.url.path) except Exception: raise aiohttp.ClientError("Can't determine filename") @@ -79,6 +79,6 @@ async def download( ), } async with aiohttp.ClientSession(**kwargs) as session: - async with session.get(url, verify_ssl=verify) as response: # type: ignore - raise_not_200(response) - yield response + async with session.get(url, verify_ssl=verify) as resp: # type: ignore + raise_not_200(resp) + yield resp diff --git a/kvmd/htserver.py b/kvmd/htserver.py index 4740ac7a..b0d78e82 100644 --- a/kvmd/htserver.py +++ b/kvmd/htserver.py @@ -157,7 +157,7 @@ def make_json_response( wrap_result: bool=True, ) -> Response: - response = Response( + resp = Response( text=json.dumps(({ "ok": (status == 200), "result": (result or {}), @@ -167,18 +167,18 @@ def make_json_response( ) if set_cookies: for (key, value) in set_cookies.items(): - response.set_cookie(key, value, httponly=True, samesite="Strict") - return response + resp.set_cookie(key, value, httponly=True, samesite="Strict") + return resp -def make_json_exception(err: Exception, status: (int | None)=None) -> Response: - name = type(err).__name__ - msg = str(err) - if isinstance(err, HttpError): - status = err.status +def make_json_exception(ex: Exception, status: (int | None)=None) -> Response: + name = type(ex).__name__ + msg = str(ex) + if isinstance(ex, HttpError): + status = ex.status else: get_logger().error("API error: %s: %s", name, msg) - assert status is not None, err + assert status is not None, ex return make_json_response({ "error": name, "error_msg": msg, @@ -186,35 +186,35 @@ def make_json_exception(err: Exception, status: (int | None)=None) -> Response: async def start_streaming( - request: Request, + req: Request, content_type: str, content_length: int=-1, file_name: str="", ) -> StreamResponse: - response = StreamResponse(status=200, reason="OK") - response.content_type = content_type + resp = StreamResponse(status=200, reason="OK") + resp.content_type = content_type if content_length >= 0: # pylint: disable=consider-using-min-builtin - response.content_length = content_length + resp.content_length = content_length if file_name: file_name = urllib.parse.quote(file_name, safe="") - response.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{file_name}" - await response.prepare(request) - return response + resp.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{file_name}" + await resp.prepare(req) + return resp -async def stream_json(response: StreamResponse, result: dict, ok: bool=True) -> None: - await response.write(json.dumps({ +async def stream_json(resp: StreamResponse, result: dict, ok: bool=True) -> None: + await resp.write(json.dumps({ "ok": ok, "result": result, }).encode("utf-8") + b"\r\n") -async def stream_json_exception(response: StreamResponse, err: Exception) -> None: - name = type(err).__name__ - msg = str(err) +async def stream_json_exception(resp: StreamResponse, ex: Exception) -> None: + name = type(ex).__name__ + msg = str(ex) get_logger().error("API error: %s: %s", name, msg) - await stream_json(response, { + await stream_json(resp, { "error": name, "error_msg": msg, }, False) @@ -249,15 +249,15 @@ def parse_ws_event(msg: str) -> tuple[str, dict]: _REQUEST_AUTH_INFO = "_kvmd_auth_info" -def _format_P(request: BaseRequest, *_, **__) -> str: # type: ignore # pylint: disable=invalid-name - return (getattr(request, _REQUEST_AUTH_INFO, None) or "-") +def _format_P(req: BaseRequest, *_, **__) -> str: # type: ignore # pylint: disable=invalid-name + return (getattr(req, _REQUEST_AUTH_INFO, None) or "-") AccessLogger._format_P = staticmethod(_format_P) # type: ignore # pylint: disable=protected-access -def set_request_auth_info(request: BaseRequest, info: str) -> None: - setattr(request, _REQUEST_AUTH_INFO, info) +def set_request_auth_info(req: BaseRequest, info: str) -> None: + setattr(req, _REQUEST_AUTH_INFO, info) # ===== @@ -318,16 +318,16 @@ class HttpServer: self.__add_exposed_ws(ws_exposed) def __add_exposed_http(self, exposed: HttpExposed) -> None: - async def wrapper(request: Request) -> Response: + async def wrapper(req: Request) -> Response: try: - await self._check_request_auth(exposed, request) - return (await exposed.handler(request)) - except IsBusyError as err: - return make_json_exception(err, 409) - except (ValidatorError, OperationError) as err: - return make_json_exception(err, 400) - except HttpError as err: - return make_json_exception(err) + await self._check_request_auth(exposed, req) + return (await exposed.handler(req)) + except IsBusyError as ex: + return make_json_exception(ex, 409) + except (ValidatorError, OperationError) as ex: + return make_json_exception(ex, 400) + except HttpError as ex: + return make_json_exception(ex) self.__app.router.add_route(exposed.method, exposed.path, wrapper) def __add_exposed_ws(self, exposed: WsExposed) -> None: @@ -342,10 +342,10 @@ class HttpServer: # ===== @contextlib.asynccontextmanager - async def _ws_session(self, request: Request, **kwargs: Any) -> AsyncGenerator[WsSession, None]: + async def _ws_session(self, req: Request, **kwargs: Any) -> AsyncGenerator[WsSession, None]: assert self.__ws_heartbeat is not None wsr = WebSocketResponse(heartbeat=self.__ws_heartbeat) - await wsr.prepare(request) + await wsr.prepare(req) ws = WsSession(wsr, kwargs) async with self.__ws_sessions_lock: @@ -364,8 +364,8 @@ class HttpServer: if msg.type == WSMsgType.TEXT: try: (event_type, event) = parse_ws_event(msg.data) - except Exception as err: - logger.error("Can't parse JSON event from websocket: %r", err) + except Exception as ex: + logger.error("Can't parse JSON event from websocket: %r", ex) else: handler = self.__ws_handlers.get(event_type) if handler: @@ -417,7 +417,7 @@ class HttpServer: # ===== - async def _check_request_auth(self, exposed: HttpExposed, request: Request) -> None: + async def _check_request_auth(self, exposed: HttpExposed, req: Request) -> None: pass async def _init_app(self) -> None: diff --git a/kvmd/inotify.py b/kvmd/inotify.py index c70ec465..302dd006 100644 --- a/kvmd/inotify.py +++ b/kvmd/inotify.py @@ -286,8 +286,8 @@ class Inotify: while True: try: return os.read(self.__fd, _EVENTS_BUFFER_LENGTH) - except OSError as err: - if err.errno == errno.EINTR: + except OSError as ex: + if ex.errno == errno.EINTR: pass def __enter__(self) -> "Inotify": diff --git a/kvmd/keyboard/keysym.py b/kvmd/keyboard/keysym.py index 5e80d789..2896ca6e 100644 --- a/kvmd/keyboard/keysym.py +++ b/kvmd/keyboard/keysym.py @@ -135,8 +135,8 @@ def _read_keyboard_layout(path: str) -> dict[int, list[At1Key]]: # Keysym to ev try: at1_code = int(parts[1], 16) - except ValueError as err: - logger.error("Syntax error at %s:%d: %s", path, lineno, err) + except ValueError as ex: + logger.error("Syntax error at %s:%d: %s", path, lineno, ex) continue rest = parts[2:] diff --git a/kvmd/network.py b/kvmd/network.py index 22721dd5..fcf67117 100644 --- a/kvmd/network.py +++ b/kvmd/network.py @@ -34,10 +34,10 @@ def is_ipv6_enabled() -> bool: with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as sock: sock.bind(("::1", 0)) return True - except OSError as err: - if err.errno in [errno.EADDRNOTAVAIL, errno.EAFNOSUPPORT]: + except OSError as ex: + if ex.errno in [errno.EADDRNOTAVAIL, errno.EAFNOSUPPORT]: return False - if err.errno == errno.EADDRINUSE: + if ex.errno == errno.EADDRINUSE: return True raise diff --git a/kvmd/plugins/atx/gpio.py b/kvmd/plugins/atx/gpio.py index 6df57bb3..538aafaf 100644 --- a/kvmd/plugins/atx/gpio.py +++ b/kvmd/plugins/atx/gpio.py @@ -76,7 +76,7 @@ class Plugin(BaseAtx): # pylint: disable=too-many-instance-attributes self.__notifier = aiotools.AioNotifier() self.__region = aiotools.AioExclusiveRegion(AtxIsBusyError, self.__notifier) - self.__line_request: (gpiod.LineRequest | None) = None + self.__line_req: (gpiod.LineRequest | None) = None self.__reader = aiogp.AioReader( path=self.__device_path, @@ -108,8 +108,8 @@ class Plugin(BaseAtx): # pylint: disable=too-many-instance-attributes } def sysprep(self) -> None: - assert self.__line_request is None - self.__line_request = gpiod.request_lines( + assert self.__line_req is None + self.__line_req = gpiod.request_lines( self.__device_path, consumer="kvmd::atx", config={ @@ -143,9 +143,9 @@ class Plugin(BaseAtx): # pylint: disable=too-many-instance-attributes await self.__reader.poll() async def cleanup(self) -> None: - if self.__line_request: + if self.__line_req: try: - self.__line_request.release() + self.__line_req.release() except Exception: pass @@ -196,11 +196,11 @@ class Plugin(BaseAtx): # pylint: disable=too-many-instance-attributes @aiotools.atomic_fg async def __inner_click(self, name: str, pin: int, delay: float) -> None: - assert self.__line_request + assert self.__line_req try: - self.__line_request.set_value(pin, gpiod.line.Value(True)) + self.__line_req.set_value(pin, gpiod.line.Value(True)) await asyncio.sleep(delay) finally: - self.__line_request.set_value(pin, gpiod.line.Value(False)) + self.__line_req.set_value(pin, gpiod.line.Value(False)) await asyncio.sleep(1) get_logger(0).info("Clicked ATX button %r", name) diff --git a/kvmd/plugins/auth/http.py b/kvmd/plugins/auth/http.py index 520f64dc..b59218aa 100644 --- a/kvmd/plugins/auth/http.py +++ b/kvmd/plugins/auth/http.py @@ -85,8 +85,8 @@ class Plugin(BaseAuthService): "User-Agent": htclient.make_user_agent("KVMD"), "X-KVMD-User": user, }, - ) as response: - htclient.raise_not_200(response) + ) as resp: + htclient.raise_not_200(resp) return True except Exception: get_logger().exception("Failed HTTP auth request for user %r", user) diff --git a/kvmd/plugins/auth/ldap.py b/kvmd/plugins/auth/ldap.py index fa6b9fff..961a47c7 100644 --- a/kvmd/plugins/auth/ldap.py +++ b/kvmd/plugins/auth/ldap.py @@ -100,10 +100,10 @@ class Plugin(BaseAuthService): return True except ldap.INVALID_CREDENTIALS: pass - except ldap.SERVER_DOWN as err: - get_logger().error("LDAP server is down: %s", tools.efmt(err)) - except Exception as err: - get_logger().error("Unexpected LDAP error: %s", tools.efmt(err)) + except ldap.SERVER_DOWN as ex: + get_logger().error("LDAP server is down: %s", tools.efmt(ex)) + except Exception as ex: + get_logger().error("Unexpected LDAP error: %s", tools.efmt(ex)) finally: if conn is not None: try: diff --git a/kvmd/plugins/auth/radius.py b/kvmd/plugins/auth/radius.py index 92c4632d..f048a2e6 100644 --- a/kvmd/plugins/auth/radius.py +++ b/kvmd/plugins/auth/radius.py @@ -435,10 +435,10 @@ class Plugin(BaseAuthService): timeout=self.__timeout, dict=dct, ) - request = client.CreateAuthPacket(code=pyrad.packet.AccessRequest, User_Name=user) - request["User-Password"] = request.PwCrypt(passwd) - response = client.SendPacket(request) - return (response.code == pyrad.packet.AccessAccept) + req = client.CreateAuthPacket(code=pyrad.packet.AccessRequest, User_Name=user) + req["User-Password"] = req.PwCrypt(passwd) + resp = client.SendPacket(req) + return (resp.code == pyrad.packet.AccessAccept) except Exception: get_logger().exception("Failed RADIUS auth request for user %r", user) return False diff --git a/kvmd/plugins/hid/_mcu/__init__.py b/kvmd/plugins/hid/_mcu/__init__.py index 01411621..53665fb2 100644 --- a/kvmd/plugins/hid/_mcu/__init__.py +++ b/kvmd/plugins/hid/_mcu/__init__.py @@ -91,7 +91,7 @@ class _TempRequestError(_RequestError): # ===== class BasePhyConnection: - def send(self, request: bytes) -> bytes: + def send(self, req: bytes) -> bytes: raise NotImplementedError @@ -374,7 +374,7 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many- self.__set_state_online(False) return False - def __process_request(self, conn: BasePhyConnection, request: bytes) -> bool: # pylint: disable=too-many-branches + def __process_request(self, conn: BasePhyConnection, req: bytes) -> bool: # pylint: disable=too-many-branches logger = get_logger() error_messages: list[str] = [] live_log_errors = False @@ -384,47 +384,47 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many- error_retval = False while self.__gpio.is_powered() and common_retries and read_retries: - response = (RESPONSE_LEGACY_OK if self.__noop else conn.send(request)) + resp = (RESPONSE_LEGACY_OK if self.__noop else conn.send(req)) try: - if len(response) < 4: + if len(resp) < 4: read_retries -= 1 - raise _TempRequestError(f"No response from HID: request={request!r}") + raise _TempRequestError(f"No response from HID: request={req!r}") - if not check_response(response): - request = REQUEST_REPEAT + if not check_response(resp): + req = REQUEST_REPEAT raise _TempRequestError("Invalid response CRC; requesting response again ...") - code = response[1] + code = resp[1] if code == 0x48: # Request timeout # pylint: disable=no-else-raise - raise _TempRequestError(f"Got request timeout from HID: request={request!r}") + raise _TempRequestError(f"Got request timeout from HID: request={req!r}") elif code == 0x40: # CRC Error - raise _TempRequestError(f"Got CRC error of request from HID: request={request!r}") + raise _TempRequestError(f"Got CRC error of request from HID: request={req!r}") elif code == 0x45: # Unknown command - raise _PermRequestError(f"HID did not recognize the request={request!r}") + raise _PermRequestError(f"HID did not recognize the request={req!r}") elif code == 0x24: # Rebooted? raise _PermRequestError("No previous command state inside HID, seems it was rebooted") elif code == 0x20: # Legacy done self.__set_state_online(True) return True elif code & 0x80: # Pong/Done with state - self.__set_state_pong(response) + self.__set_state_pong(resp) return True - raise _TempRequestError(f"Invalid response from HID: request={request!r}, response=0x{response!r}") + raise _TempRequestError(f"Invalid response from HID: request={req!r}, response=0x{resp!r}") - except _RequestError as err: + except _RequestError as ex: common_retries -= 1 if live_log_errors: - logger.error(err.msg) + logger.error(ex.msg) else: - error_messages.append(err.msg) + error_messages.append(ex.msg) if len(error_messages) > self.__errors_threshold: for msg in error_messages: logger.error(msg) error_messages = [] live_log_errors = True - if isinstance(err, _PermRequestError): + if isinstance(ex, _PermRequestError): error_retval = True break @@ -440,7 +440,7 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many- for msg in error_messages: logger.error(msg) if not (common_retries and read_retries): - logger.error("Can't process HID request due many errors: %r", request) + logger.error("Can't process HID request due many errors: %r", req) return error_retval def __set_state_online(self, online: bool) -> None: @@ -449,11 +449,11 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many- def __set_state_busy(self, busy: bool) -> None: self.__state_flags.update(busy=int(busy)) - def __set_state_pong(self, response: bytes) -> None: - status = response[1] << 16 - if len(response) > 4: - status |= (response[2] << 8) | response[3] - reset_required = (1 if response[1] & 0b01000000 else 0) + def __set_state_pong(self, resp: bytes) -> None: + status = resp[1] << 16 + if len(resp) > 4: + status |= (resp[2] << 8) | resp[3] + reset_required = (1 if resp[1] & 0b01000000 else 0) self.__state_flags.update(online=1, busy=reset_required, status=status) if reset_required: if self.__reset_self: diff --git a/kvmd/plugins/hid/_mcu/gpio.py b/kvmd/plugins/hid/_mcu/gpio.py index 0afbcc69..ce1d678b 100644 --- a/kvmd/plugins/hid/_mcu/gpio.py +++ b/kvmd/plugins/hid/_mcu/gpio.py @@ -47,12 +47,12 @@ class Gpio: # pylint: disable=too-many-instance-attributes self.__reset_inverted = reset_inverted self.__reset_delay = reset_delay - self.__line_request: (gpiod.LineRequest | None) = None + self.__line_req: (gpiod.LineRequest | None) = None self.__last_power: (bool | None) = None def __enter__(self) -> None: if self.__power_detect_pin >= 0 or self.__reset_pin >= 0: - assert self.__line_request is None + assert self.__line_req is None config: dict[int, gpiod.LineSettings] = {} if self.__power_detect_pin >= 0: config[self.__power_detect_pin] = gpiod.LineSettings( @@ -65,7 +65,7 @@ class Gpio: # pylint: disable=too-many-instance-attributes output_value=gpiod.line.Value(self.__reset_inverted), ) assert len(config) > 0 - self.__line_request = gpiod.request_lines( + self.__line_req = gpiod.request_lines( self.__device_path, consumer="kvmd::hid", config=config, @@ -78,18 +78,18 @@ class Gpio: # pylint: disable=too-many-instance-attributes _tb: types.TracebackType, ) -> None: - if self.__line_request: + if self.__line_req: try: - self.__line_request.release() + self.__line_req.release() except Exception: pass self.__last_power = None - self.__line_request = None + self.__line_req = None def is_powered(self) -> bool: if self.__power_detect_pin >= 0: - assert self.__line_request - power = bool(self.__line_request.get_value(self.__power_detect_pin).value) + assert self.__line_req + power = bool(self.__line_req.get_value(self.__power_detect_pin).value) if power != self.__last_power: get_logger(0).info("HID power state changed: %s -> %s", self.__last_power, power) self.__last_power = power @@ -98,11 +98,11 @@ class Gpio: # pylint: disable=too-many-instance-attributes def reset(self) -> None: if self.__reset_pin >= 0: - assert self.__line_request + assert self.__line_req try: - self.__line_request.set_value(self.__reset_pin, gpiod.line.Value(not self.__reset_inverted)) + self.__line_req.set_value(self.__reset_pin, gpiod.line.Value(not self.__reset_inverted)) time.sleep(self.__reset_delay) finally: - self.__line_request.set_value(self.__reset_pin, gpiod.line.Value(self.__reset_inverted)) + self.__line_req.set_value(self.__reset_pin, gpiod.line.Value(self.__reset_inverted)) time.sleep(1) get_logger(0).info("Reset HID performed") diff --git a/kvmd/plugins/hid/_mcu/proto.py b/kvmd/plugins/hid/_mcu/proto.py index 76cd5d09..deaf5c15 100644 --- a/kvmd/plugins/hid/_mcu/proto.py +++ b/kvmd/plugins/hid/_mcu/proto.py @@ -184,17 +184,17 @@ class MouseWheelEvent(BaseEvent): # ===== -def check_response(response: bytes) -> bool: - assert len(response) in (4, 8), response - return (bitbang.make_crc16(response[:-2]) == struct.unpack(">H", response[-2:])[0]) +def check_response(resp: bytes) -> bool: + assert len(resp) in (4, 8), resp + return (bitbang.make_crc16(resp[:-2]) == struct.unpack(">H", resp[-2:])[0]) -def _make_request(command: bytes) -> bytes: - assert len(command) == 5, command - request = b"\x33" + command - request += struct.pack(">H", bitbang.make_crc16(request)) - assert len(request) == 8, request - return request +def _make_request(cmd: bytes) -> bytes: + assert len(cmd) == 5, cmd + req = b"\x33" + cmd + req += struct.pack(">H", bitbang.make_crc16(req)) + assert len(req) == 8, req + return req # ===== diff --git a/kvmd/plugins/hid/bt/server.py b/kvmd/plugins/hid/bt/server.py index 2b6a4307..ad2b6982 100644 --- a/kvmd/plugins/hid/bt/server.py +++ b/kvmd/plugins/hid/bt/server.py @@ -182,8 +182,8 @@ class BtServer: # pylint: disable=too-many-instance-attributes self.__close_client("CTL", client, "ctl_sock") elif data == b"\x71": sock.send(b"\x00") - except Exception as err: - get_logger(0).exception("CTL socket error on %s: %s", client.addr, tools.efmt(err)) + except Exception as ex: + get_logger(0).exception("CTL socket error on %s: %s", client.addr, tools.efmt(ex)) self.__close_client("CTL", client, "ctl_sock") continue @@ -196,8 +196,8 @@ class BtServer: # pylint: disable=too-many-instance-attributes self.__close_client("INT", client, "int_sock") elif data[:2] == b"\xA2\x01": self.__process_leds(data[2]) - except Exception as err: - get_logger(0).exception("INT socket error on %s: %s", client.addr, tools.efmt(err)) + except Exception as ex: + get_logger(0).exception("INT socket error on %s: %s", client.addr, tools.efmt(ex)) self.__close_client("INT", client, "ctl_sock") if qr in ready_read: @@ -279,8 +279,8 @@ class BtServer: # pylint: disable=too-many-instance-attributes assert client.int_sock is not None try: client.int_sock.send(report) - except Exception as err: - get_logger(0).info("Can't send %s report to %s: %s", name, client.addr, tools.efmt(err)) + except Exception as ex: + get_logger(0).info("Can't send %s report to %s: %s", name, client.addr, tools.efmt(ex)) self.__close_client_pair(client) def __clear_modifiers(self) -> None: @@ -371,13 +371,13 @@ class BtServer: # pylint: disable=too-many-instance-attributes logger.info("Publishing ..." if public else "Unpublishing ...") try: self.__iface.set_public(public) - except Exception as err: - logger.error("Can't change public mode: %s", tools.efmt(err)) + except Exception as ex: + logger.error("Can't change public mode: %s", tools.efmt(ex)) def __unpair_client(self, client: _BtClient) -> None: logger = get_logger(0) logger.info("Unpairing %s ...", client.addr) try: self.__iface.unpair(client.addr) - except Exception as err: - logger.error("Can't unpair %s: %s", client.addr, tools.efmt(err)) + except Exception as ex: + logger.error("Can't unpair %s: %s", client.addr, tools.efmt(ex)) diff --git a/kvmd/plugins/hid/ch9329/__init__.py b/kvmd/plugins/hid/ch9329/__init__.py index 4e2be8c9..3245505d 100644 --- a/kvmd/plugins/hid/ch9329/__init__.py +++ b/kvmd/plugins/hid/ch9329/__init__.py @@ -230,9 +230,9 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst def __process_cmd(self, conn: ChipConnection, cmd: bytes) -> bool: # pylint: disable=too-many-branches try: led_byte = conn.xfer(cmd) - except ChipResponseError as err: + except ChipResponseError as ex: self.__set_state_online(False) - get_logger(0).info(err) + get_logger(0).error("Invalid chip response: %s", tools.efmt(ex)) time.sleep(2) else: if led_byte >= 0: diff --git a/kvmd/plugins/hid/otg/device.py b/kvmd/plugins/hid/otg/device.py index dfab658d..a3bc2739 100644 --- a/kvmd/plugins/hid/otg/device.py +++ b/kvmd/plugins/hid/otg/device.py @@ -192,13 +192,13 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in else: logger.error("HID-%s write() error: written (%s) != report length (%d)", self.__name, written, len(report)) - except Exception as err: - if isinstance(err, OSError) and ( + except Exception as ex: + if isinstance(ex, OSError) and ( # https://github.com/raspberrypi/linux/commit/61b7f805dc2fd364e0df682de89227e94ce88e25 - err.errno == errno.EAGAIN # pylint: disable=no-member - or err.errno == errno.ESHUTDOWN # pylint: disable=no-member + ex.errno == errno.EAGAIN # pylint: disable=no-member + or ex.errno == errno.ESHUTDOWN # pylint: disable=no-member ): - logger.debug("HID-%s busy/unplugged (write): %s", self.__name, tools.efmt(err)) + logger.debug("HID-%s busy/unplugged (write): %s", self.__name, tools.efmt(ex)) else: logger.exception("Can't write report to HID-%s", self.__name) @@ -216,16 +216,16 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in while read: try: read = bool(select.select([self.__fd], [], [], 0)[0]) - except Exception as err: - logger.error("Can't select() for read HID-%s: %s", self.__name, tools.efmt(err)) + except Exception as ex: + logger.error("Can't select() for read HID-%s: %s", self.__name, tools.efmt(ex)) break if read: try: report = os.read(self.__fd, self.__read_size) - except Exception as err: - if isinstance(err, OSError) and err.errno == errno.EAGAIN: # pylint: disable=no-member - logger.debug("HID-%s busy/unplugged (read): %s", self.__name, tools.efmt(err)) + except Exception as ex: + if isinstance(ex, OSError) and ex.errno == errno.EAGAIN: # pylint: disable=no-member + logger.debug("HID-%s busy/unplugged (read): %s", self.__name, tools.efmt(ex)) else: logger.exception("Can't read report from HID-%s", self.__name) else: @@ -255,9 +255,9 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in flags = os.O_NONBLOCK flags |= (os.O_RDWR if self.__read_size else os.O_WRONLY) self.__fd = os.open(self.__device_path, flags) - except Exception as err: + except Exception as ex: logger.error("Can't open HID-%s device %s: %s", - self.__name, self.__device_path, tools.efmt(err)) + self.__name, self.__device_path, tools.efmt(ex)) if self.__fd >= 0: try: @@ -268,8 +268,8 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in else: # Если запись недоступна, то скорее всего устройство отключено logger.debug("HID-%s is busy/unplugged (write select)", self.__name) - except Exception as err: - logger.error("Can't select() for write HID-%s: %s", self.__name, tools.efmt(err)) + except Exception as ex: + logger.error("Can't select() for write HID-%s: %s", self.__name, tools.efmt(ex)) self.__state_flags.update(online=False) return False diff --git a/kvmd/plugins/hid/serial.py b/kvmd/plugins/hid/serial.py index 4b8e6165..040828b4 100644 --- a/kvmd/plugins/hid/serial.py +++ b/kvmd/plugins/hid/serial.py @@ -44,12 +44,12 @@ class _SerialPhyConnection(BasePhyConnection): def __init__(self, tty: serial.Serial) -> None: self.__tty = tty - def send(self, request: bytes) -> bytes: - assert len(request) == 8 - assert request[0] == 0x33 + def send(self, req: bytes) -> bytes: + assert len(req) == 8 + assert req[0] == 0x33 if self.__tty.in_waiting: self.__tty.read_all() - assert self.__tty.write(request) == 8 + assert self.__tty.write(req) == 8 data = self.__tty.read(4) if len(data) == 4: if data[0] == 0x34: # New response protocol diff --git a/kvmd/plugins/hid/spi.py b/kvmd/plugins/hid/spi.py index dadcc77e..cf58b5de 100644 --- a/kvmd/plugins/hid/spi.py +++ b/kvmd/plugins/hid/spi.py @@ -57,9 +57,9 @@ class _SpiPhyConnection(BasePhyConnection): self.__xfer = xfer self.__read_timeout = read_timeout - def send(self, request: bytes) -> bytes: - assert len(request) == 8 - assert request[0] == 0x33 + def send(self, req: bytes) -> bytes: + assert len(req) == 8 + assert req[0] == 0x33 deadline_ts = time.monotonic() + self.__read_timeout dummy = b"\x00" * 10 @@ -70,26 +70,26 @@ class _SpiPhyConnection(BasePhyConnection): get_logger(0).error("SPI timeout reached while garbage reading") return b"" - self.__xfer(request) + self.__xfer(req) - response: list[int] = [] + resp: list[int] = [] deadline_ts = time.monotonic() + self.__read_timeout found = False while time.monotonic() < deadline_ts: - for byte in self.__xfer(b"\x00" * (9 - len(response))): + for byte in self.__xfer(b"\x00" * (9 - len(resp))): if not found: if byte == 0: continue found = True - response.append(byte) - if len(response) == 8: + resp.append(byte) + if len(resp) == 8: break - if len(response) == 8: + if len(resp) == 8: break else: get_logger(0).error("SPI timeout reached while responce waiting") return b"" - return bytes(response) + return bytes(resp) class _SpiPhy(BasePhy): # pylint: disable=too-many-instance-attributes diff --git a/kvmd/plugins/msd/otg/drive.py b/kvmd/plugins/msd/otg/drive.py index 1bae91e5..825354a5 100644 --- a/kvmd/plugins/msd/otg/drive.py +++ b/kvmd/plugins/msd/otg/drive.py @@ -82,7 +82,7 @@ class Drive: try: with open(os.path.join(self.__lun_path, param), "w") as file: file.write(value + "\n") - except OSError as err: - if err.errno == errno.EBUSY: + except OSError as ex: + if ex.errno == errno.EBUSY: raise MsdDriveLockedError() raise diff --git a/kvmd/plugins/ugpio/anelpwr.py b/kvmd/plugins/ugpio/anelpwr.py index a9cc432b..af83f0a2 100644 --- a/kvmd/plugins/ugpio/anelpwr.py +++ b/kvmd/plugins/ugpio/anelpwr.py @@ -113,13 +113,13 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute while True: session = self.__ensure_http_session() try: - async with session.get(f"{self.__url}/strg.cfg") as response: - htclient.raise_not_200(response) - parts = (await response.text()).split(";") + async with session.get(f"{self.__url}/strg.cfg") as resp: + htclient.raise_not_200(resp) + parts = (await resp.text()).split(";") for pin in self.__state: self.__state[pin] = (parts[1 + int(pin) * 5] == "1") - except Exception as err: - get_logger().error("Failed ANELPWR bulk GET request: %s", tools.efmt(err)) + except Exception as ex: + get_logger().error("Failed ANELPWR bulk GET request: %s", tools.efmt(ex)) self.__state = dict.fromkeys(self.__state, None) if self.__state != prev_state: self._notifier.notify() @@ -143,10 +143,10 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute url=f"{self.__url}/ctrl.htm", data=f"F{pin}={int(state)}", headers={"Content-Type": "text/plain"}, - ) as response: - htclient.raise_not_200(response) - except Exception as err: - get_logger().error("Failed ANELPWR POST request to pin %s: %s", pin, tools.efmt(err)) + ) as resp: + htclient.raise_not_200(resp) + except Exception as ex: + get_logger().error("Failed ANELPWR POST request to pin %s: %s", pin, tools.efmt(ex)) raise GpioDriverOfflineError(self) self.__update_notifier.notify() diff --git a/kvmd/plugins/ugpio/cmd.py b/kvmd/plugins/ugpio/cmd.py index c3392b68..b8581ce3 100644 --- a/kvmd/plugins/ugpio/cmd.py +++ b/kvmd/plugins/ugpio/cmd.py @@ -78,9 +78,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute proc = await aioproc.log_process(self.__cmd, logger=get_logger(0), prefix=str(self)) if proc.returncode != 0: raise RuntimeError(f"Custom command error: retcode={proc.returncode}") - except Exception as err: + except Exception as ex: get_logger(0).error("Can't run custom command [ %s ]: %s", - tools.cmdfmt(self.__cmd), tools.efmt(err)) + tools.cmdfmt(self.__cmd), tools.efmt(ex)) raise GpioDriverOfflineError(self) def __str__(self) -> str: diff --git a/kvmd/plugins/ugpio/cmdret.py b/kvmd/plugins/ugpio/cmdret.py index 0eea78f6..7080a390 100644 --- a/kvmd/plugins/ugpio/cmdret.py +++ b/kvmd/plugins/ugpio/cmdret.py @@ -71,9 +71,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute try: proc = await aioproc.log_process(self.__cmd, logger=get_logger(0), prefix=str(self)) return (proc.returncode == 0) - except Exception as err: + except Exception as ex: get_logger(0).error("Can't run custom command [ %s ]: %s", - tools.cmdfmt(self.__cmd), tools.efmt(err)) + tools.cmdfmt(self.__cmd), tools.efmt(ex)) raise GpioDriverOfflineError(self) async def write(self, pin: str, state: bool) -> None: diff --git a/kvmd/plugins/ugpio/extron.py b/kvmd/plugins/ugpio/extron.py index cb9fe96e..81a66c92 100644 --- a/kvmd/plugins/ugpio/extron.py +++ b/kvmd/plugins/ugpio/extron.py @@ -150,9 +150,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute assert channel is not None self.__send_channel(tty, channel) - except Exception as err: + except Exception as ex: self.__channel_queue.put_nowait(None) - if isinstance(err, serial.SerialException) and err.errno == errno.ENOENT: # pylint: disable=no-member + if isinstance(ex, serial.SerialException) and ex.errno == errno.ENOENT: # pylint: disable=no-member logger.error("Missing %s serial device: %s", self, self.__device_path) else: logger.exception("Unexpected %s error", self) diff --git a/kvmd/plugins/ugpio/ezcoo.py b/kvmd/plugins/ugpio/ezcoo.py index c3502a61..d5bd8ef8 100644 --- a/kvmd/plugins/ugpio/ezcoo.py +++ b/kvmd/plugins/ugpio/ezcoo.py @@ -150,9 +150,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute assert channel is not None self.__send_channel(tty, channel) - except Exception as err: + except Exception as ex: self.__channel_queue.put_nowait(None) - if isinstance(err, serial.SerialException) and err.errno == errno.ENOENT: # pylint: disable=no-member + if isinstance(ex, serial.SerialException) and ex.errno == errno.ENOENT: # pylint: disable=no-member logger.error("Missing %s serial device: %s", self, self.__device_path) else: logger.exception("Unexpected %s error", self) diff --git a/kvmd/plugins/ugpio/gpio.py b/kvmd/plugins/ugpio/gpio.py index 1ae1fbe0..6cda826b 100644 --- a/kvmd/plugins/ugpio/gpio.py +++ b/kvmd/plugins/ugpio/gpio.py @@ -54,7 +54,7 @@ class Plugin(BaseUserGpioDriver): self.__output_pins: dict[int, (bool | None)] = {} self.__reader: (aiogp.AioReader | None) = None - self.__outputs_request: (gpiod.LineRequest | None) = None + self.__outputs_req: (gpiod.LineRequest | None) = None @classmethod def get_plugin_options(cls) -> dict: @@ -74,7 +74,7 @@ class Plugin(BaseUserGpioDriver): def prepare(self) -> None: assert self.__reader is None - assert self.__outputs_request is None + assert self.__outputs_req is None self.__reader = aiogp.AioReader( path=self.__device_path, consumer="kvmd::gpio::inputs", @@ -82,7 +82,7 @@ class Plugin(BaseUserGpioDriver): notifier=self._notifier, ) if self.__output_pins: - self.__outputs_request = gpiod.request_lines( + self.__outputs_req = gpiod.request_lines( self.__device_path, consumer="kvmd::gpiod::outputs", config={ @@ -99,9 +99,9 @@ class Plugin(BaseUserGpioDriver): await self.__reader.poll() async def cleanup(self) -> None: - if self.__outputs_request: + if self.__outputs_req: try: - self.__outputs_request.release() + self.__outputs_req.release() except Exception: pass @@ -110,15 +110,15 @@ class Plugin(BaseUserGpioDriver): pin_int = int(pin) if pin_int in self.__input_pins: return self.__reader.get(pin_int) - assert self.__outputs_request + assert self.__outputs_req assert pin_int in self.__output_pins - return bool(self.__outputs_request.get_value(pin_int).value) + return bool(self.__outputs_req.get_value(pin_int).value) async def write(self, pin: str, state: bool) -> None: - assert self.__outputs_request + assert self.__outputs_req pin_int = int(pin) assert pin_int in self.__output_pins - self.__outputs_request.set_value(pin_int, gpiod.line.Value(state)) + self.__outputs_req.set_value(pin_int, gpiod.line.Value(state)) def __str__(self) -> str: return f"GPIO({self._instance_name})" diff --git a/kvmd/plugins/ugpio/hidrelay.py b/kvmd/plugins/ugpio/hidrelay.py index 894da42c..17f41e27 100644 --- a/kvmd/plugins/ugpio/hidrelay.py +++ b/kvmd/plugins/ugpio/hidrelay.py @@ -93,9 +93,9 @@ class Plugin(BaseUserGpioDriver): try: with self.__ensure_device("probing"): pass - except Exception as err: + except Exception as ex: logger.error("Can't probe %s on %s: %s", - self, self.__device_path, tools.efmt(err)) + self, self.__device_path, tools.efmt(ex)) self.__reset_pins() async def run(self) -> None: @@ -137,9 +137,9 @@ class Plugin(BaseUserGpioDriver): pin, state, self, self.__device_path) try: self.__inner_write(pin, state) - except Exception as err: + except Exception as ex: logger.error("Can't reset pin=%d of %s on %s: %s", - pin, self, self.__device_path, tools.efmt(err)) + pin, self, self.__device_path, tools.efmt(ex)) def __inner_read(self, pin: int) -> bool: assert 0 <= pin <= 7 @@ -168,9 +168,9 @@ class Plugin(BaseUserGpioDriver): get_logger(0).info("Opened %s on %s while %s", self, self.__device_path, context) try: yield self.__device - except Exception as err: + except Exception as ex: get_logger(0).error("Error occured on %s on %s while %s: %s", - self, self.__device_path, context, tools.efmt(err)) + self, self.__device_path, context, tools.efmt(ex)) self.__close_device() raise diff --git a/kvmd/plugins/ugpio/hue.py b/kvmd/plugins/ugpio/hue.py index 2c0e6749..9ed9e206 100644 --- a/kvmd/plugins/ugpio/hue.py +++ b/kvmd/plugins/ugpio/hue.py @@ -111,13 +111,13 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute while True: session = self.__ensure_http_session() try: - async with session.get(f"{self.__url}/api/{self.__token}/lights") as response: - results = await response.json() + async with session.get(f"{self.__url}/api/{self.__token}/lights") as resp: + results = await resp.json() for pin in self.__state: if pin in results: self.__state[pin] = bool(results[pin]["state"]["on"]) - except Exception as err: - get_logger().error("Failed Hue bulk GET request: %s", tools.efmt(err)) + except Exception as ex: + get_logger().error("Failed Hue bulk GET request: %s", tools.efmt(ex)) self.__state = dict.fromkeys(self.__state, None) if self.__state != prev_state: self._notifier.notify() @@ -140,10 +140,10 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute async with session.put( url=f"{self.__url}/api/{self.__token}/lights/{pin}/state", json={"on": state}, - ) as response: - htclient.raise_not_200(response) - except Exception as err: - get_logger().error("Failed Hue PUT request to pin %s: %s", pin, tools.efmt(err)) + ) as resp: + htclient.raise_not_200(resp) + except Exception as ex: + get_logger().error("Failed Hue PUT request to pin %s: %s", pin, tools.efmt(ex)) raise GpioDriverOfflineError(self) self.__update_notifier.notify() diff --git a/kvmd/plugins/ugpio/ipmi.py b/kvmd/plugins/ugpio/ipmi.py index 8aec6c3d..37a7a16f 100644 --- a/kvmd/plugins/ugpio/ipmi.py +++ b/kvmd/plugins/ugpio/ipmi.py @@ -153,9 +153,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute proc = await aioproc.log_process(**self.__make_ipmitool_kwargs(action), logger=get_logger(0), prefix=str(self)) if proc.returncode != 0: raise RuntimeError(f"Ipmitool error: retcode={proc.returncode}") - except Exception as err: + except Exception as ex: get_logger(0).error("Can't send IPMI power-%s request to %s:%d: %s", - action, self.__host, self.__port, tools.efmt(err)) + action, self.__host, self.__port, tools.efmt(ex)) raise GpioDriverOfflineError(self) # ===== @@ -171,9 +171,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute self.__online = True return raise RuntimeError(f"Invalid ipmitool response: {text}") - except Exception as err: + except Exception as ex: get_logger(0).error("Can't fetch IPMI power status from %s:%d: %s", - self.__host, self.__port, tools.efmt(err)) + self.__host, self.__port, tools.efmt(ex)) self.__power = False self.__online = False diff --git a/kvmd/plugins/ugpio/locator.py b/kvmd/plugins/ugpio/locator.py index f42116a7..d5cba719 100644 --- a/kvmd/plugins/ugpio/locator.py +++ b/kvmd/plugins/ugpio/locator.py @@ -53,7 +53,7 @@ class Plugin(BaseUserGpioDriver): self.__device_path = device_path self.__tasks: dict[int, (asyncio.Task | None)] = {} - self.__line_request: (gpiod.LineRequest | None) = None + self.__line_req: (gpiod.LineRequest | None) = None @classmethod def get_plugin_options(cls) -> dict: @@ -74,7 +74,7 @@ class Plugin(BaseUserGpioDriver): self.__tasks[int(pin)] = None def prepare(self) -> None: - self.__line_request = gpiod.request_lines( + self.__line_req = gpiod.request_lines( self.__device_path, consumer="kvmd::locator", config={ @@ -94,9 +94,9 @@ class Plugin(BaseUserGpioDriver): for task in tasks: task.cancel() await asyncio.gather(*tasks, return_exceptions=True) - if self.__line_request: + if self.__line_req: try: - self.__line_request.release() + self.__line_req.release() except Exception: pass @@ -115,17 +115,17 @@ class Plugin(BaseUserGpioDriver): async def __blink(self, pin: int) -> None: assert pin in self.__tasks - assert self.__line_request + assert self.__line_req try: state = True while True: - self.__line_request.set_value(pin, gpiod.line.Value(state)) + self.__line_req.set_value(pin, gpiod.line.Value(state)) state = (not state) await asyncio.sleep(0.1) except asyncio.CancelledError: pass finally: - self.__line_request.set_value(pin, gpiod.line.Value(False)) + self.__line_req.set_value(pin, gpiod.line.Value(False)) def __str__(self) -> str: return f"Locator({self._instance_name})" diff --git a/kvmd/plugins/ugpio/noyito.py b/kvmd/plugins/ugpio/noyito.py index f3c04a57..7363e2d4 100644 --- a/kvmd/plugins/ugpio/noyito.py +++ b/kvmd/plugins/ugpio/noyito.py @@ -91,9 +91,9 @@ class Plugin(BaseUserGpioDriver): try: with self.__ensure_device("probing"): pass - except Exception as err: + except Exception as ex: logger.error("Can't probe %s on %s: %s", - self, self.__device_path, tools.efmt(err)) + self, self.__device_path, tools.efmt(ex)) self.__reset_pins() async def cleanup(self) -> None: @@ -119,9 +119,9 @@ class Plugin(BaseUserGpioDriver): pin, state, self, self.__device_path) try: self.__inner_write(pin, state) - except Exception as err: + except Exception as ex: logger.error("Can't reset pin=%d of %s on %s: %s", - pin, self, self.__device_path, tools.efmt(err)) + pin, self, self.__device_path, tools.efmt(ex)) def __inner_write(self, pin: int, state: bool) -> None: assert 0 <= pin <= 7 @@ -144,9 +144,9 @@ class Plugin(BaseUserGpioDriver): get_logger(0).info("Opened %s on %s while %s", self, self.__device_path, context) try: yield self.__device - except Exception as err: + except Exception as ex: get_logger(0).error("Error occured on %s on %s while %s: %s", - self, self.__device_path, context, tools.efmt(err)) + self, self.__device_path, context, tools.efmt(ex)) self.__close_device() raise diff --git a/kvmd/plugins/ugpio/pway.py b/kvmd/plugins/ugpio/pway.py index 25a1f454..140cf02a 100644 --- a/kvmd/plugins/ugpio/pway.py +++ b/kvmd/plugins/ugpio/pway.py @@ -153,9 +153,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute assert channel is not None self.__send_channel(tty, channel) - except Exception as err: + except Exception as ex: self.__channel_queue.put_nowait(None) - if isinstance(err, serial.SerialException) and err.errno == errno.ENOENT: # pylint: disable=no-member + if isinstance(ex, serial.SerialException) and ex.errno == errno.ENOENT: # pylint: disable=no-member logger.error("Missing %s serial device: %s", self, self.__device_path) else: logger.exception("Unexpected %s error", self) diff --git a/kvmd/plugins/ugpio/pwm.py b/kvmd/plugins/ugpio/pwm.py index 8aedb771..f202836e 100644 --- a/kvmd/plugins/ugpio/pwm.py +++ b/kvmd/plugins/ugpio/pwm.py @@ -94,18 +94,18 @@ class Plugin(BaseUserGpioDriver): pwm.period_ns = self.__period pwm.duty_cycle_ns = self.__get_duty_cycle(bool(initial)) pwm.enable() - except Exception as err: + except Exception as ex: logger.error("Can't get PWM chip %d channel %d: %s", - self.__chip, pin, tools.efmt(err)) + self.__chip, pin, tools.efmt(ex)) async def cleanup(self) -> None: for (pin, pwm) in self.__pwms.items(): try: pwm.disable() pwm.close() - except Exception as err: + except Exception as ex: get_logger(0).error("Can't cleanup PWM chip %d channel %d: %s", - self.__chip, pin, tools.efmt(err)) + self.__chip, pin, tools.efmt(ex)) async def read(self, pin: str) -> bool: try: diff --git a/kvmd/plugins/ugpio/tesmart.py b/kvmd/plugins/ugpio/tesmart.py index 5a5cd441..bb1d39e1 100644 --- a/kvmd/plugins/ugpio/tesmart.py +++ b/kvmd/plugins/ugpio/tesmart.py @@ -146,9 +146,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute asyncio.ensure_future(self.__reader.readexactly(6)), timeout=self.__timeout, ))[4] - except Exception as err: + except Exception as ex: get_logger(0).error("Can't send command to TESmart KVM [%s]:%d: %s", - self.__host, self.__port, tools.efmt(err)) + self.__host, self.__port, tools.efmt(ex)) await self.__close_device() self.__active = -1 raise GpioDriverOfflineError(self) @@ -168,9 +168,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute asyncio.ensure_future(asyncio.open_connection(self.__host, self.__port)), timeout=self.__timeout, ) - except Exception as err: + except Exception as ex: get_logger(0).error("Can't connect to TESmart KVM [%s]:%d: %s", - self.__host, self.__port, tools.efmt(err)) + self.__host, self.__port, tools.efmt(ex)) raise GpioDriverOfflineError(self) async def __ensure_device_serial(self) -> None: @@ -179,9 +179,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute serial_asyncio.open_serial_connection(url=self.__device_path, baudrate=self.__speed), timeout=self.__timeout, ) - except Exception as err: + except Exception as ex: get_logger(0).error("Can't connect to TESmart KVM [%s]:%d: %s", - self.__device_path, self.__speed, tools.efmt(err)) + self.__device_path, self.__speed, tools.efmt(ex)) raise GpioDriverOfflineError(self) async def __close_device(self) -> None: diff --git a/kvmd/plugins/ugpio/xh_hk4401.py b/kvmd/plugins/ugpio/xh_hk4401.py index 7e064df9..d7a47679 100644 --- a/kvmd/plugins/ugpio/xh_hk4401.py +++ b/kvmd/plugins/ugpio/xh_hk4401.py @@ -157,9 +157,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute if self.__protocol == 2: self.__channel_queue.put_nowait(channel) - except Exception as err: + except Exception as ex: self.__channel_queue.put_nowait(None) - if isinstance(err, serial.SerialException) and err.errno == errno.ENOENT: # pylint: disable=no-member + if isinstance(ex, serial.SerialException) and ex.errno == errno.ENOENT: # pylint: disable=no-member logger.error("Missing %s serial device: %s", self, self.__device_path) else: logger.exception("Unexpected %s error", self) diff --git a/kvmd/tools.py b/kvmd/tools.py index c96ff38f..14a58ab3 100644 --- a/kvmd/tools.py +++ b/kvmd/tools.py @@ -39,8 +39,8 @@ def cmdfmt(cmd: list[str]) -> str: return " ".join(map(shlex.quote, cmd)) -def efmt(err: Exception) -> str: - return f"{type(err).__name__}: {err}" +def efmt(ex: Exception) -> str: + return f"{type(ex).__name__}: {ex}" # ===== diff --git a/kvmd/validators/net.py b/kvmd/validators/net.py index c14dd3f6..301b160e 100644 --- a/kvmd/validators/net.py +++ b/kvmd/validators/net.py @@ -112,8 +112,8 @@ def valid_ssl_ciphers(arg: Any) -> str: arg = valid_stripped_string_not_empty(arg, name) try: ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER).set_ciphers(arg) - except Exception as err: - raise ValidatorError(f"The argument {arg!r} is not a valid {name}: {err}") + except Exception as ex: + raise ValidatorError(f"The argument {arg!r} is not a valid {name}: {ex}") return arg diff --git a/kvmd/validators/os.py b/kvmd/validators/os.py index 7b38f171..94d3a40f 100644 --- a/kvmd/validators/os.py +++ b/kvmd/validators/os.py @@ -55,8 +55,8 @@ def valid_abs_path(arg: Any, type: str="", name: str="") -> str: # pylint: disa if type: try: st = os.stat(arg) - except Exception as err: - raise_error(arg, f"{name}: {err}") + except Exception as ex: + raise_error(arg, f"{name}: {ex}") else: if not getattr(stat, f"S_IS{type.upper()}")(st.st_mode): raise_error(arg, name) diff --git a/kvmd/yamlconf/__init__.py b/kvmd/yamlconf/__init__.py index 42a69cfa..7cd3808d 100644 --- a/kvmd/yamlconf/__init__.py +++ b/kvmd/yamlconf/__init__.py @@ -143,8 +143,8 @@ class Option: def manual_validated(value: Any, *path: str) -> Generator[None, None, None]: try: yield - except (TypeError, ValueError) as err: - raise ConfigError(f"Invalid value {value!r} for key {'/'.join(path)!r}: {err}") + except (TypeError, ValueError) as ex: + raise ConfigError(f"Invalid value {value!r} for key {'/'.join(path)!r}: {ex}") def make_config(raw: dict[str, Any], scheme: dict[str, Any], _keys: tuple[str, ...]=()) -> Section: @@ -185,8 +185,8 @@ def make_config(raw: dict[str, Any], scheme: dict[str, Any], _keys: tuple[str, . else: try: value = option.type(value) - except (TypeError, ValueError) as err: - raise ConfigError(f"Invalid value {value!r} for key {make_full_name(key)!r}: {err}") + except (TypeError, ValueError) as ex: + raise ConfigError(f"Invalid value {value!r} for key {make_full_name(key)!r}: {ex}") config[key] = value config._set_meta( # pylint: disable=protected-access diff --git a/kvmd/yamlconf/loader.py b/kvmd/yamlconf/loader.py index 215cb526..5f879354 100644 --- a/kvmd/yamlconf/loader.py +++ b/kvmd/yamlconf/loader.py @@ -40,9 +40,9 @@ def load_yaml_file(path: str) -> Any: with open(path) as file: try: return yaml.load(file, _YamlLoader) - except Exception as err: + except Exception as ex: # Reraise internal exception as standard ValueError and show the incorrect file - raise ValueError(f"Invalid YAML in the file {path!r}:\n{tools.efmt(err)}") from None + raise ValueError(f"Invalid YAML in the file {path!r}:\n{tools.efmt(ex)}") from None # ===== diff --git a/testenv/tests/plugins/auth/test_http.py b/testenv/tests/plugins/auth/test_http.py index 07b41374..252ad85b 100644 --- a/testenv/tests/plugins/auth/test_http.py +++ b/testenv/tests/plugins/auth/test_http.py @@ -32,10 +32,10 @@ from . import get_configured_auth_service # ===== -async def _handle_auth(request: aiohttp.web.BaseRequest) -> aiohttp.web.Response: +async def _handle_auth(req: aiohttp.web.BaseRequest) -> aiohttp.web.Response: status = 400 - if request.method == "POST": - credentials = (await request.json()) + if req.method == "POST": + credentials = (await req.json()) if credentials["user"] == "admin" and credentials["passwd"] == "pass": status = 200 return aiohttp.web.Response(text=str(status), status=status) diff --git a/web/share/js/kvm/msd.js b/web/share/js/kvm/msd.js index 356e39f6..662c04e4 100644 --- a/web/share/js/kvm/msd.js +++ b/web/share/js/kvm/msd.js @@ -141,8 +141,8 @@ export function Msd() { if (!result.ok) { msg = `Can't upload image to the Mass Storage Drive:
    ${result_str}`; } - } catch (err) { - msg = `Can't parse upload result:
    ${err}`; + } catch (ex) { + msg = `Can't parse upload result:
    ${ex}`; } if (msg.length > 0) { wm.error(msg); diff --git a/web/share/js/kvm/recorder.js b/web/share/js/kvm/recorder.js index 1ed340ec..03ec39b5 100644 --- a/web/share/js/kvm/recorder.js +++ b/web/share/js/kvm/recorder.js @@ -214,8 +214,8 @@ export function Recorder() { __events = events; __events_time = events_time; - } catch (err) { - wm.error(`Invalid script: ${err}`); + } catch (ex) { + wm.error(`Invalid script: ${ex}`); } el_input.value = ""; diff --git a/web/share/js/kvm/session.js b/web/share/js/kvm/session.js index a183b33e..98070927 100644 --- a/web/share/js/kvm/session.js +++ b/web/share/js/kvm/session.js @@ -411,8 +411,8 @@ export function Session() { throw new Error("Too many missed heartbeats"); } __ws.send("{\"event_type\": \"ping\", \"event\": {}}"); - } catch (err) { - __wsErrorHandler(err.message); + } catch (ex) { + __wsErrorHandler(ex.message); } }; diff --git a/web/share/js/kvm/stream_janus.js b/web/share/js/kvm/stream_janus.js index 0400267b..be62dbbd 100644 --- a/web/share/js/kvm/stream_janus.js +++ b/web/share/js/kvm/stream_janus.js @@ -435,8 +435,8 @@ JanusStreamer.ensure_janus = function(callback) { callback(true); }, }); - }).catch((err) => { - tools.error("Stream: Can't import Janus module:", err); + }).catch((ex) => { + tools.error("Stream: Can't import Janus module:", ex); callback(false); }); } else { diff --git a/web/share/js/wm.js b/web/share/js/wm.js index 11f65faa..6a26a648 100644 --- a/web/share/js/wm.js +++ b/web/share/js/wm.js @@ -145,10 +145,10 @@ function __WindowManager() { /************************************************************************/ self.copyTextToClipboard = function(text) { - let workaround = function(err) { + let workaround = function(ex) { // https://stackoverflow.com/questions/60317969/document-execcommandcopy-not-working-even-though-the-dom-element-is-created let callback = function() { - tools.error("copyTextToClipboard(): navigator.clipboard.writeText() is not working:", err); + tools.error("copyTextToClipboard(): navigator.clipboard.writeText() is not working:", ex); tools.info("copyTextToClipboard(): Trying a workaround..."); let el = document.createElement("textarea"); @@ -164,16 +164,16 @@ function __WindowManager() { el.setSelectionRange(0, el.value.length); // iOS try { - err = (document.execCommand("copy") ? null : "Unknown error"); - } catch (err) { // eslint-disable-line no-unused-vars + ex = (document.execCommand("copy") ? null : "Unknown error"); + } catch (ex) { // eslint-disable-line no-unused-vars } // Remove the added textarea again: document.body.removeChild(el); - if (err) { - tools.error("copyTextToClipboard(): Workaround failed:", err); - wm.error("Can't copy text to the clipboard:
    ", err); + if (ex) { + tools.error("copyTextToClipboard(): Workaround failed:", ex); + wm.error("Can't copy text to the clipboard:
    ", ex); } }; __modalDialog("Info", "Press OK to copy the text to the clipboard", true, false, callback); @@ -181,8 +181,8 @@ function __WindowManager() { if (navigator.clipboard) { navigator.clipboard.writeText(text).then(function() { wm.info("The text has been copied to the clipboard"); - }, function(err) { - workaround(err); + }, function(ex) { + workaround(ex); }); } else { workaround("navigator.clipboard is not available"); From 842ddc91a19b828db701c4b0b44e201a948fd627 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Fri, 20 Sep 2024 01:11:22 +0300 Subject: [PATCH 44/88] refactoring --- kvmd/apps/edidconf/__init__.py | 257 ++----------------------------- kvmd/edid.py | 269 +++++++++++++++++++++++++++++++++ 2 files changed, 279 insertions(+), 247 deletions(-) create mode 100644 kvmd/edid.py diff --git a/kvmd/apps/edidconf/__init__.py b/kvmd/apps/edidconf/__init__.py index d213a871..e21f797b 100644 --- a/kvmd/apps/edidconf/__init__.py +++ b/kvmd/apps/edidconf/__init__.py @@ -22,259 +22,22 @@ import sys import os -import re -import dataclasses -import contextlib import subprocess import argparse import time -from typing import IO -from typing import Generator from typing import Callable from ...validators.basic import valid_bool from ...validators.basic import valid_int_f0 +from ...edid import EdidNoBlockError +from ...edid import Edid + # from .. import init # ===== -class NoBlockError(Exception): - pass - - -@contextlib.contextmanager -def _smart_open(path: str, mode: str) -> Generator[IO, None, None]: - fd = (0 if "r" in mode else 1) - with (os.fdopen(fd, mode, closefd=False) if path == "-" else open(path, mode)) as file: - yield file - if "w" in mode: - file.flush() - - -@dataclasses.dataclass(frozen=True) -class _CeaBlock: - tag: int - data: bytes - - def __post_init__(self) -> None: - assert 0 < self.tag <= 0b111 - assert 0 < len(self.data) <= 0b11111 - - @property - def size(self) -> int: - return len(self.data) + 1 - - def pack(self) -> bytes: - header = (self.tag << 5) | len(self.data) - return header.to_bytes() + self.data - - @classmethod - def first_from_raw(cls, raw: (bytes | list[int])) -> "_CeaBlock": - assert 0 < raw[0] <= 0xFF - tag = (raw[0] & 0b11100000) >> 5 - data_size = (raw[0] & 0b00011111) - data = bytes(raw[1:data_size + 1]) - return _CeaBlock(tag, data) - - -_CEA = 128 -_CEA_AUDIO = 1 -_CEA_SPEAKERS = 4 - - -class _Edid: - # https://en.wikipedia.org/wiki/Extended_Display_Identification_Data - - def __init__(self, path: str) -> None: - with _smart_open(path, "rb") as file: - data = file.read() - if data.startswith(b"\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00"): - self.__data = list(data) - else: - text = re.sub(r"\s", "", data.decode()) - self.__data = [ - int(text[index:index + 2], 16) - for index in range(0, len(text), 2) - ] - assert len(self.__data) == 256, f"Invalid EDID length: {len(self.__data)}, should be 256 bytes" - assert self.__data[126] == 1, "Zero extensions number" - assert (self.__data[_CEA + 0], self.__data[_CEA + 1]) == (0x02, 0x03), "Can't find CEA extension" - - def write_hex(self, path: str) -> None: - self.__update_checksums() - text = "\n".join( - "".join( - f"{item:0{2}X}" - for item in self.__data[index:index + 16] - ) - for index in range(0, len(self.__data), 16) - ) + "\n" - with _smart_open(path, "w") as file: - file.write(text) - - def write_bin(self, path: str) -> None: - self.__update_checksums() - with _smart_open(path, "wb") as file: - file.write(bytes(self.__data)) - - def __update_checksums(self) -> None: - self.__data[127] = 256 - (sum(self.__data[:127]) % 256) - self.__data[255] = 256 - (sum(self.__data[128:255]) % 256) - - # ===== - - def get_mfc_id(self) -> str: - raw = self.__data[8] << 8 | self.__data[9] - return bytes([ - ((raw >> 10) & 0b11111) + 0x40, - ((raw >> 5) & 0b11111) + 0x40, - (raw & 0b11111) + 0x40, - ]).decode("ascii") - - def set_mfc_id(self, mfc_id: str) -> None: - assert len(mfc_id) == 3, "Mfc ID must be 3 characters long" - data = mfc_id.upper().encode("ascii") - for ch in data: - assert 0x41 <= ch <= 0x5A, "Mfc ID must contain only A-Z characters" - raw = ( - (data[2] - 0x40) - | ((data[1] - 0x40) << 5) - | ((data[0] - 0x40) << 10) - ) - self.__data[8] = (raw >> 8) & 0xFF - self.__data[9] = raw & 0xFF - - # ===== - - def get_product_id(self) -> int: - return (self.__data[10] | self.__data[11] << 8) - - def set_product_id(self, product_id: int) -> None: - assert 0 <= product_id <= 0xFFFF, f"Product ID should be from 0 to {0xFFFF}" - self.__data[10] = product_id & 0xFF - self.__data[11] = (product_id >> 8) & 0xFF - - # ===== - - def get_serial(self) -> int: - return ( - self.__data[12] - | self.__data[13] << 8 - | self.__data[14] << 16 - | self.__data[15] << 24 - ) - - def set_serial(self, serial: int) -> None: - assert 0 <= serial <= 0xFFFFFFFF, f"Serial should be from 0 to {0xFFFFFFFF}" - self.__data[12] = serial & 0xFF - self.__data[13] = (serial >> 8) & 0xFF - self.__data[14] = (serial >> 16) & 0xFF - self.__data[15] = (serial >> 24) & 0xFF - - # ===== - - def get_monitor_name(self) -> str: - return self.__get_dtd_text(0xFC, "Monitor Name") - - def set_monitor_name(self, text: str) -> None: - self.__set_dtd_text(0xFC, "Monitor Name", text) - - def get_monitor_serial(self) -> str: - return self.__get_dtd_text(0xFF, "Monitor Serial") - - def set_monitor_serial(self, text: str) -> None: - self.__set_dtd_text(0xFF, "Monitor Serial", text) - - def __get_dtd_text(self, d_type: int, name: str) -> str: - index = self.__find_dtd_text(d_type, name) - return bytes(self.__data[index:index + 13]).decode("cp437").strip() - - def __set_dtd_text(self, d_type: int, name: str, text: str) -> None: - index = self.__find_dtd_text(d_type, name) - encoded = (text[:13] + "\n" + " " * 12)[:13].encode("cp437") - for (offset, ch) in enumerate(encoded): - self.__data[index + offset] = ch - - def __find_dtd_text(self, d_type: int, name: str) -> int: - for index in [54, 72, 90, 108]: - if self.__data[index + 3] == d_type: - return index + 5 - raise NoBlockError(f"Can't find DTD {name}") - - # ===== CEA ===== - - def get_audio(self) -> bool: - (cbs, _) = self.__parse_cea() - audio = False - speakers = False - for cb in cbs: - if cb.tag == _CEA_AUDIO: - audio = True - elif cb.tag == _CEA_SPEAKERS: - speakers = True - return (audio and speakers and self.__get_basic_audio()) - - def set_audio(self, enabled: bool) -> None: - (cbs, dtds) = self.__parse_cea() - cbs = [cb for cb in cbs if cb.tag not in [_CEA_AUDIO, _CEA_SPEAKERS]] - if enabled: - cbs.append(_CeaBlock(_CEA_AUDIO, b"\x09\x7f\x07")) - cbs.append(_CeaBlock(_CEA_SPEAKERS, b"\x01\x00\x00")) - self.__replace_cea(cbs, dtds) - self.__set_basic_audio(enabled) - - def __get_basic_audio(self) -> bool: - return bool(self.__data[_CEA + 3] & 0b01000000) - - def __set_basic_audio(self, enabled: bool) -> None: - if enabled: - self.__data[_CEA + 3] |= 0b01000000 - else: - self.__data[_CEA + 3] &= (0xFF - 0b01000000) # ~X - - def __parse_cea(self) -> tuple[list[_CeaBlock], bytes]: - cea = self.__data[_CEA:] - dtd_begin = cea[2] - if dtd_begin == 0: - return ([], b"") - - cbs: list[_CeaBlock] = [] - if dtd_begin > 4: - raw = cea[4:dtd_begin] - while len(raw) != 0: - cb = _CeaBlock.first_from_raw(raw) - cbs.append(cb) - raw = raw[cb.size:] - - dtds = b"" - assert dtd_begin >= 4 - raw = cea[dtd_begin:] - while len(raw) > (18 + 1) and raw[0] != 0: - dtds += bytes(raw[:18]) - raw = raw[18:] - - return (cbs, dtds) - - def __replace_cea(self, cbs: list[_CeaBlock], dtds: bytes) -> None: - cbs_packed = b"" - for cb in cbs: - cbs_packed += cb.pack() - - raw = cbs_packed + dtds - assert len(raw) <= (128 - 4 - 1), "Too many CEA blocks or DTDs" - - self.__data[_CEA + 2] = (0 if len(raw) == 0 else (len(cbs_packed) + 4)) - - for index in range(4, 127): - try: - ch = raw[index - 4] - except IndexError: - ch = 0 - self.__data[_CEA + index] = ch - - def _format_bool(value: bool) -> str: return ("yes" if value else "no") @@ -283,7 +46,7 @@ def _make_format_hex(size: int) -> Callable[[int], str]: return (lambda value: ("0x{:0%dX} ({})" % (size * 2)).format(value, value)) -def _print_edid(edid: _Edid) -> None: +def _print_edid(edid: Edid) -> None: for (key, get, fmt) in [ ("Manufacturer ID:", edid.get_mfc_id, str), ("Product ID: ", edid.get_product_id, _make_format_hex(2)), @@ -294,7 +57,7 @@ def _print_edid(edid: _Edid) -> None: ]: try: print(key, fmt(get()), file=sys.stderr) # type: ignore - except NoBlockError: + except EdidNoBlockError: pass @@ -348,12 +111,12 @@ def main(argv: (list[str] | None)=None) -> None: # pylint: disable=too-many-bra help="Presets directory", metavar="") options = parser.parse_args(argv[1:]) - base: (_Edid | None) = None + base: (Edid | None) = None if options.import_preset: imp = options.import_preset if "." in imp: (base_name, imp) = imp.split(".", 1) # v3.1080p-by-default - base = _Edid(os.path.join(options.presets_path, f"{base_name}.hex")) + base = Edid.from_file(os.path.join(options.presets_path, f"{base_name}.hex")) imp = f"_{imp}" options.imp = os.path.join(options.presets_path, f"{imp}.hex") @@ -362,16 +125,16 @@ def main(argv: (list[str] | None)=None) -> None: # pylint: disable=too-many-bra options.export_hex = options.edid_path options.edid_path = options.imp - edid = _Edid(options.edid_path) + edid = Edid.from_file(options.edid_path) changed = False - for cmd in dir(_Edid): + for cmd in dir(Edid): if cmd.startswith("set_"): value = getattr(options, cmd) if value is None and base is not None: try: value = getattr(base, cmd.replace("set_", "get_"))() - except NoBlockError: + except EdidNoBlockError: pass if value is not None: getattr(edid, cmd)(value) diff --git a/kvmd/edid.py b/kvmd/edid.py new file mode 100644 index 00000000..b890a769 --- /dev/null +++ b/kvmd/edid.py @@ -0,0 +1,269 @@ +# ========================================================================== # +# # +# KVMD - The main PiKVM daemon. # +# # +# Copyright (C) 2018-2024 Maxim Devaev # +# # +# 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 . # +# # +# ========================================================================== # + + +import os +import re +import dataclasses +import contextlib + +from typing import IO +from typing import Generator + + +# ===== +class EdidNoBlockError(Exception): + pass + + +@contextlib.contextmanager +def _smart_open(path: str, mode: str) -> Generator[IO, None, None]: + fd = (0 if "r" in mode else 1) + with (os.fdopen(fd, mode, closefd=False) if path == "-" else open(path, mode)) as file: + yield file + if "w" in mode: + file.flush() + + +@dataclasses.dataclass(frozen=True) +class _CeaBlock: + tag: int + data: bytes + + def __post_init__(self) -> None: + assert 0 < self.tag <= 0b111 + assert 0 < len(self.data) <= 0b11111 + + @property + def size(self) -> int: + return len(self.data) + 1 + + def pack(self) -> bytes: + header = (self.tag << 5) | len(self.data) + return header.to_bytes() + self.data + + @classmethod + def first_from_raw(cls, raw: (bytes | list[int])) -> "_CeaBlock": + assert 0 < raw[0] <= 0xFF + tag = (raw[0] & 0b11100000) >> 5 + data_size = (raw[0] & 0b00011111) + data = bytes(raw[1:data_size + 1]) + return _CeaBlock(tag, data) + + +_CEA = 128 +_CEA_AUDIO = 1 +_CEA_SPEAKERS = 4 + + +class Edid: + # https://en.wikipedia.org/wiki/Extended_Display_Identification_Data + + def __init__(self, data: bytes) -> None: + assert len(data) == 256 + self.__data = list(data) + + @classmethod + def from_file(cls, path: str) -> "Edid": + with _smart_open(path, "rb") as file: + data = file.read() + if not data.startswith(b"\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00"): + text = re.sub(r"\s", "", data.decode()) + data = bytes([ + int(text[index:index + 2], 16) + for index in range(0, len(text), 2) + ]) + assert len(data) == 256, f"Invalid EDID length: {len(data)}, should be 256 bytes" + assert data[126] == 1, "Zero extensions number" + assert (data[_CEA + 0], [_CEA + 1]) == (0x02, 0x03), "Can't find CEA extension" + return Edid(data) + + def write_hex(self, path: str) -> None: + self.__update_checksums() + text = "\n".join( + "".join( + f"{item:0{2}X}" + for item in self.__data[index:index + 16] + ) + for index in range(0, len(self.__data), 16) + ) + "\n" + with _smart_open(path, "w") as file: + file.write(text) + + def write_bin(self, path: str) -> None: + self.__update_checksums() + with _smart_open(path, "wb") as file: + file.write(bytes(self.__data)) + + def __update_checksums(self) -> None: + self.__data[127] = 256 - (sum(self.__data[:127]) % 256) + self.__data[255] = 256 - (sum(self.__data[128:255]) % 256) + + # ===== + + def get_mfc_id(self) -> str: + raw = self.__data[8] << 8 | self.__data[9] + return bytes([ + ((raw >> 10) & 0b11111) + 0x40, + ((raw >> 5) & 0b11111) + 0x40, + (raw & 0b11111) + 0x40, + ]).decode("ascii") + + def set_mfc_id(self, mfc_id: str) -> None: + assert len(mfc_id) == 3, "Mfc ID must be 3 characters long" + data = mfc_id.upper().encode("ascii") + for ch in data: + assert 0x41 <= ch <= 0x5A, "Mfc ID must contain only A-Z characters" + raw = ( + (data[2] - 0x40) + | ((data[1] - 0x40) << 5) + | ((data[0] - 0x40) << 10) + ) + self.__data[8] = (raw >> 8) & 0xFF + self.__data[9] = raw & 0xFF + + # ===== + + def get_product_id(self) -> int: + return (self.__data[10] | self.__data[11] << 8) + + def set_product_id(self, product_id: int) -> None: + assert 0 <= product_id <= 0xFFFF, f"Product ID should be from 0 to {0xFFFF}" + self.__data[10] = product_id & 0xFF + self.__data[11] = (product_id >> 8) & 0xFF + + # ===== + + def get_serial(self) -> int: + return ( + self.__data[12] + | self.__data[13] << 8 + | self.__data[14] << 16 + | self.__data[15] << 24 + ) + + def set_serial(self, serial: int) -> None: + assert 0 <= serial <= 0xFFFFFFFF, f"Serial should be from 0 to {0xFFFFFFFF}" + self.__data[12] = serial & 0xFF + self.__data[13] = (serial >> 8) & 0xFF + self.__data[14] = (serial >> 16) & 0xFF + self.__data[15] = (serial >> 24) & 0xFF + + # ===== + + def get_monitor_name(self) -> str: + return self.__get_dtd_text(0xFC, "Monitor Name") + + def set_monitor_name(self, text: str) -> None: + self.__set_dtd_text(0xFC, "Monitor Name", text) + + def get_monitor_serial(self) -> str: + return self.__get_dtd_text(0xFF, "Monitor Serial") + + def set_monitor_serial(self, text: str) -> None: + self.__set_dtd_text(0xFF, "Monitor Serial", text) + + def __get_dtd_text(self, d_type: int, name: str) -> str: + index = self.__find_dtd_text(d_type, name) + return bytes(self.__data[index:index + 13]).decode("cp437").strip() + + def __set_dtd_text(self, d_type: int, name: str, text: str) -> None: + index = self.__find_dtd_text(d_type, name) + encoded = (text[:13] + "\n" + " " * 12)[:13].encode("cp437") + for (offset, ch) in enumerate(encoded): + self.__data[index + offset] = ch + + def __find_dtd_text(self, d_type: int, name: str) -> int: + for index in [54, 72, 90, 108]: + if self.__data[index + 3] == d_type: + return index + 5 + raise EdidNoBlockError(f"Can't find DTD {name}") + + # ===== CEA ===== + + def get_audio(self) -> bool: + (cbs, _) = self.__parse_cea() + audio = False + speakers = False + for cb in cbs: + if cb.tag == _CEA_AUDIO: + audio = True + elif cb.tag == _CEA_SPEAKERS: + speakers = True + return (audio and speakers and self.__get_basic_audio()) + + def set_audio(self, enabled: bool) -> None: + (cbs, dtds) = self.__parse_cea() + cbs = [cb for cb in cbs if cb.tag not in [_CEA_AUDIO, _CEA_SPEAKERS]] + if enabled: + cbs.append(_CeaBlock(_CEA_AUDIO, b"\x09\x7f\x07")) + cbs.append(_CeaBlock(_CEA_SPEAKERS, b"\x01\x00\x00")) + self.__replace_cea(cbs, dtds) + self.__set_basic_audio(enabled) + + def __get_basic_audio(self) -> bool: + return bool(self.__data[_CEA + 3] & 0b01000000) + + def __set_basic_audio(self, enabled: bool) -> None: + if enabled: + self.__data[_CEA + 3] |= 0b01000000 + else: + self.__data[_CEA + 3] &= (0xFF - 0b01000000) # ~X + + def __parse_cea(self) -> tuple[list[_CeaBlock], bytes]: + cea = self.__data[_CEA:] + dtd_begin = cea[2] + if dtd_begin == 0: + return ([], b"") + + cbs: list[_CeaBlock] = [] + if dtd_begin > 4: + raw = cea[4:dtd_begin] + while len(raw) != 0: + cb = _CeaBlock.first_from_raw(raw) + cbs.append(cb) + raw = raw[cb.size:] + + dtds = b"" + assert dtd_begin >= 4 + raw = cea[dtd_begin:] + while len(raw) > (18 + 1) and raw[0] != 0: + dtds += bytes(raw[:18]) + raw = raw[18:] + + return (cbs, dtds) + + def __replace_cea(self, cbs: list[_CeaBlock], dtds: bytes) -> None: + cbs_packed = b"" + for cb in cbs: + cbs_packed += cb.pack() + + raw = cbs_packed + dtds + assert len(raw) <= (128 - 4 - 1), "Too many CEA blocks or DTDs" + + self.__data[_CEA + 2] = (0 if len(raw) == 0 else (len(cbs_packed) + 4)) + + for index in range(4, 127): + try: + ch = raw[index - 4] + except IndexError: + ch = 0 + self.__data[_CEA + index] = ch From 1217144ecd476e28cfe43d7454a5224aa7fe7b7a Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sun, 22 Sep 2024 05:20:01 +0300 Subject: [PATCH 45/88] refactoring + some tools --- web/share/css/kvm/hid.css | 22 ----------- web/share/css/main.css | 74 +++++++++++++++++++++++++++++++++-- web/share/css/navbar.css | 24 ------------ web/share/css/x-desktop.css | 6 +-- web/share/js/tools.js | 19 ++++++++- web/share/js/wm.js | 78 +++++++++++++++++++++---------------- 6 files changed, 135 insertions(+), 88 deletions(-) diff --git a/web/share/css/kvm/hid.css b/web/share/css/kvm/hid.css index 5c710ac2..32a92e0f 100644 --- a/web/share/css/kvm/hid.css +++ b/web/share/css/kvm/hid.css @@ -24,28 +24,6 @@ div#text-menu { width: 340px; } -textarea#hid-pak-text { - display: block; - resize: none; - height: 120px; - width: 100%; - border: var(--border-default-thin); - border-radius: 4px; - color: var(--cs-code-default-fg); - background-color: var(--cs-code-default-bg); - -webkit-appearance:none; -} - -textarea#hid-pak-text::-moz-placeholder { - line-height: 60px; - text-align: center; -} - -textarea#hid-pak-text::-webkit-input-placeholder { - line-height: 60px; - text-align: center; -} - input#hid-recorder-new-script-file { display: none; } diff --git a/web/share/css/main.css b/web/share/css/main.css index 8e6af465..cd8b6342 100644 --- a/web/share/css/main.css +++ b/web/share/css/main.css @@ -123,6 +123,26 @@ select { display: block; width: 100%; padding-left: 5px; +} +select[size] { + height: auto; + padding: 5px; +} +select[size]::-webkit-scrollbar { + width: 8px; + height: 8px; +} +select[size]::-webkit-scrollbar-thumb { + border-radius: 4px; + background: var(--cs-scroll-default-bg); +} +@-moz-document url-prefix() { + select[size] { + scrollbar-width: 8px; + scrollbar-color: var(--cs-scroll-default-bg) var(--cs-code-default-bg); + } +} +select:not([size]) { padding-right: 25px; } button.small { @@ -149,22 +169,24 @@ select { -webkit-user-select: none; -moz-user-select: none; user-select: none; +} +select:not([size]) { background-image: url("../svg/select-arrow-normal.svg"); background-position: center right; background-repeat: no-repeat; } -select:disabled { +select:not([size]):disabled { background-image: url("../svg/select-arrow-inactive.svg") !important; } -select:active { +select:not([size]):active { color: var(--cs-control-intensive-fg) !important; background-image: url("../svg/select-arrow-intensive.svg") !important; } -select option { +select:not([size]) option { color: var(--cs-control-default-fg); background-color: var(--cs-control-default-bg); } -select option.comment { +select:not([size]) option.comment { color: var(--cs-control-disabled-fg); font-style: italic; } @@ -180,6 +202,26 @@ input[type=text], input[type=password] { height: 30px; } +textarea { + display: block; + resize: none; + height: 120px; + width: 100%; + border: var(--border-default-thin); + border-radius: 4px; + color: var(--cs-code-default-fg); + background-color: var(--cs-code-default-bg); + -webkit-appearance:none; +} +textarea::-moz-placeholder { + line-height: 60px; + text-align: center; +} +textarea::-webkit-input-placeholder { + line-height: 60px; + text-align: center; +} + div.buttons-row { margin: 0; padding: 0; @@ -218,6 +260,30 @@ div.buttons-row { border-bottom-right-radius: 0; } +table.kv { + border-spacing: 5px; + margin: 0 10px 0 10px; + font-size: 12px; +} +table.kv td { + text-align: left; +} +table.kv td.value { + font-weight: bold; + max-width: 310px; + overflow: hidden; +} +table.kv td.value-slider { + width: 100%; +} +table.kv td.value-number { + font-weight: bold; + max-width: 310px; + overflow: hidden; + min-width: 40px; + max-width: 40px; +} + ul.footer { list-style-type: none; bottom: 0; diff --git a/web/share/css/navbar.css b/web/share/css/navbar.css index 04d15e05..eb125f9a 100644 --- a/web/share/css/navbar.css +++ b/web/share/css/navbar.css @@ -167,30 +167,6 @@ ul#navbar li div.menu div.text { font-size: 14px; } -ul#navbar li div.menu table.kv { - border-spacing: 5px; - margin: 0 10px 0 10px; - font-size: 12px; -} -ul#navbar li div.menu table.kv td { - text-align: left; -} -ul#navbar li div.menu table.kv td.value { - font-weight: bold; - max-width: 310px; - overflow: hidden; -} -ul#navbar li div.menu table.kv td.value-slider { - width: 100%; -} -ul#navbar li div.menu table.kv td.value-number { - font-weight: bold; - max-width: 310px; - overflow: hidden; - min-width: 40px; - max-width: 40px; -} - ul#navbar li div.menu div.buttons button, ul#navbar li div.menu div.buttons select { border-radius: 0; diff --git a/web/share/css/x-desktop.css b/web/share/css/x-desktop.css index a652c61b..732f8aa2 100644 --- a/web/share/css/x-desktop.css +++ b/web/share/css/x-desktop.css @@ -23,7 +23,7 @@ /* ===== main.css ===== */ button:enabled:hover, -select:enabled:hover, +select:not([size]):enabled:hover, input[type=file]:enabled:hover::-webkit-file-selector-button, input[type=file]:enabled:hover::file-selector-button { color: var(--cs-control-hovered-fg); @@ -31,7 +31,7 @@ input[type=file]:enabled:hover::file-selector-button { } button:active, -select:active, +select:not([size]):active, input[type=file]:active::-webkit-file-selector-button, input[type=file]:active::file-selector-button { color: var(--cs-control-pressed-fg) !important; @@ -42,7 +42,7 @@ button.key:active, select.key:active { box-shadow: none; } -select:enabled:hover { +select:not([size]):enabled:hover { background-image: url("../svg/select-arrow-intensive.svg") !important; } diff --git a/web/share/js/tools.js b/web/share/js/tools.js index fbf712de..48f5b420 100644 --- a/web/share/js/tools.js +++ b/web/share/js/tools.js @@ -64,6 +64,17 @@ export var tools = new function() { /************************************************************************/ + self.escape = function(text) { + return text.replace( + /[^0-9A-Za-z ]/g, + ch => "&#" + ch.charCodeAt(0) + ";" + ); + }; + + self.makeClosure = function(func, ...args) { + return () => func(...args); + }; + self.upperFirst = function(text) { return text[0].toUpperCase() + text.slice(1); }; @@ -87,6 +98,10 @@ export var tools = new function() { return Math.floor(Math.random() * (max - min + 1)) + min; }; + self.formatHex = function(value) { + return `0x${value.toString(16).toUpperCase()}`; + }; + self.formatSize = function(size) { if (size > 0) { let index = Math.floor( Math.log(size) / Math.log(1024) ); @@ -283,9 +298,9 @@ export var tools = new function() { option.className = "comment"; el.add(option); }, - "addSeparator": function(el) { + "addSeparator": function(el, repeat=30) { if (!self.browser.is_mobile) { - self.selector.addComment(el, "\u2500".repeat(30)); + self.selector.addComment(el, "\u2500".repeat(repeat)); } }, diff --git a/web/share/js/wm.js b/web/share/js/wm.js index 6a26a648..41709dd2 100644 --- a/web/share/js/wm.js +++ b/web/share/js/wm.js @@ -147,7 +147,7 @@ function __WindowManager() { self.copyTextToClipboard = function(text) { let workaround = function(ex) { // https://stackoverflow.com/questions/60317969/document-execcommandcopy-not-working-even-though-the-dom-element-is-created - let callback = function() { + __modalDialog("Info", "Press OK to copy the text to the clipboard", true, false).then(function() { tools.error("copyTextToClipboard(): navigator.clipboard.writeText() is not working:", ex); tools.info("copyTextToClipboard(): Trying a workaround..."); @@ -175,8 +175,7 @@ function __WindowManager() { tools.error("copyTextToClipboard(): Workaround failed:", ex); wm.error("Can't copy text to the clipboard:
    ", ex); } - }; - __modalDialog("Info", "Press OK to copy the text to the clipboard", true, false, callback); + }); }; if (navigator.clipboard) { navigator.clipboard.writeText(text).then(function() { @@ -192,8 +191,9 @@ function __WindowManager() { self.info = (...args) => __modalDialog("Info", args.join(" "), true, false); self.error = (...args) => __modalDialog("Error", args.join(" "), true, false); self.confirm = (...args) => __modalDialog("Question", args.join(" "), true, true); + self.modal = (header, text, ok, cancel) => __modalDialog(header, text, ok, cancel); - var __modalDialog = function(header, text, ok, cancel, callback=null, parent=null) { + var __modalDialog = function(header, text, ok, cancel, parent=null) { let el_active_menu = (document.activeElement && document.activeElement.closest(".menu")); let el_modal = document.createElement("div"); @@ -212,22 +212,45 @@ function __WindowManager() { let el_content = document.createElement("div"); el_content.className = "modal-content"; - el_content.innerHTML = text; el_window.appendChild(el_content); + let el_buttons = document.createElement("div"); + el_buttons.classList.add("modal-buttons", "buttons-row"); + el_window.appendChild(el_buttons); + + let el_cancel_button = null; + let el_ok_button = null; + if (cancel) { + el_cancel_button = document.createElement("button"); + el_cancel_button.className = "row100"; + el_cancel_button.innerHTML = "Cancel"; + el_buttons.appendChild(el_cancel_button); + } + if (ok) { + el_ok_button = document.createElement("button"); + el_ok_button.className = "row100"; + el_ok_button.innerHTML = "OK"; + el_buttons.appendChild(el_ok_button); + } + if (ok && cancel) { + el_ok_button.className = "row50"; + el_cancel_button.className = "row50"; + } + + el_window.onkeyup = function(event) { + event.preventDefault(); + if (ok && event.code === "Enter") { + el_ok_button.click(); + } else if (cancel && event.code === "Escape") { + el_cancel_button.click(); + } + }; + let promise = null; if (ok || cancel) { promise = new Promise(function(resolve) { - let el_buttons = document.createElement("div"); - el_buttons.className = "modal-buttons"; - el_window.appendChild(el_buttons); - function close(retval) { - if (callback) { - callback(retval); - } __closeWindow(el_window); - el_modal.outerHTML = ""; let index = __windows.indexOf(el_modal); if (index !== -1) { __windows.splice(index, 1); @@ -238,38 +261,27 @@ function __WindowManager() { __activateLastWindow(el_modal); } resolve(retval); + // Так как resolve() асинхронный, надо выполнить в эвентлупе после него + setTimeout(function() { el_modal.outerHTML = ""; }, 0); } if (cancel) { - var el_cancel_button = document.createElement("button"); - el_cancel_button.innerHTML = "Cancel"; tools.el.setOnClick(el_cancel_button, () => close(false)); - el_buttons.appendChild(el_cancel_button); } if (ok) { - var el_ok_button = document.createElement("button"); - el_ok_button.innerHTML = "OK"; tools.el.setOnClick(el_ok_button, () => close(true)); - el_buttons.appendChild(el_ok_button); } - if (ok && cancel) { - el_ok_button.className = "row50"; - el_cancel_button.className = "row50"; - } - - el_window.onkeyup = function(event) { - event.preventDefault(); - if (ok && event.code === "Enter") { - el_ok_button.click(); - } else if (cancel && event.code === "Escape") { - el_cancel_button.click(); - } - }; }); } __windows.push(el_modal); (parent || document.fullscreenElement || document.body).appendChild(el_modal); + if (typeof text === "function") { + // Это должно быть здесь, потому что элемент должен иметь родителя чтобы существовать + text(el_content, el_ok_button); + } else { + el_content.innerHTML = text; + } __activateWindow(el_modal); return promise; @@ -625,7 +637,7 @@ function __WindowManager() { + "In Chrome use HTTPS and enable system-keyboard-lock
    " + "by putting at URL chrome://flags/#system-keyboard-lock" ); - __modalDialog("Keyboard lock is unsupported", msg, true, false, null, el_window); + __modalDialog("Keyboard lock is unsupported", msg, true, false, el_window); } }; From 5ed368769c889628ebae896556e5c6401cbafdaf Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sun, 22 Sep 2024 22:14:36 +0300 Subject: [PATCH 46/88] refactoring --- web/share/js/index/main.js | 4 ++-- web/share/js/ipmi/main.js | 2 +- web/share/js/kvm/atx.js | 2 +- web/share/js/kvm/gpio.js | 4 ++-- web/share/js/kvm/hid.js | 10 +++++----- web/share/js/kvm/msd.js | 10 +++++----- web/share/js/kvm/ocr.js | 16 +++++++++------- web/share/js/kvm/recorder.js | 12 +++++++----- web/share/js/kvm/session.js | 2 +- web/share/js/kvm/stream.js | 4 ++-- web/share/js/login/main.js | 2 +- web/share/js/tools.js | 16 +++++++++++----- web/share/js/vnc/main.js | 2 +- 13 files changed, 48 insertions(+), 38 deletions(-) diff --git a/web/share/js/index/main.js b/web/share/js/index/main.js index 3a0afb45..38c848c7 100644 --- a/web/share/js/index/main.js +++ b/web/share/js/index/main.js @@ -51,7 +51,7 @@ function __setAppText() { } function __loadKvmdInfo() { - tools.httpGet("/api/info?fields=auth,meta,extras", function(http) { + tools.httpGet("/api/info", {"fields": "auth,meta,extras"}, function(http) { if (http.status === 200) { let info = JSON.parse(http.responseText).result; @@ -121,7 +121,7 @@ function __makeApp(id, path, icon, name) { } function __logout() { - tools.httpPost("/api/auth/logout", function(http) { + tools.httpPost("/api/auth/logout", null, function(http) { if (http.status === 200 || http.status === 401 || http.status === 403) { document.location.href = "/login"; } else { diff --git a/web/share/js/ipmi/main.js b/web/share/js/ipmi/main.js index 237b4005..26bd1f38 100644 --- a/web/share/js/ipmi/main.js +++ b/web/share/js/ipmi/main.js @@ -31,7 +31,7 @@ export function main() { } function __loadKvmdInfo() { - tools.httpGet("/api/info", function(http) { + tools.httpGet("/api/info", null, function(http) { if (http.status === 200) { let ipmi_port = JSON.parse(http.responseText).result.extras.ipmi.port; let make_item = (comment, ipmi, api) => ` diff --git a/web/share/js/kvm/atx.js b/web/share/js/kvm/atx.js index 53c290cf..2a0e38f9 100644 --- a/web/share/js/kvm/atx.js +++ b/web/share/js/kvm/atx.js @@ -73,7 +73,7 @@ export function Atx(__recorder) { var __clickButton = function(button, confirm_msg) { let click_button = function() { - tools.httpPost(`/api/atx/click?button=${button}`, function(http) { + tools.httpPost("/api/atx/click", {"button": button}, function(http) { if (http.status === 409) { wm.error("Performing another ATX operation for other client.
    Please try again later"); } else if (http.status !== 200) { diff --git a/web/share/js/kvm/gpio.js b/web/share/js/kvm/gpio.js index 4bee901a..fe326205 100644 --- a/web/share/js/kvm/gpio.js +++ b/web/share/js/kvm/gpio.js @@ -181,7 +181,7 @@ export function Gpio(__recorder) { confirm = el.getAttribute("data-confirm-off"); } let act = () => { - __sendPost(`/api/gpio/switch?channel=${channel}&state=${to}`); + __sendPost("/api/gpio/switch", {"channel": channel, "state": to}); __recorder.recordGpioSwitchEvent(channel, to); }; if (confirm) { @@ -201,7 +201,7 @@ export function Gpio(__recorder) { let channel = el.getAttribute("data-channel"); let confirm = el.getAttribute("data-confirm"); let act = () => { - __sendPost(`/api/gpio/pulse?channel=${channel}`); + __sendPost("/api/gpio/pulse", {"channel": channel}); __recorder.recordGpioPulseEvent(channel); }; if (confirm) { diff --git a/web/share/js/kvm/hid.js b/web/share/js/kvm/hid.js index 6ca87748..ef16bcaf 100644 --- a/web/share/js/kvm/hid.js +++ b/web/share/js/kvm/hid.js @@ -253,7 +253,7 @@ export function Hid(__getGeometry, __recorder) { tools.debug(`HID: paste-as-keys ${keymap}: ${text}`); - tools.httpPost(`/api/hid/print?limit=0&keymap=${keymap}`, function(http) { + tools.httpPost("/api/hid/print", {"limit": 0, "keymap": keymap}, function(http) { tools.el.setEnabled($("hid-pak-text"), true); tools.el.setEnabled($("hid-pak-button"), true); tools.el.setEnabled($("hid-pak-keymap-selector"), true); @@ -286,7 +286,7 @@ export function Hid(__getGeometry, __recorder) { var __clickOutputsRadio = function(hid) { let output = tools.radio.getValue(`hid-outputs-${hid}-radio`); - tools.httpPost(`/api/hid/set_params?${hid}_output=${output}`, function(http) { + tools.httpPost("/api/hid/set_params", {[`${hid}_output`]: output}, function(http) { if (http.status !== 200) { wm.error("Can't configure HID:
    ", http.responseText); } @@ -295,7 +295,7 @@ export function Hid(__getGeometry, __recorder) { var __clickJigglerSwitch = function() { let enabled = $("hid-jiggler-switch").checked; - tools.httpPost(`/api/hid/set_params?jiggler=${enabled}`, function(http) { + tools.httpPost("/api/hid/set_params", {"jiggler": enabled}, function(http) { if (http.status !== 200) { wm.error(`Can't ${enabled ? "enabled" : "disable"} mouse juggler:
    `, http.responseText); } @@ -304,7 +304,7 @@ export function Hid(__getGeometry, __recorder) { var __clickConnectSwitch = function() { let connected = $("hid-connect-switch").checked; - tools.httpPost(`/api/hid/set_connected?connected=${connected}`, function(http) { + tools.httpPost("/api/hid/set_connected", {"connected": connected}, function(http) { if (http.status !== 200) { wm.error(`Can't ${connected ? "connect" : "disconnect"} HID:
    `, http.responseText); } @@ -314,7 +314,7 @@ export function Hid(__getGeometry, __recorder) { var __clickResetButton = function() { wm.confirm("Are you sure you want to reset HID (keyboard & mouse)?").then(function(ok) { if (ok) { - tools.httpPost("/api/hid/reset", function(http) { + tools.httpPost("/api/hid/reset", null, function(http) { if (http.status !== 200) { wm.error("HID reset error:
    ", http.responseText); } diff --git a/web/share/js/kvm/msd.js b/web/share/js/kvm/msd.js index 662c04e4..88491a23 100644 --- a/web/share/js/kvm/msd.js +++ b/web/share/js/kvm/msd.js @@ -86,9 +86,9 @@ export function Msd() { var __clickRemoveButton = function() { let name = $("msd-image-selector").value; - wm.confirm(`Are you sure you want to remove the image
    ${name} from PiKVM?`).then(function(ok) { + wm.confirm(`Are you sure you want to remove the image
    ${tools.escape(name)} from PiKVM?`).then(function(ok) { if (ok) { - tools.httpPost(`/api/msd/remove?image=${name}`, function(http) { + tools.httpPost("/api/msd/remove", {"image": name}, function(http) { if (http.status !== 200) { wm.error("Can't remove image:
    ", http.responseText); } @@ -98,7 +98,7 @@ export function Msd() { }; var __sendParam = function(name, value) { - tools.httpPost(`/api/msd/set_params?${name}=${encodeURIComponent(value)}`, function(http) { + tools.httpPost("/api/msd/set_params", {[name]: value}, function(http) { if (http.status !== 200) { wm.error("Can't configure MSD:
    ", http.responseText); } @@ -164,7 +164,7 @@ export function Msd() { }; var __clickConnectButton = function(connected) { - tools.httpPost(`/api/msd/set_connected?connected=${connected}`, function(http) { + tools.httpPost("/api/msd/set_connected", {"connected": connected}, function(http) { if (http.status !== 200) { wm.error("Switch error:
    ", http.responseText); } @@ -177,7 +177,7 @@ export function Msd() { var __clickResetButton = function() { wm.confirm("Are you sure you want to reset Mass Storage Drive?").then(function(ok) { if (ok) { - tools.httpPost("/api/msd/reset", function(http) { + tools.httpPost("/api/msd/reset", null, function(http) { if (http.status !== 200) { wm.error("MSD reset error:
    ", http.responseText); } diff --git a/web/share/js/kvm/ocr.js b/web/share/js/kvm/ocr.js index 94c9d563..13e1b887 100644 --- a/web/share/js/kvm/ocr.js +++ b/web/share/js/kvm/ocr.js @@ -161,13 +161,15 @@ export function Ocr(__getGeometry) { tools.el.setEnabled($("stream-ocr-button"), false); tools.el.setEnabled($("stream-ocr-lang-selector"), false); $("stream-ocr-led").className = "led-yellow-rotating-fast"; - - let lang = $("stream-ocr-lang-selector").value; - let url = `/api/streamer/snapshot?ocr=1&ocr_langs=${lang}`; - url += `&ocr_left=${__selection.left}&ocr_top=${__selection.top}`; - url += `&ocr_right=${__selection.right}&ocr_bottom=${__selection.bottom}`; - - tools.httpGet(url, function(http) { + let params = { + "ocr": 1, + "ocr_langs": $("stream-ocr-lang-selector").value, + "ocr_left": __selection.left, + "ocr_top": __selection.top, + "ocr_right": __selection.right, + "orc_bottom": __selection.bottom, + }; + tools.httpGet("/api/streamer/snapshot", params, function(http) { if (http.status === 200) { wm.copyTextToClipboard(http.responseText); } else { diff --git a/web/share/js/kvm/recorder.js b/web/share/js/kvm/recorder.js index 03ec39b5..7ba0b3cb 100644 --- a/web/share/js/kvm/recorder.js +++ b/web/share/js/kvm/recorder.js @@ -280,7 +280,7 @@ export function Recorder() { return; } else if (event.event_type === "print") { - tools.httpPost("/api/hid/print?limit=0", function(http) { + tools.httpPost("/api/hid/print", {"limit": 0}, function(http) { if (http.status === 413) { wm.error("Too many text for paste!"); __stopProcess(); @@ -294,7 +294,7 @@ export function Recorder() { return; } else if (event.event_type === "atx_button") { - tools.httpPost(`/api/atx/click?button=${event.event.button}`, function(http) { + tools.httpPost("/api/atx/click", {"button": event.event.button}, function(http) { if (http.status !== 200) { wm.error("ATX error:
    ", http.responseText); __stopProcess(); @@ -306,12 +306,14 @@ export function Recorder() { } else if (["gpio_switch", "gpio_pulse"].includes(event.event_type)) { let path = "/api/gpio"; + let params = {"channel": event.event.channel}; if (event.event_type === "gpio_switch") { - path += `/switch?channel=${event.event.channel}&state=${event.event.to}`; + path += "/switch"; + params["state"] = event.event.to; } else { // gpio_pulse - path += `/pulse?channel=${event.event.channel}`; + path += "/pulse"; } - tools.httpPost(path, function(http) { + tools.httpPost(path, params, function(http) { if (http.status !== 200) { wm.error("GPIO error:
    ", http.responseText); __stopProcess(); diff --git a/web/share/js/kvm/session.js b/web/share/js/kvm/session.js index 98070927..8100f50c 100644 --- a/web/share/js/kvm/session.js +++ b/web/share/js/kvm/session.js @@ -280,7 +280,7 @@ export function Session() { $("link-led").className = "led-yellow"; $("link-led").title = "Connecting..."; - tools.httpGet("/api/auth/check", function(http) { + tools.httpGet("/api/auth/check", null, function(http) { if (http.status === 200) { __ws = new WebSocket(`${tools.is_https ? "wss" : "ws"}://${location.host}/api/ws`); __ws.sendHidEvent = (event) => __sendHidEvent(__ws, event.event_type, event.event); diff --git a/web/share/js/kvm/stream.js b/web/share/js/kvm/stream.js index b811a657..1b93e7c4 100644 --- a/web/share/js/kvm/stream.js +++ b/web/share/js/kvm/stream.js @@ -295,7 +295,7 @@ export function Streamer() { wm.confirm("Are you sure you want to reset stream?").then(function (ok) { if (ok) { __resetStream(); - tools.httpPost("/api/streamer/reset", function(http) { + tools.httpPost("/api/streamer/reset", null, function(http) { if (http.status !== 200) { wm.error("Can't reset stream:
    ", http.responseText); } @@ -305,7 +305,7 @@ export function Streamer() { }; var __sendParam = function(name, value) { - tools.httpPost(`/api/streamer/set_params?${name}=${value}`, function(http) { + tools.httpPost("/api/streamer/set_params", {[name]: value}, function(http) { if (http.status !== 200) { wm.error("Can't configure stream:
    ", http.responseText); } diff --git a/web/share/js/login/main.js b/web/share/js/login/main.js index 4ffa276a..58e7c80d 100644 --- a/web/share/js/login/main.js +++ b/web/share/js/login/main.js @@ -51,7 +51,7 @@ function __login() { } else { let passwd = $("passwd-input").value + $("code-input").value; let body = `user=${encodeURIComponent(user)}&passwd=${encodeURIComponent(passwd)}`; - tools.httpPost("/api/auth/login", function(http) { + tools.httpPost("/api/auth/login", null, function(http) { if (http.status === 200) { document.location.href = "/"; } else if (http.status === 403) { diff --git a/web/share/js/tools.js b/web/share/js/tools.js index 48f5b420..ee3bc187 100644 --- a/web/share/js/tools.js +++ b/web/share/js/tools.js @@ -39,7 +39,13 @@ export var tools = new function() { /************************************************************************/ - self.httpRequest = function(method, url, callback, body=null, content_type=null, timeout=15000) { + self.httpRequest = function(method, url, params, callback, body=null, content_type=null, timeout=15000) { + if (params) { + params = new URLSearchParams(params); + if (params) { + url += "?" + params; + } + } let http = new XMLHttpRequest(); http.open(method, url, true); if (content_type) { @@ -54,12 +60,12 @@ export var tools = new function() { http.send(body); }; - self.httpGet = function(url, callback, body=null, content_type=null, timeout=15000) { - self.httpRequest("GET", url, callback, body, content_type, timeout); + self.httpGet = function(url, params, callback, body=null, content_type=null, timeout=15000) { + self.httpRequest("GET", url, params, callback, body, content_type, timeout); }; - self.httpPost = function(url, callback, body=null, content_type=null, timeout=15000) { - self.httpRequest("POST", url, callback, body, content_type, timeout); + self.httpPost = function(url, params, callback, body=null, content_type=null, timeout=15000) { + self.httpRequest("POST", url, params, callback, body, content_type, timeout); }; /************************************************************************/ diff --git a/web/share/js/vnc/main.js b/web/share/js/vnc/main.js index 965c7e9e..64294e3b 100644 --- a/web/share/js/vnc/main.js +++ b/web/share/js/vnc/main.js @@ -31,7 +31,7 @@ export function main() { } function __loadKvmdInfo() { - tools.httpGet("/api/info", function(http) { + tools.httpGet("/api/info", null, function(http) { if (http.status === 200) { let vnc_port = JSON.parse(http.responseText).result.extras.vnc.port; $("vnc-text").innerHTML = ` From 8209ee2eb0bd411c74d7d7dbf7b1b79e46dd79c0 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Mon, 23 Sep 2024 02:32:38 +0300 Subject: [PATCH 47/88] improved wm dialogs --- web/share/js/index/main.js | 6 +++--- web/share/js/kvm/atx.js | 27 ++++++++++----------------- web/share/js/kvm/gpio.js | 6 +++--- web/share/js/kvm/hid.js | 20 ++++++++++---------- web/share/js/kvm/msd.js | 27 +++++++++++++++------------ web/share/js/kvm/recorder.js | 8 ++++---- web/share/js/kvm/stream.js | 4 ++-- web/share/js/login/main.js | 2 +- web/share/js/wm.js | 36 +++++++++++++++++++++++------------- 9 files changed, 71 insertions(+), 65 deletions(-) diff --git a/web/share/js/index/main.js b/web/share/js/index/main.js index 38c848c7..19cba440 100644 --- a/web/share/js/index/main.js +++ b/web/share/js/index/main.js @@ -57,7 +57,7 @@ function __loadKvmdInfo() { let apps = []; if (info.extras === null) { - wm.error("Not all applications in the menu can be displayed
    due an error. See KVMD logs for details."); + wm.error("Not all applications in the menu can be displayed due an error.
    See KVMD logs for details."); } else { apps = Object.values(info.extras).sort(function(a, b) { if (a.place < b.place) { @@ -113,7 +113,7 @@ function __makeApp(id, path, icon, name) {
    - ${name} + ${tools.escape(name)}
    @@ -125,7 +125,7 @@ function __logout() { if (http.status === 200 || http.status === 401 || http.status === 403) { document.location.href = "/login"; } else { - wm.error("Logout error:
    ", http.responseText); + wm.error("Logout error", http.responseText); } }); } diff --git a/web/share/js/kvm/atx.js b/web/share/js/kvm/atx.js index 2a0e38f9..bb8b5543 100644 --- a/web/share/js/kvm/atx.js +++ b/web/share/js/kvm/atx.js @@ -38,19 +38,9 @@ export function Atx(__recorder) { tools.storage.bindSimpleSwitch($("atx-ask-switch"), "atx.ask", true); - for (let args of [ - ["atx-power-button", "power", "Are you sure you want to press the power button?"], - ["atx-power-button-long", "power_long", ` - Are you sure you want to long press the power button?
    - Warning! This could cause data loss on the server. - `], - ["atx-reset-button", "reset", ` - Are you sure you want to press the reset button?
    - Warning! This could case data loss on the server. - `], - ]) { - tools.el.setOnClick($(args[0]), () => __clickButton(args[1], args[2])); - } + tools.el.setOnClick($("atx-power-button"), () => __clickAtx("power")); + tools.el.setOnClick($("atx-power-button-long"), () => __clickAtx("power_long")); + tools.el.setOnClick($("atx-reset-button"), () => __clickAtx("reset")); }; /************************************************************************/ @@ -71,20 +61,23 @@ export function Atx(__recorder) { } }; - var __clickButton = function(button, confirm_msg) { + var __clickAtx = function(button) { let click_button = function() { tools.httpPost("/api/atx/click", {"button": button}, function(http) { if (http.status === 409) { - wm.error("Performing another ATX operation for other client.
    Please try again later"); + wm.error("Performing another ATX operation for other client.
    Please try again later."); } else if (http.status !== 200) { - wm.error("Click error:
    ", http.responseText); + wm.error("Click error", http.responseText); } }); __recorder.recordAtxButtonEvent(button); }; if ($("atx-ask-switch").checked) { - wm.confirm(confirm_msg).then(function(ok) { + wm.confirm(` + Are you sure you want to press the ${button} button?
    + Warning! This could case data loss on the server. + `).then(function(ok) { if (ok) { click_button(); } diff --git a/web/share/js/kvm/gpio.js b/web/share/js/kvm/gpio.js index fe326205..29e6da08 100644 --- a/web/share/js/kvm/gpio.js +++ b/web/share/js/kvm/gpio.js @@ -185,7 +185,7 @@ export function Gpio(__recorder) { __recorder.recordGpioSwitchEvent(channel, to); }; if (confirm) { - wm.confirm(confirm).then(function(ok) { + wm.confirm(tools.escape(confirm)).then(function(ok) { if (ok) { act(); } else { @@ -205,7 +205,7 @@ export function Gpio(__recorder) { __recorder.recordGpioPulseEvent(channel); }; if (confirm) { - wm.confirm(confirm).then(function(ok) { if (ok) act(); }); + wm.confirm(tools.escape(confirm)).then(function(ok) { if (ok) act(); }); } else { act(); } @@ -216,7 +216,7 @@ export function Gpio(__recorder) { if (http.status === 409) { wm.error("Performing another operation for this GPIO channel.
    Please try again later"); } else if (http.status !== 200) { - wm.error("GPIO error:
    ", http.responseText); + wm.error("GPIO error", http.responseText); } }); }; diff --git a/web/share/js/kvm/hid.js b/web/share/js/kvm/hid.js index ef16bcaf..68c19e37 100644 --- a/web/share/js/kvm/hid.js +++ b/web/share/js/kvm/hid.js @@ -98,8 +98,7 @@ export function Hid(__getGeometry, __recorder) { } let codes = el_shortcut.getAttribute("data-shortcut").split(" "); if (ask) { - let confirm_msg = `Do you want to press ${codes.join(" + ")}?`; - wm.confirm(confirm_msg).then(function(ok) { + wm.confirm("Do you want to press this hotkey?", codes.join(" + ")).then(function(ok) { if (ok) { __emitShortcut(codes); } @@ -261,7 +260,7 @@ export function Hid(__getGeometry, __recorder) { if (http.status === 413) { wm.error("Too many text for paste!"); } else if (http.status !== 200) { - wm.error("HID paste error:
    ", http.responseText); + wm.error("HID paste error", http.responseText); } else if (http.status === 200) { __recorder.recordPrintEvent(text); } @@ -269,9 +268,10 @@ export function Hid(__getGeometry, __recorder) { }; if ($("hid-pak-ask-switch").checked) { - let confirm_msg = `You're going to paste ${text.length} character${text.length ? "s" : ""}.
    `; - confirm_msg += "Are you sure you want to continue?"; - wm.confirm(confirm_msg).then(function(ok) { + wm.confirm(` + You're going to paste ${text.length} character${text.length ? "s" : ""}.
    + Are you sure you want to continue? + `).then(function(ok) { if (ok) { paste_as_keys(); } else { @@ -288,7 +288,7 @@ export function Hid(__getGeometry, __recorder) { let output = tools.radio.getValue(`hid-outputs-${hid}-radio`); tools.httpPost("/api/hid/set_params", {[`${hid}_output`]: output}, function(http) { if (http.status !== 200) { - wm.error("Can't configure HID:
    ", http.responseText); + wm.error("Can't configure HID", http.responseText); } }); }; @@ -297,7 +297,7 @@ export function Hid(__getGeometry, __recorder) { let enabled = $("hid-jiggler-switch").checked; tools.httpPost("/api/hid/set_params", {"jiggler": enabled}, function(http) { if (http.status !== 200) { - wm.error(`Can't ${enabled ? "enabled" : "disable"} mouse juggler:
    `, http.responseText); + wm.error(`Can't ${enabled ? "enabled" : "disable"} mouse jiggler`, http.responseText); } }); }; @@ -306,7 +306,7 @@ export function Hid(__getGeometry, __recorder) { let connected = $("hid-connect-switch").checked; tools.httpPost("/api/hid/set_connected", {"connected": connected}, function(http) { if (http.status !== 200) { - wm.error(`Can't ${connected ? "connect" : "disconnect"} HID:
    `, http.responseText); + wm.error(`Can't ${connected ? "connect" : "disconnect"} HID`, http.responseText); } }); }; @@ -316,7 +316,7 @@ export function Hid(__getGeometry, __recorder) { if (ok) { tools.httpPost("/api/hid/reset", null, function(http) { if (http.status !== 200) { - wm.error("HID reset error:
    ", http.responseText); + wm.error("HID reset error", http.responseText); } }); } diff --git a/web/share/js/kvm/msd.js b/web/share/js/kvm/msd.js index 88491a23..3885bd50 100644 --- a/web/share/js/kvm/msd.js +++ b/web/share/js/kvm/msd.js @@ -86,11 +86,11 @@ export function Msd() { var __clickRemoveButton = function() { let name = $("msd-image-selector").value; - wm.confirm(`Are you sure you want to remove the image
    ${tools.escape(name)} from PiKVM?`).then(function(ok) { + wm.confirm("Are you sure you want to remove this image?", name).then(function(ok) { if (ok) { tools.httpPost("/api/msd/remove", {"image": name}, function(http) { if (http.status !== 200) { - wm.error("Can't remove image:
    ", http.responseText); + wm.error("Can't remove image", http.responseText); } }); } @@ -100,7 +100,7 @@ export function Msd() { var __sendParam = function(name, value) { tools.httpPost("/api/msd/set_params", {[name]: value}, function(http) { if (http.status !== 200) { - wm.error("Can't configure MSD:
    ", http.responseText); + wm.error("Can't configure Mass Storage", http.responseText); } }); }; @@ -124,8 +124,9 @@ export function Msd() { var __httpStateChange = function() { if (__http.readyState === 4) { if (__http.status !== 200) { - wm.error("Can't upload image to the Mass Storage Drive:
    ", __http.responseText); + wm.error("Can't upload image", __http.responseText); } else if ($("msd-new-url").value.length > 0) { + let html = ""; let msg = ""; try { let end = __http.responseText.lastIndexOf("\r\n"); @@ -139,13 +140,15 @@ export function Msd() { let result_str = __http.responseText.slice(begin, end); let result = JSON.parse(result_str); if (!result.ok) { - msg = `Can't upload image to the Mass Storage Drive:
    ${result_str}`; + html = "Can't upload image"; + msg = result_str; } } catch (ex) { - msg = `Can't parse upload result:
    ${ex}`; + html = "Can't parse upload result"; + msg = `${ex}`; } - if (msg.length > 0) { - wm.error(msg); + if (html.length > 0) { + wm.error(html, msg); } } tools.hidden.setVisible($("msd-new-sub"), false); @@ -166,7 +169,7 @@ export function Msd() { var __clickConnectButton = function(connected) { tools.httpPost("/api/msd/set_connected", {"connected": connected}, function(http) { if (http.status !== 200) { - wm.error("Switch error:
    ", http.responseText); + wm.error("Can't switch Mass Storage", http.responseText); } __applyState(); }); @@ -175,11 +178,11 @@ export function Msd() { }; var __clickResetButton = function() { - wm.confirm("Are you sure you want to reset Mass Storage Drive?").then(function(ok) { + wm.confirm("Are you sure you want to reset Mass Storage?").then(function(ok) { if (ok) { tools.httpPost("/api/msd/reset", null, function(http) { if (http.status !== 200) { - wm.error("MSD reset error:
    ", http.responseText); + wm.error("Mass Storage reset error", http.responseText); } __applyState(); }); @@ -206,7 +209,7 @@ export function Msd() { $("msd-new-url").value = ""; let part = __state.storage.parts[$("msd-new-part-selector").value]; if (file.size > part.size) { - wm.error("New image is too big for the MSD partition.
    Maximum:", tools.formatSize(part.size)); + wm.error(`New image is too big for the Mass Storage partition.
    Maximum: ${tools.formatSize(part.size)}`); el_input.value = ""; } } diff --git a/web/share/js/kvm/recorder.js b/web/share/js/kvm/recorder.js index 7ba0b3cb..f32b5a2e 100644 --- a/web/share/js/kvm/recorder.js +++ b/web/share/js/kvm/recorder.js @@ -215,7 +215,7 @@ export function Recorder() { __events = events; __events_time = events_time; } catch (ex) { - wm.error(`Invalid script: ${ex}`); + wm.error("Invalid script", `${ex}`); } el_input.value = ""; @@ -285,7 +285,7 @@ export function Recorder() { wm.error("Too many text for paste!"); __stopProcess(); } else if (http.status !== 200) { - wm.error("HID paste error:
    ", http.responseText); + wm.error("HID paste error", http.responseText); __stopProcess(); } else if (http.status === 200) { __play_timer = setTimeout(() => __runEvents(index + 1, time), 0); @@ -296,7 +296,7 @@ export function Recorder() { } else if (event.event_type === "atx_button") { tools.httpPost("/api/atx/click", {"button": event.event.button}, function(http) { if (http.status !== 200) { - wm.error("ATX error:
    ", http.responseText); + wm.error("ATX error", http.responseText); __stopProcess(); } else if (http.status === 200) { __play_timer = setTimeout(() => __runEvents(index + 1, time), 0); @@ -315,7 +315,7 @@ export function Recorder() { } tools.httpPost(path, params, function(http) { if (http.status !== 200) { - wm.error("GPIO error:
    ", http.responseText); + wm.error("GPIO error", http.responseText); __stopProcess(); } else if (http.status === 200) { __play_timer = setTimeout(() => __runEvents(index + 1, time), 0); diff --git a/web/share/js/kvm/stream.js b/web/share/js/kvm/stream.js index 1b93e7c4..f96f02d2 100644 --- a/web/share/js/kvm/stream.js +++ b/web/share/js/kvm/stream.js @@ -297,7 +297,7 @@ export function Streamer() { __resetStream(); tools.httpPost("/api/streamer/reset", null, function(http) { if (http.status !== 200) { - wm.error("Can't reset stream:
    ", http.responseText); + wm.error("Can't reset stream", http.responseText); } }); } @@ -307,7 +307,7 @@ export function Streamer() { var __sendParam = function(name, value) { tools.httpPost("/api/streamer/set_params", {[name]: value}, function(http) { if (http.status !== 200) { - wm.error("Can't configure stream:
    ", http.responseText); + wm.error("Can't configure stream", http.responseText); } }); }; diff --git a/web/share/js/login/main.js b/web/share/js/login/main.js index 58e7c80d..c779a6d9 100644 --- a/web/share/js/login/main.js +++ b/web/share/js/login/main.js @@ -64,7 +64,7 @@ function __login() { if (error === "ValidatorError") { wm.error("Invalid characters in credentials").then(__tryAgain); } else { - wm.error("Login error:
    ", http.responseText).then(__tryAgain); + wm.error("Login error", http.responseText).then(__tryAgain); } } }, body, "application/x-www-form-urlencoded"); diff --git a/web/share/js/wm.js b/web/share/js/wm.js index 41709dd2..1c1b67df 100644 --- a/web/share/js/wm.js +++ b/web/share/js/wm.js @@ -173,13 +173,13 @@ function __WindowManager() { if (ex) { tools.error("copyTextToClipboard(): Workaround failed:", ex); - wm.error("Can't copy text to the clipboard:
    ", ex); + self.error("Can't copy text to the clipboard", `${ex}`); } }); }; if (navigator.clipboard) { navigator.clipboard.writeText(text).then(function() { - wm.info("The text has been copied to the clipboard"); + self.info("The text has been copied to the clipboard"); }, function(ex) { workaround(ex); }); @@ -188,12 +188,22 @@ function __WindowManager() { } }; - self.info = (...args) => __modalDialog("Info", args.join(" "), true, false); - self.error = (...args) => __modalDialog("Error", args.join(" "), true, false); - self.confirm = (...args) => __modalDialog("Question", args.join(" "), true, true); - self.modal = (header, text, ok, cancel) => __modalDialog(header, text, ok, cancel); + self.info = (html, ...args) => __modalCodeDialog("Info", html, args.join("\n"), true, false); + self.error = (html, ...args) => __modalCodeDialog("Error", html, args.join("\n"), true, false); + self.confirm = (html, ...args) => __modalCodeDialog("Question", html, args.join("\n"), true, true); + self.modal = (header, html, ok, cancel) => __modalDialog(header, html, ok, cancel); - var __modalDialog = function(header, text, ok, cancel, parent=null) { + var __modalCodeDialog = function(header, html, code, ok, cancel) { + let create_content = function(el_content) { + if (code) { + html += `

    ${tools.escape(code)}
    `; + } + el_content.innerHTML = html; + }; + return __modalDialog(header, create_content, ok, cancel); + }; + + var __modalDialog = function(header, html, ok, cancel, parent=null) { let el_active_menu = (document.activeElement && document.activeElement.closest(".menu")); let el_modal = document.createElement("div"); @@ -207,7 +217,7 @@ function __WindowManager() { let el_header = document.createElement("div"); el_header.className = "modal-header"; - el_header.innerHTML = header; + el_header.innerText = header; el_window.appendChild(el_header); let el_content = document.createElement("div"); @@ -223,13 +233,13 @@ function __WindowManager() { if (cancel) { el_cancel_button = document.createElement("button"); el_cancel_button.className = "row100"; - el_cancel_button.innerHTML = "Cancel"; + el_cancel_button.innerText = "Cancel"; el_buttons.appendChild(el_cancel_button); } if (ok) { el_ok_button = document.createElement("button"); el_ok_button.className = "row100"; - el_ok_button.innerHTML = "OK"; + el_ok_button.innerText = "OK"; el_buttons.appendChild(el_ok_button); } if (ok && cancel) { @@ -276,11 +286,11 @@ function __WindowManager() { __windows.push(el_modal); (parent || document.fullscreenElement || document.body).appendChild(el_modal); - if (typeof text === "function") { + if (typeof html === "function") { // Это должно быть здесь, потому что элемент должен иметь родителя чтобы существовать - text(el_content, el_ok_button); + html(el_content, el_ok_button); } else { - el_content.innerHTML = text; + el_content.innerHTML = html; } __activateWindow(el_modal); From 4e1d9815cdf79f8bc5ad1d9c9f4057136fd6238e Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Wed, 2 Oct 2024 01:05:55 +0300 Subject: [PATCH 48/88] pikvm/pikvm#1407: Save keymap on macro recording --- web/share/js/kvm/hid.js | 2 +- web/share/js/kvm/recorder.js | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/web/share/js/kvm/hid.js b/web/share/js/kvm/hid.js index 68c19e37..e95d92c2 100644 --- a/web/share/js/kvm/hid.js +++ b/web/share/js/kvm/hid.js @@ -262,7 +262,7 @@ export function Hid(__getGeometry, __recorder) { } else if (http.status !== 200) { wm.error("HID paste error", http.responseText); } else if (http.status === 200) { - __recorder.recordPrintEvent(text); + __recorder.recordPrintEvent(text, keymap); } }, text, "text/plain"); }; diff --git a/web/share/js/kvm/recorder.js b/web/share/js/kvm/recorder.js index f32b5a2e..822aa972 100644 --- a/web/share/js/kvm/recorder.js +++ b/web/share/js/kvm/recorder.js @@ -67,8 +67,8 @@ export function Recorder() { __recordEvent(event); }; - self.recordPrintEvent = function(text) { - __recordEvent({"event_type": "print", "event": {"text": text}}); + self.recordPrintEvent = function(text, keymap) { + __recordEvent({"event_type": "print", "event": {"text": text, "keymap": keymap}}); }; self.recordAtxButtonEvent = function(button) { @@ -159,6 +159,9 @@ export function Recorder() { } else if (event.event_type === "print") { __checkType(event.event.text, "string", "Non-string print text"); + if (event.event.keymap) { + __checkType(event.event.keymap, "string", "Non-string keymap"); + } } else if (event.event_type === "key") { __checkType(event.event.key, "string", "Non-string key code"); @@ -280,7 +283,11 @@ export function Recorder() { return; } else if (event.event_type === "print") { - tools.httpPost("/api/hid/print", {"limit": 0}, function(http) { + let params = {"limit": 0}; + if (event.event.keymap) { + params["keymap"] = event.event.keymap; + } + tools.httpPost("/api/hid/print", params, function(http) { if (http.status === 413) { wm.error("Too many text for paste!"); __stopProcess(); From f4ba4210e1ea1ad93dadd9eaf6eb1379d313b3a3 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Wed, 2 Oct 2024 03:32:54 +0300 Subject: [PATCH 49/88] fixed post params --- web/share/js/kvm/gpio.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/share/js/kvm/gpio.js b/web/share/js/kvm/gpio.js index 29e6da08..167eae92 100644 --- a/web/share/js/kvm/gpio.js +++ b/web/share/js/kvm/gpio.js @@ -211,8 +211,8 @@ export function Gpio(__recorder) { } }; - var __sendPost = function(url) { - tools.httpPost(url, function(http) { + var __sendPost = function(url, params) { + tools.httpPost(url, params, function(http) { if (http.status === 409) { wm.error("Performing another operation for this GPIO channel.
    Please try again later"); } else if (http.status !== 200) { From 8ce27dca3fc914c96c2234036a3f81f6c6f05543 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Wed, 2 Oct 2024 03:35:57 +0300 Subject: [PATCH 50/88] pikvm/pikvm#1405: Fixed behaviour on duplicating gpio leds --- web/share/js/kvm/gpio.js | 65 +++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/web/share/js/kvm/gpio.js b/web/share/js/kvm/gpio.js index 167eae92..293561b3 100644 --- a/web/share/js/kvm/gpio.js +++ b/web/share/js/kvm/gpio.js @@ -23,7 +23,7 @@ "use strict"; -import {tools, $, $$$} from "../tools.js"; +import {tools, $, $$} from "../tools.js"; import {wm} from "../wm.js"; @@ -39,29 +39,26 @@ export function Gpio(__recorder) { self.setState = function(state) { if (state) { for (let channel in state.inputs) { - let el = $(`gpio-led-${channel}`); - if (el) { + for (let el of $$(`gpio-led-${channel}`)) { __setLedState(el, state.inputs[channel].state); } } for (let channel in state.outputs) { for (let type of ["switch", "button"]) { - let el = $(`gpio-${type}-${channel}`); - if (el) { + for (let el of $$(`gpio-${type}-${channel}`)) { tools.el.setEnabled(el, state.outputs[channel].online && !state.outputs[channel].busy); } } - let el = $(`gpio-switch-${channel}`); - if (el) { + for (let el of $$(`gpio-switch-${channel}`)) { el.checked = state.outputs[channel].state; } } } else { - for (let el of $$$(".gpio-led")) { + for (let el of $$("gpio-led")) { __setLedState(el, false); } - for (let selector of [".gpio-switch", ".gpio-button"]) { - for (let el of $$$(selector)) { + for (let selector of ["gpio-switch", "gpio-button"]) { + for (let el of $$(selector)) { tools.el.setEnabled(el, false); } } @@ -103,13 +100,11 @@ export function Gpio(__recorder) { $("gpio-menu").innerHTML = content; for (let channel in model.scheme.outputs) { - let el = $(`gpio-switch-${channel}`); - if (el) { - tools.el.setOnClick(el, __createAction(el, __switchChannel)); + for (let el of $$(`gpio-switch-${channel}`)) { + tools.el.setOnClick(el, tools.makeClosure(__switchChannel, el)); } - el = $(`gpio-button-${channel}`); - if (el) { - tools.el.setOnClick(el, __createAction(el, __pulseChannel)); + for (let el of $$(`gpio-button-${channel}`)) { + tools.el.setOnClick(el, tools.makeClosure(__pulseChannel, el)); } } @@ -120,27 +115,33 @@ export function Gpio(__recorder) { self.setState(__state); }; - var __createAction = function(el, action) { - return () => action(el); - }; - var __createItem = function(item) { if (item.type === "label") { return item.text; } else if (item.type === "input") { return ` - + `; } else if (item.type === "output") { let controls = []; let confirm = (item.confirm ? "Are you sure you want to perform this action?" : ""); if (item.scheme["switch"]) { + let id = tools.makeId(); controls.push(`
    - -
    +
    diff --git a/web/kvm/navbar-msd.pug b/web/kvm/navbar-msd.pug index 397105df..6f5110fa 100644 --- a/web/kvm/navbar-msd.pug +++ b/web/kvm/navbar-msd.pug @@ -72,14 +72,6 @@ li(id="msd-dropdown" class="right feature-disabled") tr(id="msd-new-part" class="hidden") td Upload partition: td(width="100%") #[select(id="msd-new-part-selector")] - hr - table(class="kv") - tr - td(class="value") Note: - td • Don't close the browser page until the upload is complete. - tr - td - td • To speed up the upload, close the stream window. div(id="msd-uploading-sub" class="hidden") hr table(class="kv") @@ -92,6 +84,15 @@ li(id="msd-dropdown" class="right feature-disabled") div(class="text") div(id="msd-uploading-progress" class="progress") span(id="msd-uploading-progress-value" class="progress-value") + div(id="msd-new-tips" class="hidden") + hr + table(class="kv") + tr + td(class="value") Note: + td • Don't close the browser page until the upload is complete. + tr + td + td • To speed up the upload, close the stream window. hr div(class="buttons buttons-row") button(disabled id="msd-connect-button" class="row50") Connect drive to Server diff --git a/web/share/js/kvm/msd.js b/web/share/js/kvm/msd.js index 3885bd50..3a558b00 100644 --- a/web/share/js/kvm/msd.js +++ b/web/share/js/kvm/msd.js @@ -35,10 +35,6 @@ export function Msd() { var __state = null; var __http = null; - var __parts_names_json = ""; - var __parts_names_len = 0; - var __parts = {}; - var __init__ = function() { $("msd-led").title = "Unknown state"; @@ -68,8 +64,206 @@ export function Msd() { /************************************************************************/ self.setState = function(state) { - __state = state; - __applyState(); + if (state) { + if (!__state) { + __state = {}; + __state.storage = {}; + } + if (state.enabled !== undefined) { + tools.feature.setEnabled($("msd-dropdown"), state.enabled); + __state.enabled = state.enabled; + } + if (__state.enabled !== undefined) { + if (state.online !== undefined) { + __state.online = state.online; + } + if (state.busy !== undefined) { + __state.busy = state.busy; + } + if (state.drive !== undefined || (state.storage && state.storage.images !== undefined)) { + let drive = (state.drive !== undefined ? state.drive : __state.drive); + let images = ( + state.storage && state.storage.images !== undefined + ? state.storage.images + : __state.storage && __state.storage.images !== undefined + ? __state.storage.images + : null + ); + if (drive && images) { + __updateImageSelector(drive, images); + } + __state.drive = drive; + __state.storage.images = images; + } + if (state.storage && state.storage.parts !== undefined) { + __updateParts(state.storage.parts); + __state.storage.parts = state.storage.parts; + } + if (state.storage && state.storage.uploading !== undefined) { + __updateUploading(state.storage.uploading); + __state.storage.uploading = state.storage.uploading; + } + if (state.storage && state.storage.downloading !== undefined) { + __state.storage.downloading = state.storage.downloading; + } + } + } else { + __state = null; + } + __refreshControls(); + }; + + var __refreshControls = function() { + __updateControls(__state && (__state.online !== undefined) ? __state : null); + }; + + var __updateControls = function(state) { + let o = (state && state.online); + let d = (state ? state.drive : null); + let s = (state ? state.storage : null); + let busy = !!(state && state.busy); + + tools.hidden.setVisible($("msd-message-offline"), (state && !state.online)); + tools.hidden.setVisible($("msd-message-image-broken"), (o && d.image && !d.image.complete && !s.uploading)); + tools.hidden.setVisible($("msd-message-too-big-for-cdrom"), (o && d.cdrom && d.image && d.image.size >= 2359296000)); + tools.hidden.setVisible($("msd-message-out-of-storage"), (o && d.image && !d.image.in_storage)); + tools.hidden.setVisible($("msd-message-rw-enabled"), (o && d.rw)); + tools.hidden.setVisible($("msd-message-another-user-uploads"), (o && s.uploading && !__http)); + tools.hidden.setVisible($("msd-message-downloads"), (o && s.downloading)); + + tools.el.setEnabled($("msd-image-selector"), (o && !d.connected && !busy)); + tools.el.setEnabled($("msd-download-button"), (o && d.image && !d.connected && !busy)); + tools.el.setEnabled($("msd-remove-button"), (o && d.image && d.image.removable && !d.connected && !busy)); + + tools.radio.setEnabled("msd-mode-radio", (o && !d.connected && !busy)); + tools.radio.setValue("msd-mode-radio", `${Number(o && d.cdrom)}`); + + tools.el.setEnabled($("msd-rw-switch"), (o && !d.connected && !busy)); + $("msd-rw-switch").checked = (o && d.rw); + + tools.el.setEnabled($("msd-connect-button"), (o && d.image && !d.connected && !busy)); + tools.el.setEnabled($("msd-disconnect-button"), (o && d.connected && !busy)); + + tools.el.setEnabled($("msd-select-new-button"), (o && !d.connected && !__http && !busy)); + tools.el.setEnabled($("msd-upload-new-button"), + (o && !d.connected && (tools.input.getFile($("msd-new-file")) || $("msd-new-url").value.length > 0) && !busy)); + tools.el.setEnabled($("msd-abort-new-button"), (o && __http)); + + tools.el.setEnabled($("msd-reset-button"), (state && state.enabled && !busy)); + + tools.el.setEnabled($("msd-new-file"), (o && !d.connected && !__http && !busy)); + tools.el.setEnabled($("msd-new-url"), (o && !d.connected && !__http && !busy)); + tools.el.setEnabled($("msd-new-part-selector"), (o && !d.connected && !__http && !busy)); + + if (o && s.uploading) { + tools.hidden.setVisible($("msd-new-sub"), false); + $("msd-new-file").value = ""; + $("msd-new-url").value = ""; + } + tools.hidden.setVisible($("msd-uploading-sub"), (o && s.uploading)); + tools.hidden.setVisible($("msd-new-tips"), (o && s.uploading && __http)); + + let led_cls = "led-gray"; + let msg = "Unavailable"; + if (o && d.connected) { + led_cls = "led-green"; + msg = "Connected to Server"; + } else if (o && s.uploading) { + led_cls = "led-yellow-rotating-fast"; + msg = "Uploading new image"; + } else if (o && s.downloading) { + led_cls = "led-yellow-rotating-fast"; + msg = "Serving the image to download"; + } else if (o) { // Sic! + msg = "Disconnected"; + } + $("msd-led").className = led_cls; + $("msd-status").innerText = $("msd-led").title = msg; + }; + + var __updateUploading = function(uploading) { + $("msd-uploading-name").innerText = (uploading ? uploading.name : ""); + $("msd-uploading-size").innerText = (uploading ? tools.formatSize(uploading.size) : ""); + if (uploading) { + tools.progress.setPercentOf($("msd-uploading-progress"), uploading.size, uploading.written); + } + }; + + var __updateParts = function(parts) { + let names = Object.keys(parts).sort(); + { + let writable = names.filter(name => (name === "" || parts[name].writable)); + let writable_json = JSON.stringify(writable); + let el = $("msd-new-part-selector"); + if (el.__writable_json !== writable_json) { + let sel = (el.value || ""); + el.options.length = 0; + for (let name of writable) { + let title = (name || "\u2500 Internal \u2500"); + tools.selector.addOption(el, title, name, (name === sel)); + } + tools.hidden.setVisible($("msd-new-part"), (writable.length > 1)); + el.__writable_json = writable_json; + } + } + { + let names_json = JSON.stringify(names); + let el = $("msd-storages"); + if (el.__names_json !== names_json) { + el.innerHTML = names.map(name => ` +
    +
    + +
    +
    + `).join("
    "); + el.__names_json = names_json; + } + } + for (let name of names) { + let part = parts[name]; + let title = ( + name === "" + ? `${names.length === 1 ? "Storage: %s" : "Internal storage: %s"}` // eslint-disable-line + : `Storage [${name}${part.writable ? "]" : ", read-only]"}: %s` // eslint-disable-line + ); + let id = `__msd-storage-${tools.makeIdByText(name)}-progress`; + tools.progress.setSizeOf($(id), title, part.size, part.free); + } + }; + + var __updateImageSelector = function(drive, images) { + let sel = ""; + let el = $("msd-image-selector"); + el.options.length = 1; + for (let name of Object.keys(images).sort()) { + tools.selector.addSeparator(el); + tools.selector.addOption(el, name, name); + tools.selector.addComment(el, __makeImageSelectorInfo(images[name])); + if (drive.image && drive.image.name === name && drive.image.in_storage) { + sel = name; + } + } + if (drive.image && !drive.image.in_storage) { + sel = ".__external__"; // Just some magic name + tools.selector.addOption(el, drive.image.name, sel); + tools.selector.addComment(el, __makeImageSelectorInfo(drive.image)); + } + el.value = sel; + }; + + var __makeImageSelectorInfo = function(image) { + let text = `\xA0\xA0\xA0\xA0\xA0\u2570 ${tools.formatSize(image.size)}`; + if (!image.complete) { + text += ", broken"; + } + if (image.in_storage !== undefined && !image.in_storage) { + text += ", out of storage"; + } + let ts = new Date(image.mod_ts * 1000); + ts = new Date(ts.getTime() - (ts.getTimezoneOffset() * 60000)); + ts = ts.toISOString().slice(0, -8).replaceAll("-", ".").replace("T", "-"); + return `${text} \u2500 ${ts}`; }; var __selectImage = function() { @@ -80,8 +274,8 @@ export function Msd() { }; var __clickDownloadButton = function() { - let name = $("msd-image-selector").value; - window.open(`/api/msd/read?image=${name}`); + let image = encodeURIComponent($("msd-image-selector").value); + window.open(`/api/msd/read?image=${image}`); }; var __clickRemoveButton = function() { @@ -102,6 +296,7 @@ export function Msd() { if (http.status !== 200) { wm.error("Can't configure Mass Storage", http.responseText); } + __refreshControls(); }); }; @@ -110,60 +305,63 @@ export function Msd() { __http = new XMLHttpRequest(); let prefix = encodeURIComponent($("msd-new-part-selector").value); if (file) { - __http.open("POST", `/api/msd/write?prefix=${prefix}&image=${encodeURIComponent(file.name)}&remove_incomplete=1`, true); + let image = encodeURIComponent(file.name); + __http.open("POST", `/api/msd/write?prefix=${prefix}&image=${image}&remove_incomplete=1`, true); } else { - let url = $("msd-new-url").value; - __http.open("POST", `/api/msd/write_remote?prefix=${prefix}&url=${encodeURIComponent(url)}&remove_incomplete=1`, true); + let url = encodeURIComponent($("msd-new-url").value); + __http.open("POST", `/api/msd/write_remote?prefix=${prefix}&url=${url}&remove_incomplete=1`, true); } __http.upload.timeout = 7 * 24 * 3600; - __http.onreadystatechange = __httpStateChange; + __http.onreadystatechange = __uploadStateChange; __http.send(file); - __applyState(); + __refreshControls(); }; - var __httpStateChange = function() { - if (__http.readyState === 4) { - if (__http.status !== 200) { - wm.error("Can't upload image", __http.responseText); - } else if ($("msd-new-url").value.length > 0) { - let html = ""; - let msg = ""; - try { - let end = __http.responseText.lastIndexOf("\r\n"); - if (end < 0) { - end = __http.responseText.length; - } - let begin = __http.responseText.lastIndexOf("\r\n", end - 2); - if (begin < 0) { - end = 0; - } - let result_str = __http.responseText.slice(begin, end); - let result = JSON.parse(result_str); - if (!result.ok) { - html = "Can't upload image"; - msg = result_str; - } - } catch (ex) { - html = "Can't parse upload result"; - msg = `${ex}`; - } - if (html.length > 0) { - wm.error(html, msg); - } - } - tools.hidden.setVisible($("msd-new-sub"), false); - $("msd-new-file").value = ""; - $("msd-new-url").value = ""; - __http = null; - __applyState(); + var __uploadStateChange = function() { + if (__http.readyState !== 4) { + return; } + if (__http.status !== 200) { + wm.error("Can't upload image", __http.responseText); + } else if ($("msd-new-url").value.length > 0) { + let html = ""; + let msg = ""; + try { + let end = __http.responseText.lastIndexOf("\r\n"); + if (end < 0) { + end = __http.responseText.length; + } + let begin = __http.responseText.lastIndexOf("\r\n", end - 2); + if (begin < 0) { + end = 0; + } + let result_str = __http.responseText.slice(begin, end); + let result = JSON.parse(result_str); + if (!result.ok) { + html = "Can't upload image"; + msg = result_str; + } + } catch (ex) { + html = "Can't parse upload result"; + msg = `${ex}`; + } + if (html.length > 0) { + wm.error(html, msg); + } + } + tools.hidden.setVisible($("msd-new-sub"), false); + $("msd-new-file").value = ""; + $("msd-new-url").value = ""; + __http = null; + __refreshControls(); }; var __clickAbortNewButton = function() { __http.onreadystatechange = null; __http.abort(); __http = null; - tools.progress.setValue($("msd-uploading-progress"), "Aborted", 0); + __refreshControls(); + tools.hidden.setVisible($("msd-new-sub"), true); }; var __clickConnectButton = function(connected) { @@ -171,9 +369,9 @@ export function Msd() { if (http.status !== 200) { wm.error("Can't switch Mass Storage", http.responseText); } - __applyState(); + __refreshControls(); }); - __applyState(); + __refreshControls(); tools.el.setEnabled($(`msd-${connected ? "connect" : "disconnect"}-button`), false); }; @@ -184,9 +382,7 @@ export function Msd() { if (http.status !== 200) { wm.error("Mass Storage reset error", http.responseText); } - __applyState(); }); - __applyState(); } }); }; @@ -194,193 +390,33 @@ export function Msd() { var __toggleSelectSub = function() { let el_sub = $("msd-new-sub"); let visible = tools.hidden.isVisible(el_sub); + tools.hidden.setVisible(el_sub, !visible); if (visible) { $("msd-new-file").value = ""; $("msd-new-url").value = ""; } - tools.hidden.setVisible(el_sub, !visible); - __applyState(); + __refreshControls(); }; var __selectNewFile = function() { - let el_input = $("msd-new-file"); - let file = tools.input.getFile($("msd-new-file")); + let el = $("msd-new-file"); + let file = tools.input.getFile(el); if (file) { $("msd-new-url").value = ""; let part = __state.storage.parts[$("msd-new-part-selector").value]; if (file.size > part.size) { - wm.error(`New image is too big for the Mass Storage partition.
    Maximum: ${tools.formatSize(part.size)}`); - el_input.value = ""; + wm.error(`The new image is too big for the Mass Storage partition.
    Maximum: ${tools.formatSize(part.size)}`); + el.value = ""; } } - __applyState(); + __refreshControls(); }; var __selectNewUrl = function() { if ($("msd-new-url").value.length > 0) { $("msd-new-file").value = ""; } - __applyState(); - }; - - var __applyState = function() { - __applyStateStatus(); - - let s = __state; - let online = (s && s.online); - - if (s) { - tools.feature.setEnabled($("msd-dropdown"), s.enabled); - tools.feature.setEnabled($("msd-reset-button"), s.enabled); - } - tools.hidden.setVisible($("msd-message-offline"), (s && !s.online)); - tools.hidden.setVisible($("msd-message-image-broken"), (online && s.drive.image && !s.drive.image.complete && !s.storage.uploading)); - tools.hidden.setVisible($("msd-message-too-big-for-cdrom"), (online && s.drive.cdrom && s.drive.image && s.drive.image.size >= 2359296000)); - tools.hidden.setVisible($("msd-message-out-of-storage"), (online && s.drive.image && !s.drive.image.in_storage)); - tools.hidden.setVisible($("msd-message-rw-enabled"), (online && s.drive.rw)); - tools.hidden.setVisible($("msd-message-another-user-uploads"), (online && s.storage.uploading && !__http)); - tools.hidden.setVisible($("msd-message-downloads"), (online && s.storage.downloading)); - - if (online) { - let names = Object.keys(s.storage.parts).sort(); - let parts_names_json = JSON.stringify(names); - if (__parts_names_json !== parts_names_json) { - $("msd-storages").innerHTML = names.map(name => ` -
    -
    - -
    -
    - `).join("
    "); - __parts_names_json = parts_names_json; - __parts_names_len = names.length; - } - __parts = s.storage.parts; - } - for (let name in __parts) { - let part = __parts[name]; - let title = ( - name.length === 0 - ? `${__parts_names_len === 1 ? "Storage: %s" : "Internal storage: %s"}` // eslint-disable-line - : `Storage [${name}${part.writable ? "]" : ", read-only]"}: %s` // eslint-disable-line - ); - let id = `msd-storage-${tools.makeIdByText(name)}-progress`; - if (online) { - tools.progress.setSizeOf($(id), title, part.size, part.free); - } else { - tools.progress.setValue($(id), title.replace("%s", "unavailable"), 0); - } - } - - tools.el.setEnabled($("msd-image-selector"), (online && !s.drive.connected && !s.busy)); - __applyStateImageSelector(); - tools.el.setEnabled($("msd-download-button"), (online && s.drive.image && !s.drive.connected && !s.busy)); - tools.el.setEnabled($("msd-remove-button"), (online && s.drive.image && s.drive.image.removable && !s.drive.connected && !s.busy)); - - tools.radio.setEnabled("msd-mode-radio", (online && !s.drive.connected && !s.busy)); - tools.radio.setValue("msd-mode-radio", `${Number(online && s.drive.cdrom)}`); - - tools.el.setEnabled($("msd-rw-switch"), (online && !s.drive.connected && !s.busy)); - $("msd-rw-switch").checked = (online && s.drive.rw); - - tools.el.setEnabled($("msd-connect-button"), (online && s.drive.image && !s.drive.connected && !s.busy)); - tools.el.setEnabled($("msd-disconnect-button"), (online && s.drive.connected && !s.busy)); - - tools.el.setEnabled($("msd-select-new-button"), (online && !s.drive.connected && !__http && !s.busy)); - tools.el.setEnabled($("msd-upload-new-button"), - (online && !s.drive.connected && (tools.input.getFile($("msd-new-file")) || $("msd-new-url").value.length > 0) && !s.busy)); - tools.el.setEnabled($("msd-abort-new-button"), (online && __http)); - - tools.el.setEnabled($("msd-reset-button"), (s && s.enabled && !s.busy)); - - tools.el.setEnabled($("msd-new-file"), (online && !s.drive.connected && !__http && !s.busy)); - tools.el.setEnabled($("msd-new-url"), (online && !s.drive.connected && !__http && !s.busy)); - tools.el.setEnabled($("msd-new-part-selector"), (online && !s.drive.connected && !__http && !s.busy)); - if (online && !s.storage.uploading && !s.storage.downloading) { - let parts = Object.keys(s.storage.parts).sort().filter(name => (name === "" || s.storage.parts[name].writable)); - tools.selector.setValues($("msd-new-part-selector"), parts, "\u2500 Internal \u2500"); - tools.hidden.setVisible($("msd-new-part"), (parts.length > 1)); - } - - tools.hidden.setVisible($("msd-uploading-sub"), (online && s.storage.uploading)); - $("msd-uploading-name").innerHTML = ((online && s.storage.uploading) ? s.storage.uploading.name : ""); - $("msd-uploading-size").innerHTML = ((online && s.storage.uploading) ? tools.formatSize(s.storage.uploading.size) : ""); - if (online) { - if (s.storage.uploading) { - tools.progress.setPercentOf($("msd-uploading-progress"), s.storage.uploading.size, s.storage.uploading.written); - } else if (!__http) { - tools.progress.setValue($("msd-uploading-progress"), "Waiting for upload (press UPLOAD button) ...", 0); - } - } else { - $("msd-new-file").value = ""; - $("msd-new-url").value = ""; - tools.progress.setValue($("msd-uploading-progress"), "", 0); - } - }; - - var __applyStateStatus = function() { - let s = __state; - let online = (s && s.online); - - let led_cls = "led-gray"; - let msg = "Unavailable"; - - if (online && s.drive.connected) { - led_cls = "led-green"; - msg = "Connected to Server"; - } else if (online && s.storage.uploading) { - led_cls = "led-yellow-rotating-fast"; - msg = "Uploading new image"; - } else if (online && s.storage.downloading) { - led_cls = "led-yellow-rotating-fast"; - msg = "Serving the image to download"; - } else if (online) { // Sic! - msg = "Disconnected"; - } - - $("msd-led").className = led_cls; - $("msd-status").innerHTML = $("msd-led").title = msg; - }; - - var __applyStateImageSelector = function() { - let s = __state; - if (!(s && s.online) || s.storage.uploading || s.storage.downloading) { - return; - } - - let el = $("msd-image-selector"); - el.options.length = 1; - - let selected = ""; - - for (let name of Object.keys(s.storage.images).sort()) { - tools.selector.addSeparator(el); - tools.selector.addOption(el, name, name); - tools.selector.addComment(el, __makeImageSelectorInfo(s.storage.images[name])); - if (s.drive.image && s.drive.image.name === name && s.drive.image.in_storage) { - selected = name; - } - } - - if (s.drive.image && !s.drive.image.in_storage) { - selected = ".__external"; - tools.selector.addOption(el, s.drive.image.name, selected); - tools.selector.addComment(el, __makeImageSelectorInfo(s.drive.image)); - } - - el.value = selected; - }; - - var __makeImageSelectorInfo = function(image) { - let info = `\xA0\xA0\xA0\xA0\xA0\u2570 ${tools.formatSize(image.size)}`; - info += (image.complete ? "" : ", broken"); - if (image.in_storage !== undefined && !image.in_storage) { - info += ", out of storage"; - } - let dt = new Date(image.mod_ts * 1000); - dt = new Date(dt.getTime() - (dt.getTimezoneOffset() * 60000)); - info += " \u2500 " + dt.toISOString().slice(0, -8).replaceAll("-", ".").replace("T", "-"); - return info; + __refreshControls(); }; __init__(); From 8192b1fa95e263d41d1f9be4909dc58517b8da37 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sat, 2 Nov 2024 10:39:43 +0200 Subject: [PATCH 76/88] simplified stream js logic --- web/share/js/kvm/stream.js | 53 ++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/web/share/js/kvm/stream.js b/web/share/js/kvm/stream.js index 7c1296dc..504c7086 100644 --- a/web/share/js/kvm/stream.js +++ b/web/share/js/kvm/stream.js @@ -143,10 +143,11 @@ export function Streamer() { __state.limits = state.limits; // Following together with features } if (__state.features && state.streamer !== undefined) { + __setControlsEnabled(!!state.streamer); __state.streamer = state.streamer; } - __setControlsEnabled(!!state.streamer); } else { + __setControlsEnabled(false); __state = null; } let visible = wm.isWindowVisible($("stream-window")); @@ -207,32 +208,28 @@ export function Streamer() { tools.radio.clickValue("stream-mode-radio", mode); } - if (state.streamer !== undefined) { - let ok = (state.streamer !== null); - if (ok) { - let s = state.streamer; - __res = s.source.resolution; + if (state.streamer) { + let s = state.streamer; + __res = s.source.resolution; - { - let res = `${__res.width}x${__res.height}`; - let el = $("stream-resolution-selector"); - if (!tools.selector.hasValue(el, res)) { - tools.selector.addOption(el, res, res); - } - el.value = res; + { + let res = `${__res.width}x${__res.height}`; + let el = $("stream-resolution-selector"); + if (!tools.selector.hasValue(el, res)) { + tools.selector.addOption(el, res, res); } - tools.slider.setValue($("stream-quality-slider"), Math.max(s.encoder.quality, 1)); - tools.slider.setValue($("stream-desired-fps-slider"), s.source.desired_fps); - if (s.h264 && s.h264.bitrate) { - tools.slider.setValue($("stream-h264-bitrate-slider"), s.h264.bitrate); - tools.slider.setValue($("stream-h264-gop-slider"), s.h264.gop); // Following together with gop - } - - tools.feature.setEnabled($("stream-quality"), (s.encoder.quality > 0)); - - __streamer.ensureStream(s); + el.value = res; } - __setControlsEnabled(ok); + tools.slider.setValue($("stream-quality-slider"), Math.max(s.encoder.quality, 1)); + tools.slider.setValue($("stream-desired-fps-slider"), s.source.desired_fps); + if (s.h264 && s.h264.bitrate) { + tools.slider.setValue($("stream-h264-bitrate-slider"), s.h264.bitrate); + tools.slider.setValue($("stream-h264-gop-slider"), s.h264.gop); // Following together with gop + } + + tools.feature.setEnabled($("stream-quality"), (s.encoder.quality > 0)); + + __streamer.ensureStream(s); } }; @@ -318,7 +315,7 @@ export function Streamer() { }; var __clickResetButton = function() { - wm.confirm("Are you sure you want to reset stream?").then(function (ok) { + wm.confirm("Are you sure you want to reset stream?").then(function(ok) { if (ok) { __resetStream(); tools.httpPost("/api/streamer/reset", null, function(http) { @@ -331,12 +328,6 @@ export function Streamer() { }; var __sendParam = function(name, value) { - tools.el.setEnabled($("stream-quality-slider"), false); - tools.el.setEnabled($("stream-desired-fps-slider"), false); - tools.el.setEnabled($("stream-resolution-selector"), false); - tools.el.setEnabled($("stream-h264-bitrate-slider"), false); - tools.el.setEnabled($("stream-h264-gop-slider"), false); - tools.httpPost("/api/streamer/set_params", {[name]: value}, function(http) { if (http.status !== 200) { wm.error("Can't configure stream", http.responseText); From d6b61cb407a729169af6d678de667dae842a86f5 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sat, 2 Nov 2024 14:26:39 +0200 Subject: [PATCH 77/88] refactoring --- kvmd/apps/__init__.py | 16 +-- kvmd/apps/kvmd/__init__.py | 5 +- kvmd/apps/kvmd/api/hid.py | 84 ++++--------- kvmd/apps/kvmd/server.py | 5 +- kvmd/plugins/hid/__init__.py | 177 ++++++++++++++++++++++------ kvmd/plugins/hid/_mcu/__init__.py | 51 ++++---- kvmd/plugins/hid/bt/__init__.py | 48 ++++---- kvmd/plugins/hid/ch9329/__init__.py | 50 ++++---- kvmd/plugins/hid/otg/__init__.py | 50 ++++---- kvmd/plugins/hid/otg/keyboard.py | 6 +- 10 files changed, 258 insertions(+), 234 deletions(-) diff --git a/kvmd/apps/__init__.py b/kvmd/apps/__init__.py index dd1b8ff9..9d6d494a 100644 --- a/kvmd/apps/__init__.py +++ b/kvmd/apps/__init__.py @@ -33,8 +33,6 @@ import pygments.formatters from .. import tools -from ..mouse import MouseRange - from ..plugins import UnknownPluginError from ..plugins.auth import get_auth_service_class from ..plugins.hid import get_hid_class @@ -407,19 +405,7 @@ def _get_config_scheme() -> dict: "hid": { "type": Option("", type=valid_stripped_string_not_empty), - - "keymap": Option("/usr/share/kvmd/keymaps/en-us", type=valid_abs_file), - "ignore_keys": Option([], type=functools.partial(valid_string_list, subval=valid_hid_key)), - - "mouse_x_range": { - "min": Option(MouseRange.MIN, type=valid_hid_mouse_move), - "max": Option(MouseRange.MAX, type=valid_hid_mouse_move), - }, - "mouse_y_range": { - "min": Option(MouseRange.MIN, type=valid_hid_mouse_move), - "max": Option(MouseRange.MAX, type=valid_hid_mouse_move), - }, - + "keymap": Option("/usr/share/kvmd/keymaps/en-us", type=valid_abs_file), # Dynamic content }, diff --git a/kvmd/apps/kvmd/__init__.py b/kvmd/apps/kvmd/__init__.py index 7cf8430c..495a320f 100644 --- a/kvmd/apps/kvmd/__init__.py +++ b/kvmd/apps/kvmd/__init__.py @@ -56,7 +56,7 @@ def main(argv: (list[str] | None)=None) -> None: if config.kvmd.msd.type == "otg": msd_kwargs["gadget"] = config.otg.gadget # XXX: Small crutch to pass gadget name to the plugin - hid_kwargs = config.kvmd.hid._unpack(ignore=["type", "keymap", "ignore_keys", "mouse_x_range", "mouse_y_range"]) + hid_kwargs = config.kvmd.hid._unpack(ignore=["type", "keymap"]) if config.kvmd.hid.type == "otg": hid_kwargs["udc"] = config.otg.udc # XXX: Small crutch to pass UDC to the plugin @@ -103,9 +103,6 @@ def main(argv: (list[str] | None)=None) -> None: ), keymap_path=config.hid.keymap, - ignore_keys=config.hid.ignore_keys, - mouse_x_range=(config.hid.mouse_x_range.min, config.hid.mouse_x_range.max), - mouse_y_range=(config.hid.mouse_y_range.min, config.hid.mouse_y_range.max), stream_forever=config.streamer.forever, ).run(**config.server._unpack()) diff --git a/kvmd/apps/kvmd/api/hid.py b/kvmd/apps/kvmd/api/hid.py index 107fc858..51b9dc00 100644 --- a/kvmd/apps/kvmd/api/hid.py +++ b/kvmd/apps/kvmd/api/hid.py @@ -25,13 +25,12 @@ import stat import functools import struct +from typing import Iterable from typing import Callable from aiohttp.web import Request from aiohttp.web import Response -from ....mouse import MouseRange - from ....keyboard.keysym import build_symmap from ....keyboard.printer import text_to_web_keys @@ -59,12 +58,7 @@ class HidApi: def __init__( self, hid: BaseHid, - keymap_path: str, - ignore_keys: list[str], - - mouse_x_range: tuple[int, int], - mouse_y_range: tuple[int, int], ) -> None: self.__hid = hid @@ -73,11 +67,6 @@ class HidApi: self.__default_keymap_name = os.path.basename(keymap_path) self.__ensure_symmap(self.__default_keymap_name) - self.__ignore_keys = ignore_keys - - self.__mouse_x_range = mouse_x_range - self.__mouse_y_range = mouse_y_range - # ===== @exposed_http("GET", "/hid") @@ -134,7 +123,7 @@ class HidApi: if limit > 0: text = text[:limit] symmap = self.__ensure_symmap(req.query.get("keymap", self.__default_keymap_name)) - self.__hid.send_key_events(text_to_web_keys(text, symmap)) + self.__hid.send_key_events(text_to_web_keys(text, symmap), no_ignore_keys=True) return make_json_response() def __ensure_symmap(self, keymap_name: str) -> dict[int, dict[int, str]]: @@ -162,8 +151,7 @@ class HidApi: state = valid_bool(data[0]) except Exception: return - if key not in self.__ignore_keys: - self.__hid.send_key_events([(key, state)]) + self.__hid.send_key_event(key, state) @exposed_ws(2) async def __ws_bin_mouse_button_handler(self, _: WsSession, data: bytes) -> None: @@ -182,17 +170,17 @@ class HidApi: to_y = valid_hid_mouse_move(to_y) except Exception: return - self.__send_mouse_move_event(to_x, to_y) + self.__hid.send_mouse_move_event(to_x, to_y) @exposed_ws(4) async def __ws_bin_mouse_relative_handler(self, _: WsSession, data: bytes) -> None: - self.__process_ws_bin_delta_request(data, self.__hid.send_mouse_relative_event) + self.__process_ws_bin_delta_request(data, self.__hid.send_mouse_relative_events) @exposed_ws(5) async def __ws_bin_mouse_wheel_handler(self, _: WsSession, data: bytes) -> None: - self.__process_ws_bin_delta_request(data, self.__hid.send_mouse_wheel_event) + self.__process_ws_bin_delta_request(data, self.__hid.send_mouse_wheel_events) - def __process_ws_bin_delta_request(self, data: bytes, handler: Callable[[int, int], None]) -> None: + def __process_ws_bin_delta_request(self, data: bytes, handler: Callable[[Iterable[tuple[int, int]], bool], None]) -> None: try: squash = valid_bool(data[0]) data = data[1:] @@ -202,7 +190,7 @@ class HidApi: deltas.append((valid_hid_mouse_delta(delta_x), valid_hid_mouse_delta(delta_y))) except Exception: return - self.__send_mouse_delta_event(deltas, squash, handler) + handler(deltas, squash) # ===== @@ -213,8 +201,7 @@ class HidApi: state = valid_bool(event["state"]) except Exception: return - if key not in self.__ignore_keys: - self.__hid.send_key_events([(key, state)]) + self.__hid.send_key_event(key, state) @exposed_ws("mouse_button") async def __ws_mouse_button_handler(self, _: WsSession, event: dict) -> None: @@ -232,17 +219,17 @@ class HidApi: to_y = valid_hid_mouse_move(event["to"]["y"]) except Exception: return - self.__send_mouse_move_event(to_x, to_y) + self.__hid.send_mouse_move_event(to_x, to_y) @exposed_ws("mouse_relative") async def __ws_mouse_relative_handler(self, _: WsSession, event: dict) -> None: - self.__process_ws_delta_event(event, self.__hid.send_mouse_relative_event) + self.__process_ws_delta_event(event, self.__hid.send_mouse_relative_events) @exposed_ws("mouse_wheel") async def __ws_mouse_wheel_handler(self, _: WsSession, event: dict) -> None: - self.__process_ws_delta_event(event, self.__hid.send_mouse_wheel_event) + self.__process_ws_delta_event(event, self.__hid.send_mouse_wheel_events) - def __process_ws_delta_event(self, event: dict, handler: Callable[[int, int], None]) -> None: + def __process_ws_delta_event(self, event: dict, handler: Callable[[Iterable[tuple[int, int]], bool], None]) -> None: try: raw_delta = event["delta"] deltas = [ @@ -252,19 +239,18 @@ class HidApi: squash = valid_bool(event.get("squash", False)) except Exception: return - self.__send_mouse_delta_event(deltas, squash, handler) + handler(deltas, squash) # ===== @exposed_http("POST", "/hid/events/send_key") async def __events_send_key_handler(self, req: Request) -> Response: key = valid_hid_key(req.query.get("key")) - if key not in self.__ignore_keys: - if "state" in req.query: - state = valid_bool(req.query["state"]) - self.__hid.send_key_events([(key, state)]) - else: - self.__hid.send_key_events([(key, True), (key, False)]) + if "state" in req.query: + state = valid_bool(req.query["state"]) + self.__hid.send_key_event(key, state) + else: + self.__hid.send_key_events([(key, True), (key, False)]) return make_json_response() @exposed_http("POST", "/hid/events/send_mouse_button") @@ -282,7 +268,7 @@ class HidApi: async def __events_send_mouse_move_handler(self, req: Request) -> Response: to_x = valid_hid_mouse_move(req.query.get("to_x")) to_y = valid_hid_mouse_move(req.query.get("to_y")) - self.__send_mouse_move_event(to_x, to_y) + self.__hid.send_mouse_move_event(to_x, to_y) return make_json_response() @exposed_http("POST", "/hid/events/send_mouse_relative") @@ -298,33 +284,3 @@ class HidApi: delta_y = valid_hid_mouse_delta(req.query.get("delta_y")) handler(delta_x, delta_y) return make_json_response() - - # ===== - - def __send_mouse_move_event(self, to_x: int, to_y: int) -> None: - if self.__mouse_x_range != MouseRange.RANGE: - to_x = MouseRange.remap(to_x, *self.__mouse_x_range) - if self.__mouse_y_range != MouseRange.RANGE: - to_y = MouseRange.remap(to_y, *self.__mouse_y_range) - self.__hid.send_mouse_move_event(to_x, to_y) - - def __send_mouse_delta_event( - self, - deltas: list[tuple[int, int]], - squash: bool, - handler: Callable[[int, int], None], - ) -> None: - - if squash: - prev = (0, 0) - for cur in deltas: - if abs(prev[0] + cur[0]) > 127 or abs(prev[1] + cur[1]) > 127: - handler(*prev) - prev = cur - else: - prev = (prev[0] + cur[0], prev[1] + cur[1]) - if prev[0] or prev[1]: - handler(*prev) - else: - for xy in deltas: - handler(*xy) diff --git a/kvmd/apps/kvmd/server.py b/kvmd/apps/kvmd/server.py index d9e07484..74f74f26 100644 --- a/kvmd/apps/kvmd/server.py +++ b/kvmd/apps/kvmd/server.py @@ -173,9 +173,6 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins snapshoter: Snapshoter, keymap_path: str, - ignore_keys: list[str], - mouse_x_range: tuple[int, int], - mouse_y_range: tuple[int, int], stream_forever: bool, ) -> None: @@ -189,7 +186,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins self.__stream_forever = stream_forever - self.__hid_api = HidApi(hid, keymap_path, ignore_keys, mouse_x_range, mouse_y_range) # Ugly hack to get keymaps state + self.__hid_api = HidApi(hid, keymap_path) # Ugly hack to get keymaps state self.__apis: list[object] = [ self, AuthApi(auth_manager), diff --git a/kvmd/plugins/hid/__init__.py b/kvmd/plugins/hid/__init__.py index 3cba01f4..f7debe1d 100644 --- a/kvmd/plugins/hid/__init__.py +++ b/kvmd/plugins/hid/__init__.py @@ -21,9 +21,11 @@ import asyncio +import functools import time from typing import Iterable +from typing import Callable from typing import AsyncGenerator from typing import Any @@ -31,14 +33,37 @@ from ...yamlconf import Option from ...validators.basic import valid_bool from ...validators.basic import valid_int_f1 +from ...validators.basic import valid_string_list +from ...validators.hid import valid_hid_key +from ...validators.hid import valid_hid_mouse_move + +from ...mouse import MouseRange from .. import BasePlugin from .. import get_plugin_class # ===== -class BaseHid(BasePlugin): - def __init__(self, jiggler_enabled: bool, jiggler_active: bool, jiggler_interval: int) -> None: +class BaseHid(BasePlugin): # pylint: disable=too-many-instance-attributes + def __init__( + self, + ignore_keys: list[str], + + mouse_x_min: int, + mouse_x_max: int, + mouse_y_min: int, + mouse_y_max: int, + + jiggler_enabled: bool, + jiggler_active: bool, + jiggler_interval: int, + ) -> None: + + self.__ignore_keys = ignore_keys + + self.__mouse_x_range = (mouse_x_min, mouse_x_max) + self.__mouse_y_range = (mouse_y_min, mouse_y_max) + self.__jiggler_enabled = jiggler_enabled self.__jiggler_active = jiggler_active self.__jiggler_interval = jiggler_interval @@ -46,8 +71,17 @@ class BaseHid(BasePlugin): self.__activity_ts = 0 @classmethod - def _get_jiggler_options(cls) -> dict[str, Any]: + def _get_base_options(cls) -> dict[str, Any]: return { + "ignore_keys": Option([], type=functools.partial(valid_string_list, subval=valid_hid_key)), + "mouse_x_range": { + "min": Option(MouseRange.MIN, type=valid_hid_mouse_move, unpack_as="mouse_x_min"), + "max": Option(MouseRange.MAX, type=valid_hid_mouse_move, unpack_as="mouse_x_max"), + }, + "mouse_y_range": { + "min": Option(MouseRange.MIN, type=valid_hid_mouse_move, unpack_as="mouse_y_min"), + "max": Option(MouseRange.MAX, type=valid_hid_mouse_move, unpack_as="mouse_y_max"), + }, "jiggler": { "enabled": Option(False, type=valid_bool, unpack_as="jiggler_enabled"), "active": Option(False, type=valid_bool, unpack_as="jiggler_active"), @@ -76,25 +110,6 @@ class BaseHid(BasePlugin): async def cleanup(self) -> None: pass - # ===== - - def send_key_events(self, keys: Iterable[tuple[str, bool]]) -> None: - raise NotImplementedError - - def send_mouse_button_event(self, button: str, state: bool) -> None: - raise NotImplementedError - - def send_mouse_move_event(self, to_x: int, to_y: int) -> None: - _ = to_x - _ = to_y - - def send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None: - _ = delta_x - _ = delta_y - - def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: - raise NotImplementedError - def set_params( self, keyboard_output: (str | None)=None, @@ -107,25 +122,100 @@ class BaseHid(BasePlugin): def set_connected(self, connected: bool) -> None: _ = connected - def clear_events(self) -> None: + # ===== + + def send_key_events(self, keys: Iterable[tuple[str, bool]], no_ignore_keys: bool=False) -> None: + for (key, state) in keys: + if no_ignore_keys or key not in self.__ignore_keys: + self.send_key_event(key, state) + + def send_key_event(self, key: str, state: bool) -> None: + self._send_key_event(key, state) + self.__bump_activity() + + def _send_key_event(self, key: str, state: bool) -> None: raise NotImplementedError # ===== - async def systask(self) -> None: - factor = 1 - while True: - if self.__jiggler_active and (self.__activity_ts + self.__jiggler_interval < int(time.monotonic())): - for _ in range(5): - if self.__jiggler_absolute: - self.send_mouse_move_event(100 * factor, 100 * factor) - else: - self.send_mouse_relative_event(10 * factor, 10 * factor) - factor *= -1 - await asyncio.sleep(0.1) - await asyncio.sleep(1) + def send_mouse_button_event(self, button: str, state: bool) -> None: + self._send_mouse_button_event(button, state) + self.__bump_activity() - def _bump_activity(self) -> None: + def _send_mouse_button_event(self, button: str, state: bool) -> None: + raise NotImplementedError + + # ===== + + def send_mouse_move_event(self, to_x: int, to_y: int) -> None: + if self.__mouse_x_range != MouseRange.RANGE: + to_x = MouseRange.remap(to_x, *self.__mouse_x_range) + if self.__mouse_y_range != MouseRange.RANGE: + to_y = MouseRange.remap(to_y, *self.__mouse_y_range) + self._send_mouse_move_event(to_x, to_y) + self.__bump_activity() + + def _send_mouse_move_event(self, to_x: int, to_y: int) -> None: + _ = to_x # XXX: NotImplementedError + _ = to_y + + # ===== + + def send_mouse_relative_events(self, deltas: Iterable[tuple[int, int]], squash: bool) -> None: + self.__process_mouse_delta_event(deltas, squash, self.send_mouse_relative_event) + + def send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None: + self._send_mouse_relative_event(delta_x, delta_y) + self.__bump_activity() + + def _send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None: + _ = delta_x # XXX: NotImplementedError + _ = delta_y + + # ===== + + def send_mouse_wheel_events(self, deltas: Iterable[tuple[int, int]], squash: bool) -> None: + self.__process_mouse_delta_event(deltas, squash, self.send_mouse_wheel_event) + + def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: + self._send_mouse_wheel_event(delta_x, delta_y) + self.__bump_activity() + + def _send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: + raise NotImplementedError + + # ===== + + def clear_events(self) -> None: + self._clear_events() # Don't bump activity here + + def _clear_events(self) -> None: + raise NotImplementedError + + # ===== + + def __process_mouse_delta_event( + self, + deltas: Iterable[tuple[int, int]], + squash: bool, + handler: Callable[[int, int], None], + ) -> None: + + if squash: + prev = (0, 0) + for cur in deltas: + if abs(prev[0] + cur[0]) > 127 or abs(prev[1] + cur[1]) > 127: + handler(*prev) + prev = cur + else: + prev = (prev[0] + cur[0], prev[1] + cur[1]) + if prev[0] or prev[1]: + handler(*prev) + else: + for xy in deltas: + handler(*xy) + + def __bump_activity(self) -> None: self.__activity_ts = int(time.monotonic()) def _set_jiggler_absolute(self, absolute: bool) -> None: @@ -144,6 +234,21 @@ class BaseHid(BasePlugin): }, } + # ===== + + async def systask(self) -> None: + factor = 1 + while True: + if self.__jiggler_active and (self.__activity_ts + self.__jiggler_interval < int(time.monotonic())): + for _ in range(5): + if self.__jiggler_absolute: + self.send_mouse_move_event(100 * factor, 100 * factor) + else: + self.send_mouse_relative_event(10 * factor, 10 * factor) + factor *= -1 + await asyncio.sleep(0.1) + await asyncio.sleep(1) + # ===== def get_hid_class(name: str) -> type[BaseHid]: diff --git a/kvmd/plugins/hid/_mcu/__init__.py b/kvmd/plugins/hid/_mcu/__init__.py index e058fd6c..a4c903b7 100644 --- a/kvmd/plugins/hid/_mcu/__init__.py +++ b/kvmd/plugins/hid/_mcu/__init__.py @@ -26,7 +26,6 @@ import queue import copy import time -from typing import Iterable from typing import Generator from typing import AsyncGenerator from typing import Any @@ -109,17 +108,22 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many- def __init__( # pylint: disable=too-many-arguments,super-init-not-called self, phy: BasePhy, + + ignore_keys: list[str], + mouse_x_range: dict[str, Any], + mouse_y_range: dict[str, Any], + jiggler: dict[str, Any], + reset_self: bool, read_retries: int, common_retries: int, retries_delay: float, errors_threshold: int, noop: bool, - jiggler: dict[str, Any], **gpio_kwargs: Any, ) -> None: - BaseHid.__init__(self, **jiggler) + BaseHid.__init__(self, ignore_keys=ignore_keys, **mouse_x_range, **mouse_y_range, **jiggler) multiprocessing.Process.__init__(self, daemon=True) self.__read_retries = read_retries @@ -164,7 +168,7 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many- "errors_threshold": Option(5, type=valid_int_f0), "noop": Option(False, type=valid_bool), - **cls._get_jiggler_options(), + **cls._get_base_options(), } def sysprep(self) -> None: @@ -259,27 +263,6 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many- # ===== - def send_key_events(self, keys: Iterable[tuple[str, bool]]) -> None: - for (key, state) in keys: - self.__queue_event(KeyEvent(key, state)) - self._bump_activity() - - def send_mouse_button_event(self, button: str, state: bool) -> None: - self.__queue_event(MouseButtonEvent(button, state)) - self._bump_activity() - - def send_mouse_move_event(self, to_x: int, to_y: int) -> None: - self.__queue_event(MouseMoveEvent(to_x, to_y)) - self._bump_activity() - - def send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None: - self.__queue_event(MouseRelativeEvent(delta_x, delta_y)) - self._bump_activity() - - def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: - self.__queue_event(MouseWheelEvent(delta_x, delta_y)) - self._bump_activity() - def set_params( self, keyboard_output: (str | None)=None, @@ -301,9 +284,23 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many- def set_connected(self, connected: bool) -> None: self.__queue_event(SetConnectedEvent(connected), clear=True) - def clear_events(self) -> None: + def _send_key_event(self, key: str, state: bool) -> None: + self.__queue_event(KeyEvent(key, state)) + + def _send_mouse_button_event(self, button: str, state: bool) -> None: + self.__queue_event(MouseButtonEvent(button, state)) + + def _send_mouse_move_event(self, to_x: int, to_y: int) -> None: + self.__queue_event(MouseMoveEvent(to_x, to_y)) + + def _send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None: + self.__queue_event(MouseRelativeEvent(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)) + + def _clear_events(self) -> None: self.__queue_event(ClearEvent(), clear=True) - self._bump_activity() def __queue_event(self, event: BaseEvent, clear: bool=False) -> None: if not self.__stop_event.is_set(): diff --git a/kvmd/plugins/hid/bt/__init__.py b/kvmd/plugins/hid/bt/__init__.py index bca8f9a5..4d0effb5 100644 --- a/kvmd/plugins/hid/bt/__init__.py +++ b/kvmd/plugins/hid/bt/__init__.py @@ -24,7 +24,6 @@ import multiprocessing import copy import time -from typing import Iterable from typing import AsyncGenerator from typing import Any @@ -64,6 +63,11 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes def __init__( # pylint: disable=too-many-arguments,too-many-locals self, + ignore_keys: list[str], + mouse_x_range: dict[str, Any], + mouse_y_range: dict[str, Any], + jiggler: dict[str, Any], + manufacturer: str, product: str, description: str, @@ -79,11 +83,9 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes max_clients: int, socket_timeout: float, select_timeout: float, - - jiggler: dict[str, Any], ) -> None: - super().__init__(**jiggler) + super().__init__(ignore_keys=ignore_keys, **mouse_x_range, **mouse_y_range, **jiggler) self._set_jiggler_absolute(False) self.__proc: (multiprocessing.Process | None) = None @@ -127,7 +129,7 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes "socket_timeout": Option(5.0, type=valid_float_f01), "select_timeout": Option(1.0, type=valid_float_f01), - **cls._get_jiggler_options(), + **cls._get_base_options(), } def sysprep(self) -> None: @@ -187,27 +189,6 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes # ===== - def send_key_events(self, keys: Iterable[tuple[str, bool]]) -> None: - for (key, state) in keys: - self.__server.queue_event(make_keyboard_event(key, state)) - self._bump_activity() - - def send_mouse_button_event(self, button: str, state: bool) -> None: - self.__server.queue_event(MouseButtonEvent(button, state)) - self._bump_activity() - - def send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None: - self.__server.queue_event(MouseRelativeEvent(delta_x, delta_y)) - self._bump_activity() - - def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: - self.__server.queue_event(MouseWheelEvent(delta_x, delta_y)) - self._bump_activity() - - def clear_events(self) -> None: - self.__server.clear_events() - self._bump_activity() - def set_params( self, keyboard_output: (str | None)=None, @@ -221,6 +202,21 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes self._set_jiggler_active(jiggler) self.__notifier.notify() + def _send_key_event(self, key: str, state: bool) -> None: + self.__server.queue_event(make_keyboard_event(key, state)) + + def _send_mouse_button_event(self, button: str, state: bool) -> None: + self.__server.queue_event(MouseButtonEvent(button, state)) + + def _send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None: + self.__server.queue_event(MouseRelativeEvent(delta_x, delta_y)) + + def _send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: + self.__server.queue_event(MouseWheelEvent(delta_x, delta_y)) + + def _clear_events(self) -> None: + self.__server.clear_events() + # ===== def __server_worker(self) -> None: # pylint: disable=too-many-branches diff --git a/kvmd/plugins/hid/ch9329/__init__.py b/kvmd/plugins/hid/ch9329/__init__.py index f93be95c..c5d10688 100644 --- a/kvmd/plugins/hid/ch9329/__init__.py +++ b/kvmd/plugins/hid/ch9329/__init__.py @@ -25,7 +25,6 @@ import queue import copy import time -from typing import Iterable from typing import AsyncGenerator from typing import Any @@ -55,13 +54,17 @@ from .keyboard import Keyboard class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-instance-attributes def __init__( # pylint: disable=too-many-arguments,super-init-not-called self, + ignore_keys: list[str], + mouse_x_range: dict[str, Any], + mouse_y_range: dict[str, Any], + jiggler: dict[str, Any], + device_path: str, speed: int, read_timeout: float, - jiggler: dict[str, Any], ) -> None: - BaseHid.__init__(self, **jiggler) + BaseHid.__init__(self, ignore_keys=ignore_keys, **mouse_x_range, **mouse_y_range, **jiggler) multiprocessing.Process.__init__(self, daemon=True) self.__device_path = device_path @@ -89,7 +92,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst "device": Option("/dev/kvmd-hid", type=valid_abs_path, unpack_as="device_path"), "speed": Option(9600, type=valid_tty_speed), "read_timeout": Option(0.3, type=valid_float_f01), - **cls._get_jiggler_options(), + **cls._get_base_options(), } def sysprep(self) -> None: @@ -146,27 +149,6 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst # ===== - def send_key_events(self, keys: Iterable[tuple[str, bool]]) -> None: - for (key, state) in keys: - self.__queue_cmd(self.__keyboard.process_key(key, state)) - self._bump_activity() - - def send_mouse_button_event(self, button: str, state: bool) -> None: - self.__queue_cmd(self.__mouse.process_button(button, state)) - self._bump_activity() - - def send_mouse_move_event(self, to_x: int, to_y: int) -> None: - self.__queue_cmd(self.__mouse.process_move(to_x, to_y)) - self._bump_activity() - - def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: - self.__queue_cmd(self.__mouse.process_wheel(delta_x, delta_y)) - self._bump_activity() - - def send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None: - self.__queue_cmd(self.__mouse.process_relative(delta_x, delta_y)) - self._bump_activity() - def set_params( self, keyboard_output: (str | None)=None, @@ -185,10 +167,22 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst self._set_jiggler_active(jiggler) self.__notifier.notify() - def set_connected(self, connected: bool) -> None: - pass + def _send_key_event(self, key: str, state: bool) -> None: + self.__queue_cmd(self.__keyboard.process_key(key, state)) - def clear_events(self) -> None: + def _send_mouse_button_event(self, button: str, state: bool) -> None: + self.__queue_cmd(self.__mouse.process_button(button, state)) + + def _send_mouse_move_event(self, to_x: int, to_y: int) -> None: + self.__queue_cmd(self.__mouse.process_move(to_x, to_y)) + + def _send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: + self.__queue_cmd(self.__mouse.process_wheel(delta_x, delta_y)) + + def _send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None: + self.__queue_cmd(self.__mouse.process_relative(delta_x, delta_y)) + + def _clear_events(self) -> None: tools.clear_queue(self.__cmd_queue) def __queue_cmd(self, cmd: bytes, clear: bool=False) -> None: diff --git a/kvmd/plugins/hid/otg/__init__.py b/kvmd/plugins/hid/otg/__init__.py index 7686ebdd..c95fb7fb 100644 --- a/kvmd/plugins/hid/otg/__init__.py +++ b/kvmd/plugins/hid/otg/__init__.py @@ -22,7 +22,6 @@ import copy -from typing import Iterable from typing import AsyncGenerator from typing import Any @@ -48,15 +47,20 @@ from .mouse import MouseProcess class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes def __init__( self, + ignore_keys: list[str], + mouse_x_range: dict[str, Any], + mouse_y_range: dict[str, Any], + jiggler: dict[str, Any], + keyboard: dict[str, Any], mouse: dict[str, Any], mouse_alt: dict[str, Any], - jiggler: dict[str, Any], noop: bool, + udc: str, # XXX: Not from options, see /kvmd/apps/kvmd/__init__.py for details ) -> None: - super().__init__(**jiggler) + super().__init__(ignore_keys=ignore_keys, **mouse_x_range, **mouse_y_range, **jiggler) self.__udc = udc @@ -115,7 +119,7 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes "horizontal_wheel": Option(True, type=valid_bool), }, "noop": Option(False, type=valid_bool), - **cls._get_jiggler_options(), + **cls._get_base_options(), } def sysprep(self) -> None: @@ -183,26 +187,6 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes # ===== - def send_key_events(self, keys: Iterable[tuple[str, bool]]) -> None: - self.__keyboard_proc.send_key_events(keys) - self._bump_activity() - - def send_mouse_button_event(self, button: str, state: bool) -> None: - self.__mouse_current.send_button_event(button, state) - self._bump_activity() - - def send_mouse_move_event(self, to_x: int, to_y: int) -> None: - self.__mouse_current.send_move_event(to_x, to_y) - self._bump_activity() - - def send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None: - self.__mouse_current.send_relative_event(delta_x, delta_y) - self._bump_activity() - - def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: - self.__mouse_current.send_wheel_event(delta_x, delta_y) - self._bump_activity() - def set_params( self, keyboard_output: (str | None)=None, @@ -221,12 +205,26 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes self._set_jiggler_active(jiggler) self.__notifier.notify() - def clear_events(self) -> None: + def _send_key_event(self, key: str, state: bool) -> None: + self.__keyboard_proc.send_key_event(key, state) + + def _send_mouse_button_event(self, button: str, state: bool) -> None: + self.__mouse_current.send_button_event(button, state) + + def _send_mouse_move_event(self, to_x: int, to_y: int) -> None: + self.__mouse_current.send_move_event(to_x, to_y) + + def _send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None: + self.__mouse_current.send_relative_event(delta_x, delta_y) + + def _send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: + self.__mouse_current.send_wheel_event(delta_x, delta_y) + + def _clear_events(self) -> None: self.__keyboard_proc.send_clear_event() self.__mouse_proc.send_clear_event() if self.__mouse_alt_proc: self.__mouse_alt_proc.send_clear_event() - self._bump_activity() # ===== diff --git a/kvmd/plugins/hid/otg/keyboard.py b/kvmd/plugins/hid/otg/keyboard.py index 9008db06..e82d95a3 100644 --- a/kvmd/plugins/hid/otg/keyboard.py +++ b/kvmd/plugins/hid/otg/keyboard.py @@ -20,7 +20,6 @@ # ========================================================================== # -from typing import Iterable from typing import Generator from typing import Any @@ -68,9 +67,8 @@ class KeyboardProcess(BaseDeviceProcess): self._clear_queue() self._queue_event(ResetEvent()) - def send_key_events(self, keys: Iterable[tuple[str, bool]]) -> None: - for (key, state) in keys: - self._queue_event(make_keyboard_event(key, state)) + def send_key_event(self, key: str, state: bool) -> None: + self._queue_event(make_keyboard_event(key, state)) # ===== From d4fb640418efdb924bf30f1487acde4a675d6e2f Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sat, 2 Nov 2024 14:46:48 +0200 Subject: [PATCH 78/88] refactoring --- kvmd/apps/kvmd/server.py | 84 ++++++++++++---------------------------- 1 file changed, 25 insertions(+), 59 deletions(-) diff --git a/kvmd/apps/kvmd/server.py b/kvmd/apps/kvmd/server.py index 74f74f26..ed85bb24 100644 --- a/kvmd/apps/kvmd/server.py +++ b/kvmd/apps/kvmd/server.py @@ -20,8 +20,6 @@ # ========================================================================== # -import asyncio -import operator import dataclasses from typing import Callable @@ -100,58 +98,40 @@ class StreamerH264NotSupported(OperationError): # ===== -@dataclasses.dataclass(frozen=True) -class _SubsystemEventSource: - get_state: (Callable[[], Coroutine[Any, Any, dict]] | None) = None +@dataclasses.dataclass +class _Subsystem: + name: str + event_type: str + sysprep: (Callable[[], None] | None) + systask: (Callable[[], Coroutine[Any, Any, None]] | None) + cleanup: (Callable[[], Coroutine[Any, Any, dict]] | None) trigger_state: (Callable[[], Coroutine[Any, Any, None]] | None) = None poll_state: (Callable[[], AsyncGenerator[dict, None]] | None) = None - -@dataclasses.dataclass -class _Subsystem: - name: str - sysprep: (Callable[[], None] | None) - systask: (Callable[[], Coroutine[Any, Any, None]] | None) - cleanup: (Callable[[], Coroutine[Any, Any, dict]] | None) - sources: dict[str, _SubsystemEventSource] + def __post_init__(self) -> None: + if self.event_type: + assert self.trigger_state + assert self.poll_state @classmethod def make(cls, obj: object, name: str, event_type: str="") -> "_Subsystem": if isinstance(obj, BasePlugin): name = f"{name} ({obj.get_plugin_name()})" - sub = _Subsystem( + return _Subsystem( name=name, + event_type=event_type, sysprep=getattr(obj, "sysprep", None), systask=getattr(obj, "systask", None), cleanup=getattr(obj, "cleanup", None), - sources={}, + trigger_state=getattr(obj, "trigger_state", None), + poll_state=getattr(obj, "poll_state", None), + ) - if event_type: - sub.add_source( - event_type=event_type, - get_state=getattr(obj, "get_state", None), - trigger_state=getattr(obj, "trigger_state", None), - poll_state=getattr(obj, "poll_state", None), - ) - return sub - - def add_source( - self, - event_type: str, - get_state: (Callable[[], Coroutine[Any, Any, dict]] | None), - trigger_state: (Callable[[], Coroutine[Any, Any, None]] | None), - poll_state: (Callable[[], AsyncGenerator[dict, None]] | None), - ) -> "_Subsystem": - - assert event_type - assert event_type not in self.sources, (self, event_type) - assert get_state or poll_state, (self, event_type) - self.sources[event_type] = _SubsystemEventSource(get_state, trigger_state, poll_state) - return self class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-instance-attributes __EV_GPIO_STATE = "gpio_state" + __EV_HID_STATE = "hid_state" __EV_ATX_STATE = "atx_state" __EV_MSD_STATE = "msd_state" __EV_STREAMER_STATE = "streamer_state" @@ -200,11 +180,10 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins ExportApi(info_manager, atx, user_gpio), RedfishApi(info_manager, atx), ] - 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(hid, "HID", self.__EV_HID_STATE), _Subsystem.make(atx, "ATX", self.__EV_ATX_STATE), _Subsystem.make(msd, "MSD", self.__EV_MSD_STATE), _Subsystem.make(streamer, "Streamer", self.__EV_STREAMER_STATE), @@ -259,24 +238,11 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins "minor": int(minor), }, }) - states = [ - (event_type, src.get_state()) - for sub in self.__subsystems - for (event_type, src) in sub.sources.items() - if src.get_state and not src.trigger_state - ] - events = dict(zip( - map(operator.itemgetter(0), states), - await asyncio.gather(*map(operator.itemgetter(1), states)), - )) - await asyncio.gather(*[ - ws.send_event(event_type, events.pop(event_type)) - for (event_type, _) in states - ]) for sub in self.__subsystems: - for src in sub.sources.values(): - if src.trigger_state: - await src.trigger_state() + if sub.event_type: + assert sub.trigger_state + await sub.trigger_state() + await self._broadcast_ws_event("hid_keymaps_state", await self.__hid_api.get_keymaps()) # FIXME return (await self._ws_loop(ws)) @exposed_ws("ping") @@ -300,9 +266,9 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins for sub in self.__subsystems: if sub.systask: aiotools.create_deadly_task(sub.name, sub.systask()) - for (event_type, src) in sub.sources.items(): - if src.poll_state: - aiotools.create_deadly_task(f"{sub.name} [poller]", self.__poll_state(event_type, src.poll_state())) + if sub.event_type: + assert sub.poll_state + aiotools.create_deadly_task(f"{sub.name} [poller]", self.__poll_state(sub.event_type, sub.poll_state())) aiotools.create_deadly_task("Stream snapshoter", self.__stream_snapshoter()) self._add_exposed(*self.__apis) From 0fd1174bc5354d8f69adde3218edd121e48c84f0 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sat, 2 Nov 2024 18:06:45 +0200 Subject: [PATCH 79/88] granularity info and minor fixes --- kvmd/apps/kvmd/info/__init__.py | 9 ++++++++ kvmd/apps/kvmd/ocr.py | 5 +++++ kvmd/apps/kvmd/streamer.py | 8 +++++++ kvmd/apps/kvmd/ugpio.py | 6 ++++++ kvmd/plugins/atx/__init__.py | 6 ++++++ kvmd/plugins/msd/__init__.py | 12 +++++++++++ web/share/js/kvm/atx.js | 38 ++++++++++++++++++++++++++------- web/share/js/kvm/ocr.js | 12 +++++++---- 8 files changed, 84 insertions(+), 12 deletions(-) diff --git a/kvmd/apps/kvmd/info/__init__.py b/kvmd/apps/kvmd/info/__init__.py index b346c10c..9ede5489 100644 --- a/kvmd/apps/kvmd/info/__init__.py +++ b/kvmd/apps/kvmd/info/__init__.py @@ -65,6 +65,15 @@ class InfoManager: ]) async def poll_state(self) -> AsyncGenerator[dict, None]: + # ==== Granularity table ==== + # - system -- Partial + # - auth -- Partial + # - meta -- Partial, nullable + # - extras -- Partial, nullable + # - hw -- Partial + # - fan -- Partial + # =========================== + while True: (field, value) = await self.__queue.get() yield {field: value} diff --git a/kvmd/apps/kvmd/ocr.py b/kvmd/apps/kvmd/ocr.py index e110a720..367c0c80 100644 --- a/kvmd/apps/kvmd/ocr.py +++ b/kvmd/apps/kvmd/ocr.py @@ -129,6 +129,11 @@ class Ocr: self.__notifier.notify() async def poll_state(self) -> AsyncGenerator[dict, None]: + # ===== Granularity table ===== + # - enabled -- Full + # - langs -- Partial + # ============================= + while True: await self.__notifier.wait() yield (await self.get_state()) diff --git a/kvmd/apps/kvmd/streamer.py b/kvmd/apps/kvmd/streamer.py index d02bf50d..08c48eb1 100644 --- a/kvmd/apps/kvmd/streamer.py +++ b/kvmd/apps/kvmd/streamer.py @@ -287,6 +287,14 @@ class Streamer: # pylint: disable=too-many-instance-attributes self.__notifier.notify(self.__ST_FULL) async def poll_state(self) -> AsyncGenerator[dict, None]: + # ==== Granularity table ==== + # - features -- Full + # - limits -- Partial, paired with params + # - params -- Partial, paired with limits + # - streamer -- Partial, nullable + # - snapshot -- Partial + # =========================== + def signal_handler(*_: Any) -> None: get_logger(0).info("Got SIGUSR2, checking the stream state ...") self.__notifier.notify(self.__ST_STREAMER) diff --git a/kvmd/apps/kvmd/ugpio.py b/kvmd/apps/kvmd/ugpio.py index b5b4a621..e4735c61 100644 --- a/kvmd/apps/kvmd/ugpio.py +++ b/kvmd/apps/kvmd/ugpio.py @@ -271,6 +271,12 @@ class UserGpio: self.__notifier.notify(1) async def poll_state(self) -> AsyncGenerator[dict, None]: + # ==== Granularity table ==== + # - model -- Full + # - state.inputs -- Partial + # - state.outputs -- Partial + # =========================== + prev: dict = {"inputs": {}, "outputs": {}} while True: # pylint: disable=too-many-nested-blocks if (await self.__notifier.wait()) > 0: diff --git a/kvmd/plugins/atx/__init__.py b/kvmd/plugins/atx/__init__.py index 7545b030..d8bea96d 100644 --- a/kvmd/plugins/atx/__init__.py +++ b/kvmd/plugins/atx/__init__.py @@ -52,6 +52,12 @@ class BaseAtx(BasePlugin): raise NotImplementedError async def poll_state(self) -> AsyncGenerator[dict, None]: + # ==== Granularity table ==== + # - enabled -- Full + # - busy -- Partial + # - leds -- Partial + # =========================== + yield {} raise NotImplementedError diff --git a/kvmd/plugins/msd/__init__.py b/kvmd/plugins/msd/__init__.py index 193b81b1..b2f9d50e 100644 --- a/kvmd/plugins/msd/__init__.py +++ b/kvmd/plugins/msd/__init__.py @@ -121,6 +121,18 @@ class BaseMsd(BasePlugin): raise NotImplementedError() async def poll_state(self) -> AsyncGenerator[dict, None]: + # ==== Granularity table ==== + # - enabled -- Full + # - online -- Partial + # - busy -- Partial + # - drive -- Partial, nullable + # - storage -- Partial, nullable + # - storage.parts -- Partial + # - storage.images -- Partial + # - storage.downloading -- Partial, nullable + # - storage.uploading -- Partial, nullable + # =========================== + if self is not None: # XXX: Vulture and pylint hack raise NotImplementedError() yield diff --git a/web/share/js/kvm/atx.js b/web/share/js/kvm/atx.js index bb8b5543..4d9764ea 100644 --- a/web/share/js/kvm/atx.js +++ b/web/share/js/kvm/atx.js @@ -32,6 +32,8 @@ export function Atx(__recorder) { /************************************************************************/ + var __state = null; + var __init__ = function() { $("atx-power-led").title = "Power Led"; $("atx-hdd-led").title = "Disk Activity Led"; @@ -46,18 +48,38 @@ export function Atx(__recorder) { /************************************************************************/ self.setState = function(state) { - let buttons_enabled = false; if (state) { - tools.feature.setEnabled($("atx-dropdown"), state.enabled); - $("atx-power-led").className = (state.busy ? "led-yellow" : (state.leds.power ? "led-green" : "led-gray")); - $("atx-hdd-led").className = (state.leds.hdd ? "led-red" : "led-gray"); - buttons_enabled = !state.busy; + if (!__state) { + __state = {"leds": {}}; + } + if (state.enabled !== undefined) { + tools.feature.setEnabled($("atx-dropdown"), state.enabled); + __state.enabled = state.enabled; + } + if (__state.enabled !== undefined) { + if (state.busy !== undefined) { + __updateButtons(!state.busy); + __state.busy = state.busy; + } + if (state.leds !== undefined) { + __state.leds = state.leds; + } + if (state.busy !== undefined || state.leds !== undefined) { + let busy = __state.busy; + let leds = __state.leds; + $("atx-power-led").className = (busy ? "led-yellow" : (leds.power ? "led-green" : "led-gray")); + $("atx-hdd-led").className = (leds.hdd ? "led-red" : "led-gray"); + } + } } else { - $("atx-power-led").className = "led-gray"; - $("atx-hdd-led").className = "led-gray"; + __state = null; + __updateButtons(false); } + }; + + var __updateButtons = function(enabled) { for (let id of ["atx-power-button", "atx-power-button-long", "atx-reset-button"]) { - tools.el.setEnabled($(id), buttons_enabled); + tools.el.setEnabled($(id), enabled); } }; diff --git a/web/share/js/kvm/ocr.js b/web/share/js/kvm/ocr.js index 849b19b1..5f44791a 100644 --- a/web/share/js/kvm/ocr.js +++ b/web/share/js/kvm/ocr.js @@ -32,6 +32,8 @@ export function Ocr(__getGeometry) { /************************************************************************/ + var __enabled = null; + var __start_pos = null; var __end_pos = null; var __sel = null; @@ -71,8 +73,10 @@ export function Ocr(__getGeometry) { /************************************************************************/ self.setState = function(state) { - let enabled = (state && state.enabled && !tools.browser.is_mobile); - if (enabled) { + if (state.enabled !== undefined) { + __enabled = (state.enabled && !tools.browser.is_mobile); + } + if (__enabled) { let el = $("stream-ocr-lang-selector"); el.options.length = 0; for (let lang of state.langs.available) { @@ -80,8 +84,8 @@ export function Ocr(__getGeometry) { } el.value = tools.storage.get("stream.ocr.lang", state.langs["default"]); } - tools.feature.setEnabled($("stream-ocr"), enabled); - $("stream-ocr-led").className = (enabled ? "led-gray" : "hidden"); + tools.feature.setEnabled($("stream-ocr"), __enabled); + $("stream-ocr-led").className = (__enabled ? "led-gray" : "hidden"); }; var __startSelection = function(event) { From 5aef0a2193775cfaf3dc05e79edd34418e71885c Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sat, 2 Nov 2024 18:47:59 +0200 Subject: [PATCH 80/88] refactoring --- web/share/js/kvm/hid.js | 71 +--------------------- web/share/js/kvm/keyboard.js | 6 +- web/share/js/kvm/mouse.js | 10 ++-- web/share/js/kvm/paste.js | 112 +++++++++++++++++++++++++++++++++++ web/share/js/kvm/session.js | 6 +- web/share/js/tools.js | 24 -------- 6 files changed, 127 insertions(+), 102 deletions(-) create mode 100644 web/share/js/kvm/paste.js diff --git a/web/share/js/kvm/hid.js b/web/share/js/kvm/hid.js index e95d92c2..e7aa29e6 100644 --- a/web/share/js/kvm/hid.js +++ b/web/share/js/kvm/hid.js @@ -71,21 +71,6 @@ export function Hid(__getGeometry, __recorder) { window.addEventListener("pagehide", __releaseAll); window.addEventListener("blur", __releaseAll); - tools.storage.bindSimpleSwitch($("hid-pak-ask-switch"), "hid.pak.ask", true); - tools.storage.bindSimpleSwitch($("hid-pak-secure-switch"), "hid.pak.secure", false, function(value) { - $("hid-pak-text").style.setProperty("-webkit-text-security", (value ? "disc" : "none")); - }); - tools.feature.setEnabled($("hid-pak-secure"), ( - tools.browser.is_chrome - || tools.browser.is_safari - || tools.browser.is_opera - )); - - $("hid-pak-keymap-selector").addEventListener("change", function() { - tools.storage.set("hid.pak.keymap", $("hid-pak-keymap-selector").value); - }); - - tools.el.setOnClick($("hid-pak-button"), __clickPasteAsKeysButton); tools.el.setOnClick($("hid-connect-switch"), __clickConnectSwitch); tools.el.setOnClick($("hid-reset-button"), __clickResetButton); @@ -117,8 +102,6 @@ export function Hid(__getGeometry, __recorder) { /************************************************************************/ self.setSocket = function(ws) { - tools.el.setEnabled($("hid-pak-text"), ws); - tools.el.setEnabled($("hid-pak-button"), ws); tools.el.setEnabled($("hid-reset-button"), ws); tools.el.setEnabled($("hid-jiggler-switch"), ws); if (!ws) { @@ -198,17 +181,11 @@ export function Hid(__getGeometry, __recorder) { tools.el.setEnabled($("hid-connect-switch"), (state && state.online && !state.busy)); if (state) { - __keyboard.setState(state.keyboard, state.online, state.busy); - __mouse.setState(state.mouse, state.online, state.busy); + __keyboard.setState(state.keyboard.online, state.keyboard.leds, state.online, state.busy); + __mouse.setState(state.mouse.online, state.mouse.absolute, state.online, state.busy); } }; - self.setKeymaps = function(state) { - let el = $("hid-pak-keymap-selector"); - tools.selector.setValues(el, state.keymaps.available); - tools.selector.setSelectedValue(el, tools.storage.get("hid.pak.keymap", state.keymaps["default"])); - }; - var __releaseAll = function() { __keyboard.releaseAll(); __mouse.releaseAll(); @@ -240,50 +217,6 @@ export function Hid(__getGeometry, __recorder) { }); }; - var __clickPasteAsKeysButton = function() { - let text = $("hid-pak-text").value; - if (text) { - let paste_as_keys = function() { - tools.el.setEnabled($("hid-pak-text"), false); - tools.el.setEnabled($("hid-pak-button"), false); - tools.el.setEnabled($("hid-pak-keymap-selector"), false); - - let keymap = $("hid-pak-keymap-selector").value; - - tools.debug(`HID: paste-as-keys ${keymap}: ${text}`); - - tools.httpPost("/api/hid/print", {"limit": 0, "keymap": keymap}, function(http) { - tools.el.setEnabled($("hid-pak-text"), true); - tools.el.setEnabled($("hid-pak-button"), true); - tools.el.setEnabled($("hid-pak-keymap-selector"), 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", http.responseText); - } else if (http.status === 200) { - __recorder.recordPrintEvent(text, keymap); - } - }, text, "text/plain"); - }; - - if ($("hid-pak-ask-switch").checked) { - wm.confirm(` - You're going to paste ${text.length} character${text.length ? "s" : ""}.
    - Are you sure you want to continue? - `).then(function(ok) { - if (ok) { - paste_as_keys(); - } else { - $("hid-pak-text").value = ""; - } - }); - } else { - paste_as_keys(); - } - } - }; - var __clickOutputsRadio = function(hid) { let output = tools.radio.getValue(`hid-outputs-${hid}-radio`); tools.httpPost("/api/hid/set_params", {[`${hid}_output`]: output}, function(http) { diff --git a/web/share/js/kvm/keyboard.js b/web/share/js/kvm/keyboard.js index 68fb2167..2377d07a 100644 --- a/web/share/js/kvm/keyboard.js +++ b/web/share/js/kvm/keyboard.js @@ -65,17 +65,17 @@ export function Keyboard(__recordWsEvent) { __updateOnlineLeds(); }; - self.setState = function(state, hid_online, hid_busy) { + self.setState = function(online, leds, hid_online, hid_busy) { if (!hid_online) { __online = null; } else { - __online = (state.online && !hid_busy); + __online = (online && !hid_busy); } __updateOnlineLeds(); for (let led of ["caps", "scroll", "num"]) { for (let el of $$$(`.hid-keyboard-${led}-led`)) { - if (state.leds[led]) { + if (leds[led]) { el.classList.add("led-green"); el.classList.remove("led-gray"); } else { diff --git a/web/share/js/kvm/mouse.js b/web/share/js/kvm/mouse.js index 5f75175c..3310d17e 100644 --- a/web/share/js/kvm/mouse.js +++ b/web/share/js/kvm/mouse.js @@ -90,20 +90,20 @@ export function Mouse(__getGeometry, __recordWsEvent) { __updateOnlineLeds(); }; - self.setState = function(state, hid_online, hid_busy) { + self.setState = function(online, absolute, hid_online, hid_busy) { if (!hid_online) { __online = null; } else { - __online = (state.online && !hid_busy); + __online = (online && !hid_busy); } - if (!__absolute && state.absolute && __isRelativeCaptured()) { + if (!__absolute && absolute && __isRelativeCaptured()) { document.exitPointerLock(); } - if (__absolute && !state.absolute) { + if (__absolute && !absolute) { __relative_deltas = []; __relative_touch_pos = null; } - __absolute = state.absolute; + __absolute = absolute; __updateOnlineLeds(); }; diff --git a/web/share/js/kvm/paste.js b/web/share/js/kvm/paste.js new file mode 100644 index 00000000..8b770122 --- /dev/null +++ b/web/share/js/kvm/paste.js @@ -0,0 +1,112 @@ +/***************************************************************************** +# # +# KVMD - The main PiKVM daemon. # +# # +# Copyright (C) 2018-2024 Maxim Devaev # +# # +# 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 . # +# # +*****************************************************************************/ + + +"use strict"; + + +import {tools, $} from "../tools.js"; +import {wm} from "../wm.js"; + + +export function Paste(__recorder) { + var self = this; + + /************************************************************************/ + + var __init__ = function() { + tools.storage.bindSimpleSwitch($("hid-pak-ask-switch"), "hid.pak.ask", true); + tools.storage.bindSimpleSwitch($("hid-pak-secure-switch"), "hid.pak.secure", false, function(value) { + $("hid-pak-text").style.setProperty("-webkit-text-security", (value ? "disc" : "none")); + }); + tools.feature.setEnabled($("hid-pak-secure"), ( + tools.browser.is_chrome + || tools.browser.is_safari + || tools.browser.is_opera + )); + + $("hid-pak-keymap-selector").addEventListener("change", function() { + tools.storage.set("hid.pak.keymap", $("hid-pak-keymap-selector").value); + }); + tools.el.setOnClick($("hid-pak-button"), __clickPasteAsKeysButton); + }; + + /************************************************************************/ + + self.setState = function(state) { + tools.el.setEnabled($("hid-pak-text"), state); + tools.el.setEnabled($("hid-pak-button"), state); + if (state) { + let el = $("hid-pak-keymap-selector"); + let sel = tools.storage.get("hid.pak.keymap", state.keymaps["default"]); + el.options.length = 0; + for (let keymap of state.keymaps.available) { + tools.selector.addOption(el, keymap, keymap, (keymap === sel)); + } + } + }; + + var __clickPasteAsKeysButton = function() { + let text = $("hid-pak-text").value; + if (text) { + let paste_as_keys = function() { + tools.el.setEnabled($("hid-pak-text"), false); + tools.el.setEnabled($("hid-pak-button"), false); + tools.el.setEnabled($("hid-pak-keymap-selector"), false); + + let keymap = $("hid-pak-keymap-selector").value; + + tools.debug(`HID: paste-as-keys ${keymap}: ${text}`); + + tools.httpPost("/api/hid/print", {"limit": 0, "keymap": keymap}, function(http) { + tools.el.setEnabled($("hid-pak-text"), true); + tools.el.setEnabled($("hid-pak-button"), true); + tools.el.setEnabled($("hid-pak-keymap-selector"), 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", http.responseText); + } else if (http.status === 200) { + __recorder.recordPrintEvent(text, keymap); + } + }, text, "text/plain"); + }; + + if ($("hid-pak-ask-switch").checked) { + wm.confirm(` + You're going to paste ${text.length} character${text.length ? "s" : ""}.
    + Are you sure you want to continue? + `).then(function(ok) { + if (ok) { + paste_as_keys(); + } else { + $("hid-pak-text").value = ""; + } + }); + } else { + paste_as_keys(); + } + } + }; + + __init__(); +} diff --git a/web/share/js/kvm/session.js b/web/share/js/kvm/session.js index 523b4097..a97b1a3b 100644 --- a/web/share/js/kvm/session.js +++ b/web/share/js/kvm/session.js @@ -28,6 +28,7 @@ import {wm} from "../wm.js"; import {Recorder} from "./recorder.js"; import {Hid} from "./hid.js"; +import {Paste} from "./paste.js"; import {Atx} from "./atx.js"; import {Msd} from "./msd.js"; import {Streamer} from "./stream.js"; @@ -48,6 +49,7 @@ export function Session() { var __streamer = new Streamer(); var __recorder = new Recorder(); var __hid = new Hid(__streamer.getGeometry, __recorder); + var __paste = new Paste(__recorder); var __atx = new Atx(__recorder); var __msd = new Msd(); var __gpio = new Gpio(__recorder); @@ -363,8 +365,8 @@ export function Session() { case "pong": __missed_heartbeats = 0; 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; + case "hid_keymaps_state": __paste.setState(data.event); break; case "atx_state": __atx.setState(data.event); break; case "msd_state": __msd.setState(data.event); break; case "streamer_state": __streamer.setState(data.event); break; @@ -395,6 +397,8 @@ export function Session() { __gpio.setState(null); __hid.setSocket(null); __recorder.setSocket(null); + + __paste.setState(null); __atx.setState(null); __msd.setState(null); __streamer.setState(null); diff --git a/web/share/js/tools.js b/web/share/js/tools.js index 97eb0abf..046813c6 100644 --- a/web/share/js/tools.js +++ b/web/share/js/tools.js @@ -317,30 +317,6 @@ export var tools = new function() { } return false; }, - - "setValues": function(el, values, empty_title=null) { - if (values.constructor == Object) { - values = Object.keys(values).sort(); - } - let values_json = JSON.stringify(values); - if (el.__values_json !== values_json) { - el.options.length = 0; - for (let value of values) { - let title = value; - if (title.length === 0 && empty_title !== null) { - title = empty_title; - } - self.selector.addOption(el, title, value); - } - el.__values_json = values_json; - el.__values = values; - } - }, - "setSelectedValue": function(el, value) { - if (el.__values && el.__values.includes(value)) { - el.value = value; - } - }, }; }; From 28167c4b45dc2e6d7d1003cfc1c645b43ba194ea Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sat, 2 Nov 2024 18:48:14 +0200 Subject: [PATCH 81/88] fixed ocr null event handling --- web/share/js/kvm/ocr.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/share/js/kvm/ocr.js b/web/share/js/kvm/ocr.js index 5f44791a..8dab2741 100644 --- a/web/share/js/kvm/ocr.js +++ b/web/share/js/kvm/ocr.js @@ -73,8 +73,12 @@ export function Ocr(__getGeometry) { /************************************************************************/ self.setState = function(state) { - if (state.enabled !== undefined) { - __enabled = (state.enabled && !tools.browser.is_mobile); + if (state) { + if (state.enabled !== undefined) { + __enabled = (state.enabled && !tools.browser.is_mobile); + } + } else { + __enabled = false; } if (__enabled) { let el = $("stream-ocr-lang-selector"); From 95597b15e45fc38c52613d4cca4ec6309f8a2c82 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sat, 2 Nov 2024 20:03:00 +0200 Subject: [PATCH 82/88] fix --- web/share/js/kvm/session.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/web/share/js/kvm/session.js b/web/share/js/kvm/session.js index a97b1a3b..d3bd7f94 100644 --- a/web/share/js/kvm/session.js +++ b/web/share/js/kvm/session.js @@ -393,15 +393,14 @@ export function Session() { __ping_timer = null; } - __ocr.setState(null); __gpio.setState(null); - __hid.setSocket(null); - __recorder.setSocket(null); - + __hid.setSocket(null); // auto setState(null); __paste.setState(null); __atx.setState(null); __msd.setState(null); __streamer.setState(null); + __ocr.setState(null); + __recorder.setSocket(null); __ws = null; setTimeout(function() { From 1e277c0f06f25e67a35a1b2910ed59333f97f6c0 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sat, 2 Nov 2024 21:04:57 +0200 Subject: [PATCH 83/88] lint fix --- testenv/Dockerfile | 1 - testenv/linters/pylint.ini | 1 + testenv/requirements.txt | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/testenv/Dockerfile b/testenv/Dockerfile index e8415828..67fcd748 100644 --- a/testenv/Dockerfile +++ b/testenv/Dockerfile @@ -44,7 +44,6 @@ RUN pacman --noconfirm --ask=4 -Syy \ python-aiohttp \ python-aiofiles \ python-async-lru \ - python-periphery \ python-passlib \ python-pyotp \ python-qrcode \ diff --git a/testenv/linters/pylint.ini b/testenv/linters/pylint.ini index 978a2e43..fd2d8b35 100644 --- a/testenv/linters/pylint.ini +++ b/testenv/linters/pylint.ini @@ -38,6 +38,7 @@ disable = unspecified-encoding, consider-using-f-string, unnecessary-lambda-assignment, + too-many-positional-arguments, # https://github.com/PyCQA/pylint/issues/3882 [CLASSES] diff --git a/testenv/requirements.txt b/testenv/requirements.txt index 3f848431..36d2407a 100644 --- a/testenv/requirements.txt +++ b/testenv/requirements.txt @@ -1,3 +1,4 @@ +python-periphery pyserial-asyncio pyghmi spidev From d93639ba8dcd003f82221f4ed99159755f2213c3 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sun, 3 Nov 2024 18:28:28 +0200 Subject: [PATCH 84/88] hid with granularity prototype --- kvmd/apps/vnc/server.py | 6 +- kvmd/plugins/hid/__init__.py | 13 ++ kvmd/plugins/hid/_mcu/__init__.py | 1 + kvmd/plugins/hid/bt/__init__.py | 1 + kvmd/plugins/hid/ch9329/__init__.py | 1 + kvmd/plugins/hid/otg/__init__.py | 1 + web/kvm/index.html | 32 +++-- web/kvm/navbar-system.pug | 17 ++- web/share/js/kvm/hid.js | 182 ++++++++++++++++++---------- 9 files changed, 164 insertions(+), 90 deletions(-) diff --git a/kvmd/apps/vnc/server.py b/kvmd/apps/vnc/server.py index c14bb21f..e8524a38 100644 --- a/kvmd/apps/vnc/server.py +++ b/kvmd/apps/vnc/server.py @@ -189,7 +189,11 @@ class _Client(RfbClient): # pylint: disable=too-many-instance-attributes self.__shared_params.name = name elif event_type == "hid_state": - if self._encodings.has_leds_state: + if ( + self._encodings.has_leds_state + and ("keyboard" in event) + and ("leds" in event["keyboard"]) + ): await self._send_leds_state(**event["keyboard"]["leds"]) # ===== diff --git a/kvmd/plugins/hid/__init__.py b/kvmd/plugins/hid/__init__.py index f7debe1d..73ff5d04 100644 --- a/kvmd/plugins/hid/__init__.py +++ b/kvmd/plugins/hid/__init__.py @@ -101,6 +101,19 @@ class BaseHid(BasePlugin): # pylint: disable=too-many-instance-attributes raise NotImplementedError async def poll_state(self) -> AsyncGenerator[dict, None]: + # ==== Granularity table ==== + # - enabled -- Full + # - online -- Partial + # - busy -- Partial + # - connected -- Partial, nullable + # - keyboard.online -- Partial + # - keyboard.outputs -- Partial + # - keyboard.leds -- Partial + # - mouse.online -- Partial + # - mouse.outputs -- Partial, follows with absolute + # - mouse.absolute -- Partial, follows with outputs + # =========================== + yield {} raise NotImplementedError diff --git a/kvmd/plugins/hid/_mcu/__init__.py b/kvmd/plugins/hid/_mcu/__init__.py index a4c903b7..d6f04f76 100644 --- a/kvmd/plugins/hid/_mcu/__init__.py +++ b/kvmd/plugins/hid/_mcu/__init__.py @@ -217,6 +217,7 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many- mouse_outputs["active"] = active_mouse return { + "enabled": True, "online": online, "busy": bool(state["busy"]), "connected": (bool(outputs2 & 0b01000000) if outputs2 & 0b10000000 else None), diff --git a/kvmd/plugins/hid/bt/__init__.py b/kvmd/plugins/hid/bt/__init__.py index 4d0effb5..0c95a6d5 100644 --- a/kvmd/plugins/hid/bt/__init__.py +++ b/kvmd/plugins/hid/bt/__init__.py @@ -141,6 +141,7 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes state = await self.__server.get_state() outputs: dict = {"available": [], "active": ""} return { + "enabled": True, "online": True, "busy": False, "connected": None, diff --git a/kvmd/plugins/hid/ch9329/__init__.py b/kvmd/plugins/hid/ch9329/__init__.py index c5d10688..1b235090 100644 --- a/kvmd/plugins/hid/ch9329/__init__.py +++ b/kvmd/plugins/hid/ch9329/__init__.py @@ -104,6 +104,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst absolute = self.__mouse.is_absolute() leds = await self.__keyboard.get_leds() return { + "enabled": True, "online": state["online"], "busy": False, "connected": None, diff --git a/kvmd/plugins/hid/otg/__init__.py b/kvmd/plugins/hid/otg/__init__.py index c95fb7fb..25424257 100644 --- a/kvmd/plugins/hid/otg/__init__.py +++ b/kvmd/plugins/hid/otg/__init__.py @@ -134,6 +134,7 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes keyboard_state = await self.__keyboard_proc.get_state() mouse_state = await self.__mouse_current.get_state() return { + "enabled": True, "online": True, "busy": False, "connected": None, diff --git a/web/kvm/index.html b/web/kvm/index.html index d2f264df..81eb9753 100644 --- a/web/kvm/index.html +++ b/web/kvm/index.html @@ -256,23 +256,21 @@
    -
    -
    - - - - - - - - - -
    Keyboard mode: -
    -
    Mouse mode: -
    -
    -
    +
    + + + + + + + + + +
    Keyboard mode: +
    +
    Mouse mode: +
    +
    Keyboard & Mouse (HID) settings
    diff --git a/web/kvm/navbar-system.pug b/web/kvm/navbar-system.pug index d10dbee0..6e2a33d2 100644 --- a/web/kvm/navbar-system.pug +++ b/web/kvm/navbar-system.pug @@ -71,15 +71,14 @@ li(id="system-dropdown" class="right") button(data-force-hide-menu data-show-window="stream-window" class="row33") • Show stream button(data-force-hide-menu id="stream-screenshot-button" class="row33") • Screenshot button(id="stream-reset-button" class="row33") Reset stream - div(id="hid-outputs" class="feature-disabled") - hr - table(class="kv") - tr(id="hid-outputs-keyboard", class="feature-disabled") - td Keyboard mode: - td #[div(id="hid-outputs-keyboard-box" class="radio-box")] - tr(id="hid-outputs-mouse", class="feature-disabled") - td Mouse #[a(target="_blank" href="https://docs.pikvm.org/mouse") mode]: - td #[div(id="hid-outputs-mouse-box" class="radio-box")] + hr + table(class="kv") + tr(id="hid-outputs-keyboard", class="feature-disabled") + td Keyboard mode: + td #[div(id="hid-outputs-keyboard-box" class="radio-box")] + tr(id="hid-outputs-mouse", class="feature-disabled") + td Mouse #[a(target="_blank" href="https://docs.pikvm.org/mouse") mode]: + td #[div(id="hid-outputs-mouse-box" class="radio-box")] details summary Keyboard & Mouse (HID) settings div(class="spoiler") diff --git a/web/share/js/kvm/hid.js b/web/share/js/kvm/hid.js index e7aa29e6..65bd480d 100644 --- a/web/share/js/kvm/hid.js +++ b/web/share/js/kvm/hid.js @@ -35,6 +35,7 @@ export function Hid(__getGeometry, __recorder) { /************************************************************************/ + var __state = null; var __keyboard = null; var __mouse = null; @@ -102,8 +103,6 @@ export function Hid(__getGeometry, __recorder) { /************************************************************************/ self.setSocket = function(ws) { - tools.el.setEnabled($("hid-reset-button"), ws); - tools.el.setEnabled($("hid-jiggler-switch"), ws); if (!ws) { self.setState(null); } @@ -112,78 +111,135 @@ export function Hid(__getGeometry, __recorder) { }; self.setState = function(state) { - let has_relative_squash = false; - if (state) { - tools.feature.setEnabled($("hid-jiggler"), state.jiggler.enabled); - $("hid-jiggler-switch").checked = state.jiggler.active; + if (!__state) { + __state = {"keyboard": {}, "mouse": {}}; + } + if (state.enabled !== undefined) { + __state.enabled = state.enabled; // Currently unused, always true + } + if (__state.enabled !== undefined) { + for (let key of ["online", "busy", "connected", "jiggler"]) { + if (state[key] !== undefined) { + __state[key] = state[key]; + } + } + for (let hid of ["keyboard", "mouse"]) { + if (state[hid] === undefined) { + state[hid] = {}; // Add some stubs for processing + } + for (let key of ["online", "outputs", (hid === "keyboard" ? "leds" : "absolute")]) { + __state[hid][key] = state[hid][key]; + } + } + if (state.connected !== undefined) { + tools.feature.setEnabled($("hid-connect"), (__state.connected !== null)); + $("hid-connect-switch").checked = !!__state.connected; + } + if (state.jiggler !== undefined) { + tools.feature.setEnabled($("hid-jiggler"), __state.jiggler.enabled); + $("hid-jiggler-switch").checked = __state.jiggler.active; + } + if (state.keyboard.outputs !== undefined) { + __updateKeyboardOutputs(__state.keyboard.outputs); + } + if (state.mouse.outputs !== undefined) { + __updateMouseOutputs(__state.mouse.outputs, __state.mouse.absolute); // Follows together + } + if ( + state.keyboard.online !== undefined || state.keyboard.leds !== undefined + || state.online !== undefined || state.busy !== undefined + ) { + __keyboard.setState(__state.keyboard.online, __state.keyboard.leds, __state.online, __state.busy); + } + if ( + state.mouse.online !== undefined || state.mouse.absolute !== undefined + || state.online !== undefined || state.busy !== undefined + ) { + __mouse.setState(__state.mouse.online, __state.mouse.absolute, __state.online, __state.busy); + } + if (state.online !== undefined || state.busy !== undefined) { + tools.radio.setEnabled("hid-outputs-keyboard-radio", (__state.online && !__state.busy)); + tools.radio.setEnabled("hid-outputs-mouse-radio", (__state.online && !__state.busy)); + tools.el.setEnabled($("hid-connect-switch"), (__state.online && !__state.busy)); + } + } + } else { + __state = null; + tools.radio.setEnabled("hid-outputs-keyboard-radio", false); + tools.radio.setEnabled("hid-outputs-mouse-radio", false); + tools.el.setEnabled($("hid-connect-switch"), false); + tools.el.setEnabled($("hid-mouse-squash-switch"), false); + tools.el.setEnabled($("hid-mouse-sens-slider"), false); } - if (state && state.online) { - let keyboard_outputs = state.keyboard.outputs.available; - let mouse_outputs = state.mouse.outputs.available; - if (keyboard_outputs.length) { - if ($("hid-outputs-keyboard-box").outputs !== keyboard_outputs) { - let html = ""; - for (let args of [ - ["USB", "usb"], - ["PS/2", "ps2"], - ["Off", "disabled"], - ]) { - if (keyboard_outputs.includes(args[1])) { - html += tools.radio.makeItem("hid-outputs-keyboard-radio", args[0], args[1]); - } + tools.el.setEnabled($("hid-reset-button"), __state); + tools.el.setEnabled($("hid-jiggler-switch"), __state); + }; + + var __updateKeyboardOutputs = function(outputs) { + let avail = outputs.available; + if (avail.length > 0) { + let el = $("hid-outputs-keyboard-box"); + let avail_json = JSON.stringify(avail); + if (el.__avail_json !== avail_json) { + let html = ""; + for (let pair of [ + ["USB", "usb"], + ["PS/2", "ps2"], + ["Off", "disabled"], + ]) { + if (avail.includes(pair[1])) { + html += tools.radio.makeItem("hid-outputs-keyboard-radio", pair[0], pair[1]); } - $("hid-outputs-keyboard-box").innerHTML = html; - $("hid-outputs-keyboard-box").outputs = keyboard_outputs; - tools.radio.setOnClick("hid-outputs-keyboard-radio", () => __clickOutputsRadio("keyboard")); } - tools.radio.setValue("hid-outputs-keyboard-radio", state.keyboard.outputs.active); + el.innerHTML = html; + tools.radio.setOnClick("hid-outputs-keyboard-radio", () => __clickOutputsRadio("keyboard")); + el.__avail_json = avail_json; } - let has_relative = false; - if (mouse_outputs.length) { - if ($("hid-outputs-mouse-box").outputs !== mouse_outputs) { - let html = ""; - for (let args of [ - ["Absolute", "usb", false], - ["Abs-Win98", "usb_win98", false], - ["Relative", "usb_rel", true], - ["PS/2", "ps2", true], - ["Off", "disabled"], - ]) { - if (mouse_outputs.includes(args[1])) { - html += tools.radio.makeItem("hid-outputs-mouse-radio", args[0], args[1]); - has_relative = (has_relative || args[2]); - } + tools.radio.setValue("hid-outputs-keyboard-radio", outputs.active); + } + tools.feature.setEnabled($("hid-outputs-keyboard"), (avail.length > 0)); + }; + + var __updateMouseOutputs = function(outputs, absolute) { + let has_relative = null; + let has_relative_squash = null; + let avail = outputs.available; + if (avail.length > 0) { + let el = $("hid-outputs-mouse-box"); + let avail_json = JSON.stringify(avail); + if (el.__avail_json !== avail_json) { + has_relative = false; + let html = ""; + for (let pair of [ + ["Absolute", "usb", false], + ["Abs-Win98", "usb_win98", false], + ["Relative", "usb_rel", true], + ["PS/2", "ps2", true], + ["Off", "disabled", false], + ]) { + if (avail.includes(pair[1])) { + html += tools.radio.makeItem("hid-outputs-mouse-radio", pair[0], pair[1]); + has_relative = (has_relative || pair[2]); } - $("hid-outputs-mouse-box").innerHTML = html; - $("hid-outputs-mouse-box").outputs = mouse_outputs; - tools.radio.setOnClick("hid-outputs-mouse-radio", () => __clickOutputsRadio("mouse")); } - tools.radio.setValue("hid-outputs-mouse-radio", state.mouse.outputs.active); - has_relative_squash = ["usb_rel", "ps2"].includes(state.mouse.outputs.active); - } else { - has_relative = !state.mouse.absolute; - has_relative_squash = has_relative; + el.innerHTML = html; + tools.radio.setOnClick("hid-outputs-mouse-radio", () => __clickOutputsRadio("mouse")); + el.__avail_json = avail_json; } - tools.feature.setEnabled($("hid-outputs"), (keyboard_outputs.length || mouse_outputs.length)); - tools.feature.setEnabled($("hid-outputs-keyboard"), keyboard_outputs.length); - tools.feature.setEnabled($("hid-outputs-mouse"), mouse_outputs.length); + tools.radio.setValue("hid-outputs-mouse-radio", outputs.active); + has_relative_squash = (["usb_rel", "ps2"].includes(outputs.active)); + } else { + has_relative = !absolute; + has_relative_squash = has_relative; + } + if (has_relative !== null) { tools.feature.setEnabled($("hid-mouse-squash"), has_relative); tools.feature.setEnabled($("hid-mouse-sens"), has_relative); - tools.feature.setEnabled($("hid-connect"), (state.connected !== null)); - $("hid-connect-switch").checked = !!state.connected; - } - - tools.radio.setEnabled("hid-outputs-keyboard-radio", (state && state.online && !state.busy)); - tools.radio.setEnabled("hid-outputs-mouse-radio", (state && state.online && !state.busy)); - tools.el.setEnabled($("hid-mouse-squash-switch"), (has_relative_squash && !state.busy)); - tools.el.setEnabled($("hid-mouse-sens-slider"), (has_relative_squash && !state.busy)); - tools.el.setEnabled($("hid-connect-switch"), (state && state.online && !state.busy)); - - if (state) { - __keyboard.setState(state.keyboard.online, state.keyboard.leds, state.online, state.busy); - __mouse.setState(state.mouse.online, state.mouse.absolute, state.online, state.busy); } + tools.feature.setEnabled($("hid-outputs-mouse"), (avail.length > 0)); + tools.el.setEnabled($("hid-mouse-squash-switch"), has_relative_squash); + tools.el.setEnabled($("hid-mouse-sens-slider"), has_relative_squash); }; var __releaseAll = function() { From 7ef2e16b51eab24897cc6447c826a5a1c2b8efed Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Mon, 4 Nov 2024 18:06:16 +0200 Subject: [PATCH 85/88] minor partial state fixes --- web/kvm/index.html | 2 +- web/kvm/navbar-system.pug | 2 +- web/share/js/kvm/atx.js | 15 ++++++---- web/share/js/kvm/gpio.js | 52 ++++++++++++++++++---------------- web/share/js/kvm/msd.js | 57 ++++++++++++++++++-------------------- web/share/js/kvm/ocr.js | 24 ++++++++++------ web/share/js/kvm/stream.js | 8 +++--- 7 files changed, 85 insertions(+), 75 deletions(-) diff --git a/web/kvm/index.html b/web/kvm/index.html index 81eb9753..960535f2 100644 --- a/web/kvm/index.html +++ b/web/kvm/index.html @@ -394,7 +394,7 @@ Connect main USB to Server:
    - +
    diff --git a/web/kvm/navbar-system.pug b/web/kvm/navbar-system.pug index 6e2a33d2..484bed86 100644 --- a/web/kvm/navbar-system.pug +++ b/web/kvm/navbar-system.pug @@ -122,7 +122,7 @@ li(id="system-dropdown" class="right") +menu_switch_notable("hid-mute-switch", "Mute HID input events", true, false) tr(id="v3-usb-breaker" class="feature-disabled") +menu_switch_notable_gpio("__v3_usb_breaker__", "Connect main USB to Server", - "Turning off this switch will disconnect the main USB
    from the server. Are you sure you want to continue?") + "Turning off this switch will disconnect the main USB from the server. Are you sure you want to continue?") tr(id="v4-locator" class="feature-disabled") +menu_switch_notable_gpio("__v4_locator__", "Enable locator LED") tr diff --git a/web/share/js/kvm/atx.js b/web/share/js/kvm/atx.js index 4d9764ea..796a4eeb 100644 --- a/web/share/js/kvm/atx.js +++ b/web/share/js/kvm/atx.js @@ -53,30 +53,33 @@ export function Atx(__recorder) { __state = {"leds": {}}; } if (state.enabled !== undefined) { - tools.feature.setEnabled($("atx-dropdown"), state.enabled); __state.enabled = state.enabled; + tools.feature.setEnabled($("atx-dropdown"), __state.enabled); } if (__state.enabled !== undefined) { if (state.busy !== undefined) { - __updateButtons(!state.busy); __state.busy = state.busy; + __updateButtons(!__state.busy); } if (state.leds !== undefined) { __state.leds = state.leds; } if (state.busy !== undefined || state.leds !== undefined) { - let busy = __state.busy; - let leds = __state.leds; - $("atx-power-led").className = (busy ? "led-yellow" : (leds.power ? "led-green" : "led-gray")); - $("atx-hdd-led").className = (leds.hdd ? "led-red" : "led-gray"); + __updateLeds(__state.leds.power, __state.leds.hdd, __state.busy); } } } else { __state = null; + __updateLeds(false, false, false); __updateButtons(false); } }; + var __updateLeds = function(power, hdd, busy) { + $("atx-power-led").className = (busy ? "led-yellow" : (power ? "led-green" : "led-gray")); + $("atx-hdd-led").className = (hdd ? "led-red" : "led-gray"); + }; + var __updateButtons = function(enabled) { for (let id of ["atx-power-button", "atx-power-button-long", "atx-reset-button"]) { tools.el.setEnabled($(id), enabled); diff --git a/web/share/js/kvm/gpio.js b/web/share/js/kvm/gpio.js index 9e237a54..41d5ee92 100644 --- a/web/share/js/kvm/gpio.js +++ b/web/share/js/kvm/gpio.js @@ -38,14 +38,20 @@ export function Gpio(__recorder) { self.setState = function(state) { if (state) { - if (state.model) { - __applyModel(state.model); + if (state.model !== undefined) { __has_model = true; + __updateModel(state.model); } - if (__has_model && state.state) { - __applyState(state.state); + if (__has_model && state.state !== undefined) { + if (state.state.inputs !== undefined) { + __updateInputs(state.state.inputs); + } + if (state.state.outputs !== undefined) { + __updateOutputs(state.state.outputs); + } } } else { + __has_model = false; for (let el of $$("__gpio-led")) { __setLedState(el, false); } @@ -54,33 +60,31 @@ export function Gpio(__recorder) { tools.el.setEnabled(el, false); } } - __has_model = false; } }; - var __applyState = function(state) { - if (state.inputs) { - for (let ch in state.inputs) { - for (let el of $$(`__gpio-led-${ch}`)) { - __setLedState(el, state.inputs[ch].state); - } - } - } - if (state.outputs) { - for (let ch in state.outputs) { - for (let type of ["switch", "button"]) { - for (let el of $$(`__gpio-${type}-${ch}`)) { - tools.el.setEnabled(el, state.outputs[ch].online && !state.outputs[ch].busy); - } - } - for (let el of $$(`__gpio-switch-${ch}`)) { - el.checked = state.outputs[ch].state; - } + var __updateInputs = function(inputs) { + for (let ch in inputs) { + for (let el of $$(`__gpio-led-${ch}`)) { + __setLedState(el, inputs[ch].state); } } }; - var __applyModel = function(model) { + var __updateOutputs = function(outputs) { + for (let ch in outputs) { + for (let type of ["switch", "button"]) { + for (let el of $$(`__gpio-${type}-${ch}`)) { + tools.el.setEnabled(el, (outputs[ch].online && !outputs[ch].busy)); + } + } + for (let el of $$(`__gpio-switch-${ch}`)) { + el.checked = outputs[ch].state; + } + } + }; + + var __updateModel = function(model) { tools.feature.setEnabled($("gpio-dropdown"), model.view.table.length); if (model.view.table.length) { let title = []; diff --git a/web/share/js/kvm/msd.js b/web/share/js/kvm/msd.js index 3a558b00..c20bd70a 100644 --- a/web/share/js/kvm/msd.js +++ b/web/share/js/kvm/msd.js @@ -66,12 +66,11 @@ export function Msd() { self.setState = function(state) { if (state) { if (!__state) { - __state = {}; - __state.storage = {}; + __state = {"storage": {}}; } if (state.enabled !== undefined) { - tools.feature.setEnabled($("msd-dropdown"), state.enabled); __state.enabled = state.enabled; + tools.feature.setEnabled($("msd-dropdown"), __state.enabled); } if (__state.enabled !== undefined) { if (state.online !== undefined) { @@ -80,31 +79,27 @@ export function Msd() { if (state.busy !== undefined) { __state.busy = state.busy; } - if (state.drive !== undefined || (state.storage && state.storage.images !== undefined)) { - let drive = (state.drive !== undefined ? state.drive : __state.drive); - let images = ( - state.storage && state.storage.images !== undefined - ? state.storage.images - : __state.storage && __state.storage.images !== undefined - ? __state.storage.images - : null - ); - if (drive && images) { - __updateImageSelector(drive, images); + if (state.drive) { // Null on offline, ignore + __state.drive = state.drive; + } + if (state.storage) { // Null on offline, ignore + if (state.storage.parts !== undefined) { + __state.storage.parts = state.storage.parts; + __updateParts(__state.storage.parts); + } + if (state.storage.uploading !== undefined) { + __state.storage.uploading = state.storage.uploading; + __updateUploading(__state.storage.uploading); + } + if (state.storage.downloading !== undefined) { + __state.storage.downloading = state.storage.downloading; + } + if (state.storage.images !== undefined) { + __state.storage.images = state.storage.images; } - __state.drive = drive; - __state.storage.images = images; } - if (state.storage && state.storage.parts !== undefined) { - __updateParts(state.storage.parts); - __state.storage.parts = state.storage.parts; - } - if (state.storage && state.storage.uploading !== undefined) { - __updateUploading(state.storage.uploading); - __state.storage.uploading = state.storage.uploading; - } - if (state.storage && state.storage.downloading !== undefined) { - __state.storage.downloading = state.storage.downloading; + if (state.drive || (state.storage && state.storage.images !== undefined)) { + __updateImageSelector(__state.drive, __state.storage.images); } } } else { @@ -403,10 +398,12 @@ export function Msd() { let file = tools.input.getFile(el); if (file) { $("msd-new-url").value = ""; - let part = __state.storage.parts[$("msd-new-part-selector").value]; - if (file.size > part.size) { - wm.error(`The new image is too big for the Mass Storage partition.
    Maximum: ${tools.formatSize(part.size)}`); - el.value = ""; + if (__state && __state.storage && __state.storage.parts) { + let part = __state.storage.parts[$("msd-new-part-selector").value]; + if (part && (file.size > part.size)) { + wm.error(`The new image is too big for the Mass Storage partition.
    Maximum: ${tools.formatSize(part.size)}`); + el.value = ""; + } } } __refreshControls(); diff --git a/web/share/js/kvm/ocr.js b/web/share/js/kvm/ocr.js index 8dab2741..87ef58d8 100644 --- a/web/share/js/kvm/ocr.js +++ b/web/share/js/kvm/ocr.js @@ -76,20 +76,26 @@ export function Ocr(__getGeometry) { if (state) { if (state.enabled !== undefined) { __enabled = (state.enabled && !tools.browser.is_mobile); + tools.feature.setEnabled($("stream-ocr"), __enabled); + $("stream-ocr-led").className = (__enabled ? "led-gray" : "hidden"); + } + if (__enabled && state.langs !== undefined) { + __updateLangs(state.langs); } } else { __enabled = false; + tools.feature.setEnabled($("stream-ocr"), false); + $("stream-ocr-led").className = "hidden"; } - if (__enabled) { - let el = $("stream-ocr-lang-selector"); - el.options.length = 0; - for (let lang of state.langs.available) { - tools.selector.addOption(el, lang, lang); - } - el.value = tools.storage.get("stream.ocr.lang", state.langs["default"]); + }; + + var __updateLangs = function(langs) { + let el = $("stream-ocr-lang-selector"); + el.options.length = 0; + for (let lang of langs.available) { + tools.selector.addOption(el, lang, lang); } - tools.feature.setEnabled($("stream-ocr"), __enabled); - $("stream-ocr-led").className = (__enabled ? "led-gray" : "hidden"); + el.value = tools.storage.get("stream.ocr.lang", langs["default"]); }; var __startSelection = function(event) { diff --git a/web/share/js/kvm/stream.js b/web/share/js/kvm/stream.js index 504c7086..b436c093 100644 --- a/web/share/js/kvm/stream.js +++ b/web/share/js/kvm/stream.js @@ -138,17 +138,17 @@ export function Streamer() { if (!__state) { __state = {}; } - if (state.features) { + if (state.features !== undefined) { __state.features = state.features; __state.limits = state.limits; // Following together with features } - if (__state.features && state.streamer !== undefined) { - __setControlsEnabled(!!state.streamer); + if (__state.features !== undefined && state.streamer !== undefined) { __state.streamer = state.streamer; + __setControlsEnabled(!!state.streamer); } } else { - __setControlsEnabled(false); __state = null; + __setControlsEnabled(false); } let visible = wm.isWindowVisible($("stream-window")); __applyState((visible && __state && __state.features) ? state : null); From 0010dd1d114d5c03b7397394b8e12dee5908669d Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Mon, 4 Nov 2024 18:59:50 +0200 Subject: [PATCH 86/88] pikvm/pikvm#1420: VNC: Ignore CUT event 3 seconds after connection --- kvmd/apps/__init__.py | 7 ++++--- kvmd/apps/vnc/__init__.py | 1 + kvmd/apps/vnc/rfb/__init__.py | 13 ++++++++++++- kvmd/apps/vnc/server.py | 4 ++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/kvmd/apps/__init__.py b/kvmd/apps/__init__.py index 9d6d494a..cfc39499 100644 --- a/kvmd/apps/__init__.py +++ b/kvmd/apps/__init__.py @@ -667,9 +667,10 @@ def _get_config_scheme() -> dict: }, "vnc": { - "desired_fps": Option(30, type=valid_stream_fps), - "mouse_output": Option("usb", type=valid_hid_mouse_output), - "keymap": Option("/usr/share/kvmd/keymaps/en-us", type=valid_abs_file), + "desired_fps": Option(30, type=valid_stream_fps), + "mouse_output": Option("usb", type=valid_hid_mouse_output), + "keymap": Option("/usr/share/kvmd/keymaps/en-us", type=valid_abs_file), + "allow_cut_after": Option(3.0, type=valid_float_f0), "server": { "host": Option("", type=valid_ip_or_host, if_empty=""), diff --git a/kvmd/apps/vnc/__init__.py b/kvmd/apps/vnc/__init__.py index f41989d1..1e2c486a 100644 --- a/kvmd/apps/vnc/__init__.py +++ b/kvmd/apps/vnc/__init__.py @@ -71,6 +71,7 @@ def main(argv: (list[str] | None)=None) -> None: desired_fps=config.desired_fps, mouse_output=config.mouse_output, keymap_path=config.keymap, + allow_cut_after=config.allow_cut_after, kvmd=KvmdClient(user_agent=user_agent, **config.kvmd._unpack()), streamers=streamers, diff --git a/kvmd/apps/vnc/rfb/__init__.py b/kvmd/apps/vnc/rfb/__init__.py index c145b4b3..d2c6b50b 100644 --- a/kvmd/apps/vnc/rfb/__init__.py +++ b/kvmd/apps/vnc/rfb/__init__.py @@ -22,6 +22,7 @@ import asyncio import ssl +import time from typing import Callable from typing import Coroutine @@ -64,6 +65,7 @@ class RfbClient(RfbClientStream): # pylint: disable=too-many-instance-attribute width: int, height: int, name: str, + allow_cut_after: float, vnc_passwds: list[str], vencrypt: bool, none_auth_only: bool, @@ -79,6 +81,7 @@ class RfbClient(RfbClientStream): # pylint: disable=too-many-instance-attribute self._width = width self._height = height self.__name = name + self.__allow_cut_after = allow_cut_after self.__vnc_passwds = vnc_passwds self.__vencrypt = vencrypt self.__none_auth_only = none_auth_only @@ -90,6 +93,8 @@ class RfbClient(RfbClientStream): # pylint: disable=too-many-instance-attribute self.__fb_cont_updates = False self.__fb_reset_h264 = False + self.__allow_cut_since_ts = 0.0 + self.__lock = asyncio.Lock() # ===== @@ -414,6 +419,7 @@ class RfbClient(RfbClientStream): # pylint: disable=too-many-instance-attribute # ===== async def __main_loop(self) -> None: + self.__allow_cut_since_ts = time.monotonic() + self.__allow_cut_after handlers = { 0: self.__handle_set_pixel_format, 2: self.__handle_set_encodings, @@ -499,7 +505,12 @@ class RfbClient(RfbClientStream): # pylint: disable=too-many-instance-attribute async def __handle_client_cut_text(self) -> None: length = (await self._read_struct("cut text length", "xxx L"))[0] text = await self._read_text("cut text data", length) - await self._on_cut_event(text) + if self.__allow_cut_since_ts > 0 and time.monotonic() >= self.__allow_cut_since_ts: + # We should ignore cut event a few seconds after handshake + # because bVNC, AVNC and maybe some other clients perform + # it right after the connection automatically. + # - https://github.com/pikvm/pikvm/issues/1420 + await self._on_cut_event(text) async def __handle_enable_cont_updates(self) -> None: enabled = bool((await self._read_struct("enabled ContUpdates", "B HH HH"))[0]) diff --git a/kvmd/apps/vnc/server.py b/kvmd/apps/vnc/server.py index e8524a38..5abca7b0 100644 --- a/kvmd/apps/vnc/server.py +++ b/kvmd/apps/vnc/server.py @@ -81,6 +81,7 @@ class _Client(RfbClient): # pylint: disable=too-many-instance-attributes mouse_output: str, keymap_name: str, symmap: dict[int, dict[int, str]], + allow_cut_after: float, kvmd: KvmdClient, streamers: list[BaseStreamerClient], @@ -100,6 +101,7 @@ class _Client(RfbClient): # pylint: disable=too-many-instance-attributes tls_timeout=tls_timeout, x509_cert_path=x509_cert_path, x509_key_path=x509_key_path, + allow_cut_after=allow_cut_after, vnc_passwds=list(vnc_credentials), vencrypt=vencrypt, none_auth_only=none_auth_only, @@ -444,6 +446,7 @@ class VncServer: # pylint: disable=too-many-instance-attributes desired_fps: int, mouse_output: str, keymap_path: str, + allow_cut_after: float, kvmd: KvmdClient, streamers: list[BaseStreamerClient], @@ -501,6 +504,7 @@ class VncServer: # pylint: disable=too-many-instance-attributes mouse_output=mouse_output, keymap_name=keymap_name, symmap=symmap, + allow_cut_after=allow_cut_after, kvmd=kvmd, streamers=streamers, vnc_credentials=(await self.__vnc_auth_manager.read_credentials())[0], From f1503d69e0b15caa5d8fd0f4fb30b91d516db2ea Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 5 Nov 2024 18:17:00 +0200 Subject: [PATCH 87/88] pikvm/pikvm#1207: Draw UI tips via meta.yaml --- web/kvm/index.html | 19 ++++++++++++++++--- web/kvm/index.pug | 10 ++++++++-- web/kvm/window-about.pug | 9 +++++++++ web/login/index.html | 2 +- web/login/index.pug | 2 +- web/share/css/main.css | 4 ++-- web/share/css/navbar.css | 30 ++++++++++++++++++++++++++++++ web/share/js/kvm/session.js | 18 +++++++++--------- 8 files changed, 76 insertions(+), 18 deletions(-) diff --git a/web/kvm/index.html b/web/kvm/index.html index 960535f2..0c40cc83 100644 --- a/web/kvm/index.html +++ b/web/kvm/index.html @@ -1999,7 +1999,12 @@
    -
    No data +
    +
    // You can get this JSON using handle /api/info?fields=meta
    + // In the standard configuration this data
    + // is specified in the file /etc/kvmd/meta.yaml

    +
    No data
    +
    @@ -2673,9 +2678,17 @@
    + \ No newline at end of file diff --git a/web/kvm/index.pug b/web/kvm/index.pug index a6c53f70..ef694dde 100644 --- a/web/kvm/index.pug +++ b/web/kvm/index.pug @@ -11,14 +11,20 @@ block body include navbar.pug include windows.pug + ul(class="navbar-bg-tips") + li(class="left") + pre(id="kvmd-meta-tips-left") + li(class="right") + pre(id="kvmd-meta-tips-right") + ul(class="footer") - li(class="footer-left") + li(class="left") span(id="kvmd-meta-server-host" title="Server name (see System/About)") |   |   span(id="kvmd-version-kvmd" title="KVMD version") |   |   span(id="kvmd-version-streamer" title="Streamer version") - li(class="footer-right") + li(class="right") a(target="_blank" href="https://pikvm.org") PiKVM Project |   |   a(target="_blank" href="https://docs.pikvm.org") Documentation diff --git a/web/kvm/window-about.pug b/web/kvm/window-about.pug index 42503c6c..a9c50aac 100644 --- a/web/kvm/window-about.pug +++ b/web/kvm/window-about.pug @@ -29,6 +29,15 @@ div(id="about-window" class="window") br div(class="tabs-box") +about_tab("meta", "Meta", true) + div + span(class="code-comment") + | // You can get this JSON using handle #[a(target="_blank" href="/api/info?fields=meta") /api/info?fields=meta]#[br] + | // In the standard configuration this data#[br] + | // is specified in the file /etc/kvmd/meta.yaml + br + pre(id="kvmd-meta-json") + | No data + +about_tab("hardware", "Hardware") +about_tab("version", "Version") diff --git a/web/login/index.html b/web/login/index.html index 26b07640..99fa2aed 100644 --- a/web/login/index.html +++ b/web/login/index.html @@ -82,7 +82,7 @@
    +
    + Web UI settings +
    + + + + + + + + + +
    Ask page close confirmation: +
    + + +
    +
    Expand for the entire tab by default: +
    + + +
    +
    +
    +
    @@ -408,15 +433,6 @@ - - - -
    Connect HID to Server:
    Ask page close confirmation: -
    - - -
    -

    diff --git a/web/kvm/navbar-system.pug b/web/kvm/navbar-system.pug index 484bed86..8112d441 100644 --- a/web/kvm/navbar-system.pug +++ b/web/kvm/navbar-system.pug @@ -113,6 +113,14 @@ li(id="system-dropdown" class="right") td(id="hid-mouse-scroll-value" class="value-number") tr +menu_switch_notable("hid-mouse-dot-switch", "Show the blue dot", true, true) + details + summary Web UI settings + div(class="spoiler") + table(class="kv") + tr + +menu_switch_notable("page-close-ask-switch", "Ask page close confirmation", true, true) + tr + +menu_switch_notable("page-full-tab-stream-switch", "Expand for the entire tab by default", true, false) table(class="kv") tr(id="hid-connect" class="feature-disabled") +menu_switch_notable("hid-connect-switch", "Connect HID to Server", true, true) @@ -125,8 +133,6 @@ li(id="system-dropdown" class="right") "Turning off this switch will disconnect the main USB from the server. Are you sure you want to continue?") tr(id="v4-locator" class="feature-disabled") +menu_switch_notable_gpio("__v4_locator__", "Enable locator LED") - tr - +menu_switch_notable("page-close-ask-switch", "Ask page close confirmation", true, true) hr div(class="buttons buttons-row") button(data-force-hide-menu data-show-window="keyboard-window" class="row50") • Show keyboard diff --git a/web/share/js/kvm/main.js b/web/share/js/kvm/main.js index e32992a9..c71e7fd6 100644 --- a/web/share/js/kvm/main.js +++ b/web/share/js/kvm/main.js @@ -50,9 +50,14 @@ export function main() { tools.el.setOnClick($("open-log-button"), () => window.open("/api/log?seek=3600&follow=1", "_blank")); - if (tools.config.getBool("kvm--full-tab-stream", false)) { - wm.toggleFullTabWindow($("stream-window"), true); + tools.storage.bindSimpleSwitch( + $("page-full-tab-stream-switch"), + "page.full_tab_stream", + tools.config.getBool("kvm--full-tab-stream", false)); + if ($("page-full-tab-stream-switch").checked) { + wm.setFullTabWindow($("stream-window"), true); } + wm.showWindow($("stream-window")); new Session(); diff --git a/web/share/js/wm.js b/web/share/js/wm.js index 1c1b67df..162f43ba 100644 --- a/web/share/js/wm.js +++ b/web/share/js/wm.js @@ -111,8 +111,8 @@ function __WindowManager() { let el_exit_full_tab_button = el_window.querySelector(".window-button-exit-full-tab"); if (el_enter_full_tab_button && el_exit_full_tab_button) { el_enter_full_tab_button.title = "Stretch to the entire tab"; - tools.el.setOnClick(el_enter_full_tab_button, () => self.toggleFullTabWindow(el_window, true)); - tools.el.setOnClick(el_exit_full_tab_button, () => self.toggleFullTabWindow(el_window, false)); + tools.el.setOnClick(el_enter_full_tab_button, () => self.setFullTabWindow(el_window, true)); + tools.el.setOnClick(el_exit_full_tab_button, () => self.setFullTabWindow(el_window, false)); } let el_full_screen_button = el_window.querySelector(".window-header .window-button-full-screen"); @@ -334,7 +334,7 @@ function __WindowManager() { __activateLastWindow(el_window); }; - self.toggleFullTabWindow = function(el_window, enabled) { + self.setFullTabWindow = function(el_window, enabled) { el_window.classList.toggle("window-full-tab", enabled); __activateLastWindow(el_window); let el_navbar = $("navbar");