mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-13 17:50:29 +08:00
剪枝
This commit is contained in:
parent
ddb4d752c0
commit
5bf2466037
@ -1,27 +0,0 @@
|
||||
/pkg/
|
||||
/src/
|
||||
/site/
|
||||
/dist/
|
||||
/kvmd.egg-info/
|
||||
/testenv/run/
|
||||
/testenv/.tox/
|
||||
/testenv/.mypy_cache/
|
||||
/testenv/.ssl/
|
||||
/hid/arduino/.pio/
|
||||
/hid/arduino/.platformio/
|
||||
/hid/pico/.pico-sdk.tmp/
|
||||
/hid/pico/.pico-sdk/
|
||||
/hid/pico/.tinyusb.tmp/
|
||||
/hid/pico/.tinyusb/
|
||||
/hid/pico/.build/
|
||||
/hid/pico/*.uf2
|
||||
/.git/
|
||||
/v*.tar.gz
|
||||
/*.pkg.tar.xz
|
||||
/*.pkg.tar.zst
|
||||
/*.egg-info
|
||||
/*kvmd-*.tar.gz
|
||||
kvmd-launcher.bin
|
||||
kvmd-launcher.build
|
||||
kvmd-launcher.dist
|
||||
kvmd-launcher.onefile-build
|
||||
268
PKGBUILD
268
PKGBUILD
@ -1,268 +0,0 @@
|
||||
# Contributor: Maxim Devaev <mdevaev@gmail.com>
|
||||
# Author: Maxim Devaev <mdevaev@gmail.com>
|
||||
|
||||
|
||||
_variants=(
|
||||
v0-hdmi:zero2w
|
||||
v0-hdmi:rpi2
|
||||
v0-hdmi:rpi3
|
||||
|
||||
v0-hdmiusb:zero2w
|
||||
v0-hdmiusb:rpi2
|
||||
v0-hdmiusb:rpi3
|
||||
|
||||
v1-hdmi:zero2w
|
||||
v1-hdmi:rpi2
|
||||
v1-hdmi:rpi3
|
||||
|
||||
v1-hdmiusb:zero2w
|
||||
v1-hdmiusb:rpi2
|
||||
v1-hdmiusb:rpi3
|
||||
|
||||
v2-hdmi:zero2w
|
||||
v2-hdmi:rpi3
|
||||
v2-hdmi:rpi4
|
||||
|
||||
v2-hdmiusb:rpi4
|
||||
|
||||
v3-hdmi:rpi4
|
||||
|
||||
v4mini-hdmi:rpi4
|
||||
v4plus-hdmi:rpi4
|
||||
)
|
||||
|
||||
|
||||
pkgname=(kvmd)
|
||||
for _variant in "${_variants[@]}"; do
|
||||
_platform=${_variant%:*}
|
||||
_board=${_variant#*:}
|
||||
pkgname+=(kvmd-platform-$_platform-$_board)
|
||||
done
|
||||
pkgbase=kvmd
|
||||
pkgver=4.20
|
||||
pkgrel=1
|
||||
pkgdesc="The main PiKVM daemon"
|
||||
url="https://github.com/pikvm/kvmd"
|
||||
license=(GPL)
|
||||
arch=(any)
|
||||
depends=(
|
||||
"python>=3.12"
|
||||
"python<3.13"
|
||||
python-yaml
|
||||
python-aiohttp
|
||||
python-aiofiles
|
||||
python-async-lru
|
||||
python-passlib
|
||||
python-pyotp
|
||||
python-qrcode
|
||||
python-periphery
|
||||
python-pyserial
|
||||
python-pyserial-asyncio
|
||||
python-spidev
|
||||
python-setproctitle
|
||||
python-psutil
|
||||
python-netifaces
|
||||
python-systemd
|
||||
python-dbus
|
||||
python-dbus-next
|
||||
python-pygments
|
||||
python-pyghmi
|
||||
python-pam
|
||||
python-pillow
|
||||
python-xlib
|
||||
libxkbcommon
|
||||
python-hidapi
|
||||
python-six
|
||||
python-pyrad
|
||||
python-ldap
|
||||
python-zstandard
|
||||
python-mako
|
||||
python-luma-oled
|
||||
python-pyusb
|
||||
"libgpiod>=2.1"
|
||||
freetype2
|
||||
"v4l-utils>=1.22.1-1"
|
||||
"nginx-mainline>=1.25.1"
|
||||
openssl
|
||||
sudo
|
||||
iptables
|
||||
iproute2
|
||||
dnsmasq
|
||||
ipmitool
|
||||
"janus-gateway-pikvm>=0.14.2-3"
|
||||
certbot
|
||||
platform-io-access
|
||||
raspberrypi-utils
|
||||
"ustreamer>=6.16"
|
||||
|
||||
# Systemd UDEV bug
|
||||
"systemd>=248.3-2"
|
||||
|
||||
# https://bugzilla.redhat.com/show_bug.cgi?id=2035802
|
||||
# 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
|
||||
e2fsprogs
|
||||
openssh
|
||||
# FIXME:
|
||||
# - https://archlinuxarm.org/forum/viewtopic.php?f=15&t=17007&p=72789
|
||||
# - https://github.com/pikvm/pikvm/issues/1375
|
||||
wpa_supplicant-pikvm
|
||||
run-parts
|
||||
|
||||
# fsck for /boot
|
||||
dosfstools
|
||||
|
||||
# pgrep for kvmd-udev-restart-pass
|
||||
procps-ng
|
||||
|
||||
# Misc
|
||||
hostapd
|
||||
)
|
||||
optdepends=(
|
||||
tesseract
|
||||
)
|
||||
conflicts=(
|
||||
python-pikvm
|
||||
python-aiohttp-pikvm
|
||||
platformio
|
||||
avrdude-pikvm
|
||||
kvmd-oled
|
||||
)
|
||||
makedepends=(
|
||||
python-setuptools
|
||||
python-pip
|
||||
)
|
||||
source=("$url/archive/v$pkgver.tar.gz")
|
||||
md5sums=(SKIP)
|
||||
backup=(
|
||||
etc/kvmd/{override,logging,auth,meta}.yaml
|
||||
etc/kvmd/{ht,ipmi,vnc}passwd
|
||||
etc/kvmd/totp.secret
|
||||
etc/kvmd/nginx/{kvmd.ctx-{http,server},certbot.ctx-server}.conf
|
||||
etc/kvmd/nginx/loc-{login,nocache,proxy,websocket,nobuffering,bigpost}.conf
|
||||
etc/kvmd/nginx/{mime-types,ssl}.conf
|
||||
etc/kvmd/nginx/nginx.conf.mako
|
||||
etc/kvmd/janus/janus{,.plugin.ustreamer,.transport.websockets}.jcfg
|
||||
etc/kvmd/web.css
|
||||
)
|
||||
|
||||
|
||||
package_kvmd() {
|
||||
install=$pkgname.install
|
||||
|
||||
cd "$srcdir/kvmd-$pkgver"
|
||||
pip install --root="$pkgdir" --no-deps .
|
||||
|
||||
install -Dm755 -t "$pkgdir/usr/bin" scripts/kvmd-{bootconfig,gencert,certbot}
|
||||
|
||||
install -Dm644 -t "$pkgdir/usr/lib/systemd/system" configs/os/services/*
|
||||
install -DTm644 configs/os/sysusers.conf "$pkgdir/usr/lib/sysusers.d/kvmd.conf"
|
||||
install -DTm644 configs/os/tmpfiles.conf "$pkgdir/usr/lib/tmpfiles.d/kvmd.conf"
|
||||
|
||||
mkdir -p "$pkgdir/usr/share/kvmd"
|
||||
cp -r {hid,web,extras,contrib/keymaps} "$pkgdir/usr/share/kvmd"
|
||||
find "$pkgdir/usr/share/kvmd/web" -name '*.pug' -exec rm -f '{}' \;
|
||||
|
||||
local _cfg_default="$pkgdir/usr/share/kvmd/configs.default"
|
||||
mkdir -p "$_cfg_default"
|
||||
cp -r configs/* "$_cfg_default"
|
||||
|
||||
find "$pkgdir" -name ".gitignore" -delete
|
||||
find "$_cfg_default" -type f -exec chmod 444 '{}' \;
|
||||
chmod 400 "$_cfg_default/kvmd"/*passwd
|
||||
chmod 400 "$_cfg_default/kvmd"/*.secret
|
||||
chmod 750 "$_cfg_default/os/sudoers"
|
||||
chmod 400 "$_cfg_default/os/sudoers"/*
|
||||
|
||||
mkdir -p "$pkgdir/etc/kvmd/"{nginx,vnc}"/ssl"
|
||||
chmod 755 "$pkgdir/etc/kvmd/"{nginx,vnc}"/ssl"
|
||||
install -Dm444 -t "$pkgdir/etc/kvmd/nginx" "$_cfg_default/nginx"/*.conf*
|
||||
chmod 644 "$pkgdir/etc/kvmd/nginx/"{nginx,ssl}.conf*
|
||||
|
||||
mkdir -p "$pkgdir/etc/kvmd/janus"
|
||||
chmod 755 "$pkgdir/etc/kvmd/janus"
|
||||
install -Dm444 -t "$pkgdir/etc/kvmd/janus" "$_cfg_default/janus"/*.jcfg
|
||||
|
||||
install -Dm644 -t "$pkgdir/etc/kvmd" "$_cfg_default/kvmd"/*.yaml
|
||||
install -Dm600 -t "$pkgdir/etc/kvmd" "$_cfg_default/kvmd"/*passwd
|
||||
install -Dm600 -t "$pkgdir/etc/kvmd" "$_cfg_default/kvmd"/*.secret
|
||||
install -Dm644 -t "$pkgdir/etc/kvmd" "$_cfg_default/kvmd"/web.css
|
||||
mkdir -p "$pkgdir/etc/kvmd/override.d"
|
||||
|
||||
mkdir -p "$pkgdir/var/lib/kvmd/"{msd,pst}
|
||||
}
|
||||
|
||||
|
||||
for _variant in "${_variants[@]}"; do
|
||||
_platform=${_variant%:*}
|
||||
_board=${_variant#*:}
|
||||
_base=${_platform%-*}
|
||||
_video=${_platform#*-}
|
||||
eval "package_kvmd-platform-$_platform-$_board() {
|
||||
cd \"kvmd-\$pkgver\"
|
||||
|
||||
pkgdesc=\"PiKVM platform configs - $_platform for $_board\"
|
||||
depends=(kvmd=$pkgver-$pkgrel \"linux-rpi-pikvm>=6.6.45-1\" \"raspberrypi-bootloader-pikvm>=20240818-1\")
|
||||
|
||||
backup=(
|
||||
etc/sysctl.d/99-kvmd.conf
|
||||
etc/udev/rules.d/99-kvmd.rules
|
||||
etc/kvmd/main.yaml
|
||||
)
|
||||
|
||||
if [[ $_base == v0 ]]; then
|
||||
depends=(\"\${depends[@]}\" platformio-core avrdude make patch)
|
||||
elif [[ $_base == v4plus ]]; then
|
||||
depends=(\"\${depends[@]}\" flashrom-pikvm)
|
||||
fi
|
||||
|
||||
if [[ $_platform =~ ^.*-hdmiusb$ ]]; then
|
||||
install -Dm755 -t \"\$pkgdir/usr/bin\" scripts/kvmd-udev-hdmiusb-check
|
||||
fi
|
||||
if [[ $_base == v4plus ]]; then
|
||||
install -Dm755 -t \"\$pkgdir/usr/bin\" scripts/kvmd-udev-restart-pass
|
||||
fi
|
||||
|
||||
install -DTm644 configs/os/sysctl.conf \"\$pkgdir/etc/sysctl.d/99-kvmd.conf\"
|
||||
install -DTm644 configs/os/udev/common.rules \"\$pkgdir/usr/lib/udev/rules.d/99-kvmd-common.rules\"
|
||||
install -DTm644 configs/os/udev/$_platform-$_board.rules \"\$pkgdir/etc/udev/rules.d/99-kvmd.rules\"
|
||||
install -DTm444 configs/kvmd/main/$_platform-$_board.yaml \"\$pkgdir/etc/kvmd/main.yaml\"
|
||||
|
||||
if [ -f configs/kvmd/fan/$_platform.ini ]; then
|
||||
backup=(\"\${backup[@]}\" etc/kvmd/fan.ini)
|
||||
depends=(\"\${depends[@]}\" \"kvmd-fan>=0.18\")
|
||||
install -DTm444 configs/kvmd/fan/$_platform.ini \"\$pkgdir/etc/kvmd/fan.ini\"
|
||||
fi
|
||||
|
||||
if [ -f configs/os/modules-load/$_platform.conf ]; then
|
||||
backup=(\"\${backup[@]}\" etc/modules-load.d/kvmd.conf)
|
||||
install -DTm644 configs/os/modules-load/$_platform.conf \"\$pkgdir/etc/modules-load.d/kvmd.conf\"
|
||||
fi
|
||||
|
||||
if [ -f configs/os/sudoers/$_platform ]; then
|
||||
backup=(\"\${backup[@]}\" etc/sudoers.d/99_kvmd)
|
||||
install -DTm440 configs/os/sudoers/$_platform \"\$pkgdir/etc/sudoers.d/99_kvmd\"
|
||||
chmod 750 \"\$pkgdir/etc/sudoers.d\"
|
||||
fi
|
||||
|
||||
if [[ $_platform =~ ^.*-hdmi$ ]]; then
|
||||
backup=(\"\${backup[@]}\" etc/kvmd/tc358743-edid.hex)
|
||||
install -DTm444 configs/kvmd/edid/$_base.hex \"\$pkgdir/etc/kvmd/tc358743-edid.hex\"
|
||||
fi
|
||||
|
||||
mkdir -p \"\$pkgdir/usr/share/kvmd\"
|
||||
local _platform=\"\$pkgdir/usr/share/kvmd/platform\"
|
||||
rm -f \"\$_platform\"
|
||||
echo PIKVM_MODEL=$_base > \"\$_platform\"
|
||||
echo PIKVM_VIDEO=$_video >> \"\$_platform\"
|
||||
echo PIKVM_BOARD=$_board >> \"\$_platform\"
|
||||
chmod 444 \"\$_platform\"
|
||||
}"
|
||||
done
|
||||
@ -1,82 +0,0 @@
|
||||
# syntax = docker/dockerfile:experimental
|
||||
FROM python:3.12.8-slim-bookworm AS builder
|
||||
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/' /etc/apt/sources.list.d/debian.sources \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends build-essential libssl-dev libffi-dev python3-dev libevent-dev libjpeg-dev \
|
||||
libbsd-dev libudev-dev git pkg-config wget curl libmicrohttpd-dev libjansson-dev libssl-dev libsofia-sip-ua-dev \
|
||||
libopus-dev libogg-dev libcurl4-openssl-dev liblua5.3-dev libconfig-dev libopus-dev libtool automake autoconf \
|
||||
libx264-dev libyuv-dev libasound2-dev libspeex-dev libspeexdsp-dev libopus-dev libgpiod-dev libsystemd-dev \
|
||||
libglib2.0-dev libwebsockets-dev libsrtp2-dev libnice-dev patchelf libxkbcommon0 sudo iproute2 iptables libusb-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY build/cargo_config /tmp/config
|
||||
|
||||
RUN --security=insecure pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple \
|
||||
&& if [ ${TARGETARCH} = arm ]; then \
|
||||
mkdir -p /root/.cargo \
|
||||
&& chmod 777 /root/.cargo && mount -t tmpfs none /root/.cargo \
|
||||
&& export RUSTUP_DIST_SERVER="https://mirrors.tuna.tsinghua.edu.cn/rustup" \
|
||||
#&& export RUSTUP_UPDATE_ROOT="https://mirrors.ustc.edu.cn/rust-static/rustup" \
|
||||
&& wget https://sh.rustup.rs -O /root/rustup-init.sh \
|
||||
&& sh /root/rustup-init.sh -y \
|
||||
&& export PATH=$PATH:/root/.cargo/bin \
|
||||
&& cp /tmp/config /root/.cargo/config.toml; \
|
||||
fi \
|
||||
&& pip install cryptography
|
||||
|
||||
RUN pip install --no-cache-dir --root-user-action=ignore --disable-pip-version-check build nuitka \
|
||||
&& pip install --no-cache-dir aiofiles aiohttp appdirs asn1crypto async_lru async-timeout bottle cffi chardet click colorama \
|
||||
dbus_next gpiod hidapi idna mako marshmallow more-itertools multidict netifaces packaging passlib pillow ply psutil pycparser \
|
||||
pyelftools pyghmi pygments pyparsing pyotp qrcode requests semantic-version setproctitle setuptools six spidev \
|
||||
tabulate urllib3 wrapt xlib yarl pyserial pyyaml zstandard supervisor
|
||||
|
||||
RUN git clone --depth=1 https://github.com/meetecho/janus-gateway.git /tmp/janus-gateway \
|
||||
&& cd /tmp/janus-gateway \
|
||||
&& bash autogen.sh \
|
||||
&& ./configure --enable-static --enable-websockets --enable-plugin-audiobridge \
|
||||
--disable-data-channels --disable-rabbitmq --disable-mqtt --disable-all-plugins --disable-all-loggers \
|
||||
--prefix=/usr \
|
||||
&& make && make install
|
||||
|
||||
RUN sed --in-place --expression 's|^#include "refcount.h"$|#include "../refcount.h"|g' /usr/include/janus/plugins/plugin.h \
|
||||
&& git clone --depth=1 https://github.com/mofeng-git/ustreamer /tmp/ustreamer \
|
||||
&& make clean -C /tmp/ustreamer \
|
||||
&& make WITH_GPIO=1 WITH_JANUS=1 WITH_PYTHON=1 WITH_LIBX264=1 WITH_SETPROCTITLE=0 -C /tmp/ustreamer install
|
||||
|
||||
COPY kvmd /One-KVM/kvmd
|
||||
COPY kvmd-launcher.py /One-KVM/
|
||||
|
||||
RUN bash -c "cd /One-KVM && ls && python3 -m nuitka kvmd-launcher.py --standalone --onefile \
|
||||
--show-progress --no-deployment-flag=self-execution --include-module=\
|
||||
kvmd.plugins.auth.htpasswd,kvmd.plugins.auth.http,kvmd.plugins.auth.ldap,\
|
||||
kvmd.plugins.auth.pam,kvmd.plugins.auth.radius,\
|
||||
kvmd.plugins.hid.ch9329,kvmd.plugins.hid.bt,kvmd.plugins.hid.otg,\
|
||||
kvmd.plugins.atx.disabled,kvmd.plugins.atx.gpio,\
|
||||
kvmd.plugins.msd.disabled,kvmd.plugins.msd.otg,\
|
||||
kvmd.plugins.ugpio.gpio,kvmd.plugins.ugpio.wol,kvmd.plugins.ugpio.cmd,\
|
||||
kvmd.plugins.ugpio.ipmi,kvmd.plugins.ugpio.anelpwr,kvmd.plugins.ugpio.cmdret,\
|
||||
kvmd.plugins.ugpio.extron,kvmd.plugins.ugpio.ezcoo,kvmd.plugins.ugpio.hidrelay,\
|
||||
kvmd.plugins.ugpio.hue,kvmd.plugins.ugpio.locator,kvmd.plugins.ugpio.noyito,\
|
||||
kvmd.plugins.ugpio.otgconf,kvmd.plugins.ugpio.pway,kvmd.plugins.ugpio.pwm,\
|
||||
kvmd.plugins.ugpio.servo,kvmd.plugins.ugpio.tesmart,kvmd.plugins.ugpio.xh_hk4401,\
|
||||
passlib.handlers.sha1_crypt,pygments.formatters.terminal"
|
||||
|
||||
COPY kvmd_data /One-KVM/kvmd_data
|
||||
RUN cp /usr/local/bin/ustreamer /usr/local/bin/ustreamer-dump /One-KVM/kvmd_data/usr/bin \
|
||||
&& cd /One-KVM && python3 -m kvmd.apps.kvmd -m -c kvmd_data/etc/kvmd/main.yaml
|
||||
|
||||
FROM debian:stable-slim
|
||||
|
||||
COPY --from=builder /One-KVM/kvmd_data kvmd_data
|
||||
COPY --from=builder /One-KVM/kvmd-launcher.bin kvmd-launcher.bin
|
||||
|
||||
RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/' /etc/apt/sources.list.d/debian.sources \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y libxkbcommon0 sudo iproute2 iptables nano \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENTRYPOINT [ "/kvmd-launcher.bin" ]
|
||||
#RUN cd /One-KVM && /One-KVM/kvmd-launcher.bin
|
||||
@ -1,38 +0,0 @@
|
||||
FROM silentwind0/kvmd-stage-0 AS builder
|
||||
|
||||
FROM python:3.12.0rc2-slim-bookworm
|
||||
|
||||
LABEL maintainer="mofeng654321@hotmail.com"
|
||||
|
||||
COPY --from=builder /tmp/lib/* /tmp/lib/
|
||||
COPY --from=builder /tmp/ustreamer/ustreamer /tmp/ustreamer/ustreamer-dump /usr/bin/janus /usr/bin/
|
||||
COPY --from=builder /tmp/wheel/*.whl /tmp/wheel/
|
||||
COPY --from=builder /tmp/ustreamer/libjanus_ustreamer.so /usr/lib/ustreamer/janus/
|
||||
COPY --from=builder /usr/lib/janus/transports/* /usr/lib/janus/transports/
|
||||
|
||||
ARG TARGETARCH
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
RUN cp /tmp/lib/* /lib/*-linux-*/ \
|
||||
&& pip install --no-cache-dir --root-user-action=ignore --disable-pip-version-check /tmp/wheel/*.whl \
|
||||
&& pip install --no-cache-dir --root-user-action=ignore --disable-pip-version-check pyfatfs \
|
||||
&& rm -rf /tmp/lib /tmp/wheel
|
||||
|
||||
RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/' /etc/apt/sources.list.d/debian.sources \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends libxkbcommon-x11-0 nginx tesseract-ocr tesseract-ocr-eng tesseract-ocr-chi-sim iptables sudo curl kmod \
|
||||
libmicrohttpd12 libjansson4 libssl3 libsofia-sip-ua0 libglib2.0-0 libopus0 libogg0 libcurl4 libconfig9 libusrsctp2 libwebsockets17 libnss3 libasound2 nano \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN if [ ${TARGETARCH} = arm ]; then ARCH=armhf; elif [ ${TARGETARCH} = arm64 ]; then ARCH=aarch64; elif [ ${TARGETARCH} = amd64 ]; then ARCH=x86_64; fi \
|
||||
&& curl https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.$ARCH -L -o /usr/local/bin/ttyd \
|
||||
&& chmod +x /usr/local/bin/ttyd \
|
||||
&& ln -sf /usr/share/tesseract-ocr/*/tessdata /usr/share/tessdata
|
||||
|
||||
|
||||
COPY . /One-KVM
|
||||
|
||||
ENTRYPOINT ["bash"]
|
||||
@ -1,311 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
SRCPATH=/mnt/nas/src
|
||||
BOOTFS=/tmp/bootfs
|
||||
ROOTFS=/tmp/rootfs
|
||||
OUTPUTDIR=/mnt/nas/src/output
|
||||
LOOPDEV=/dev/loop10
|
||||
DATE=241204
|
||||
export LC_ALL=C
|
||||
|
||||
write_meta() {
|
||||
sudo chroot --userspec "root:root" $ROOTFS bash -c "sed -i 's/localhost.localdomain/$1/g' /etc/kvmd/meta.yaml"
|
||||
}
|
||||
|
||||
mount_rootfs() {
|
||||
mkdir $ROOTFS
|
||||
sudo mount $LOOPDEV $ROOTFS || exit -1
|
||||
sudo mount -t proc proc $ROOTFS/proc || exit -1
|
||||
sudo mount -t sysfs sys $ROOTFS/sys || exit -1
|
||||
sudo mount -o bind /dev $ROOTFS/dev || exit -1
|
||||
}
|
||||
|
||||
umount_rootfs() {
|
||||
sudo umount $ROOTFS/sys
|
||||
sudo umount $ROOTFS/dev
|
||||
sudo umount $ROOTFS/proc
|
||||
sudo umount $ROOTFS
|
||||
sudo losetup -d $LOOPDEV
|
||||
}
|
||||
|
||||
parpare_dns() {
|
||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
||||
mkdir -p /run/systemd/resolve/ \
|
||||
&& touch /run/systemd/resolve/stub-resolv.conf \
|
||||
&& printf '%s\n' 'nameserver 1.1.1.1' 'nameserver 1.0.0.1' > /etc/resolv.conf \
|
||||
&& bash <(curl -sSL https://gitee.com/SuperManito/LinuxMirrors/raw/main/ChangeMirrors.sh) \
|
||||
--source mirrors.tuna.tsinghua.edu.cn --updata-software false --web-protocol http "
|
||||
}
|
||||
|
||||
delete_armbain_verify(){
|
||||
sudo chroot --userspec "root:root" $ROOTFS bash -c "echo 'deb http://mirrors.ustc.edu.cn/armbian bullseye main bullseye-utils bullseye-desktop' > /etc/apt/sources.list.d/armbian.list "
|
||||
}
|
||||
|
||||
config_file() {
|
||||
sudo mkdir -p $ROOTFS/etc/kvmd/override.d $ROOTFS/etc/kvmd/vnc $ROOTFS/var/lib/kvmd/msd $ROOTFS/opt/vc/bin $ROOTFS/usr/share/kvmd $ROOTFS/One-KVM \
|
||||
$ROOTFS/usr/share/janus/javascript $ROOTFS/usr/lib/ustreamer/janus $ROOTFS/run/kvmd $ROOTFS/var/lib/kvmd/msd/images $ROOTFS/var/lib/kvmd/msd/meta
|
||||
sudo rsync -a --exclude={src,.github} . $ROOTFS/One-KVM
|
||||
sudo cp -r configs/kvmd/* configs/nginx configs/janus $ROOTFS/etc/kvmd
|
||||
sudo cp -r web extras contrib/keymaps $ROOTFS/usr/share/kvmd
|
||||
sudo cp testenv/fakes/vcgencmd $ROOTFS/usr/bin/
|
||||
sudo cp -r testenv/js/* $ROOTFS/usr/share/janus/javascript/
|
||||
sudo cp build/platform/$1 $ROOTFS/usr/share/kvmd/platform
|
||||
if [ -f "$SRCPATH/image/$1/rc.local" ]; then
|
||||
sudo cp $SRCPATH/image/$1/rc.local $ROOTFS/etc/
|
||||
fi
|
||||
}
|
||||
|
||||
pack_img() {
|
||||
sudo mv $SRCPATH/tmp/rootfs.img $OUTPUTDIR/One-KVM_by-SilentWind_$1_$DATE.img
|
||||
if [ "$1" = "Vm" ]; then
|
||||
sudo qemu-img convert -f raw -O vmdk $OUTPUTDIR/One-KVM_by-SilentWind_Vm_$DATE.img $OUTPUTDIR/One-KVM_by-SilentWind_Vmare-uefi_$DATE.vmdk
|
||||
sudo qemu-img convert -f raw -O vdi $OUTPUTDIR/One-KVM_by-SilentWind_Vm_$DATE.img $OUTPUTDIR/One-KVM_by-SilentWind_Virtualbox-uefi_$DATE.vdi
|
||||
fi
|
||||
}
|
||||
|
||||
onecloud_rootfs() {
|
||||
$SRCPATH/image/onecloud/AmlImg_v0.3.1_linux_amd64 unpack $SRCPATH/image/onecloud/Armbian_by-SilentWind_24.5.0-trunk_Onecloud_bookworm_legacy_5.9.0-rc7_minimal.burn.img $SRCPATH/tmp
|
||||
simg2img $SRCPATH/tmp/6.boot.PARTITION.sparse $SRCPATH/tmp/bootfs.img
|
||||
simg2img $SRCPATH/tmp/7.rootfs.PARTITION.sparse $SRCPATH/tmp/rootfs.img
|
||||
mkdir $BOOTFS
|
||||
sudo losetup $LOOPDEV $SRCPATH/tmp/bootfs.img || exit -1
|
||||
sudo mount $LOOPDEV $BOOTFS
|
||||
sudo cp $SRCPATH/image/onecloud/meson8b-onecloud-fix.dtb $BOOTFS/dtb/meson8b-onecloud.dtb
|
||||
sudo umount $BOOTFS
|
||||
sudo losetup -d $LOOPDEV
|
||||
dd if=/dev/zero of=/tmp/add.img bs=1M count=1024 && cat /tmp/add.img >> $SRCPATH/tmp/rootfs.img && rm /tmp/add.img
|
||||
e2fsck -f $SRCPATH/tmp/rootfs.img && resize2fs $SRCPATH/tmp/rootfs.img
|
||||
sudo losetup $LOOPDEV $SRCPATH/tmp/rootfs.img
|
||||
}
|
||||
|
||||
cumebox2_rootfs() {
|
||||
cp $SRCPATH/image/cumebox2/Armbian_24.8.1_Khadas-vim1_bookworm_current_6.6.47_minimal.img $SRCPATH/tmp/rootfs.img
|
||||
dd if=/dev/zero of=/tmp/add.img bs=1M count=1500 && cat /tmp/add.img >> $SRCPATH/tmp/rootfs.img && rm /tmp/add.img
|
||||
sudo parted -s $SRCPATH/tmp/rootfs.img resizepart 1 100% || exit -1
|
||||
sudo losetup --offset $((8192*512)) $LOOPDEV $SRCPATH/tmp/rootfs.img || exit -1
|
||||
sudo e2fsck -f $LOOPDEV && sudo resize2fs $LOOPDEV
|
||||
}
|
||||
|
||||
chainedbox_rootfs_and_fix_dtb() {
|
||||
cp $SRCPATH/image/chainedbox/Armbian_24.11.0_rockchip_chainedbox_bookworm_6.1.112_server_2024.10.02_add800m.img $SRCPATH/tmp/rootfs.img
|
||||
mkdir $BOOTFS
|
||||
sudo losetup --offset $((32768*512)) $LOOPDEV $SRCPATH/tmp/rootfs.img || exit -1
|
||||
sudo mount $LOOPDEV $BOOTFS
|
||||
sudo cp $SRCPATH/image/chainedbox/rk3328-l1pro-1296mhz-fix.dtb $BOOTFS/dtb/rockchip/rk3328-l1pro-1296mhz.dtb
|
||||
sudo umount $BOOTFS
|
||||
sudo losetup -d $LOOPDEV
|
||||
sudo losetup --offset $((1081344*512)) $LOOPDEV $SRCPATH/tmp/rootfs.img
|
||||
}
|
||||
|
||||
vm_rootfs() {
|
||||
cp $SRCPATH/image/vm/Armbian_24.8.1_Uefi-x86_bookworm_current_6.6.47_minimal_add1g.img $SRCPATH/tmp/rootfs.img
|
||||
sudo losetup --offset $((540672*512)) $LOOPDEV $SRCPATH/tmp/rootfs.img || exit -1
|
||||
}
|
||||
|
||||
e900v22c_rootfs() {
|
||||
cp $SRCPATH/image/e900v22c/Armbian_23.08.0_amlogic_s905l3a_bookworm_5.15.123_server_2023.08.01.img $SRCPATH/tmp/rootfs.img
|
||||
dd if=/dev/zero of=/tmp/add.img bs=1M count=400 && cat /tmp/add.img >> $SRCPATH/tmp/rootfs.img && rm /tmp/add.img
|
||||
sudo parted -s $SRCPATH/tmp/rootfs.img resizepart 2 100% || exit -1
|
||||
sudo losetup --offset $((532480*512)) $LOOPDEV $SRCPATH/tmp/rootfs.img || exit -1
|
||||
sudo e2fsck -f $LOOPDEV && sudo resize2fs $LOOPDEV
|
||||
}
|
||||
|
||||
|
||||
octopus-flanet_rootfs() {
|
||||
cp $SRCPATH/image/octopus-flanet/Armbian_24.11.0_amlogic_s912_bookworm_6.1.114_server_2024.11.01.img $SRCPATH/tmp/rootfs.img
|
||||
mkdir $BOOTFS
|
||||
sudo losetup --offset $((8192*512)) $LOOPDEV $SRCPATH/tmp/rootfs.img || exit -1
|
||||
sudo mount $LOOPDEV $BOOTFS
|
||||
sudo sed -i "s/meson-gxm-octopus-planet.dtb/meson-gxm-khadas-vim2.dtb/g" $BOOTFS/uEnv.txt
|
||||
sudo umount $BOOTFS
|
||||
sudo losetup -d $LOOPDEV
|
||||
dd if=/dev/zero of=/tmp/add.img bs=1M count=400 && cat /tmp/add.img >> $SRCPATH/tmp/rootfs.img && rm /tmp/add.img
|
||||
sudo parted -s $SRCPATH/tmp/rootfs.img resizepart 2 100% || exit -1
|
||||
sudo losetup --offset $((1056768*512)) $LOOPDEV $SRCPATH/tmp/rootfs.img || exit -1
|
||||
sudo e2fsck -f $LOOPDEV && sudo resize2fs $LOOPDEV
|
||||
}
|
||||
|
||||
|
||||
config_cumebox2_file() {
|
||||
sudo mkdir $ROOTFS/etc/oled
|
||||
sudo cp $SRCPATH/image/cumebox2/v-fix.dtb $ROOTFS/boot/dtb/amlogic/meson-gxl-s905x-khadas-vim.dtb
|
||||
sudo cp $SRCPATH/image/cumebox2/ssd $ROOTFS/usr/bin/
|
||||
sudo cp $SRCPATH/image/cumebox2/config.json $ROOTFS/etc/oled/config.json
|
||||
}
|
||||
|
||||
config_octopus-flanet_file() {
|
||||
sudo cp $SRCPATH/image/octopus-flanet/model_database.conf $ROOTFS/etc/model_database.conf
|
||||
}
|
||||
|
||||
instal_one-kvm() {
|
||||
#$1 arch; $2 deivce: "gpio" or "video1"; $3 network: "systemd-networkd",default is network-manager
|
||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
||||
df -h \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y python3-aiofiles python3-aiohttp python3-appdirs python3-asn1crypto python3-async-timeout \
|
||||
python3-bottle python3-cffi python3-chardet python3-click python3-colorama python3-cryptography python3-dateutil \
|
||||
python3-dbus python3-dev python3-hidapi python3-hid python3-idna python3-libgpiod python3-mako python3-marshmallow python3-more-itertools \
|
||||
python3-multidict python3-netifaces python3-packaging python3-passlib python3-pillow python3-ply python3-psutil \
|
||||
python3-pycparser python3-pyelftools python3-pyghmi python3-pygments python3-pyparsing python3-requests \
|
||||
python3-semantic-version python3-setproctitle python3-setuptools python3-six python3-spidev python3-systemd \
|
||||
python3-tabulate python3-urllib3 python3-wrapt python3-xlib python3-yaml python3-yarl python3-pyotp python3-qrcode \
|
||||
python3-serial python3-zstandard python3-dbus-next python3-pip python3-dev python3-build python3-wheel \
|
||||
nginx net-tools tesseract-ocr tesseract-ocr-eng tesseract-ocr-chi-sim cpufrequtils iptables network-manager \
|
||||
git gpiod libxkbcommon0 build-essential janus-dev libssl-dev libffi-dev libevent-dev libjpeg-dev libbsd-dev libudev-dev \
|
||||
pkg-config libx264-dev libyuv-dev libasound2-dev libsndfile-dev libspeexdsp-dev \
|
||||
&& rm -rf /var/lib/apt/lists/* "
|
||||
|
||||
sudo chroot --userspec "root:root" $ROOTFS sed --in-place --expression 's|^#include "refcount.h"$|#include "../refcount.h"|g' /usr/include/janus/plugins/plugin.h
|
||||
|
||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
||||
git clone --depth=1 https://github.com/mofeng-git/ustreamer /tmp/ustreamer \
|
||||
&& make -j WITH_PYTHON=1 WITH_JANUS=1 WITH_LIBX264=1 -C /tmp/ustreamer \
|
||||
&& cp /tmp/ustreamer/src/ustreamer.bin /usr/bin/ustreamer \
|
||||
&& cp /tmp/ustreamer/src/ustreamer-dump.bin /usr/bin/ustreamer-dump \
|
||||
&& chmod +x /usr/bin/ustreamer /usr/bin/ustreamer-dump \
|
||||
&& cp /tmp/ustreamer/janus/libjanus_ustreamer.so /usr/lib/ustreamer/janus \
|
||||
&& pip3 install --target=/usr/lib/python3/dist-packages --break-system-packages /tmp/ustreamer/python/dist/*.whl "
|
||||
|
||||
if [ "$3" = "systemd-networkd" ]; then
|
||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
||||
echo -e '[Match]\nName=eth0\n\n[Network]\nDHCP=yes\n\n[Link]\nMACAddress=B6:AE:B3:21:42:0C' > /etc/systemd/network/99-eth0.network \
|
||||
&& systemctl mask NetworkManager \
|
||||
&& systemctl unmask systemd-networkd \
|
||||
&& systemctl enable systemd-networkd systemd-resolved "
|
||||
fi
|
||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
||||
pip3 config set global.index-url https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple \
|
||||
&& pip3 install --target=/usr/lib/python3/dist-packages --break-system-packages async-lru gpiod pyfatfs \
|
||||
&& pip3 cache purge "
|
||||
|
||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
||||
cd /One-KVM \
|
||||
&& python3 setup.py install \
|
||||
&& bash scripts/kvmd-gencert --do-the-thing \
|
||||
&& bash scripts/kvmd-gencert --do-the-thing --vnc \
|
||||
&& kvmd-nginx-mkconf /etc/kvmd/nginx/nginx.conf.mako /etc/kvmd/nginx/nginx.conf \
|
||||
&& kvmd -m "
|
||||
|
||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
||||
cat /One-KVM/configs/os/sudoers/v2-hdmiusb >> /etc/sudoers \
|
||||
&& cat /One-KVM/configs/os/udev/v2-hdmiusb-generic.rules > /etc/udev/rules.d/99-kvmd.rules \
|
||||
&& echo 'libcomposite' >> /etc/modules \
|
||||
&& mv /usr/local/bin/kvmd* /usr/bin \
|
||||
&& cp /One-KVM/configs/os/services/* /etc/systemd/system/ \
|
||||
&& cp /One-KVM/configs/os/tmpfiles.conf /usr/lib/tmpfiles.d/ \
|
||||
&& chmod +x /etc/update-motd.d/* \
|
||||
&& echo 'kvmd ALL=(ALL) NOPASSWD: /etc/kvmd/custom_atx/gpio.sh' >> /etc/sudoers \
|
||||
&& echo 'kvmd ALL=(ALL) NOPASSWD: /etc/kvmd/custom_atx/usbrelay_hid.sh' >> /etc/sudoers \
|
||||
&& systemd-sysusers /One-KVM/configs/os/sysusers.conf \
|
||||
&& systemd-sysusers /One-KVM/configs/os/kvmd-webterm.conf \
|
||||
&& ln -sf /usr/share/tesseract-ocr/*/tessdata /usr/share/tessdata \
|
||||
&& sed -i 's/8080/80/g' /etc/kvmd/override.yaml \
|
||||
&& sed -i 's/4430/443/g' /etc/kvmd/override.yaml \
|
||||
&& chown kvmd -R /var/lib/kvmd/msd/ \
|
||||
&& systemctl enable kvmd kvmd-otg kvmd-nginx kvmd-vnc kvmd-ipmi kvmd-webterm kvmd-janus \
|
||||
&& systemctl disable nginx janus \
|
||||
&& rm -r /One-KVM "
|
||||
|
||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
||||
curl https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.$1 -L -o /usr/bin/ttyd \
|
||||
&& chmod +x /usr/bin/ttyd \
|
||||
&& mkdir -p /home/kvmd-webterm \
|
||||
&& chown kvmd-webterm /home/kvmd-webterm "
|
||||
|
||||
if [ "$1" = "x86_64" ]; then
|
||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
||||
systemctl disable kvmd-otg \
|
||||
&& sed -i '2c ATX=USBRELAY_HID' /etc/kvmd/atx.sh \
|
||||
&& sed -i 's/device: \/dev\/ttyUSB0/device: \/dev\/kvmd-hid/g' /etc/kvmd/override.yaml "
|
||||
else
|
||||
if [ "$2" = "gpio" ]; then
|
||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
||||
sed -i '2c ATX=GPIO' /etc/kvmd/atx.sh \
|
||||
&& sed -i 's/SHUTDOWNPIN/gpiochip1 7/g' /etc/kvmd/custom_atx/gpio.sh \
|
||||
&& sed -i 's/REBOOTPIN/gpiochip0 11/g' /etc/kvmd/custom_atx/gpio.sh "
|
||||
else
|
||||
sudo chroot --userspec "root:root" $ROOTFS sed -i '2c ATX=USBRELAY_HID' /etc/kvmd/atx.sh
|
||||
|
||||
fi
|
||||
if [ "$2" = "video1" ]; then
|
||||
sudo chroot --userspec "root:root" $ROOTFS sed -i 's/\/dev\/video0/\/dev\/video1/g' /etc/kvmd/override.yaml
|
||||
fi
|
||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
||||
sed -i 's/ch9329/otg/g' /etc/kvmd/override.yaml \
|
||||
&& sed -i 's/device: \/dev\/ttyUSB0//g' /etc/kvmd/override.yaml \
|
||||
&& sed -i 's/#type: otg/type: otg/g' /etc/kvmd/override.yaml "
|
||||
fi
|
||||
}
|
||||
|
||||
pack_img_onecloud() {
|
||||
sudo rm $SRCPATH/tmp/7.rootfs.PARTITION.sparse
|
||||
sudo img2simg $SRCPATH/tmp/rootfs.img $SRCPATH/tmp/7.rootfs.PARTITION.sparse
|
||||
sudo $SRCPATH/image/onecloud/AmlImg_v0.3.1_linux_amd64 pack $OUTPUTDIR/One-KVM_by-SilentWind_Onecloud_$DATE.burn.img $SRCPATH/tmp/
|
||||
sudo rm $SRCPATH/tmp/*
|
||||
}
|
||||
|
||||
case $1 in
|
||||
onecloud)
|
||||
onecloud_rootfs
|
||||
mount_rootfs
|
||||
config_file $1
|
||||
instal_one-kvm armhf gpio systemd-networkd
|
||||
write_meta $1
|
||||
umount_rootfs
|
||||
pack_img_onecloud
|
||||
;;
|
||||
cumebox2)
|
||||
cumebox2_rootfs
|
||||
mount_rootfs
|
||||
config_file $1
|
||||
config_cumebox2_file
|
||||
parpare_dns
|
||||
instal_one-kvm aarch64 video1
|
||||
write_meta $1
|
||||
umount_rootfs
|
||||
pack_img Cumebox2
|
||||
;;
|
||||
chainedbox)
|
||||
chainedbox_rootfs_and_fix_dtb
|
||||
mount_rootfs
|
||||
config_file $1
|
||||
parpare_dns
|
||||
instal_one-kvm aarch64 video1
|
||||
write_meta $1
|
||||
umount_rootfs
|
||||
pack_img Chainedbox
|
||||
;;
|
||||
vm)
|
||||
vm_rootfs
|
||||
mount_rootfs
|
||||
config_file $1
|
||||
parpare_dns
|
||||
instal_one-kvm x86_64
|
||||
write_meta $1
|
||||
umount_rootfs
|
||||
pack_img Vm
|
||||
;;
|
||||
e900v22c)
|
||||
e900v22c_rootfs
|
||||
mount_rootfs
|
||||
config_file $1
|
||||
instal_one-kvm aarch64 video1
|
||||
write_meta $1
|
||||
umount_rootfs
|
||||
pack_img E900v22c
|
||||
;;
|
||||
octopus-flanet)
|
||||
octopus-flanet_rootfs
|
||||
mount_rootfs
|
||||
config_file $1
|
||||
config_octopus-flanet_file
|
||||
parpare_dns
|
||||
instal_one-kvm aarch64 video1
|
||||
write_meta $1
|
||||
umount_rootfs
|
||||
pack_img Octopus-Flanet
|
||||
;;
|
||||
*)
|
||||
echo "Do no thing."
|
||||
;;
|
||||
esac
|
||||
@ -1,5 +0,0 @@
|
||||
[source.crates-io]
|
||||
replace-with = 'ustc'
|
||||
|
||||
[source.ustc]
|
||||
registry = "sparse+https://mirrors.ustc.edu.cn/crates.io-index/"
|
||||
166
build/init.sh
166
build/init.sh
@ -1,166 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${GREEN}One-KVM pre-starting...${NC}"
|
||||
|
||||
if [ ! -f /etc/kvmd/.init_flag ]; then
|
||||
echo -e "${GREEN}One-KVM is initializing first...${NC}" \
|
||||
&& mkdir -p /etc/kvmd/ \
|
||||
&& mv /etc/kvmd_backup/* /etc/kvmd/ \
|
||||
&& touch /etc/kvmd/.docker_flag \
|
||||
&& sed -i 's/localhost.localdomain/docker/g' /etc/kvmd/meta.yaml \
|
||||
&& sed -i 's/localhost/localhost:4430/g' /etc/kvmd/kvm_input.sh \
|
||||
&& /usr/share/kvmd/kvmd-gencert --do-the-thing \
|
||||
&& /usr/share/kvmd/kvmd-gencert --do-the-thing --vnc \
|
||||
|| echo -e "${RED}One-KVM config moving and self-signed SSL certificates init failed.${NC}"
|
||||
|
||||
if [ "$NOSSL" == 1 ]; then
|
||||
echo -e "${GREEN}One-KVM self-signed SSL is disabled.${NC}" \
|
||||
&& python -m kvmd.apps.ngxmkconf /etc/kvmd/nginx/nginx.conf.mako /etc/kvmd/nginx/nginx.conf -o nginx/https/enabled=false \
|
||||
|| echo -e "${RED}One-KVM nginx config init failed.${NC}"
|
||||
else
|
||||
python -m kvmd.apps.ngxmkconf /etc/kvmd/nginx/nginx.conf.mako /etc/kvmd/nginx/nginx.conf \
|
||||
|| echo -e "${RED}One-KVM nginx config init failed.${NC}"
|
||||
fi
|
||||
|
||||
if [ "$NOAUTH" == "1" ]; then
|
||||
sed -i "s/enabled: true/enabled: false/g" /etc/kvmd/override.yaml \
|
||||
&& echo -e "${GREEN}One-KVM auth is disabled.${NC}"
|
||||
fi
|
||||
|
||||
#add supervisord conf
|
||||
if [ "$NOWEBTERM" == "1" ]; then
|
||||
echo -e "${GREEN}One-KVM webterm is disabled.${NC}"
|
||||
rm -r /usr/share/kvmd/extras/webterm
|
||||
else
|
||||
cat >> /etc/kvmd/supervisord.conf << EOF
|
||||
|
||||
[program:kvmd-webterm]
|
||||
command=/usr/local/bin/ttyd --interface=/run/kvmd/ttyd.sock --port=0 --writable /bin/bash -c '/etc/kvmd/armbain-motd; bash'
|
||||
directory=/
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=14
|
||||
stopasgroup=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes = 0
|
||||
redirect_stderr=true
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [ "$NOWEBTERMWRITE" == "1" ]; then
|
||||
sed -i "s/--writable//g" /etc/kvmd/supervisord.conf
|
||||
fi
|
||||
|
||||
if [ "$NOVNC" == "1" ]; then
|
||||
echo -e "${GREEN}One-KVM VNC is disabled.${NC}"
|
||||
rm -r /usr/share/kvmd/extras/vnc
|
||||
else
|
||||
cat >> /etc/kvmd/supervisord.conf << EOF
|
||||
|
||||
[program:kvmd-vnc]
|
||||
command=python -m kvmd.apps.vnc --run
|
||||
directory=/
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=11
|
||||
stopasgroup=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes = 0
|
||||
redirect_stderr=true
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [ "$NOIPMI" == "1" ]; then
|
||||
echo -e "${GREEN}One-KVM IPMI is disabled.${NC}"
|
||||
rm -r /usr/share/kvmd/extras/ipmi
|
||||
else
|
||||
cat >> /etc/kvmd/supervisord.conf << EOF
|
||||
|
||||
[program:kvmd-ipmi]
|
||||
command=python -m kvmd.apps.ipmi --run
|
||||
directory=/
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=12
|
||||
stopasgroup=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes = 0
|
||||
redirect_stderr=true
|
||||
EOF
|
||||
fi
|
||||
|
||||
#switch OTG config
|
||||
if [ "$OTG" == "1" ]; then
|
||||
echo -e "${GREEN}One-KVM OTG is enabled.${NC}"
|
||||
sed -i "s/ch9329/otg/g" /etc/kvmd/override.yaml
|
||||
sed -i "s/device: \/dev\/ttyUSB0//g" /etc/kvmd/override.yaml
|
||||
if [ "$NOMSD" == 1 ]; then
|
||||
echo -e "${GREEN}One-KVM MSD is disabled.${NC}"
|
||||
else
|
||||
sed -i "s/#type: otg/type: otg/g" /etc/kvmd/override.yaml
|
||||
fi
|
||||
fi
|
||||
|
||||
#if [ ! -z "$SHUTDOWNPIN" ! -z "$REBOOTPIN" ]; then
|
||||
|
||||
if [ ! -z "$VIDEONUM" ]; then
|
||||
sed -i "s/\/dev\/video0/\/dev\/video$VIDEONUM/g" /etc/kvmd/override.yaml \
|
||||
&& sed -i "s/\/dev\/video0/\/dev\/video$VIDEONUM/g" /etc/kvmd/janus/janus.plugin.ustreamer.jcfg \
|
||||
&& echo -e "${GREEN}One-KVM video device is set to /dev/video$VIDEONUM.${NC}"
|
||||
fi
|
||||
|
||||
if [ ! -z "$AUDIONUM" ]; then
|
||||
sed -i "s/hw:0/hw:$AUDIONUM/g" /etc/kvmd/janus/janus.plugin.ustreamer.jcfg \
|
||||
&& echo -e "${GREEN}One-KVM audio device is set to hw:$VIDEONUM.${NC}"
|
||||
fi
|
||||
|
||||
if [ ! -z "$CH9329SPEED" ]; then
|
||||
sed -i "s/speed: 9600/speed: $CH9329SPEED/g" /etc/kvmd/override.yaml \
|
||||
&& echo -e "${GREEN}One-KVM CH9329 serial speed is set to $CH9329SPEED.${NC}"
|
||||
fi
|
||||
|
||||
if [ ! -z "$CH9329TIMEOUT" ]; then
|
||||
sed -i "s/read_timeout: 0.3/read_timeout: $CH9329TIMEOUT/g" /etc/kvmd/override.yaml \
|
||||
&& echo -e "${GREEN}One-KVM CH9329 timeout is set to $CH9329TIMEOUT s.${NC}"
|
||||
fi
|
||||
|
||||
#set htpasswd
|
||||
if [ ! -z "$USERNAME" ] && [ ! -z "$PASSWORD" ]; then
|
||||
python -m kvmd.apps.htpasswd del admin \
|
||||
&& echo $PASSWORD | python -m kvmd.apps.htpasswd set -i "$USERNAME" \
|
||||
&& echo "$PASSWORD -> $USERNAME:$PASSWORD" > /etc/kvmd/vncpasswd \
|
||||
&& echo "$USERNAME:$PASSWORD -> $USERNAME:$PASSWORD" > /etc/kvmd/ipmipasswd \
|
||||
|| echo -e "${RED}One-KVM htpasswd init failed.${NC}"
|
||||
else
|
||||
echo -e "${YELLOW} USERNAME and PASSWORD environment variables are not set, using defalut(admin/admin).${NC}"
|
||||
fi
|
||||
|
||||
if [ ! -z "$VIDEOFORMAT" ]; then
|
||||
sed -i "s/format=mjpeg/format=$VIDFORMAT/g" /etc/kvmd/override.yaml \
|
||||
&& echo -e "${GREEN}One-KVM input video format is set to $VIDFORMAT.${NC}"
|
||||
fi
|
||||
|
||||
touch /etc/kvmd/.init_flag
|
||||
fi
|
||||
|
||||
|
||||
#Trying usb_gadget
|
||||
if [ "$OTG" == "1" ]; then
|
||||
echo "Trying OTG Port..."
|
||||
rm -r /run/kvmd/otg &> /dev/null
|
||||
modprobe libcomposite || echo -e "${RED}Linux libcomposite module modprobe failed.${NC}"
|
||||
python -m kvmd.apps.otg start \
|
||||
&& ln -s /dev/hidg1 /dev/kvmd-hid-mouse \
|
||||
&& ln -s /dev/hidg0 /dev/kvmd-hid-keyboard \
|
||||
|| echo -e "${RED}OTG Port mount failed.${NC}"
|
||||
ln -s /dev/hidg2 /dev/kvmd-hid-mouse-alt
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}One-KVM starting...${NC}"
|
||||
exec supervisord -c /etc/kvmd/supervisord.conf
|
||||
@ -1,3 +0,0 @@
|
||||
PIKVM_MODEL=v2_model
|
||||
PIKVM_VIDEO=usb_video
|
||||
PIKVM_BOARD=chainedbox
|
||||
@ -1,3 +0,0 @@
|
||||
PIKVM_MODEL=v2_model
|
||||
PIKVM_VIDEO=usb_video
|
||||
PIKVM_BOARD=cumebox2
|
||||
@ -1,3 +0,0 @@
|
||||
PIKVM_MODEL=docker_model
|
||||
PIKVM_VIDEO=docker_video
|
||||
PIKVM_BOARD=docker_board
|
||||
@ -1,3 +0,0 @@
|
||||
PIKVM_MODEL=v2_model
|
||||
PIKVM_VIDEO=usb_video
|
||||
PIKVM_BOARD=e900v22c
|
||||
@ -1,3 +0,0 @@
|
||||
PIKVM_MODEL=v2_model
|
||||
PIKVM_VIDEO=usb_video
|
||||
PIKVM_BOARD=octopus-flanet
|
||||
@ -1,3 +0,0 @@
|
||||
PIKVM_MODEL=v2_model
|
||||
PIKVM_VIDEO=usb_video
|
||||
PIKVM_BOARD=onecloud
|
||||
@ -1,3 +0,0 @@
|
||||
PIKVM_MODEL=v2_model
|
||||
PIKVM_VIDEO=usb_video
|
||||
PIKVM_BOARD=vm
|
||||
108
kvmd.install
108
kvmd.install
@ -1,108 +0,0 @@
|
||||
# shellcheck disable=SC2148
|
||||
|
||||
# arg 1: the new package version
|
||||
post_install() {
|
||||
post_upgrade "$1" ""
|
||||
}
|
||||
|
||||
# arg 1: the new package version
|
||||
# arg 2: the old package version
|
||||
post_upgrade() {
|
||||
echo "==> Ensuring KVMD users and groups ..."
|
||||
systemd-sysusers /usr/lib/sysusers.d/kvmd.conf
|
||||
|
||||
# https://github.com/systemd/systemd/issues/13522
|
||||
# shellcheck disable=SC2013
|
||||
for user in $(grep '^u ' /usr/lib/sysusers.d/kvmd.conf | awk '{print $2}'); do
|
||||
usermod --expiredate= "$user" >/dev/null
|
||||
done
|
||||
|
||||
chown kvmd:kvmd /etc/kvmd/htpasswd || true
|
||||
chown kvmd:kvmd /etc/kvmd/totp.secret || true
|
||||
chown kvmd-ipmi:kvmd-ipmi /etc/kvmd/ipmipasswd || true
|
||||
chown kvmd-vnc:kvmd-vnc /etc/kvmd/vncpasswd || true
|
||||
chmod 600 /etc/kvmd/*passwd || true
|
||||
for target in nginx.conf.mako ssl.conf; do
|
||||
chmod 644 "/etc/kvmd/nginx/$target" || true
|
||||
done
|
||||
|
||||
chown kvmd /var/lib/kvmd/msd 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 ..."
|
||||
kvmd-gencert --do-the-thing
|
||||
fi
|
||||
|
||||
if [ ! -e /etc/kvmd/vnc/ssl/server.crt ]; then
|
||||
echo "==> Generating KVMD-VNC certificate ..."
|
||||
kvmd-gencert --do-the-thing --vnc
|
||||
fi
|
||||
|
||||
for target in nginx vnc; do
|
||||
chown root:root /etc/kvmd/$target/ssl || true
|
||||
owner="root:kvmd-$target"
|
||||
path="/etc/kvmd/$target/ssl/server.key"
|
||||
if [ ! -L "$path" ]; then
|
||||
chown "$owner" "$path" || true
|
||||
chmod 440 "$path" || true
|
||||
fi
|
||||
path="/etc/kvmd/$target/ssl/server.crt"
|
||||
if [ ! -L "$path" ]; then
|
||||
chown "$owner" "$path" || true
|
||||
chmod 444 "$path" || true
|
||||
fi
|
||||
done
|
||||
|
||||
echo "==> Patching configs ..."
|
||||
|
||||
if [[ "$(vercmp "$2" 3.301)" -lt 0 ]]; then
|
||||
[ ! -f /etc/fstab ] || (sed -i -e "s|,data=journal||g" /etc/fstab && touch -t 200701011000 /etc/fstab)
|
||||
[ ! -f /etc/fstab ] || (sed -i -e "/tmpfs \/run\s/d" /etc/fstab && touch -t 200701011000 /etc/fstab)
|
||||
[ ! -f /etc/pacman.conf ] || sed -i -e "s|^Server = https://pikvm.org/repos/|Server = https://files.pikvm.org/repos/arch/|g" /etc/pacman.conf
|
||||
[ ! -f /boot/config.txt ] || sed -i -e 's/^dtoverlay=pi3-disable-bt$/dtoverlay=disable-bt/g' /boot/config.txt
|
||||
[ ! -f /boot/config.txt ] || sed -i -e 's/^dtoverlay=dwc2$/dtoverlay=dwc2,dr_mode=peripheral/g' /boot/config.txt
|
||||
[ ! -f /etc/conf.d/rngd ] || (echo 'RNGD_OPTS="-o /dev/random -r /dev/hwrng -x jitter -x pkcs11 -x rtlsdr"' > /etc/conf.d/rngd)
|
||||
[ ! -f /etc/pam.d/system-login ] || sed -i -e '/\<pam_systemd\.so\>/ s/^#*/#/' /etc/pam.d/system-login
|
||||
[ ! -f /etc/pam.d/system-auth ] || sed -i -e '/\<pam_systemd_home\.so\>/ s/^#*/#/' /etc/pam.d/system-auth
|
||||
[ -e /etc/systemd/network/99-default.link ] || ln -s /dev/null /etc/systemd/network/99-default.link
|
||||
fi
|
||||
|
||||
if [[ "$(vercmp "$2" 3.317)" -lt 0 ]]; then
|
||||
[ ! -f /boot/config.txt ] || sed -i -e 's/^dtoverlay=i2c-rtc,pcf8563$/dtoverlay=i2c-rtc,pcf8563,wakeup-source/g' /boot/config.txt
|
||||
fi
|
||||
|
||||
if [[ "$(vercmp "$2" 3.320)" -lt 0 ]]; then
|
||||
# https://github.com/pikvm/pikvm/issues/1245
|
||||
systemctl mask \
|
||||
dirmngr@etc-pacman.d-gnupg.socket \
|
||||
gpg-agent-browser@etc-pacman.d-gnupg.socket \
|
||||
gpg-agent-extra@etc-pacman.d-gnupg.socket \
|
||||
gpg-agent-ssh@etc-pacman.d-gnupg.socket \
|
||||
gpg-agent@etc-pacman.d-gnupg.socket \
|
||||
keyboxd@etc-pacman.d-gnupg.socket
|
||||
fi
|
||||
|
||||
if [[ "$(vercmp "$2" 3.332)" -lt 0 ]]; then
|
||||
grep -q "^dtoverlay=vc4-kms-v3d" /boot/config.txt || cat << EOF >> /boot/config.txt
|
||||
|
||||
# Passthrough
|
||||
dtoverlay=vc4-kms-v3d
|
||||
disable_overscan=1
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [[ "$(vercmp "$2" 4.4)" -lt 0 ]]; then
|
||||
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
|
||||
}
|
||||
@ -1,354 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import json
|
||||
import time
|
||||
import argparse
|
||||
|
||||
from os.path import join # pylint: disable=ungrouped-imports
|
||||
|
||||
from ...logging import get_logger
|
||||
|
||||
from ...yamlconf import Section
|
||||
|
||||
from ...validators import ValidatorError
|
||||
|
||||
from ... import usb
|
||||
|
||||
from .. import init
|
||||
|
||||
from .hid import Hid
|
||||
from .hid.keyboard import make_keyboard_hid
|
||||
from .hid.mouse import make_mouse_hid
|
||||
|
||||
|
||||
# =====
|
||||
def _mkdir(path: str) -> None:
|
||||
get_logger().info("MKDIR --- %s", path)
|
||||
os.mkdir(path)
|
||||
|
||||
|
||||
def _chown(path: str, user: str, optional: bool=False) -> None:
|
||||
logger = get_logger()
|
||||
if optional and not os.access(path, os.F_OK):
|
||||
logger.info("CHOWN --- %s - [SKIPPED] %s", user, path)
|
||||
return
|
||||
logger.info("CHOWN --- %s - %s", user, path)
|
||||
shutil.chown(path, user)
|
||||
|
||||
|
||||
def _symlink(src: str, dest: str) -> None:
|
||||
get_logger().info("SYMLINK - %s --> %s", dest, src)
|
||||
os.symlink(src, dest)
|
||||
|
||||
|
||||
def _rmdir(path: str) -> None:
|
||||
get_logger().info("RMDIR --- %s", path)
|
||||
os.rmdir(path)
|
||||
|
||||
|
||||
def _unlink(path: str, optional: bool=False) -> None:
|
||||
logger = get_logger()
|
||||
if optional and not os.access(path, os.F_OK):
|
||||
logger.info("RM ------ [SKIPPED] %s", path)
|
||||
return
|
||||
logger.info("RM ------ %s", path)
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
def _write(path: str, value: (str | int), optional: bool=False) -> None:
|
||||
logger = get_logger()
|
||||
if optional and not os.access(path, os.F_OK):
|
||||
logger.info("WRITE --- [SKIPPED] %s", path)
|
||||
return
|
||||
logger.info("WRITE --- %s", path)
|
||||
with open(path, "w") as file:
|
||||
file.write(str(value))
|
||||
|
||||
|
||||
def _write_bytes(path: str, data: bytes) -> None:
|
||||
get_logger().info("WRITE --- %s", path)
|
||||
with open(path, "wb") as file:
|
||||
file.write(data)
|
||||
|
||||
|
||||
def _check_config(config: Section) -> None:
|
||||
if (
|
||||
not config.otg.devices.serial.enabled
|
||||
and not config.otg.devices.ethernet.enabled
|
||||
and config.kvmd.hid.type != "otg"
|
||||
and config.kvmd.msd.type != "otg"
|
||||
):
|
||||
raise RuntimeError("Nothing to do")
|
||||
|
||||
|
||||
# =====
|
||||
class _GadgetConfig:
|
||||
def __init__(self, gadget_path: str, profile_path: str, meta_path: str) -> None:
|
||||
self.__gadget_path = gadget_path
|
||||
self.__profile_path = profile_path
|
||||
self.__meta_path = meta_path
|
||||
self.__hid_instance = 0
|
||||
self.__msd_instance = 0
|
||||
_mkdir(meta_path)
|
||||
|
||||
def add_serial(self, start: bool) -> None:
|
||||
func = "acm.usb0"
|
||||
func_path = join(self.__gadget_path, "functions", func)
|
||||
_mkdir(func_path)
|
||||
if start:
|
||||
_symlink(func_path, join(self.__profile_path, func))
|
||||
self.__create_meta(func, "Serial Port")
|
||||
|
||||
def add_ethernet(self, start: bool, driver: str, host_mac: str, kvm_mac: str) -> None:
|
||||
if host_mac and kvm_mac and host_mac == kvm_mac:
|
||||
raise RuntimeError("Ethernet host_mac should not be equal to kvm_mac")
|
||||
real_driver = driver
|
||||
if driver == "rndis5":
|
||||
real_driver = "rndis"
|
||||
func = f"{real_driver}.usb0"
|
||||
func_path = join(self.__gadget_path, "functions", func)
|
||||
_mkdir(func_path)
|
||||
if host_mac:
|
||||
_write(join(func_path, "host_addr"), host_mac)
|
||||
if kvm_mac:
|
||||
_write(join(func_path, "dev_addr"), kvm_mac)
|
||||
if driver in ["ncm", "rndis"]:
|
||||
_write(join(self.__gadget_path, "os_desc/use"), "1")
|
||||
_write(join(self.__gadget_path, "os_desc/b_vendor_code"), "0xCD")
|
||||
_write(join(self.__gadget_path, "os_desc/qw_sign"), "MSFT100")
|
||||
if driver == "ncm":
|
||||
_write(join(func_path, "os_desc/interface.ncm/compatible_id"), "WINNCM")
|
||||
elif driver == "rndis":
|
||||
# On Windows 7 and later, the RNDIS 5.1 driver would be used by default,
|
||||
# but it does not work very well. The RNDIS 6.0 driver works better.
|
||||
# In order to get this driver to load automatically, we have to use
|
||||
# a Microsoft-specific extension of USB.
|
||||
_write(join(func_path, "os_desc/interface.rndis/compatible_id"), "RNDIS")
|
||||
_write(join(func_path, "os_desc/interface.rndis/sub_compatible_id"), "5162001")
|
||||
_symlink(self.__profile_path, join(self.__gadget_path, "os_desc", usb.G_PROFILE_NAME))
|
||||
if start:
|
||||
_symlink(func_path, join(self.__profile_path, func))
|
||||
self.__create_meta(func, "Ethernet")
|
||||
|
||||
def add_keyboard(self, start: bool, remote_wakeup: bool) -> None:
|
||||
self.__add_hid("Keyboard", start, remote_wakeup, make_keyboard_hid())
|
||||
|
||||
def add_mouse(self, start: bool, remote_wakeup: bool, absolute: bool, horizontal_wheel: bool) -> None:
|
||||
name = ("Absolute" if absolute else "Relative") + " Mouse"
|
||||
self.__add_hid(name, start, remote_wakeup, make_mouse_hid(absolute, horizontal_wheel))
|
||||
|
||||
def __add_hid(self, name: str, start: bool, remote_wakeup: bool, hid: Hid) -> None:
|
||||
func = f"hid.usb{self.__hid_instance}"
|
||||
func_path = join(self.__gadget_path, "functions", func)
|
||||
_mkdir(func_path)
|
||||
_write(join(func_path, "no_out_endpoint"), "1", optional=True)
|
||||
if remote_wakeup:
|
||||
_write(join(func_path, "wakeup_on_write"), "1", optional=True)
|
||||
_write(join(func_path, "protocol"), hid.protocol)
|
||||
_write(join(func_path, "subclass"), hid.subclass)
|
||||
_write(join(func_path, "report_length"), hid.report_length)
|
||||
_write_bytes(join(func_path, "report_desc"), hid.report_descriptor)
|
||||
if start:
|
||||
_symlink(func_path, join(self.__profile_path, func))
|
||||
self.__create_meta(func, name)
|
||||
self.__hid_instance += 1
|
||||
|
||||
def add_msd(self, start: bool, user: str, stall: bool, cdrom: bool, rw: bool, removable: bool, fua: bool) -> None:
|
||||
func = f"mass_storage.usb{self.__msd_instance}"
|
||||
func_path = join(self.__gadget_path, "functions", func)
|
||||
_mkdir(func_path)
|
||||
_write(join(func_path, "stall"), int(stall))
|
||||
_write(join(func_path, "lun.0/cdrom"), int(cdrom))
|
||||
_write(join(func_path, "lun.0/ro"), int(not rw))
|
||||
_write(join(func_path, "lun.0/removable"), int(removable))
|
||||
_write(join(func_path, "lun.0/nofua"), int(not fua))
|
||||
if user != "root":
|
||||
_chown(join(func_path, "lun.0/cdrom"), user)
|
||||
_chown(join(func_path, "lun.0/ro"), user)
|
||||
_chown(join(func_path, "lun.0/file"), user)
|
||||
_chown(join(func_path, "lun.0/forced_eject"), user, optional=True)
|
||||
if start:
|
||||
_symlink(func_path, join(self.__profile_path, func))
|
||||
name = ("Mass Storage Drive" if self.__msd_instance == 0 else f"Extra Drive #{self.__msd_instance}")
|
||||
self.__create_meta(func, name)
|
||||
self.__msd_instance += 1
|
||||
|
||||
def __create_meta(self, func: str, name: str) -> None:
|
||||
_write(join(self.__meta_path, f"{func}@meta.json"), json.dumps({"func": func, "name": name}))
|
||||
|
||||
|
||||
def _cmd_start(config: Section) -> None: # pylint: disable=too-many-statements,too-many-branches
|
||||
# https://www.kernel.org/doc/Documentation/usb/gadget_configfs.txt
|
||||
# https://www.isticktoit.net/?p=1383
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
_check_config(config)
|
||||
|
||||
udc = usb.find_udc(config.otg.udc)
|
||||
logger.info("Using UDC %s", udc)
|
||||
|
||||
logger.info("Creating gadget %r ...", config.otg.gadget)
|
||||
gadget_path = usb.get_gadget_path(config.otg.gadget)
|
||||
_mkdir(gadget_path)
|
||||
|
||||
_write(join(gadget_path, "idVendor"), f"0x{config.otg.vendor_id:04X}")
|
||||
_write(join(gadget_path, "idProduct"), f"0x{config.otg.product_id:04X}")
|
||||
_write(join(gadget_path, "bcdUSB"), f"0x{config.otg.usb_version:04X}")
|
||||
|
||||
# bcdDevice should be incremented any time there are breaking changes
|
||||
# to this script so that the host OS sees it as a new device
|
||||
# and re-enumerates everything rather than relying on cached values.
|
||||
device_version = config.otg.device_version
|
||||
if device_version < 0:
|
||||
device_version = 0x0100
|
||||
if config.otg.devices.ethernet.enabled:
|
||||
if config.otg.devices.ethernet.driver == "ncm":
|
||||
device_version = 0x0102
|
||||
elif config.otg.devices.ethernet.driver == "rndis":
|
||||
device_version = 0x0101
|
||||
_write(join(gadget_path, "bcdDevice"), f"0x{device_version:04X}")
|
||||
|
||||
lang_path = join(gadget_path, "strings/0x409")
|
||||
_mkdir(lang_path)
|
||||
_write(join(lang_path, "manufacturer"), config.otg.manufacturer)
|
||||
_write(join(lang_path, "product"), config.otg.product)
|
||||
if config.otg.serial is not None:
|
||||
_write(join(lang_path, "serialnumber"), config.otg.serial)
|
||||
|
||||
profile_path = join(gadget_path, usb.G_PROFILE)
|
||||
_mkdir(profile_path)
|
||||
_mkdir(join(profile_path, "strings/0x409"))
|
||||
_write(join(profile_path, "strings/0x409/configuration"), f"Config 1: {config.otg.config}")
|
||||
_write(join(profile_path, "MaxPower"), config.otg.max_power)
|
||||
if config.otg.remote_wakeup:
|
||||
# XXX: Should we use MaxPower=100 with Remote Wakeup?
|
||||
_write(join(profile_path, "bmAttributes"), "0xA0")
|
||||
|
||||
gc = _GadgetConfig(gadget_path, profile_path, config.otg.meta)
|
||||
cod = config.otg.devices
|
||||
|
||||
if cod.serial.enabled:
|
||||
logger.info("===== Serial =====")
|
||||
gc.add_serial(cod.serial.start)
|
||||
|
||||
if cod.ethernet.enabled:
|
||||
logger.info("===== Ethernet =====")
|
||||
gc.add_ethernet(**cod.ethernet._unpack(ignore=["enabled"]))
|
||||
|
||||
if config.kvmd.hid.type == "otg":
|
||||
logger.info("===== HID-Keyboard =====")
|
||||
gc.add_keyboard(cod.hid.keyboard.start, config.otg.remote_wakeup)
|
||||
logger.info("===== HID-Mouse =====")
|
||||
gc.add_mouse(cod.hid.mouse.start, config.otg.remote_wakeup, config.kvmd.hid.mouse.absolute, config.kvmd.hid.mouse.horizontal_wheel)
|
||||
if config.kvmd.hid.mouse_alt.device:
|
||||
logger.info("===== HID-Mouse-Alt =====")
|
||||
gc.add_mouse(cod.hid.mouse.start, config.otg.remote_wakeup, (not config.kvmd.hid.mouse.absolute), config.kvmd.hid.mouse.horizontal_wheel)
|
||||
|
||||
if config.kvmd.msd.type == "otg":
|
||||
logger.info("===== MSD =====")
|
||||
gc.add_msd(cod.msd.start, config.otg.user, **cod.msd.default._unpack())
|
||||
if cod.drives.enabled:
|
||||
for count in range(cod.drives.count):
|
||||
logger.info("===== MSD Extra: %d =====", count + 1)
|
||||
gc.add_msd(cod.drives.start, "root", **cod.drives.default._unpack())
|
||||
|
||||
logger.info("===== Preparing complete =====")
|
||||
|
||||
logger.info("Enabling the gadget ...")
|
||||
_write(join(gadget_path, "UDC"), udc)
|
||||
time.sleep(config.otg.init_delay)
|
||||
_chown(join(gadget_path, "UDC"), config.otg.user)
|
||||
_chown(profile_path, config.otg.user)
|
||||
|
||||
logger.info("Ready to work")
|
||||
|
||||
|
||||
# =====
|
||||
def _cmd_stop(config: Section) -> None:
|
||||
# https://www.kernel.org/doc/Documentation/usb/gadget_configfs.txt
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
_check_config(config)
|
||||
|
||||
gadget_path = usb.get_gadget_path(config.otg.gadget)
|
||||
|
||||
logger.info("Disabling gadget %r ...", config.otg.gadget)
|
||||
_write(join(gadget_path, "UDC"), "\n")
|
||||
|
||||
_unlink(join(gadget_path, "os_desc", usb.G_PROFILE_NAME), optional=True)
|
||||
|
||||
profile_path = join(gadget_path, usb.G_PROFILE)
|
||||
for func in os.listdir(profile_path):
|
||||
if re.search(r"\.usb\d+$", func):
|
||||
_unlink(join(profile_path, func))
|
||||
_rmdir(join(profile_path, "strings/0x409"))
|
||||
_rmdir(profile_path)
|
||||
|
||||
funcs_path = join(gadget_path, "functions")
|
||||
for func in os.listdir(funcs_path):
|
||||
if re.search(r"\.usb\d+$", func):
|
||||
_rmdir(join(funcs_path, func))
|
||||
|
||||
_rmdir(join(gadget_path, "strings/0x409"))
|
||||
_rmdir(gadget_path)
|
||||
|
||||
for meta in os.listdir(config.otg.meta):
|
||||
_unlink(join(config.otg.meta, meta))
|
||||
_rmdir(config.otg.meta)
|
||||
|
||||
logger.info("Bye-bye")
|
||||
|
||||
|
||||
# =====
|
||||
def main(argv: (list[str] | None)=None) -> None:
|
||||
(parent_parser, argv, config) = init(
|
||||
add_help=False,
|
||||
argv=argv,
|
||||
load_hid=True,
|
||||
load_atx=True,
|
||||
load_msd=True,
|
||||
)
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="kvmd-otg",
|
||||
description="Control KVMD OTG device",
|
||||
parents=[parent_parser],
|
||||
)
|
||||
parser.set_defaults(cmd=(lambda *_: parser.print_help()))
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
cmd_start_parser = subparsers.add_parser("start", help="Start OTG")
|
||||
cmd_start_parser.set_defaults(cmd=_cmd_start)
|
||||
|
||||
cmd_stop_parser = subparsers.add_parser("stop", help="Stop OTG")
|
||||
cmd_stop_parser.set_defaults(cmd=_cmd_stop)
|
||||
|
||||
options = parser.parse_args(argv[1:])
|
||||
try:
|
||||
options.cmd(config)
|
||||
except ValidatorError as ex:
|
||||
raise SystemExit(str(ex))
|
||||
@ -1,24 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from . import main
|
||||
main()
|
||||
@ -1,32 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import dataclasses
|
||||
|
||||
|
||||
# =====
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Hid:
|
||||
protocol: int
|
||||
subclass: int
|
||||
report_length: int
|
||||
report_descriptor: bytes
|
||||
@ -1,86 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from . import Hid
|
||||
|
||||
|
||||
# =====
|
||||
def make_keyboard_hid(report_id: (int | None)=None) -> Hid:
|
||||
return Hid(
|
||||
protocol=1, # Keyboard protocol
|
||||
subclass=1, # Boot interface subclass
|
||||
|
||||
report_length=8,
|
||||
|
||||
report_descriptor=bytes([
|
||||
# Logitech descriptor. It's very similar to https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt
|
||||
# Dumped using usbhid-dump; parsed using https://eleccelerator.com/usbdescreqparser
|
||||
|
||||
# Keyboard
|
||||
0x05, 0x01, # USAGE_PAGE (Generic Desktop)
|
||||
0x09, 0x06, # USAGE (Keyboard)
|
||||
0xA1, 0x01, # COLLECTION (Application)
|
||||
|
||||
# Report ID
|
||||
*([0x85, report_id] if report_id is not None else []),
|
||||
|
||||
# Modifiers
|
||||
0x05, 0x07, # USAGE_PAGE (Keyboard)
|
||||
0x19, 0xE0, # USAGE_MINIMUM (Keyboard LeftControl)
|
||||
0x29, 0xE7, # USAGE_MAXIMUM (Keyboard Right GUI)
|
||||
0x15, 0x00, # LOGICAL_MINIMUM (0)
|
||||
0x25, 0x01, # LOGICAL_MAXIMUM (1)
|
||||
0x75, 0x01, # REPORT_SIZE (1)
|
||||
0x95, 0x08, # REPORT_COUNT (8)
|
||||
0x81, 0x02, # INPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
|
||||
|
||||
# Reserved byte
|
||||
0x95, 0x01, # REPORT_COUNT (1)
|
||||
0x75, 0x08, # REPORT_SIZE (8)
|
||||
0x81, 0x01, # INPUT (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
|
||||
|
||||
# LEDs output
|
||||
0x95, 0x05, # REPORT_COUNT (5)
|
||||
0x75, 0x01, # REPORT_SIZE (1)
|
||||
0x05, 0x08, # USAGE_PAGE (LEDs)
|
||||
0x19, 0x01, # USAGE_MINIMUM (Num Lock)
|
||||
0x29, 0x05, # USAGE_MAXIMUM (Kana)
|
||||
0x91, 0x02, # OUTPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
|
||||
|
||||
# Reserved 3 bits in output
|
||||
0x95, 0x01, # REPORT_COUNT (1)
|
||||
0x75, 0x03, # REPORT_SIZE (3)
|
||||
0x91, 0x01, # OUTPUT (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
|
||||
|
||||
# 6 keys
|
||||
0x95, 0x06, # REPORT_COUNT (6)
|
||||
0x75, 0x08, # REPORT_SIZE (8)
|
||||
0x15, 0x00, # LOGICAL_MINIMUM (0)
|
||||
0x26, 0xFF, 0x00, # LOGICAL_MAXIMUM (0xFF)
|
||||
0x05, 0x07, # USAGE_PAGE (Keyboard)
|
||||
0x19, 0x00, # USAGE_MINIMUM (Reserved)
|
||||
0x2A, 0xFF, 0x00, # USAGE_MAXIMUM (0xFF)
|
||||
0x81, 0x00, # INPUT (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
|
||||
|
||||
0xC0, # END_COLLECTION
|
||||
]),
|
||||
)
|
||||
@ -1,158 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from . import Hid
|
||||
|
||||
|
||||
# =====
|
||||
def make_mouse_hid(absolute: bool, horizontal_wheel: bool, report_id: (int | None)=None) -> Hid:
|
||||
maker = (_make_absolute_hid if absolute else _make_relative_hid)
|
||||
return maker(horizontal_wheel, report_id)
|
||||
|
||||
|
||||
_HORIZONTAL_WHEEL = [
|
||||
0x05, 0x0C, # USAGE PAGE (Consumer Devices)
|
||||
0x0A, 0x38, 0x02, # USAGE (AC Pan)
|
||||
0x15, 0x81, # LOGICAL_MINIMUM (-127)
|
||||
0x25, 0x7F, # LOGICAL_MAXIMUM (127)
|
||||
0x75, 0x08, # REPORT_SIZE (8)
|
||||
0x95, 0x01, # REPORT_COUNT (1)
|
||||
0x81, 0x06, # INPUT (Data,Var,Rel)
|
||||
]
|
||||
|
||||
|
||||
def _make_absolute_hid(horizontal_wheel: bool, report_id: (int | None)) -> Hid:
|
||||
return Hid(
|
||||
protocol=0, # None protocol
|
||||
subclass=0, # No subclass
|
||||
|
||||
report_length=(7 if horizontal_wheel else 6),
|
||||
|
||||
report_descriptor=bytes([
|
||||
# https://github.com/NicoHood/HID/blob/0835e6a/src/SingleReport/SingleAbsoluteMouse.cpp
|
||||
# Репорт взят отсюда ^^^, но изменен диапазон значений координат перемещений.
|
||||
# Автор предлагает использовать -32768...32767, но семерка почему-то не хочет работать
|
||||
# с отрицательными значениями координат, как не хочет хавать 65536 и 32768.
|
||||
# Так что мы ей скармливаем диапазон 0...32767, и передаем рукожопам из микрософта привет,
|
||||
# потому что линуксы прекрасно работают с любыми двухбайтовыми диапазонами.
|
||||
|
||||
# Absolute mouse
|
||||
0x05, 0x01, # USAGE_PAGE (Generic Desktop)
|
||||
0x09, 0x02, # USAGE (Mouse)
|
||||
0xA1, 0x01, # COLLECTION (Application)
|
||||
|
||||
# Report ID
|
||||
*([0x85, report_id] if report_id is not None else []),
|
||||
|
||||
# Pointer and Physical are required by Apple Recovery
|
||||
0x09, 0x01, # USAGE (Pointer)
|
||||
0xA1, 0x00, # COLLECTION (Physical)
|
||||
|
||||
# 8 Buttons
|
||||
0x05, 0x09, # USAGE_PAGE (Button)
|
||||
0x19, 0x01, # USAGE_MINIMUM (Button 1)
|
||||
0x29, 0x08, # USAGE_MAXIMUM (Button 8)
|
||||
0x15, 0x00, # LOGICAL_MINIMUM (0)
|
||||
0x25, 0x01, # LOGICAL_MAXIMUM (1)
|
||||
0x95, 0x08, # REPORT_COUNT (8)
|
||||
0x75, 0x01, # REPORT_SIZE (1)
|
||||
0x81, 0x02, # INPUT (Data,Var,Abs)
|
||||
|
||||
# X, Y
|
||||
0x05, 0x01, # USAGE_PAGE (Generic Desktop)
|
||||
0x09, 0x30, # USAGE (X)
|
||||
0x09, 0x31, # USAGE (Y)
|
||||
0x16, 0x00, 0x00, # LOGICAL_MINIMUM (0)
|
||||
0x26, 0xFF, 0x7F, # LOGICAL_MAXIMUM (32767)
|
||||
0x75, 0x10, # REPORT_SIZE (16)
|
||||
0x95, 0x02, # REPORT_COUNT (2)
|
||||
0x81, 0x02, # INPUT (Data,Var,Abs)
|
||||
|
||||
# Wheel
|
||||
0x09, 0x38, # USAGE (Wheel)
|
||||
0x15, 0x81, # LOGICAL_MINIMUM (-127)
|
||||
0x25, 0x7F, # LOGICAL_MAXIMUM (127)
|
||||
0x75, 0x08, # REPORT_SIZE (8)
|
||||
0x95, 0x01, # REPORT_COUNT (1)
|
||||
0x81, 0x06, # INPUT (Data,Var,Rel)
|
||||
|
||||
*(_HORIZONTAL_WHEEL if horizontal_wheel else []),
|
||||
|
||||
# End
|
||||
0xC0, # END_COLLECTION (Physical)
|
||||
0xC0, # END_COLLECTION
|
||||
]),
|
||||
)
|
||||
|
||||
|
||||
def _make_relative_hid(horizontal_wheel: bool, report_id: (int | None)) -> Hid:
|
||||
return Hid(
|
||||
protocol=2, # Mouse protocol
|
||||
subclass=1, # Boot interface subclass
|
||||
|
||||
report_length=(5 if horizontal_wheel else 4),
|
||||
|
||||
report_descriptor=bytes([
|
||||
# https://github.com/NicoHood/HID/blob/0835e6a/src/SingleReport/BootMouse.cpp
|
||||
|
||||
# Relative mouse
|
||||
0x05, 0x01, # USAGE_PAGE (Generic Desktop)
|
||||
0x09, 0x02, # USAGE (Mouse)
|
||||
0xA1, 0x01, # COLLECTION (Application)
|
||||
|
||||
# Report ID
|
||||
*([0x85, report_id] if report_id is not None else []),
|
||||
|
||||
# Pointer and Physical are required by Apple Recovery
|
||||
0x09, 0x01, # USAGE (Pointer)
|
||||
0xA1, 0x00, # COLLECTION (Physical)
|
||||
|
||||
# 8 Buttons
|
||||
0x05, 0x09, # USAGE_PAGE (Button)
|
||||
0x19, 0x01, # USAGE_MINIMUM (Button 1)
|
||||
0x29, 0x08, # USAGE_MAXIMUM (Button 8)
|
||||
0x15, 0x00, # LOGICAL_MINIMUM (0)
|
||||
0x25, 0x01, # LOGICAL_MAXIMUM (1)
|
||||
0x95, 0x08, # REPORT_COUNT (8)
|
||||
0x75, 0x01, # REPORT_SIZE (1)
|
||||
0x81, 0x02, # INPUT (Data,Var,Abs)
|
||||
|
||||
# X, Y
|
||||
0x05, 0x01, # USAGE_PAGE (Generic Desktop)
|
||||
0x09, 0x30, # USAGE (X)
|
||||
0x09, 0x31, # USAGE (Y)
|
||||
|
||||
# Wheel
|
||||
0x09, 0x38, # USAGE (Wheel)
|
||||
0x15, 0x81, # LOGICAL_MINIMUM (-127)
|
||||
0x25, 0x7F, # LOGICAL_MAXIMUM (127)
|
||||
0x75, 0x08, # REPORT_SIZE (8)
|
||||
0x95, 0x03, # REPORT_COUNT (3)
|
||||
0x81, 0x06, # INPUT (Data,Var,Rel)
|
||||
|
||||
*(_HORIZONTAL_WHEEL if horizontal_wheel else []),
|
||||
|
||||
# End
|
||||
0xC0, # END_COLLECTION (Physical)
|
||||
0xC0, # END_COLLECTION
|
||||
]),
|
||||
)
|
||||
@ -1,190 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import os
|
||||
import json
|
||||
import contextlib
|
||||
import argparse
|
||||
import time
|
||||
|
||||
from typing import Generator
|
||||
|
||||
import yaml
|
||||
|
||||
from ...validators.basic import valid_stripped_string_not_empty
|
||||
|
||||
from ... import usb
|
||||
|
||||
from .. import init
|
||||
|
||||
|
||||
# =====
|
||||
class _GadgetControl:
|
||||
def __init__(self, meta_path: str, gadget: str, udc: str, init_delay: float) -> None:
|
||||
self.__meta_path = meta_path
|
||||
self.__gadget = gadget
|
||||
self.__udc = udc
|
||||
self.__init_delay = init_delay
|
||||
|
||||
@contextlib.contextmanager
|
||||
def __udc_stopped(self) -> Generator[None, None, None]:
|
||||
udc = usb.find_udc(self.__udc)
|
||||
udc_path = usb.get_gadget_path(self.__gadget, usb.G_UDC)
|
||||
with open(udc_path) as file:
|
||||
enabled = bool(file.read().strip())
|
||||
if enabled:
|
||||
with open(udc_path, "w") as file:
|
||||
file.write("\n")
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.__recreate_profile()
|
||||
time.sleep(self.__init_delay)
|
||||
with open(udc_path, "w") as file:
|
||||
file.write(udc)
|
||||
|
||||
def __recreate_profile(self) -> None:
|
||||
# XXX: See pikvm/pikvm#1235
|
||||
# After unbind and bind, the gadgets stop working,
|
||||
# unless we recreate their links in the profile.
|
||||
# Some kind of kernel bug.
|
||||
for func in os.listdir(self.__get_fdest_path()):
|
||||
path = self.__get_fdest_path(func)
|
||||
if os.path.islink(path):
|
||||
try:
|
||||
os.unlink(path)
|
||||
os.symlink(self.__get_fsrc_path(func), path)
|
||||
except (FileNotFoundError, FileExistsError):
|
||||
pass
|
||||
|
||||
def __read_metas(self) -> Generator[dict, None, None]:
|
||||
for meta_name in sorted(os.listdir(self.__meta_path)):
|
||||
with open(os.path.join(self.__meta_path, meta_name)) as file:
|
||||
yield json.loads(file.read())
|
||||
|
||||
def __get_fsrc_path(self, func: str) -> str:
|
||||
return usb.get_gadget_path(self.__gadget, usb.G_FUNCTIONS, func)
|
||||
|
||||
def __get_fdest_path(self, func: (str | None)=None) -> str:
|
||||
if func is None:
|
||||
return usb.get_gadget_path(self.__gadget, usb.G_PROFILE)
|
||||
return usb.get_gadget_path(self.__gadget, usb.G_PROFILE, func)
|
||||
|
||||
def enable_functions(self, funcs: list[str]) -> None:
|
||||
with self.__udc_stopped():
|
||||
for func in funcs:
|
||||
os.symlink(self.__get_fsrc_path(func), self.__get_fdest_path(func))
|
||||
|
||||
def disable_functions(self, funcs: list[str]) -> None:
|
||||
with self.__udc_stopped():
|
||||
for func in funcs:
|
||||
os.unlink(self.__get_fdest_path(func))
|
||||
|
||||
def list_functions(self) -> None:
|
||||
for meta in self.__read_metas():
|
||||
enabled = os.path.exists(self.__get_fdest_path(meta["func"]))
|
||||
print(f"{'+' if enabled else '-'} {meta['func']} # {meta['name']}")
|
||||
|
||||
def make_gpio_config(self) -> None:
|
||||
class Dumper(yaml.Dumper):
|
||||
def increase_indent(self, flow: bool=False, indentless: bool=False) -> None:
|
||||
_ = indentless
|
||||
super().increase_indent(flow, False)
|
||||
|
||||
def ignore_aliases(self, data) -> bool: # type: ignore
|
||||
_ = data
|
||||
return True
|
||||
|
||||
class InlineList(list):
|
||||
pass
|
||||
|
||||
def represent_inline_list(dumper: yaml.Dumper, data): # type: ignore
|
||||
return dumper.represent_sequence("tag:yaml.org,2002:seq", data, flow_style=True)
|
||||
|
||||
Dumper.add_representer(InlineList, represent_inline_list)
|
||||
|
||||
config = {
|
||||
"drivers": {"otgconf": {"type": "otgconf"}},
|
||||
"scheme": {},
|
||||
"view": {"table": []},
|
||||
}
|
||||
for meta in self.__read_metas():
|
||||
config["scheme"][meta["func"]] = { # type: ignore
|
||||
"driver": "otgconf",
|
||||
"pin": meta["func"],
|
||||
"mode": "output",
|
||||
"pulse": False,
|
||||
}
|
||||
config["view"]["table"].append(InlineList([ # type: ignore
|
||||
"#" + meta["name"],
|
||||
"#" + meta["func"],
|
||||
meta["func"],
|
||||
]))
|
||||
print(yaml.dump({"kvmd": {"gpio": config}}, indent=4, Dumper=Dumper))
|
||||
|
||||
def reset(self) -> None:
|
||||
with self.__udc_stopped():
|
||||
pass
|
||||
|
||||
|
||||
# =====
|
||||
def main(argv: (list[str] | None)=None) -> None:
|
||||
(parent_parser, argv, config) = init(
|
||||
add_help=False,
|
||||
cli_logging=True,
|
||||
argv=argv,
|
||||
)
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="kvmd-otgconf",
|
||||
description="KVMD OTG low-level runtime configuration tool",
|
||||
parents=[parent_parser],
|
||||
)
|
||||
parser.add_argument("-l", "--list-functions", action="store_true", help="List functions")
|
||||
parser.add_argument("-e", "--enable-function", nargs="+", metavar="<name>", help="Enable function(s)")
|
||||
parser.add_argument("-d", "--disable-function", nargs="+", metavar="<name>", help="Disable function(s)")
|
||||
parser.add_argument("-r", "--reset-gadget", action="store_true", help="Reset gadget")
|
||||
parser.add_argument("--make-gpio-config", action="store_true")
|
||||
options = parser.parse_args(argv[1:])
|
||||
|
||||
gc = _GadgetControl(config.otg.meta, config.otg.gadget, config.otg.udc, config.otg.init_delay)
|
||||
|
||||
if options.list_functions:
|
||||
gc.list_functions()
|
||||
|
||||
elif options.enable_function:
|
||||
funcs = list(map(valid_stripped_string_not_empty, options.enable_function))
|
||||
gc.enable_functions(funcs)
|
||||
gc.list_functions()
|
||||
|
||||
elif options.disable_function:
|
||||
funcs = list(map(valid_stripped_string_not_empty, options.disable_function))
|
||||
gc.disable_functions(funcs)
|
||||
gc.list_functions()
|
||||
|
||||
elif options.reset_gadget:
|
||||
gc.reset()
|
||||
|
||||
elif options.make_gpio_config:
|
||||
gc.make_gpio_config()
|
||||
|
||||
else:
|
||||
gc.list_functions()
|
||||
@ -1,24 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from . import main
|
||||
main()
|
||||
@ -1,112 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import os
|
||||
import errno
|
||||
import argparse
|
||||
|
||||
from ...validators.basic import valid_bool
|
||||
from ...validators.basic import valid_int_f0
|
||||
from ...validators.os import valid_abs_file
|
||||
|
||||
from ... import usb
|
||||
|
||||
from .. import init
|
||||
|
||||
|
||||
# =====
|
||||
def _has_param(gadget: str, instance: int, param: str) -> bool:
|
||||
return os.access(_get_param_path(gadget, instance, param), os.F_OK)
|
||||
|
||||
def _get_param_path(gadget: str, instance: int, param: str) -> str:
|
||||
return usb.get_gadget_path(gadget, usb.G_FUNCTIONS, f"mass_storage.usb{instance}/lun.0", param)
|
||||
|
||||
|
||||
def _get_param(gadget: str, instance: int, param: str) -> str:
|
||||
with open(_get_param_path(gadget, instance, param)) as file:
|
||||
return file.read().strip()
|
||||
|
||||
|
||||
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 ex:
|
||||
if ex.errno == errno.EBUSY:
|
||||
raise SystemExit(f"Can't change {param!r} value because device is locked: {ex}")
|
||||
raise
|
||||
|
||||
|
||||
# =====
|
||||
def main(argv: (list[str] | None)=None) -> None:
|
||||
(parent_parser, argv, config) = init(
|
||||
add_help=False,
|
||||
cli_logging=True,
|
||||
argv=argv,
|
||||
load_msd=True,
|
||||
)
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="kvmd-otgmsd",
|
||||
description="KVMD OTG-MSD low-level hand tool",
|
||||
parents=[parent_parser],
|
||||
)
|
||||
parser.add_argument("-i", "--instance", default=0, type=valid_int_f0,
|
||||
metavar="<N>", help="Drive instance (0 for KVMD drive)")
|
||||
parser.add_argument("--set-cdrom", default=None, type=valid_bool,
|
||||
metavar="<1|0|yes|no>", help="Set CD-ROM flag")
|
||||
parser.add_argument("--set-rw", default=None, type=valid_bool,
|
||||
metavar="<1|0|yes|no>", help="Set RW flag")
|
||||
parser.add_argument("--set-image", default=None, type=valid_abs_file,
|
||||
metavar="<path>", help="Set the image file")
|
||||
parser.add_argument("--eject", action="store_true",
|
||||
help="Eject the image")
|
||||
parser.add_argument("--unlock", action="store_true",
|
||||
help="Does nothing, just for backward compatibility")
|
||||
options = parser.parse_args(argv[1:])
|
||||
|
||||
if config.kvmd.msd.type != "otg":
|
||||
raise SystemExit(f"Error: KVMD MSD not using 'otg'"
|
||||
f" (now configured {config.kvmd.msd.type!r})")
|
||||
has_param = (lambda param: _has_param(config.otg.gadget, options.instance, param))
|
||||
set_param = (lambda param, value: _set_param(config.otg.gadget, options.instance, param, value))
|
||||
get_param = (lambda param: _get_param(config.otg.gadget, options.instance, param))
|
||||
|
||||
if options.eject:
|
||||
if has_param("forced_eject"):
|
||||
set_param("forced_eject", "")
|
||||
else:
|
||||
set_param("file", "")
|
||||
|
||||
if options.set_cdrom is not None:
|
||||
set_param("cdrom", str(int(options.set_cdrom)))
|
||||
|
||||
if options.set_rw is not None:
|
||||
set_param("ro", str(int(not options.set_rw)))
|
||||
|
||||
if options.set_image:
|
||||
if not os.path.isfile(options.set_image):
|
||||
raise SystemExit(f"Not a file: {options.set_image}")
|
||||
set_param("file", options.set_image)
|
||||
|
||||
print("Image file: ", (get_param("file") or "<none>"))
|
||||
print("CD-ROM flag:", ("yes" if int(get_param("cdrom")) else "no"))
|
||||
print("RW flag: ", ("no" if int(get_param("ro")) else "yes"))
|
||||
@ -1,24 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from . import main
|
||||
main()
|
||||
@ -1,209 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import asyncio
|
||||
import ipaddress
|
||||
import dataclasses
|
||||
import itertools
|
||||
import argparse
|
||||
|
||||
from ...logging import get_logger
|
||||
|
||||
from ...yamlconf import Section
|
||||
|
||||
from ... import tools
|
||||
from ... import aioproc
|
||||
from ... import usb
|
||||
|
||||
from .. import init
|
||||
|
||||
from .netctl import BaseCtl
|
||||
from .netctl import IfaceUpCtl
|
||||
from .netctl import IfaceAddIpCtl
|
||||
from .netctl import IptablesAllowEstRelCtl
|
||||
from .netctl import IptablesDropAllCtl
|
||||
from .netctl import IptablesAllowIcmpCtl
|
||||
from .netctl import IptablesAllowPortCtl
|
||||
from .netctl import IptablesForwardOut
|
||||
from .netctl import IptablesForwardIn
|
||||
from .netctl import CustomCtl
|
||||
|
||||
|
||||
# =====
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class _Netcfg: # pylint: disable=too-many-instance-attributes
|
||||
iface: str
|
||||
iface_ip: str
|
||||
net_ip: str
|
||||
net_prefix: int
|
||||
net_mask: str
|
||||
dhcp_ip_begin: str
|
||||
dhcp_ip_end: str
|
||||
dhcp_option_3: str
|
||||
|
||||
|
||||
class _Service: # pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, config: Section) -> None:
|
||||
self.__iface_net: str = config.otgnet.iface.net
|
||||
self.__ip_cmd: list[str] = config.otgnet.iface.ip_cmd
|
||||
|
||||
self.__allow_icmp: bool = config.otgnet.firewall.allow_icmp
|
||||
self.__allow_tcp: list[int] = sorted(set(config.otgnet.firewall.allow_tcp))
|
||||
self.__allow_udp: list[int] = sorted(set(config.otgnet.firewall.allow_udp))
|
||||
self.__forward_iface: str = config.otgnet.firewall.forward_iface
|
||||
self.__iptables_cmd: list[str] = config.otgnet.firewall.iptables_cmd
|
||||
|
||||
def build_cmd(key: str) -> list[str]:
|
||||
return tools.build_cmd(
|
||||
getattr(config.otgnet.commands, key),
|
||||
getattr(config.otgnet.commands, f"{key}_remove"),
|
||||
getattr(config.otgnet.commands, f"{key}_append"),
|
||||
)
|
||||
|
||||
self.__pre_start_cmd: list[str] = build_cmd("pre_start_cmd")
|
||||
self.__post_start_cmd: list[str] = build_cmd("post_start_cmd")
|
||||
self.__pre_stop_cmd: list[str] = build_cmd("pre_stop_cmd")
|
||||
self.__post_stop_cmd: list[str] = build_cmd("post_stop_cmd")
|
||||
|
||||
self.__gadget: str = config.otg.gadget
|
||||
self.__driver: str = config.otg.devices.ethernet.driver
|
||||
|
||||
def start(self) -> None:
|
||||
asyncio.run(self.__run(True))
|
||||
|
||||
def stop(self) -> None:
|
||||
asyncio.run(self.__run(False))
|
||||
|
||||
async def __run(self, direct: bool) -> None:
|
||||
netcfg = self.__make_netcfg()
|
||||
placeholders = {
|
||||
key: str(value)
|
||||
for (key, value) in dataclasses.asdict(netcfg).items()
|
||||
}
|
||||
ctls: list[BaseCtl] = [
|
||||
CustomCtl(self.__pre_start_cmd, self.__post_stop_cmd, placeholders),
|
||||
IfaceUpCtl(self.__ip_cmd, netcfg.iface),
|
||||
IptablesAllowEstRelCtl(self.__iptables_cmd, netcfg.iface),
|
||||
*([IptablesAllowIcmpCtl(self.__iptables_cmd, netcfg.iface)] if self.__allow_icmp else []),
|
||||
*[
|
||||
IptablesAllowPortCtl(self.__iptables_cmd, netcfg.iface, port, tcp)
|
||||
for (port, tcp) in [
|
||||
*zip(self.__allow_tcp, itertools.repeat(True)),
|
||||
*zip(self.__allow_udp, itertools.repeat(False)),
|
||||
]
|
||||
],
|
||||
*([IptablesForwardOut(self.__iptables_cmd, self.__forward_iface)] if self.__forward_iface else []),
|
||||
*([IptablesForwardIn(self.__iptables_cmd, netcfg.iface)] if self.__forward_iface else []),
|
||||
IptablesDropAllCtl(self.__iptables_cmd, netcfg.iface),
|
||||
IfaceAddIpCtl(self.__ip_cmd, netcfg.iface, f"{netcfg.iface_ip}/{netcfg.net_prefix}"),
|
||||
CustomCtl(self.__post_start_cmd, self.__pre_stop_cmd, placeholders),
|
||||
]
|
||||
if direct:
|
||||
for ctl in ctls:
|
||||
if not (await self.__run_ctl(ctl, True)):
|
||||
raise SystemExit(1)
|
||||
get_logger(0).info("Ready to work")
|
||||
else:
|
||||
for ctl in reversed(ctls):
|
||||
await self.__run_ctl(ctl, False)
|
||||
get_logger(0).info("Bye-bye")
|
||||
|
||||
async def __run_ctl(self, ctl: BaseCtl, direct: bool) -> bool:
|
||||
logger = get_logger()
|
||||
cmd = ctl.get_command(direct)
|
||||
logger.info("CMD: %s", tools.cmdfmt(cmd))
|
||||
try:
|
||||
return (not (await aioproc.log_process(cmd, logger)).returncode)
|
||||
except Exception as ex:
|
||||
logger.exception("Can't execute command: %s", ex)
|
||||
return False
|
||||
|
||||
# =====
|
||||
|
||||
def __make_netcfg(self) -> _Netcfg:
|
||||
iface = self.__find_iface()
|
||||
logger = get_logger()
|
||||
|
||||
logger.info("Using IPv4 network %s ...", self.__iface_net)
|
||||
net = ipaddress.IPv4Network(self.__iface_net)
|
||||
if net.prefixlen > 31:
|
||||
raise RuntimeError("Too small network, required at least /31")
|
||||
|
||||
if net.prefixlen == 31:
|
||||
iface_ip = str(net[0])
|
||||
dhcp_ip_begin = dhcp_ip_end = str(net[1])
|
||||
else:
|
||||
iface_ip = str(net[1])
|
||||
dhcp_ip_begin = str(net[2])
|
||||
dhcp_ip_end = str(net[-2])
|
||||
|
||||
netcfg = _Netcfg(
|
||||
iface=iface,
|
||||
iface_ip=iface_ip,
|
||||
net_ip=str(net.network_address),
|
||||
net_prefix=net.prefixlen,
|
||||
net_mask=str(net.netmask),
|
||||
dhcp_ip_begin=dhcp_ip_begin,
|
||||
dhcp_ip_end=dhcp_ip_end,
|
||||
dhcp_option_3=(f"3,{iface_ip}" if self.__forward_iface else "3"),
|
||||
)
|
||||
logger.info("Calculated %r address is %s/%d", iface, iface_ip, netcfg.net_prefix)
|
||||
return netcfg
|
||||
|
||||
def __find_iface(self) -> str:
|
||||
logger = get_logger()
|
||||
real_driver = self.__driver
|
||||
if self.__driver == "rndis5":
|
||||
real_driver = "rndis"
|
||||
path = usb.get_gadget_path(self.__gadget, usb.G_FUNCTIONS, f"{real_driver}.usb0/ifname")
|
||||
logger.info("Using OTG gadget %r ...", self.__gadget)
|
||||
with open(path) as file:
|
||||
iface = file.read().strip()
|
||||
logger.info("Using OTG Ethernet interface %r ...", iface)
|
||||
assert iface
|
||||
return iface
|
||||
|
||||
|
||||
# =====
|
||||
def main(argv: (list[str] | None)=None) -> None:
|
||||
(parent_parser, argv, config) = init(
|
||||
add_help=False,
|
||||
argv=argv,
|
||||
)
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="kvmd-otgnet",
|
||||
description="Control KVMD OTG network",
|
||||
parents=[parent_parser],
|
||||
)
|
||||
parser.set_defaults(cmd=(lambda *_: parser.print_help()))
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
service = _Service(config)
|
||||
|
||||
cmd_start_parser = subparsers.add_parser("start", help="Start OTG network")
|
||||
cmd_start_parser.set_defaults(cmd=service.start)
|
||||
|
||||
cmd_stop_parser = subparsers.add_parser("stop", help="Stop OTG network")
|
||||
cmd_stop_parser.set_defaults(cmd=service.stop)
|
||||
|
||||
options = parser.parse_args(argv[1:])
|
||||
options.cmd()
|
||||
@ -1,24 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from . import main
|
||||
main()
|
||||
@ -1,140 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
# =====
|
||||
class BaseCtl:
|
||||
def get_command(self, direct: bool) -> list[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class IfaceUpCtl(BaseCtl):
|
||||
def __init__(self, base_cmd: list[str], iface: str) -> None:
|
||||
self.__base_cmd = base_cmd
|
||||
self.__iface = iface
|
||||
|
||||
def get_command(self, direct: bool) -> list[str]:
|
||||
return [*self.__base_cmd, "link", "set", self.__iface, ("up" if direct else "down")]
|
||||
|
||||
|
||||
class IfaceAddIpCtl(BaseCtl):
|
||||
def __init__(self, base_cmd: list[str], iface: str, cidr: str) -> None:
|
||||
self.__base_cmd = base_cmd
|
||||
self.__iface = iface
|
||||
self.__cidr = cidr
|
||||
|
||||
def get_command(self, direct: bool) -> list[str]:
|
||||
return [*self.__base_cmd, "address", ("add" if direct else "del"), self.__cidr, "dev", self.__iface]
|
||||
|
||||
|
||||
class IptablesAllowEstRelCtl(BaseCtl):
|
||||
def __init__(self, base_cmd: list[str], iface: str) -> None:
|
||||
self.__base_cmd = base_cmd
|
||||
self.__iface = iface
|
||||
|
||||
def get_command(self, direct: bool) -> list[str]:
|
||||
return [
|
||||
*self.__base_cmd,
|
||||
("-A" if direct else "-D"), "INPUT", "-i", self.__iface,
|
||||
"-m", "state", "--state", "ESTABLISHED,RELATED", "-j", "ACCEPT",
|
||||
]
|
||||
|
||||
|
||||
class IptablesDropAllCtl(BaseCtl):
|
||||
def __init__(self, base_cmd: list[str], iface: str) -> None:
|
||||
self.__base_cmd = base_cmd
|
||||
self.__iface = iface
|
||||
|
||||
def get_command(self, direct: bool) -> list[str]:
|
||||
return [*self.__base_cmd, ("-A" if direct else "-D"), "INPUT", "-i", self.__iface, "-j", "DROP"]
|
||||
|
||||
|
||||
class IptablesAllowIcmpCtl(BaseCtl):
|
||||
def __init__(self, base_cmd: list[str], iface: str) -> None:
|
||||
self.__base_cmd = base_cmd
|
||||
self.__iface = iface
|
||||
|
||||
def get_command(self, direct: bool) -> list[str]:
|
||||
return [
|
||||
*self.__base_cmd,
|
||||
("-A" if direct else "-D"), "INPUT", "-i", self.__iface, "-p", "icmp", "-j", "ACCEPT",
|
||||
]
|
||||
|
||||
|
||||
class IptablesAllowPortCtl(BaseCtl):
|
||||
def __init__(self, base_cmd: list[str], iface: str, port: int, tcp: bool) -> None:
|
||||
self.__base_cmd = base_cmd
|
||||
self.__iface = iface
|
||||
self.__port = port
|
||||
self.__proto = ("tcp" if tcp else "udp")
|
||||
|
||||
def get_command(self, direct: bool) -> list[str]:
|
||||
return [
|
||||
*self.__base_cmd,
|
||||
("-A" if direct else "-D"), "INPUT", "-i", self.__iface, "-p", self.__proto,
|
||||
"--dport", str(self.__port), "-j", "ACCEPT",
|
||||
]
|
||||
|
||||
|
||||
class IptablesForwardOut(BaseCtl):
|
||||
def __init__(self, base_cmd: list[str], iface: str) -> None:
|
||||
self.__base_cmd = base_cmd
|
||||
self.__iface = iface
|
||||
|
||||
def get_command(self, direct: bool) -> list[str]:
|
||||
return [
|
||||
*self.__base_cmd,
|
||||
"--table", "nat",
|
||||
("-A" if direct else "-D"), "POSTROUTING",
|
||||
"-o", self.__iface, "-j", "MASQUERADE",
|
||||
]
|
||||
|
||||
|
||||
class IptablesForwardIn(BaseCtl):
|
||||
def __init__(self, base_cmd: list[str], iface: str) -> None:
|
||||
self.__base_cmd = base_cmd
|
||||
self.__iface = iface
|
||||
|
||||
def get_command(self, direct: bool) -> list[str]:
|
||||
return [
|
||||
*self.__base_cmd,
|
||||
("-A" if direct else "-D"), "FORWARD",
|
||||
"-i", self.__iface, "-j", "ACCEPT",
|
||||
]
|
||||
|
||||
|
||||
class CustomCtl(BaseCtl):
|
||||
def __init__(
|
||||
self,
|
||||
direct_cmd: list[str],
|
||||
reverse_cmd: list[str],
|
||||
placeholders: dict[str, str],
|
||||
) -> None:
|
||||
|
||||
self.__direct_cmd = direct_cmd
|
||||
self.__reverse_cmd = reverse_cmd
|
||||
self.__placeholders = placeholders
|
||||
|
||||
def get_command(self, direct: bool) -> list[str]:
|
||||
return [
|
||||
part.format(**self.__placeholders)
|
||||
for part in (self.__direct_cmd if direct else self.__reverse_cmd)
|
||||
]
|
||||
@ -1,211 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
|
||||
from typing import AsyncGenerator
|
||||
|
||||
import gpiod
|
||||
|
||||
from ...logging import get_logger
|
||||
|
||||
from ... import aiotools
|
||||
from ... import aiogp
|
||||
|
||||
from ...yamlconf import Option
|
||||
|
||||
from ...validators.basic import valid_bool
|
||||
from ...validators.basic import valid_float_f0
|
||||
from ...validators.basic import valid_float_f01
|
||||
from ...validators.os import valid_abs_path
|
||||
from ...validators.hw import valid_gpio_pin
|
||||
|
||||
from . import AtxIsBusyError
|
||||
from . import BaseAtx
|
||||
|
||||
|
||||
# =====
|
||||
class Plugin(BaseAtx): # pylint: disable=too-many-instance-attributes
|
||||
def __init__( # pylint: disable=too-many-arguments,super-init-not-called
|
||||
self,
|
||||
device_path: str,
|
||||
|
||||
power_led_pin: int,
|
||||
power_led_inverted: bool,
|
||||
power_led_debounce: float,
|
||||
|
||||
hdd_led_pin: int,
|
||||
hdd_led_inverted: bool,
|
||||
hdd_led_debounce: float,
|
||||
|
||||
power_switch_pin: int,
|
||||
reset_switch_pin: int,
|
||||
click_delay: float,
|
||||
long_click_delay: float,
|
||||
) -> None:
|
||||
|
||||
self.__device_path = device_path
|
||||
|
||||
self.__power_led_pin = power_led_pin
|
||||
self.__hdd_led_pin = hdd_led_pin
|
||||
self.__power_switch_pin = power_switch_pin
|
||||
self.__reset_switch_pin = reset_switch_pin
|
||||
|
||||
self.__click_delay = click_delay
|
||||
self.__long_click_delay = long_click_delay
|
||||
|
||||
self.__notifier = aiotools.AioNotifier()
|
||||
self.__region = aiotools.AioExclusiveRegion(AtxIsBusyError, self.__notifier)
|
||||
|
||||
self.__line_req: (gpiod.LineRequest | None) = None
|
||||
|
||||
self.__reader = aiogp.AioReader(
|
||||
path=self.__device_path,
|
||||
consumer="kvmd::atx",
|
||||
pins={
|
||||
power_led_pin: aiogp.AioReaderPinParams(power_led_inverted, power_led_debounce),
|
||||
hdd_led_pin: aiogp.AioReaderPinParams(hdd_led_inverted, hdd_led_debounce),
|
||||
},
|
||||
notifier=self.__notifier,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_plugin_options(cls) -> dict:
|
||||
return {
|
||||
"device": Option("/dev/gpiochip0", type=valid_abs_path, unpack_as="device_path"),
|
||||
|
||||
"power_led_pin": Option(24, type=valid_gpio_pin),
|
||||
"power_led_inverted": Option(False, type=valid_bool),
|
||||
"power_led_debounce": Option(0.1, type=valid_float_f0),
|
||||
|
||||
"hdd_led_pin": Option(22, type=valid_gpio_pin),
|
||||
"hdd_led_inverted": Option(False, type=valid_bool),
|
||||
"hdd_led_debounce": Option(0.1, type=valid_float_f0),
|
||||
|
||||
"power_switch_pin": Option(23, type=valid_gpio_pin),
|
||||
"reset_switch_pin": Option(27, type=valid_gpio_pin),
|
||||
"click_delay": Option(0.1, type=valid_float_f01),
|
||||
"long_click_delay": Option(5.5, type=valid_float_f01),
|
||||
}
|
||||
|
||||
def sysprep(self) -> None:
|
||||
assert self.__line_req is None
|
||||
self.__line_req = gpiod.request_lines(
|
||||
self.__device_path,
|
||||
consumer="kvmd::atx",
|
||||
config={
|
||||
(self.__power_switch_pin, self.__reset_switch_pin): gpiod.LineSettings(
|
||||
direction=gpiod.line.Direction.OUTPUT,
|
||||
output_value=gpiod.line.Value(False),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
async def get_state(self) -> dict:
|
||||
return {
|
||||
"enabled": True,
|
||||
"busy": self.__region.is_busy(),
|
||||
"leds": {
|
||||
"power": self.__reader.get(self.__power_led_pin),
|
||||
"hdd": self.__reader.get(self.__hdd_led_pin),
|
||||
},
|
||||
}
|
||||
|
||||
async def trigger_state(self) -> None:
|
||||
self.__notifier.notify(1)
|
||||
|
||||
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
||||
prev: dict = {}
|
||||
while True:
|
||||
if (await self.__notifier.wait()) > 0:
|
||||
prev = {}
|
||||
new = await self.get_state()
|
||||
if new != prev:
|
||||
prev = copy.deepcopy(new)
|
||||
yield new
|
||||
|
||||
async def systask(self) -> None:
|
||||
await self.__reader.poll()
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
if self.__line_req:
|
||||
try:
|
||||
self.__line_req.release()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# =====
|
||||
|
||||
async def power_on(self, wait: bool) -> None:
|
||||
if not (await self.__get_power()):
|
||||
await self.click_power(wait)
|
||||
|
||||
async def power_off(self, wait: bool) -> None:
|
||||
if (await self.__get_power()):
|
||||
await self.click_power(wait)
|
||||
|
||||
async def power_off_hard(self, wait: bool) -> None:
|
||||
if (await self.__get_power()):
|
||||
await self.click_power_long(wait)
|
||||
|
||||
async def power_reset_hard(self, wait: bool) -> None:
|
||||
if (await self.__get_power()):
|
||||
await self.click_reset(wait)
|
||||
|
||||
# =====
|
||||
|
||||
async def click_power(self, wait: bool) -> None:
|
||||
await self.__click("power", self.__power_switch_pin, self.__click_delay, wait)
|
||||
|
||||
async def click_power_long(self, wait: bool) -> None:
|
||||
await self.__click("power_long", self.__power_switch_pin, self.__long_click_delay, wait)
|
||||
|
||||
async def click_reset(self, wait: bool) -> None:
|
||||
await self.__click("reset", self.__reset_switch_pin, self.__click_delay, wait)
|
||||
|
||||
# =====
|
||||
|
||||
async def __get_power(self) -> bool:
|
||||
return (await self.get_state())["leds"]["power"]
|
||||
|
||||
@aiotools.atomic_fg
|
||||
async def __click(self, name: str, pin: int, delay: float, wait: bool) -> None:
|
||||
if wait:
|
||||
with self.__region:
|
||||
await self.__inner_click(name, pin, delay)
|
||||
else:
|
||||
await aiotools.run_region_task(
|
||||
f"Can't perform ATX {name} click or operation was not completed",
|
||||
self.__region, self.__inner_click, name, pin, delay,
|
||||
)
|
||||
|
||||
@aiotools.atomic_fg
|
||||
async def __inner_click(self, name: str, pin: int, delay: float) -> None:
|
||||
assert self.__line_req
|
||||
try:
|
||||
self.__line_req.set_value(pin, gpiod.line.Value(True))
|
||||
await asyncio.sleep(delay)
|
||||
finally:
|
||||
self.__line_req.set_value(pin, gpiod.line.Value(False))
|
||||
await asyncio.sleep(1)
|
||||
get_logger(0).info("Clicked ATX button %r", name)
|
||||
@ -1,237 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import copy
|
||||
|
||||
from typing import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
from ....logging import get_logger
|
||||
|
||||
from .... import aiomulti
|
||||
from .... import usb
|
||||
|
||||
from ....yamlconf import Option
|
||||
|
||||
from ....validators.basic import valid_bool
|
||||
from ....validators.basic import valid_int_f1
|
||||
from ....validators.basic import valid_float_f01
|
||||
from ....validators.os import valid_abs_path
|
||||
|
||||
from .. import BaseHid
|
||||
|
||||
from .keyboard import KeyboardProcess
|
||||
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],
|
||||
noop: bool,
|
||||
|
||||
udc: str, # XXX: Not from options, see /kvmd/apps/kvmd/__init__.py for details
|
||||
) -> None:
|
||||
|
||||
super().__init__(ignore_keys=ignore_keys, **mouse_x_range, **mouse_y_range, **jiggler)
|
||||
|
||||
self.__udc = udc
|
||||
|
||||
self.__notifier = aiomulti.AioProcessNotifier()
|
||||
|
||||
win98_fix = mouse.pop("absolute_win98_fix")
|
||||
common = {"notifier": self.__notifier, "noop": noop}
|
||||
|
||||
self.__keyboard_proc = KeyboardProcess(**common, **keyboard)
|
||||
self.__mouse_current = self.__mouse_proc = MouseProcess(**common, **mouse)
|
||||
|
||||
self.__mouse_alt_proc: (MouseProcess | None) = None
|
||||
self.__mouses: dict[str, MouseProcess] = {}
|
||||
if mouse_alt["device_path"]:
|
||||
self.__mouse_alt_proc = MouseProcess(
|
||||
absolute=(not mouse["absolute"]),
|
||||
**common,
|
||||
**mouse_alt,
|
||||
)
|
||||
self.__mouses = {
|
||||
"usb": (self.__mouse_proc if mouse["absolute"] else self.__mouse_alt_proc),
|
||||
"usb_rel": (self.__mouse_alt_proc if mouse["absolute"] else self.__mouse_proc),
|
||||
}
|
||||
if win98_fix:
|
||||
# На самом деле мультимышка и win95 не зависят друг от друга,
|
||||
# но так было проще реализовать переключение режимов
|
||||
self.__mouses["usb_win98"] = self.__mouses["usb"]
|
||||
|
||||
self._set_jiggler_absolute(self.__mouse_current.is_absolute())
|
||||
|
||||
@classmethod
|
||||
def get_plugin_options(cls) -> dict:
|
||||
return {
|
||||
"keyboard": {
|
||||
"device": Option("/dev/kvmd-hid-keyboard", type=valid_abs_path, unpack_as="device_path"),
|
||||
"select_timeout": Option(0.1, type=valid_float_f01),
|
||||
"queue_timeout": Option(0.1, type=valid_float_f01),
|
||||
"write_retries": Option(150, type=valid_int_f1),
|
||||
},
|
||||
"mouse": {
|
||||
"device": Option("/dev/kvmd-hid-mouse", type=valid_abs_path, unpack_as="device_path"),
|
||||
"select_timeout": Option(0.1, type=valid_float_f01),
|
||||
"queue_timeout": Option(0.1, type=valid_float_f01),
|
||||
"write_retries": Option(150, type=valid_int_f1),
|
||||
"absolute": Option(True, type=valid_bool),
|
||||
"absolute_win98_fix": Option(False, type=valid_bool),
|
||||
"horizontal_wheel": Option(True, type=valid_bool),
|
||||
},
|
||||
"mouse_alt": {
|
||||
"device": Option("", type=valid_abs_path, if_empty="", unpack_as="device_path"),
|
||||
"select_timeout": Option(0.1, type=valid_float_f01),
|
||||
"queue_timeout": Option(0.1, type=valid_float_f01),
|
||||
"write_retries": Option(150, type=valid_int_f1),
|
||||
# No absolute option here, initialized by (not mouse.absolute)
|
||||
# Also no absolute_win98_fix
|
||||
"horizontal_wheel": Option(True, type=valid_bool),
|
||||
},
|
||||
"noop": Option(False, type=valid_bool),
|
||||
**cls._get_base_options(),
|
||||
}
|
||||
|
||||
def sysprep(self) -> None:
|
||||
udc = usb.find_udc(self.__udc)
|
||||
get_logger(0).info("Using UDC %s", udc)
|
||||
self.__keyboard_proc.start(udc)
|
||||
self.__mouse_proc.start(udc)
|
||||
if self.__mouse_alt_proc:
|
||||
self.__mouse_alt_proc.start(udc)
|
||||
|
||||
async def get_state(self) -> dict:
|
||||
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,
|
||||
"keyboard": {
|
||||
"online": keyboard_state["online"],
|
||||
"leds": {
|
||||
"caps": keyboard_state["caps"],
|
||||
"scroll": keyboard_state["scroll"],
|
||||
"num": keyboard_state["num"],
|
||||
},
|
||||
"outputs": {"available": [], "active": ""},
|
||||
},
|
||||
"mouse": {
|
||||
"outputs": {
|
||||
"available": list(self.__mouses),
|
||||
"active": self.__get_current_mouse_mode(),
|
||||
},
|
||||
**mouse_state,
|
||||
},
|
||||
**self._get_jiggler_state(),
|
||||
}
|
||||
|
||||
async def trigger_state(self) -> None:
|
||||
self.__notifier.notify(1)
|
||||
|
||||
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
||||
prev: dict = {}
|
||||
while True:
|
||||
if (await self.__notifier.wait()) > 0:
|
||||
prev = {}
|
||||
new = await self.get_state()
|
||||
if new != prev:
|
||||
prev = copy.deepcopy(new)
|
||||
yield new
|
||||
|
||||
async def reset(self) -> None:
|
||||
self.__keyboard_proc.send_reset_event()
|
||||
self.__mouse_proc.send_reset_event()
|
||||
if self.__mouse_alt_proc:
|
||||
self.__mouse_alt_proc.send_reset_event()
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
try:
|
||||
self.__keyboard_proc.cleanup()
|
||||
finally:
|
||||
try:
|
||||
self.__mouse_proc.cleanup()
|
||||
finally:
|
||||
if self.__mouse_alt_proc:
|
||||
self.__mouse_alt_proc.cleanup()
|
||||
|
||||
# =====
|
||||
|
||||
def set_params(
|
||||
self,
|
||||
keyboard_output: (str | None)=None,
|
||||
mouse_output: (str | None)=None,
|
||||
jiggler: (bool | None)=None,
|
||||
) -> None:
|
||||
|
||||
_ = keyboard_output
|
||||
if mouse_output in self.__mouses and mouse_output != self.__get_current_mouse_mode():
|
||||
self.__mouse_current.send_clear_event()
|
||||
self.__mouse_current = self.__mouses[mouse_output]
|
||||
self.__mouse_current.set_win98_fix(mouse_output == "usb_win98")
|
||||
self._set_jiggler_absolute(self.__mouse_current.is_absolute())
|
||||
self.__notifier.notify()
|
||||
if jiggler is not None:
|
||||
self._set_jiggler_active(jiggler)
|
||||
self.__notifier.notify()
|
||||
|
||||
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()
|
||||
|
||||
# =====
|
||||
|
||||
def __get_current_mouse_mode(self) -> str:
|
||||
if len(self.__mouses) == 0:
|
||||
return ""
|
||||
if self.__mouse_current.is_absolute():
|
||||
return ("usb_win98" if self.__mouse_current.get_win98_fix() else "usb")
|
||||
return "usb_rel"
|
||||
@ -1,284 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import os
|
||||
import select
|
||||
import multiprocessing
|
||||
import queue
|
||||
import errno
|
||||
import logging
|
||||
import time
|
||||
|
||||
from typing import Generator
|
||||
|
||||
from ....logging import get_logger
|
||||
|
||||
from .... import tools
|
||||
from .... import aiomulti
|
||||
from .... import aioproc
|
||||
from .... import usb
|
||||
|
||||
from .events import BaseEvent
|
||||
|
||||
|
||||
# =====
|
||||
class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-instance-attributes
|
||||
def __init__( # pylint: disable=too-many-arguments
|
||||
self,
|
||||
name: str,
|
||||
read_size: int,
|
||||
initial_state: dict,
|
||||
notifier: aiomulti.AioProcessNotifier,
|
||||
|
||||
device_path: str,
|
||||
select_timeout: float,
|
||||
queue_timeout: float,
|
||||
write_retries: int,
|
||||
noop: bool,
|
||||
) -> None:
|
||||
|
||||
super().__init__(daemon=True)
|
||||
|
||||
self.__name = name
|
||||
self.__read_size = read_size
|
||||
|
||||
self.__device_path = device_path
|
||||
self.__select_timeout = select_timeout
|
||||
self.__queue_timeout = queue_timeout
|
||||
self.__write_retries = write_retries
|
||||
self.__noop = noop
|
||||
|
||||
self.__udc_state_path = ""
|
||||
self.__fd = -1
|
||||
self.__events_queue: "multiprocessing.Queue[BaseEvent]" = multiprocessing.Queue()
|
||||
self.__state_flags = aiomulti.AioSharedFlags({"online": True, **initial_state}, notifier)
|
||||
self.__stop_event = multiprocessing.Event()
|
||||
self.__no_device_reported = False
|
||||
|
||||
self.__logger: (logging.Logger | None) = None
|
||||
|
||||
def start(self, udc: str) -> None: # type: ignore # pylint: disable=arguments-differ
|
||||
self.__udc_state_path = usb.get_udc_path(udc, usb.U_STATE)
|
||||
super().start()
|
||||
|
||||
def run(self) -> None: # pylint: disable=too-many-branches
|
||||
self.__logger = aioproc.settle(f"HID-{self.__name}", f"hid-{self.__name}")
|
||||
report = b""
|
||||
retries = 0
|
||||
while not self.__stop_event.is_set():
|
||||
try:
|
||||
while not self.__stop_event.is_set():
|
||||
if self.__ensure_device():
|
||||
self.__read_all_reports()
|
||||
|
||||
try:
|
||||
event = self.__events_queue.get(timeout=self.__queue_timeout)
|
||||
except queue.Empty:
|
||||
# Проблема в том, что устройство может отвечать EAGAIN или ESHUTDOWN,
|
||||
# если оно было отключено физически. См:
|
||||
# - https://github.com/raspberrypi/linux/issues/3870
|
||||
# - https://github.com/raspberrypi/linux/pull/3151
|
||||
# Так что нам нужно проверять состояние контроллера, чтобы не спамить
|
||||
# в устройство и отслеживать его состояние.
|
||||
if not self.__is_udc_configured():
|
||||
self.__state_flags.update(online=False)
|
||||
else:
|
||||
# Посылка свежих репортов важнее старого
|
||||
for report in self._process_event(event):
|
||||
retries = self.__write_retries
|
||||
if self.__ensure_device():
|
||||
if self.__write_report(report):
|
||||
retries = 0
|
||||
continue
|
||||
|
||||
# Повторение последнего репорта до победного или пока не кончатся попытки
|
||||
if retries > 0 and self.__ensure_device():
|
||||
if self.__write_report(report):
|
||||
retries = 0
|
||||
else:
|
||||
retries -= 1
|
||||
|
||||
except Exception:
|
||||
self.__logger.exception("Unexpected HID-%s error", self.__name)
|
||||
time.sleep(1)
|
||||
|
||||
self.__close_device()
|
||||
|
||||
async def get_state(self) -> dict:
|
||||
return (await self.__state_flags.get())
|
||||
|
||||
# =====
|
||||
|
||||
def _process_event(self, event: BaseEvent) -> Generator[bytes, None, None]:
|
||||
_ = event
|
||||
if self is not None: # XXX: Vulture and pylint hack
|
||||
raise NotImplementedError()
|
||||
yield
|
||||
|
||||
def _process_read_report(self, report: bytes) -> None:
|
||||
pass
|
||||
|
||||
def _update_state(self, **kwargs: bool) -> None:
|
||||
assert "online" not in kwargs
|
||||
self.__state_flags.update(**kwargs)
|
||||
|
||||
# =====
|
||||
|
||||
def _stop(self) -> None:
|
||||
if self.is_alive():
|
||||
get_logger().info("Stopping HID-%s daemon ...", self.__name)
|
||||
self.__stop_event.set()
|
||||
if self.is_alive() or self.exitcode is not None:
|
||||
self.join()
|
||||
|
||||
def _queue_event(self, event: BaseEvent) -> None:
|
||||
self.__events_queue.put_nowait(event)
|
||||
|
||||
def _clear_queue(self) -> None:
|
||||
tools.clear_queue(self.__events_queue)
|
||||
|
||||
def _cleanup_write(self, report: bytes) -> None:
|
||||
assert not self.is_alive()
|
||||
assert self.__fd < 0
|
||||
if self.__ensure_device():
|
||||
self.__write_report(report)
|
||||
self.__close_device()
|
||||
|
||||
# =====
|
||||
|
||||
def __get_logger(self) -> logging.Logger:
|
||||
# Внутри процесса логгер из цикла, снаружи - каждый раз берем новый
|
||||
if self.__logger is not None:
|
||||
return self.__logger
|
||||
return get_logger()
|
||||
|
||||
def __is_udc_configured(self) -> bool:
|
||||
with open(self.__udc_state_path) as file:
|
||||
return (file.read().strip().lower() == "configured")
|
||||
|
||||
def __write_report(self, report: bytes) -> bool:
|
||||
assert report
|
||||
|
||||
if self.__noop:
|
||||
return True
|
||||
|
||||
assert self.__fd >= 0
|
||||
logger = self.__get_logger()
|
||||
|
||||
try:
|
||||
written = os.write(self.__fd, report)
|
||||
if written == len(report):
|
||||
self.__state_flags.update(online=True)
|
||||
return True
|
||||
else:
|
||||
logger.error("HID-%s write() error: written (%s) != report length (%d)",
|
||||
self.__name, written, len(report))
|
||||
except Exception as ex:
|
||||
if isinstance(ex, OSError) and (
|
||||
# https://github.com/raspberrypi/linux/commit/61b7f805dc2fd364e0df682de89227e94ce88e25
|
||||
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(ex))
|
||||
else:
|
||||
logger.exception("Can't write report to HID-%s", self.__name)
|
||||
|
||||
self.__state_flags.update(online=False)
|
||||
return False
|
||||
|
||||
def __read_all_reports(self) -> None:
|
||||
if self.__noop or self.__read_size == 0:
|
||||
return
|
||||
|
||||
assert self.__fd >= 0
|
||||
logger = self.__get_logger()
|
||||
|
||||
read = True
|
||||
while read:
|
||||
try:
|
||||
read = bool(select.select([self.__fd], [], [], 0)[0])
|
||||
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 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:
|
||||
self._process_read_report(report)
|
||||
|
||||
def __ensure_device(self) -> bool:
|
||||
if self.__noop:
|
||||
return True
|
||||
|
||||
logger = self.__get_logger()
|
||||
|
||||
if not os.path.exists(self.__device_path):
|
||||
# Во-первых, не пытаемся открыть устройство, если его нет.
|
||||
# Во-вторых, если у нас из под ног вытаскивают UDC, то надо закрыть устройство,
|
||||
# чтобы избежать гонки при пересоздании оного.
|
||||
self.__close_device()
|
||||
self.__state_flags.update(online=False)
|
||||
if not self.__no_device_reported:
|
||||
logger.error("Missing HID-%s device: %s", self.__name, self.__device_path)
|
||||
self.__no_device_reported = True
|
||||
return False
|
||||
self.__no_device_reported = False
|
||||
|
||||
if self.__fd < 0:
|
||||
if os.path.exists(self.__device_path):
|
||||
try:
|
||||
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 ex:
|
||||
logger.error("Can't open HID-%s device %s: %s",
|
||||
self.__name, self.__device_path, tools.efmt(ex))
|
||||
|
||||
if self.__fd >= 0:
|
||||
try:
|
||||
if select.select([], [self.__fd], [], self.__select_timeout)[1]:
|
||||
# Закомментировано, потому что иногда запись доступна, но устройство отключено
|
||||
# self.__state_flags.update(online=True)
|
||||
return True
|
||||
else:
|
||||
# Если запись недоступна, то скорее всего устройство отключено
|
||||
logger.debug("HID-%s is busy/unplugged (write select)", self.__name)
|
||||
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
|
||||
|
||||
def __close_device(self) -> None:
|
||||
if self.__fd >= 0:
|
||||
try:
|
||||
os.close(self.__fd)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self.__fd = -1
|
||||
@ -1,176 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import struct
|
||||
import dataclasses
|
||||
|
||||
from ....keyboard.mappings import UsbKey
|
||||
from ....keyboard.mappings import KEYMAP
|
||||
|
||||
from ....mouse import MouseRange
|
||||
|
||||
|
||||
# =====
|
||||
class BaseEvent:
|
||||
pass
|
||||
|
||||
|
||||
class ClearEvent(BaseEvent):
|
||||
pass
|
||||
|
||||
|
||||
class ResetEvent(BaseEvent):
|
||||
pass
|
||||
|
||||
|
||||
# =====
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class KeyEvent(BaseEvent):
|
||||
key: UsbKey
|
||||
state: bool
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
assert (not self.key.is_modifier)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class ModifierEvent(BaseEvent):
|
||||
modifier: UsbKey
|
||||
state: bool
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
assert self.modifier.is_modifier
|
||||
|
||||
|
||||
def make_keyboard_event(key: str, state: bool) -> (KeyEvent | ModifierEvent):
|
||||
usb_key = KEYMAP[key].usb
|
||||
if usb_key.is_modifier:
|
||||
return ModifierEvent(usb_key, state)
|
||||
return KeyEvent(usb_key, state)
|
||||
|
||||
|
||||
def get_led_caps(flags: int) -> bool:
|
||||
# https://wiki.osdev.org/USB_Human_Interface_Devices#LED_lamps
|
||||
return bool(flags & 2)
|
||||
|
||||
|
||||
def get_led_scroll(flags: int) -> bool:
|
||||
return bool(flags & 4)
|
||||
|
||||
|
||||
def get_led_num(flags: int) -> bool:
|
||||
return bool(flags & 1)
|
||||
|
||||
|
||||
def make_keyboard_report(
|
||||
pressed_modifiers: set[UsbKey],
|
||||
pressed_keys: list[UsbKey | None],
|
||||
) -> bytes:
|
||||
|
||||
modifiers = 0
|
||||
for modifier in pressed_modifiers:
|
||||
modifiers |= modifier.code
|
||||
|
||||
assert len(pressed_keys) == 6
|
||||
keys = [
|
||||
(0 if key is None else key.code)
|
||||
for key in pressed_keys
|
||||
]
|
||||
return bytes([modifiers, 0] + keys)
|
||||
|
||||
|
||||
# =====
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class MouseButtonEvent(BaseEvent):
|
||||
button: str
|
||||
state: bool
|
||||
code: int = 0
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
object.__setattr__(self, "code", {
|
||||
"left": 0x1,
|
||||
"right": 0x2,
|
||||
"middle": 0x4,
|
||||
"up": 0x8, # Back
|
||||
"down": 0x10, # Forward
|
||||
}[self.button])
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class MouseMoveEvent(BaseEvent):
|
||||
to_x: int
|
||||
to_y: int
|
||||
win98_fix: bool = False
|
||||
to_fixed_x: int = 0
|
||||
to_fixed_y: int = 0
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
assert MouseRange.MIN <= self.to_x <= MouseRange.MAX
|
||||
assert MouseRange.MIN <= self.to_y <= MouseRange.MAX
|
||||
to_fixed_x = MouseRange.remap(self.to_x, 0, MouseRange.MAX)
|
||||
to_fixed_y = MouseRange.remap(self.to_y, 0, MouseRange.MAX)
|
||||
if self.win98_fix:
|
||||
# https://github.com/pikvm/pikvm/issues/159
|
||||
# For some reason, the correct implementation of this fix
|
||||
# is a shift to the left, and not to the right, as in VirtualBox
|
||||
to_fixed_x <<= 1
|
||||
to_fixed_y <<= 1
|
||||
object.__setattr__(self, "to_fixed_x", to_fixed_x)
|
||||
object.__setattr__(self, "to_fixed_y", to_fixed_y)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class MouseRelativeEvent(BaseEvent):
|
||||
delta_x: int
|
||||
delta_y: int
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
assert -127 <= self.delta_x <= 127
|
||||
assert -127 <= self.delta_y <= 127
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class MouseWheelEvent(BaseEvent):
|
||||
delta_x: int
|
||||
delta_y: int
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
assert -127 <= self.delta_x <= 127
|
||||
assert -127 <= self.delta_y <= 127
|
||||
|
||||
|
||||
def make_mouse_report(
|
||||
absolute: bool,
|
||||
buttons: int,
|
||||
move_x: int,
|
||||
move_y: int,
|
||||
wheel_x: (int | None),
|
||||
wheel_y: int,
|
||||
) -> bytes:
|
||||
|
||||
# XXX: Wheel Y before X: it's ok.
|
||||
# See /kvmd/apps/otg/hid/mouse.py for details
|
||||
|
||||
if wheel_x is not None:
|
||||
return struct.pack(("<BHHbb" if absolute else "<Bbbbb"), buttons, move_x, move_y, wheel_y, wheel_x)
|
||||
else:
|
||||
return struct.pack(("<BHHb" if absolute else "<Bbbb"), buttons, move_x, move_y, wheel_y)
|
||||
@ -1,133 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from typing import Generator
|
||||
from typing import Any
|
||||
|
||||
from ....logging import get_logger
|
||||
|
||||
from ....keyboard.mappings import UsbKey
|
||||
|
||||
from .device import BaseDeviceProcess
|
||||
|
||||
from .events import BaseEvent
|
||||
from .events import ClearEvent
|
||||
from .events import ResetEvent
|
||||
from .events import KeyEvent
|
||||
from .events import ModifierEvent
|
||||
from .events import make_keyboard_event
|
||||
from .events import get_led_caps
|
||||
from .events import get_led_scroll
|
||||
from .events import get_led_num
|
||||
from .events import make_keyboard_report
|
||||
|
||||
|
||||
# =====
|
||||
class KeyboardProcess(BaseDeviceProcess):
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(
|
||||
name="keyboard",
|
||||
read_size=1,
|
||||
initial_state={"caps": False, "scroll": False, "num": False},
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.__pressed_modifiers: set[UsbKey] = set()
|
||||
self.__pressed_keys: list[UsbKey | None] = [None] * 6
|
||||
|
||||
def cleanup(self) -> None:
|
||||
self._stop()
|
||||
get_logger().info("Clearing HID-keyboard events ...")
|
||||
self._cleanup_write(b"\x00" * 8) # Release all keys and modifiers
|
||||
|
||||
def send_clear_event(self) -> None:
|
||||
self._clear_queue()
|
||||
self._queue_event(ClearEvent())
|
||||
|
||||
def send_reset_event(self) -> None:
|
||||
self._clear_queue()
|
||||
self._queue_event(ResetEvent())
|
||||
|
||||
def send_key_event(self, key: str, state: bool) -> None:
|
||||
self._queue_event(make_keyboard_event(key, state))
|
||||
|
||||
# =====
|
||||
|
||||
def _process_read_report(self, report: bytes) -> None:
|
||||
assert len(report) == 1, report
|
||||
self._update_state(
|
||||
caps=get_led_caps(report[0]),
|
||||
scroll=get_led_scroll(report[0]),
|
||||
num=get_led_num(report[0]),
|
||||
)
|
||||
|
||||
# =====
|
||||
|
||||
def _process_event(self, event: BaseEvent) -> Generator[bytes, None, None]:
|
||||
if isinstance(event, (ClearEvent, ResetEvent)):
|
||||
yield self.__process_clear_event()
|
||||
elif isinstance(event, ModifierEvent):
|
||||
yield from self.__process_modifier_event(event)
|
||||
elif isinstance(event, KeyEvent):
|
||||
yield from self.__process_key_event(event)
|
||||
else:
|
||||
raise RuntimeError(f"Not implemented event: {event}")
|
||||
|
||||
def __process_clear_event(self) -> bytes:
|
||||
self.__clear_modifiers()
|
||||
self.__clear_keys()
|
||||
return self.__make_report()
|
||||
|
||||
def __process_modifier_event(self, event: ModifierEvent) -> Generator[bytes, None, None]:
|
||||
if event.modifier in self.__pressed_modifiers:
|
||||
# Ранее нажатый модификатор отжимаем
|
||||
self.__pressed_modifiers.remove(event.modifier)
|
||||
yield self.__make_report()
|
||||
if event.state:
|
||||
# Нажимаем если нужно
|
||||
self.__pressed_modifiers.add(event.modifier)
|
||||
yield self.__make_report()
|
||||
|
||||
def __process_key_event(self, event: KeyEvent) -> Generator[bytes, None, None]:
|
||||
if event.key in self.__pressed_keys:
|
||||
# Ранее нажатую клавишу отжимаем
|
||||
self.__pressed_keys[self.__pressed_keys.index(event.key)] = None
|
||||
yield self.__make_report()
|
||||
elif event.state and None not in self.__pressed_keys:
|
||||
# Если нужно нажать что-то новое, но свободных слотов нет - отжимаем всё
|
||||
self.__clear_keys()
|
||||
yield self.__make_report()
|
||||
if event.state:
|
||||
# Нажимаем если нужно
|
||||
self.__pressed_keys[self.__pressed_keys.index(None)] = event.key
|
||||
yield self.__make_report()
|
||||
|
||||
# =====
|
||||
|
||||
def __make_report(self) -> bytes:
|
||||
return make_keyboard_report(self.__pressed_modifiers, self.__pressed_keys)
|
||||
|
||||
def __clear_modifiers(self) -> None:
|
||||
self.__pressed_modifiers.clear()
|
||||
|
||||
def __clear_keys(self) -> None:
|
||||
self.__pressed_keys = [None] * 6
|
||||
@ -1,181 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from typing import Generator
|
||||
from typing import Any
|
||||
|
||||
from ....logging import get_logger
|
||||
|
||||
from .device import BaseDeviceProcess
|
||||
|
||||
from .events import BaseEvent
|
||||
from .events import ClearEvent
|
||||
from .events import ResetEvent
|
||||
from .events import MouseButtonEvent
|
||||
from .events import MouseMoveEvent
|
||||
from .events import MouseRelativeEvent
|
||||
from .events import MouseWheelEvent
|
||||
from .events import make_mouse_report
|
||||
|
||||
|
||||
# =====
|
||||
class MouseProcess(BaseDeviceProcess):
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
self.__absolute: bool = kwargs.pop("absolute")
|
||||
self.__horizontal_wheel: bool = kwargs.pop("horizontal_wheel")
|
||||
|
||||
super().__init__(
|
||||
name="mouse",
|
||||
read_size=0,
|
||||
initial_state={"absolute": self.__absolute}, # Just for the state
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.__pressed_buttons = 0
|
||||
self.__x = 0 # For absolute
|
||||
self.__y = 0
|
||||
self.__win98_fix = False
|
||||
|
||||
def is_absolute(self) -> bool:
|
||||
return self.__absolute
|
||||
|
||||
def set_win98_fix(self, enabled: bool) -> None:
|
||||
self.__win98_fix = enabled
|
||||
|
||||
def get_win98_fix(self) -> bool:
|
||||
return self.__win98_fix
|
||||
|
||||
def cleanup(self) -> None:
|
||||
self._stop()
|
||||
get_logger().info("Clearing HID-mouse events ...")
|
||||
report = make_mouse_report(
|
||||
absolute=self.__absolute,
|
||||
buttons=0,
|
||||
move_x=(self.__x if self.__absolute else 0),
|
||||
move_y=(self.__y if self.__absolute else 0),
|
||||
wheel_x=(0 if self.__horizontal_wheel else None),
|
||||
wheel_y=0,
|
||||
)
|
||||
self._cleanup_write(report) # Release all buttons
|
||||
|
||||
def send_clear_event(self) -> None:
|
||||
self._clear_queue()
|
||||
self._queue_event(ClearEvent())
|
||||
|
||||
def send_reset_event(self) -> None:
|
||||
self._clear_queue()
|
||||
self._queue_event(ResetEvent())
|
||||
|
||||
def send_button_event(self, button: str, state: bool) -> None:
|
||||
self._queue_event(MouseButtonEvent(button, state))
|
||||
|
||||
def send_move_event(self, to_x: int, to_y: int) -> None:
|
||||
if self.__absolute:
|
||||
self._queue_event(MouseMoveEvent(to_x, to_y, self.__win98_fix))
|
||||
|
||||
def send_relative_event(self, delta_x: int, delta_y: int) -> None:
|
||||
if not self.__absolute:
|
||||
self._queue_event(MouseRelativeEvent(delta_x, delta_y))
|
||||
|
||||
def send_wheel_event(self, delta_x: int, delta_y: int) -> None:
|
||||
self._queue_event(MouseWheelEvent(delta_x, delta_y))
|
||||
|
||||
# =====
|
||||
|
||||
def _process_event(self, event: BaseEvent) -> Generator[bytes, None, None]:
|
||||
if isinstance(event, (ClearEvent, ResetEvent)):
|
||||
yield self.__process_clear_event()
|
||||
elif isinstance(event, MouseButtonEvent):
|
||||
yield from self.__process_button_event(event)
|
||||
elif isinstance(event, MouseMoveEvent):
|
||||
yield self.__process_move_event(event)
|
||||
elif isinstance(event, MouseRelativeEvent):
|
||||
yield self.__process_relative_event(event)
|
||||
elif isinstance(event, MouseWheelEvent):
|
||||
yield self.__process_wheel_event(event)
|
||||
else:
|
||||
raise RuntimeError(f"Not implemented event: {event}")
|
||||
|
||||
def __process_clear_event(self) -> bytes:
|
||||
self.__clear_state()
|
||||
return self.__make_report()
|
||||
|
||||
def __process_button_event(self, event: MouseButtonEvent) -> Generator[bytes, None, None]:
|
||||
if event.code & self.__pressed_buttons:
|
||||
# Ранее нажатую кнопку отжимаем
|
||||
self.__pressed_buttons &= ~event.code
|
||||
yield self.__make_report()
|
||||
if event.state:
|
||||
# Нажимаем если нужно
|
||||
self.__pressed_buttons |= event.code
|
||||
yield self.__make_report()
|
||||
|
||||
def __process_move_event(self, event: MouseMoveEvent) -> bytes:
|
||||
self.__x = event.to_fixed_x
|
||||
self.__y = event.to_fixed_y
|
||||
return self.__make_report()
|
||||
|
||||
def __process_relative_event(self, event: MouseRelativeEvent) -> bytes:
|
||||
return self.__make_report(relative_event=event)
|
||||
|
||||
def __process_wheel_event(self, event: MouseWheelEvent) -> bytes:
|
||||
return self.__make_report(wheel_event=event)
|
||||
|
||||
# =====
|
||||
|
||||
def __make_report(
|
||||
self,
|
||||
relative_event: (MouseRelativeEvent | None)=None,
|
||||
wheel_event: (MouseWheelEvent | None)=None,
|
||||
) -> bytes:
|
||||
|
||||
if self.__absolute:
|
||||
assert relative_event is None
|
||||
move_x = self.__x
|
||||
move_y = self.__y
|
||||
else:
|
||||
assert self.__x == self.__y == 0
|
||||
if relative_event is not None:
|
||||
move_x = relative_event.delta_x
|
||||
move_y = relative_event.delta_y
|
||||
else:
|
||||
move_x = move_y = 0
|
||||
|
||||
if wheel_event is not None:
|
||||
wheel_x = wheel_event.delta_x
|
||||
wheel_y = wheel_event.delta_y
|
||||
else:
|
||||
wheel_x = wheel_y = 0
|
||||
|
||||
return make_mouse_report(
|
||||
absolute=self.__absolute,
|
||||
buttons=self.__pressed_buttons,
|
||||
move_x=move_x,
|
||||
move_y=move_y,
|
||||
wheel_x=(wheel_x if self.__horizontal_wheel else None),
|
||||
wheel_y=wheel_y,
|
||||
)
|
||||
|
||||
def __clear_state(self) -> None:
|
||||
self.__pressed_buttons = 0
|
||||
self.__x = 0
|
||||
self.__y = 0
|
||||
@ -1,670 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import dataclasses
|
||||
import functools
|
||||
import time
|
||||
import os
|
||||
import copy
|
||||
import pyfatfs
|
||||
import pyfatfs.PyFat
|
||||
import pyfatfs.PyFatFS
|
||||
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from ....logging import get_logger
|
||||
|
||||
from ....inotify import Inotify
|
||||
|
||||
from ....yamlconf import Option
|
||||
|
||||
from ....validators.basic import valid_bool
|
||||
from ....validators.basic import valid_number, valid_stripped_string_not_empty
|
||||
from ....validators.os import valid_command, valid_abs_path
|
||||
from ....validators.kvm import valid_msd_image_name
|
||||
|
||||
from .... import aiotools
|
||||
from .... import fstab
|
||||
|
||||
from .. import MsdIsBusyError
|
||||
from .. import MsdOfflineError
|
||||
from .. import MsdConnectedError
|
||||
from .. import MsdDisconnectedError
|
||||
from .. import MsdImageNotSelected
|
||||
from .. import MsdUnknownImageError
|
||||
from .. import MsdImageExistsError
|
||||
from .. import BaseMsd
|
||||
from .. import MsdFileReader
|
||||
from .. import MsdFileWriter
|
||||
|
||||
from .storage import Image
|
||||
from .storage import Storage
|
||||
from .drive import Drive
|
||||
|
||||
|
||||
# =====
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class _DriveState:
|
||||
image: (Image | None)
|
||||
cdrom: bool
|
||||
rw: bool
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _VirtualDriveState:
|
||||
image: (Image | None)
|
||||
connected: bool
|
||||
cdrom: bool
|
||||
rw: bool
|
||||
|
||||
@classmethod
|
||||
def from_drive_state(cls, state: _DriveState) -> "_VirtualDriveState":
|
||||
return _VirtualDriveState(
|
||||
image=state.image,
|
||||
connected=bool(state.image),
|
||||
cdrom=state.cdrom,
|
||||
rw=state.rw,
|
||||
)
|
||||
|
||||
|
||||
class _State:
|
||||
def __init__(self, notifier: aiotools.AioNotifier) -> None:
|
||||
self.__notifier = notifier
|
||||
|
||||
self.storage: (Storage | None) = None
|
||||
self.vd: (_VirtualDriveState | None) = None
|
||||
|
||||
self._region = aiotools.AioExclusiveRegion(MsdIsBusyError)
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def busy(self, check_online: bool=True) -> AsyncGenerator[None, None]:
|
||||
try:
|
||||
with self._region:
|
||||
async with self._lock:
|
||||
self.__notifier.notify()
|
||||
if check_online:
|
||||
if self.vd is None:
|
||||
raise MsdOfflineError()
|
||||
assert self.storage
|
||||
yield
|
||||
finally:
|
||||
self.__notifier.notify()
|
||||
|
||||
def is_busy(self) -> bool:
|
||||
return self._region.is_busy()
|
||||
|
||||
|
||||
# =====
|
||||
class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||
def __init__( # pylint: disable=super-init-not-called
|
||||
self,
|
||||
read_chunk_size: int,
|
||||
write_chunk_size: int,
|
||||
sync_chunk_size: int,
|
||||
normalfiles_size: int,
|
||||
|
||||
remount_cmd: list[str],
|
||||
|
||||
initial: dict,
|
||||
|
||||
normalfiles_path: str,
|
||||
msd_path: str,
|
||||
|
||||
gadget: str, # XXX: Not from options, see /kvmd/apps/kvmd/__init__.py for details
|
||||
) -> None:
|
||||
|
||||
self.__read_chunk_size = read_chunk_size
|
||||
self.__write_chunk_size = write_chunk_size
|
||||
self.__sync_chunk_size = sync_chunk_size
|
||||
self.__normalfiles_path = normalfiles_path
|
||||
self.__msd_path = msd_path
|
||||
self.__normalfiles_size = normalfiles_size
|
||||
|
||||
self.__initial_image: str = initial["image"]
|
||||
self.__initial_cdrom: bool = initial["cdrom"]
|
||||
|
||||
self.__drive = Drive(gadget, instance=0, lun=0)
|
||||
self.__storage = Storage(fstab.find_msd(msd_path).root_path, remount_cmd)
|
||||
|
||||
self.__reader: (MsdFileReader | None) = None
|
||||
self.__writer: (MsdFileWriter | None) = None
|
||||
|
||||
self.__notifier = aiotools.AioNotifier()
|
||||
self.__state = _State(self.__notifier)
|
||||
self.__reset = False
|
||||
|
||||
logger = get_logger(0)
|
||||
logger.info("Using OTG gadget %r as MSD", gadget)
|
||||
aiotools.run_sync(self.__unsafe_reload_state())
|
||||
|
||||
@classmethod
|
||||
def get_plugin_options(cls) -> dict:
|
||||
return {
|
||||
"read_chunk_size": Option(65536, type=functools.partial(valid_number, min=1024)),
|
||||
"write_chunk_size": Option(65536, type=functools.partial(valid_number, min=1024)),
|
||||
"sync_chunk_size": Option(4194304, type=functools.partial(valid_number, min=1024)),
|
||||
|
||||
"remount_cmd": Option([
|
||||
"/usr/bin/sudo", "--non-interactive",
|
||||
"/usr/bin/kvmd-helper-otgmsd-remount", "{mode}",
|
||||
], type=valid_command),
|
||||
|
||||
"initial": {
|
||||
"image": Option("", type=valid_msd_image_name, if_empty=""),
|
||||
"cdrom": Option(False, type=valid_bool),
|
||||
},
|
||||
"msd_path": Option("/var/lib/kvmd/msd", type=valid_abs_path),
|
||||
"normalfiles_path": Option("NormalFiles", type=valid_stripped_string_not_empty),
|
||||
"normalfiles_size": Option(256, type=functools.partial(valid_number, min=64)),
|
||||
}
|
||||
|
||||
# =====
|
||||
|
||||
# =====
|
||||
|
||||
async def get_state(self) -> dict:
|
||||
async with self.__state._lock: # pylint: disable=protected-access
|
||||
storage: (dict | None) = None
|
||||
if self.__state.storage:
|
||||
assert self.__state.vd
|
||||
storage = dataclasses.asdict(self.__state.storage)
|
||||
for name in list(storage["images"]):
|
||||
del storage["images"][name]["name"]
|
||||
del storage["images"][name]["path"]
|
||||
del storage["images"][name]["in_storage"]
|
||||
for name in list(storage["parts"]):
|
||||
del storage["parts"][name]["name"]
|
||||
|
||||
storage["downloading"] = (self.__reader.get_state() if self.__reader else None)
|
||||
storage["uploading"] = (self.__writer.get_state() if self.__writer else None)
|
||||
storage["filespath"] = self.__normalfiles_path
|
||||
|
||||
vd: (dict | None) = None
|
||||
if self.__state.vd:
|
||||
assert self.__state.storage
|
||||
vd = dataclasses.asdict(self.__state.vd)
|
||||
if vd["image"]:
|
||||
del vd["image"]["path"]
|
||||
|
||||
return {
|
||||
"enabled": True,
|
||||
"online": (bool(vd) and self.__drive.is_enabled()),
|
||||
"busy": self.__state.is_busy(),
|
||||
"storage": storage,
|
||||
"drive": vd,
|
||||
}
|
||||
|
||||
async def trigger_state(self) -> None:
|
||||
self.__notifier.notify(1)
|
||||
|
||||
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
||||
prev: dict = {}
|
||||
while True:
|
||||
if (await self.__notifier.wait()) > 0:
|
||||
prev = {}
|
||||
new = await self.get_state()
|
||||
if not prev or (prev.get("online") != new["online"]):
|
||||
prev = copy.deepcopy(new)
|
||||
yield new
|
||||
else:
|
||||
diff: dict = {}
|
||||
for sub in ["busy", "drive"]:
|
||||
if prev.get(sub) != new[sub]:
|
||||
diff[sub] = new[sub]
|
||||
for sub in ["images", "parts", "downloading", "uploading"]:
|
||||
if (prev.get("storage") or {}).get(sub) != (new["storage"] or {}).get(sub):
|
||||
if "storage" not in diff:
|
||||
diff["storage"] = {}
|
||||
diff["storage"][sub] = new["storage"][sub]
|
||||
if diff:
|
||||
prev = copy.deepcopy(new)
|
||||
yield diff
|
||||
|
||||
@aiotools.atomic_fg
|
||||
async def reset(self) -> None:
|
||||
async with self.__state.busy(check_online=False):
|
||||
try:
|
||||
self.__reset = True
|
||||
self.__drive.set_image_path("")
|
||||
self.__drive.set_cdrom_flag(False)
|
||||
self.__drive.set_rw_flag(False)
|
||||
await self.__storage.remount_rw(False)
|
||||
except Exception:
|
||||
get_logger(0).exception("Can't reset MSD properly")
|
||||
|
||||
# =====
|
||||
|
||||
@aiotools.atomic_fg
|
||||
async def set_params(
|
||||
self,
|
||||
name: (str | None)=None,
|
||||
cdrom: (bool | None)=None,
|
||||
rw: (bool | None)=None,
|
||||
) -> None:
|
||||
|
||||
async with self.__state.busy():
|
||||
assert self.__state.vd
|
||||
self.__STATE_check_disconnected()
|
||||
|
||||
if name is not None:
|
||||
if name:
|
||||
self.__state.vd.image = await self.__STATE_get_storage_image(name)
|
||||
else:
|
||||
self.__state.vd.image = None
|
||||
|
||||
if cdrom is not None:
|
||||
self.__state.vd.cdrom = cdrom
|
||||
if cdrom:
|
||||
rw = False
|
||||
|
||||
if rw is not None:
|
||||
self.__state.vd.rw = rw
|
||||
if rw:
|
||||
self.__state.vd.cdrom = False
|
||||
|
||||
@aiotools.atomic_fg
|
||||
async def set_connected(self, connected: bool) -> None:
|
||||
print(self.__drive)
|
||||
async with self.__state.busy():
|
||||
assert self.__state.vd
|
||||
if connected:
|
||||
self.__STATE_check_disconnected()
|
||||
|
||||
if self.__state.vd.image is None:
|
||||
raise MsdImageNotSelected()
|
||||
|
||||
if not (await self.__state.vd.image.exists()):
|
||||
raise MsdUnknownImageError()
|
||||
|
||||
assert self.__state.vd.image.in_storage
|
||||
|
||||
self.__drive.set_rw_flag(self.__state.vd.rw)
|
||||
self.__drive.set_cdrom_flag(self.__state.vd.cdrom)
|
||||
#reset UDC to fix otg cd-rom and flash switch
|
||||
try:
|
||||
udc_path = self.__drive.get_udc_path()
|
||||
with open(udc_path) as file:
|
||||
enabled = bool(file.read().strip())
|
||||
if enabled:
|
||||
with open(udc_path, "w") as file:
|
||||
file.write("\n")
|
||||
with open(udc_path, "w") as file:
|
||||
file.write(sorted(os.listdir("/sys/class/udc"))[0])
|
||||
except:
|
||||
logger = get_logger(0)
|
||||
logger.error("Can't reset UDC")
|
||||
if self.__state.vd.rw:
|
||||
await self.__state.vd.image.remount_rw(True)
|
||||
self.__drive.set_image_path(self.__state.vd.image.path)
|
||||
|
||||
else:
|
||||
self.__STATE_check_connected()
|
||||
self.__drive.set_image_path("")
|
||||
await self.__storage.remount_rw(False, fatal=False)
|
||||
|
||||
self.__state.vd.connected = connected
|
||||
|
||||
@aiotools.atomic_fg
|
||||
async def make_image(self, zipped: bool) -> None:
|
||||
#Note: img size >= 64M
|
||||
def create_fat_image(img_size: int, file_img_path: str, source_dir: str, fat_type: int = 32, label: str = 'One-KVM'):
|
||||
def add_directory_to_fat(fat: str, src_path: str, dst_path: str):
|
||||
for item in os.listdir(src_path):
|
||||
src_item_path = os.path.join(src_path, item)
|
||||
dst_item_path = os.path.join(dst_path, item)
|
||||
|
||||
if os.path.isdir(src_item_path):
|
||||
fat.makedir(dst_item_path)
|
||||
add_directory_to_fat(fat, src_item_path, dst_item_path)
|
||||
elif os.path.isfile(src_item_path):
|
||||
with open(src_item_path, 'rb') as src_file:
|
||||
fat.create(dst_item_path)
|
||||
with fat.open(dst_item_path, 'wb') as dst_file:
|
||||
dst_file.write(src_file.read())
|
||||
print(file_img_path)
|
||||
with open(file_img_path, 'wb') as f:
|
||||
f.seek(img_size * 1024 *1024 - 1)
|
||||
f.write(b'\0')
|
||||
fat_file = pyfatfs.PyFat.PyFat()
|
||||
try:
|
||||
fat_file.mkfs(file_img_path, fat_type = fat_type, label = label)
|
||||
except Exception as e:
|
||||
get_logger(0).exception(f"Error making FAT Filesystem: {e}")
|
||||
finally:
|
||||
fat_file.close()
|
||||
fat_handle = pyfatfs.PyFatFS.PyFatFS(file_img_path)
|
||||
try:
|
||||
add_directory_to_fat(fat_handle, source_dir, '/')
|
||||
except Exception as e:
|
||||
get_logger(0).exception(f"Error adding directory to FAT image: {e}")
|
||||
finally:
|
||||
fat_handle.close()
|
||||
|
||||
def extract_fat_image(file_img_path: str, output_dir: str):
|
||||
try:
|
||||
for root, dirs, files in os.walk(output_dir, topdown=False):
|
||||
for name in files:
|
||||
os.remove(os.path.join(root, name))
|
||||
for name in dirs:
|
||||
os.rmdir(os.path.join(root, name))
|
||||
except Exception as e:
|
||||
get_logger(0).exception(f"Error removing normal file or directory: {e}")
|
||||
fat_handle = pyfatfs.PyFatFS.PyFatFS(file_img_path)
|
||||
try:
|
||||
def extract_directory(fat_handle, src_path: str, dst_path: str):
|
||||
for entry in fat_handle.listdir(src_path):
|
||||
src_item_path = os.path.join(src_path, entry)
|
||||
dst_item_path = os.path.join(dst_path, entry)
|
||||
|
||||
if fat_handle.gettype(src_item_path) is pyfatfs.PyFatFS.ResourceType.directory:
|
||||
os.makedirs(dst_item_path, exist_ok=True)
|
||||
extract_directory(fat_handle, src_item_path, dst_item_path)
|
||||
else:
|
||||
with fat_handle.open(src_item_path, 'rb') as src_file:
|
||||
with open(dst_item_path, 'wb') as dst_file:
|
||||
dst_file.write(src_file.read())
|
||||
extract_directory(fat_handle, '/', output_dir)
|
||||
except Exception as e:
|
||||
get_logger(0).exception(f"Error extracting FAT image: {e}")
|
||||
finally:
|
||||
fat_handle.close()
|
||||
|
||||
async with self.__state.busy():
|
||||
msd_path = self.__msd_path
|
||||
file_storage_path = os.path.join(msd_path, self.__normalfiles_path)
|
||||
file_img_path = os.path.join(msd_path, self.__normalfiles_path + ".img")
|
||||
img_size = self.__normalfiles_size
|
||||
if zipped:
|
||||
if not os.path.exists(file_storage_path):
|
||||
os.makedirs(file_storage_path)
|
||||
if os.path.exists(file_img_path):
|
||||
os.remove(file_img_path)
|
||||
create_fat_image(img_size, file_img_path, file_storage_path)
|
||||
else:
|
||||
if os.path.exists(file_img_path):
|
||||
extract_fat_image(file_img_path, file_storage_path)
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def read_image(self, name: str) -> AsyncGenerator[MsdFileReader, None]:
|
||||
try:
|
||||
with self.__state._region: # pylint: disable=protected-access
|
||||
try:
|
||||
async with self.__state._lock: # pylint: disable=protected-access
|
||||
self.__notifier.notify()
|
||||
self.__STATE_check_disconnected()
|
||||
|
||||
image = await self.__STATE_get_storage_image(name)
|
||||
self.__reader = await MsdFileReader(
|
||||
notifier=self.__notifier,
|
||||
name=image.name,
|
||||
path=image.path,
|
||||
chunk_size=self.__read_chunk_size,
|
||||
).open()
|
||||
|
||||
self.__notifier.notify()
|
||||
yield self.__reader
|
||||
|
||||
finally:
|
||||
await aiotools.shield_fg(self.__close_reader())
|
||||
finally:
|
||||
self.__notifier.notify()
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def write_image(self, name: str, size: int, remove_incomplete: (bool | None)) -> AsyncGenerator[MsdFileWriter, None]:
|
||||
image: (Image | None) = None
|
||||
complete = False
|
||||
|
||||
async def finish_writing() -> None:
|
||||
# Делаем под блокировкой, чтобы эвент айнотифи не был обработан
|
||||
# до того, как мы не закончим все процедуры.
|
||||
async with self.__state._lock: # pylint: disable=protected-access
|
||||
try:
|
||||
self.__notifier.notify()
|
||||
finally:
|
||||
try:
|
||||
if image:
|
||||
await image.set_complete(complete)
|
||||
finally:
|
||||
try:
|
||||
if image and remove_incomplete and not complete:
|
||||
await image.remove(fatal=False)
|
||||
finally:
|
||||
try:
|
||||
await self.__close_writer()
|
||||
finally:
|
||||
if image:
|
||||
await image.remount_rw(False, fatal=False)
|
||||
|
||||
try:
|
||||
with self.__state._region: # pylint: disable=protected-access
|
||||
try:
|
||||
async with self.__state._lock: # pylint: disable=protected-access
|
||||
self.__notifier.notify()
|
||||
self.__STATE_check_disconnected()
|
||||
|
||||
image = await self.__STORAGE_create_new_image(name)
|
||||
await image.remount_rw(True)
|
||||
await image.set_complete(False)
|
||||
self.__writer = await MsdFileWriter(
|
||||
notifier=self.__notifier,
|
||||
name=image.name,
|
||||
path=image.path,
|
||||
file_size=size,
|
||||
sync_size=self.__sync_chunk_size,
|
||||
chunk_size=self.__write_chunk_size,
|
||||
).open()
|
||||
|
||||
self.__notifier.notify()
|
||||
yield self.__writer
|
||||
complete = await self.__writer.finish()
|
||||
|
||||
finally:
|
||||
await aiotools.shield_fg(finish_writing())
|
||||
finally:
|
||||
self.__notifier.notify()
|
||||
|
||||
@aiotools.atomic_fg
|
||||
async def remove(self, name: str) -> None:
|
||||
async with self.__state.busy():
|
||||
assert self.__state.storage
|
||||
assert self.__state.vd
|
||||
self.__STATE_check_disconnected()
|
||||
image = await self.__STATE_get_storage_image(name)
|
||||
|
||||
if self.__state.vd.image == image:
|
||||
self.__state.vd.image = None
|
||||
|
||||
await image.remount_rw(True)
|
||||
try:
|
||||
await image.remove(fatal=True)
|
||||
finally:
|
||||
await aiotools.shield_fg(image.remount_rw(False, fatal=False))
|
||||
|
||||
# =====
|
||||
|
||||
def __STATE_check_connected(self) -> None: # pylint: disable=invalid-name
|
||||
assert self.__state.vd
|
||||
if not (self.__state.vd.connected or self.__drive.get_image_path()):
|
||||
raise MsdDisconnectedError()
|
||||
|
||||
def __STATE_check_disconnected(self) -> None: # pylint: disable=invalid-name
|
||||
assert self.__state.vd
|
||||
if self.__state.vd.connected or self.__drive.get_image_path():
|
||||
raise MsdConnectedError()
|
||||
|
||||
async def __STATE_get_storage_image(self, name: str) -> Image: # pylint: disable=invalid-name
|
||||
assert self.__state.storage
|
||||
image = self.__state.storage.images.get(name)
|
||||
if image is None or not (await image.exists()):
|
||||
raise MsdUnknownImageError()
|
||||
assert image.in_storage
|
||||
return image
|
||||
|
||||
async def __STORAGE_create_new_image(self, name: str) -> Image: # pylint: disable=invalid-name
|
||||
assert self.__state.storage
|
||||
image = await self.__storage.make_image_by_name(name)
|
||||
if image.name in self.__state.storage.images or (await image.exists()):
|
||||
raise MsdImageExistsError()
|
||||
return image
|
||||
|
||||
# =====
|
||||
|
||||
async def __close_reader(self) -> None:
|
||||
if self.__reader:
|
||||
try:
|
||||
await self.__reader.close()
|
||||
finally:
|
||||
self.__reader = None
|
||||
|
||||
async def __close_writer(self) -> None:
|
||||
if self.__writer:
|
||||
try:
|
||||
await self.__writer.close()
|
||||
finally:
|
||||
self.__writer = None
|
||||
|
||||
# =====
|
||||
|
||||
@aiotools.atomic_fg
|
||||
async def cleanup(self) -> None:
|
||||
await self.__close_reader()
|
||||
await self.__close_writer()
|
||||
|
||||
async def systask(self) -> None:
|
||||
logger = get_logger(0)
|
||||
while True:
|
||||
try:
|
||||
while True:
|
||||
# Активно ждем, пока не будут на месте все каталоги.
|
||||
await self.__reload_state()
|
||||
if self.__state.vd:
|
||||
break
|
||||
await asyncio.sleep(5)
|
||||
|
||||
with Inotify() as inotify:
|
||||
# Из-за гонки между первым релоадом и установкой вотчеров,
|
||||
# мы можем потерять какие-то каталоги стораджа, но это допустимо,
|
||||
# так как всегда есть ручной перезапуск.
|
||||
await inotify.watch_all_changes(*self.__storage.get_watchable_paths())
|
||||
await inotify.watch_all_changes(*self.__drive.get_watchable_paths())
|
||||
|
||||
# После установки вотчеров еще раз проверяем стейт,
|
||||
# чтобы не потерять состояние привода.
|
||||
await self.__reload_state()
|
||||
|
||||
while self.__state.vd: # Если живы после предыдущей проверки
|
||||
need_restart = self.__reset
|
||||
self.__reset = False
|
||||
need_reload_state = False
|
||||
for event in (await inotify.get_series(timeout=1)):
|
||||
need_reload_state = True
|
||||
if event.restart:
|
||||
# Если выгрузили OTG, изменили каталоги, что-то отмонтировали или делают еще какую-то странную фигню.
|
||||
# Проверяется маска InotifyMask.ALL_RESTART_EVENTS
|
||||
logger.info("Got a big inotify event: %s; reinitializing MSD ...", event)
|
||||
need_restart = True
|
||||
break
|
||||
if need_restart:
|
||||
break
|
||||
if need_reload_state:
|
||||
await self.__reload_state()
|
||||
elif self.__writer:
|
||||
# При загрузке файла обновляем статистику раз в секунду (по таймауту).
|
||||
# Это не нужно при обычном релоаде, потому что там и так проверяются все разделы.
|
||||
await self.__reload_parts_info()
|
||||
|
||||
except Exception:
|
||||
logger.exception("Unexpected MSD watcher error")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def __reload_state(self) -> None:
|
||||
async with self.__state._lock: # pylint: disable=protected-access
|
||||
await self.__unsafe_reload_state()
|
||||
self.__notifier.notify()
|
||||
|
||||
async def __reload_parts_info(self) -> None:
|
||||
assert self.__writer # Использовать только при записи образа
|
||||
async with self.__state._lock: # pylint: disable=protected-access
|
||||
await self.__storage.reload_parts_info()
|
||||
self.__notifier.notify()
|
||||
|
||||
# ===== Don't call this directly ====
|
||||
|
||||
async def __unsafe_reload_state(self) -> None:
|
||||
logger = get_logger(0)
|
||||
try:
|
||||
path = self.__drive.get_image_path()
|
||||
drive_state = _DriveState(
|
||||
image=((await self.__storage.make_image_by_path(path)) if path else None),
|
||||
cdrom=self.__drive.get_cdrom_flag(),
|
||||
rw=self.__drive.get_rw_flag(),
|
||||
)
|
||||
|
||||
await self.__storage.reload()
|
||||
|
||||
if self.__state.vd is None and drive_state.image is None:
|
||||
# Если только что включились и образ не подключен - попробовать
|
||||
# перемонтировать хранилище (и создать images и meta).
|
||||
logger.info("Probing to remount storage ...")
|
||||
await self.__storage.remount_rw(True)
|
||||
await self.__storage.remount_rw(False)
|
||||
await self.__unsafe_setup_initial()
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error while reloading MSD state; switching to offline")
|
||||
self.__state.storage = None
|
||||
self.__state.vd = None
|
||||
|
||||
else:
|
||||
self.__state.storage = self.__storage
|
||||
if drive_state.image:
|
||||
# При подключенном образе виртуальный стейт заменяется реальным
|
||||
self.__state.vd = _VirtualDriveState.from_drive_state(drive_state)
|
||||
else:
|
||||
if self.__state.vd is None:
|
||||
# Если раньше MSD был отключен
|
||||
self.__state.vd = _VirtualDriveState.from_drive_state(drive_state)
|
||||
|
||||
image = self.__state.vd.image
|
||||
if image and (not image.in_storage or not (await image.exists())):
|
||||
# Если только что отключили ручной образ вне хранилища или ранее выбранный образ был удален
|
||||
self.__state.vd.image = None
|
||||
|
||||
self.__state.vd.connected = False
|
||||
|
||||
async def __unsafe_setup_initial(self) -> None:
|
||||
if self.__initial_image:
|
||||
logger = get_logger(0)
|
||||
image = await self.__storage.make_image_by_name(self.__initial_image)
|
||||
if (await image.exists()):
|
||||
logger.info("Setting up initial image %r ...", self.__initial_image)
|
||||
try:
|
||||
self.__drive.set_rw_flag(False)
|
||||
self.__drive.set_cdrom_flag(self.__initial_cdrom)
|
||||
self.__drive.set_image_path(image.path)
|
||||
except Exception:
|
||||
logger.exception("Can't setup initial image: ignored")
|
||||
else:
|
||||
logger.error("Can't find initial image %r: ignored", self.__initial_image)
|
||||
@ -1,94 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import os
|
||||
import errno
|
||||
|
||||
from .... import usb
|
||||
|
||||
from .. import MsdOperationError
|
||||
|
||||
|
||||
# =====
|
||||
class MsdDriveLockedError(MsdOperationError):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("MSD drive is locked on IO operation")
|
||||
|
||||
|
||||
# =====
|
||||
class Drive:
|
||||
def __init__(self, gadget: str, instance: int, lun: int) -> None:
|
||||
func = f"mass_storage.usb{instance}"
|
||||
self.__udc_path = os.path.join(f"/sys/kernel/config/usb_gadget", gadget, usb.G_UDC)
|
||||
self.__profile_func_path = usb.get_gadget_path(gadget, usb.G_PROFILE, func)
|
||||
self.__profile_path = usb.get_gadget_path(gadget, usb.G_PROFILE)
|
||||
self.__lun_path = usb.get_gadget_path(gadget, usb.G_FUNCTIONS, func, f"lun.{lun}")
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
return os.path.exists(self.__profile_func_path)
|
||||
|
||||
def get_watchable_paths(self) -> list[str]:
|
||||
return [self.__lun_path, self.__profile_path]
|
||||
|
||||
def get_udc_path(self) -> str:
|
||||
return self.__udc_path
|
||||
|
||||
# =====
|
||||
|
||||
def set_image_path(self, path: str) -> None:
|
||||
if path or not self.__has_param("forced_eject"):
|
||||
self.__set_param("file", path)
|
||||
else:
|
||||
self.__set_param("forced_eject", "")
|
||||
|
||||
def get_image_path(self) -> str:
|
||||
path = self.__get_param("file")
|
||||
return (os.path.normpath(path) if path else "")
|
||||
|
||||
def set_cdrom_flag(self, flag: bool) -> None:
|
||||
self.__set_param("cdrom", str(int(flag)))
|
||||
|
||||
def get_cdrom_flag(self) -> bool:
|
||||
return bool(int(self.__get_param("cdrom")))
|
||||
|
||||
def set_rw_flag(self, flag: bool) -> None:
|
||||
self.__set_param("ro", str(int(not flag)))
|
||||
|
||||
def get_rw_flag(self) -> bool:
|
||||
return (not int(self.__get_param("ro")))
|
||||
|
||||
# =====
|
||||
def __has_param(self, param: str) -> bool:
|
||||
return os.access(os.path.join(self.__lun_path, param), os.F_OK)
|
||||
|
||||
def __get_param(self, param: str) -> str:
|
||||
with open(os.path.join(self.__lun_path, param)) as file:
|
||||
return file.read().strip()
|
||||
|
||||
def __set_param(self, param: str, value: str) -> None:
|
||||
try:
|
||||
with open(os.path.join(self.__lun_path, param), "w") as file:
|
||||
file.write(value + "\n")
|
||||
except OSError as ex:
|
||||
if ex.errno == errno.EBUSY:
|
||||
raise MsdDriveLockedError()
|
||||
raise
|
||||
@ -1,284 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
import operator
|
||||
import dataclasses
|
||||
|
||||
from typing import Generator
|
||||
from typing import Optional
|
||||
|
||||
import aiofiles
|
||||
import aiofiles.os
|
||||
|
||||
from .... import aiotools
|
||||
from .... import aiohelpers
|
||||
|
||||
from .. import MsdError
|
||||
|
||||
|
||||
# =====
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class _ImageDc:
|
||||
name: str
|
||||
path: str
|
||||
in_storage: bool = dataclasses.field(init=False, compare=False)
|
||||
complete: bool = dataclasses.field(init=False, compare=False)
|
||||
removable: bool = dataclasses.field(init=False, compare=False)
|
||||
size: int = dataclasses.field(init=False, compare=False)
|
||||
mod_ts: float = dataclasses.field(init=False, compare=False)
|
||||
|
||||
|
||||
class Image(_ImageDc):
|
||||
def __init__(self, name: str, path: str, storage: Optional["Storage"]) -> None:
|
||||
super().__init__(name, path)
|
||||
self.__storage = storage
|
||||
(self.__dir_path, file_name) = os.path.split(path)
|
||||
self.__incomplete_path = os.path.join(self.__dir_path, f".__{file_name}.incomplete")
|
||||
self.__adopted = False
|
||||
|
||||
async def _reload(self) -> None: # Only for Storage() and set_complete()
|
||||
# adopted используется в последующих проверках
|
||||
self.__adopted = await aiotools.run_async(self.__is_adopted)
|
||||
complete = await self.__is_complete()
|
||||
removable = await self.__is_removable()
|
||||
(size, mod_ts) = await self.__get_stat()
|
||||
object.__setattr__(self, "complete", complete)
|
||||
object.__setattr__(self, "removable", removable)
|
||||
object.__setattr__(self, "size", size)
|
||||
object.__setattr__(self, "mod_ts", mod_ts)
|
||||
|
||||
def __is_adopted(self) -> bool:
|
||||
# True, если образ находится вне хранилища
|
||||
# или в другой точке монтирования под ним
|
||||
if self.__storage is None:
|
||||
return True
|
||||
path = self.path
|
||||
while not os.path.ismount(path):
|
||||
path = os.path.dirname(path)
|
||||
return (self.__storage._get_root_path() != path) # pylint: disable=protected-access
|
||||
|
||||
async def __is_complete(self) -> bool:
|
||||
if self.__storage:
|
||||
return (not (await aiofiles.os.path.exists(self.__incomplete_path)))
|
||||
return True
|
||||
|
||||
async def __is_removable(self) -> bool:
|
||||
if not self.__storage:
|
||||
return False
|
||||
if not self.__adopted:
|
||||
return True
|
||||
return (await aiofiles.os.access(self.__dir_path, os.W_OK)) # type: ignore
|
||||
|
||||
async def __get_stat(self) -> tuple[int, float]:
|
||||
try:
|
||||
st = (await aiofiles.os.stat(self.path))
|
||||
return (st.st_size, st.st_mtime)
|
||||
except Exception:
|
||||
return (0, 0.0)
|
||||
|
||||
# =====
|
||||
|
||||
@property
|
||||
def in_storage(self) -> bool:
|
||||
return bool(self.__storage)
|
||||
|
||||
async def exists(self) -> bool:
|
||||
return (await aiofiles.os.path.exists(self.path))
|
||||
|
||||
async def remount_rw(self, rw: bool, fatal: bool=True) -> None:
|
||||
assert self.__storage
|
||||
if not self.__adopted:
|
||||
await self.__storage.remount_rw(rw, fatal)
|
||||
|
||||
async def remove(self, fatal: bool) -> None:
|
||||
assert self.__storage
|
||||
removed = False
|
||||
try:
|
||||
await aiofiles.os.remove(self.path)
|
||||
removed = True
|
||||
self.__storage.images.pop(self.name, None)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except Exception:
|
||||
if fatal:
|
||||
raise
|
||||
finally:
|
||||
# Удаляем .incomplete вместе с файлом
|
||||
if removed:
|
||||
await self.set_complete(True)
|
||||
|
||||
async def set_complete(self, flag: bool) -> None:
|
||||
assert self.__storage
|
||||
if flag:
|
||||
try:
|
||||
await aiofiles.os.remove(self.__incomplete_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
else:
|
||||
async with aiofiles.open(self.__incomplete_path, "w"):
|
||||
pass
|
||||
await self._reload()
|
||||
|
||||
|
||||
# =====
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class _PartDc:
|
||||
name: str
|
||||
size: int = dataclasses.field(init=False, compare=False)
|
||||
free: int = dataclasses.field(init=False, compare=False)
|
||||
writable: bool = dataclasses.field(init=False, compare=False)
|
||||
|
||||
|
||||
class _Part(_PartDc):
|
||||
def __init__(self, name: str, path: str) -> None:
|
||||
super().__init__(name)
|
||||
self.__path = path
|
||||
|
||||
async def _reload(self) -> None: # Only for Storage()
|
||||
st = await aiotools.run_async(os.statvfs, self.__path)
|
||||
if self.name == "":
|
||||
writable = True
|
||||
else:
|
||||
writable = await aiofiles.os.access(self.__path, os.W_OK) # type: ignore
|
||||
object.__setattr__(self, "size", st.f_blocks * st.f_frsize)
|
||||
object.__setattr__(self, "free", st.f_bavail * st.f_frsize)
|
||||
object.__setattr__(self, "writable", writable)
|
||||
|
||||
|
||||
# =====
|
||||
@dataclasses.dataclass(frozen=True, eq=False)
|
||||
class _StorageDc:
|
||||
images: dict[str, Image] = dataclasses.field(init=False)
|
||||
parts: dict[str, _Part] = dataclasses.field(init=False)
|
||||
|
||||
|
||||
class Storage(_StorageDc):
|
||||
def __init__(self, path: str, remount_cmd: list[str]) -> None:
|
||||
super().__init__()
|
||||
self.__path = path
|
||||
self.__remount_cmd = remount_cmd
|
||||
|
||||
self.__watchable_paths: (list[str] | None) = None
|
||||
self.__images: (dict[str, Image] | None) = None
|
||||
self.__parts: (dict[str, _Part] | None) = None
|
||||
|
||||
@property
|
||||
def images(self) -> dict[str, Image]:
|
||||
assert self.__images is not None
|
||||
return dict(self.__images)
|
||||
|
||||
@property
|
||||
def parts(self) -> dict[str, _Part]:
|
||||
assert self.__parts is not None
|
||||
return dict(self.__parts)
|
||||
|
||||
async def reload(self) -> None:
|
||||
self.__watchable_paths = None
|
||||
self.__images = {}
|
||||
|
||||
watchable_paths: list[str] = []
|
||||
images: dict[str, Image] = {}
|
||||
parts: dict[str, _Part] = {}
|
||||
for (root_path, is_part, files) in (await aiotools.run_async(self.__walk)):
|
||||
watchable_paths.append(root_path)
|
||||
for path in files:
|
||||
name = self.__make_relative_name(path)
|
||||
images[name] = await self.make_image_by_name(name)
|
||||
if is_part:
|
||||
name = self.__make_relative_name(root_path, dot_to_empty=True)
|
||||
part = _Part(name, root_path)
|
||||
await part._reload() # pylint: disable=protected-access
|
||||
parts[name] = part
|
||||
assert "" in parts, parts
|
||||
|
||||
self.__watchable_paths = watchable_paths
|
||||
self.__images = images
|
||||
self.__parts = parts
|
||||
|
||||
async def reload_parts_info(self) -> None:
|
||||
await asyncio.gather(*[part._reload() for part in self.parts.values()]) # pylint: disable=protected-access
|
||||
|
||||
def get_watchable_paths(self) -> list[str]:
|
||||
assert self.__watchable_paths is not None
|
||||
return list(self.__watchable_paths)
|
||||
|
||||
def __walk(self) -> list[tuple[str, bool, list[str]]]:
|
||||
return list(self.__inner_walk(self.__path))
|
||||
|
||||
def __inner_walk(self, root_path: str) -> Generator[tuple[str, bool, list[str]], None, None]:
|
||||
files: list[str] = []
|
||||
with os.scandir(root_path) as dir_iter:
|
||||
for item in sorted(dir_iter, key=operator.attrgetter("name")):
|
||||
if item.name.startswith(".") or item.name == "lost+found":
|
||||
continue
|
||||
try:
|
||||
if item.is_dir(follow_symlinks=False):
|
||||
item.stat() # Проверяем, не сдохла ли смонтированная NFS
|
||||
yield from self.__inner_walk(item.path)
|
||||
elif item.is_file(follow_symlinks=False):
|
||||
files.append(item.path)
|
||||
except Exception:
|
||||
pass
|
||||
yield (root_path, (root_path == self.__path or os.path.ismount(root_path)), files)
|
||||
|
||||
def __make_relative_name(self, path: str, dot_to_empty: bool=False) -> str:
|
||||
name = os.path.relpath(path, self.__path)
|
||||
assert name
|
||||
if dot_to_empty and name == ".":
|
||||
name = ""
|
||||
assert not name.startswith(".")
|
||||
return name
|
||||
|
||||
# =====
|
||||
|
||||
async def make_image_by_name(self, name: str) -> Image:
|
||||
assert name
|
||||
path = os.path.join(self.__path, name)
|
||||
return (await self.__make_image(name, path, True))
|
||||
|
||||
async def make_image_by_path(self, path: str) -> Image:
|
||||
assert path
|
||||
in_storage = (os.path.commonpath([self.__path, path]) == self.__path)
|
||||
if in_storage:
|
||||
name = self.__make_relative_name(path)
|
||||
else:
|
||||
name = os.path.basename(path)
|
||||
return (await self.__make_image(name, path, in_storage))
|
||||
|
||||
async def __make_image(self, name: str, path: str, in_storage: bool) -> Image:
|
||||
assert name
|
||||
assert path
|
||||
image = Image(name, path, (self if in_storage else None))
|
||||
await image._reload() # pylint: disable=protected-access
|
||||
return image
|
||||
|
||||
def _get_root_path(self) -> str: # Only for Image()
|
||||
return self.__path
|
||||
|
||||
# =====
|
||||
|
||||
async def remount_rw(self, rw: bool, fatal: bool=True) -> None:
|
||||
if not (await aiohelpers.remount("MSD", self.__remount_cmd, rw)):
|
||||
if fatal:
|
||||
raise MsdError("Can't execute remount helper")
|
||||
@ -1,171 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
|
||||
from typing import Callable
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
from ...logging import get_logger
|
||||
|
||||
from ... import tools
|
||||
from ... import aiotools
|
||||
from ... import htclient
|
||||
|
||||
from ...yamlconf import Option
|
||||
|
||||
from ...validators.basic import valid_stripped_string_not_empty
|
||||
from ...validators.basic import valid_bool
|
||||
from ...validators.basic import valid_number
|
||||
from ...validators.basic import valid_float_f01
|
||||
|
||||
from . import BaseUserGpioDriver
|
||||
from . import GpioDriverOfflineError
|
||||
|
||||
|
||||
# =====
|
||||
class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attributes
|
||||
def __init__(
|
||||
self,
|
||||
instance_name: str,
|
||||
notifier: aiotools.AioNotifier,
|
||||
|
||||
url: str,
|
||||
verify: bool,
|
||||
user: str,
|
||||
passwd: str,
|
||||
state_poll: float,
|
||||
timeout: float,
|
||||
) -> None:
|
||||
|
||||
super().__init__(instance_name, notifier)
|
||||
|
||||
self.__url = url
|
||||
self.__verify = verify
|
||||
self.__user = user
|
||||
self.__passwd = passwd
|
||||
self.__state_poll = state_poll
|
||||
self.__timeout = timeout
|
||||
|
||||
self.__initial: dict[str, (bool | None)] = {}
|
||||
|
||||
self.__state: dict[str, (bool | None)] = {}
|
||||
self.__update_notifier = aiotools.AioNotifier()
|
||||
|
||||
self.__http_session: (aiohttp.ClientSession | None) = None
|
||||
|
||||
@classmethod
|
||||
def get_plugin_options(cls) -> dict[str, Option]:
|
||||
return {
|
||||
"url": Option("", type=valid_stripped_string_not_empty),
|
||||
"verify": Option(True, type=valid_bool),
|
||||
"user": Option(""),
|
||||
"passwd": Option(""),
|
||||
"state_poll": Option(5.0, type=valid_float_f01),
|
||||
"timeout": Option(5.0, type=valid_float_f01),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_pin_validator(cls) -> Callable[[Any], Any]:
|
||||
return functools.partial(valid_number, min=0, max=7, name="ANELPWR channel")
|
||||
|
||||
def register_input(self, pin: str, debounce: float) -> None:
|
||||
_ = debounce
|
||||
self.__state[pin] = None
|
||||
|
||||
def register_output(self, pin: str, initial: (bool | None)) -> None:
|
||||
self.__initial[pin] = initial
|
||||
self.__state[pin] = None
|
||||
|
||||
def prepare(self) -> None:
|
||||
async def inner_prepare() -> None:
|
||||
await asyncio.gather(*[
|
||||
self.write(pin, state)
|
||||
for (pin, state) in self.__initial.items()
|
||||
if state is not None
|
||||
], return_exceptions=True)
|
||||
aiotools.run_sync(inner_prepare())
|
||||
|
||||
async def run(self) -> None:
|
||||
prev_state: (dict | None) = None
|
||||
while True:
|
||||
session = self.__ensure_http_session()
|
||||
try:
|
||||
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 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()
|
||||
prev_state = self.__state
|
||||
await self.__update_notifier.wait(self.__state_poll)
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
if self.__http_session:
|
||||
await self.__http_session.close()
|
||||
self.__http_session = None
|
||||
|
||||
async def read(self, pin: str) -> bool:
|
||||
if self.__state[pin] is None:
|
||||
raise GpioDriverOfflineError(self)
|
||||
return self.__state[pin] # type: ignore
|
||||
|
||||
async def write(self, pin: str, state: bool) -> None:
|
||||
session = self.__ensure_http_session()
|
||||
try:
|
||||
async with session.post(
|
||||
url=f"{self.__url}/ctrl.htm",
|
||||
data=f"F{pin}={int(state)}",
|
||||
headers={"Content-Type": "text/plain"},
|
||||
) 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()
|
||||
|
||||
def __ensure_http_session(self) -> aiohttp.ClientSession:
|
||||
if not self.__http_session:
|
||||
kwargs: dict = {
|
||||
"headers": {
|
||||
"User-Agent": htclient.make_user_agent("KVMD"),
|
||||
},
|
||||
"timeout": aiohttp.ClientTimeout(total=self.__timeout),
|
||||
}
|
||||
if self.__user:
|
||||
kwargs["auth"] = aiohttp.BasicAuth(self.__user, self.__passwd)
|
||||
if not self.__verify:
|
||||
kwargs["connector"] = aiohttp.TCPConnector(ssl=False)
|
||||
self.__http_session = aiohttp.ClientSession(**kwargs)
|
||||
return self.__http_session
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"ANELPWR({self._instance_name})"
|
||||
|
||||
__repr__ = __str__
|
||||
@ -1,86 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from typing import Callable
|
||||
from typing import Any
|
||||
|
||||
from ...logging import get_logger
|
||||
|
||||
from ... import tools
|
||||
from ... import aiotools
|
||||
from ... import aioproc
|
||||
|
||||
from ...yamlconf import Option
|
||||
|
||||
from ...validators.os import valid_command
|
||||
|
||||
from . import GpioDriverOfflineError
|
||||
from . import UserGpioModes
|
||||
from . import BaseUserGpioDriver
|
||||
|
||||
|
||||
# =====
|
||||
class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attributes
|
||||
def __init__( # pylint: disable=super-init-not-called
|
||||
self,
|
||||
instance_name: str,
|
||||
notifier: aiotools.AioNotifier,
|
||||
|
||||
cmd: list[str],
|
||||
) -> None:
|
||||
|
||||
super().__init__(instance_name, notifier)
|
||||
|
||||
self.__cmd = cmd
|
||||
|
||||
@classmethod
|
||||
def get_plugin_options(cls) -> dict:
|
||||
return {
|
||||
"cmd": Option([], type=valid_command),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_modes(cls) -> set[str]:
|
||||
return set([UserGpioModes.INPUT])
|
||||
|
||||
@classmethod
|
||||
def get_pin_validator(cls) -> Callable[[Any], Any]:
|
||||
return str
|
||||
|
||||
async def read(self, pin: str) -> bool:
|
||||
_ = pin
|
||||
try:
|
||||
proc = await aioproc.log_process(self.__cmd, logger=get_logger(0), prefix=str(self))
|
||||
return (proc.returncode == 0)
|
||||
except Exception as ex:
|
||||
get_logger(0).error("Can't run custom command [ %s ]: %s",
|
||||
tools.cmdfmt(self.__cmd), tools.efmt(ex))
|
||||
raise GpioDriverOfflineError(self)
|
||||
|
||||
async def write(self, pin: str, state: bool) -> None:
|
||||
_ = pin
|
||||
_ = state
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"CMDRET({self._instance_name})"
|
||||
|
||||
__repr__ = __str__
|
||||
@ -1,188 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import re
|
||||
import multiprocessing
|
||||
import functools
|
||||
import errno
|
||||
import time
|
||||
|
||||
from typing import Callable
|
||||
from typing import Any
|
||||
|
||||
import serial
|
||||
|
||||
from ...logging import get_logger
|
||||
|
||||
from ... import aiotools
|
||||
from ... import aiomulti
|
||||
from ... import aioproc
|
||||
|
||||
from ...yamlconf import Option
|
||||
|
||||
from ...validators.basic import valid_number
|
||||
from ...validators.basic import valid_float_f01
|
||||
from ...validators.os import valid_abs_path
|
||||
from ...validators.hw import valid_tty_speed
|
||||
|
||||
from . import GpioDriverOfflineError
|
||||
from . import BaseUserGpioDriver
|
||||
|
||||
|
||||
# =====
|
||||
class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attributes
|
||||
def __init__(
|
||||
self,
|
||||
instance_name: str,
|
||||
notifier: aiotools.AioNotifier,
|
||||
|
||||
device_path: str,
|
||||
speed: int,
|
||||
read_timeout: float,
|
||||
protocol: int,
|
||||
) -> None:
|
||||
|
||||
super().__init__(instance_name, notifier)
|
||||
|
||||
self.__device_path = device_path
|
||||
self.__speed = speed
|
||||
self.__read_timeout = read_timeout
|
||||
self.__protocol = protocol
|
||||
|
||||
self.__ctl_queue: "multiprocessing.Queue[int]" = multiprocessing.Queue()
|
||||
self.__channel_queue: "multiprocessing.Queue[int | None]" = multiprocessing.Queue()
|
||||
self.__channel: (int | None) = -1
|
||||
|
||||
self.__proc: (multiprocessing.Process | None) = None
|
||||
self.__stop_event = multiprocessing.Event()
|
||||
|
||||
@classmethod
|
||||
def get_plugin_options(cls) -> dict:
|
||||
return {
|
||||
"device": Option("", type=valid_abs_path, unpack_as="device_path"),
|
||||
"speed": Option(9600, type=valid_tty_speed),
|
||||
"read_timeout": Option(2.0, type=valid_float_f01),
|
||||
"protocol": Option(1, type=functools.partial(valid_number, min=1, max=2)),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_pin_validator(cls) -> Callable[[Any], Any]:
|
||||
return functools.partial(valid_number, min=0, max=3, name="Extron USB channel")
|
||||
|
||||
def prepare(self) -> None:
|
||||
assert self.__proc is None
|
||||
self.__proc = multiprocessing.Process(target=self.__serial_worker, daemon=True)
|
||||
self.__proc.start()
|
||||
|
||||
async def run(self) -> None:
|
||||
while True:
|
||||
(got, channel) = await aiomulti.queue_get_last(self.__channel_queue, 1)
|
||||
if got and self.__channel != channel:
|
||||
self.__channel = channel
|
||||
self._notifier.notify()
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
if self.__proc is not None:
|
||||
if self.__proc.is_alive():
|
||||
get_logger(0).info("Stopping %s daemon ...", self)
|
||||
self.__stop_event.set()
|
||||
if self.__proc.is_alive() or self.__proc.exitcode is not None:
|
||||
self.__proc.join()
|
||||
|
||||
async def read(self, pin: str) -> bool:
|
||||
if not self.__is_online():
|
||||
raise GpioDriverOfflineError(self)
|
||||
return (self.__channel == int(pin))
|
||||
|
||||
async def write(self, pin: str, state: bool) -> None:
|
||||
if not self.__is_online():
|
||||
raise GpioDriverOfflineError(self)
|
||||
if state:
|
||||
self.__ctl_queue.put_nowait(int(pin))
|
||||
|
||||
# =====
|
||||
|
||||
def __is_online(self) -> bool:
|
||||
return (
|
||||
self.__proc is not None
|
||||
and self.__proc.is_alive()
|
||||
and self.__channel is not None
|
||||
)
|
||||
|
||||
def __serial_worker(self) -> None:
|
||||
logger = aioproc.settle(str(self), f"gpio-extron-{self._instance_name}")
|
||||
while not self.__stop_event.is_set():
|
||||
try:
|
||||
with self.__get_serial() as tty:
|
||||
data = b""
|
||||
self.__channel_queue.put_nowait(-1)
|
||||
|
||||
# Switch and then recieve the state.
|
||||
# FIXME: Get actual state without modifying the current.
|
||||
self.__send_channel(tty, 0)
|
||||
|
||||
while not self.__stop_event.is_set():
|
||||
(channel, data) = self.__recv_channel(tty, data)
|
||||
if channel is not None:
|
||||
self.__channel_queue.put_nowait(channel)
|
||||
|
||||
(got, channel) = aiomulti.queue_get_last_sync(self.__ctl_queue, 0.1) # type: ignore
|
||||
if got:
|
||||
assert channel is not None
|
||||
self.__send_channel(tty, channel)
|
||||
|
||||
except Exception as ex:
|
||||
self.__channel_queue.put_nowait(None)
|
||||
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)
|
||||
time.sleep(1)
|
||||
|
||||
def __get_serial(self) -> serial.Serial:
|
||||
return serial.Serial(self.__device_path, self.__speed, timeout=self.__read_timeout)
|
||||
|
||||
def __recv_channel(self, tty: serial.Serial, data: bytes) -> tuple[(int | None), bytes]:
|
||||
channel: (int | None) = None
|
||||
if tty.in_waiting:
|
||||
data += tty.read_all()
|
||||
found = re.findall(b"Chn[0-3]", data)
|
||||
if found:
|
||||
channel = {
|
||||
b"Chn1": 0,
|
||||
b"Chn2": 1,
|
||||
b"Chn3": 2,
|
||||
b"Chn4": 3,
|
||||
}.get(found[-1], -1)
|
||||
data = data[-8:]
|
||||
return (channel, data)
|
||||
|
||||
def __send_channel(self, tty: serial.Serial, channel: int) -> None:
|
||||
assert 0 <= channel <= 3
|
||||
cmd = b"%d!\n" % (channel + 1)
|
||||
tty.write(cmd)
|
||||
tty.flush()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Extron({self._instance_name})"
|
||||
|
||||
__repr__ = __str__
|
||||
@ -1,191 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import re
|
||||
import multiprocessing
|
||||
import functools
|
||||
import errno
|
||||
import time
|
||||
|
||||
from typing import Callable
|
||||
from typing import Any
|
||||
|
||||
import serial
|
||||
|
||||
from ...logging import get_logger
|
||||
|
||||
from ... import aiotools
|
||||
from ... import aiomulti
|
||||
from ... import aioproc
|
||||
|
||||
from ...yamlconf import Option
|
||||
|
||||
from ...validators.basic import valid_number
|
||||
from ...validators.basic import valid_float_f01
|
||||
from ...validators.os import valid_abs_path
|
||||
from ...validators.hw import valid_tty_speed
|
||||
|
||||
from . import GpioDriverOfflineError
|
||||
from . import BaseUserGpioDriver
|
||||
|
||||
|
||||
# =====
|
||||
class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attributes
|
||||
def __init__(
|
||||
self,
|
||||
instance_name: str,
|
||||
notifier: aiotools.AioNotifier,
|
||||
|
||||
device_path: str,
|
||||
speed: int,
|
||||
read_timeout: float,
|
||||
protocol: int,
|
||||
) -> None:
|
||||
|
||||
super().__init__(instance_name, notifier)
|
||||
|
||||
self.__device_path = device_path
|
||||
self.__speed = speed
|
||||
self.__read_timeout = read_timeout
|
||||
self.__protocol = protocol
|
||||
|
||||
self.__ctl_queue: "multiprocessing.Queue[int]" = multiprocessing.Queue()
|
||||
self.__channel_queue: "multiprocessing.Queue[int | None]" = multiprocessing.Queue()
|
||||
self.__channel: (int | None) = -1
|
||||
|
||||
self.__proc: (multiprocessing.Process | None) = None
|
||||
self.__stop_event = multiprocessing.Event()
|
||||
|
||||
@classmethod
|
||||
def get_plugin_options(cls) -> dict:
|
||||
return {
|
||||
"device": Option("", type=valid_abs_path, unpack_as="device_path"),
|
||||
"speed": Option(115200, type=valid_tty_speed),
|
||||
"read_timeout": Option(2.0, type=valid_float_f01),
|
||||
"protocol": Option(1, type=functools.partial(valid_number, min=1, max=2)),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_pin_validator(cls) -> Callable[[Any], Any]:
|
||||
return functools.partial(valid_number, min=0, max=3, name="Ezcoo channel")
|
||||
|
||||
def prepare(self) -> None:
|
||||
assert self.__proc is None
|
||||
self.__proc = multiprocessing.Process(target=self.__serial_worker, daemon=True)
|
||||
self.__proc.start()
|
||||
|
||||
async def run(self) -> None:
|
||||
while True:
|
||||
(got, channel) = await aiomulti.queue_get_last(self.__channel_queue, 1)
|
||||
if got and self.__channel != channel:
|
||||
self.__channel = channel
|
||||
self._notifier.notify()
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
if self.__proc is not None:
|
||||
if self.__proc.is_alive():
|
||||
get_logger(0).info("Stopping %s daemon ...", self)
|
||||
self.__stop_event.set()
|
||||
if self.__proc.is_alive() or self.__proc.exitcode is not None:
|
||||
self.__proc.join()
|
||||
|
||||
async def read(self, pin: str) -> bool:
|
||||
if not self.__is_online():
|
||||
raise GpioDriverOfflineError(self)
|
||||
return (self.__channel == int(pin))
|
||||
|
||||
async def write(self, pin: str, state: bool) -> None:
|
||||
if not self.__is_online():
|
||||
raise GpioDriverOfflineError(self)
|
||||
if state:
|
||||
self.__ctl_queue.put_nowait(int(pin))
|
||||
|
||||
# =====
|
||||
|
||||
def __is_online(self) -> bool:
|
||||
return (
|
||||
self.__proc is not None
|
||||
and self.__proc.is_alive()
|
||||
and self.__channel is not None
|
||||
)
|
||||
|
||||
def __serial_worker(self) -> None:
|
||||
logger = aioproc.settle(str(self), f"gpio-ezcoo-{self._instance_name}")
|
||||
while not self.__stop_event.is_set():
|
||||
try:
|
||||
with self.__get_serial() as tty:
|
||||
data = b""
|
||||
self.__channel_queue.put_nowait(-1)
|
||||
|
||||
# Switch and then recieve the state.
|
||||
# FIXME: Get actual state without modifying the current.
|
||||
self.__send_channel(tty, 0)
|
||||
|
||||
while not self.__stop_event.is_set():
|
||||
(channel, data) = self.__recv_channel(tty, data)
|
||||
if channel is not None:
|
||||
self.__channel_queue.put_nowait(channel)
|
||||
|
||||
(got, channel) = aiomulti.queue_get_last_sync(self.__ctl_queue, 0.1) # type: ignore
|
||||
if got:
|
||||
assert channel is not None
|
||||
self.__send_channel(tty, channel)
|
||||
|
||||
except Exception as ex:
|
||||
self.__channel_queue.put_nowait(None)
|
||||
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)
|
||||
time.sleep(1)
|
||||
|
||||
def __get_serial(self) -> serial.Serial:
|
||||
return serial.Serial(self.__device_path, self.__speed, timeout=self.__read_timeout)
|
||||
|
||||
def __recv_channel(self, tty: serial.Serial, data: bytes) -> tuple[(int | None), bytes]:
|
||||
channel: (int | None) = None
|
||||
if tty.in_waiting:
|
||||
data += tty.read_all()
|
||||
found = re.findall(b"V[0-9a-fA-F]{2}S", data)
|
||||
if found:
|
||||
channel = {
|
||||
b"V0CS": 0,
|
||||
b"V18S": 1,
|
||||
b"V5ES": 2,
|
||||
b"V08S": 3,
|
||||
}.get(found[-1], -1)
|
||||
data = data[-8:]
|
||||
return (channel, data)
|
||||
|
||||
def __send_channel(self, tty: serial.Serial, channel: int) -> None:
|
||||
assert 0 <= channel <= 3
|
||||
cmd = b"%s OUT1 VS IN%d\n" % (
|
||||
(b"SET" if self.__protocol == 1 else b"EZS"),
|
||||
channel + 1,
|
||||
)
|
||||
tty.write(cmd * 2) # Twice because of ezcoo bugs
|
||||
tty.flush()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Ezcoo({self._instance_name})"
|
||||
|
||||
__repr__ = __str__
|
||||
@ -1,189 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import functools
|
||||
|
||||
from typing import Callable
|
||||
from typing import Any
|
||||
|
||||
import hid
|
||||
|
||||
from ...logging import get_logger
|
||||
|
||||
from ... import tools
|
||||
from ... import aiotools
|
||||
|
||||
from ...yamlconf import Option
|
||||
|
||||
from ...validators.basic import valid_number
|
||||
from ...validators.basic import valid_float_f01
|
||||
from ...validators.os import valid_abs_path
|
||||
|
||||
from . import GpioDriverOfflineError
|
||||
from . import UserGpioModes
|
||||
from . import BaseUserGpioDriver
|
||||
|
||||
|
||||
# =====
|
||||
class Plugin(BaseUserGpioDriver):
|
||||
# http://vusb.wikidot.com/project:driver-less-usb-relays-hid-interface
|
||||
# https://github.com/trezor/cython-hidapi/blob/6057d41b5a2552a70ff7117a9d19fc21bf863867/chid.pxd
|
||||
|
||||
def __init__( # pylint: disable=super-init-not-called
|
||||
self,
|
||||
instance_name: str,
|
||||
notifier: aiotools.AioNotifier,
|
||||
|
||||
device_path: str,
|
||||
state_poll: float,
|
||||
) -> None:
|
||||
|
||||
super().__init__(instance_name, notifier)
|
||||
|
||||
self.__device_path = device_path
|
||||
self.__state_poll = state_poll
|
||||
|
||||
self.__device: (hid.device | None) = None # type: ignore
|
||||
self.__stop = False
|
||||
|
||||
self.__initials: dict[int, (bool | None)] = {}
|
||||
|
||||
@classmethod
|
||||
def get_plugin_options(cls) -> dict:
|
||||
return {
|
||||
"device": Option("", type=valid_abs_path, unpack_as="device_path"),
|
||||
"state_poll": Option(5.0, type=valid_float_f01),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_modes(cls) -> set[str]:
|
||||
return set([UserGpioModes.OUTPUT])
|
||||
|
||||
@classmethod
|
||||
def get_pin_validator(cls) -> Callable[[Any], Any]:
|
||||
return functools.partial(valid_number, min=0, max=7, name="HID relay channel")
|
||||
|
||||
def register_output(self, pin: str, initial: (bool | None)) -> None:
|
||||
self.__initials[int(pin)] = initial
|
||||
|
||||
def prepare(self) -> None:
|
||||
logger = get_logger(0)
|
||||
logger.info("Probing driver %s on %s ...", self, self.__device_path)
|
||||
try:
|
||||
with self.__ensure_device("probing"):
|
||||
pass
|
||||
except Exception as ex:
|
||||
logger.error("Can't probe %s on %s: %s",
|
||||
self, self.__device_path, tools.efmt(ex))
|
||||
self.__reset_pins()
|
||||
|
||||
async def run(self) -> None:
|
||||
prev_raw = -1
|
||||
while True:
|
||||
try:
|
||||
raw = self.__inner_read_raw()
|
||||
except Exception:
|
||||
raw = -1
|
||||
if raw != prev_raw:
|
||||
self._notifier.notify()
|
||||
prev_raw = raw
|
||||
await asyncio.sleep(self.__state_poll)
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
self.__reset_pins()
|
||||
self.__close_device()
|
||||
self.__stop = True
|
||||
|
||||
async def read(self, pin: str) -> bool:
|
||||
try:
|
||||
return self.__inner_read(int(pin))
|
||||
except Exception:
|
||||
raise GpioDriverOfflineError(self)
|
||||
|
||||
async def write(self, pin: str, state: bool) -> None:
|
||||
try:
|
||||
return self.__inner_write(int(pin), state)
|
||||
except Exception:
|
||||
raise GpioDriverOfflineError(self)
|
||||
|
||||
# =====
|
||||
|
||||
def __reset_pins(self) -> None:
|
||||
logger = get_logger(0)
|
||||
for (pin, state) in self.__initials.items():
|
||||
if state is not None:
|
||||
logger.info("Resetting pin=%d to state=%d of %s on %s: ...",
|
||||
pin, state, self, self.__device_path)
|
||||
try:
|
||||
self.__inner_write(pin, state)
|
||||
except Exception as ex:
|
||||
logger.error("Can't reset pin=%d of %s on %s: %s",
|
||||
pin, self, self.__device_path, tools.efmt(ex))
|
||||
|
||||
def __inner_read(self, pin: int) -> bool:
|
||||
assert 0 <= pin <= 7
|
||||
return bool(self.__inner_read_raw() & (1 << pin))
|
||||
|
||||
def __inner_read_raw(self) -> int:
|
||||
with self.__ensure_device("reading") as device:
|
||||
return device.get_feature_report(1, 8)[7]
|
||||
|
||||
def __inner_write(self, pin: int, state: bool) -> None:
|
||||
assert 0 <= pin <= 7
|
||||
with self.__ensure_device("writing") as device:
|
||||
report = [(0xFF if state else 0xFD), pin + 1] # Pin numeration starts from 0
|
||||
result = device.send_feature_report(report)
|
||||
if result < 0:
|
||||
raise RuntimeError(f"Retval of send_feature_report() < 0: {result}")
|
||||
|
||||
@contextlib.contextmanager
|
||||
def __ensure_device(self, context: str) -> hid.device: # type: ignore
|
||||
assert not self.__stop
|
||||
if self.__device is None:
|
||||
device = hid.device() # type: ignore
|
||||
device.open_path(self.__device_path.encode("utf-8"))
|
||||
device.set_nonblocking(True)
|
||||
self.__device = device
|
||||
get_logger(0).info("Opened %s on %s while %s", self, self.__device_path, context)
|
||||
try:
|
||||
yield self.__device
|
||||
except Exception as ex:
|
||||
get_logger(0).error("Error occured on %s on %s while %s: %s",
|
||||
self, self.__device_path, context, tools.efmt(ex))
|
||||
self.__close_device()
|
||||
raise
|
||||
|
||||
def __close_device(self) -> None:
|
||||
if self.__device:
|
||||
try:
|
||||
self.__device.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.__device = None
|
||||
get_logger(0).info("Closed %s on %s", self, self.__device_path)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"HidRelay({self._instance_name})"
|
||||
|
||||
__repr__ = __str__
|
||||
@ -1,166 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import asyncio
|
||||
|
||||
from typing import Callable
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
from ...logging import get_logger
|
||||
|
||||
from ... import tools
|
||||
from ... import aiotools
|
||||
from ... import htclient
|
||||
|
||||
from ...yamlconf import Option
|
||||
|
||||
from ...validators.basic import valid_stripped_string_not_empty
|
||||
from ...validators.basic import valid_bool
|
||||
from ...validators.basic import valid_float_f01
|
||||
|
||||
from . import GpioDriverOfflineError
|
||||
from . import BaseUserGpioDriver
|
||||
|
||||
|
||||
# =====
|
||||
class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attributes
|
||||
# https://developers.meethue.com/develop/hue-api/lights-api
|
||||
# https://www.burgestrand.se/hue-api/api/lights
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
instance_name: str,
|
||||
notifier: aiotools.AioNotifier,
|
||||
|
||||
url: str,
|
||||
verify: bool,
|
||||
token: str,
|
||||
state_poll: float,
|
||||
timeout: float,
|
||||
) -> None:
|
||||
|
||||
super().__init__(instance_name, notifier)
|
||||
|
||||
self.__url = url
|
||||
self.__verify = verify
|
||||
self.__token = token
|
||||
self.__state_poll = state_poll
|
||||
self.__timeout = timeout
|
||||
|
||||
self.__initial: dict[str, (bool | None)] = {}
|
||||
|
||||
self.__state: dict[str, (bool | None)] = {}
|
||||
self.__update_notifier = aiotools.AioNotifier()
|
||||
|
||||
self.__http_session: (aiohttp.ClientSession | None) = None
|
||||
|
||||
@classmethod
|
||||
def get_plugin_options(cls) -> dict:
|
||||
return {
|
||||
"url": Option("", type=valid_stripped_string_not_empty),
|
||||
"verify": Option(True, type=valid_bool),
|
||||
"token": Option("", type=valid_stripped_string_not_empty),
|
||||
"state_poll": Option(5.0, type=valid_float_f01),
|
||||
"timeout": Option(5.0, type=valid_float_f01),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_pin_validator(cls) -> Callable[[Any], Any]:
|
||||
return valid_stripped_string_not_empty
|
||||
|
||||
def register_input(self, pin: str, debounce: float) -> None:
|
||||
_ = debounce
|
||||
self.__state[pin] = None
|
||||
|
||||
def register_output(self, pin: str, initial: (bool | None)) -> None:
|
||||
self.__initial[pin] = initial
|
||||
self.__state[pin] = None
|
||||
|
||||
def prepare(self) -> None:
|
||||
async def inner_prepare() -> None:
|
||||
await asyncio.gather(*[
|
||||
self.write(pin, state)
|
||||
for (pin, state) in self.__initial.items()
|
||||
if state is not None
|
||||
], return_exceptions=True)
|
||||
aiotools.run_sync(inner_prepare())
|
||||
|
||||
async def run(self) -> None:
|
||||
prev_state: (dict | None) = None
|
||||
while True:
|
||||
session = self.__ensure_http_session()
|
||||
try:
|
||||
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 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()
|
||||
prev_state = self.__state
|
||||
await self.__update_notifier.wait(self.__state_poll)
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
if self.__http_session:
|
||||
await self.__http_session.close()
|
||||
self.__http_session = None
|
||||
|
||||
async def read(self, pin: str) -> bool:
|
||||
if self.__state[pin] is None:
|
||||
raise GpioDriverOfflineError(self)
|
||||
return self.__state[pin] # type: ignore
|
||||
|
||||
async def write(self, pin: str, state: bool) -> None:
|
||||
session = self.__ensure_http_session()
|
||||
try:
|
||||
async with session.put(
|
||||
url=f"{self.__url}/api/{self.__token}/lights/{pin}/state",
|
||||
json={"on": state},
|
||||
) 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()
|
||||
|
||||
def __ensure_http_session(self) -> aiohttp.ClientSession:
|
||||
if not self.__http_session:
|
||||
kwargs: dict = {
|
||||
"headers": {
|
||||
"User-Agent": htclient.make_user_agent("KVMD"),
|
||||
},
|
||||
"timeout": aiohttp.ClientTimeout(total=self.__timeout),
|
||||
}
|
||||
if not self.__verify:
|
||||
kwargs["connector"] = aiohttp.TCPConnector(ssl=False)
|
||||
self.__http_session = aiohttp.ClientSession(**kwargs)
|
||||
return self.__http_session
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Hue({self._instance_name})"
|
||||
|
||||
__repr__ = __str__
|
||||
@ -1,133 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import asyncio
|
||||
|
||||
from typing import Callable
|
||||
from typing import Any
|
||||
|
||||
import gpiod
|
||||
|
||||
from ... import aiotools
|
||||
|
||||
from ...yamlconf import Option
|
||||
|
||||
from ...validators.os import valid_abs_path
|
||||
from ...validators.hw import valid_gpio_pin
|
||||
|
||||
from . import UserGpioModes
|
||||
from . import BaseUserGpioDriver
|
||||
|
||||
|
||||
# =====
|
||||
class Plugin(BaseUserGpioDriver):
|
||||
def __init__(
|
||||
self,
|
||||
instance_name: str,
|
||||
notifier: aiotools.AioNotifier,
|
||||
|
||||
device_path: str,
|
||||
) -> None:
|
||||
|
||||
super().__init__(instance_name, notifier)
|
||||
|
||||
self.__device_path = device_path
|
||||
|
||||
self.__tasks: dict[int, (asyncio.Task | None)] = {}
|
||||
self.__line_req: (gpiod.LineRequest | None) = None
|
||||
|
||||
@classmethod
|
||||
def get_plugin_options(cls) -> dict:
|
||||
return {
|
||||
"device": Option("/dev/gpiochip0", type=valid_abs_path, unpack_as="device_path"),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_modes(cls) -> set[str]:
|
||||
return set([UserGpioModes.OUTPUT])
|
||||
|
||||
@classmethod
|
||||
def get_pin_validator(cls) -> Callable[[Any], Any]:
|
||||
return valid_gpio_pin
|
||||
|
||||
def register_output(self, pin: str, initial: (bool | None)) -> None:
|
||||
_ = initial
|
||||
self.__tasks[int(pin)] = None
|
||||
|
||||
def prepare(self) -> None:
|
||||
self.__line_req = gpiod.request_lines(
|
||||
self.__device_path,
|
||||
consumer="kvmd::locator",
|
||||
config={
|
||||
tuple(self.__tasks): gpiod.LineSettings(
|
||||
direction=gpiod.line.Direction.OUTPUT,
|
||||
output_value=gpiod.line.Value(False),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
tasks = [
|
||||
task
|
||||
for task in self.__tasks.values()
|
||||
if task is not None
|
||||
]
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
if self.__line_req:
|
||||
try:
|
||||
self.__line_req.release()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def read(self, pin: str) -> bool:
|
||||
return (self.__tasks[int(pin)] is not None)
|
||||
|
||||
async def write(self, pin: str, state: bool) -> None:
|
||||
pin_int = int(pin)
|
||||
task = self.__tasks[pin_int]
|
||||
if state and task is None:
|
||||
self.__tasks[pin_int] = asyncio.create_task(self.__blink(pin_int))
|
||||
elif not state and task is not None:
|
||||
task.cancel()
|
||||
await task
|
||||
self.__tasks[pin_int] = None
|
||||
|
||||
async def __blink(self, pin: int) -> None:
|
||||
assert pin in self.__tasks
|
||||
assert self.__line_req
|
||||
try:
|
||||
state = True
|
||||
while True:
|
||||
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_req.set_value(pin, gpiod.line.Value(False))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Locator({self._instance_name})"
|
||||
|
||||
__repr__ = __str__
|
||||
@ -1,165 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
|
||||
from typing import Callable
|
||||
from typing import Any
|
||||
|
||||
import hid
|
||||
|
||||
from ...logging import get_logger
|
||||
|
||||
from ... import tools
|
||||
from ... import aiotools
|
||||
|
||||
from ...yamlconf import Option
|
||||
|
||||
from ...validators.basic import valid_number
|
||||
from ...validators.os import valid_abs_path
|
||||
|
||||
from . import GpioDriverOfflineError
|
||||
from . import UserGpioModes
|
||||
from . import BaseUserGpioDriver
|
||||
|
||||
|
||||
# =====
|
||||
class Plugin(BaseUserGpioDriver):
|
||||
# This is like a HID relay, but does not support the common protocol.
|
||||
# So no status reports, ugh.
|
||||
# Why make a HID USB if you can't implement such simple things?
|
||||
# So many questions, and so few answers...
|
||||
|
||||
def __init__( # pylint: disable=super-init-not-called
|
||||
self,
|
||||
instance_name: str,
|
||||
notifier: aiotools.AioNotifier,
|
||||
|
||||
device_path: str,
|
||||
) -> None:
|
||||
|
||||
super().__init__(instance_name, notifier)
|
||||
|
||||
self.__device_path = device_path
|
||||
|
||||
self.__device: (hid.device | None) = None # type: ignore
|
||||
self.__stop = False
|
||||
|
||||
self.__initials: dict[int, bool] = {}
|
||||
self.__state: dict[int, bool] = dict.fromkeys(range(8), False)
|
||||
|
||||
@classmethod
|
||||
def get_plugin_options(cls) -> dict:
|
||||
return {
|
||||
"device": Option("", type=valid_abs_path, unpack_as="device_path"),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_modes(cls) -> set[str]:
|
||||
return set([UserGpioModes.OUTPUT])
|
||||
|
||||
@classmethod
|
||||
def get_pin_validator(cls) -> Callable[[Any], Any]:
|
||||
return functools.partial(valid_number, min=0, max=7, name="NOYITO relay channel")
|
||||
|
||||
def register_output(self, pin: str, initial: (bool | None)) -> None:
|
||||
self.__initials[int(pin)] = bool(initial)
|
||||
|
||||
def prepare(self) -> None:
|
||||
logger = get_logger(0)
|
||||
logger.info("Probing driver %s on %s ...", self, self.__device_path)
|
||||
try:
|
||||
with self.__ensure_device("probing"):
|
||||
pass
|
||||
except Exception as ex:
|
||||
logger.error("Can't probe %s on %s: %s",
|
||||
self, self.__device_path, tools.efmt(ex))
|
||||
self.__reset_pins()
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
self.__reset_pins()
|
||||
self.__close_device()
|
||||
self.__stop = True
|
||||
|
||||
async def read(self, pin: str) -> bool:
|
||||
return self.__state[int(pin)]
|
||||
|
||||
async def write(self, pin: str, state: bool) -> None:
|
||||
try:
|
||||
return self.__inner_write(int(pin), state)
|
||||
except Exception:
|
||||
raise GpioDriverOfflineError(self)
|
||||
|
||||
# =====
|
||||
|
||||
def __reset_pins(self) -> None:
|
||||
logger = get_logger(0)
|
||||
for (pin, state) in self.__initials.items():
|
||||
logger.info("Resetting pin=%d to state=%d of %s on %s: ...",
|
||||
pin, state, self, self.__device_path)
|
||||
try:
|
||||
self.__inner_write(pin, state)
|
||||
except Exception as ex:
|
||||
logger.error("Can't reset pin=%d of %s on %s: %s",
|
||||
pin, self, self.__device_path, tools.efmt(ex))
|
||||
|
||||
def __inner_write(self, pin: int, state: bool) -> None:
|
||||
assert 0 <= pin <= 7
|
||||
with self.__ensure_device("writing") as device:
|
||||
report = [0xA0, pin + 1, int(state), 0]
|
||||
report[-1] = sum(report)
|
||||
result = device.write(report)
|
||||
if result < 0:
|
||||
raise RuntimeError(f"Retval of send_feature_report() < 0: {result}")
|
||||
self.__state[pin] = state
|
||||
|
||||
@contextlib.contextmanager
|
||||
def __ensure_device(self, context: str) -> hid.device: # type: ignore
|
||||
assert not self.__stop
|
||||
if self.__device is None:
|
||||
device = hid.device() # type: ignore
|
||||
device.open_path(self.__device_path.encode("utf-8"))
|
||||
device.set_nonblocking(True)
|
||||
self.__device = device
|
||||
get_logger(0).info("Opened %s on %s while %s", self, self.__device_path, context)
|
||||
try:
|
||||
yield self.__device
|
||||
except Exception as ex:
|
||||
get_logger(0).error("Error occured on %s on %s while %s: %s",
|
||||
self, self.__device_path, context, tools.efmt(ex))
|
||||
self.__close_device()
|
||||
raise
|
||||
|
||||
def __close_device(self) -> None:
|
||||
if self.__device:
|
||||
try:
|
||||
self.__device.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.__device = None
|
||||
get_logger(0).info("Closed %s on %s", self, self.__device_path)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Noyito({self._instance_name})"
|
||||
|
||||
__repr__ = __str__
|
||||
@ -1,165 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
from typing import Callable
|
||||
from typing import Any
|
||||
|
||||
from ...logging import get_logger
|
||||
|
||||
from ...inotify import Inotify
|
||||
|
||||
from ... import aiotools
|
||||
from ... import usb
|
||||
|
||||
from ...yamlconf import Section
|
||||
|
||||
from ...validators.basic import valid_stripped_string_not_empty
|
||||
|
||||
from . import BaseUserGpioDriver
|
||||
|
||||
|
||||
# =====
|
||||
class Plugin(BaseUserGpioDriver):
|
||||
def __init__(
|
||||
self,
|
||||
instance_name: str,
|
||||
notifier: aiotools.AioNotifier,
|
||||
|
||||
otg_config: Section, # XXX: Not from options, see /kvmd/apps/kvmd/__init__.py for details
|
||||
) -> None:
|
||||
|
||||
super().__init__(instance_name, notifier)
|
||||
|
||||
self.__udc: str = otg_config.udc
|
||||
self.__init_delay: float = otg_config.init_delay
|
||||
|
||||
gadget: str = otg_config.gadget
|
||||
self.__udc_path = usb.get_gadget_path(gadget, usb.G_UDC)
|
||||
self.__functions_path = usb.get_gadget_path(gadget, usb.G_FUNCTIONS)
|
||||
self.__profile_path = usb.get_gadget_path(gadget, usb.G_PROFILE)
|
||||
|
||||
self.__lock = asyncio.Lock()
|
||||
|
||||
@classmethod
|
||||
def get_pin_validator(cls) -> Callable[[Any], Any]:
|
||||
return valid_stripped_string_not_empty
|
||||
|
||||
def prepare(self) -> None:
|
||||
self.__udc = usb.find_udc(self.__udc)
|
||||
get_logger().info("Using UDC %s", self.__udc)
|
||||
|
||||
async def run(self) -> None:
|
||||
logger = get_logger(0)
|
||||
while True:
|
||||
try:
|
||||
while True:
|
||||
self._notifier.notify()
|
||||
if os.path.isfile(self.__udc_path):
|
||||
break
|
||||
await asyncio.sleep(5)
|
||||
|
||||
with Inotify() as inotify:
|
||||
await inotify.watch_all_changes(os.path.dirname(self.__udc_path))
|
||||
await inotify.watch_all_changes(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.restart:
|
||||
logger.warning("Got fatal inotify event: %s; reinitializing OTG-bind ...", event)
|
||||
need_restart = True
|
||||
break
|
||||
if need_restart:
|
||||
break
|
||||
if need_notify:
|
||||
self._notifier.notify()
|
||||
except Exception:
|
||||
logger.exception("Unexpected OTG-bind watcher error")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def read(self, pin: str) -> bool:
|
||||
if pin == "udc":
|
||||
return self.__is_udc_enabled()
|
||||
return os.path.exists(self.__get_fdest_path(pin))
|
||||
|
||||
async def write(self, pin: str, state: bool) -> None:
|
||||
async with self.__lock:
|
||||
if self.read(pin) == state:
|
||||
return
|
||||
if pin == "udc":
|
||||
if state:
|
||||
self.__recreate_profile()
|
||||
self.__set_udc_enabled(state)
|
||||
else:
|
||||
if self.__is_udc_enabled():
|
||||
self.__set_udc_enabled(False)
|
||||
try:
|
||||
if state:
|
||||
os.symlink(self.__get_fsrc_path(pin), self.__get_fdest_path(pin))
|
||||
else:
|
||||
os.unlink(self.__get_fdest_path(pin))
|
||||
except (FileNotFoundError, FileExistsError):
|
||||
pass
|
||||
finally:
|
||||
self.__recreate_profile()
|
||||
try:
|
||||
await asyncio.sleep(self.__init_delay)
|
||||
finally:
|
||||
self.__set_udc_enabled(True)
|
||||
|
||||
def __recreate_profile(self) -> None:
|
||||
# XXX: See pikvm/pikvm#1235
|
||||
# After unbind and bind, the gadgets stop working,
|
||||
# unless we recreate their links in the profile.
|
||||
# Some kind of kernel bug.
|
||||
for func in os.listdir(self.__profile_path):
|
||||
path = self.__get_fdest_path(func)
|
||||
if os.path.islink(path):
|
||||
try:
|
||||
os.unlink(path)
|
||||
os.symlink(self.__get_fsrc_path(func), path)
|
||||
except (FileNotFoundError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
def __get_fsrc_path(self, func: str) -> str:
|
||||
return os.path.join(self.__functions_path, func)
|
||||
|
||||
def __get_fdest_path(self, func: str) -> str:
|
||||
return os.path.join(self.__profile_path, func)
|
||||
|
||||
def __set_udc_enabled(self, enabled: bool) -> None:
|
||||
with open(self.__udc_path, "w") as file:
|
||||
file.write(self.__udc if enabled else "\n")
|
||||
|
||||
def __is_udc_enabled(self) -> bool:
|
||||
with open(self.__udc_path) as file:
|
||||
return bool(file.read().strip())
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"GPIO({self._instance_name})"
|
||||
|
||||
__repr__ = __str__
|
||||
@ -1,193 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# Modified by SppokHCK September 2021 <Find me on Discord spook#8911> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import re
|
||||
import multiprocessing
|
||||
import functools
|
||||
import errno
|
||||
import time
|
||||
|
||||
from typing import Callable
|
||||
from typing import Any
|
||||
|
||||
import serial
|
||||
|
||||
from ...logging import get_logger
|
||||
|
||||
from ... import aiotools
|
||||
from ... import aiomulti
|
||||
from ... import aioproc
|
||||
|
||||
from ...yamlconf import Option
|
||||
|
||||
from ...validators.basic import valid_number
|
||||
from ...validators.basic import valid_float_f01
|
||||
from ...validators.os import valid_abs_path
|
||||
from ...validators.hw import valid_tty_speed
|
||||
|
||||
from . import GpioDriverOfflineError
|
||||
from . import BaseUserGpioDriver
|
||||
|
||||
|
||||
# =====
|
||||
class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attributes
|
||||
def __init__(
|
||||
self,
|
||||
instance_name: str,
|
||||
notifier: aiotools.AioNotifier,
|
||||
|
||||
device_path: str,
|
||||
speed: int,
|
||||
read_timeout: float,
|
||||
protocol: int,
|
||||
) -> None:
|
||||
|
||||
super().__init__(instance_name, notifier)
|
||||
|
||||
self.__device_path = device_path
|
||||
self.__speed = speed
|
||||
self.__read_timeout = read_timeout
|
||||
self.__protocol = protocol
|
||||
|
||||
self.__ctl_queue: "multiprocessing.Queue[int]" = multiprocessing.Queue()
|
||||
self.__channel_queue: "multiprocessing.Queue[int | None]" = multiprocessing.Queue()
|
||||
self.__channel: (int | None) = -1
|
||||
|
||||
self.__proc: (multiprocessing.Process | None) = None
|
||||
self.__stop_event = multiprocessing.Event()
|
||||
|
||||
@classmethod
|
||||
def get_plugin_options(cls) -> dict:
|
||||
return {
|
||||
"device": Option("", type=valid_abs_path, unpack_as="device_path"),
|
||||
"speed": Option(19200, type=valid_tty_speed),
|
||||
"read_timeout": Option(2.0, type=valid_float_f01),
|
||||
"protocol": Option(1, type=functools.partial(valid_number, min=1, max=2)),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_pin_validator(cls) -> Callable[[Any], Any]:
|
||||
return functools.partial(valid_number, min=0, max=15, name="PWAY channel")
|
||||
|
||||
def prepare(self) -> None:
|
||||
assert self.__proc is None
|
||||
self.__proc = multiprocessing.Process(target=self.__serial_worker, daemon=True)
|
||||
self.__proc.start()
|
||||
|
||||
async def run(self) -> None:
|
||||
while True:
|
||||
(got, channel) = await aiomulti.queue_get_last(self.__channel_queue, 1)
|
||||
if got and self.__channel != channel:
|
||||
self.__channel = channel
|
||||
self._notifier.notify()
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
if self.__proc is not None:
|
||||
if self.__proc.is_alive():
|
||||
get_logger(0).info("Stopping %s daemon ...", self)
|
||||
self.__stop_event.set()
|
||||
if self.__proc.is_alive() or self.__proc.exitcode is not None:
|
||||
self.__proc.join()
|
||||
|
||||
async def read(self, pin: str) -> bool:
|
||||
if not self.__is_online():
|
||||
raise GpioDriverOfflineError(self)
|
||||
return (self.__channel == int(pin))
|
||||
|
||||
async def write(self, pin: str, state: bool) -> None:
|
||||
if not self.__is_online():
|
||||
raise GpioDriverOfflineError(self)
|
||||
if state:
|
||||
self.__ctl_queue.put_nowait(int(pin))
|
||||
|
||||
# =====
|
||||
|
||||
def __is_online(self) -> bool:
|
||||
return (
|
||||
self.__proc is not None
|
||||
and self.__proc.is_alive()
|
||||
and self.__channel is not None
|
||||
)
|
||||
|
||||
def __serial_worker(self) -> None:
|
||||
logger = aioproc.settle(str(self), f"gpio-pway-{self._instance_name}")
|
||||
while not self.__stop_event.is_set():
|
||||
try:
|
||||
with self.__get_serial() as tty:
|
||||
data = b""
|
||||
self.__channel_queue.put_nowait(-1)
|
||||
|
||||
# Switch and then recieve the state.
|
||||
# FIXME: Get actual state without modifying the current.
|
||||
# I'm lazy and like the idea of the KVM resetting to port 1 on reboot of the PiKVM.
|
||||
self.__reset(tty)
|
||||
|
||||
while not self.__stop_event.is_set():
|
||||
(channel, data) = self.__recv_channel(tty, data)
|
||||
if channel is not None:
|
||||
self.__channel_queue.put_nowait(channel)
|
||||
|
||||
(got, channel) = aiomulti.queue_get_last_sync(self.__ctl_queue, 0.1) # type: ignore
|
||||
if got:
|
||||
assert channel is not None
|
||||
self.__send_channel(tty, channel)
|
||||
|
||||
except Exception as ex:
|
||||
self.__channel_queue.put_nowait(None)
|
||||
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)
|
||||
time.sleep(1)
|
||||
|
||||
def __get_serial(self) -> serial.Serial:
|
||||
return serial.Serial(self.__device_path, self.__speed, timeout=self.__read_timeout)
|
||||
|
||||
def __recv_channel(self, tty: serial.Serial, data: bytes) -> tuple[(int | None), bytes]:
|
||||
channel: (int | None) = None
|
||||
if tty.in_waiting:
|
||||
data += tty.read_all()
|
||||
# When you switch ports you see something like "VGA_SWITCH_CONTROL=[0-15]" for ports 1-16
|
||||
found = re.findall(b"VGA_SWITCH_CONTROL=[0-9]*", data)
|
||||
if found:
|
||||
channel = int(found[0].decode().split("=")[1])
|
||||
data = data[-8:]
|
||||
return (channel, data)
|
||||
|
||||
def __send_channel(self, tty: serial.Serial, channel: int) -> None:
|
||||
# Set a channel by sending PS [1-16]
|
||||
# Note that the recv is 0-based index, while send is 1-based
|
||||
# We add 1 to the "channel" to normalize for the 1-based index on send
|
||||
tty.write(b"PS %d\r" % (channel + 1))
|
||||
tty.flush()
|
||||
|
||||
def __reset(self, tty: serial.Serial) -> None:
|
||||
# Reset by sending PS without port number
|
||||
tty.write(b"PS\r")
|
||||
tty.flush()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"PWAY({self._instance_name})"
|
||||
|
||||
__repr__ = __str__
|
||||
@ -1,128 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Shantur Rathore <i@shantur.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from typing import Callable
|
||||
from typing import Any
|
||||
|
||||
from periphery import PWM
|
||||
|
||||
from ...logging import get_logger
|
||||
|
||||
from ... import tools
|
||||
from ... import aiotools
|
||||
|
||||
from ...yamlconf import Option
|
||||
|
||||
from ...validators.basic import valid_int_f0
|
||||
from ...validators.hw import valid_gpio_pin
|
||||
|
||||
from . import GpioDriverOfflineError
|
||||
from . import UserGpioModes
|
||||
from . import BaseUserGpioDriver
|
||||
|
||||
|
||||
# =====
|
||||
class Plugin(BaseUserGpioDriver):
|
||||
def __init__( # pylint: disable=super-init-not-called
|
||||
self,
|
||||
instance_name: str,
|
||||
notifier: aiotools.AioNotifier,
|
||||
|
||||
chip: int,
|
||||
period: int,
|
||||
duty_cycle_push: int,
|
||||
duty_cycle_release: int,
|
||||
) -> None:
|
||||
|
||||
super().__init__(instance_name, notifier)
|
||||
|
||||
self.__chip = chip
|
||||
self.__period = period
|
||||
self.__duty_cycle_push = duty_cycle_push
|
||||
self.__duty_cycle_release = duty_cycle_release
|
||||
|
||||
self.__channels: dict[int, (bool | None)] = {}
|
||||
self.__pwms: dict[int, PWM] = {}
|
||||
|
||||
@classmethod
|
||||
def get_plugin_options(cls) -> dict:
|
||||
return {
|
||||
"chip": Option(0, type=valid_int_f0),
|
||||
"period": Option(20000000, type=valid_int_f0),
|
||||
"duty_cycle_push": Option(1500000, type=valid_int_f0),
|
||||
"duty_cycle_release": Option(1000000, type=valid_int_f0),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_modes(cls) -> set[str]:
|
||||
return set([UserGpioModes.OUTPUT])
|
||||
|
||||
@classmethod
|
||||
def get_pin_validator(cls) -> Callable[[Any], Any]:
|
||||
return valid_gpio_pin
|
||||
|
||||
def register_output(self, pin: str, initial: (bool | None)) -> None:
|
||||
self.__channels[int(pin)] = initial
|
||||
|
||||
def prepare(self) -> None:
|
||||
logger = get_logger(0)
|
||||
for (pin, initial) in self.__channels.items():
|
||||
try:
|
||||
logger.info("Probing pwm chip %d channel %d ...", self.__chip, pin)
|
||||
pwm = PWM(self.__chip, pin)
|
||||
self.__pwms[pin] = pwm
|
||||
pwm.period_ns = self.__period
|
||||
pwm.duty_cycle_ns = self.__get_duty_cycle(bool(initial))
|
||||
pwm.enable()
|
||||
except Exception as ex:
|
||||
logger.error("Can't get PWM chip %d channel %d: %s",
|
||||
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 ex:
|
||||
get_logger(0).error("Can't cleanup PWM chip %d channel %d: %s",
|
||||
self.__chip, pin, tools.efmt(ex))
|
||||
|
||||
async def read(self, pin: str) -> bool:
|
||||
try:
|
||||
return (self.__pwms[int(pin)].duty_cycle_ns == self.__duty_cycle_push)
|
||||
except Exception:
|
||||
raise GpioDriverOfflineError(self)
|
||||
|
||||
async def write(self, pin: str, state: bool) -> None:
|
||||
try:
|
||||
self.__pwms[int(pin)].duty_cycle_ns = self.__get_duty_cycle(state)
|
||||
except Exception:
|
||||
raise GpioDriverOfflineError(self)
|
||||
|
||||
def __get_duty_cycle(self, state: bool) -> int:
|
||||
return (self.__duty_cycle_push if state else self.__duty_cycle_release)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"PWM({self._instance_name})"
|
||||
|
||||
__repr__ = __str__
|
||||
@ -1,86 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Shantur Rathore <i@shantur.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from ... import aiotools
|
||||
|
||||
from ...yamlconf import Option
|
||||
|
||||
from ...validators.basic import valid_number
|
||||
from ...validators.basic import valid_int_f0
|
||||
|
||||
from .pwm import Plugin as PwmPlugin
|
||||
|
||||
|
||||
# =====
|
||||
class Plugin(PwmPlugin):
|
||||
def __init__( # pylint: disable=super-init-not-called,too-many-arguments
|
||||
self,
|
||||
instance_name: str,
|
||||
notifier: aiotools.AioNotifier,
|
||||
|
||||
chip: int,
|
||||
period: int,
|
||||
duty_cycle_min: int,
|
||||
duty_cycle_max: int,
|
||||
angle_min: float,
|
||||
angle_max: float,
|
||||
angle_push: float,
|
||||
angle_release: float,
|
||||
) -> None:
|
||||
|
||||
angle_push = min(max(angle_push, angle_min), angle_max)
|
||||
angle_release = min(max(angle_release, angle_min), angle_max)
|
||||
|
||||
duty_cycle_per_degree = (duty_cycle_max - duty_cycle_min) / (angle_max - angle_min)
|
||||
|
||||
duty_cycle_push = int(duty_cycle_per_degree * (angle_push - angle_min) + duty_cycle_min)
|
||||
duty_cycle_release = int(duty_cycle_per_degree * (angle_release - angle_min) + duty_cycle_min)
|
||||
|
||||
super().__init__(
|
||||
instance_name=instance_name,
|
||||
notifier=notifier,
|
||||
|
||||
chip=chip,
|
||||
period=period,
|
||||
duty_cycle_push=duty_cycle_push,
|
||||
duty_cycle_release=duty_cycle_release,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_plugin_options(cls) -> dict:
|
||||
valid_angle = (lambda arg: valid_number(arg, min=-360.0, max=360.0, type=float))
|
||||
return {
|
||||
"chip": Option(0, type=valid_int_f0),
|
||||
"period": Option(20000000, type=valid_int_f0),
|
||||
"duty_cycle_min": Option(1000000, type=valid_int_f0),
|
||||
"duty_cycle_max": Option(2000000, type=valid_int_f0),
|
||||
"angle_min": Option(0.0, type=valid_angle),
|
||||
"angle_max": Option(180.0, type=valid_angle),
|
||||
"angle_push": Option(100.0, type=valid_angle),
|
||||
"angle_release": Option(120.0, type=valid_angle),
|
||||
}
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Servo({self._instance_name})"
|
||||
|
||||
__repr__ = __str__
|
||||
@ -1,198 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
|
||||
from typing import Callable
|
||||
from typing import Any
|
||||
|
||||
import serial_asyncio
|
||||
|
||||
from ...logging import get_logger
|
||||
|
||||
from ... import tools
|
||||
from ... import aiotools
|
||||
|
||||
from ...yamlconf import Option
|
||||
|
||||
from ...validators.basic import valid_number
|
||||
from ...validators.basic import valid_float_f0
|
||||
from ...validators.basic import valid_float_f01
|
||||
from ...validators.net import valid_ip_or_host
|
||||
from ...validators.net import valid_port
|
||||
from ...validators.os import valid_abs_path
|
||||
from ...validators.hw import valid_tty_speed
|
||||
|
||||
from . import BaseUserGpioDriver
|
||||
from . import GpioDriverOfflineError
|
||||
|
||||
|
||||
# =====
|
||||
class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attributes
|
||||
def __init__(
|
||||
self,
|
||||
instance_name: str,
|
||||
notifier: aiotools.AioNotifier,
|
||||
|
||||
host: str,
|
||||
port: int,
|
||||
|
||||
device_path: str,
|
||||
speed: int,
|
||||
|
||||
timeout: float,
|
||||
switch_delay: float,
|
||||
state_poll: float,
|
||||
) -> None:
|
||||
|
||||
super().__init__(instance_name, notifier)
|
||||
|
||||
self.__host = host
|
||||
self.__port = port
|
||||
|
||||
self.__device_path = device_path
|
||||
self.__speed = speed
|
||||
|
||||
self.__timeout = timeout
|
||||
self.__switch_delay = switch_delay
|
||||
self.__state_poll = state_poll
|
||||
|
||||
self.__reader: (asyncio.StreamReader | None) = None
|
||||
self.__writer: (asyncio.StreamWriter | None) = None
|
||||
self.__active: int = -1
|
||||
self.__update_notifier = aiotools.AioNotifier()
|
||||
|
||||
@classmethod
|
||||
def get_plugin_options(cls) -> dict:
|
||||
return {
|
||||
"host": Option("", type=valid_ip_or_host, if_empty=""),
|
||||
"port": Option(5000, type=valid_port),
|
||||
|
||||
"device": Option("", type=valid_abs_path, only_if="!host", unpack_as="device_path"),
|
||||
"speed": Option(9600, type=valid_tty_speed),
|
||||
|
||||
"timeout": Option(5.0, type=valid_float_f01),
|
||||
"switch_delay": Option(1.0, type=valid_float_f0),
|
||||
"state_poll": Option(10.0, type=valid_float_f01),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_pin_validator(cls) -> Callable[[Any], Any]:
|
||||
return functools.partial(valid_number, min=0, max=15, name="TESmart channel")
|
||||
|
||||
async def run(self) -> None:
|
||||
prev_active = -2
|
||||
while True:
|
||||
try:
|
||||
# Current active port command uses 0-based numbering (0x00->PC1...0x0F->PC16)
|
||||
self.__active = int(await self.__send_command(b"\x10\x00"))
|
||||
except Exception:
|
||||
pass
|
||||
if self.__active != prev_active:
|
||||
self._notifier.notify()
|
||||
prev_active = self.__active
|
||||
await self.__update_notifier.wait(self.__state_poll)
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
await self.__close_device()
|
||||
self.__active = -1
|
||||
|
||||
async def read(self, pin: str) -> bool:
|
||||
return (self.__active == int(pin))
|
||||
|
||||
async def write(self, pin: str, state: bool) -> None:
|
||||
# Switch input source command uses 1-based numbering (0x01->PC1...0x10->PC16)
|
||||
channel = int(pin) + 1
|
||||
assert 1 <= channel <= 16
|
||||
if state:
|
||||
await self.__send_command("{:c}{:c}".format(1, channel).encode())
|
||||
await asyncio.sleep(self.__switch_delay) # Slowdown
|
||||
self.__update_notifier.notify()
|
||||
|
||||
# =====
|
||||
|
||||
async def __send_command(self, cmd: bytes) -> int:
|
||||
assert len(cmd) == 2
|
||||
await self.__ensure_device()
|
||||
assert self.__reader is not None
|
||||
assert self.__writer is not None
|
||||
try:
|
||||
self.__writer.write(b"\xAA\xBB\x03%s\xEE" % (cmd))
|
||||
await asyncio.wait_for(
|
||||
asyncio.ensure_future(self.__writer.drain()),
|
||||
timeout=self.__timeout,
|
||||
)
|
||||
return (await asyncio.wait_for(
|
||||
asyncio.ensure_future(self.__reader.readexactly(6)),
|
||||
timeout=self.__timeout,
|
||||
))[4]
|
||||
except Exception as ex:
|
||||
get_logger(0).error("Can't send command to TESmart KVM [%s]:%d: %s",
|
||||
self.__host, self.__port, tools.efmt(ex))
|
||||
await self.__close_device()
|
||||
self.__active = -1
|
||||
raise GpioDriverOfflineError(self)
|
||||
finally:
|
||||
await self.__close_device()
|
||||
|
||||
async def __ensure_device(self) -> None:
|
||||
if self.__reader is None or self.__writer is None:
|
||||
if self.__host:
|
||||
await self.__ensure_device_net()
|
||||
else:
|
||||
await self.__ensure_device_serial()
|
||||
|
||||
async def __ensure_device_net(self) -> None:
|
||||
try:
|
||||
(self.__reader, self.__writer) = await asyncio.wait_for(
|
||||
asyncio.ensure_future(asyncio.open_connection(self.__host, self.__port)),
|
||||
timeout=self.__timeout,
|
||||
)
|
||||
except Exception as ex:
|
||||
get_logger(0).error("Can't connect to TESmart KVM [%s]:%d: %s",
|
||||
self.__host, self.__port, tools.efmt(ex))
|
||||
raise GpioDriverOfflineError(self)
|
||||
|
||||
async def __ensure_device_serial(self) -> None:
|
||||
try:
|
||||
(self.__reader, self.__writer) = await asyncio.wait_for(
|
||||
serial_asyncio.open_serial_connection(url=self.__device_path, baudrate=self.__speed),
|
||||
timeout=self.__timeout,
|
||||
)
|
||||
except Exception as ex:
|
||||
get_logger(0).error("Can't connect to TESmart KVM [%s]:%d: %s",
|
||||
self.__device_path, self.__speed, tools.efmt(ex))
|
||||
raise GpioDriverOfflineError(self)
|
||||
|
||||
async def __close_device(self) -> None:
|
||||
if self.__writer:
|
||||
await aiotools.close_writer(self.__writer)
|
||||
self.__reader = None
|
||||
self.__writer = None
|
||||
|
||||
# =====
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"TESmart({self._instance_name})"
|
||||
|
||||
__repr__ = __str__
|
||||
@ -1,193 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# 2021-2021 Sebastian Goscik <sebastian.goscik@live.co.uk> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import re
|
||||
import multiprocessing
|
||||
import functools
|
||||
import errno
|
||||
import time
|
||||
|
||||
from typing import Callable
|
||||
from typing import Any
|
||||
|
||||
import serial
|
||||
|
||||
from ...logging import get_logger
|
||||
|
||||
from ... import aiotools
|
||||
from ... import aiomulti
|
||||
from ... import aioproc
|
||||
|
||||
from ...yamlconf import Option
|
||||
|
||||
from ...validators.basic import valid_number
|
||||
from ...validators.basic import valid_float_f01
|
||||
from ...validators.os import valid_abs_path
|
||||
from ...validators.hw import valid_tty_speed
|
||||
|
||||
from . import GpioDriverOfflineError
|
||||
from . import BaseUserGpioDriver
|
||||
|
||||
|
||||
# =====
|
||||
class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attributes
|
||||
def __init__(
|
||||
self,
|
||||
instance_name: str,
|
||||
notifier: aiotools.AioNotifier,
|
||||
|
||||
device_path: str,
|
||||
speed: int,
|
||||
read_timeout: float,
|
||||
protocol: int,
|
||||
) -> None:
|
||||
|
||||
super().__init__(instance_name, notifier)
|
||||
|
||||
self.__device_path = device_path
|
||||
self.__speed = speed
|
||||
self.__read_timeout = read_timeout
|
||||
self.__protocol = protocol # https://github.com/pikvm/kvmd/pull/158
|
||||
|
||||
self.__ctl_queue: "multiprocessing.Queue[int]" = multiprocessing.Queue()
|
||||
self.__channel_queue: "multiprocessing.Queue[int | None]" = multiprocessing.Queue()
|
||||
self.__channel: (int | None) = -1
|
||||
|
||||
self.__proc: (multiprocessing.Process | None) = None
|
||||
self.__stop_event = multiprocessing.Event()
|
||||
|
||||
@classmethod
|
||||
def get_plugin_options(cls) -> dict:
|
||||
return {
|
||||
"device": Option("", type=valid_abs_path, unpack_as="device_path"),
|
||||
"speed": Option(19200, type=valid_tty_speed),
|
||||
"read_timeout": Option(2.0, type=valid_float_f01),
|
||||
"protocol": Option(1, type=functools.partial(valid_number, min=1, max=2)),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_pin_validator(cls) -> Callable[[Any], Any]:
|
||||
return functools.partial(valid_number, min=0, max=3, name="XH-HK4401 channel")
|
||||
|
||||
def prepare(self) -> None:
|
||||
assert self.__proc is None
|
||||
self.__proc = multiprocessing.Process(target=self.__serial_worker, daemon=True)
|
||||
self.__proc.start()
|
||||
|
||||
async def run(self) -> None:
|
||||
while True:
|
||||
(got, channel) = await aiomulti.queue_get_last(self.__channel_queue, 1)
|
||||
if got and self.__channel != channel:
|
||||
self.__channel = channel
|
||||
self._notifier.notify()
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
if self.__proc is not None:
|
||||
if self.__proc.is_alive():
|
||||
get_logger(0).info("Stopping %s daemon ...", self)
|
||||
self.__stop_event.set()
|
||||
if self.__proc.is_alive() or self.__proc.exitcode is not None:
|
||||
self.__proc.join()
|
||||
|
||||
async def read(self, pin: str) -> bool:
|
||||
if not self.__is_online():
|
||||
raise GpioDriverOfflineError(self)
|
||||
return (self.__channel == int(pin))
|
||||
|
||||
async def write(self, pin: str, state: bool) -> None:
|
||||
if not self.__is_online():
|
||||
raise GpioDriverOfflineError(self)
|
||||
if state:
|
||||
self.__ctl_queue.put_nowait(int(pin))
|
||||
|
||||
# =====
|
||||
|
||||
def __is_online(self) -> bool:
|
||||
return (
|
||||
self.__proc is not None
|
||||
and self.__proc.is_alive()
|
||||
and self.__channel is not None
|
||||
)
|
||||
|
||||
def __serial_worker(self) -> None:
|
||||
logger = aioproc.settle(str(self), f"gpio-xh-hk4401-{self._instance_name}")
|
||||
while not self.__stop_event.is_set():
|
||||
try:
|
||||
with self.__get_serial() as tty:
|
||||
data = b""
|
||||
self.__channel_queue.put_nowait(-1)
|
||||
|
||||
# Wait for first port heartbeat to set correct channel (~2 sec max).
|
||||
# Only for the classic switch with protocol version 1.
|
||||
while self.__protocol == 1:
|
||||
(channel, data) = self.__recv_channel(tty, data)
|
||||
if channel is not None:
|
||||
self.__channel_queue.put_nowait(channel)
|
||||
break
|
||||
|
||||
while not self.__stop_event.is_set():
|
||||
(channel, data) = self.__recv_channel(tty, data)
|
||||
if channel is not None:
|
||||
self.__channel_queue.put_nowait(channel)
|
||||
|
||||
(got, channel) = aiomulti.queue_get_last_sync(self.__ctl_queue, 0.1) # type: ignore
|
||||
if got:
|
||||
assert channel is not None
|
||||
self.__send_channel(tty, channel)
|
||||
if self.__protocol == 2:
|
||||
self.__channel_queue.put_nowait(channel)
|
||||
|
||||
except Exception as ex:
|
||||
self.__channel_queue.put_nowait(None)
|
||||
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)
|
||||
time.sleep(1)
|
||||
|
||||
def __get_serial(self) -> serial.Serial:
|
||||
return serial.Serial(self.__device_path, self.__speed, timeout=self.__read_timeout)
|
||||
|
||||
def __recv_channel(self, tty: serial.Serial, data: bytes) -> tuple[(int | None), bytes]:
|
||||
channel: (int | None) = None
|
||||
if tty.in_waiting:
|
||||
data += tty.read_all()
|
||||
found = re.findall((b"AG0[1-4]gA" if self.__protocol == 1 else b"G0[1-4]gA\x00"), data)
|
||||
if found:
|
||||
try:
|
||||
channel = int(found[-1][2:4] if self.__protocol == 1 else found[-1][1:3]) - 1
|
||||
except Exception:
|
||||
channel = None
|
||||
data = data[-12:]
|
||||
return (channel, data)
|
||||
|
||||
def __send_channel(self, tty: serial.Serial, channel: int) -> None:
|
||||
assert 0 <= channel <= 3
|
||||
cmd = ("SW{port}\r\nAG{port:02d}gA" if self.__protocol == 1 else "G{port:02d}gA\x00")
|
||||
tty.write(cmd.format(port=(channel + 1)).encode())
|
||||
tty.flush()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"XH-HK4401({self._instance_name})"
|
||||
|
||||
__repr__ = __str__
|
||||
@ -1,115 +0,0 @@
|
||||
FROM archlinux/archlinux:base
|
||||
|
||||
RUN mkdir -p /etc/pacman.d/hooks \
|
||||
&& ln -s /dev/null /etc/pacman.d/hooks/30-systemd-tmpfiles.hook
|
||||
|
||||
RUN echo 'Server = https://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch' > /etc/pacman.d/mirrorlist \
|
||||
&& pacman-key --init \
|
||||
&& pacman-key --populate archlinux
|
||||
|
||||
RUN pacman --noconfirm --ask=4 -Syy \
|
||||
&& pacman --needed --noconfirm --ask=4 -S \
|
||||
glibc \
|
||||
pacman \
|
||||
openssl \
|
||||
openssl-1.1 \
|
||||
&& pacman-db-upgrade \
|
||||
&& pacman --noconfirm --ask=4 -Syu \
|
||||
&& pacman --needed --noconfirm --ask=4 -S \
|
||||
p11-kit \
|
||||
archlinux-keyring \
|
||||
ca-certificates \
|
||||
ca-certificates-mozilla \
|
||||
ca-certificates-utils \
|
||||
&& pacman -Syu --noconfirm --ask=4 \
|
||||
&& pacman -S --needed --noconfirm --ask=4 \
|
||||
base-devel \
|
||||
autoconf-archive \
|
||||
help2man \
|
||||
m4 \
|
||||
vim \
|
||||
git \
|
||||
libjpeg \
|
||||
libevent \
|
||||
libutil-linux \
|
||||
libbsd \
|
||||
python \
|
||||
python-pip \
|
||||
python-build \
|
||||
python-wheel \
|
||||
python-setuptools \
|
||||
python-tox \
|
||||
python-mako \
|
||||
python-yaml \
|
||||
python-aiohttp \
|
||||
python-aiofiles \
|
||||
python-async-lru \
|
||||
python-passlib \
|
||||
python-pyotp \
|
||||
python-qrcode \
|
||||
python-pyserial \
|
||||
python-setproctitle \
|
||||
python-psutil \
|
||||
python-netifaces \
|
||||
python-systemd \
|
||||
python-dbus \
|
||||
python-dbus-next \
|
||||
python-pygments \
|
||||
python-pam \
|
||||
python-pillow \
|
||||
python-xlib \
|
||||
python-mako \
|
||||
libxkbcommon \
|
||||
python-hidapi \
|
||||
python-ldap \
|
||||
python-zstandard \
|
||||
libgpiod \
|
||||
freetype2 \
|
||||
nginx-mainline \
|
||||
tesseract \
|
||||
tesseract-data-eng \
|
||||
tesseract-data-rus \
|
||||
ipmitool \
|
||||
socat \
|
||||
eslint \
|
||||
npm \
|
||||
shellcheck \
|
||||
&& (pacman -Sc --noconfirm || true) \
|
||||
&& rm -rf /var/cache/pacman/pkg/*
|
||||
|
||||
COPY testenv/requirements.txt requirements.txt
|
||||
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple \
|
||||
&& pip install --break-system-packages -r requirements.txt \
|
||||
&& pip cache purge
|
||||
|
||||
# https://stackoverflow.com/questions/57534295
|
||||
WORKDIR /root
|
||||
RUN npm config set registry https://registry.npmmirror.com \
|
||||
&& npm install htmlhint -g \
|
||||
&& npm install pug \
|
||||
&& npm install pug-cli -g \
|
||||
&& npm install @babel/eslint-parser -g \
|
||||
&& npm cache clean -f
|
||||
WORKDIR /
|
||||
|
||||
ARG USTREAMER_MIN_VERSION
|
||||
ENV USTREAMER_MIN_VERSION $USTREAMER_MIN_VERSION
|
||||
RUN echo $USTREAMER_MIN_VERSION
|
||||
RUN git clone https://github.com/pikvm/ustreamer \
|
||||
&& cd ustreamer \
|
||||
&& make WITH_PYTHON=1 PREFIX=/usr DESTDIR=/ install \
|
||||
&& cd - \
|
||||
&& rm -rf ustreamer
|
||||
|
||||
RUN mkdir -p \
|
||||
/etc/kvmd/{nginx,vnc} \
|
||||
/var/lib/kvmd/msd \
|
||||
/var/lib/kvmd/pst/data \
|
||||
/opt/vc/bin
|
||||
|
||||
COPY testenv/fakes/vcgencmd /usr/bin/
|
||||
COPY testenv/fakes/sys /fake_sysfs/sys
|
||||
COPY testenv/fakes/proc /fake_procfs/proc
|
||||
COPY testenv/fakes/etc /fake_etc/etc
|
||||
|
||||
CMD /bin/bash
|
||||
@ -1,26 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
# =====
|
||||
ETC_PREFIX = "/fake_etc"
|
||||
SYSFS_PREFIX = "/fake_sysfs"
|
||||
PROCFS_PREFIX = "/fake_procfs"
|
||||
@ -1,2 +0,0 @@
|
||||
LABEL=PIPST /var/lib/kvmd/pst ext4 nodev,nosuid,noexec,ro,errors=remount-ro,data=journal,X-kvmd.pst-user=kvmd-pst 0 0
|
||||
LABEL=PIMSD /var/lib/kvmd/msd ext4 nodev,nosuid,noexec,ro,errors=remount-ro,data=journal,X-kvmd.otgmsd-user=kvmd 0 0
|
||||
@ -1 +0,0 @@
|
||||
Virtual Raspberry Pi
|
||||
@ -1 +0,0 @@
|
||||
0000000000000000
|
||||
@ -1 +0,0 @@
|
||||
36511
|
||||
@ -1 +0,0 @@
|
||||
../../../../bus/platform/drivers/dwc2
|
||||
@ -1 +0,0 @@
|
||||
configured
|
||||
@ -1 +0,0 @@
|
||||
../../functions/mass_storage.usb0
|
||||
@ -1 +0,0 @@
|
||||
1
|
||||
@ -1 +0,0 @@
|
||||
file
|
||||
@ -1 +0,0 @@
|
||||
1
|
||||
@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
case $1 in
|
||||
get_throttled) echo "throttled=0x0";;
|
||||
esac
|
||||
File diff suppressed because it is too large
Load Diff
3651
testenv/js/janus.js
3651
testenv/js/janus.js
File diff suppressed because it is too large
Load Diff
@ -1,4 +0,0 @@
|
||||
[run]
|
||||
data_file = testenv/.coverage
|
||||
omit =
|
||||
*/__main__.py,
|
||||
@ -1,57 +0,0 @@
|
||||
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"},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
];
|
||||
@ -1,10 +0,0 @@
|
||||
[flake8]
|
||||
inline-quotes = double
|
||||
max-line-length = 160
|
||||
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
|
||||
# Q003 Change outer quotes to avoid escaping inner quotes
|
||||
@ -1,3 +0,0 @@
|
||||
{
|
||||
"src-not-empty": false
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
[mypy]
|
||||
python_version = 3.11
|
||||
ignore_missing_imports = true
|
||||
disallow_untyped_defs = true
|
||||
strict_optional = true
|
||||
@ -1,74 +0,0 @@
|
||||
[MASTER]
|
||||
ignore = .git
|
||||
extension-pkg-whitelist =
|
||||
setproctitle,
|
||||
gpiod,
|
||||
spidev,
|
||||
netifaces,
|
||||
_ldap,
|
||||
ustreamer,
|
||||
hid,
|
||||
|
||||
[DESIGN]
|
||||
min-public-methods = 0
|
||||
max-args = 10
|
||||
|
||||
[TYPECHECK]
|
||||
ignored-classes=
|
||||
AioQueue,
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable =
|
||||
file-ignored,
|
||||
locally-disabled,
|
||||
fixme,
|
||||
missing-docstring,
|
||||
superfluous-parens,
|
||||
duplicate-code,
|
||||
broad-except,
|
||||
redundant-keyword-arg,
|
||||
wrong-import-order,
|
||||
too-many-ancestors,
|
||||
no-else-return,
|
||||
len-as-condition,
|
||||
raise-missing-from,
|
||||
consider-using-in,
|
||||
unsubscriptable-object,
|
||||
unused-private-member,
|
||||
unspecified-encoding,
|
||||
consider-using-f-string,
|
||||
unnecessary-lambda-assignment,
|
||||
too-many-positional-arguments,
|
||||
# https://github.com/PyCQA/pylint/issues/3882
|
||||
|
||||
[CLASSES]
|
||||
exclude-protected =
|
||||
_unpack,
|
||||
|
||||
[REPORTS]
|
||||
msg-template = {symbol} -- {path}:{line}({obj}): {msg}
|
||||
|
||||
[FORMAT]
|
||||
max-line-length = 160
|
||||
|
||||
[BASIC]
|
||||
# Good variable names which should always be accepted, separated by a comma
|
||||
good-names = _, __, x, y, ws
|
||||
|
||||
# Regular expression matching correct method names
|
||||
method-rgx = [a-z_][a-z0-9_]{2,50}$
|
||||
|
||||
# Regular expression matching correct function names
|
||||
function-rgx = [a-z_][a-z0-9_]{2,50}$
|
||||
|
||||
# Regular expression which should only match correct module level names
|
||||
const-rgx = ([a-zA-Z_][a-zA-Z0-9_]*)$
|
||||
|
||||
# Regular expression which should only match correct argument names
|
||||
argument-rgx = [a-z_][a-z0-9_]{1,30}$
|
||||
|
||||
# Regular expression which should only match correct variable names
|
||||
variable-rgx = [a-z_][a-z0-9_]{1,30}$
|
||||
|
||||
# Regular expression which should only match correct instance attribute names
|
||||
attr-rgx = [a-z_][a-z0-9_]{1,30}$
|
||||
@ -1,59 +0,0 @@
|
||||
InotifyMask.ACCESS
|
||||
InotifyMask.ATTRIB
|
||||
InotifyMask.CLOSE_WRITE
|
||||
InotifyMask.CLOSE_NOWRITE
|
||||
InotifyMask.CREATE
|
||||
InotifyMask.DELETE
|
||||
InotifyMask.DELETE_SELF
|
||||
InotifyMask.MODIFY
|
||||
InotifyMask.MOVE_SELF
|
||||
InotifyMask.MOVED_FROM
|
||||
InotifyMask.MOVED_TO
|
||||
InotifyMask.OPEN
|
||||
|
||||
InotifyMask.IGNORED
|
||||
InotifyMask.ISDIR
|
||||
InotifyMask.Q_OVERFLOW
|
||||
InotifyMask.UNMOUNT
|
||||
|
||||
IpmiServer.handle_raw_request
|
||||
|
||||
SpiDev.no_cs
|
||||
SpiDev.cshigh
|
||||
SpiDev.max_speed_hz
|
||||
|
||||
_DriveImage.complete
|
||||
|
||||
_AtxApiPart.switch_power
|
||||
|
||||
_UsbKey.arduino_modifier_code
|
||||
|
||||
_KeyMapping.web_name
|
||||
_KeyMapping.mcu_code
|
||||
_KeyMapping.usb_key
|
||||
_KeyMapping.ps2_key
|
||||
_KeyMapping.at1_code
|
||||
_KeyMapping.x11_keys
|
||||
|
||||
_SharedParams.width
|
||||
_SharedParams.height
|
||||
|
||||
_Netcfg.net_ip
|
||||
_Netcfg.net_mask
|
||||
_Netcfg.dhcp_option_3
|
||||
|
||||
_ScriptWriter.get_args
|
||||
|
||||
_pwm.period_ns
|
||||
|
||||
_Edid.set_mfc_id
|
||||
_Edid.set_product_id
|
||||
_Edid.set_serial
|
||||
_Edid.set_monitor_name
|
||||
_Edid.set_monitor_serial
|
||||
_Edid.set_audio
|
||||
|
||||
Dumper.ignore_aliases
|
||||
|
||||
_auth_server_port_fixture
|
||||
_test_user
|
||||
@ -1,3 +0,0 @@
|
||||
PIKVM_MODEL=test_model
|
||||
PIKVM_VIDEO=test_video
|
||||
PIKVM_BOARD=test_board
|
||||
@ -1,9 +0,0 @@
|
||||
python-periphery
|
||||
pyserial-asyncio
|
||||
pyghmi
|
||||
spidev
|
||||
pyrad
|
||||
types-PyYAML
|
||||
types-aiofiles
|
||||
luma.oled
|
||||
pyfatfs
|
||||
0
testenv/run/.gitignore
vendored
0
testenv/run/.gitignore
vendored
@ -1,20 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
@ -1,20 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
@ -1,20 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
@ -1,169 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import os
|
||||
import hashlib
|
||||
import tempfile
|
||||
import builtins
|
||||
import getpass
|
||||
|
||||
from typing import Generator
|
||||
from typing import Any
|
||||
|
||||
import passlib.apache
|
||||
|
||||
import pytest
|
||||
|
||||
from kvmd.apps.htpasswd import main
|
||||
|
||||
|
||||
# =====
|
||||
def _make_passwd(user: str) -> str:
|
||||
return hashlib.md5(user.encode()).hexdigest()
|
||||
|
||||
|
||||
@pytest.fixture(name="htpasswd", params=[[], ["admin"], ["admin", "user"]])
|
||||
def _htpasswd_fixture(request) -> Generator[passlib.apache.HtpasswdFile, None, None]: # type: ignore
|
||||
(fd, path) = tempfile.mkstemp()
|
||||
os.close(fd)
|
||||
htpasswd = passlib.apache.HtpasswdFile(path)
|
||||
for user in request.param:
|
||||
htpasswd.set_password(user, _make_passwd(user))
|
||||
htpasswd.save()
|
||||
yield htpasswd
|
||||
os.remove(path)
|
||||
|
||||
|
||||
def _run_htpasswd(cmd: list[str], htpasswd_path: str, internal_type: str="htpasswd") -> None:
|
||||
cmd = ["kvmd-htpasswd", *cmd, "--set-options"]
|
||||
if internal_type != "htpasswd": # By default
|
||||
cmd.append("kvmd/auth/internal/type=" + internal_type)
|
||||
if htpasswd_path:
|
||||
cmd.append("kvmd/auth/internal/file=" + htpasswd_path)
|
||||
main(cmd)
|
||||
|
||||
|
||||
# =====
|
||||
def test_ok__list(htpasswd: passlib.apache.HtpasswdFile, capsys) -> None: # type: ignore
|
||||
_run_htpasswd(["list"], htpasswd.path)
|
||||
(out, err) = capsys.readouterr()
|
||||
assert len(err) == 0
|
||||
assert sorted(filter(None, out.split("\n"))) == sorted(htpasswd.users()) == sorted(set(htpasswd.users()))
|
||||
|
||||
|
||||
# =====
|
||||
def test_ok__set_change_stdin(htpasswd: passlib.apache.HtpasswdFile, mocker) -> None: # type: ignore
|
||||
old_users = set(htpasswd.users())
|
||||
if old_users:
|
||||
assert htpasswd.check_password("admin", _make_passwd("admin"))
|
||||
|
||||
mocker.patch.object(builtins, "input", (lambda: " test "))
|
||||
_run_htpasswd(["set", "admin", "--read-stdin"], htpasswd.path)
|
||||
|
||||
htpasswd.load(force=True)
|
||||
assert htpasswd.check_password("admin", " test ")
|
||||
assert old_users == set(htpasswd.users())
|
||||
|
||||
|
||||
def test_ok__set_add_stdin(htpasswd: passlib.apache.HtpasswdFile, mocker) -> None: # type: ignore
|
||||
old_users = set(htpasswd.users())
|
||||
if old_users:
|
||||
mocker.patch.object(builtins, "input", (lambda: " test "))
|
||||
_run_htpasswd(["set", "new", "--read-stdin"], htpasswd.path)
|
||||
|
||||
htpasswd.load(force=True)
|
||||
assert htpasswd.check_password("new", " test ")
|
||||
assert old_users.union(["new"]) == set(htpasswd.users())
|
||||
|
||||
|
||||
# =====
|
||||
def test_ok__set_change_getpass(htpasswd: passlib.apache.HtpasswdFile, mocker) -> None: # type: ignore
|
||||
old_users = set(htpasswd.users())
|
||||
if old_users:
|
||||
assert htpasswd.check_password("admin", _make_passwd("admin"))
|
||||
|
||||
mocker.patch.object(getpass, "getpass", (lambda *_, **__: " test "))
|
||||
_run_htpasswd(["set", "admin"], htpasswd.path)
|
||||
|
||||
htpasswd.load(force=True)
|
||||
assert htpasswd.check_password("admin", " test ")
|
||||
assert old_users == set(htpasswd.users())
|
||||
|
||||
|
||||
def test_fail__set_change_getpass(htpasswd: passlib.apache.HtpasswdFile, mocker) -> None: # type: ignore
|
||||
old_users = set(htpasswd.users())
|
||||
if old_users:
|
||||
assert htpasswd.check_password("admin", _make_passwd("admin"))
|
||||
|
||||
count = 0
|
||||
|
||||
def fake_getpass(*_: Any, **__: Any) -> str:
|
||||
nonlocal count
|
||||
assert count <= 1
|
||||
if count == 0:
|
||||
passwd = " test "
|
||||
else:
|
||||
passwd = "test "
|
||||
count += 1
|
||||
return passwd
|
||||
|
||||
mocker.patch.object(getpass, "getpass", fake_getpass)
|
||||
with pytest.raises(SystemExit, match="Sorry, passwords do not match"):
|
||||
_run_htpasswd(["set", "admin"], htpasswd.path)
|
||||
assert count == 2
|
||||
|
||||
htpasswd.load(force=True)
|
||||
assert htpasswd.check_password("admin", _make_passwd("admin"))
|
||||
assert old_users == set(htpasswd.users())
|
||||
|
||||
|
||||
# =====
|
||||
def test_ok__del(htpasswd: passlib.apache.HtpasswdFile) -> None:
|
||||
old_users = set(htpasswd.users())
|
||||
|
||||
if old_users:
|
||||
assert htpasswd.check_password("admin", _make_passwd("admin"))
|
||||
|
||||
_run_htpasswd(["del", "admin"], htpasswd.path)
|
||||
|
||||
htpasswd.load(force=True)
|
||||
assert not htpasswd.check_password("admin", _make_passwd("admin"))
|
||||
assert old_users.difference(["admin"]) == set(htpasswd.users())
|
||||
|
||||
|
||||
# =====
|
||||
def test_fail__not_htpasswd() -> None:
|
||||
with pytest.raises(SystemExit, match="Error: KVMD internal auth not using 'htpasswd'"):
|
||||
_run_htpasswd(["list"], "", internal_type="http")
|
||||
|
||||
|
||||
def test_fail__unknown_plugin() -> None:
|
||||
with pytest.raises(SystemExit, match="ConfigError: Unknown plugin 'auth/foobar'"):
|
||||
_run_htpasswd(["list"], "", internal_type="foobar")
|
||||
|
||||
|
||||
def test_fail__invalid_passwd(mocker, tmpdir) -> None: # type: ignore
|
||||
path = os.path.abspath(str(tmpdir.join("htpasswd")))
|
||||
open(path, "w").close() # pylint: disable=consider-using-with
|
||||
mocker.patch.object(builtins, "input", (lambda: "\n"))
|
||||
with pytest.raises(SystemExit, match="The argument is not a valid passwd characters"):
|
||||
_run_htpasswd(["set", "admin", "--read-stdin"], path)
|
||||
@ -1,20 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
@ -1,223 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import os
|
||||
import contextlib
|
||||
|
||||
from typing import AsyncGenerator
|
||||
|
||||
import passlib.apache
|
||||
|
||||
import pytest
|
||||
|
||||
from kvmd.yamlconf import make_config
|
||||
|
||||
from kvmd.apps.kvmd.auth import AuthManager
|
||||
|
||||
from kvmd.plugins.auth import get_auth_service_class
|
||||
|
||||
from kvmd.htserver import HttpExposed
|
||||
|
||||
|
||||
# =====
|
||||
_E_AUTH = HttpExposed("GET", "/foo_auth", True, (lambda: None))
|
||||
_E_UNAUTH = HttpExposed("GET", "/bar_unauth", True, (lambda: None))
|
||||
_E_FREE = HttpExposed("GET", "/baz_free", False, (lambda: None))
|
||||
|
||||
|
||||
def _make_service_kwargs(path: str) -> dict:
|
||||
cls = get_auth_service_class("htpasswd")
|
||||
scheme = cls.get_plugin_options()
|
||||
return make_config({"file": path}, scheme)._unpack()
|
||||
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def _get_configured_manager(
|
||||
unauth_paths: list[str],
|
||||
internal_path: str,
|
||||
external_path: str="",
|
||||
force_internal_users: (list[str] | None)=None,
|
||||
) -> AsyncGenerator[AuthManager, None]:
|
||||
|
||||
manager = AuthManager(
|
||||
enabled=True,
|
||||
unauth_paths=unauth_paths,
|
||||
|
||||
internal_type="htpasswd",
|
||||
internal_kwargs=_make_service_kwargs(internal_path),
|
||||
force_internal_users=(force_internal_users or []),
|
||||
|
||||
external_type=("htpasswd" if external_path else ""),
|
||||
external_kwargs=(_make_service_kwargs(external_path) if external_path else {}),
|
||||
|
||||
totp_secret_path="",
|
||||
)
|
||||
|
||||
try:
|
||||
yield manager
|
||||
finally:
|
||||
await manager.cleanup()
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.asyncio
|
||||
async def test_ok__internal(tmpdir) -> None: # type: ignore
|
||||
path = os.path.abspath(str(tmpdir.join("htpasswd")))
|
||||
|
||||
htpasswd = passlib.apache.HtpasswdFile(path, new=True)
|
||||
htpasswd.set_password("admin", "pass")
|
||||
htpasswd.save()
|
||||
|
||||
async with _get_configured_manager([], path) as manager:
|
||||
assert manager.is_auth_enabled()
|
||||
assert manager.is_auth_required(_E_AUTH)
|
||||
assert manager.is_auth_required(_E_UNAUTH)
|
||||
assert not manager.is_auth_required(_E_FREE)
|
||||
|
||||
assert manager.check("xxx") is None
|
||||
manager.logout("xxx")
|
||||
|
||||
assert (await manager.login("user", "foo")) is None
|
||||
assert (await manager.login("admin", "foo")) is None
|
||||
assert (await manager.login("user", "pass")) is None
|
||||
|
||||
token1 = await manager.login("admin", "pass")
|
||||
assert isinstance(token1, str)
|
||||
assert len(token1) == 64
|
||||
|
||||
token2 = await manager.login("admin", "pass")
|
||||
assert isinstance(token2, str)
|
||||
assert len(token2) == 64
|
||||
assert token1 != token2
|
||||
|
||||
assert manager.check(token1) == "admin"
|
||||
assert manager.check(token2) == "admin"
|
||||
assert manager.check("foobar") is None
|
||||
|
||||
manager.logout(token1)
|
||||
|
||||
assert manager.check(token1) is None
|
||||
assert manager.check(token2) is None
|
||||
assert manager.check("foobar") is None
|
||||
|
||||
token3 = await manager.login("admin", "pass")
|
||||
assert isinstance(token3, str)
|
||||
assert len(token3) == 64
|
||||
assert token1 != token3
|
||||
assert token2 != token3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ok__external(tmpdir) -> None: # type: ignore
|
||||
path1 = os.path.abspath(str(tmpdir.join("htpasswd1")))
|
||||
path2 = os.path.abspath(str(tmpdir.join("htpasswd2")))
|
||||
|
||||
htpasswd1 = passlib.apache.HtpasswdFile(path1, new=True)
|
||||
htpasswd1.set_password("admin", "pass1")
|
||||
htpasswd1.set_password("local", "foobar")
|
||||
htpasswd1.save()
|
||||
|
||||
htpasswd2 = passlib.apache.HtpasswdFile(path2, new=True)
|
||||
htpasswd2.set_password("admin", "pass2")
|
||||
htpasswd2.set_password("user", "foobar")
|
||||
htpasswd2.save()
|
||||
|
||||
async with _get_configured_manager([], path1, path2, ["admin"]) as manager:
|
||||
assert manager.is_auth_enabled()
|
||||
assert manager.is_auth_required(_E_AUTH)
|
||||
assert manager.is_auth_required(_E_UNAUTH)
|
||||
assert not manager.is_auth_required(_E_FREE)
|
||||
|
||||
assert (await manager.login("local", "foobar")) is None
|
||||
assert (await manager.login("admin", "pass2")) is None
|
||||
|
||||
token = await manager.login("admin", "pass1")
|
||||
assert token is not None
|
||||
|
||||
assert manager.check(token) == "admin"
|
||||
manager.logout(token)
|
||||
assert manager.check(token) is None
|
||||
|
||||
token = await manager.login("user", "foobar")
|
||||
assert token is not None
|
||||
|
||||
assert manager.check(token) == "user"
|
||||
manager.logout(token)
|
||||
assert manager.check(token) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ok__unauth(tmpdir) -> None: # type: ignore
|
||||
path = os.path.abspath(str(tmpdir.join("htpasswd")))
|
||||
|
||||
htpasswd = passlib.apache.HtpasswdFile(path, new=True)
|
||||
htpasswd.set_password("admin", "pass")
|
||||
htpasswd.save()
|
||||
|
||||
async with _get_configured_manager([
|
||||
"", " ",
|
||||
"foo_auth", "/foo_auth ", " /foo_auth",
|
||||
"/foo_authx", "/foo_auth/", "/foo_auth/x",
|
||||
"/bar_unauth", # Only this one is matching
|
||||
], path) as manager:
|
||||
|
||||
assert manager.is_auth_enabled()
|
||||
assert manager.is_auth_required(_E_AUTH)
|
||||
assert not manager.is_auth_required(_E_UNAUTH)
|
||||
assert not manager.is_auth_required(_E_FREE)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ok__disabled() -> None:
|
||||
try:
|
||||
manager = AuthManager(
|
||||
enabled=False,
|
||||
unauth_paths=[],
|
||||
|
||||
internal_type="foobar",
|
||||
internal_kwargs={},
|
||||
force_internal_users=[],
|
||||
|
||||
external_type="",
|
||||
external_kwargs={},
|
||||
|
||||
totp_secret_path="",
|
||||
)
|
||||
|
||||
assert not manager.is_auth_enabled()
|
||||
assert not manager.is_auth_required(_E_AUTH)
|
||||
assert not manager.is_auth_required(_E_UNAUTH)
|
||||
assert not manager.is_auth_required(_E_FREE)
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
await manager.authorize("admin", "admin")
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
await manager.login("admin", "admin")
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
manager.logout("xxx")
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
manager.check("xxx")
|
||||
finally:
|
||||
await manager.cleanup()
|
||||
@ -1,20 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
@ -1,35 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
from kvmd.keyboard.mappings import KEYMAP
|
||||
|
||||
|
||||
# =====
|
||||
def test_ok__keymap() -> None:
|
||||
assert KEYMAP["KeyA"].mcu.code == 1
|
||||
|
||||
|
||||
def test_fail__keymap() -> None:
|
||||
with pytest.raises(KeyError):
|
||||
print(KEYMAP["keya"])
|
||||
@ -1,20 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
@ -1,43 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import contextlib
|
||||
|
||||
from typing import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
from kvmd.yamlconf import make_config
|
||||
|
||||
from kvmd.plugins.auth import BaseAuthService
|
||||
from kvmd.plugins.auth import get_auth_service_class
|
||||
|
||||
|
||||
# =====
|
||||
@contextlib.asynccontextmanager
|
||||
async def get_configured_auth_service(name: str, **kwargs: Any) -> AsyncGenerator[BaseAuthService, None]:
|
||||
service_class = get_auth_service_class(name)
|
||||
config = make_config(kwargs, service_class.get_plugin_options())
|
||||
service = service_class(**config._unpack())
|
||||
try:
|
||||
yield service
|
||||
finally:
|
||||
await service.cleanup()
|
||||
@ -1,54 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import os
|
||||
|
||||
import passlib.apache
|
||||
|
||||
import pytest
|
||||
|
||||
from . import get_configured_auth_service
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.asyncio
|
||||
async def test_ok__htpasswd_service(tmpdir) -> None: # type: ignore
|
||||
path = os.path.abspath(str(tmpdir.join("htpasswd")))
|
||||
|
||||
htpasswd = passlib.apache.HtpasswdFile(path, new=True)
|
||||
htpasswd.set_password("admin", "pass")
|
||||
htpasswd.save()
|
||||
|
||||
async with get_configured_auth_service("htpasswd", file=path) as service:
|
||||
assert not (await service.authorize("user", "foo"))
|
||||
assert not (await service.authorize("admin", "foo"))
|
||||
assert not (await service.authorize("user", "pass"))
|
||||
assert (await service.authorize("admin", "pass"))
|
||||
|
||||
htpasswd.set_password("admin", "bar")
|
||||
htpasswd.set_password("user", "bar")
|
||||
htpasswd.save()
|
||||
|
||||
assert (await service.authorize("admin", "bar"))
|
||||
assert (await service.authorize("user", "bar"))
|
||||
assert not (await service.authorize("admin", "foo"))
|
||||
assert not (await service.authorize("user", "foo"))
|
||||
@ -1,79 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from typing import AsyncGenerator
|
||||
|
||||
import aiohttp.web
|
||||
import aiohttp_basicauth
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from . import get_configured_auth_service
|
||||
|
||||
|
||||
# =====
|
||||
async def _handle_auth(req: aiohttp.web.BaseRequest) -> aiohttp.web.Response:
|
||||
status = 400
|
||||
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)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(name="auth_server_port")
|
||||
async def _auth_server_port_fixture(aiohttp_server) -> AsyncGenerator[int, None]: # type: ignore
|
||||
auth = aiohttp_basicauth.BasicAuthMiddleware(
|
||||
username="server-admin",
|
||||
password="server-pass",
|
||||
force=False,
|
||||
)
|
||||
|
||||
app = aiohttp.web.Application(middlewares=[auth])
|
||||
app.router.add_post("/auth", _handle_auth)
|
||||
app.router.add_post("/auth_plus_basic", auth.required(_handle_auth))
|
||||
|
||||
server = await aiohttp_server(app)
|
||||
try:
|
||||
yield server.port
|
||||
finally:
|
||||
await server.close()
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("kwargs", [
|
||||
{},
|
||||
{"verify": False},
|
||||
{"user": "server-admin", "passwd": "server-pass"},
|
||||
])
|
||||
async def test_ok(auth_server_port: int, kwargs: dict) -> None:
|
||||
url = "http://localhost:%d/%s" % (
|
||||
auth_server_port,
|
||||
("auth_plus_basic" if kwargs.get("user") else "auth"),
|
||||
)
|
||||
async with get_configured_auth_service("http", url=url, **kwargs) as service:
|
||||
assert not (await service.authorize("user", "foobar"))
|
||||
assert not (await service.authorize("admin", "foobar"))
|
||||
assert not (await service.authorize("user", "pass"))
|
||||
assert (await service.authorize("admin", "pass"))
|
||||
@ -1,93 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
import pwd
|
||||
|
||||
from typing import AsyncGenerator
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from . import get_configured_auth_service
|
||||
|
||||
|
||||
# =====
|
||||
_UID = 1500
|
||||
_USER = "foobar"
|
||||
_PASSWD = "query"
|
||||
|
||||
|
||||
# =====
|
||||
async def _run_process(cmd: str, input: (str | None)=None) -> None: # pylint: disable=redefined-builtin
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd.split(" "),
|
||||
stdin=(asyncio.subprocess.PIPE if input is not None else None),
|
||||
preexec_fn=os.setpgrp,
|
||||
)
|
||||
await proc.communicate(input.encode() if input is not None else None)
|
||||
assert proc.returncode == 0
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(name="test_user")
|
||||
async def _test_user() -> AsyncGenerator[None, None]:
|
||||
with pytest.raises(KeyError):
|
||||
pwd.getpwnam(_USER)
|
||||
await _run_process(f"useradd -u {_UID} -s /bin/bash {_USER}")
|
||||
await _run_process("chpasswd", input=f"{_USER}:{_PASSWD}\n")
|
||||
|
||||
assert pwd.getpwnam(_USER).pw_uid == _UID
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
await _run_process(f"userdel -r {_USER}")
|
||||
with pytest.raises(KeyError):
|
||||
pwd.getpwnam(_USER)
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("kwargs", [
|
||||
{},
|
||||
{"allow_users": [_USER]},
|
||||
{"allow_uids_at": _UID},
|
||||
])
|
||||
async def test_ok(test_user, kwargs: dict) -> None: # type: ignore
|
||||
_ = test_user
|
||||
async with get_configured_auth_service("pam", **kwargs) as service:
|
||||
assert not (await service.authorize(_USER, "invalid_password"))
|
||||
assert (await service.authorize(_USER, _PASSWD))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("kwargs", [
|
||||
{"allow_users": ["root"]},
|
||||
{"deny_users": [_USER]},
|
||||
{"allow_uids_at": _UID + 1},
|
||||
])
|
||||
async def test_fail(test_user, kwargs: dict) -> None: # type: ignore
|
||||
_ = test_user
|
||||
async with get_configured_auth_service("pam", **kwargs) as service:
|
||||
assert not (await service.authorize(_USER, "invalid_password"))
|
||||
assert not (await service.authorize(_USER, _PASSWD))
|
||||
@ -1,149 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from kvmd.aiotools import AioExclusiveRegion
|
||||
from kvmd.aiotools import shield_fg
|
||||
|
||||
|
||||
# =====
|
||||
class RegionIsBusyError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.asyncio
|
||||
async def test_ok__region__access_one() -> None:
|
||||
region = AioExclusiveRegion(RegionIsBusyError)
|
||||
|
||||
async def func() -> None:
|
||||
assert not region.is_busy()
|
||||
with region:
|
||||
assert region.is_busy()
|
||||
assert not region.is_busy()
|
||||
|
||||
await func()
|
||||
|
||||
assert not region.is_busy()
|
||||
region.exit()
|
||||
assert not region.is_busy()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fail__region__access_one() -> None:
|
||||
region = AioExclusiveRegion(RegionIsBusyError)
|
||||
|
||||
async def func() -> None:
|
||||
assert not region.is_busy()
|
||||
with region:
|
||||
assert region.is_busy()
|
||||
region.enter()
|
||||
assert not region.is_busy()
|
||||
|
||||
with pytest.raises(RegionIsBusyError):
|
||||
await func()
|
||||
|
||||
assert not region.is_busy()
|
||||
region.exit()
|
||||
assert not region.is_busy()
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.asyncio
|
||||
async def test_ok__region__access_two() -> None:
|
||||
region = AioExclusiveRegion(RegionIsBusyError)
|
||||
|
||||
async def func1() -> None:
|
||||
with region:
|
||||
await asyncio.sleep(1)
|
||||
print("done func1()")
|
||||
|
||||
async def func2() -> None:
|
||||
await asyncio.sleep(2)
|
||||
print("waiking up func2()")
|
||||
with region:
|
||||
await asyncio.sleep(1)
|
||||
print("done func2()")
|
||||
|
||||
await asyncio.gather(func1(), func2())
|
||||
|
||||
assert not region.is_busy()
|
||||
region.exit()
|
||||
assert not region.is_busy()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fail__region__access_two() -> None:
|
||||
region = AioExclusiveRegion(RegionIsBusyError)
|
||||
|
||||
async def func1() -> None:
|
||||
with region:
|
||||
await asyncio.sleep(2)
|
||||
print("done func1()")
|
||||
|
||||
async def func2() -> None:
|
||||
await asyncio.sleep(1)
|
||||
with region:
|
||||
await asyncio.sleep(1)
|
||||
print("done func2()")
|
||||
|
||||
results = await asyncio.gather(func1(), func2(), return_exceptions=True)
|
||||
assert results[0] is None
|
||||
assert type(results[1]) is RegionIsBusyError # pylint: disable=unidiomatic-typecheck
|
||||
|
||||
assert not region.is_busy()
|
||||
region.exit()
|
||||
assert not region.is_busy()
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.asyncio
|
||||
async def test_ok__shield_fg() -> None:
|
||||
ops: list[str] = []
|
||||
|
||||
async def foo(op: str, delay: float) -> None: # pylint: disable=disallowed-name
|
||||
await asyncio.sleep(delay)
|
||||
ops.append(op)
|
||||
|
||||
async def bar() -> None: # pylint: disable=disallowed-name
|
||||
try:
|
||||
try:
|
||||
try:
|
||||
raise RuntimeError()
|
||||
finally:
|
||||
await shield_fg(foo("foo1", 2.0))
|
||||
ops.append("foo1-noexc")
|
||||
finally:
|
||||
await shield_fg(foo("foo2", 1.0))
|
||||
ops.append("foo2-noexc")
|
||||
finally:
|
||||
ops.append("done")
|
||||
|
||||
task = asyncio.create_task(bar())
|
||||
await asyncio.sleep(0.1)
|
||||
task.cancel()
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await task
|
||||
assert ops == ["foo1", "foo2", "foo2-noexc", "done"]
|
||||
@ -1,35 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
from kvmd.logging import get_logger
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("depth, name", [
|
||||
(0, "tests.test_logging"),
|
||||
(1, "_pytest.python"),
|
||||
(2, "pluggy._callers"),
|
||||
])
|
||||
def test_ok__get_logger(depth: int, name: str) -> None:
|
||||
assert get_logger(depth).name == name
|
||||
@ -1,42 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import pathlib
|
||||
import textwrap
|
||||
|
||||
from kvmd.yamlconf.loader import load_yaml_file
|
||||
|
||||
|
||||
# =====
|
||||
def test_load_yaml_file__bools(tmp_path: pathlib.Path) -> None: # type: ignore
|
||||
pobj = tmp_path / "test.yaml"
|
||||
pobj.write_text(textwrap.dedent("""
|
||||
a: true
|
||||
b: false
|
||||
c: yes
|
||||
d: no
|
||||
"""))
|
||||
data = load_yaml_file(str(pobj))
|
||||
assert data["a"] is True
|
||||
assert data["b"] is False
|
||||
assert data["c"] == "yes"
|
||||
assert data["d"] == "no"
|
||||
@ -1,20 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
@ -1,130 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from kvmd.validators import ValidatorError
|
||||
from kvmd.validators.auth import valid_user
|
||||
from kvmd.validators.auth import valid_users_list
|
||||
from kvmd.validators.auth import valid_passwd
|
||||
from kvmd.validators.auth import valid_auth_token
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", [
|
||||
"test-",
|
||||
"glados",
|
||||
"test",
|
||||
"_",
|
||||
"_foo_bar_",
|
||||
" aix",
|
||||
])
|
||||
def test_ok__valid_user(arg: Any) -> None:
|
||||
assert valid_user(arg) == arg.strip()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
"тест",
|
||||
"-molestia",
|
||||
"te~st",
|
||||
"-",
|
||||
"-foo_bar",
|
||||
"foo bar",
|
||||
" ",
|
||||
"",
|
||||
None,
|
||||
])
|
||||
def test_fail__valid_user(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_user(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg, retval", [
|
||||
("foo, bar, ", ["foo", "bar"]),
|
||||
("foo bar", ["foo", "bar"]),
|
||||
(["foo", "bar"], ["foo", "bar"]),
|
||||
("", []),
|
||||
(" ", []),
|
||||
(", ", []),
|
||||
(", foo, ", ["foo"]),
|
||||
([], []),
|
||||
])
|
||||
def test_ok__valid_users_list(arg: Any, retval: list) -> None:
|
||||
assert valid_users_list(arg) == retval
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [None, [None], [""], [" "], ["user,"]])
|
||||
def test_fail__valid_users_list(arg: Any) -> None: # pylint: disable=invalid-name
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_users_list(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", [
|
||||
"glados",
|
||||
"test",
|
||||
"_",
|
||||
"_foo_bar_",
|
||||
" aix",
|
||||
" ",
|
||||
"",
|
||||
" O(*#&@)FD*S)D(F ",
|
||||
])
|
||||
def test_ok__valid_passwd(arg: Any) -> None:
|
||||
assert valid_passwd(arg) == arg
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
"тест",
|
||||
"\n",
|
||||
" \n",
|
||||
"\n\n",
|
||||
"\r",
|
||||
None,
|
||||
])
|
||||
def test_fail__valid_passwd(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_passwd(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", [
|
||||
("0" * 64) + " ",
|
||||
("f" * 64) + " ",
|
||||
])
|
||||
def test_ok__valid_auth_token(arg: Any) -> None:
|
||||
assert valid_auth_token(arg) == arg.strip()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
("F" * 64),
|
||||
"0" * 63,
|
||||
"0" * 65,
|
||||
"",
|
||||
None,
|
||||
])
|
||||
def test_fail__valid_auth_token(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_auth_token(arg))
|
||||
@ -1,174 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from kvmd.validators import ValidatorError
|
||||
from kvmd.validators.basic import valid_bool
|
||||
from kvmd.validators.basic import valid_number
|
||||
from kvmd.validators.basic import valid_int_f0
|
||||
from kvmd.validators.basic import valid_int_f1
|
||||
from kvmd.validators.basic import valid_float_f0
|
||||
from kvmd.validators.basic import valid_float_f01
|
||||
from kvmd.validators.basic import valid_string_list
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg, retval", [
|
||||
("1", True),
|
||||
("true", True),
|
||||
("TRUE", True),
|
||||
("yes ", True),
|
||||
(1, True),
|
||||
(True, True),
|
||||
("0", False),
|
||||
("false", False),
|
||||
("FALSE", False),
|
||||
("no ", False),
|
||||
(0, False),
|
||||
(False, False),
|
||||
])
|
||||
def test_ok__valid_bool(arg: Any, retval: bool) -> None:
|
||||
assert valid_bool(arg) == retval
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None, -1, "x"])
|
||||
def test_fail__valid_bool(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_bool(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", ["1 ", "-1", 1, -1, 0, 100500])
|
||||
def test_ok__valid_number(arg: Any) -> None:
|
||||
assert valid_number(arg) == int(str(arg).strip())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None, "1x", 100500.0])
|
||||
def test_fail__valid_number(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_number(arg))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [-5, 0, 5, "-5 ", "0 ", "5 "])
|
||||
def test_ok__valid_number__min_max(arg: Any) -> None:
|
||||
assert valid_number(arg, -5, 5) == int(str(arg).strip())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None, -6, 6, "-6 ", "6 "])
|
||||
def test_fail__valid_number__min_max(arg: Any) -> None: # pylint: disable=invalid-name
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_number(arg, -5, 5))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", [0, 1, 5, "5 "])
|
||||
def test_ok__valid_int_f0(arg: Any) -> None:
|
||||
value = valid_int_f0(arg)
|
||||
assert type(value) is int # pylint: disable=unidiomatic-typecheck
|
||||
assert value == int(str(arg).strip())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None, -6, "-6 ", "5.0"])
|
||||
def test_fail__valid_int_f0(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_int_f0(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", [1, 5, "5 "])
|
||||
def test_ok__valid_int_f1(arg: Any) -> None:
|
||||
value = valid_int_f1(arg)
|
||||
assert type(value) is int # pylint: disable=unidiomatic-typecheck
|
||||
assert value == int(str(arg).strip())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None, -6, "-6 ", 0, "0 ", "5.0"])
|
||||
def test_fail__valid_int_f1(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_int_f1(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", [0, 1, 5, "5 ", "5.0 "])
|
||||
def test_ok__valid_float_f0(arg: Any) -> None:
|
||||
value = valid_float_f0(arg)
|
||||
assert type(value) is float # pylint: disable=unidiomatic-typecheck
|
||||
assert value == float(str(arg).strip())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None, -6, "-6"])
|
||||
def test_fail__valid_float_f0(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_float_f0(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", [0.1, 1, 5, "5 ", "5.0 "])
|
||||
def test_ok__valid_float_f01(arg: Any) -> None:
|
||||
value = valid_float_f01(arg)
|
||||
assert type(value) is float # pylint: disable=unidiomatic-typecheck
|
||||
assert value == float(str(arg).strip())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None, 0.0, "0.0", -6, "-6", 0, "0"])
|
||||
def test_fail__valid_float_f01(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_float_f01(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg, retval", [
|
||||
("a, b, c", ["a", "b", "c"]),
|
||||
("a, b,, c", ["a", "b", "c"]),
|
||||
("a b c", ["a", "b", "c"]),
|
||||
(["a", "b", "c"], ["a", "b", "c"]),
|
||||
("", []),
|
||||
(" ", []),
|
||||
(", ", []),
|
||||
(", a, ", ["a"]),
|
||||
([], []),
|
||||
])
|
||||
def test_ok__valid_string_list(arg: Any, retval: list) -> None:
|
||||
assert valid_string_list(arg) == retval
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg, retval", [
|
||||
("1, 2, 3", [1, 2, 3]),
|
||||
("1 2 3", [1, 2, 3]),
|
||||
([1, 2, 3], [1, 2, 3]),
|
||||
("", []),
|
||||
(" ", []),
|
||||
(", ", []),
|
||||
(", 1, ", [1]),
|
||||
([], []),
|
||||
])
|
||||
def test_ok__valid_string_list__subval(arg: Any, retval: list) -> None: # pylint: disable=invalid-name
|
||||
assert valid_string_list(arg, subval=int) == retval
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [None, [None]])
|
||||
def test_fail__valid_string_list(arg: Any) -> None: # pylint: disable=invalid-name
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_string_list(arg))
|
||||
@ -1,98 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from kvmd.keyboard.mappings import KEYMAP
|
||||
|
||||
from kvmd.validators import ValidatorError
|
||||
from kvmd.validators.hid import valid_hid_key
|
||||
from kvmd.validators.hid import valid_hid_mouse_move
|
||||
from kvmd.validators.hid import valid_hid_mouse_button
|
||||
from kvmd.validators.hid import valid_hid_mouse_delta
|
||||
|
||||
|
||||
# =====
|
||||
def test_ok__valid_hid_key() -> None:
|
||||
for key in KEYMAP:
|
||||
print(valid_hid_key(key))
|
||||
print(valid_hid_key(key + " "))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None, "keya"])
|
||||
def test_fail__valid_hid_key(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_hid_key(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", [-20000, "1 ", "-1", 1, -1, 0, "20000 "])
|
||||
def test_ok__valid_hid_mouse_move(arg: Any) -> None:
|
||||
assert valid_hid_mouse_move(arg) == int(str(arg).strip())
|
||||
|
||||
|
||||
def test_ok__valid_hid_mouse_move__m50000() -> None:
|
||||
assert valid_hid_mouse_move(-50000) == -32768
|
||||
|
||||
|
||||
def test_ok__valid_hid_mouse_move__p50000() -> None:
|
||||
assert valid_hid_mouse_move(50000) == 32767
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None, 1.1])
|
||||
def test_fail__valid_hid_mouse_move(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_hid_mouse_move(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", ["LEFT ", "RIGHT ", "Up ", " Down", " MiDdLe "])
|
||||
def test_ok__valid_hid_mouse_button(arg: Any) -> None:
|
||||
assert valid_hid_mouse_button(arg) == arg.strip().lower()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None])
|
||||
def test_fail__valid_hid_mouse_button(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_hid_mouse_button(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", [-100, "1 ", "-1", 1, -1, 0, "100 "])
|
||||
def test_ok__valid_hid_mouse_delta(arg: Any) -> None:
|
||||
assert valid_hid_mouse_delta(arg) == int(str(arg).strip())
|
||||
|
||||
|
||||
def test_ok__valid_hid_mouse_delta__m200() -> None:
|
||||
assert valid_hid_mouse_delta(-200) == -127
|
||||
|
||||
|
||||
def test_ok__valid_hid_mouse_delta__p200() -> None:
|
||||
assert valid_hid_mouse_delta(200) == 127
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None, 1.1])
|
||||
def test_fail__valid_hid_mouse_delta(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_hid_mouse_delta(arg))
|
||||
@ -1,132 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from kvmd.validators import ValidatorError
|
||||
from kvmd.validators.hw import valid_tty_speed
|
||||
from kvmd.validators.hw import valid_gpio_pin
|
||||
from kvmd.validators.hw import valid_gpio_pin_optional
|
||||
from kvmd.validators.hw import valid_otg_gadget
|
||||
from kvmd.validators.hw import valid_otg_id
|
||||
from kvmd.validators.hw import valid_otg_ethernet
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", ["1200 ", 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200])
|
||||
def test_ok__valid_tty_speed(arg: Any) -> None:
|
||||
value = valid_tty_speed(arg)
|
||||
assert type(value) is int # pylint: disable=unidiomatic-typecheck
|
||||
assert value == int(str(arg).strip())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None, 0, 1200.1])
|
||||
def test_fail__valid_tty_speed(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_tty_speed(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", ["0 ", 0, 1, 13])
|
||||
def test_ok__valid_gpio_pin(arg: Any) -> None:
|
||||
value = valid_gpio_pin(arg)
|
||||
assert type(value) is int # pylint: disable=unidiomatic-typecheck
|
||||
assert value == int(str(arg).strip())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None, -1, -13, 1.1])
|
||||
def test_fail__valid_gpio_pin(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_gpio_pin(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", ["0 ", -1, 0, 1, 13])
|
||||
def test_ok__valid_gpio_pin_optional(arg: Any) -> None:
|
||||
value = valid_gpio_pin_optional(arg)
|
||||
assert type(value) is int # pylint: disable=unidiomatic-typecheck
|
||||
assert value == int(str(arg).strip())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None, -2, -13, 1.1])
|
||||
def test_fail__valid_gpio_pin_optional(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_gpio_pin_optional(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", [
|
||||
"test-",
|
||||
"glados",
|
||||
"test",
|
||||
"_",
|
||||
"_foo_bar_",
|
||||
" aix",
|
||||
"a" * 255,
|
||||
])
|
||||
def test_ok__valid_otg_gadget(arg: Any) -> None:
|
||||
assert valid_otg_gadget(arg) == arg.strip()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
"тест",
|
||||
"-molestia",
|
||||
"te~st",
|
||||
"-",
|
||||
"-foo_bar",
|
||||
"foo bar",
|
||||
"a" * 256,
|
||||
" ",
|
||||
"",
|
||||
None,
|
||||
])
|
||||
def test_fail__valid_otg_gadget(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_otg_gadget(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", ["0 ", 0, 1, 13, 65535])
|
||||
def test_ok__valid_otg_id(arg: Any) -> None:
|
||||
value = valid_otg_id(arg)
|
||||
assert type(value) is int # pylint: disable=unidiomatic-typecheck
|
||||
assert value == int(str(arg).strip())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None, -1, -13, 1.1, 65534.5, 65536])
|
||||
def test_fail__valid_otg_id(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_otg_id(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", ["ECM ", "EeM ", "ncm ", " Rndis", "RNDIS5"])
|
||||
def test_ok__valid_otg_ethernet(arg: Any) -> None:
|
||||
assert valid_otg_ethernet(arg) == arg.strip().lower()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None])
|
||||
def test_fail__valid_otg_ethernet(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_otg_ethernet(arg))
|
||||
@ -1,210 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from kvmd.validators import ValidatorError
|
||||
from kvmd.validators.kvm import valid_atx_power_action
|
||||
from kvmd.validators.kvm import valid_atx_button
|
||||
from kvmd.validators.kvm import valid_msd_image_name
|
||||
from kvmd.validators.kvm import valid_info_fields
|
||||
from kvmd.validators.kvm import valid_log_seek
|
||||
from kvmd.validators.kvm import valid_stream_quality
|
||||
from kvmd.validators.kvm import valid_stream_fps
|
||||
from kvmd.validators.kvm import valid_stream_resolution
|
||||
from kvmd.validators.kvm import valid_stream_h264_bitrate
|
||||
from kvmd.validators.kvm import valid_stream_h264_gop
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", ["ON ", "OFF ", "OFF_HARD ", "RESET_HARD "])
|
||||
def test_ok__valid_atx_power_action(arg: Any) -> None:
|
||||
assert valid_atx_power_action(arg) == arg.strip().lower()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None])
|
||||
def test_fail__valid_atx_power_action(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_atx_power_action(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", ["POWER ", "POWER_LONG ", "RESET "])
|
||||
def test_ok__valid_atx_button(arg: Any) -> None:
|
||||
assert valid_atx_button(arg) == arg.strip().lower()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None])
|
||||
def test_fail__valid_atx_button(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_atx_button(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg, retval", [
|
||||
("archlinux-2018.07.01-i686.iso", "archlinux-2018.07.01-i686.iso"),
|
||||
("archlinux-2018.07.01-x86_64.iso", "archlinux-2018.07.01-x86_64.iso"),
|
||||
("dsl-4.11.rc1.iso", "dsl-4.11.rc1.iso"),
|
||||
("systemrescuecd-x86-5.3.1.iso", "systemrescuecd-x86-5.3.1.iso"),
|
||||
("ubuntu-16.04.5-desktop-i386.iso", "ubuntu-16.04.5-desktop-i386.iso"),
|
||||
(" тест(){}[ \t].iso\t", "тест(){}[ _].iso"),
|
||||
("\n" + "x" * 1000, "x" * 255),
|
||||
("test", "test"),
|
||||
("test test [test] #test$", "test test [test] #test$"),
|
||||
("test/", "test"),
|
||||
("/test", "test"),
|
||||
("foo/bar.iso", "foo/bar.iso"),
|
||||
("//foo//bar.iso", "foo/bar.iso"),
|
||||
("foo/lost-found/bar.iso", "foo/lost-found/bar.iso"),
|
||||
("/bar.iso/", "bar.iso"),
|
||||
|
||||
])
|
||||
def test_ok__valid_msd_image_name(arg: Any, retval: str) -> None:
|
||||
assert valid_msd_image_name(arg) == retval
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
".",
|
||||
"..",
|
||||
" ..",
|
||||
"../test",
|
||||
"./.",
|
||||
"../.",
|
||||
"./..",
|
||||
"../..",
|
||||
"/ ..",
|
||||
".. /",
|
||||
"/.. /",
|
||||
".test",
|
||||
"foo/../bar.iso",
|
||||
"foo/./foo.iso",
|
||||
"foo/lost+found/bar.iso",
|
||||
"../bar.iso",
|
||||
"/../bar.iso",
|
||||
"foo/.bar.iso",
|
||||
"",
|
||||
" ",
|
||||
None,
|
||||
])
|
||||
def test_fail__valid_msd_image_name(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
valid_msd_image_name(arg)
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", [" foo ", "bar", "foo, ,bar,", " ", " , ", ""])
|
||||
def test_ok__valid_info_fields(arg: Any) -> None:
|
||||
value = valid_info_fields(arg, set(["foo", "bar"]))
|
||||
assert type(value) is set # pylint: disable=unidiomatic-typecheck
|
||||
assert value == set(filter(None, map(str.strip, str(arg).split(","))))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["xxx", "yyy", "foo,xxx", None])
|
||||
def test_fail__valid_info_fields(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_info_fields(arg, set(["foo", "bar"])))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", ["0 ", 0, 1, 13])
|
||||
def test_ok__valid_log_seek(arg: Any) -> None:
|
||||
value = valid_log_seek(arg)
|
||||
assert type(value) is int # pylint: disable=unidiomatic-typecheck
|
||||
assert value == int(str(arg).strip())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None, -1, -13, 1.1])
|
||||
def test_fail__valid_log_seek(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_log_seek(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", ["1 ", 20, 100])
|
||||
def test_ok__valid_stream_quality(arg: Any) -> None:
|
||||
value = valid_stream_quality(arg)
|
||||
assert type(value) is int # pylint: disable=unidiomatic-typecheck
|
||||
assert value == int(str(arg).strip())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None, 0, 101, 1.1])
|
||||
def test_fail__valid_stream_quality(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_stream_quality(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", ["1 ", 120])
|
||||
def test_ok__valid_stream_fps(arg: Any) -> None:
|
||||
value = valid_stream_fps(arg)
|
||||
assert type(value) is int # pylint: disable=unidiomatic-typecheck
|
||||
assert value == int(str(arg).strip())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None, 121, 1.1])
|
||||
def test_fail__valid_stream_fps(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_stream_fps(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", ["1280x720 ", "1x1"])
|
||||
def test_ok__valid_stream_resolution(arg: Any) -> None:
|
||||
value = valid_stream_resolution(arg)
|
||||
assert type(value) is str # pylint: disable=unidiomatic-typecheck
|
||||
assert value == str(arg).strip()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["x", None, "0x0", "0x1", "1x0", "1280", "1280x", "1280x720x"])
|
||||
def test_fail__valid_stream_resolution(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_stream_resolution(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", ["25", " 20000 ", 5000])
|
||||
def test_ok__valid_stream_h264_bitrate(arg: Any) -> None:
|
||||
value = valid_stream_h264_bitrate(arg)
|
||||
assert type(value) is int # pylint: disable=unidiomatic-typecheck
|
||||
assert value == int(str(arg).strip())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["0", "-1", "100.0", 5000.1, None, ""])
|
||||
def test_fail__valid_stream_h264_bitrate(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_stream_h264_bitrate(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", ["1 ", 0, 60])
|
||||
def test_ok__valid_stream_h264_gop(arg: Any) -> None:
|
||||
value = valid_stream_h264_gop(arg)
|
||||
assert type(value) is int # pylint: disable=unidiomatic-typecheck
|
||||
assert value == int(str(arg).strip())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None, 61, 1.1])
|
||||
def test_fail__valid_stream_h264_gop(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_stream_h264_gop(arg))
|
||||
@ -1,210 +0,0 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from kvmd.validators import ValidatorError
|
||||
from kvmd.validators.net import valid_ip_or_host
|
||||
from kvmd.validators.net import valid_ip
|
||||
from kvmd.validators.net import valid_net
|
||||
from kvmd.validators.net import valid_rfc_host
|
||||
from kvmd.validators.net import valid_port
|
||||
from kvmd.validators.net import valid_ports_list
|
||||
from kvmd.validators.net import valid_mac
|
||||
from kvmd.validators.net import valid_ssl_ciphers
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", [
|
||||
"yandex.ru ",
|
||||
"foobar",
|
||||
"foo-bar.ru",
|
||||
"127.0.0.1",
|
||||
"8.8.8.8",
|
||||
"::",
|
||||
"::1",
|
||||
"2001:500:2f::f",
|
||||
])
|
||||
def test_ok__valid_ip_or_host(arg: Any) -> None:
|
||||
assert valid_ip_or_host(arg) == arg.strip()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
"foo_bar.ru",
|
||||
"1.1.1.",
|
||||
":",
|
||||
"",
|
||||
None,
|
||||
])
|
||||
def test_fail__valid_ip_or_host(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_ip_or_host(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", [
|
||||
"127.0.0.1 ",
|
||||
"8.8.8.8",
|
||||
"::",
|
||||
"::1",
|
||||
"2001:500:2f::f",
|
||||
])
|
||||
def test_ok__valid_ip(arg: Any) -> None:
|
||||
assert valid_ip(arg) == arg.strip()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
"ya.ru",
|
||||
"1",
|
||||
"1.1.1",
|
||||
"1.1.1.",
|
||||
":",
|
||||
"",
|
||||
None,
|
||||
])
|
||||
def test_fail__valid_ip(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_ip(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", [
|
||||
"127.0.0.0/24 ",
|
||||
"8.8.8.8/31",
|
||||
"::/16",
|
||||
"::ffff:0:0:0/96",
|
||||
"64:ff9b::/96",
|
||||
])
|
||||
def test_ok__valid_net(arg: Any) -> None:
|
||||
assert valid_net(arg) == arg.strip()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
"127.0.0.1/33",
|
||||
"127.0.0.1/0",
|
||||
"127.0.0.1/",
|
||||
"127.0.0.1",
|
||||
"8.8.8.8//31",
|
||||
"ya.ru",
|
||||
"1",
|
||||
"1.1.1",
|
||||
"1.1.1.",
|
||||
":",
|
||||
"",
|
||||
None,
|
||||
])
|
||||
def test_fail__valid_net(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_net(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", [
|
||||
"yandex.ru ",
|
||||
"foobar",
|
||||
"foo-bar.ru",
|
||||
"z0r.de",
|
||||
"11.ru",
|
||||
"127.0.0.1",
|
||||
])
|
||||
def test_ok__valid_rfc_host(arg: Any) -> None:
|
||||
assert valid_rfc_host(arg) == arg.strip()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
"foobar.ru.",
|
||||
"foo_bar.ru",
|
||||
"",
|
||||
None,
|
||||
])
|
||||
def test_fail__valid_rfc_host(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_rfc_host(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", ["0 ", 0, "22", 443, 65535])
|
||||
def test_ok__valid_port(arg: Any) -> None:
|
||||
value = valid_port(arg)
|
||||
assert type(value) is int # pylint: disable=unidiomatic-typecheck
|
||||
assert value == int(str(arg).strip())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "", None, 1.1])
|
||||
def test_fail__valid_port(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_port(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg, retval", [
|
||||
("", []),
|
||||
(",, , ", []),
|
||||
("0 ", [0]),
|
||||
("1,", [1]),
|
||||
("22,23", [22, 23]),
|
||||
("80,443,443,", [80, 443, 443]),
|
||||
(65535, [65535]),
|
||||
])
|
||||
def test_ok__valid_ports_list(arg: Any, retval: list[int]) -> None:
|
||||
assert valid_ports_list(arg) == retval
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "13,test", None, 1.1])
|
||||
def test_fail__valid_ports_list(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_ports_list(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", [
|
||||
" 00:00:00:00:00:00 ",
|
||||
" 9f:00:00:00:00:00 ",
|
||||
" FF:FF:FF:FF:FF:FF ",
|
||||
])
|
||||
def test_ok__valid_mac(arg: Any) -> None:
|
||||
assert valid_mac(arg) == arg.strip().lower()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
"00:00:00:00:00:0",
|
||||
"9x:00:00:00:00:00",
|
||||
"",
|
||||
None,
|
||||
])
|
||||
def test_fail__valid_mac(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_mac(arg))
|
||||
|
||||
|
||||
# =====
|
||||
@pytest.mark.parametrize("arg", ["ALL", " ALL:@SECLEVEL=0 "])
|
||||
def test_ok__valid_ssl_ciphers(arg: Any) -> None:
|
||||
assert valid_ssl_ciphers(arg) == str(arg).strip()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", ["test", "all", "", None])
|
||||
def test_fail__valid_ssl_ciphers(arg: Any) -> None:
|
||||
with pytest.raises(ValidatorError):
|
||||
print(valid_ssl_ciphers(arg))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user