Skip to content
/ kvmd Public
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
4775a31
Add GPIO driver for gzvich gz-hk401x 4port HDMI USB KVM
Aug 15, 2023
df17960
Add copyrights
Aug 15, 2023
c2f3617
web: bring back mouse buttons
mdevaev Dec 4, 2025
da01ba9
Bump version: 4.131 → 4.132
mdevaev Dec 4, 2025
35d0d60
aiotools.spawn_and_follow(): improved implementation
mdevaev Dec 15, 2025
7bfeb91
using own AioMpQueue class
mdevaev Dec 15, 2025
0eb05b7
refactoring
mdevaev Dec 15, 2025
01dcf6d
fix reader/writer closing
mdevaev Dec 17, 2025
f3eda2e
aioproc.kill_process(): Don't suppress CancelledError
mdevaev Dec 17, 2025
2cb1fb7
spawn_and_follow(): access to tasks list
mdevaev Dec 23, 2025
46821d2
AioMpNotifier(): timeout support
mdevaev Dec 23, 2025
ab4322d
switch: use spawn_and_follow()
mdevaev Dec 23, 2025
d9d986a
refactoring
mdevaev Jan 1, 2026
47f3fde
AioMpProcess
mdevaev Jan 2, 2026
c842486
aiotools.spawn_and_follow(): better cleanup
mdevaev Jan 2, 2026
ac5b818
nbd libs
mdevaev Jan 2, 2026
40d52a8
nbd configs
mdevaev Jan 2, 2026
4081bba
inotify: fixed logging depth
mdevaev Jan 2, 2026
82698e4
refactoring
mdevaev Jan 2, 2026
185d23a
nbd app: fixed log format
mdevaev Jan 2, 2026
fefb65a
nbd configs: group=kvmd
mdevaev Jan 3, 2026
0fe2c3e
camera js
mdevaev Jan 3, 2026
ecb19c5
fix
mdevaev Jan 5, 2026
faa90f6
nbd: some improvements
mdevaev Jan 5, 2026
27fa14a
web: don't allow disabled audio/mic/camera
mdevaev Jan 5, 2026
03d5867
Bump version: 4.132 → 4.133
mdevaev Jan 5, 2026
0ba00fc
AioMpQueue: Fixed async_fetch_last()
mdevaev Jan 6, 2026
ede35a5
ezcoo: refactoring
mdevaev Jan 6, 2026
9e9cafb
pikvm/pikvm#206: ezcoo: Don't switch channel at startup
mdevaev Jan 6, 2026
02f53cf
Bump version: 4.133 → 4.134
mdevaev Jan 6, 2026
a1e13bb
ezcoo: fixed status getting
mdevaev Jan 7, 2026
7fb720b
Bump version: 4.134 → 4.135
mdevaev Jan 7, 2026
7a88974
moved linters to python 3.14
mdevaev Jan 12, 2026
6516665
forced multiprocessing to use old fork method
mdevaev Jan 12, 2026
adc4eae
get rid of run_sync()
mdevaev Jan 12, 2026
042930f
using asyncio.to_thread() instead of aiotools.run_async()
mdevaev Jan 12, 2026
8a16218
janus: refactoring
mdevaev Jan 12, 2026
f228aa5
janus: async getaddrinfo()
mdevaev Jan 12, 2026
6dfbc02
web: msd: fixed sort buttons in firefox
mdevaev Jan 13, 2026
a06cd21
Bump version: 4.135 → 4.136
mdevaev Jan 16, 2026
871d03d
fixed python in PKGBUILD
mdevaev Jan 16, 2026
ff6b213
Bump version: 4.136 → 4.137
mdevaev Jan 16, 2026
8aea89d
fixed aiotools.run() for python 3.14
mdevaev Jan 16, 2026
0f01707
Bump version: 4.137 → 4.138
mdevaev Jan 16, 2026
3eef06d
web: msd: redesign
mdevaev Jan 17, 2026
19e20ca
web: msd: disable sorting buttons for some cases
mdevaev Jan 17, 2026
fae6ac0
otg hid: handle ESHUTDOWN
mdevaev Jan 18, 2026
2851756
Bump version: 4.138 → 4.139
mdevaev Jan 18, 2026
6ec7c1f
bumped kernel
mdevaev Jan 18, 2026
3c6edcf
Bump version: 4.139 → 4.140
mdevaev Jan 18, 2026
ca434ef
web: using webp instead of png
mdevaev Jan 19, 2026
fcee55a
web: kvm: preload some images
mdevaev Jan 19, 2026
1f118ab
web: x/y renamed to horiz/vert
mdevaev Jan 23, 2026
d2850a6
ustreamer 6.47 with new fps limiter
mdevaev Jan 23, 2026
d758dce
Bump version: 4.140 → 4.141
mdevaev Jan 23, 2026
ae63c29
configs: removed dubs from v4plus-hdmi-rpi4.txt
mdevaev Jan 26, 2026
ac0eee9
web: janus: refactoring
mdevaev Jan 28, 2026
fa23334
web: janus: latency experiment
mdevaev Jan 28, 2026
91256f4
openssl-1.1 is deprecated
mdevaev Jan 28, 2026
adec7f3
lint fix
mdevaev Jan 28, 2026
6464283
Bump version: 4.141 → 4.142
mdevaev Jan 28, 2026
60f9cc3
web: janus: fixed ice servers callback
mdevaev Jan 28, 2026
e7c14e8
Bump version: 4.142 → 4.143
mdevaev Jan 28, 2026
a0c31e4
web: janus: Fixed playout-delay transition to 0,0 on Firefox
mdevaev Jan 31, 2026
4d867b3
Bump version: 4.143 → 4.144
mdevaev Jan 31, 2026
2ef6af7
web: mouse: welcome cursor for inactive relative mouse
mdevaev Feb 4, 2026
a18ad81
Bump version: 4.144 → 4.145
mdevaev Feb 4, 2026
57ead0c
Add GPIO driver for gzvich gz-hk401x 4port HDMI USB KVM
Aug 15, 2023
14c066b
Add copyrights
Aug 15, 2023
ab03629
update gitignore
Feb 5, 2026
26ec0a3
Merge branch 'gzvich' of https://github.com/gy6221/kvmd into gzvich
Feb 5, 2026
08ad114
adapt for new kvmd
Feb 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[bumpversion]
commit = True
tag = True
current_version = 4.131
current_version = 4.145
parse = (?P<major>\d+)\.(?P<minor>\d+)(\.(?P<patch>\d+)(\-(?P<release>[a-z]+))?)?
serialize =
{major}.{minor}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@
*.pyc
*.swp
/venv/
*.code-workspace
13 changes: 5 additions & 8 deletions PKGBUILD
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@ for _variant in "${_variants[@]}"; do
pkgname+=(kvmd-platform-$_platform-$_board)
done
pkgbase=kvmd
pkgver=4.131
pkgver=4.145
pkgrel=1
pkgdesc="The main PiKVM daemon"
url="https://github.com/pikvm/kvmd"
license=(GPL)
arch=(any)
depends=(
"python>=3.13"
"python<3.14"
"python>=3.14"
"python<3.15"
python-yaml
python-ruamel-yaml
python-aiohttp
Expand Down Expand Up @@ -97,7 +97,7 @@ depends=(
certbot
"raspberrypi-io-access>=0.7"
raspberrypi-utils
"ustreamer>=6.41"
"ustreamer>=6.47"

# Systemd UDEV bug
"systemd>=248.3-2"
Expand All @@ -106,9 +106,6 @@ depends=(
# https://archlinuxarm.org/forum/viewtopic.php?f=15&t=15725&start=40
"zstd>=1.5.1-2.1"

# Possible hotfix for the new os update
openssl-1.1

# Bootconfig
dos2unix
parted
Expand Down Expand Up @@ -225,7 +222,7 @@ for _variant in "${_variants[@]}"; do
backup=()

pkgdesc=\"PiKVM platform configs - $_platform for $_board\"
depends=(kvmd=$pkgver-$pkgrel \"linux-rpi-pikvm>=6.12.56-1\" \"raspberrypi-bootloader-pikvm>=20251031-1\")
depends=(kvmd=$pkgver-$pkgrel \"linux-rpi-pikvm>=6.12.56-5\" \"raspberrypi-bootloader-pikvm>=20251031-1\")

if [[ $_base == v0 ]]; then
depends=(\"\${depends[@]}\" platformio-core avrdude make patch)
Expand Down
4 changes: 0 additions & 4 deletions configs/os/boot-config/v4plus-hdmi-rpi4.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ dtoverlay=dwc2,dr_mode=peripheral
dtoverlay=tc358743,4lane=1
dtoverlay=tc358743-audio

# Passthrough
dtoverlay=vc4-kms-v3d
disable_overscan=1

# I2C (display)
dtparam=i2c_arm=on

Expand Down
1 change: 1 addition & 0 deletions configs/os/modules-load/v2-hdmi.conf
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
dwc2
libcomposite
tc358743
nbd
1 change: 1 addition & 0 deletions configs/os/modules-load/v2-hdmiusb.conf
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
dwc2
libcomposite
nbd
1 change: 1 addition & 0 deletions configs/os/modules-load/v3-hdmi.conf
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ dwc2
libcomposite
tc358743
i2c-dev
nbd
1 change: 1 addition & 0 deletions configs/os/modules-load/v4mini-hdmi.conf
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ dwc2
libcomposite
tc358743
i2c-dev
nbd
1 change: 1 addition & 0 deletions configs/os/modules-load/v4plus-hdmi.conf
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ dwc2
libcomposite
tc358743
i2c-dev
nbd
1 change: 1 addition & 0 deletions configs/os/udev/common.rules
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

ACTION!="remove", KERNEL=="ttyACM[0-9]*", SUBSYSTEM=="tty", SUBSYSTEMS=="usb", ATTRS{idVendor}=="1209", ATTRS{idProduct}=="eda3", SYMLINK+="kvmd-hid-bridge"
ACTION!="remove", KERNEL=="ttyACM[0-9]*", SUBSYSTEM=="tty", SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="1080", SYMLINK+="kvmd-switch"
ACTION!="remove", KERNEL=="nbd15", SUBSYSTEM=="block", GROUP="kvmd", SYMLINK+="kvmd-nbd"

# Disable USB autosuspend for critical devices
ACTION!="remove", SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="eda3", GOTO="kvmd-usb"
Expand Down
7 changes: 6 additions & 1 deletion kvmd/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@
# ========================================================================== #


__version__ = "4.131"
__version__ = "4.145"


import multiprocessing

multiprocessing.set_start_method("fork") # FIXME
4 changes: 2 additions & 2 deletions kvmd/aiogp.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ async def poll(self) -> None:
self.__loop = asyncio.get_running_loop()
self.__thread.start()
try:
await aiotools.run_async(self.__thread.join)
await asyncio.to_thread(self.__thread.join)
finally:
self.__stop_event.set()
await aiotools.run_async(self.__thread.join)
await asyncio.to_thread(self.__thread.join)

def __run(self) -> None:
assert self.__values is None
Expand Down
209 changes: 175 additions & 34 deletions kvmd/aiomulti.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,64 +20,205 @@
# ========================================================================== #


import os
import signal
import asyncio
import multiprocessing
import multiprocessing.queues
import multiprocessing.connection
import queue
import logging

from typing import Callable
from typing import Type
from typing import TypeVar
from typing import Generic
from typing import Any

from . import aiotools
import setproctitle


# =====
_QueueItemT = TypeVar("_QueueItemT")
def rename_process(suffix: str) -> None:
setproctitle.setproctitle(f"kvmd/{suffix}: {setproctitle.getproctitle()}")


async def queue_get_last( # pylint: disable=invalid-name
q: "multiprocessing.Queue[_QueueItemT]",
timeout: float,
) -> tuple[bool, (_QueueItemT | None)]:
# =====
class AioMpProcess:
def __init__(
self,
name: str,
target: Callable[..., None],
args: tuple[Any, ...]=(),
) -> None:

self.__name = name
self.__target = target

return (await aiotools.run_async(queue_get_last_sync, q, timeout))
self.__proc = multiprocessing.Process(
target=self.__target_wrapper,
args=args,
daemon=True,
name=name,
)

def __target_wrapper(self, *args: Any, **kwargs: Any) -> None:
logger = logging.getLogger(self.__target.__module__)
logger.info("Started process kvmd/%s: pid=%s", self.__name, os.getpid())
os.setpgrp()
rename_process(self.__name)
self.__target(*args, **kwargs)

def queue_get_last_sync( # pylint: disable=invalid-name
q: "multiprocessing.Queue[_QueueItemT]",
timeout: float,
) -> tuple[bool, (_QueueItemT | None)]:
def is_alive(self) -> bool:
return self.__proc.is_alive()

try:
item = q.get(timeout=timeout)
while not q.empty():
item = q.get()
return (True, item)
except queue.Empty:
return (False, None)
@property
def exitcode(self) -> (int | None):
return self.__proc.exitcode

def start(self) -> None:
self.__proc.start()

def send_sigterm(self) -> None:
if self.__proc.pid is None:
return
try:
os.kill(self.__proc.pid, signal.SIGTERM)
except ProcessLookupError:
pass

def sendpg_sigkill(self) -> None:
if self.__proc.pid is None:
return
try:
own = os.getpgid(os.getpid())
target = os.getpgid(self.__proc.pid)
if own != target:
os.killpg(target, signal.SIGKILL)
else:
os.kill(self.__proc.pid, signal.SIGKILL)
except ProcessLookupError:
pass

async def async_join(self, timeout: float=0.0) -> bool:
if self.__proc.pid is None:
return False

prev = self.__proc.is_alive()

loop = asyncio.get_running_loop()
fut = asyncio.Future() # type: ignore
try:
fd = os.pidfd_open(self.__proc.pid, os.PIDFD_NONBLOCK)
except ProcessLookupError:
pass
else:
try:
loop.add_reader(fd, fut.set_result, None)
fut.add_done_callback(lambda _: loop.remove_reader(fd))
if timeout > 0:
await asyncio.wait_for(fut, timeout)
else:
await fut
except TimeoutError:
pass
finally:
try:
loop.remove_reader(fd)
finally:
os.close(fd)

# Crank the internal MP machinery and return a status code.
# It should be non-blocking.
new = self.__proc.is_alive()
if prev != new:
self.__get_logger().info("Stopped kvmd/%s: exitcode=%s", self.__name, self.exitcode)
return new

def __get_logger(self) -> logging.Logger:
return logging.getLogger(self.__target.__module__)


# =====
class AioProcessNotifier:
class AioMpQueue[T](multiprocessing.queues.Queue[T]):
def __init__(self, maxsize: int=0) -> None:
super().__init__(maxsize=maxsize, ctx=multiprocessing.get_context())

def get_reader(self) -> multiprocessing.connection.Connection:
return self._reader # type: ignore # pylint: disable=protected-access

def get_reader_fd(self) -> int:
return self.get_reader().fileno()

async def async_fetch(self, timeout: float=0.0) -> tuple[bool, (T | None)]:
return (await self.__async_get(timeout, False))

async def async_fetch_last(self, timeout: float=0.0) -> tuple[bool, (T | None)]:
return (await self.__async_get(timeout, True))

async def __async_get(self, timeout: float, last_only: bool) -> tuple[bool, (T | None)]:
loop = asyncio.get_running_loop()
fut = asyncio.Future() # type: ignore
fd = self.get_reader_fd()

try:
loop.add_reader(fd, fut.set_result, None)
fut.add_done_callback(lambda _: loop.remove_reader(fd))
if timeout > 0:
await asyncio.wait_for(fut, timeout)
else:
await fut

if not last_only:
return (True, self.get(False))

got = False
item: (T | None) = None
while not self.empty():
got = True
item = self.get(False)
await asyncio.sleep(0) # Switch task to prevent hanging in a loop
return (got, item)
except (TimeoutError, queue.Empty):
return (False, None)
finally:
loop.remove_reader(fd)

def fetch_last(self, timeout: float=0.0) -> tuple[bool, (T | None)]:
try:
item = self.get(timeout=timeout)
while not self.empty():
item = self.get()
return (True, item)
except queue.Empty:
return (False, None)

def clear_current(self) -> None:
for _ in range(self.qsize()):
try:
self.get_nowait()
except queue.Empty:
break


# =====
class AioMpNotifier:
def __init__(self) -> None:
self.__queue: "multiprocessing.Queue[int]" = multiprocessing.Queue()
self.__queue: AioMpQueue[int] = AioMpQueue()

def notify(self, mask: int=0) -> None:
self.__queue.put_nowait(mask)

async def wait(self) -> int:
while True:
mask = await aiotools.run_async(self.__get)
if mask >= 0:
return mask

def __get(self) -> int:
try:
mask = self.__queue.get(timeout=0.1)
async def wait(self, timeout: float=0) -> int:
(got, mask) = await self.__queue.async_fetch(timeout)
if not got: # Timeout
return -1
assert mask is not None
if got:
while not self.__queue.empty():
mask |= self.__queue.get()
return mask
except queue.Empty:
return -1
await asyncio.sleep(0)
return mask


# =====
Expand All @@ -88,7 +229,7 @@ class AioSharedFlags(Generic[_SharedFlagT]):
def __init__(
self,
initial: dict[str, _SharedFlagT],
notifier: AioProcessNotifier,
notifier: AioMpNotifier,
type: Type[_SharedFlagT]=bool, # pylint: disable=redefined-builtin
) -> None:

Expand All @@ -114,7 +255,7 @@ def update(self, **kwargs: _SharedFlagT) -> None:
self.__notifier.notify()

async def get(self) -> dict[str, _SharedFlagT]:
return (await aiotools.run_async(self.__inner_get))
return (await asyncio.to_thread(self.__inner_get))

def __inner_get(self) -> dict[str, _SharedFlagT]:
with self.__lock:
Expand Down
Loading