From 5bf246603711174ce4334a54afd5e4162e76e5a0 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Mon, 3 Feb 2025 13:05:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=AA=E6=9E=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 27 - PKGBUILD | 268 -- build/Dockerfile | 82 - build/Dockerfile-backup | 38 - build/build_img.sh | 311 -- build/cargo_config | 5 - build/init.sh | 166 - build/platform/chainedbox | 3 - build/platform/cumebox2 | 3 - build/platform/docker | 3 - build/platform/e900v22c | 3 - build/platform/octopus-flanet | 3 - build/platform/onecloud | 3 - build/platform/vm | 3 - kvmd.install | 108 - kvmd/apps/otg/__init__.py | 354 -- kvmd/apps/otg/__main__.py | 24 - kvmd/apps/otg/hid/__init__.py | 32 - kvmd/apps/otg/hid/keyboard.py | 86 - kvmd/apps/otg/hid/mouse.py | 158 - kvmd/apps/otgconf/__init__.py | 190 - kvmd/apps/otgconf/__main__.py | 24 - kvmd/apps/otgmsd/__init__.py | 112 - kvmd/apps/otgmsd/__main__.py | 24 - kvmd/apps/otgnet/__init__.py | 209 - kvmd/apps/otgnet/__main__.py | 24 - kvmd/apps/otgnet/netctl.py | 140 - kvmd/plugins/atx/gpio.py | 211 - kvmd/plugins/hid/otg/__init__.py | 237 -- kvmd/plugins/hid/otg/device.py | 284 -- kvmd/plugins/hid/otg/events.py | 176 - kvmd/plugins/hid/otg/keyboard.py | 133 - kvmd/plugins/hid/otg/mouse.py | 181 - kvmd/plugins/msd/otg/__init__.py | 670 --- kvmd/plugins/msd/otg/drive.py | 94 - kvmd/plugins/msd/otg/storage.py | 284 -- kvmd/plugins/ugpio/anelpwr.py | 171 - kvmd/plugins/ugpio/cmdret.py | 86 - kvmd/plugins/ugpio/extron.py | 188 - kvmd/plugins/ugpio/ezcoo.py | 191 - kvmd/plugins/ugpio/hidrelay.py | 189 - kvmd/plugins/ugpio/hue.py | 166 - kvmd/plugins/ugpio/locator.py | 133 - kvmd/plugins/ugpio/noyito.py | 165 - kvmd/plugins/ugpio/otgconf.py | 165 - kvmd/plugins/ugpio/pway.py | 193 - kvmd/plugins/ugpio/pwm.py | 128 - kvmd/plugins/ugpio/servo.py | 86 - kvmd/plugins/ugpio/tesmart.py | 198 - kvmd/plugins/ugpio/xh_hk4401.py | 193 - testenv/Dockerfile | 115 - testenv/env.py | 26 - testenv/fakes/etc/fstab | 2 - testenv/fakes/proc/device-tree/model | 1 - testenv/fakes/proc/device-tree/serial-number | 1 - .../sys/bus/platform/drivers/dwc2/.gitignore | 0 .../sys/class/thermal/thermal_zone0/temp | 1 - .../sys/class/udc/fe980000.usb/device/driver | 1 - .../fakes/sys/class/udc/fe980000.usb/state | 1 - .../kvmd/configs/c.1/mass_storage.usb0 | 1 - .../functions/mass_storage.usb0/lun.0/cdrom | 1 - .../functions/mass_storage.usb0/lun.0/file | 0 .../mass_storage.usb0/lun.0/forced_eject | 1 - .../kvmd/functions/mass_storage.usb0/lun.0/ro | 1 - testenv/fakes/vcgencmd | 4 - testenv/js/adapter.js | 3364 --------------- testenv/js/janus.js | 3651 ----------------- testenv/linters/coverage.ini | 4 - testenv/linters/eslintrc.js | 57 - testenv/linters/flake8.ini | 10 - testenv/linters/htmlhint.json | 3 - testenv/linters/mypy.ini | 5 - testenv/linters/pylint.ini | 74 - testenv/linters/vulture-wl.py | 59 - testenv/platform | 3 - testenv/requirements.txt | 9 - testenv/run/.gitignore | 0 testenv/tests/__init__.py | 20 - testenv/tests/apps/__init__.py | 20 - testenv/tests/apps/htpasswd/__init__.py | 20 - testenv/tests/apps/htpasswd/test_main.py | 169 - testenv/tests/apps/kvmd/__init__.py | 20 - testenv/tests/apps/kvmd/test_auth.py | 223 - testenv/tests/keyboard/__init__.py | 20 - testenv/tests/keyboard/test_keymap.py | 35 - testenv/tests/plugins/__init__.py | 20 - testenv/tests/plugins/auth/__init__.py | 43 - testenv/tests/plugins/auth/test_htpasswd.py | 54 - testenv/tests/plugins/auth/test_http.py | 79 - testenv/tests/plugins/auth/test_pam.py | 93 - testenv/tests/test_aiotools.py | 149 - testenv/tests/test_logging.py | 35 - testenv/tests/test_yamlconf.py | 42 - testenv/tests/validators/__init__.py | 20 - testenv/tests/validators/test_auth.py | 130 - testenv/tests/validators/test_basic.py | 174 - testenv/tests/validators/test_hid.py | 98 - testenv/tests/validators/test_hw.py | 132 - testenv/tests/validators/test_kvm.py | 210 - testenv/tests/validators/test_net.py | 210 - testenv/tests/validators/test_os.py | 159 - testenv/tests/validators/test_ugpio.py | 128 - testenv/tests/yamlconf/test_merger.py | 164 - testenv/tox.ini | 65 - testenv/v2-hdmi-rpi4.override.yaml | 171 - testenv/v2-hdmiusb-rpi4.override.yaml | 78 - testenv/web.css | 0 ustreamer-win/ustreamer-win.py | 2 - 108 files changed, 17103 deletions(-) delete mode 100644 .dockerignore delete mode 100644 PKGBUILD delete mode 100644 build/Dockerfile delete mode 100644 build/Dockerfile-backup delete mode 100644 build/build_img.sh delete mode 100644 build/cargo_config delete mode 100755 build/init.sh delete mode 100644 build/platform/chainedbox delete mode 100644 build/platform/cumebox2 delete mode 100644 build/platform/docker delete mode 100644 build/platform/e900v22c delete mode 100644 build/platform/octopus-flanet delete mode 100644 build/platform/onecloud delete mode 100644 build/platform/vm delete mode 100644 kvmd.install delete mode 100644 kvmd/apps/otg/__init__.py delete mode 100644 kvmd/apps/otg/__main__.py delete mode 100644 kvmd/apps/otg/hid/__init__.py delete mode 100644 kvmd/apps/otg/hid/keyboard.py delete mode 100644 kvmd/apps/otg/hid/mouse.py delete mode 100644 kvmd/apps/otgconf/__init__.py delete mode 100644 kvmd/apps/otgconf/__main__.py delete mode 100644 kvmd/apps/otgmsd/__init__.py delete mode 100644 kvmd/apps/otgmsd/__main__.py delete mode 100644 kvmd/apps/otgnet/__init__.py delete mode 100644 kvmd/apps/otgnet/__main__.py delete mode 100644 kvmd/apps/otgnet/netctl.py delete mode 100644 kvmd/plugins/atx/gpio.py delete mode 100644 kvmd/plugins/hid/otg/__init__.py delete mode 100644 kvmd/plugins/hid/otg/device.py delete mode 100644 kvmd/plugins/hid/otg/events.py delete mode 100644 kvmd/plugins/hid/otg/keyboard.py delete mode 100644 kvmd/plugins/hid/otg/mouse.py delete mode 100644 kvmd/plugins/msd/otg/__init__.py delete mode 100644 kvmd/plugins/msd/otg/drive.py delete mode 100644 kvmd/plugins/msd/otg/storage.py delete mode 100644 kvmd/plugins/ugpio/anelpwr.py delete mode 100644 kvmd/plugins/ugpio/cmdret.py delete mode 100644 kvmd/plugins/ugpio/extron.py delete mode 100644 kvmd/plugins/ugpio/ezcoo.py delete mode 100644 kvmd/plugins/ugpio/hidrelay.py delete mode 100644 kvmd/plugins/ugpio/hue.py delete mode 100644 kvmd/plugins/ugpio/locator.py delete mode 100644 kvmd/plugins/ugpio/noyito.py delete mode 100644 kvmd/plugins/ugpio/otgconf.py delete mode 100644 kvmd/plugins/ugpio/pway.py delete mode 100644 kvmd/plugins/ugpio/pwm.py delete mode 100644 kvmd/plugins/ugpio/servo.py delete mode 100644 kvmd/plugins/ugpio/tesmart.py delete mode 100644 kvmd/plugins/ugpio/xh_hk4401.py delete mode 100644 testenv/Dockerfile delete mode 100644 testenv/env.py delete mode 100644 testenv/fakes/etc/fstab delete mode 100644 testenv/fakes/proc/device-tree/model delete mode 100644 testenv/fakes/proc/device-tree/serial-number delete mode 100644 testenv/fakes/sys/bus/platform/drivers/dwc2/.gitignore delete mode 100644 testenv/fakes/sys/class/thermal/thermal_zone0/temp delete mode 120000 testenv/fakes/sys/class/udc/fe980000.usb/device/driver delete mode 100644 testenv/fakes/sys/class/udc/fe980000.usb/state delete mode 120000 testenv/fakes/sys/kernel/config/usb_gadget/kvmd/configs/c.1/mass_storage.usb0 delete mode 100644 testenv/fakes/sys/kernel/config/usb_gadget/kvmd/functions/mass_storage.usb0/lun.0/cdrom delete mode 100644 testenv/fakes/sys/kernel/config/usb_gadget/kvmd/functions/mass_storage.usb0/lun.0/file delete mode 120000 testenv/fakes/sys/kernel/config/usb_gadget/kvmd/functions/mass_storage.usb0/lun.0/forced_eject delete mode 100644 testenv/fakes/sys/kernel/config/usb_gadget/kvmd/functions/mass_storage.usb0/lun.0/ro delete mode 100755 testenv/fakes/vcgencmd delete mode 100644 testenv/js/adapter.js delete mode 100644 testenv/js/janus.js delete mode 100644 testenv/linters/coverage.ini delete mode 100644 testenv/linters/eslintrc.js delete mode 100644 testenv/linters/flake8.ini delete mode 100644 testenv/linters/htmlhint.json delete mode 100644 testenv/linters/mypy.ini delete mode 100644 testenv/linters/pylint.ini delete mode 100644 testenv/linters/vulture-wl.py delete mode 100644 testenv/platform delete mode 100644 testenv/requirements.txt delete mode 100644 testenv/run/.gitignore delete mode 100644 testenv/tests/__init__.py delete mode 100644 testenv/tests/apps/__init__.py delete mode 100644 testenv/tests/apps/htpasswd/__init__.py delete mode 100644 testenv/tests/apps/htpasswd/test_main.py delete mode 100644 testenv/tests/apps/kvmd/__init__.py delete mode 100644 testenv/tests/apps/kvmd/test_auth.py delete mode 100644 testenv/tests/keyboard/__init__.py delete mode 100644 testenv/tests/keyboard/test_keymap.py delete mode 100644 testenv/tests/plugins/__init__.py delete mode 100644 testenv/tests/plugins/auth/__init__.py delete mode 100644 testenv/tests/plugins/auth/test_htpasswd.py delete mode 100644 testenv/tests/plugins/auth/test_http.py delete mode 100644 testenv/tests/plugins/auth/test_pam.py delete mode 100644 testenv/tests/test_aiotools.py delete mode 100644 testenv/tests/test_logging.py delete mode 100644 testenv/tests/test_yamlconf.py delete mode 100644 testenv/tests/validators/__init__.py delete mode 100644 testenv/tests/validators/test_auth.py delete mode 100644 testenv/tests/validators/test_basic.py delete mode 100644 testenv/tests/validators/test_hid.py delete mode 100644 testenv/tests/validators/test_hw.py delete mode 100644 testenv/tests/validators/test_kvm.py delete mode 100644 testenv/tests/validators/test_net.py delete mode 100644 testenv/tests/validators/test_os.py delete mode 100644 testenv/tests/validators/test_ugpio.py delete mode 100644 testenv/tests/yamlconf/test_merger.py delete mode 100644 testenv/tox.ini delete mode 100644 testenv/v2-hdmi-rpi4.override.yaml delete mode 100644 testenv/v2-hdmiusb-rpi4.override.yaml delete mode 100644 testenv/web.css diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index b114a631..00000000 --- a/.dockerignore +++ /dev/null @@ -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 \ No newline at end of file diff --git a/PKGBUILD b/PKGBUILD deleted file mode 100644 index 960ed365..00000000 --- a/PKGBUILD +++ /dev/null @@ -1,268 +0,0 @@ -# Contributor: Maxim Devaev -# Author: Maxim Devaev - - -_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 diff --git a/build/Dockerfile b/build/Dockerfile deleted file mode 100644 index 850c8eee..00000000 --- a/build/Dockerfile +++ /dev/null @@ -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 \ No newline at end of file diff --git a/build/Dockerfile-backup b/build/Dockerfile-backup deleted file mode 100644 index d87119a0..00000000 --- a/build/Dockerfile-backup +++ /dev/null @@ -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"] \ No newline at end of file diff --git a/build/build_img.sh b/build/build_img.sh deleted file mode 100644 index da757be6..00000000 --- a/build/build_img.sh +++ /dev/null @@ -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 \ No newline at end of file diff --git a/build/cargo_config b/build/cargo_config deleted file mode 100644 index 7e96e143..00000000 --- a/build/cargo_config +++ /dev/null @@ -1,5 +0,0 @@ -[source.crates-io] -replace-with = 'ustc' - -[source.ustc] -registry = "sparse+https://mirrors.ustc.edu.cn/crates.io-index/" \ No newline at end of file diff --git a/build/init.sh b/build/init.sh deleted file mode 100755 index 5431d1ce..00000000 --- a/build/init.sh +++ /dev/null @@ -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 \ No newline at end of file diff --git a/build/platform/chainedbox b/build/platform/chainedbox deleted file mode 100644 index e34cae32..00000000 --- a/build/platform/chainedbox +++ /dev/null @@ -1,3 +0,0 @@ -PIKVM_MODEL=v2_model -PIKVM_VIDEO=usb_video -PIKVM_BOARD=chainedbox \ No newline at end of file diff --git a/build/platform/cumebox2 b/build/platform/cumebox2 deleted file mode 100644 index 2dd3ad3f..00000000 --- a/build/platform/cumebox2 +++ /dev/null @@ -1,3 +0,0 @@ -PIKVM_MODEL=v2_model -PIKVM_VIDEO=usb_video -PIKVM_BOARD=cumebox2 \ No newline at end of file diff --git a/build/platform/docker b/build/platform/docker deleted file mode 100644 index 0f00370c..00000000 --- a/build/platform/docker +++ /dev/null @@ -1,3 +0,0 @@ -PIKVM_MODEL=docker_model -PIKVM_VIDEO=docker_video -PIKVM_BOARD=docker_board diff --git a/build/platform/e900v22c b/build/platform/e900v22c deleted file mode 100644 index cda3c034..00000000 --- a/build/platform/e900v22c +++ /dev/null @@ -1,3 +0,0 @@ -PIKVM_MODEL=v2_model -PIKVM_VIDEO=usb_video -PIKVM_BOARD=e900v22c \ No newline at end of file diff --git a/build/platform/octopus-flanet b/build/platform/octopus-flanet deleted file mode 100644 index 8166d87d..00000000 --- a/build/platform/octopus-flanet +++ /dev/null @@ -1,3 +0,0 @@ -PIKVM_MODEL=v2_model -PIKVM_VIDEO=usb_video -PIKVM_BOARD=octopus-flanet \ No newline at end of file diff --git a/build/platform/onecloud b/build/platform/onecloud deleted file mode 100644 index 02e1ab83..00000000 --- a/build/platform/onecloud +++ /dev/null @@ -1,3 +0,0 @@ -PIKVM_MODEL=v2_model -PIKVM_VIDEO=usb_video -PIKVM_BOARD=onecloud diff --git a/build/platform/vm b/build/platform/vm deleted file mode 100644 index 22efc697..00000000 --- a/build/platform/vm +++ /dev/null @@ -1,3 +0,0 @@ -PIKVM_MODEL=v2_model -PIKVM_VIDEO=usb_video -PIKVM_BOARD=vm \ No newline at end of file diff --git a/kvmd.install b/kvmd.install deleted file mode 100644 index 469fba8c..00000000 --- a/kvmd.install +++ /dev/null @@ -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 '/\/ s/^#*/#/' /etc/pam.d/system-login - [ ! -f /etc/pam.d/system-auth ] || sed -i -e '/\/ 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 -} diff --git a/kvmd/apps/otg/__init__.py b/kvmd/apps/otg/__init__.py deleted file mode 100644 index 9e212cf6..00000000 --- a/kvmd/apps/otg/__init__.py +++ /dev/null @@ -1,354 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import os -import re -import 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)) diff --git a/kvmd/apps/otg/__main__.py b/kvmd/apps/otg/__main__.py deleted file mode 100644 index 4827fc49..00000000 --- a/kvmd/apps/otg/__main__.py +++ /dev/null @@ -1,24 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -from . import main -main() diff --git a/kvmd/apps/otg/hid/__init__.py b/kvmd/apps/otg/hid/__init__.py deleted file mode 100644 index 2be5d2d6..00000000 --- a/kvmd/apps/otg/hid/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import dataclasses - - -# ===== -@dataclasses.dataclass(frozen=True) -class Hid: - protocol: int - subclass: int - report_length: int - report_descriptor: bytes diff --git a/kvmd/apps/otg/hid/keyboard.py b/kvmd/apps/otg/hid/keyboard.py deleted file mode 100644 index e3232afa..00000000 --- a/kvmd/apps/otg/hid/keyboard.py +++ /dev/null @@ -1,86 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -from . import 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 - ]), - ) diff --git a/kvmd/apps/otg/hid/mouse.py b/kvmd/apps/otg/hid/mouse.py deleted file mode 100644 index 2fcb578c..00000000 --- a/kvmd/apps/otg/hid/mouse.py +++ /dev/null @@ -1,158 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -from . import 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 - ]), - ) diff --git a/kvmd/apps/otgconf/__init__.py b/kvmd/apps/otgconf/__init__.py deleted file mode 100644 index b7aa2277..00000000 --- a/kvmd/apps/otgconf/__init__.py +++ /dev/null @@ -1,190 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import os -import 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="", help="Enable function(s)") - parser.add_argument("-d", "--disable-function", nargs="+", metavar="", 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() diff --git a/kvmd/apps/otgconf/__main__.py b/kvmd/apps/otgconf/__main__.py deleted file mode 100644 index 4827fc49..00000000 --- a/kvmd/apps/otgconf/__main__.py +++ /dev/null @@ -1,24 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -from . import main -main() diff --git a/kvmd/apps/otgmsd/__init__.py b/kvmd/apps/otgmsd/__init__.py deleted file mode 100644 index cd8f7718..00000000 --- a/kvmd/apps/otgmsd/__init__.py +++ /dev/null @@ -1,112 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import os -import 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="", 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="", 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 "")) - print("CD-ROM flag:", ("yes" if int(get_param("cdrom")) else "no")) - print("RW flag: ", ("no" if int(get_param("ro")) else "yes")) diff --git a/kvmd/apps/otgmsd/__main__.py b/kvmd/apps/otgmsd/__main__.py deleted file mode 100644 index 4827fc49..00000000 --- a/kvmd/apps/otgmsd/__main__.py +++ /dev/null @@ -1,24 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -from . import main -main() diff --git a/kvmd/apps/otgnet/__init__.py b/kvmd/apps/otgnet/__init__.py deleted file mode 100644 index 35c0bc45..00000000 --- a/kvmd/apps/otgnet/__init__.py +++ /dev/null @@ -1,209 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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() diff --git a/kvmd/apps/otgnet/__main__.py b/kvmd/apps/otgnet/__main__.py deleted file mode 100644 index 4827fc49..00000000 --- a/kvmd/apps/otgnet/__main__.py +++ /dev/null @@ -1,24 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -from . import main -main() diff --git a/kvmd/apps/otgnet/netctl.py b/kvmd/apps/otgnet/netctl.py deleted file mode 100644 index 13de1f00..00000000 --- a/kvmd/apps/otgnet/netctl.py +++ /dev/null @@ -1,140 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -# ===== -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) - ] diff --git a/kvmd/plugins/atx/gpio.py b/kvmd/plugins/atx/gpio.py deleted file mode 100644 index 578d2717..00000000 --- a/kvmd/plugins/atx/gpio.py +++ /dev/null @@ -1,211 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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) diff --git a/kvmd/plugins/hid/otg/__init__.py b/kvmd/plugins/hid/otg/__init__.py deleted file mode 100644 index 25424257..00000000 --- a/kvmd/plugins/hid/otg/__init__.py +++ /dev/null @@ -1,237 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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" diff --git a/kvmd/plugins/hid/otg/device.py b/kvmd/plugins/hid/otg/device.py deleted file mode 100644 index a3bc2739..00000000 --- a/kvmd/plugins/hid/otg/device.py +++ /dev/null @@ -1,284 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import os -import 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 diff --git a/kvmd/plugins/hid/otg/events.py b/kvmd/plugins/hid/otg/events.py deleted file mode 100644 index ae2ba67d..00000000 --- a/kvmd/plugins/hid/otg/events.py +++ /dev/null @@ -1,176 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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((" # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -from 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 diff --git a/kvmd/plugins/hid/otg/mouse.py b/kvmd/plugins/hid/otg/mouse.py deleted file mode 100644 index 7d3bd2e6..00000000 --- a/kvmd/plugins/hid/otg/mouse.py +++ /dev/null @@ -1,181 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -from 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 diff --git a/kvmd/plugins/msd/otg/__init__.py b/kvmd/plugins/msd/otg/__init__.py deleted file mode 100644 index a1d7f8fe..00000000 --- a/kvmd/plugins/msd/otg/__init__.py +++ /dev/null @@ -1,670 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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) diff --git a/kvmd/plugins/msd/otg/drive.py b/kvmd/plugins/msd/otg/drive.py deleted file mode 100644 index 9c2a158e..00000000 --- a/kvmd/plugins/msd/otg/drive.py +++ /dev/null @@ -1,94 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import os -import 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 diff --git a/kvmd/plugins/msd/otg/storage.py b/kvmd/plugins/msd/otg/storage.py deleted file mode 100644 index 046f10fa..00000000 --- a/kvmd/plugins/msd/otg/storage.py +++ /dev/null @@ -1,284 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import os -import 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") diff --git a/kvmd/plugins/ugpio/anelpwr.py b/kvmd/plugins/ugpio/anelpwr.py deleted file mode 100644 index af83f0a2..00000000 --- a/kvmd/plugins/ugpio/anelpwr.py +++ /dev/null @@ -1,171 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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__ diff --git a/kvmd/plugins/ugpio/cmdret.py b/kvmd/plugins/ugpio/cmdret.py deleted file mode 100644 index 7080a390..00000000 --- a/kvmd/plugins/ugpio/cmdret.py +++ /dev/null @@ -1,86 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -from 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__ diff --git a/kvmd/plugins/ugpio/extron.py b/kvmd/plugins/ugpio/extron.py deleted file mode 100644 index 81a66c92..00000000 --- a/kvmd/plugins/ugpio/extron.py +++ /dev/null @@ -1,188 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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__ diff --git a/kvmd/plugins/ugpio/ezcoo.py b/kvmd/plugins/ugpio/ezcoo.py deleted file mode 100644 index d5bd8ef8..00000000 --- a/kvmd/plugins/ugpio/ezcoo.py +++ /dev/null @@ -1,191 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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__ diff --git a/kvmd/plugins/ugpio/hidrelay.py b/kvmd/plugins/ugpio/hidrelay.py deleted file mode 100644 index 17f41e27..00000000 --- a/kvmd/plugins/ugpio/hidrelay.py +++ /dev/null @@ -1,189 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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__ diff --git a/kvmd/plugins/ugpio/hue.py b/kvmd/plugins/ugpio/hue.py deleted file mode 100644 index 9ed9e206..00000000 --- a/kvmd/plugins/ugpio/hue.py +++ /dev/null @@ -1,166 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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__ diff --git a/kvmd/plugins/ugpio/locator.py b/kvmd/plugins/ugpio/locator.py deleted file mode 100644 index d5cba719..00000000 --- a/kvmd/plugins/ugpio/locator.py +++ /dev/null @@ -1,133 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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__ diff --git a/kvmd/plugins/ugpio/noyito.py b/kvmd/plugins/ugpio/noyito.py deleted file mode 100644 index 7363e2d4..00000000 --- a/kvmd/plugins/ugpio/noyito.py +++ /dev/null @@ -1,165 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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__ diff --git a/kvmd/plugins/ugpio/otgconf.py b/kvmd/plugins/ugpio/otgconf.py deleted file mode 100644 index 4c85b0e8..00000000 --- a/kvmd/plugins/ugpio/otgconf.py +++ /dev/null @@ -1,165 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import os -import 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__ diff --git a/kvmd/plugins/ugpio/pway.py b/kvmd/plugins/ugpio/pway.py deleted file mode 100644 index 140cf02a..00000000 --- a/kvmd/plugins/ugpio/pway.py +++ /dev/null @@ -1,193 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# Modified by SppokHCK September 2021 # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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__ diff --git a/kvmd/plugins/ugpio/pwm.py b/kvmd/plugins/ugpio/pwm.py deleted file mode 100644 index f202836e..00000000 --- a/kvmd/plugins/ugpio/pwm.py +++ /dev/null @@ -1,128 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# Shantur Rathore # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -from 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__ diff --git a/kvmd/plugins/ugpio/servo.py b/kvmd/plugins/ugpio/servo.py deleted file mode 100644 index 55ad7265..00000000 --- a/kvmd/plugins/ugpio/servo.py +++ /dev/null @@ -1,86 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# Shantur Rathore # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -from ... import 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__ diff --git a/kvmd/plugins/ugpio/tesmart.py b/kvmd/plugins/ugpio/tesmart.py deleted file mode 100644 index bb1d39e1..00000000 --- a/kvmd/plugins/ugpio/tesmart.py +++ /dev/null @@ -1,198 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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__ diff --git a/kvmd/plugins/ugpio/xh_hk4401.py b/kvmd/plugins/ugpio/xh_hk4401.py deleted file mode 100644 index d7a47679..00000000 --- a/kvmd/plugins/ugpio/xh_hk4401.py +++ /dev/null @@ -1,193 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# 2021-2021 Sebastian Goscik # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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__ diff --git a/testenv/Dockerfile b/testenv/Dockerfile deleted file mode 100644 index 95b62a86..00000000 --- a/testenv/Dockerfile +++ /dev/null @@ -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 diff --git a/testenv/env.py b/testenv/env.py deleted file mode 100644 index b3cdf34f..00000000 --- a/testenv/env.py +++ /dev/null @@ -1,26 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -# ===== -ETC_PREFIX = "/fake_etc" -SYSFS_PREFIX = "/fake_sysfs" -PROCFS_PREFIX = "/fake_procfs" diff --git a/testenv/fakes/etc/fstab b/testenv/fakes/etc/fstab deleted file mode 100644 index 27a0f9f4..00000000 --- a/testenv/fakes/etc/fstab +++ /dev/null @@ -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 diff --git a/testenv/fakes/proc/device-tree/model b/testenv/fakes/proc/device-tree/model deleted file mode 100644 index 3afd3566..00000000 --- a/testenv/fakes/proc/device-tree/model +++ /dev/null @@ -1 +0,0 @@ -Virtual Raspberry Pi diff --git a/testenv/fakes/proc/device-tree/serial-number b/testenv/fakes/proc/device-tree/serial-number deleted file mode 100644 index baa82f2a..00000000 --- a/testenv/fakes/proc/device-tree/serial-number +++ /dev/null @@ -1 +0,0 @@ -0000000000000000 diff --git a/testenv/fakes/sys/bus/platform/drivers/dwc2/.gitignore b/testenv/fakes/sys/bus/platform/drivers/dwc2/.gitignore deleted file mode 100644 index e69de29b..00000000 diff --git a/testenv/fakes/sys/class/thermal/thermal_zone0/temp b/testenv/fakes/sys/class/thermal/thermal_zone0/temp deleted file mode 100644 index 9db0c8a7..00000000 --- a/testenv/fakes/sys/class/thermal/thermal_zone0/temp +++ /dev/null @@ -1 +0,0 @@ -36511 diff --git a/testenv/fakes/sys/class/udc/fe980000.usb/device/driver b/testenv/fakes/sys/class/udc/fe980000.usb/device/driver deleted file mode 120000 index cd14daa3..00000000 --- a/testenv/fakes/sys/class/udc/fe980000.usb/device/driver +++ /dev/null @@ -1 +0,0 @@ -../../../../bus/platform/drivers/dwc2 \ No newline at end of file diff --git a/testenv/fakes/sys/class/udc/fe980000.usb/state b/testenv/fakes/sys/class/udc/fe980000.usb/state deleted file mode 100644 index 6ad6460c..00000000 --- a/testenv/fakes/sys/class/udc/fe980000.usb/state +++ /dev/null @@ -1 +0,0 @@ -configured diff --git a/testenv/fakes/sys/kernel/config/usb_gadget/kvmd/configs/c.1/mass_storage.usb0 b/testenv/fakes/sys/kernel/config/usb_gadget/kvmd/configs/c.1/mass_storage.usb0 deleted file mode 120000 index b7637404..00000000 --- a/testenv/fakes/sys/kernel/config/usb_gadget/kvmd/configs/c.1/mass_storage.usb0 +++ /dev/null @@ -1 +0,0 @@ -../../functions/mass_storage.usb0 \ No newline at end of file diff --git a/testenv/fakes/sys/kernel/config/usb_gadget/kvmd/functions/mass_storage.usb0/lun.0/cdrom b/testenv/fakes/sys/kernel/config/usb_gadget/kvmd/functions/mass_storage.usb0/lun.0/cdrom deleted file mode 100644 index d00491fd..00000000 --- a/testenv/fakes/sys/kernel/config/usb_gadget/kvmd/functions/mass_storage.usb0/lun.0/cdrom +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/testenv/fakes/sys/kernel/config/usb_gadget/kvmd/functions/mass_storage.usb0/lun.0/file b/testenv/fakes/sys/kernel/config/usb_gadget/kvmd/functions/mass_storage.usb0/lun.0/file deleted file mode 100644 index e69de29b..00000000 diff --git a/testenv/fakes/sys/kernel/config/usb_gadget/kvmd/functions/mass_storage.usb0/lun.0/forced_eject b/testenv/fakes/sys/kernel/config/usb_gadget/kvmd/functions/mass_storage.usb0/lun.0/forced_eject deleted file mode 120000 index 1a010b1c..00000000 --- a/testenv/fakes/sys/kernel/config/usb_gadget/kvmd/functions/mass_storage.usb0/lun.0/forced_eject +++ /dev/null @@ -1 +0,0 @@ -file \ No newline at end of file diff --git a/testenv/fakes/sys/kernel/config/usb_gadget/kvmd/functions/mass_storage.usb0/lun.0/ro b/testenv/fakes/sys/kernel/config/usb_gadget/kvmd/functions/mass_storage.usb0/lun.0/ro deleted file mode 100644 index d00491fd..00000000 --- a/testenv/fakes/sys/kernel/config/usb_gadget/kvmd/functions/mass_storage.usb0/lun.0/ro +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/testenv/fakes/vcgencmd b/testenv/fakes/vcgencmd deleted file mode 100755 index 3985bc1f..00000000 --- a/testenv/fakes/vcgencmd +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -case $1 in - get_throttled) echo "throttled=0x0";; -esac diff --git a/testenv/js/adapter.js b/testenv/js/adapter.js deleted file mode 100644 index 51d0f276..00000000 --- a/testenv/js/adapter.js +++ /dev/null @@ -1,3364 +0,0 @@ -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.adapter = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i 0 && arguments[0] !== undefined ? arguments[0] : {}, - window = _ref.window; - var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { - shimChrome: true, - shimFirefox: true, - shimSafari: true - }; - // Utils. - var logging = utils.log; - var browserDetails = utils.detectBrowser(window); - var adapter = { - browserDetails: browserDetails, - commonShim: commonShim, - extractVersion: utils.extractVersion, - disableLog: utils.disableLog, - disableWarnings: utils.disableWarnings, - // Expose sdp as a convenience. For production apps include directly. - sdp: sdp - }; - - // Shim browser if found. - switch (browserDetails.browser) { - case 'chrome': - if (!chromeShim || !chromeShim.shimPeerConnection || !options.shimChrome) { - logging('Chrome shim is not included in this adapter release.'); - return adapter; - } - if (browserDetails.version === null) { - logging('Chrome shim can not determine version, not shimming.'); - return adapter; - } - logging('adapter.js shimming chrome.'); - // Export to the adapter global object visible in the browser. - adapter.browserShim = chromeShim; - - // Must be called before shimPeerConnection. - commonShim.shimAddIceCandidateNullOrEmpty(window, browserDetails); - commonShim.shimParameterlessSetLocalDescription(window, browserDetails); - chromeShim.shimGetUserMedia(window, browserDetails); - chromeShim.shimMediaStream(window, browserDetails); - chromeShim.shimPeerConnection(window, browserDetails); - chromeShim.shimOnTrack(window, browserDetails); - chromeShim.shimAddTrackRemoveTrack(window, browserDetails); - chromeShim.shimGetSendersWithDtmf(window, browserDetails); - chromeShim.shimGetStats(window, browserDetails); - chromeShim.shimSenderReceiverGetStats(window, browserDetails); - chromeShim.fixNegotiationNeeded(window, browserDetails); - commonShim.shimRTCIceCandidate(window, browserDetails); - commonShim.shimRTCIceCandidateRelayProtocol(window, browserDetails); - commonShim.shimConnectionState(window, browserDetails); - commonShim.shimMaxMessageSize(window, browserDetails); - commonShim.shimSendThrowTypeError(window, browserDetails); - commonShim.removeExtmapAllowMixed(window, browserDetails); - break; - case 'firefox': - if (!firefoxShim || !firefoxShim.shimPeerConnection || !options.shimFirefox) { - logging('Firefox shim is not included in this adapter release.'); - return adapter; - } - logging('adapter.js shimming firefox.'); - // Export to the adapter global object visible in the browser. - adapter.browserShim = firefoxShim; - - // Must be called before shimPeerConnection. - commonShim.shimAddIceCandidateNullOrEmpty(window, browserDetails); - commonShim.shimParameterlessSetLocalDescription(window, browserDetails); - firefoxShim.shimGetUserMedia(window, browserDetails); - firefoxShim.shimPeerConnection(window, browserDetails); - firefoxShim.shimOnTrack(window, browserDetails); - firefoxShim.shimRemoveStream(window, browserDetails); - firefoxShim.shimSenderGetStats(window, browserDetails); - firefoxShim.shimReceiverGetStats(window, browserDetails); - firefoxShim.shimRTCDataChannel(window, browserDetails); - firefoxShim.shimAddTransceiver(window, browserDetails); - firefoxShim.shimGetParameters(window, browserDetails); - firefoxShim.shimCreateOffer(window, browserDetails); - firefoxShim.shimCreateAnswer(window, browserDetails); - commonShim.shimRTCIceCandidate(window, browserDetails); - commonShim.shimConnectionState(window, browserDetails); - commonShim.shimMaxMessageSize(window, browserDetails); - commonShim.shimSendThrowTypeError(window, browserDetails); - break; - case 'safari': - if (!safariShim || !options.shimSafari) { - logging('Safari shim is not included in this adapter release.'); - return adapter; - } - logging('adapter.js shimming safari.'); - // Export to the adapter global object visible in the browser. - adapter.browserShim = safariShim; - - // Must be called before shimCallbackAPI. - commonShim.shimAddIceCandidateNullOrEmpty(window, browserDetails); - commonShim.shimParameterlessSetLocalDescription(window, browserDetails); - safariShim.shimRTCIceServerUrls(window, browserDetails); - safariShim.shimCreateOfferLegacy(window, browserDetails); - safariShim.shimCallbacksAPI(window, browserDetails); - safariShim.shimLocalStreamsAPI(window, browserDetails); - safariShim.shimRemoteStreamsAPI(window, browserDetails); - safariShim.shimTrackEventTransceiver(window, browserDetails); - safariShim.shimGetUserMedia(window, browserDetails); - safariShim.shimAudioContext(window, browserDetails); - commonShim.shimRTCIceCandidate(window, browserDetails); - commonShim.shimRTCIceCandidateRelayProtocol(window, browserDetails); - commonShim.shimMaxMessageSize(window, browserDetails); - commonShim.shimSendThrowTypeError(window, browserDetails); - commonShim.removeExtmapAllowMixed(window, browserDetails); - break; - default: - logging('Unsupported browser!'); - break; - } - return adapter; -} - -},{"./chrome/chrome_shim":3,"./common_shim":6,"./firefox/firefox_shim":7,"./safari/safari_shim":10,"./utils":11,"sdp":12}],3:[function(require,module,exports){ -/* - * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. - */ -/* eslint-env node */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.fixNegotiationNeeded = fixNegotiationNeeded; -exports.shimAddTrackRemoveTrack = shimAddTrackRemoveTrack; -exports.shimAddTrackRemoveTrackWithNative = shimAddTrackRemoveTrackWithNative; -Object.defineProperty(exports, "shimGetDisplayMedia", { - enumerable: true, - get: function get() { - return _getdisplaymedia.shimGetDisplayMedia; - } -}); -exports.shimGetSendersWithDtmf = shimGetSendersWithDtmf; -exports.shimGetStats = shimGetStats; -Object.defineProperty(exports, "shimGetUserMedia", { - enumerable: true, - get: function get() { - return _getusermedia.shimGetUserMedia; - } -}); -exports.shimMediaStream = shimMediaStream; -exports.shimOnTrack = shimOnTrack; -exports.shimPeerConnection = shimPeerConnection; -exports.shimSenderReceiverGetStats = shimSenderReceiverGetStats; -var utils = _interopRequireWildcard(require("../utils.js")); -var _getusermedia = require("./getusermedia"); -var _getdisplaymedia = require("./getdisplaymedia"); -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } -function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } -function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } -function shimMediaStream(window) { - window.MediaStream = window.MediaStream || window.webkitMediaStream; -} -function shimOnTrack(window) { - if (_typeof(window) === 'object' && window.RTCPeerConnection && !('ontrack' in window.RTCPeerConnection.prototype)) { - Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { - get: function get() { - return this._ontrack; - }, - set: function set(f) { - if (this._ontrack) { - this.removeEventListener('track', this._ontrack); - } - this.addEventListener('track', this._ontrack = f); - }, - enumerable: true, - configurable: true - }); - var origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription; - window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription() { - var _this = this; - if (!this._ontrackpoly) { - this._ontrackpoly = function (e) { - // onaddstream does not fire when a track is added to an existing - // stream. But stream.onaddtrack is implemented so we use that. - e.stream.addEventListener('addtrack', function (te) { - var receiver; - if (window.RTCPeerConnection.prototype.getReceivers) { - receiver = _this.getReceivers().find(function (r) { - return r.track && r.track.id === te.track.id; - }); - } else { - receiver = { - track: te.track - }; - } - var event = new Event('track'); - event.track = te.track; - event.receiver = receiver; - event.transceiver = { - receiver: receiver - }; - event.streams = [e.stream]; - _this.dispatchEvent(event); - }); - e.stream.getTracks().forEach(function (track) { - var receiver; - if (window.RTCPeerConnection.prototype.getReceivers) { - receiver = _this.getReceivers().find(function (r) { - return r.track && r.track.id === track.id; - }); - } else { - receiver = { - track: track - }; - } - var event = new Event('track'); - event.track = track; - event.receiver = receiver; - event.transceiver = { - receiver: receiver - }; - event.streams = [e.stream]; - _this.dispatchEvent(event); - }); - }; - this.addEventListener('addstream', this._ontrackpoly); - } - return origSetRemoteDescription.apply(this, arguments); - }; - } else { - // even if RTCRtpTransceiver is in window, it is only used and - // emitted in unified-plan. Unfortunately this means we need - // to unconditionally wrap the event. - utils.wrapPeerConnectionEvent(window, 'track', function (e) { - if (!e.transceiver) { - Object.defineProperty(e, 'transceiver', { - value: { - receiver: e.receiver - } - }); - } - return e; - }); - } -} -function shimGetSendersWithDtmf(window) { - // Overrides addTrack/removeTrack, depends on shimAddTrackRemoveTrack. - if (_typeof(window) === 'object' && window.RTCPeerConnection && !('getSenders' in window.RTCPeerConnection.prototype) && 'createDTMFSender' in window.RTCPeerConnection.prototype) { - var shimSenderWithDtmf = function shimSenderWithDtmf(pc, track) { - return { - track: track, - get dtmf() { - if (this._dtmf === undefined) { - if (track.kind === 'audio') { - this._dtmf = pc.createDTMFSender(track); - } else { - this._dtmf = null; - } - } - return this._dtmf; - }, - _pc: pc - }; - }; - - // augment addTrack when getSenders is not available. - if (!window.RTCPeerConnection.prototype.getSenders) { - window.RTCPeerConnection.prototype.getSenders = function getSenders() { - this._senders = this._senders || []; - return this._senders.slice(); // return a copy of the internal state. - }; - - var origAddTrack = window.RTCPeerConnection.prototype.addTrack; - window.RTCPeerConnection.prototype.addTrack = function addTrack(track, stream) { - var sender = origAddTrack.apply(this, arguments); - if (!sender) { - sender = shimSenderWithDtmf(this, track); - this._senders.push(sender); - } - return sender; - }; - var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack; - window.RTCPeerConnection.prototype.removeTrack = function removeTrack(sender) { - origRemoveTrack.apply(this, arguments); - var idx = this._senders.indexOf(sender); - if (idx !== -1) { - this._senders.splice(idx, 1); - } - }; - } - var origAddStream = window.RTCPeerConnection.prototype.addStream; - window.RTCPeerConnection.prototype.addStream = function addStream(stream) { - var _this2 = this; - this._senders = this._senders || []; - origAddStream.apply(this, [stream]); - stream.getTracks().forEach(function (track) { - _this2._senders.push(shimSenderWithDtmf(_this2, track)); - }); - }; - var origRemoveStream = window.RTCPeerConnection.prototype.removeStream; - window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) { - var _this3 = this; - this._senders = this._senders || []; - origRemoveStream.apply(this, [stream]); - stream.getTracks().forEach(function (track) { - var sender = _this3._senders.find(function (s) { - return s.track === track; - }); - if (sender) { - // remove sender - _this3._senders.splice(_this3._senders.indexOf(sender), 1); - } - }); - }; - } else if (_typeof(window) === 'object' && window.RTCPeerConnection && 'getSenders' in window.RTCPeerConnection.prototype && 'createDTMFSender' in window.RTCPeerConnection.prototype && window.RTCRtpSender && !('dtmf' in window.RTCRtpSender.prototype)) { - var origGetSenders = window.RTCPeerConnection.prototype.getSenders; - window.RTCPeerConnection.prototype.getSenders = function getSenders() { - var _this4 = this; - var senders = origGetSenders.apply(this, []); - senders.forEach(function (sender) { - return sender._pc = _this4; - }); - return senders; - }; - Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', { - get: function get() { - if (this._dtmf === undefined) { - if (this.track.kind === 'audio') { - this._dtmf = this._pc.createDTMFSender(this.track); - } else { - this._dtmf = null; - } - } - return this._dtmf; - } - }); - } -} -function shimGetStats(window) { - if (!window.RTCPeerConnection) { - return; - } - var origGetStats = window.RTCPeerConnection.prototype.getStats; - window.RTCPeerConnection.prototype.getStats = function getStats() { - var _this5 = this; - var _arguments = Array.prototype.slice.call(arguments), - selector = _arguments[0], - onSucc = _arguments[1], - onErr = _arguments[2]; - - // If selector is a function then we are in the old style stats so just - // pass back the original getStats format to avoid breaking old users. - if (arguments.length > 0 && typeof selector === 'function') { - return origGetStats.apply(this, arguments); - } - - // When spec-style getStats is supported, return those when called with - // either no arguments or the selector argument is null. - if (origGetStats.length === 0 && (arguments.length === 0 || typeof selector !== 'function')) { - return origGetStats.apply(this, []); - } - var fixChromeStats_ = function fixChromeStats_(response) { - var standardReport = {}; - var reports = response.result(); - reports.forEach(function (report) { - var standardStats = { - id: report.id, - timestamp: report.timestamp, - type: { - localcandidate: 'local-candidate', - remotecandidate: 'remote-candidate' - }[report.type] || report.type - }; - report.names().forEach(function (name) { - standardStats[name] = report.stat(name); - }); - standardReport[standardStats.id] = standardStats; - }); - return standardReport; - }; - - // shim getStats with maplike support - var makeMapStats = function makeMapStats(stats) { - return new Map(Object.keys(stats).map(function (key) { - return [key, stats[key]]; - })); - }; - if (arguments.length >= 2) { - var successCallbackWrapper_ = function successCallbackWrapper_(response) { - onSucc(makeMapStats(fixChromeStats_(response))); - }; - return origGetStats.apply(this, [successCallbackWrapper_, selector]); - } - - // promise-support - return new Promise(function (resolve, reject) { - origGetStats.apply(_this5, [function (response) { - resolve(makeMapStats(fixChromeStats_(response))); - }, reject]); - }).then(onSucc, onErr); - }; -} -function shimSenderReceiverGetStats(window) { - if (!(_typeof(window) === 'object' && window.RTCPeerConnection && window.RTCRtpSender && window.RTCRtpReceiver)) { - return; - } - - // shim sender stats. - if (!('getStats' in window.RTCRtpSender.prototype)) { - var origGetSenders = window.RTCPeerConnection.prototype.getSenders; - if (origGetSenders) { - window.RTCPeerConnection.prototype.getSenders = function getSenders() { - var _this6 = this; - var senders = origGetSenders.apply(this, []); - senders.forEach(function (sender) { - return sender._pc = _this6; - }); - return senders; - }; - } - var origAddTrack = window.RTCPeerConnection.prototype.addTrack; - if (origAddTrack) { - window.RTCPeerConnection.prototype.addTrack = function addTrack() { - var sender = origAddTrack.apply(this, arguments); - sender._pc = this; - return sender; - }; - } - window.RTCRtpSender.prototype.getStats = function getStats() { - var sender = this; - return this._pc.getStats().then(function (result) { - return ( - /* Note: this will include stats of all senders that - * send a track with the same id as sender.track as - * it is not possible to identify the RTCRtpSender. - */ - utils.filterStats(result, sender.track, true) - ); - }); - }; - } - - // shim receiver stats. - if (!('getStats' in window.RTCRtpReceiver.prototype)) { - var origGetReceivers = window.RTCPeerConnection.prototype.getReceivers; - if (origGetReceivers) { - window.RTCPeerConnection.prototype.getReceivers = function getReceivers() { - var _this7 = this; - var receivers = origGetReceivers.apply(this, []); - receivers.forEach(function (receiver) { - return receiver._pc = _this7; - }); - return receivers; - }; - } - utils.wrapPeerConnectionEvent(window, 'track', function (e) { - e.receiver._pc = e.srcElement; - return e; - }); - window.RTCRtpReceiver.prototype.getStats = function getStats() { - var receiver = this; - return this._pc.getStats().then(function (result) { - return utils.filterStats(result, receiver.track, false); - }); - }; - } - if (!('getStats' in window.RTCRtpSender.prototype && 'getStats' in window.RTCRtpReceiver.prototype)) { - return; - } - - // shim RTCPeerConnection.getStats(track). - var origGetStats = window.RTCPeerConnection.prototype.getStats; - window.RTCPeerConnection.prototype.getStats = function getStats() { - if (arguments.length > 0 && arguments[0] instanceof window.MediaStreamTrack) { - var track = arguments[0]; - var sender; - var receiver; - var err; - this.getSenders().forEach(function (s) { - if (s.track === track) { - if (sender) { - err = true; - } else { - sender = s; - } - } - }); - this.getReceivers().forEach(function (r) { - if (r.track === track) { - if (receiver) { - err = true; - } else { - receiver = r; - } - } - return r.track === track; - }); - if (err || sender && receiver) { - return Promise.reject(new DOMException('There are more than one sender or receiver for the track.', 'InvalidAccessError')); - } else if (sender) { - return sender.getStats(); - } else if (receiver) { - return receiver.getStats(); - } - return Promise.reject(new DOMException('There is no sender or receiver for the track.', 'InvalidAccessError')); - } - return origGetStats.apply(this, arguments); - }; -} -function shimAddTrackRemoveTrackWithNative(window) { - // shim addTrack/removeTrack with native variants in order to make - // the interactions with legacy getLocalStreams behave as in other browsers. - // Keeps a mapping stream.id => [stream, rtpsenders...] - window.RTCPeerConnection.prototype.getLocalStreams = function getLocalStreams() { - var _this8 = this; - this._shimmedLocalStreams = this._shimmedLocalStreams || {}; - return Object.keys(this._shimmedLocalStreams).map(function (streamId) { - return _this8._shimmedLocalStreams[streamId][0]; - }); - }; - var origAddTrack = window.RTCPeerConnection.prototype.addTrack; - window.RTCPeerConnection.prototype.addTrack = function addTrack(track, stream) { - if (!stream) { - return origAddTrack.apply(this, arguments); - } - this._shimmedLocalStreams = this._shimmedLocalStreams || {}; - var sender = origAddTrack.apply(this, arguments); - if (!this._shimmedLocalStreams[stream.id]) { - this._shimmedLocalStreams[stream.id] = [stream, sender]; - } else if (this._shimmedLocalStreams[stream.id].indexOf(sender) === -1) { - this._shimmedLocalStreams[stream.id].push(sender); - } - return sender; - }; - var origAddStream = window.RTCPeerConnection.prototype.addStream; - window.RTCPeerConnection.prototype.addStream = function addStream(stream) { - var _this9 = this; - this._shimmedLocalStreams = this._shimmedLocalStreams || {}; - stream.getTracks().forEach(function (track) { - var alreadyExists = _this9.getSenders().find(function (s) { - return s.track === track; - }); - if (alreadyExists) { - throw new DOMException('Track already exists.', 'InvalidAccessError'); - } - }); - var existingSenders = this.getSenders(); - origAddStream.apply(this, arguments); - var newSenders = this.getSenders().filter(function (newSender) { - return existingSenders.indexOf(newSender) === -1; - }); - this._shimmedLocalStreams[stream.id] = [stream].concat(newSenders); - }; - var origRemoveStream = window.RTCPeerConnection.prototype.removeStream; - window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) { - this._shimmedLocalStreams = this._shimmedLocalStreams || {}; - delete this._shimmedLocalStreams[stream.id]; - return origRemoveStream.apply(this, arguments); - }; - var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack; - window.RTCPeerConnection.prototype.removeTrack = function removeTrack(sender) { - var _this10 = this; - this._shimmedLocalStreams = this._shimmedLocalStreams || {}; - if (sender) { - Object.keys(this._shimmedLocalStreams).forEach(function (streamId) { - var idx = _this10._shimmedLocalStreams[streamId].indexOf(sender); - if (idx !== -1) { - _this10._shimmedLocalStreams[streamId].splice(idx, 1); - } - if (_this10._shimmedLocalStreams[streamId].length === 1) { - delete _this10._shimmedLocalStreams[streamId]; - } - }); - } - return origRemoveTrack.apply(this, arguments); - }; -} -function shimAddTrackRemoveTrack(window, browserDetails) { - if (!window.RTCPeerConnection) { - return; - } - // shim addTrack and removeTrack. - if (window.RTCPeerConnection.prototype.addTrack && browserDetails.version >= 65) { - return shimAddTrackRemoveTrackWithNative(window); - } - - // also shim pc.getLocalStreams when addTrack is shimmed - // to return the original streams. - var origGetLocalStreams = window.RTCPeerConnection.prototype.getLocalStreams; - window.RTCPeerConnection.prototype.getLocalStreams = function getLocalStreams() { - var _this11 = this; - var nativeStreams = origGetLocalStreams.apply(this); - this._reverseStreams = this._reverseStreams || {}; - return nativeStreams.map(function (stream) { - return _this11._reverseStreams[stream.id]; - }); - }; - var origAddStream = window.RTCPeerConnection.prototype.addStream; - window.RTCPeerConnection.prototype.addStream = function addStream(stream) { - var _this12 = this; - this._streams = this._streams || {}; - this._reverseStreams = this._reverseStreams || {}; - stream.getTracks().forEach(function (track) { - var alreadyExists = _this12.getSenders().find(function (s) { - return s.track === track; - }); - if (alreadyExists) { - throw new DOMException('Track already exists.', 'InvalidAccessError'); - } - }); - // Add identity mapping for consistency with addTrack. - // Unless this is being used with a stream from addTrack. - if (!this._reverseStreams[stream.id]) { - var newStream = new window.MediaStream(stream.getTracks()); - this._streams[stream.id] = newStream; - this._reverseStreams[newStream.id] = stream; - stream = newStream; - } - origAddStream.apply(this, [stream]); - }; - var origRemoveStream = window.RTCPeerConnection.prototype.removeStream; - window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) { - this._streams = this._streams || {}; - this._reverseStreams = this._reverseStreams || {}; - origRemoveStream.apply(this, [this._streams[stream.id] || stream]); - delete this._reverseStreams[this._streams[stream.id] ? this._streams[stream.id].id : stream.id]; - delete this._streams[stream.id]; - }; - window.RTCPeerConnection.prototype.addTrack = function addTrack(track, stream) { - var _this13 = this; - if (this.signalingState === 'closed') { - throw new DOMException('The RTCPeerConnection\'s signalingState is \'closed\'.', 'InvalidStateError'); - } - var streams = [].slice.call(arguments, 1); - if (streams.length !== 1 || !streams[0].getTracks().find(function (t) { - return t === track; - })) { - // this is not fully correct but all we can manage without - // [[associated MediaStreams]] internal slot. - throw new DOMException('The adapter.js addTrack polyfill only supports a single ' + ' stream which is associated with the specified track.', 'NotSupportedError'); - } - var alreadyExists = this.getSenders().find(function (s) { - return s.track === track; - }); - if (alreadyExists) { - throw new DOMException('Track already exists.', 'InvalidAccessError'); - } - this._streams = this._streams || {}; - this._reverseStreams = this._reverseStreams || {}; - var oldStream = this._streams[stream.id]; - if (oldStream) { - // this is using odd Chrome behaviour, use with caution: - // https://bugs.chromium.org/p/webrtc/issues/detail?id=7815 - // Note: we rely on the high-level addTrack/dtmf shim to - // create the sender with a dtmf sender. - oldStream.addTrack(track); - - // Trigger ONN async. - Promise.resolve().then(function () { - _this13.dispatchEvent(new Event('negotiationneeded')); - }); - } else { - var newStream = new window.MediaStream([track]); - this._streams[stream.id] = newStream; - this._reverseStreams[newStream.id] = stream; - this.addStream(newStream); - } - return this.getSenders().find(function (s) { - return s.track === track; - }); - }; - - // replace the internal stream id with the external one and - // vice versa. - function replaceInternalStreamId(pc, description) { - var sdp = description.sdp; - Object.keys(pc._reverseStreams || []).forEach(function (internalId) { - var externalStream = pc._reverseStreams[internalId]; - var internalStream = pc._streams[externalStream.id]; - sdp = sdp.replace(new RegExp(internalStream.id, 'g'), externalStream.id); - }); - return new RTCSessionDescription({ - type: description.type, - sdp: sdp - }); - } - function replaceExternalStreamId(pc, description) { - var sdp = description.sdp; - Object.keys(pc._reverseStreams || []).forEach(function (internalId) { - var externalStream = pc._reverseStreams[internalId]; - var internalStream = pc._streams[externalStream.id]; - sdp = sdp.replace(new RegExp(externalStream.id, 'g'), internalStream.id); - }); - return new RTCSessionDescription({ - type: description.type, - sdp: sdp - }); - } - ['createOffer', 'createAnswer'].forEach(function (method) { - var nativeMethod = window.RTCPeerConnection.prototype[method]; - var methodObj = _defineProperty({}, method, function () { - var _this14 = this; - var args = arguments; - var isLegacyCall = arguments.length && typeof arguments[0] === 'function'; - if (isLegacyCall) { - return nativeMethod.apply(this, [function (description) { - var desc = replaceInternalStreamId(_this14, description); - args[0].apply(null, [desc]); - }, function (err) { - if (args[1]) { - args[1].apply(null, err); - } - }, arguments[2]]); - } - return nativeMethod.apply(this, arguments).then(function (description) { - return replaceInternalStreamId(_this14, description); - }); - }); - window.RTCPeerConnection.prototype[method] = methodObj[method]; - }); - var origSetLocalDescription = window.RTCPeerConnection.prototype.setLocalDescription; - window.RTCPeerConnection.prototype.setLocalDescription = function setLocalDescription() { - if (!arguments.length || !arguments[0].type) { - return origSetLocalDescription.apply(this, arguments); - } - arguments[0] = replaceExternalStreamId(this, arguments[0]); - return origSetLocalDescription.apply(this, arguments); - }; - - // TODO: mangle getStats: https://w3c.github.io/webrtc-stats/#dom-rtcmediastreamstats-streamidentifier - - var origLocalDescription = Object.getOwnPropertyDescriptor(window.RTCPeerConnection.prototype, 'localDescription'); - Object.defineProperty(window.RTCPeerConnection.prototype, 'localDescription', { - get: function get() { - var description = origLocalDescription.get.apply(this); - if (description.type === '') { - return description; - } - return replaceInternalStreamId(this, description); - } - }); - window.RTCPeerConnection.prototype.removeTrack = function removeTrack(sender) { - var _this15 = this; - if (this.signalingState === 'closed') { - throw new DOMException('The RTCPeerConnection\'s signalingState is \'closed\'.', 'InvalidStateError'); - } - // We can not yet check for sender instanceof RTCRtpSender - // since we shim RTPSender. So we check if sender._pc is set. - if (!sender._pc) { - throw new DOMException('Argument 1 of RTCPeerConnection.removeTrack ' + 'does not implement interface RTCRtpSender.', 'TypeError'); - } - var isLocal = sender._pc === this; - if (!isLocal) { - throw new DOMException('Sender was not created by this connection.', 'InvalidAccessError'); - } - - // Search for the native stream the senders track belongs to. - this._streams = this._streams || {}; - var stream; - Object.keys(this._streams).forEach(function (streamid) { - var hasTrack = _this15._streams[streamid].getTracks().find(function (track) { - return sender.track === track; - }); - if (hasTrack) { - stream = _this15._streams[streamid]; - } - }); - if (stream) { - if (stream.getTracks().length === 1) { - // if this is the last track of the stream, remove the stream. This - // takes care of any shimmed _senders. - this.removeStream(this._reverseStreams[stream.id]); - } else { - // relying on the same odd chrome behaviour as above. - stream.removeTrack(sender.track); - } - this.dispatchEvent(new Event('negotiationneeded')); - } - }; -} -function shimPeerConnection(window, browserDetails) { - if (!window.RTCPeerConnection && window.webkitRTCPeerConnection) { - // very basic support for old versions. - window.RTCPeerConnection = window.webkitRTCPeerConnection; - } - if (!window.RTCPeerConnection) { - return; - } - - // shim implicit creation of RTCSessionDescription/RTCIceCandidate - if (browserDetails.version < 53) { - ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'].forEach(function (method) { - var nativeMethod = window.RTCPeerConnection.prototype[method]; - var methodObj = _defineProperty({}, method, function () { - arguments[0] = new (method === 'addIceCandidate' ? window.RTCIceCandidate : window.RTCSessionDescription)(arguments[0]); - return nativeMethod.apply(this, arguments); - }); - window.RTCPeerConnection.prototype[method] = methodObj[method]; - }); - } -} - -// Attempt to fix ONN in plan-b mode. -function fixNegotiationNeeded(window, browserDetails) { - utils.wrapPeerConnectionEvent(window, 'negotiationneeded', function (e) { - var pc = e.target; - if (browserDetails.version < 72 || pc.getConfiguration && pc.getConfiguration().sdpSemantics === 'plan-b') { - if (pc.signalingState !== 'stable') { - return; - } - } - return e; - }); -} - -},{"../utils.js":11,"./getdisplaymedia":4,"./getusermedia":5}],4:[function(require,module,exports){ -/* - * Copyright (c) 2018 The adapter.js project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. - */ -/* eslint-env node */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.shimGetDisplayMedia = shimGetDisplayMedia; -function shimGetDisplayMedia(window, getSourceId) { - if (window.navigator.mediaDevices && 'getDisplayMedia' in window.navigator.mediaDevices) { - return; - } - if (!window.navigator.mediaDevices) { - return; - } - // getSourceId is a function that returns a promise resolving with - // the sourceId of the screen/window/tab to be shared. - if (typeof getSourceId !== 'function') { - console.error('shimGetDisplayMedia: getSourceId argument is not ' + 'a function'); - return; - } - window.navigator.mediaDevices.getDisplayMedia = function getDisplayMedia(constraints) { - return getSourceId(constraints).then(function (sourceId) { - var widthSpecified = constraints.video && constraints.video.width; - var heightSpecified = constraints.video && constraints.video.height; - var frameRateSpecified = constraints.video && constraints.video.frameRate; - constraints.video = { - mandatory: { - chromeMediaSource: 'desktop', - chromeMediaSourceId: sourceId, - maxFrameRate: frameRateSpecified || 3 - } - }; - if (widthSpecified) { - constraints.video.mandatory.maxWidth = widthSpecified; - } - if (heightSpecified) { - constraints.video.mandatory.maxHeight = heightSpecified; - } - return window.navigator.mediaDevices.getUserMedia(constraints); - }); - }; -} - -},{}],5:[function(require,module,exports){ -/* - * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. - */ -/* eslint-env node */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.shimGetUserMedia = shimGetUserMedia; -var utils = _interopRequireWildcard(require("../utils.js")); -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } -function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } -var logging = utils.log; -function shimGetUserMedia(window, browserDetails) { - var navigator = window && window.navigator; - if (!navigator.mediaDevices) { - return; - } - var constraintsToChrome_ = function constraintsToChrome_(c) { - if (_typeof(c) !== 'object' || c.mandatory || c.optional) { - return c; - } - var cc = {}; - Object.keys(c).forEach(function (key) { - if (key === 'require' || key === 'advanced' || key === 'mediaSource') { - return; - } - var r = _typeof(c[key]) === 'object' ? c[key] : { - ideal: c[key] - }; - if (r.exact !== undefined && typeof r.exact === 'number') { - r.min = r.max = r.exact; - } - var oldname_ = function oldname_(prefix, name) { - if (prefix) { - return prefix + name.charAt(0).toUpperCase() + name.slice(1); - } - return name === 'deviceId' ? 'sourceId' : name; - }; - if (r.ideal !== undefined) { - cc.optional = cc.optional || []; - var oc = {}; - if (typeof r.ideal === 'number') { - oc[oldname_('min', key)] = r.ideal; - cc.optional.push(oc); - oc = {}; - oc[oldname_('max', key)] = r.ideal; - cc.optional.push(oc); - } else { - oc[oldname_('', key)] = r.ideal; - cc.optional.push(oc); - } - } - if (r.exact !== undefined && typeof r.exact !== 'number') { - cc.mandatory = cc.mandatory || {}; - cc.mandatory[oldname_('', key)] = r.exact; - } else { - ['min', 'max'].forEach(function (mix) { - if (r[mix] !== undefined) { - cc.mandatory = cc.mandatory || {}; - cc.mandatory[oldname_(mix, key)] = r[mix]; - } - }); - } - }); - if (c.advanced) { - cc.optional = (cc.optional || []).concat(c.advanced); - } - return cc; - }; - var shimConstraints_ = function shimConstraints_(constraints, func) { - if (browserDetails.version >= 61) { - return func(constraints); - } - constraints = JSON.parse(JSON.stringify(constraints)); - if (constraints && _typeof(constraints.audio) === 'object') { - var remap = function remap(obj, a, b) { - if (a in obj && !(b in obj)) { - obj[b] = obj[a]; - delete obj[a]; - } - }; - constraints = JSON.parse(JSON.stringify(constraints)); - remap(constraints.audio, 'autoGainControl', 'googAutoGainControl'); - remap(constraints.audio, 'noiseSuppression', 'googNoiseSuppression'); - constraints.audio = constraintsToChrome_(constraints.audio); - } - if (constraints && _typeof(constraints.video) === 'object') { - // Shim facingMode for mobile & surface pro. - var face = constraints.video.facingMode; - face = face && (_typeof(face) === 'object' ? face : { - ideal: face - }); - var getSupportedFacingModeLies = browserDetails.version < 66; - if (face && (face.exact === 'user' || face.exact === 'environment' || face.ideal === 'user' || face.ideal === 'environment') && !(navigator.mediaDevices.getSupportedConstraints && navigator.mediaDevices.getSupportedConstraints().facingMode && !getSupportedFacingModeLies)) { - delete constraints.video.facingMode; - var matches; - if (face.exact === 'environment' || face.ideal === 'environment') { - matches = ['back', 'rear']; - } else if (face.exact === 'user' || face.ideal === 'user') { - matches = ['front']; - } - if (matches) { - // Look for matches in label, or use last cam for back (typical). - return navigator.mediaDevices.enumerateDevices().then(function (devices) { - devices = devices.filter(function (d) { - return d.kind === 'videoinput'; - }); - var dev = devices.find(function (d) { - return matches.some(function (match) { - return d.label.toLowerCase().includes(match); - }); - }); - if (!dev && devices.length && matches.includes('back')) { - dev = devices[devices.length - 1]; // more likely the back cam - } - - if (dev) { - constraints.video.deviceId = face.exact ? { - exact: dev.deviceId - } : { - ideal: dev.deviceId - }; - } - constraints.video = constraintsToChrome_(constraints.video); - logging('chrome: ' + JSON.stringify(constraints)); - return func(constraints); - }); - } - } - constraints.video = constraintsToChrome_(constraints.video); - } - logging('chrome: ' + JSON.stringify(constraints)); - return func(constraints); - }; - var shimError_ = function shimError_(e) { - if (browserDetails.version >= 64) { - return e; - } - return { - name: { - PermissionDeniedError: 'NotAllowedError', - PermissionDismissedError: 'NotAllowedError', - InvalidStateError: 'NotAllowedError', - DevicesNotFoundError: 'NotFoundError', - ConstraintNotSatisfiedError: 'OverconstrainedError', - TrackStartError: 'NotReadableError', - MediaDeviceFailedDueToShutdown: 'NotAllowedError', - MediaDeviceKillSwitchOn: 'NotAllowedError', - TabCaptureError: 'AbortError', - ScreenCaptureError: 'AbortError', - DeviceCaptureError: 'AbortError' - }[e.name] || e.name, - message: e.message, - constraint: e.constraint || e.constraintName, - toString: function toString() { - return this.name + (this.message && ': ') + this.message; - } - }; - }; - var getUserMedia_ = function getUserMedia_(constraints, onSuccess, onError) { - shimConstraints_(constraints, function (c) { - navigator.webkitGetUserMedia(c, onSuccess, function (e) { - if (onError) { - onError(shimError_(e)); - } - }); - }); - }; - navigator.getUserMedia = getUserMedia_.bind(navigator); - - // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia - // function which returns a Promise, it does not accept spec-style - // constraints. - if (navigator.mediaDevices.getUserMedia) { - var origGetUserMedia = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices); - navigator.mediaDevices.getUserMedia = function (cs) { - return shimConstraints_(cs, function (c) { - return origGetUserMedia(c).then(function (stream) { - if (c.audio && !stream.getAudioTracks().length || c.video && !stream.getVideoTracks().length) { - stream.getTracks().forEach(function (track) { - track.stop(); - }); - throw new DOMException('', 'NotFoundError'); - } - return stream; - }, function (e) { - return Promise.reject(shimError_(e)); - }); - }); - }; - } -} - -},{"../utils.js":11}],6:[function(require,module,exports){ -/* - * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. - */ -/* eslint-env node */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.removeExtmapAllowMixed = removeExtmapAllowMixed; -exports.shimAddIceCandidateNullOrEmpty = shimAddIceCandidateNullOrEmpty; -exports.shimConnectionState = shimConnectionState; -exports.shimMaxMessageSize = shimMaxMessageSize; -exports.shimParameterlessSetLocalDescription = shimParameterlessSetLocalDescription; -exports.shimRTCIceCandidate = shimRTCIceCandidate; -exports.shimRTCIceCandidateRelayProtocol = shimRTCIceCandidateRelayProtocol; -exports.shimSendThrowTypeError = shimSendThrowTypeError; -var _sdp = _interopRequireDefault(require("sdp")); -var utils = _interopRequireWildcard(require("./utils")); -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } -function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } -function shimRTCIceCandidate(window) { - // foundation is arbitrarily chosen as an indicator for full support for - // https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface - if (!window.RTCIceCandidate || window.RTCIceCandidate && 'foundation' in window.RTCIceCandidate.prototype) { - return; - } - var NativeRTCIceCandidate = window.RTCIceCandidate; - window.RTCIceCandidate = function RTCIceCandidate(args) { - // Remove the a= which shouldn't be part of the candidate string. - if (_typeof(args) === 'object' && args.candidate && args.candidate.indexOf('a=') === 0) { - args = JSON.parse(JSON.stringify(args)); - args.candidate = args.candidate.substring(2); - } - if (args.candidate && args.candidate.length) { - // Augment the native candidate with the parsed fields. - var nativeCandidate = new NativeRTCIceCandidate(args); - var parsedCandidate = _sdp["default"].parseCandidate(args.candidate); - for (var key in parsedCandidate) { - if (!(key in nativeCandidate)) { - Object.defineProperty(nativeCandidate, key, { - value: parsedCandidate[key] - }); - } - } - - // Override serializer to not serialize the extra attributes. - nativeCandidate.toJSON = function toJSON() { - return { - candidate: nativeCandidate.candidate, - sdpMid: nativeCandidate.sdpMid, - sdpMLineIndex: nativeCandidate.sdpMLineIndex, - usernameFragment: nativeCandidate.usernameFragment - }; - }; - return nativeCandidate; - } - return new NativeRTCIceCandidate(args); - }; - window.RTCIceCandidate.prototype = NativeRTCIceCandidate.prototype; - - // Hook up the augmented candidate in onicecandidate and - // addEventListener('icecandidate', ...) - utils.wrapPeerConnectionEvent(window, 'icecandidate', function (e) { - if (e.candidate) { - Object.defineProperty(e, 'candidate', { - value: new window.RTCIceCandidate(e.candidate), - writable: 'false' - }); - } - return e; - }); -} -function shimRTCIceCandidateRelayProtocol(window) { - if (!window.RTCIceCandidate || window.RTCIceCandidate && 'relayProtocol' in window.RTCIceCandidate.prototype) { - return; - } - - // Hook up the augmented candidate in onicecandidate and - // addEventListener('icecandidate', ...) - utils.wrapPeerConnectionEvent(window, 'icecandidate', function (e) { - if (e.candidate) { - var parsedCandidate = _sdp["default"].parseCandidate(e.candidate.candidate); - if (parsedCandidate.type === 'relay') { - // This is a libwebrtc-specific mapping of local type preference - // to relayProtocol. - e.candidate.relayProtocol = { - 0: 'tls', - 1: 'tcp', - 2: 'udp' - }[parsedCandidate.priority >> 24]; - } - } - return e; - }); -} -function shimMaxMessageSize(window, browserDetails) { - if (!window.RTCPeerConnection) { - return; - } - if (!('sctp' in window.RTCPeerConnection.prototype)) { - Object.defineProperty(window.RTCPeerConnection.prototype, 'sctp', { - get: function get() { - return typeof this._sctp === 'undefined' ? null : this._sctp; - } - }); - } - var sctpInDescription = function sctpInDescription(description) { - if (!description || !description.sdp) { - return false; - } - var sections = _sdp["default"].splitSections(description.sdp); - sections.shift(); - return sections.some(function (mediaSection) { - var mLine = _sdp["default"].parseMLine(mediaSection); - return mLine && mLine.kind === 'application' && mLine.protocol.indexOf('SCTP') !== -1; - }); - }; - var getRemoteFirefoxVersion = function getRemoteFirefoxVersion(description) { - // TODO: Is there a better solution for detecting Firefox? - var match = description.sdp.match(/mozilla...THIS_IS_SDPARTA-(\d+)/); - if (match === null || match.length < 2) { - return -1; - } - var version = parseInt(match[1], 10); - // Test for NaN (yes, this is ugly) - return version !== version ? -1 : version; - }; - var getCanSendMaxMessageSize = function getCanSendMaxMessageSize(remoteIsFirefox) { - // Every implementation we know can send at least 64 KiB. - // Note: Although Chrome is technically able to send up to 256 KiB, the - // data does not reach the other peer reliably. - // See: https://bugs.chromium.org/p/webrtc/issues/detail?id=8419 - var canSendMaxMessageSize = 65536; - if (browserDetails.browser === 'firefox') { - if (browserDetails.version < 57) { - if (remoteIsFirefox === -1) { - // FF < 57 will send in 16 KiB chunks using the deprecated PPID - // fragmentation. - canSendMaxMessageSize = 16384; - } else { - // However, other FF (and RAWRTC) can reassemble PPID-fragmented - // messages. Thus, supporting ~2 GiB when sending. - canSendMaxMessageSize = 2147483637; - } - } else if (browserDetails.version < 60) { - // Currently, all FF >= 57 will reset the remote maximum message size - // to the default value when a data channel is created at a later - // stage. :( - // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831 - canSendMaxMessageSize = browserDetails.version === 57 ? 65535 : 65536; - } else { - // FF >= 60 supports sending ~2 GiB - canSendMaxMessageSize = 2147483637; - } - } - return canSendMaxMessageSize; - }; - var getMaxMessageSize = function getMaxMessageSize(description, remoteIsFirefox) { - // Note: 65536 bytes is the default value from the SDP spec. Also, - // every implementation we know supports receiving 65536 bytes. - var maxMessageSize = 65536; - - // FF 57 has a slightly incorrect default remote max message size, so - // we need to adjust it here to avoid a failure when sending. - // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1425697 - if (browserDetails.browser === 'firefox' && browserDetails.version === 57) { - maxMessageSize = 65535; - } - var match = _sdp["default"].matchPrefix(description.sdp, 'a=max-message-size:'); - if (match.length > 0) { - maxMessageSize = parseInt(match[0].substring(19), 10); - } else if (browserDetails.browser === 'firefox' && remoteIsFirefox !== -1) { - // If the maximum message size is not present in the remote SDP and - // both local and remote are Firefox, the remote peer can receive - // ~2 GiB. - maxMessageSize = 2147483637; - } - return maxMessageSize; - }; - var origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription; - window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription() { - this._sctp = null; - // Chrome decided to not expose .sctp in plan-b mode. - // As usual, adapter.js has to do an 'ugly worakaround' - // to cover up the mess. - if (browserDetails.browser === 'chrome' && browserDetails.version >= 76) { - var _this$getConfiguratio = this.getConfiguration(), - sdpSemantics = _this$getConfiguratio.sdpSemantics; - if (sdpSemantics === 'plan-b') { - Object.defineProperty(this, 'sctp', { - get: function get() { - return typeof this._sctp === 'undefined' ? null : this._sctp; - }, - enumerable: true, - configurable: true - }); - } - } - if (sctpInDescription(arguments[0])) { - // Check if the remote is FF. - var isFirefox = getRemoteFirefoxVersion(arguments[0]); - - // Get the maximum message size the local peer is capable of sending - var canSendMMS = getCanSendMaxMessageSize(isFirefox); - - // Get the maximum message size of the remote peer. - var remoteMMS = getMaxMessageSize(arguments[0], isFirefox); - - // Determine final maximum message size - var maxMessageSize; - if (canSendMMS === 0 && remoteMMS === 0) { - maxMessageSize = Number.POSITIVE_INFINITY; - } else if (canSendMMS === 0 || remoteMMS === 0) { - maxMessageSize = Math.max(canSendMMS, remoteMMS); - } else { - maxMessageSize = Math.min(canSendMMS, remoteMMS); - } - - // Create a dummy RTCSctpTransport object and the 'maxMessageSize' - // attribute. - var sctp = {}; - Object.defineProperty(sctp, 'maxMessageSize', { - get: function get() { - return maxMessageSize; - } - }); - this._sctp = sctp; - } - return origSetRemoteDescription.apply(this, arguments); - }; -} -function shimSendThrowTypeError(window) { - if (!(window.RTCPeerConnection && 'createDataChannel' in window.RTCPeerConnection.prototype)) { - return; - } - - // Note: Although Firefox >= 57 has a native implementation, the maximum - // message size can be reset for all data channels at a later stage. - // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831 - - function wrapDcSend(dc, pc) { - var origDataChannelSend = dc.send; - dc.send = function send() { - var data = arguments[0]; - var length = data.length || data.size || data.byteLength; - if (dc.readyState === 'open' && pc.sctp && length > pc.sctp.maxMessageSize) { - throw new TypeError('Message too large (can send a maximum of ' + pc.sctp.maxMessageSize + ' bytes)'); - } - return origDataChannelSend.apply(dc, arguments); - }; - } - var origCreateDataChannel = window.RTCPeerConnection.prototype.createDataChannel; - window.RTCPeerConnection.prototype.createDataChannel = function createDataChannel() { - var dataChannel = origCreateDataChannel.apply(this, arguments); - wrapDcSend(dataChannel, this); - return dataChannel; - }; - utils.wrapPeerConnectionEvent(window, 'datachannel', function (e) { - wrapDcSend(e.channel, e.target); - return e; - }); -} - -/* shims RTCConnectionState by pretending it is the same as iceConnectionState. - * See https://bugs.chromium.org/p/webrtc/issues/detail?id=6145#c12 - * for why this is a valid hack in Chrome. In Firefox it is slightly incorrect - * since DTLS failures would be hidden. See - * https://bugzilla.mozilla.org/show_bug.cgi?id=1265827 - * for the Firefox tracking bug. - */ -function shimConnectionState(window) { - if (!window.RTCPeerConnection || 'connectionState' in window.RTCPeerConnection.prototype) { - return; - } - var proto = window.RTCPeerConnection.prototype; - Object.defineProperty(proto, 'connectionState', { - get: function get() { - return { - completed: 'connected', - checking: 'connecting' - }[this.iceConnectionState] || this.iceConnectionState; - }, - enumerable: true, - configurable: true - }); - Object.defineProperty(proto, 'onconnectionstatechange', { - get: function get() { - return this._onconnectionstatechange || null; - }, - set: function set(cb) { - if (this._onconnectionstatechange) { - this.removeEventListener('connectionstatechange', this._onconnectionstatechange); - delete this._onconnectionstatechange; - } - if (cb) { - this.addEventListener('connectionstatechange', this._onconnectionstatechange = cb); - } - }, - enumerable: true, - configurable: true - }); - ['setLocalDescription', 'setRemoteDescription'].forEach(function (method) { - var origMethod = proto[method]; - proto[method] = function () { - if (!this._connectionstatechangepoly) { - this._connectionstatechangepoly = function (e) { - var pc = e.target; - if (pc._lastConnectionState !== pc.connectionState) { - pc._lastConnectionState = pc.connectionState; - var newEvent = new Event('connectionstatechange', e); - pc.dispatchEvent(newEvent); - } - return e; - }; - this.addEventListener('iceconnectionstatechange', this._connectionstatechangepoly); - } - return origMethod.apply(this, arguments); - }; - }); -} -function removeExtmapAllowMixed(window, browserDetails) { - /* remove a=extmap-allow-mixed for webrtc.org < M71 */ - if (!window.RTCPeerConnection) { - return; - } - if (browserDetails.browser === 'chrome' && browserDetails.version >= 71) { - return; - } - if (browserDetails.browser === 'safari' && browserDetails.version >= 605) { - return; - } - var nativeSRD = window.RTCPeerConnection.prototype.setRemoteDescription; - window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription(desc) { - if (desc && desc.sdp && desc.sdp.indexOf('\na=extmap-allow-mixed') !== -1) { - var sdp = desc.sdp.split('\n').filter(function (line) { - return line.trim() !== 'a=extmap-allow-mixed'; - }).join('\n'); - // Safari enforces read-only-ness of RTCSessionDescription fields. - if (window.RTCSessionDescription && desc instanceof window.RTCSessionDescription) { - arguments[0] = new window.RTCSessionDescription({ - type: desc.type, - sdp: sdp - }); - } else { - desc.sdp = sdp; - } - } - return nativeSRD.apply(this, arguments); - }; -} -function shimAddIceCandidateNullOrEmpty(window, browserDetails) { - // Support for addIceCandidate(null or undefined) - // as well as addIceCandidate({candidate: "", ...}) - // https://bugs.chromium.org/p/chromium/issues/detail?id=978582 - // Note: must be called before other polyfills which change the signature. - if (!(window.RTCPeerConnection && window.RTCPeerConnection.prototype)) { - return; - } - var nativeAddIceCandidate = window.RTCPeerConnection.prototype.addIceCandidate; - if (!nativeAddIceCandidate || nativeAddIceCandidate.length === 0) { - return; - } - window.RTCPeerConnection.prototype.addIceCandidate = function addIceCandidate() { - if (!arguments[0]) { - if (arguments[1]) { - arguments[1].apply(null); - } - return Promise.resolve(); - } - // Firefox 68+ emits and processes {candidate: "", ...}, ignore - // in older versions. - // Native support for ignoring exists for Chrome M77+. - // Safari ignores as well, exact version unknown but works in the same - // version that also ignores addIceCandidate(null). - if ((browserDetails.browser === 'chrome' && browserDetails.version < 78 || browserDetails.browser === 'firefox' && browserDetails.version < 68 || browserDetails.browser === 'safari') && arguments[0] && arguments[0].candidate === '') { - return Promise.resolve(); - } - return nativeAddIceCandidate.apply(this, arguments); - }; -} - -// Note: Make sure to call this ahead of APIs that modify -// setLocalDescription.length -function shimParameterlessSetLocalDescription(window, browserDetails) { - if (!(window.RTCPeerConnection && window.RTCPeerConnection.prototype)) { - return; - } - var nativeSetLocalDescription = window.RTCPeerConnection.prototype.setLocalDescription; - if (!nativeSetLocalDescription || nativeSetLocalDescription.length === 0) { - return; - } - window.RTCPeerConnection.prototype.setLocalDescription = function setLocalDescription() { - var _this = this; - var desc = arguments[0] || {}; - if (_typeof(desc) !== 'object' || desc.type && desc.sdp) { - return nativeSetLocalDescription.apply(this, arguments); - } - // The remaining steps should technically happen when SLD comes off the - // RTCPeerConnection's operations chain (not ahead of going on it), but - // this is too difficult to shim. Instead, this shim only covers the - // common case where the operations chain is empty. This is imperfect, but - // should cover many cases. Rationale: Even if we can't reduce the glare - // window to zero on imperfect implementations, there's value in tapping - // into the perfect negotiation pattern that several browsers support. - desc = { - type: desc.type, - sdp: desc.sdp - }; - if (!desc.type) { - switch (this.signalingState) { - case 'stable': - case 'have-local-offer': - case 'have-remote-pranswer': - desc.type = 'offer'; - break; - default: - desc.type = 'answer'; - break; - } - } - if (desc.sdp || desc.type !== 'offer' && desc.type !== 'answer') { - return nativeSetLocalDescription.apply(this, [desc]); - } - var func = desc.type === 'offer' ? this.createOffer : this.createAnswer; - return func.apply(this).then(function (d) { - return nativeSetLocalDescription.apply(_this, [d]); - }); - }; -} - -},{"./utils":11,"sdp":12}],7:[function(require,module,exports){ -/* - * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. - */ -/* eslint-env node */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.shimAddTransceiver = shimAddTransceiver; -exports.shimCreateAnswer = shimCreateAnswer; -exports.shimCreateOffer = shimCreateOffer; -Object.defineProperty(exports, "shimGetDisplayMedia", { - enumerable: true, - get: function get() { - return _getdisplaymedia.shimGetDisplayMedia; - } -}); -exports.shimGetParameters = shimGetParameters; -Object.defineProperty(exports, "shimGetUserMedia", { - enumerable: true, - get: function get() { - return _getusermedia.shimGetUserMedia; - } -}); -exports.shimOnTrack = shimOnTrack; -exports.shimPeerConnection = shimPeerConnection; -exports.shimRTCDataChannel = shimRTCDataChannel; -exports.shimReceiverGetStats = shimReceiverGetStats; -exports.shimRemoveStream = shimRemoveStream; -exports.shimSenderGetStats = shimSenderGetStats; -var utils = _interopRequireWildcard(require("../utils")); -var _getusermedia = require("./getusermedia"); -var _getdisplaymedia = require("./getdisplaymedia"); -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } -function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } -function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } -function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } -function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } -function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } -function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } -function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } -function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } -function shimOnTrack(window) { - if (_typeof(window) === 'object' && window.RTCTrackEvent && 'receiver' in window.RTCTrackEvent.prototype && !('transceiver' in window.RTCTrackEvent.prototype)) { - Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', { - get: function get() { - return { - receiver: this.receiver - }; - } - }); - } -} -function shimPeerConnection(window, browserDetails) { - if (_typeof(window) !== 'object' || !(window.RTCPeerConnection || window.mozRTCPeerConnection)) { - return; // probably media.peerconnection.enabled=false in about:config - } - - if (!window.RTCPeerConnection && window.mozRTCPeerConnection) { - // very basic support for old versions. - window.RTCPeerConnection = window.mozRTCPeerConnection; - } - if (browserDetails.version < 53) { - // shim away need for obsolete RTCIceCandidate/RTCSessionDescription. - ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'].forEach(function (method) { - var nativeMethod = window.RTCPeerConnection.prototype[method]; - var methodObj = _defineProperty({}, method, function () { - arguments[0] = new (method === 'addIceCandidate' ? window.RTCIceCandidate : window.RTCSessionDescription)(arguments[0]); - return nativeMethod.apply(this, arguments); - }); - window.RTCPeerConnection.prototype[method] = methodObj[method]; - }); - } - var modernStatsTypes = { - inboundrtp: 'inbound-rtp', - outboundrtp: 'outbound-rtp', - candidatepair: 'candidate-pair', - localcandidate: 'local-candidate', - remotecandidate: 'remote-candidate' - }; - var nativeGetStats = window.RTCPeerConnection.prototype.getStats; - window.RTCPeerConnection.prototype.getStats = function getStats() { - var _arguments = Array.prototype.slice.call(arguments), - selector = _arguments[0], - onSucc = _arguments[1], - onErr = _arguments[2]; - return nativeGetStats.apply(this, [selector || null]).then(function (stats) { - if (browserDetails.version < 53 && !onSucc) { - // Shim only promise getStats with spec-hyphens in type names - // Leave callback version alone; misc old uses of forEach before Map - try { - stats.forEach(function (stat) { - stat.type = modernStatsTypes[stat.type] || stat.type; - }); - } catch (e) { - if (e.name !== 'TypeError') { - throw e; - } - // Avoid TypeError: "type" is read-only, in old versions. 34-43ish - stats.forEach(function (stat, i) { - stats.set(i, Object.assign({}, stat, { - type: modernStatsTypes[stat.type] || stat.type - })); - }); - } - } - return stats; - }).then(onSucc, onErr); - }; -} -function shimSenderGetStats(window) { - if (!(_typeof(window) === 'object' && window.RTCPeerConnection && window.RTCRtpSender)) { - return; - } - if (window.RTCRtpSender && 'getStats' in window.RTCRtpSender.prototype) { - return; - } - var origGetSenders = window.RTCPeerConnection.prototype.getSenders; - if (origGetSenders) { - window.RTCPeerConnection.prototype.getSenders = function getSenders() { - var _this = this; - var senders = origGetSenders.apply(this, []); - senders.forEach(function (sender) { - return sender._pc = _this; - }); - return senders; - }; - } - var origAddTrack = window.RTCPeerConnection.prototype.addTrack; - if (origAddTrack) { - window.RTCPeerConnection.prototype.addTrack = function addTrack() { - var sender = origAddTrack.apply(this, arguments); - sender._pc = this; - return sender; - }; - } - window.RTCRtpSender.prototype.getStats = function getStats() { - return this.track ? this._pc.getStats(this.track) : Promise.resolve(new Map()); - }; -} -function shimReceiverGetStats(window) { - if (!(_typeof(window) === 'object' && window.RTCPeerConnection && window.RTCRtpSender)) { - return; - } - if (window.RTCRtpSender && 'getStats' in window.RTCRtpReceiver.prototype) { - return; - } - var origGetReceivers = window.RTCPeerConnection.prototype.getReceivers; - if (origGetReceivers) { - window.RTCPeerConnection.prototype.getReceivers = function getReceivers() { - var _this2 = this; - var receivers = origGetReceivers.apply(this, []); - receivers.forEach(function (receiver) { - return receiver._pc = _this2; - }); - return receivers; - }; - } - utils.wrapPeerConnectionEvent(window, 'track', function (e) { - e.receiver._pc = e.srcElement; - return e; - }); - window.RTCRtpReceiver.prototype.getStats = function getStats() { - return this._pc.getStats(this.track); - }; -} -function shimRemoveStream(window) { - if (!window.RTCPeerConnection || 'removeStream' in window.RTCPeerConnection.prototype) { - return; - } - window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) { - var _this3 = this; - utils.deprecated('removeStream', 'removeTrack'); - this.getSenders().forEach(function (sender) { - if (sender.track && stream.getTracks().includes(sender.track)) { - _this3.removeTrack(sender); - } - }); - }; -} -function shimRTCDataChannel(window) { - // rename DataChannel to RTCDataChannel (native fix in FF60): - // https://bugzilla.mozilla.org/show_bug.cgi?id=1173851 - if (window.DataChannel && !window.RTCDataChannel) { - window.RTCDataChannel = window.DataChannel; - } -} -function shimAddTransceiver(window) { - // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647 - // Firefox ignores the init sendEncodings options passed to addTransceiver - // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918 - if (!(_typeof(window) === 'object' && window.RTCPeerConnection)) { - return; - } - var origAddTransceiver = window.RTCPeerConnection.prototype.addTransceiver; - if (origAddTransceiver) { - window.RTCPeerConnection.prototype.addTransceiver = function addTransceiver() { - this.setParametersPromises = []; - // WebIDL input coercion and validation - var sendEncodings = arguments[1] && arguments[1].sendEncodings; - if (sendEncodings === undefined) { - sendEncodings = []; - } - sendEncodings = _toConsumableArray(sendEncodings); - var shouldPerformCheck = sendEncodings.length > 0; - if (shouldPerformCheck) { - // If sendEncodings params are provided, validate grammar - sendEncodings.forEach(function (encodingParam) { - if ('rid' in encodingParam) { - var ridRegex = /^[a-z0-9]{0,16}$/i; - if (!ridRegex.test(encodingParam.rid)) { - throw new TypeError('Invalid RID value provided.'); - } - } - if ('scaleResolutionDownBy' in encodingParam) { - if (!(parseFloat(encodingParam.scaleResolutionDownBy) >= 1.0)) { - throw new RangeError('scale_resolution_down_by must be >= 1.0'); - } - } - if ('maxFramerate' in encodingParam) { - if (!(parseFloat(encodingParam.maxFramerate) >= 0)) { - throw new RangeError('max_framerate must be >= 0.0'); - } - } - }); - } - var transceiver = origAddTransceiver.apply(this, arguments); - if (shouldPerformCheck) { - // Check if the init options were applied. If not we do this in an - // asynchronous way and save the promise reference in a global object. - // This is an ugly hack, but at the same time is way more robust than - // checking the sender parameters before and after the createOffer - // Also note that after the createoffer we are not 100% sure that - // the params were asynchronously applied so we might miss the - // opportunity to recreate offer. - var sender = transceiver.sender; - var params = sender.getParameters(); - if (!('encodings' in params) || - // Avoid being fooled by patched getParameters() below. - params.encodings.length === 1 && Object.keys(params.encodings[0]).length === 0) { - params.encodings = sendEncodings; - sender.sendEncodings = sendEncodings; - this.setParametersPromises.push(sender.setParameters(params).then(function () { - delete sender.sendEncodings; - })["catch"](function () { - delete sender.sendEncodings; - })); - } - } - return transceiver; - }; - } -} -function shimGetParameters(window) { - if (!(_typeof(window) === 'object' && window.RTCRtpSender)) { - return; - } - var origGetParameters = window.RTCRtpSender.prototype.getParameters; - if (origGetParameters) { - window.RTCRtpSender.prototype.getParameters = function getParameters() { - var params = origGetParameters.apply(this, arguments); - if (!('encodings' in params)) { - params.encodings = [].concat(this.sendEncodings || [{}]); - } - return params; - }; - } -} -function shimCreateOffer(window) { - // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647 - // Firefox ignores the init sendEncodings options passed to addTransceiver - // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918 - if (!(_typeof(window) === 'object' && window.RTCPeerConnection)) { - return; - } - var origCreateOffer = window.RTCPeerConnection.prototype.createOffer; - window.RTCPeerConnection.prototype.createOffer = function createOffer() { - var _arguments2 = arguments, - _this4 = this; - if (this.setParametersPromises && this.setParametersPromises.length) { - return Promise.all(this.setParametersPromises).then(function () { - return origCreateOffer.apply(_this4, _arguments2); - })["finally"](function () { - _this4.setParametersPromises = []; - }); - } - return origCreateOffer.apply(this, arguments); - }; -} -function shimCreateAnswer(window) { - // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647 - // Firefox ignores the init sendEncodings options passed to addTransceiver - // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918 - if (!(_typeof(window) === 'object' && window.RTCPeerConnection)) { - return; - } - var origCreateAnswer = window.RTCPeerConnection.prototype.createAnswer; - window.RTCPeerConnection.prototype.createAnswer = function createAnswer() { - var _arguments3 = arguments, - _this5 = this; - if (this.setParametersPromises && this.setParametersPromises.length) { - return Promise.all(this.setParametersPromises).then(function () { - return origCreateAnswer.apply(_this5, _arguments3); - })["finally"](function () { - _this5.setParametersPromises = []; - }); - } - return origCreateAnswer.apply(this, arguments); - }; -} - -},{"../utils":11,"./getdisplaymedia":8,"./getusermedia":9}],8:[function(require,module,exports){ -/* - * Copyright (c) 2018 The adapter.js project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. - */ -/* eslint-env node */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.shimGetDisplayMedia = shimGetDisplayMedia; -function shimGetDisplayMedia(window, preferredMediaSource) { - if (window.navigator.mediaDevices && 'getDisplayMedia' in window.navigator.mediaDevices) { - return; - } - if (!window.navigator.mediaDevices) { - return; - } - window.navigator.mediaDevices.getDisplayMedia = function getDisplayMedia(constraints) { - if (!(constraints && constraints.video)) { - var err = new DOMException('getDisplayMedia without video ' + 'constraints is undefined'); - err.name = 'NotFoundError'; - // from https://heycam.github.io/webidl/#idl-DOMException-error-names - err.code = 8; - return Promise.reject(err); - } - if (constraints.video === true) { - constraints.video = { - mediaSource: preferredMediaSource - }; - } else { - constraints.video.mediaSource = preferredMediaSource; - } - return window.navigator.mediaDevices.getUserMedia(constraints); - }; -} - -},{}],9:[function(require,module,exports){ -/* - * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. - */ -/* eslint-env node */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.shimGetUserMedia = shimGetUserMedia; -var utils = _interopRequireWildcard(require("../utils")); -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } -function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } -function shimGetUserMedia(window, browserDetails) { - var navigator = window && window.navigator; - var MediaStreamTrack = window && window.MediaStreamTrack; - navigator.getUserMedia = function (constraints, onSuccess, onError) { - // Replace Firefox 44+'s deprecation warning with unprefixed version. - utils.deprecated('navigator.getUserMedia', 'navigator.mediaDevices.getUserMedia'); - navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError); - }; - if (!(browserDetails.version > 55 && 'autoGainControl' in navigator.mediaDevices.getSupportedConstraints())) { - var remap = function remap(obj, a, b) { - if (a in obj && !(b in obj)) { - obj[b] = obj[a]; - delete obj[a]; - } - }; - var nativeGetUserMedia = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices); - navigator.mediaDevices.getUserMedia = function (c) { - if (_typeof(c) === 'object' && _typeof(c.audio) === 'object') { - c = JSON.parse(JSON.stringify(c)); - remap(c.audio, 'autoGainControl', 'mozAutoGainControl'); - remap(c.audio, 'noiseSuppression', 'mozNoiseSuppression'); - } - return nativeGetUserMedia(c); - }; - if (MediaStreamTrack && MediaStreamTrack.prototype.getSettings) { - var nativeGetSettings = MediaStreamTrack.prototype.getSettings; - MediaStreamTrack.prototype.getSettings = function () { - var obj = nativeGetSettings.apply(this, arguments); - remap(obj, 'mozAutoGainControl', 'autoGainControl'); - remap(obj, 'mozNoiseSuppression', 'noiseSuppression'); - return obj; - }; - } - if (MediaStreamTrack && MediaStreamTrack.prototype.applyConstraints) { - var nativeApplyConstraints = MediaStreamTrack.prototype.applyConstraints; - MediaStreamTrack.prototype.applyConstraints = function (c) { - if (this.kind === 'audio' && _typeof(c) === 'object') { - c = JSON.parse(JSON.stringify(c)); - remap(c, 'autoGainControl', 'mozAutoGainControl'); - remap(c, 'noiseSuppression', 'mozNoiseSuppression'); - } - return nativeApplyConstraints.apply(this, [c]); - }; - } - } -} - -},{"../utils":11}],10:[function(require,module,exports){ -/* - * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. - */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.shimAudioContext = shimAudioContext; -exports.shimCallbacksAPI = shimCallbacksAPI; -exports.shimConstraints = shimConstraints; -exports.shimCreateOfferLegacy = shimCreateOfferLegacy; -exports.shimGetUserMedia = shimGetUserMedia; -exports.shimLocalStreamsAPI = shimLocalStreamsAPI; -exports.shimRTCIceServerUrls = shimRTCIceServerUrls; -exports.shimRemoteStreamsAPI = shimRemoteStreamsAPI; -exports.shimTrackEventTransceiver = shimTrackEventTransceiver; -var utils = _interopRequireWildcard(require("../utils")); -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } -function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } -function shimLocalStreamsAPI(window) { - if (_typeof(window) !== 'object' || !window.RTCPeerConnection) { - return; - } - if (!('getLocalStreams' in window.RTCPeerConnection.prototype)) { - window.RTCPeerConnection.prototype.getLocalStreams = function getLocalStreams() { - if (!this._localStreams) { - this._localStreams = []; - } - return this._localStreams; - }; - } - if (!('addStream' in window.RTCPeerConnection.prototype)) { - var _addTrack = window.RTCPeerConnection.prototype.addTrack; - window.RTCPeerConnection.prototype.addStream = function addStream(stream) { - var _this = this; - if (!this._localStreams) { - this._localStreams = []; - } - if (!this._localStreams.includes(stream)) { - this._localStreams.push(stream); - } - // Try to emulate Chrome's behaviour of adding in audio-video order. - // Safari orders by track id. - stream.getAudioTracks().forEach(function (track) { - return _addTrack.call(_this, track, stream); - }); - stream.getVideoTracks().forEach(function (track) { - return _addTrack.call(_this, track, stream); - }); - }; - window.RTCPeerConnection.prototype.addTrack = function addTrack(track) { - var _this2 = this; - for (var _len = arguments.length, streams = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - streams[_key - 1] = arguments[_key]; - } - if (streams) { - streams.forEach(function (stream) { - if (!_this2._localStreams) { - _this2._localStreams = [stream]; - } else if (!_this2._localStreams.includes(stream)) { - _this2._localStreams.push(stream); - } - }); - } - return _addTrack.apply(this, arguments); - }; - } - if (!('removeStream' in window.RTCPeerConnection.prototype)) { - window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) { - var _this3 = this; - if (!this._localStreams) { - this._localStreams = []; - } - var index = this._localStreams.indexOf(stream); - if (index === -1) { - return; - } - this._localStreams.splice(index, 1); - var tracks = stream.getTracks(); - this.getSenders().forEach(function (sender) { - if (tracks.includes(sender.track)) { - _this3.removeTrack(sender); - } - }); - }; - } -} -function shimRemoteStreamsAPI(window) { - if (_typeof(window) !== 'object' || !window.RTCPeerConnection) { - return; - } - if (!('getRemoteStreams' in window.RTCPeerConnection.prototype)) { - window.RTCPeerConnection.prototype.getRemoteStreams = function getRemoteStreams() { - return this._remoteStreams ? this._remoteStreams : []; - }; - } - if (!('onaddstream' in window.RTCPeerConnection.prototype)) { - Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', { - get: function get() { - return this._onaddstream; - }, - set: function set(f) { - var _this4 = this; - if (this._onaddstream) { - this.removeEventListener('addstream', this._onaddstream); - this.removeEventListener('track', this._onaddstreampoly); - } - this.addEventListener('addstream', this._onaddstream = f); - this.addEventListener('track', this._onaddstreampoly = function (e) { - e.streams.forEach(function (stream) { - if (!_this4._remoteStreams) { - _this4._remoteStreams = []; - } - if (_this4._remoteStreams.includes(stream)) { - return; - } - _this4._remoteStreams.push(stream); - var event = new Event('addstream'); - event.stream = stream; - _this4.dispatchEvent(event); - }); - }); - } - }); - var origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription; - window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription() { - var pc = this; - if (!this._onaddstreampoly) { - this.addEventListener('track', this._onaddstreampoly = function (e) { - e.streams.forEach(function (stream) { - if (!pc._remoteStreams) { - pc._remoteStreams = []; - } - if (pc._remoteStreams.indexOf(stream) >= 0) { - return; - } - pc._remoteStreams.push(stream); - var event = new Event('addstream'); - event.stream = stream; - pc.dispatchEvent(event); - }); - }); - } - return origSetRemoteDescription.apply(pc, arguments); - }; - } -} -function shimCallbacksAPI(window) { - if (_typeof(window) !== 'object' || !window.RTCPeerConnection) { - return; - } - var prototype = window.RTCPeerConnection.prototype; - var origCreateOffer = prototype.createOffer; - var origCreateAnswer = prototype.createAnswer; - var setLocalDescription = prototype.setLocalDescription; - var setRemoteDescription = prototype.setRemoteDescription; - var addIceCandidate = prototype.addIceCandidate; - prototype.createOffer = function createOffer(successCallback, failureCallback) { - var options = arguments.length >= 2 ? arguments[2] : arguments[0]; - var promise = origCreateOffer.apply(this, [options]); - if (!failureCallback) { - return promise; - } - promise.then(successCallback, failureCallback); - return Promise.resolve(); - }; - prototype.createAnswer = function createAnswer(successCallback, failureCallback) { - var options = arguments.length >= 2 ? arguments[2] : arguments[0]; - var promise = origCreateAnswer.apply(this, [options]); - if (!failureCallback) { - return promise; - } - promise.then(successCallback, failureCallback); - return Promise.resolve(); - }; - var withCallback = function withCallback(description, successCallback, failureCallback) { - var promise = setLocalDescription.apply(this, [description]); - if (!failureCallback) { - return promise; - } - promise.then(successCallback, failureCallback); - return Promise.resolve(); - }; - prototype.setLocalDescription = withCallback; - withCallback = function withCallback(description, successCallback, failureCallback) { - var promise = setRemoteDescription.apply(this, [description]); - if (!failureCallback) { - return promise; - } - promise.then(successCallback, failureCallback); - return Promise.resolve(); - }; - prototype.setRemoteDescription = withCallback; - withCallback = function withCallback(candidate, successCallback, failureCallback) { - var promise = addIceCandidate.apply(this, [candidate]); - if (!failureCallback) { - return promise; - } - promise.then(successCallback, failureCallback); - return Promise.resolve(); - }; - prototype.addIceCandidate = withCallback; -} -function shimGetUserMedia(window) { - var navigator = window && window.navigator; - if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { - // shim not needed in Safari 12.1 - var mediaDevices = navigator.mediaDevices; - var _getUserMedia = mediaDevices.getUserMedia.bind(mediaDevices); - navigator.mediaDevices.getUserMedia = function (constraints) { - return _getUserMedia(shimConstraints(constraints)); - }; - } - if (!navigator.getUserMedia && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { - navigator.getUserMedia = function getUserMedia(constraints, cb, errcb) { - navigator.mediaDevices.getUserMedia(constraints).then(cb, errcb); - }.bind(navigator); - } -} -function shimConstraints(constraints) { - if (constraints && constraints.video !== undefined) { - return Object.assign({}, constraints, { - video: utils.compactObject(constraints.video) - }); - } - return constraints; -} -function shimRTCIceServerUrls(window) { - if (!window.RTCPeerConnection) { - return; - } - // migrate from non-spec RTCIceServer.url to RTCIceServer.urls - var OrigPeerConnection = window.RTCPeerConnection; - window.RTCPeerConnection = function RTCPeerConnection(pcConfig, pcConstraints) { - if (pcConfig && pcConfig.iceServers) { - var newIceServers = []; - for (var i = 0; i < pcConfig.iceServers.length; i++) { - var server = pcConfig.iceServers[i]; - if (server.urls === undefined && server.url) { - utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls'); - server = JSON.parse(JSON.stringify(server)); - server.urls = server.url; - delete server.url; - newIceServers.push(server); - } else { - newIceServers.push(pcConfig.iceServers[i]); - } - } - pcConfig.iceServers = newIceServers; - } - return new OrigPeerConnection(pcConfig, pcConstraints); - }; - window.RTCPeerConnection.prototype = OrigPeerConnection.prototype; - // wrap static methods. Currently just generateCertificate. - if ('generateCertificate' in OrigPeerConnection) { - Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { - get: function get() { - return OrigPeerConnection.generateCertificate; - } - }); - } -} -function shimTrackEventTransceiver(window) { - // Add event.transceiver member over deprecated event.receiver - if (_typeof(window) === 'object' && window.RTCTrackEvent && 'receiver' in window.RTCTrackEvent.prototype && !('transceiver' in window.RTCTrackEvent.prototype)) { - Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', { - get: function get() { - return { - receiver: this.receiver - }; - } - }); - } -} -function shimCreateOfferLegacy(window) { - var origCreateOffer = window.RTCPeerConnection.prototype.createOffer; - window.RTCPeerConnection.prototype.createOffer = function createOffer(offerOptions) { - if (offerOptions) { - if (typeof offerOptions.offerToReceiveAudio !== 'undefined') { - // support bit values - offerOptions.offerToReceiveAudio = !!offerOptions.offerToReceiveAudio; - } - var audioTransceiver = this.getTransceivers().find(function (transceiver) { - return transceiver.receiver.track.kind === 'audio'; - }); - if (offerOptions.offerToReceiveAudio === false && audioTransceiver) { - if (audioTransceiver.direction === 'sendrecv') { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection('sendonly'); - } else { - audioTransceiver.direction = 'sendonly'; - } - } else if (audioTransceiver.direction === 'recvonly') { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection('inactive'); - } else { - audioTransceiver.direction = 'inactive'; - } - } - } else if (offerOptions.offerToReceiveAudio === true && !audioTransceiver) { - this.addTransceiver('audio', { - direction: 'recvonly' - }); - } - if (typeof offerOptions.offerToReceiveVideo !== 'undefined') { - // support bit values - offerOptions.offerToReceiveVideo = !!offerOptions.offerToReceiveVideo; - } - var videoTransceiver = this.getTransceivers().find(function (transceiver) { - return transceiver.receiver.track.kind === 'video'; - }); - if (offerOptions.offerToReceiveVideo === false && videoTransceiver) { - if (videoTransceiver.direction === 'sendrecv') { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection('sendonly'); - } else { - videoTransceiver.direction = 'sendonly'; - } - } else if (videoTransceiver.direction === 'recvonly') { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection('inactive'); - } else { - videoTransceiver.direction = 'inactive'; - } - } - } else if (offerOptions.offerToReceiveVideo === true && !videoTransceiver) { - this.addTransceiver('video', { - direction: 'recvonly' - }); - } - } - return origCreateOffer.apply(this, arguments); - }; -} -function shimAudioContext(window) { - if (_typeof(window) !== 'object' || window.AudioContext) { - return; - } - window.AudioContext = window.webkitAudioContext; -} - -},{"../utils":11}],11:[function(require,module,exports){ -/* - * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. - */ -/* eslint-env node */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.compactObject = compactObject; -exports.deprecated = deprecated; -exports.detectBrowser = detectBrowser; -exports.disableLog = disableLog; -exports.disableWarnings = disableWarnings; -exports.extractVersion = extractVersion; -exports.filterStats = filterStats; -exports.log = log; -exports.walkStats = walkStats; -exports.wrapPeerConnectionEvent = wrapPeerConnectionEvent; -function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } -function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } -var logDisabled_ = true; -var deprecationWarnings_ = true; - -/** - * Extract browser version out of the provided user agent string. - * - * @param {!string} uastring userAgent string. - * @param {!string} expr Regular expression used as match criteria. - * @param {!number} pos position in the version string to be returned. - * @return {!number} browser version. - */ -function extractVersion(uastring, expr, pos) { - var match = uastring.match(expr); - return match && match.length >= pos && parseInt(match[pos], 10); -} - -// Wraps the peerconnection event eventNameToWrap in a function -// which returns the modified event object (or false to prevent -// the event). -function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) { - if (!window.RTCPeerConnection) { - return; - } - var proto = window.RTCPeerConnection.prototype; - var nativeAddEventListener = proto.addEventListener; - proto.addEventListener = function (nativeEventName, cb) { - if (nativeEventName !== eventNameToWrap) { - return nativeAddEventListener.apply(this, arguments); - } - var wrappedCallback = function wrappedCallback(e) { - var modifiedEvent = wrapper(e); - if (modifiedEvent) { - if (cb.handleEvent) { - cb.handleEvent(modifiedEvent); - } else { - cb(modifiedEvent); - } - } - }; - this._eventMap = this._eventMap || {}; - if (!this._eventMap[eventNameToWrap]) { - this._eventMap[eventNameToWrap] = new Map(); - } - this._eventMap[eventNameToWrap].set(cb, wrappedCallback); - return nativeAddEventListener.apply(this, [nativeEventName, wrappedCallback]); - }; - var nativeRemoveEventListener = proto.removeEventListener; - proto.removeEventListener = function (nativeEventName, cb) { - if (nativeEventName !== eventNameToWrap || !this._eventMap || !this._eventMap[eventNameToWrap]) { - return nativeRemoveEventListener.apply(this, arguments); - } - if (!this._eventMap[eventNameToWrap].has(cb)) { - return nativeRemoveEventListener.apply(this, arguments); - } - var unwrappedCb = this._eventMap[eventNameToWrap].get(cb); - this._eventMap[eventNameToWrap]["delete"](cb); - if (this._eventMap[eventNameToWrap].size === 0) { - delete this._eventMap[eventNameToWrap]; - } - if (Object.keys(this._eventMap).length === 0) { - delete this._eventMap; - } - return nativeRemoveEventListener.apply(this, [nativeEventName, unwrappedCb]); - }; - Object.defineProperty(proto, 'on' + eventNameToWrap, { - get: function get() { - return this['_on' + eventNameToWrap]; - }, - set: function set(cb) { - if (this['_on' + eventNameToWrap]) { - this.removeEventListener(eventNameToWrap, this['_on' + eventNameToWrap]); - delete this['_on' + eventNameToWrap]; - } - if (cb) { - this.addEventListener(eventNameToWrap, this['_on' + eventNameToWrap] = cb); - } - }, - enumerable: true, - configurable: true - }); -} -function disableLog(bool) { - if (typeof bool !== 'boolean') { - return new Error('Argument type: ' + _typeof(bool) + '. Please use a boolean.'); - } - logDisabled_ = bool; - return bool ? 'adapter.js logging disabled' : 'adapter.js logging enabled'; -} - -/** - * Disable or enable deprecation warnings - * @param {!boolean} bool set to true to disable warnings. - */ -function disableWarnings(bool) { - if (typeof bool !== 'boolean') { - return new Error('Argument type: ' + _typeof(bool) + '. Please use a boolean.'); - } - deprecationWarnings_ = !bool; - return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled'); -} -function log() { - if ((typeof window === "undefined" ? "undefined" : _typeof(window)) === 'object') { - if (logDisabled_) { - return; - } - if (typeof console !== 'undefined' && typeof console.log === 'function') { - console.log.apply(console, arguments); - } - } -} - -/** - * Shows a deprecation warning suggesting the modern and spec-compatible API. - */ -function deprecated(oldMethod, newMethod) { - if (!deprecationWarnings_) { - return; - } - console.warn(oldMethod + ' is deprecated, please use ' + newMethod + ' instead.'); -} - -/** - * Browser detector. - * - * @return {object} result containing browser and version - * properties. - */ -function detectBrowser(window) { - // Returned result object. - var result = { - browser: null, - version: null - }; - - // Fail early if it's not a browser - if (typeof window === 'undefined' || !window.navigator) { - result.browser = 'Not a browser.'; - return result; - } - var navigator = window.navigator; - if (navigator.mozGetUserMedia) { - // Firefox. - result.browser = 'firefox'; - result.version = extractVersion(navigator.userAgent, /Firefox\/(\d+)\./, 1); - } else if (navigator.webkitGetUserMedia || window.isSecureContext === false && window.webkitRTCPeerConnection) { - // Chrome, Chromium, Webview, Opera. - // Version matches Chrome/WebRTC version. - // Chrome 74 removed webkitGetUserMedia on http as well so we need the - // more complicated fallback to webkitRTCPeerConnection. - result.browser = 'chrome'; - result.version = extractVersion(navigator.userAgent, /Chrom(e|ium)\/(\d+)\./, 2); - } else if (window.RTCPeerConnection && navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) { - // Safari. - result.browser = 'safari'; - result.version = extractVersion(navigator.userAgent, /AppleWebKit\/(\d+)\./, 1); - result.supportsUnifiedPlan = window.RTCRtpTransceiver && 'currentDirection' in window.RTCRtpTransceiver.prototype; - } else { - // Default fallthrough: not supported. - result.browser = 'Not a supported browser.'; - return result; - } - return result; -} - -/** - * Checks if something is an object. - * - * @param {*} val The something you want to check. - * @return true if val is an object, false otherwise. - */ -function isObject(val) { - return Object.prototype.toString.call(val) === '[object Object]'; -} - -/** - * Remove all empty objects and undefined values - * from a nested object -- an enhanced and vanilla version - * of Lodash's `compact`. - */ -function compactObject(data) { - if (!isObject(data)) { - return data; - } - return Object.keys(data).reduce(function (accumulator, key) { - var isObj = isObject(data[key]); - var value = isObj ? compactObject(data[key]) : data[key]; - var isEmptyObject = isObj && !Object.keys(value).length; - if (value === undefined || isEmptyObject) { - return accumulator; - } - return Object.assign(accumulator, _defineProperty({}, key, value)); - }, {}); -} - -/* iterates the stats graph recursively. */ -function walkStats(stats, base, resultSet) { - if (!base || resultSet.has(base.id)) { - return; - } - resultSet.set(base.id, base); - Object.keys(base).forEach(function (name) { - if (name.endsWith('Id')) { - walkStats(stats, stats.get(base[name]), resultSet); - } else if (name.endsWith('Ids')) { - base[name].forEach(function (id) { - walkStats(stats, stats.get(id), resultSet); - }); - } - }); -} - -/* filter getStats for a sender/receiver track. */ -function filterStats(result, track, outbound) { - var streamStatsType = outbound ? 'outbound-rtp' : 'inbound-rtp'; - var filteredResult = new Map(); - if (track === null) { - return filteredResult; - } - var trackStats = []; - result.forEach(function (value) { - if (value.type === 'track' && value.trackIdentifier === track.id) { - trackStats.push(value); - } - }); - trackStats.forEach(function (trackStat) { - result.forEach(function (stats) { - if (stats.type === streamStatsType && stats.trackId === trackStat.id) { - walkStats(result, stats, filteredResult); - } - }); - }); - return filteredResult; -} - -},{}],12:[function(require,module,exports){ -/* eslint-env node */ -'use strict'; - -// SDP helpers. - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -var SDPUtils = {}; - -// Generate an alphanumeric identifier for cname or mids. -// TODO: use UUIDs instead? https://gist.github.com/jed/982883 -SDPUtils.generateIdentifier = function () { - return Math.random().toString(36).substring(2, 12); -}; - -// The RTCP CNAME used by all peerconnections from the same JS. -SDPUtils.localCName = SDPUtils.generateIdentifier(); - -// Splits SDP into lines, dealing with both CRLF and LF. -SDPUtils.splitLines = function (blob) { - return blob.trim().split('\n').map(function (line) { - return line.trim(); - }); -}; -// Splits SDP into sessionpart and mediasections. Ensures CRLF. -SDPUtils.splitSections = function (blob) { - var parts = blob.split('\nm='); - return parts.map(function (part, index) { - return (index > 0 ? 'm=' + part : part).trim() + '\r\n'; - }); -}; - -// Returns the session description. -SDPUtils.getDescription = function (blob) { - var sections = SDPUtils.splitSections(blob); - return sections && sections[0]; -}; - -// Returns the individual media sections. -SDPUtils.getMediaSections = function (blob) { - var sections = SDPUtils.splitSections(blob); - sections.shift(); - return sections; -}; - -// Returns lines that start with a certain prefix. -SDPUtils.matchPrefix = function (blob, prefix) { - return SDPUtils.splitLines(blob).filter(function (line) { - return line.indexOf(prefix) === 0; - }); -}; - -// Parses an ICE candidate line. Sample input: -// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8 -// rport 55996" -// Input can be prefixed with a=. -SDPUtils.parseCandidate = function (line) { - var parts = void 0; - // Parse both variants. - if (line.indexOf('a=candidate:') === 0) { - parts = line.substring(12).split(' '); - } else { - parts = line.substring(10).split(' '); - } - - var candidate = { - foundation: parts[0], - component: { 1: 'rtp', 2: 'rtcp' }[parts[1]] || parts[1], - protocol: parts[2].toLowerCase(), - priority: parseInt(parts[3], 10), - ip: parts[4], - address: parts[4], // address is an alias for ip. - port: parseInt(parts[5], 10), - // skip parts[6] == 'typ' - type: parts[7] - }; - - for (var i = 8; i < parts.length; i += 2) { - switch (parts[i]) { - case 'raddr': - candidate.relatedAddress = parts[i + 1]; - break; - case 'rport': - candidate.relatedPort = parseInt(parts[i + 1], 10); - break; - case 'tcptype': - candidate.tcpType = parts[i + 1]; - break; - case 'ufrag': - candidate.ufrag = parts[i + 1]; // for backward compatibility. - candidate.usernameFragment = parts[i + 1]; - break; - default: - // extension handling, in particular ufrag. Don't overwrite. - if (candidate[parts[i]] === undefined) { - candidate[parts[i]] = parts[i + 1]; - } - break; - } - } - return candidate; -}; - -// Translates a candidate object into SDP candidate attribute. -// This does not include the a= prefix! -SDPUtils.writeCandidate = function (candidate) { - var sdp = []; - sdp.push(candidate.foundation); - - var component = candidate.component; - if (component === 'rtp') { - sdp.push(1); - } else if (component === 'rtcp') { - sdp.push(2); - } else { - sdp.push(component); - } - sdp.push(candidate.protocol.toUpperCase()); - sdp.push(candidate.priority); - sdp.push(candidate.address || candidate.ip); - sdp.push(candidate.port); - - var type = candidate.type; - sdp.push('typ'); - sdp.push(type); - if (type !== 'host' && candidate.relatedAddress && candidate.relatedPort) { - sdp.push('raddr'); - sdp.push(candidate.relatedAddress); - sdp.push('rport'); - sdp.push(candidate.relatedPort); - } - if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') { - sdp.push('tcptype'); - sdp.push(candidate.tcpType); - } - if (candidate.usernameFragment || candidate.ufrag) { - sdp.push('ufrag'); - sdp.push(candidate.usernameFragment || candidate.ufrag); - } - return 'candidate:' + sdp.join(' '); -}; - -// Parses an ice-options line, returns an array of option tags. -// Sample input: -// a=ice-options:foo bar -SDPUtils.parseIceOptions = function (line) { - return line.substring(14).split(' '); -}; - -// Parses a rtpmap line, returns RTCRtpCoddecParameters. Sample input: -// a=rtpmap:111 opus/48000/2 -SDPUtils.parseRtpMap = function (line) { - var parts = line.substring(9).split(' '); - var parsed = { - payloadType: parseInt(parts.shift(), 10) // was: id - }; - - parts = parts[0].split('/'); - - parsed.name = parts[0]; - parsed.clockRate = parseInt(parts[1], 10); // was: clockrate - parsed.channels = parts.length === 3 ? parseInt(parts[2], 10) : 1; - // legacy alias, got renamed back to channels in ORTC. - parsed.numChannels = parsed.channels; - return parsed; -}; - -// Generates a rtpmap line from RTCRtpCodecCapability or -// RTCRtpCodecParameters. -SDPUtils.writeRtpMap = function (codec) { - var pt = codec.payloadType; - if (codec.preferredPayloadType !== undefined) { - pt = codec.preferredPayloadType; - } - var channels = codec.channels || codec.numChannels || 1; - return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate + (channels !== 1 ? '/' + channels : '') + '\r\n'; -}; - -// Parses a extmap line (headerextension from RFC 5285). Sample input: -// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset -// a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset -SDPUtils.parseExtmap = function (line) { - var parts = line.substring(9).split(' '); - return { - id: parseInt(parts[0], 10), - direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv', - uri: parts[1], - attributes: parts.slice(2).join(' ') - }; -}; - -// Generates an extmap line from RTCRtpHeaderExtensionParameters or -// RTCRtpHeaderExtension. -SDPUtils.writeExtmap = function (headerExtension) { - return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) + (headerExtension.direction && headerExtension.direction !== 'sendrecv' ? '/' + headerExtension.direction : '') + ' ' + headerExtension.uri + (headerExtension.attributes ? ' ' + headerExtension.attributes : '') + '\r\n'; -}; - -// Parses a fmtp line, returns dictionary. Sample input: -// a=fmtp:96 vbr=on;cng=on -// Also deals with vbr=on; cng=on -SDPUtils.parseFmtp = function (line) { - var parsed = {}; - var kv = void 0; - var parts = line.substring(line.indexOf(' ') + 1).split(';'); - for (var j = 0; j < parts.length; j++) { - kv = parts[j].trim().split('='); - parsed[kv[0].trim()] = kv[1]; - } - return parsed; -}; - -// Generates a fmtp line from RTCRtpCodecCapability or RTCRtpCodecParameters. -SDPUtils.writeFmtp = function (codec) { - var line = ''; - var pt = codec.payloadType; - if (codec.preferredPayloadType !== undefined) { - pt = codec.preferredPayloadType; - } - if (codec.parameters && Object.keys(codec.parameters).length) { - var params = []; - Object.keys(codec.parameters).forEach(function (param) { - if (codec.parameters[param] !== undefined) { - params.push(param + '=' + codec.parameters[param]); - } else { - params.push(param); - } - }); - line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n'; - } - return line; -}; - -// Parses a rtcp-fb line, returns RTCPRtcpFeedback object. Sample input: -// a=rtcp-fb:98 nack rpsi -SDPUtils.parseRtcpFb = function (line) { - var parts = line.substring(line.indexOf(' ') + 1).split(' '); - return { - type: parts.shift(), - parameter: parts.join(' ') - }; -}; - -// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters. -SDPUtils.writeRtcpFb = function (codec) { - var lines = ''; - var pt = codec.payloadType; - if (codec.preferredPayloadType !== undefined) { - pt = codec.preferredPayloadType; - } - if (codec.rtcpFeedback && codec.rtcpFeedback.length) { - // FIXME: special handling for trr-int? - codec.rtcpFeedback.forEach(function (fb) { - lines += 'a=rtcp-fb:' + pt + ' ' + fb.type + (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') + '\r\n'; - }); - } - return lines; -}; - -// Parses a RFC 5576 ssrc media attribute. Sample input: -// a=ssrc:3735928559 cname:something -SDPUtils.parseSsrcMedia = function (line) { - var sp = line.indexOf(' '); - var parts = { - ssrc: parseInt(line.substring(7, sp), 10) - }; - var colon = line.indexOf(':', sp); - if (colon > -1) { - parts.attribute = line.substring(sp + 1, colon); - parts.value = line.substring(colon + 1); - } else { - parts.attribute = line.substring(sp + 1); - } - return parts; -}; - -// Parse a ssrc-group line (see RFC 5576). Sample input: -// a=ssrc-group:semantics 12 34 -SDPUtils.parseSsrcGroup = function (line) { - var parts = line.substring(13).split(' '); - return { - semantics: parts.shift(), - ssrcs: parts.map(function (ssrc) { - return parseInt(ssrc, 10); - }) - }; -}; - -// Extracts the MID (RFC 5888) from a media section. -// Returns the MID or undefined if no mid line was found. -SDPUtils.getMid = function (mediaSection) { - var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0]; - if (mid) { - return mid.substring(6); - } -}; - -// Parses a fingerprint line for DTLS-SRTP. -SDPUtils.parseFingerprint = function (line) { - var parts = line.substring(14).split(' '); - return { - algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge. - value: parts[1].toUpperCase() // the definition is upper-case in RFC 4572. - }; -}; - -// Extracts DTLS parameters from SDP media section or sessionpart. -// FIXME: for consistency with other functions this should only -// get the fingerprint line as input. See also getIceParameters. -SDPUtils.getDtlsParameters = function (mediaSection, sessionpart) { - var lines = SDPUtils.matchPrefix(mediaSection + sessionpart, 'a=fingerprint:'); - // Note: a=setup line is ignored since we use the 'auto' role in Edge. - return { - role: 'auto', - fingerprints: lines.map(SDPUtils.parseFingerprint) - }; -}; - -// Serializes DTLS parameters to SDP. -SDPUtils.writeDtlsParameters = function (params, setupType) { - var sdp = 'a=setup:' + setupType + '\r\n'; - params.fingerprints.forEach(function (fp) { - sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n'; - }); - return sdp; -}; - -// Parses a=crypto lines into -// https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#dictionary-rtcsrtpsdesparameters-members -SDPUtils.parseCryptoLine = function (line) { - var parts = line.substring(9).split(' '); - return { - tag: parseInt(parts[0], 10), - cryptoSuite: parts[1], - keyParams: parts[2], - sessionParams: parts.slice(3) - }; -}; - -SDPUtils.writeCryptoLine = function (parameters) { - return 'a=crypto:' + parameters.tag + ' ' + parameters.cryptoSuite + ' ' + (_typeof(parameters.keyParams) === 'object' ? SDPUtils.writeCryptoKeyParams(parameters.keyParams) : parameters.keyParams) + (parameters.sessionParams ? ' ' + parameters.sessionParams.join(' ') : '') + '\r\n'; -}; - -// Parses the crypto key parameters into -// https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#rtcsrtpkeyparam* -SDPUtils.parseCryptoKeyParams = function (keyParams) { - if (keyParams.indexOf('inline:') !== 0) { - return null; - } - var parts = keyParams.substring(7).split('|'); - return { - keyMethod: 'inline', - keySalt: parts[0], - lifeTime: parts[1], - mkiValue: parts[2] ? parts[2].split(':')[0] : undefined, - mkiLength: parts[2] ? parts[2].split(':')[1] : undefined - }; -}; - -SDPUtils.writeCryptoKeyParams = function (keyParams) { - return keyParams.keyMethod + ':' + keyParams.keySalt + (keyParams.lifeTime ? '|' + keyParams.lifeTime : '') + (keyParams.mkiValue && keyParams.mkiLength ? '|' + keyParams.mkiValue + ':' + keyParams.mkiLength : ''); -}; - -// Extracts all SDES parameters. -SDPUtils.getCryptoParameters = function (mediaSection, sessionpart) { - var lines = SDPUtils.matchPrefix(mediaSection + sessionpart, 'a=crypto:'); - return lines.map(SDPUtils.parseCryptoLine); -}; - -// Parses ICE information from SDP media section or sessionpart. -// FIXME: for consistency with other functions this should only -// get the ice-ufrag and ice-pwd lines as input. -SDPUtils.getIceParameters = function (mediaSection, sessionpart) { - var ufrag = SDPUtils.matchPrefix(mediaSection + sessionpart, 'a=ice-ufrag:')[0]; - var pwd = SDPUtils.matchPrefix(mediaSection + sessionpart, 'a=ice-pwd:')[0]; - if (!(ufrag && pwd)) { - return null; - } - return { - usernameFragment: ufrag.substring(12), - password: pwd.substring(10) - }; -}; - -// Serializes ICE parameters to SDP. -SDPUtils.writeIceParameters = function (params) { - var sdp = 'a=ice-ufrag:' + params.usernameFragment + '\r\n' + 'a=ice-pwd:' + params.password + '\r\n'; - if (params.iceLite) { - sdp += 'a=ice-lite\r\n'; - } - return sdp; -}; - -// Parses the SDP media section and returns RTCRtpParameters. -SDPUtils.parseRtpParameters = function (mediaSection) { - var description = { - codecs: [], - headerExtensions: [], - fecMechanisms: [], - rtcp: [] - }; - var lines = SDPUtils.splitLines(mediaSection); - var mline = lines[0].split(' '); - description.profile = mline[2]; - for (var i = 3; i < mline.length; i++) { - // find all codecs from mline[3..] - var pt = mline[i]; - var rtpmapline = SDPUtils.matchPrefix(mediaSection, 'a=rtpmap:' + pt + ' ')[0]; - if (rtpmapline) { - var codec = SDPUtils.parseRtpMap(rtpmapline); - var fmtps = SDPUtils.matchPrefix(mediaSection, 'a=fmtp:' + pt + ' '); - // Only the first a=fmtp: is considered. - codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {}; - codec.rtcpFeedback = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-fb:' + pt + ' ').map(SDPUtils.parseRtcpFb); - description.codecs.push(codec); - // parse FEC mechanisms from rtpmap lines. - switch (codec.name.toUpperCase()) { - case 'RED': - case 'ULPFEC': - description.fecMechanisms.push(codec.name.toUpperCase()); - break; - default: - // only RED and ULPFEC are recognized as FEC mechanisms. - break; - } - } - } - SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function (line) { - description.headerExtensions.push(SDPUtils.parseExtmap(line)); - }); - var wildcardRtcpFb = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-fb:* ').map(SDPUtils.parseRtcpFb); - description.codecs.forEach(function (codec) { - wildcardRtcpFb.forEach(function (fb) { - var duplicate = codec.rtcpFeedback.find(function (existingFeedback) { - return existingFeedback.type === fb.type && existingFeedback.parameter === fb.parameter; - }); - if (!duplicate) { - codec.rtcpFeedback.push(fb); - } - }); - }); - // FIXME: parse rtcp. - return description; -}; - -// Generates parts of the SDP media section describing the capabilities / -// parameters. -SDPUtils.writeRtpDescription = function (kind, caps) { - var sdp = ''; - - // Build the mline. - sdp += 'm=' + kind + ' '; - sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs. - sdp += ' ' + (caps.profile || 'UDP/TLS/RTP/SAVPF') + ' '; - sdp += caps.codecs.map(function (codec) { - if (codec.preferredPayloadType !== undefined) { - return codec.preferredPayloadType; - } - return codec.payloadType; - }).join(' ') + '\r\n'; - - sdp += 'c=IN IP4 0.0.0.0\r\n'; - sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n'; - - // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb. - caps.codecs.forEach(function (codec) { - sdp += SDPUtils.writeRtpMap(codec); - sdp += SDPUtils.writeFmtp(codec); - sdp += SDPUtils.writeRtcpFb(codec); - }); - var maxptime = 0; - caps.codecs.forEach(function (codec) { - if (codec.maxptime > maxptime) { - maxptime = codec.maxptime; - } - }); - if (maxptime > 0) { - sdp += 'a=maxptime:' + maxptime + '\r\n'; - } - - if (caps.headerExtensions) { - caps.headerExtensions.forEach(function (extension) { - sdp += SDPUtils.writeExtmap(extension); - }); - } - // FIXME: write fecMechanisms. - return sdp; -}; - -// Parses the SDP media section and returns an array of -// RTCRtpEncodingParameters. -SDPUtils.parseRtpEncodingParameters = function (mediaSection) { - var encodingParameters = []; - var description = SDPUtils.parseRtpParameters(mediaSection); - var hasRed = description.fecMechanisms.indexOf('RED') !== -1; - var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1; - - // filter a=ssrc:... cname:, ignore PlanB-msid - var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:').map(function (line) { - return SDPUtils.parseSsrcMedia(line); - }).filter(function (parts) { - return parts.attribute === 'cname'; - }); - var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc; - var secondarySsrc = void 0; - - var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID').map(function (line) { - var parts = line.substring(17).split(' '); - return parts.map(function (part) { - return parseInt(part, 10); - }); - }); - if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) { - secondarySsrc = flows[0][1]; - } - - description.codecs.forEach(function (codec) { - if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) { - var encParam = { - ssrc: primarySsrc, - codecPayloadType: parseInt(codec.parameters.apt, 10) - }; - if (primarySsrc && secondarySsrc) { - encParam.rtx = { ssrc: secondarySsrc }; - } - encodingParameters.push(encParam); - if (hasRed) { - encParam = JSON.parse(JSON.stringify(encParam)); - encParam.fec = { - ssrc: primarySsrc, - mechanism: hasUlpfec ? 'red+ulpfec' : 'red' - }; - encodingParameters.push(encParam); - } - } - }); - if (encodingParameters.length === 0 && primarySsrc) { - encodingParameters.push({ - ssrc: primarySsrc - }); - } - - // we support both b=AS and b=TIAS but interpret AS as TIAS. - var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b='); - if (bandwidth.length) { - if (bandwidth[0].indexOf('b=TIAS:') === 0) { - bandwidth = parseInt(bandwidth[0].substring(7), 10); - } else if (bandwidth[0].indexOf('b=AS:') === 0) { - // use formula from JSEP to convert b=AS to TIAS value. - bandwidth = parseInt(bandwidth[0].substring(5), 10) * 1000 * 0.95 - 50 * 40 * 8; - } else { - bandwidth = undefined; - } - encodingParameters.forEach(function (params) { - params.maxBitrate = bandwidth; - }); - } - return encodingParameters; -}; - -// parses http://draft.ortc.org/#rtcrtcpparameters* -SDPUtils.parseRtcpParameters = function (mediaSection) { - var rtcpParameters = {}; - - // Gets the first SSRC. Note that with RTX there might be multiple - // SSRCs. - var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:').map(function (line) { - return SDPUtils.parseSsrcMedia(line); - }).filter(function (obj) { - return obj.attribute === 'cname'; - })[0]; - if (remoteSsrc) { - rtcpParameters.cname = remoteSsrc.value; - rtcpParameters.ssrc = remoteSsrc.ssrc; - } - - // Edge uses the compound attribute instead of reducedSize - // compound is !reducedSize - var rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize'); - rtcpParameters.reducedSize = rsize.length > 0; - rtcpParameters.compound = rsize.length === 0; - - // parses the rtcp-mux attrіbute. - // Note that Edge does not support unmuxed RTCP. - var mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux'); - rtcpParameters.mux = mux.length > 0; - - return rtcpParameters; -}; - -SDPUtils.writeRtcpParameters = function (rtcpParameters) { - var sdp = ''; - if (rtcpParameters.reducedSize) { - sdp += 'a=rtcp-rsize\r\n'; - } - if (rtcpParameters.mux) { - sdp += 'a=rtcp-mux\r\n'; - } - if (rtcpParameters.ssrc !== undefined && rtcpParameters.cname) { - sdp += 'a=ssrc:' + rtcpParameters.ssrc + ' cname:' + rtcpParameters.cname + '\r\n'; - } - return sdp; -}; - -// parses either a=msid: or a=ssrc:... msid lines and returns -// the id of the MediaStream and MediaStreamTrack. -SDPUtils.parseMsid = function (mediaSection) { - var parts = void 0; - var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:'); - if (spec.length === 1) { - parts = spec[0].substring(7).split(' '); - return { stream: parts[0], track: parts[1] }; - } - var planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:').map(function (line) { - return SDPUtils.parseSsrcMedia(line); - }).filter(function (msidParts) { - return msidParts.attribute === 'msid'; - }); - if (planB.length > 0) { - parts = planB[0].value.split(' '); - return { stream: parts[0], track: parts[1] }; - } -}; - -// SCTP -// parses draft-ietf-mmusic-sctp-sdp-26 first and falls back -// to draft-ietf-mmusic-sctp-sdp-05 -SDPUtils.parseSctpDescription = function (mediaSection) { - var mline = SDPUtils.parseMLine(mediaSection); - var maxSizeLine = SDPUtils.matchPrefix(mediaSection, 'a=max-message-size:'); - var maxMessageSize = void 0; - if (maxSizeLine.length > 0) { - maxMessageSize = parseInt(maxSizeLine[0].substring(19), 10); - } - if (isNaN(maxMessageSize)) { - maxMessageSize = 65536; - } - var sctpPort = SDPUtils.matchPrefix(mediaSection, 'a=sctp-port:'); - if (sctpPort.length > 0) { - return { - port: parseInt(sctpPort[0].substring(12), 10), - protocol: mline.fmt, - maxMessageSize: maxMessageSize - }; - } - var sctpMapLines = SDPUtils.matchPrefix(mediaSection, 'a=sctpmap:'); - if (sctpMapLines.length > 0) { - var parts = sctpMapLines[0].substring(10).split(' '); - return { - port: parseInt(parts[0], 10), - protocol: parts[1], - maxMessageSize: maxMessageSize - }; - } -}; - -// SCTP -// outputs the draft-ietf-mmusic-sctp-sdp-26 version that all browsers -// support by now receiving in this format, unless we originally parsed -// as the draft-ietf-mmusic-sctp-sdp-05 format (indicated by the m-line -// protocol of DTLS/SCTP -- without UDP/ or TCP/) -SDPUtils.writeSctpDescription = function (media, sctp) { - var output = []; - if (media.protocol !== 'DTLS/SCTP') { - output = ['m=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.protocol + '\r\n', 'c=IN IP4 0.0.0.0\r\n', 'a=sctp-port:' + sctp.port + '\r\n']; - } else { - output = ['m=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.port + '\r\n', 'c=IN IP4 0.0.0.0\r\n', 'a=sctpmap:' + sctp.port + ' ' + sctp.protocol + ' 65535\r\n']; - } - if (sctp.maxMessageSize !== undefined) { - output.push('a=max-message-size:' + sctp.maxMessageSize + '\r\n'); - } - return output.join(''); -}; - -// Generate a session ID for SDP. -// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1 -// recommends using a cryptographically random +ve 64-bit value -// but right now this should be acceptable and within the right range -SDPUtils.generateSessionId = function () { - return Math.random().toString().substr(2, 22); -}; - -// Write boiler plate for start of SDP -// sessId argument is optional - if not supplied it will -// be generated randomly -// sessVersion is optional and defaults to 2 -// sessUser is optional and defaults to 'thisisadapterortc' -SDPUtils.writeSessionBoilerplate = function (sessId, sessVer, sessUser) { - var sessionId = void 0; - var version = sessVer !== undefined ? sessVer : 2; - if (sessId) { - sessionId = sessId; - } else { - sessionId = SDPUtils.generateSessionId(); - } - var user = sessUser || 'thisisadapterortc'; - // FIXME: sess-id should be an NTP timestamp. - return 'v=0\r\n' + 'o=' + user + ' ' + sessionId + ' ' + version + ' IN IP4 127.0.0.1\r\n' + 's=-\r\n' + 't=0 0\r\n'; -}; - -// Gets the direction from the mediaSection or the sessionpart. -SDPUtils.getDirection = function (mediaSection, sessionpart) { - // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv. - var lines = SDPUtils.splitLines(mediaSection); - for (var i = 0; i < lines.length; i++) { - switch (lines[i]) { - case 'a=sendrecv': - case 'a=sendonly': - case 'a=recvonly': - case 'a=inactive': - return lines[i].substring(2); - default: - // FIXME: What should happen here? - } - } - if (sessionpart) { - return SDPUtils.getDirection(sessionpart); - } - return 'sendrecv'; -}; - -SDPUtils.getKind = function (mediaSection) { - var lines = SDPUtils.splitLines(mediaSection); - var mline = lines[0].split(' '); - return mline[0].substring(2); -}; - -SDPUtils.isRejected = function (mediaSection) { - return mediaSection.split(' ', 2)[1] === '0'; -}; - -SDPUtils.parseMLine = function (mediaSection) { - var lines = SDPUtils.splitLines(mediaSection); - var parts = lines[0].substring(2).split(' '); - return { - kind: parts[0], - port: parseInt(parts[1], 10), - protocol: parts[2], - fmt: parts.slice(3).join(' ') - }; -}; - -SDPUtils.parseOLine = function (mediaSection) { - var line = SDPUtils.matchPrefix(mediaSection, 'o=')[0]; - var parts = line.substring(2).split(' '); - return { - username: parts[0], - sessionId: parts[1], - sessionVersion: parseInt(parts[2], 10), - netType: parts[3], - addressType: parts[4], - address: parts[5] - }; -}; - -// a very naive interpretation of a valid SDP. -SDPUtils.isValidSDP = function (blob) { - if (typeof blob !== 'string' || blob.length === 0) { - return false; - } - var lines = SDPUtils.splitLines(blob); - for (var i = 0; i < lines.length; i++) { - if (lines[i].length < 2 || lines[i].charAt(1) !== '=') { - return false; - } - // TODO: check the modifier a bit more. - } - return true; -}; - -// Expose public methods. -if ((typeof module === 'undefined' ? 'undefined' : _typeof(module)) === 'object') { - module.exports = SDPUtils; -} -},{}]},{},[1])(1) -}); diff --git a/testenv/js/janus.js b/testenv/js/janus.js deleted file mode 100644 index 4544ccfc..00000000 --- a/testenv/js/janus.js +++ /dev/null @@ -1,3651 +0,0 @@ -import "./adapter.js" -"use strict"; - -/* - The MIT License (MIT) - - Copyright (c) 2016 Meetecho - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included - in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR - OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, - ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. - */ - -// List of sessions -Janus.sessions = {}; - -Janus.isExtensionEnabled = function() { - if(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) { - // No need for the extension, getDisplayMedia is supported - return true; - } - if(window.navigator.userAgent.match('Chrome')) { - var chromever = parseInt(window.navigator.userAgent.match(/Chrome\/(.*) /)[1], 10); - var maxver = 33; - if(window.navigator.userAgent.match('Linux')) - maxver = 35; // "known" crash in chrome 34 and 35 on linux - if(chromever >= 26 && chromever <= maxver) { - // Older versions of Chrome don't support this extension-based approach, so lie - return true; - } - return Janus.extension.isInstalled(); - } else { - // Firefox and others, no need for the extension (but this doesn't mean it will work) - return true; - } -}; - -var defaultExtension = { - // Screensharing Chrome Extension ID - extensionId: 'hapfgfdkleiggjjpfpenajgdnfckjpaj', - isInstalled: function() { return document.querySelector('#janus-extension-installed') !== null; }, - getScreen: function (callback) { - var pending = window.setTimeout(function () { - var error = new Error('NavigatorUserMediaError'); - error.name = 'The required Chrome extension is not installed: click here to install it. (NOTE: this will need you to refresh the page)'; - return callback(error); - }, 1000); - this.cache[pending] = callback; - window.postMessage({ type: 'janusGetScreen', id: pending }, '*'); - }, - init: function () { - var cache = {}; - this.cache = cache; - // Wait for events from the Chrome Extension - window.addEventListener('message', function (event) { - if(event.origin != window.location.origin) - return; - if(event.data.type == 'janusGotScreen' && cache[event.data.id]) { - var callback = cache[event.data.id]; - delete cache[event.data.id]; - - if (event.data.sourceId === '') { - // user canceled - var error = new Error('NavigatorUserMediaError'); - error.name = 'You cancelled the request for permission, giving up...'; - callback(error); - } else { - callback(null, event.data.sourceId); - } - } else if (event.data.type == 'janusGetScreenPending') { - console.log('clearing ', event.data.id); - window.clearTimeout(event.data.id); - } - }); - } -}; - -Janus.useDefaultDependencies = function (deps) { - var f = (deps && deps.fetch) || fetch; - var p = (deps && deps.Promise) || Promise; - var socketCls = (deps && deps.WebSocket) || WebSocket; - - return { - newWebSocket: function(server, proto) { return new socketCls(server, proto); }, - extension: (deps && deps.extension) || defaultExtension, - isArray: function(arr) { return Array.isArray(arr); }, - webRTCAdapter: (deps && deps.adapter) || adapter, - httpAPICall: function(url, options) { - var fetchOptions = { - method: options.verb, - headers: { - 'Accept': 'application/json, text/plain, */*' - }, - cache: 'no-cache' - }; - if(options.verb === "POST") { - fetchOptions.headers['Content-Type'] = 'application/json'; - } - if(options.withCredentials !== undefined) { - fetchOptions.credentials = options.withCredentials === true ? 'include' : (options.withCredentials ? options.withCredentials : 'omit'); - } - if(options.body) { - fetchOptions.body = JSON.stringify(options.body); - } - - var fetching = f(url, fetchOptions).catch(function(error) { - return p.reject({message: 'Probably a network error, is the server down?', error: error}); - }); - - /* - * fetch() does not natively support timeouts. - * Work around this by starting a timeout manually, and racing it agains the fetch() to see which thing resolves first. - */ - - if(options.timeout) { - var timeout = new p(function(resolve, reject) { - var timerId = setTimeout(function() { - clearTimeout(timerId); - return reject({message: 'Request timed out', timeout: options.timeout}); - }, options.timeout); - }); - fetching = p.race([fetching, timeout]); - } - - fetching.then(function(response) { - if(response.ok) { - if(typeof(options.success) === typeof(Janus.noop)) { - return response.json().then(function(parsed) { - try { - options.success(parsed); - } catch(error) { - Janus.error('Unhandled httpAPICall success callback error', error); - } - }, function(error) { - return p.reject({message: 'Failed to parse response body', error: error, response: response}); - }); - } - } - else { - return p.reject({message: 'API call failed', response: response}); - } - }).catch(function(error) { - if(typeof(options.error) === typeof(Janus.noop)) { - options.error(error.message || '<< internal error >>', error); - } - }); - - return fetching; - } - } -}; - -Janus.useOldDependencies = function (deps) { - var jq = (deps && deps.jQuery) || jQuery; - var socketCls = (deps && deps.WebSocket) || WebSocket; - return { - newWebSocket: function(server, proto) { return new socketCls(server, proto); }, - isArray: function(arr) { return jq.isArray(arr); }, - extension: (deps && deps.extension) || defaultExtension, - webRTCAdapter: (deps && deps.adapter) || adapter, - httpAPICall: function(url, options) { - var payload = options.body !== undefined ? { - contentType: 'application/json', - data: JSON.stringify(options.body) - } : {}; - var credentials = options.withCredentials !== undefined ? {xhrFields: {withCredentials: options.withCredentials}} : {}; - - return jq.ajax(jq.extend(payload, credentials, { - url: url, - type: options.verb, - cache: false, - dataType: 'json', - async: options.async, - timeout: options.timeout, - success: function(result) { - if(typeof(options.success) === typeof(Janus.noop)) { - options.success(result); - } - }, - error: function(xhr, status, err) { - if(typeof(options.error) === typeof(Janus.noop)) { - options.error(status, err); - } - } - })); - } - }; -}; - -Janus.noop = function() {}; - -Janus.dataChanDefaultLabel = "JanusDataChannel"; - -// Note: in the future we may want to change this, e.g., as was -// attempted in https://github.com/meetecho/janus-gateway/issues/1670 -Janus.endOfCandidates = null; - -// Stop all tracks from a given stream -Janus.stopAllTracks = function(stream) { - try { - // Try a MediaStreamTrack.stop() for each track - var tracks = stream.getTracks(); - for(var mst of tracks) { - Janus.log(mst); - if(mst) { - mst.stop(); - } - } - } catch(e) { - // Do nothing if this fails - } -} - -// Initialization -Janus.init = function(options) { - options = options || {}; - options.callback = (typeof options.callback == "function") ? options.callback : Janus.noop; - if(Janus.initDone) { - // Already initialized - options.callback(); - } else { - if(typeof console.log == "undefined") { - console.log = function() {}; - } - // Console logging (all debugging disabled by default) - Janus.trace = Janus.noop; - Janus.debug = Janus.noop; - Janus.vdebug = Janus.noop; - Janus.log = Janus.noop; - Janus.warn = Janus.noop; - Janus.error = Janus.noop; - if(options.debug === true || options.debug === "all") { - // Enable all debugging levels - Janus.trace = console.trace.bind(console); - Janus.debug = console.debug.bind(console); - Janus.vdebug = console.debug.bind(console); - Janus.log = console.log.bind(console); - Janus.warn = console.warn.bind(console); - Janus.error = console.error.bind(console); - } else if(Array.isArray(options.debug)) { - for(var d of options.debug) { - switch(d) { - case "trace": - Janus.trace = console.trace.bind(console); - break; - case "debug": - Janus.debug = console.debug.bind(console); - break; - case "vdebug": - Janus.vdebug = console.debug.bind(console); - break; - case "log": - Janus.log = console.log.bind(console); - break; - case "warn": - Janus.warn = console.warn.bind(console); - break; - case "error": - Janus.error = console.error.bind(console); - break; - default: - console.error("Unknown debugging option '" + d + "' (supported: 'trace', 'debug', 'vdebug', 'log', warn', 'error')"); - break; - } - } - } - Janus.log("Initializing library"); - - var usedDependencies = options.dependencies || Janus.useDefaultDependencies(); - Janus.isArray = usedDependencies.isArray; - Janus.webRTCAdapter = usedDependencies.webRTCAdapter; - Janus.httpAPICall = usedDependencies.httpAPICall; - Janus.newWebSocket = usedDependencies.newWebSocket; - Janus.extension = usedDependencies.extension; - Janus.extension.init(); - - // Helper method to enumerate devices - Janus.listDevices = function(callback, config) { - callback = (typeof callback == "function") ? callback : Janus.noop; - if (config == null) config = { audio: true, video: true }; - if(Janus.isGetUserMediaAvailable()) { - navigator.mediaDevices.getUserMedia(config) - .then(function(stream) { - navigator.mediaDevices.enumerateDevices().then(function(devices) { - Janus.debug(devices); - callback(devices); - // Get rid of the now useless stream - Janus.stopAllTracks(stream) - }); - }) - .catch(function(err) { - Janus.error(err); - callback([]); - }); - } else { - Janus.warn("navigator.mediaDevices unavailable"); - callback([]); - } - }; - // Helper methods to attach/reattach a stream to a video element (previously part of adapter.js) - Janus.attachMediaStream = function(element, stream) { - try { - element.srcObject = stream; - } catch (e) { - try { - element.src = URL.createObjectURL(stream); - } catch (e) { - Janus.error("Error attaching stream to element"); - } - } - }; - Janus.reattachMediaStream = function(to, from) { - try { - to.srcObject = from.srcObject; - } catch (e) { - try { - to.src = from.src; - } catch (e) { - Janus.error("Error reattaching stream to element"); - } - } - }; - // Detect tab close: make sure we don't loose existing onbeforeunload handlers - // (note: for iOS we need to subscribe to a different event, 'pagehide', see - // https://gist.github.com/thehunmonkgroup/6bee8941a49b86be31a787fe8f4b8cfe) - var iOS = ['iPad', 'iPhone', 'iPod'].indexOf(navigator.platform) >= 0; - var eventName = iOS ? 'pagehide' : 'beforeunload'; - var oldOBF = window["on" + eventName]; - window.addEventListener(eventName, function() { - Janus.log("Closing window"); - for(var s in Janus.sessions) { - if(Janus.sessions[s] && Janus.sessions[s].destroyOnUnload) { - Janus.log("Destroying session " + s); - Janus.sessions[s].destroy({unload: true, notifyDestroyed: false}); - } - } - if(oldOBF && typeof oldOBF == "function") { - oldOBF(); - } - }); - // If this is a Safari, check if VP8 or VP9 are supported - Janus.safariVp8 = false; - Janus.safariVp9 = false; - if(Janus.webRTCAdapter.browserDetails.browser === 'safari' && - Janus.webRTCAdapter.browserDetails.version >= 605) { - // Let's see if RTCRtpSender.getCapabilities() is there - if(RTCRtpSender && RTCRtpSender.getCapabilities && RTCRtpSender.getCapabilities("video") && - RTCRtpSender.getCapabilities("video").codecs && RTCRtpSender.getCapabilities("video").codecs.length) { - for(var codec of RTCRtpSender.getCapabilities("video").codecs) { - if(codec && codec.mimeType && codec.mimeType.toLowerCase() === "video/vp8") { - Janus.safariVp8 = true; - } else if(codec && codec.mimeType && codec.mimeType.toLowerCase() === "video/vp9") { - Janus.safariVp9 = true; - } - } - if(Janus.safariVp8) { - Janus.log("This version of Safari supports VP8"); - } else { - Janus.warn("This version of Safari does NOT support VP8: if you're using a Technology Preview, " + - "try enabling the 'WebRTC VP8 codec' setting in the 'Experimental Features' Develop menu"); - } - } else { - // We do it in a very ugly way, as there's no alternative... - // We create a PeerConnection to see if VP8 is in an offer - var testpc = new RTCPeerConnection({}); - testpc.createOffer({offerToReceiveVideo: true}).then(function(offer) { - Janus.safariVp8 = offer.sdp.indexOf("VP8") !== -1; - Janus.safariVp9 = offer.sdp.indexOf("VP9") !== -1; - if(Janus.safariVp8) { - Janus.log("This version of Safari supports VP8"); - } else { - Janus.warn("This version of Safari does NOT support VP8: if you're using a Technology Preview, " + - "try enabling the 'WebRTC VP8 codec' setting in the 'Experimental Features' Develop menu"); - } - testpc.close(); - testpc = null; - }); - } - } - // Check if this browser supports Unified Plan and transceivers - // Based on https://codepen.io/anon/pen/ZqLwWV?editors=0010 - Janus.unifiedPlan = false; - if(Janus.webRTCAdapter.browserDetails.browser === 'firefox' && - Janus.webRTCAdapter.browserDetails.version >= 59) { - // Firefox definitely does, starting from version 59 - Janus.unifiedPlan = true; - } else if(Janus.webRTCAdapter.browserDetails.browser === 'chrome' && - Janus.webRTCAdapter.browserDetails.version >= 72) { - // Chrome does, but it's only usable from version 72 on - Janus.unifiedPlan = true; - } else if(!window.RTCRtpTransceiver || !('currentDirection' in RTCRtpTransceiver.prototype)) { - // Safari supports addTransceiver() but not Unified Plan when - // currentDirection is not defined (see codepen above). - Janus.unifiedPlan = false; - } else { - // Check if addTransceiver() throws an exception - var tempPc = new RTCPeerConnection(); - try { - tempPc.addTransceiver('audio'); - Janus.unifiedPlan = true; - } catch (e) {} - tempPc.close(); - } - Janus.initDone = true; - options.callback(); - } -}; - -// Helper method to check whether WebRTC is supported by this browser -Janus.isWebrtcSupported = function() { - return !!window.RTCPeerConnection; -}; -// Helper method to check whether devices can be accessed by this browser (e.g., not possible via plain HTTP) -Janus.isGetUserMediaAvailable = function() { - return navigator.mediaDevices && navigator.mediaDevices.getUserMedia; -}; - -// Helper method to create random identifiers (e.g., transaction) -Janus.randomString = function(len) { - var charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - var randomString = ''; - for (var i = 0; i < len; i++) { - var randomPoz = Math.floor(Math.random() * charSet.length); - randomString += charSet.substring(randomPoz,randomPoz+1); - } - return randomString; -}; - -export function Janus(gatewayCallbacks) { - gatewayCallbacks = gatewayCallbacks || {}; - gatewayCallbacks.success = (typeof gatewayCallbacks.success == "function") ? gatewayCallbacks.success : Janus.noop; - gatewayCallbacks.error = (typeof gatewayCallbacks.error == "function") ? gatewayCallbacks.error : Janus.noop; - gatewayCallbacks.destroyed = (typeof gatewayCallbacks.destroyed == "function") ? gatewayCallbacks.destroyed : Janus.noop; - if(!Janus.initDone) { - gatewayCallbacks.error("Library not initialized"); - return {}; - } - if(!Janus.isWebrtcSupported()) { - gatewayCallbacks.error("WebRTC not supported by this browser"); - return {}; - } - Janus.log("Library initialized: " + Janus.initDone); - if(!gatewayCallbacks.server) { - gatewayCallbacks.error("Invalid server url"); - return {}; - } - var websockets = false; - var ws = null; - var wsHandlers = {}; - var wsKeepaliveTimeoutId = null; - var servers = null; - var serversIndex = 0; - var server = gatewayCallbacks.server; - if(Janus.isArray(server)) { - Janus.log("Multiple servers provided (" + server.length + "), will use the first that works"); - server = null; - servers = gatewayCallbacks.server; - Janus.debug(servers); - } else { - if(server.indexOf("ws") === 0) { - websockets = true; - Janus.log("Using WebSockets to contact Janus: " + server); - } else { - websockets = false; - Janus.log("Using REST API to contact Janus: " + server); - } - } - var iceServers = gatewayCallbacks.iceServers || [{urls: "stun:stun.l.google.com:19302"}]; - var iceTransportPolicy = gatewayCallbacks.iceTransportPolicy; - var bundlePolicy = gatewayCallbacks.bundlePolicy; - // Whether IPv6 candidates should be gathered - var ipv6Support = (gatewayCallbacks.ipv6 === true); - // Whether we should enable the withCredentials flag for XHR requests - var withCredentials = false; - if(gatewayCallbacks.withCredentials !== undefined && gatewayCallbacks.withCredentials !== null) - withCredentials = gatewayCallbacks.withCredentials === true; - // Optional max events - var maxev = 10; - if(gatewayCallbacks.max_poll_events !== undefined && gatewayCallbacks.max_poll_events !== null) - maxev = gatewayCallbacks.max_poll_events; - if(maxev < 1) - maxev = 1; - // Token to use (only if the token based authentication mechanism is enabled) - var token = null; - if(gatewayCallbacks.token !== undefined && gatewayCallbacks.token !== null) - token = gatewayCallbacks.token; - // API secret to use (only if the shared API secret is enabled) - var apisecret = null; - if(gatewayCallbacks.apisecret !== undefined && gatewayCallbacks.apisecret !== null) - apisecret = gatewayCallbacks.apisecret; - // Whether we should destroy this session when onbeforeunload is called - this.destroyOnUnload = true; - if(gatewayCallbacks.destroyOnUnload !== undefined && gatewayCallbacks.destroyOnUnload !== null) - this.destroyOnUnload = (gatewayCallbacks.destroyOnUnload === true); - // Some timeout-related values - var keepAlivePeriod = 25000; - if(gatewayCallbacks.keepAlivePeriod !== undefined && gatewayCallbacks.keepAlivePeriod !== null) - keepAlivePeriod = gatewayCallbacks.keepAlivePeriod; - if(isNaN(keepAlivePeriod)) - keepAlivePeriod = 25000; - var longPollTimeout = 60000; - if(gatewayCallbacks.longPollTimeout !== undefined && gatewayCallbacks.longPollTimeout !== null) - longPollTimeout = gatewayCallbacks.longPollTimeout; - if(isNaN(longPollTimeout)) - longPollTimeout = 60000; - - // overrides for default maxBitrate values for simulcasting - function getMaxBitrates(simulcastMaxBitrates) { - var maxBitrates = { - high: 900000, - medium: 300000, - low: 100000, - }; - - if (simulcastMaxBitrates !== undefined && simulcastMaxBitrates !== null) { - if (simulcastMaxBitrates.high) - maxBitrates.high = simulcastMaxBitrates.high; - if (simulcastMaxBitrates.medium) - maxBitrates.medium = simulcastMaxBitrates.medium; - if (simulcastMaxBitrates.low) - maxBitrates.low = simulcastMaxBitrates.low; - } - - return maxBitrates; - } - - var connected = false; - var sessionId = null; - var pluginHandles = {}; - var that = this; - var retries = 0; - var transactions = {}; - createSession(gatewayCallbacks); - - // Public methods - this.getServer = function() { return server; }; - this.isConnected = function() { return connected; }; - this.reconnect = function(callbacks) { - callbacks = callbacks || {}; - callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; - callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; - callbacks["reconnect"] = true; - createSession(callbacks); - }; - this.getSessionId = function() { return sessionId; }; - this.getInfo = function(callbacks) { getInfo(callbacks); }; - this.destroy = function(callbacks) { destroySession(callbacks); }; - this.attach = function(callbacks) { createHandle(callbacks); }; - - function eventHandler() { - if(sessionId == null) - return; - Janus.debug('Long poll...'); - if(!connected) { - Janus.warn("Is the server down? (connected=false)"); - return; - } - var longpoll = server + "/" + sessionId + "?rid=" + new Date().getTime(); - if(maxev) - longpoll = longpoll + "&maxev=" + maxev; - if(token) - longpoll = longpoll + "&token=" + encodeURIComponent(token); - if(apisecret) - longpoll = longpoll + "&apisecret=" + encodeURIComponent(apisecret); - Janus.httpAPICall(longpoll, { - verb: 'GET', - withCredentials: withCredentials, - success: handleEvent, - timeout: longPollTimeout, - error: function(textStatus, errorThrown) { - Janus.error(textStatus + ":", errorThrown); - retries++; - if(retries > 3) { - // Did we just lose the server? :-( - connected = false; - gatewayCallbacks.error("Lost connection to the server (is it down?)"); - return; - } - eventHandler(); - } - }); - } - - // Private event handler: this will trigger plugin callbacks, if set - function handleEvent(json, skipTimeout) { - retries = 0; - if(!websockets && sessionId !== undefined && sessionId !== null && skipTimeout !== true) - eventHandler(); - if(!websockets && Janus.isArray(json)) { - // We got an array: it means we passed a maxev > 1, iterate on all objects - for(var i=0; i data channel: ' + dcState); - if(dcState === 'open') { - // Any pending messages to send? - if(config.dataChannel[label].pending && config.dataChannel[label].pending.length > 0) { - Janus.log("Sending pending messages on <" + label + ">:", config.dataChannel[label].pending.length); - for(var data of config.dataChannel[label].pending) { - Janus.log("Sending data on data channel <" + label + ">"); - Janus.debug(data); - config.dataChannel[label].send(data); - } - config.dataChannel[label].pending = []; - } - // Notify the open data channel - pluginHandle.ondataopen(label, protocol); - } - }; - var onDataChannelError = function(error) { - Janus.error('Got error on data channel:', error); - // TODO - }; - if(!incoming) { - // FIXME Add options (ordered, maxRetransmits, etc.) - var dcoptions = config.dataChannelOptions; - if(dcprotocol) - dcoptions.protocol = dcprotocol; - config.dataChannel[dclabel] = config.pc.createDataChannel(dclabel, dcoptions); - } else { - // The channel was created by Janus - config.dataChannel[dclabel] = incoming; - } - config.dataChannel[dclabel].onmessage = onDataChannelMessage; - config.dataChannel[dclabel].onopen = onDataChannelStateChange; - config.dataChannel[dclabel].onclose = onDataChannelStateChange; - config.dataChannel[dclabel].onerror = onDataChannelError; - config.dataChannel[dclabel].pending = []; - if(pendingData) - config.dataChannel[dclabel].pending.push(pendingData); - } - - // Private method to send a data channel message - function sendData(handleId, callbacks) { - callbacks = callbacks || {}; - callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; - callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - callbacks.error("Invalid handle"); - return; - } - var config = pluginHandle.webrtcStuff; - var data = callbacks.text || callbacks.data; - if(!data) { - Janus.warn("Invalid data"); - callbacks.error("Invalid data"); - return; - } - var label = callbacks.label ? callbacks.label : Janus.dataChanDefaultLabel; - if(!config.dataChannel[label]) { - // Create new data channel and wait for it to open - createDataChannel(handleId, label, callbacks.protocol, false, data, callbacks.protocol); - callbacks.success(); - return; - } - if(config.dataChannel[label].readyState !== "open") { - config.dataChannel[label].pending.push(data); - callbacks.success(); - return; - } - Janus.log("Sending data on data channel <" + label + ">"); - Janus.debug(data); - config.dataChannel[label].send(data); - callbacks.success(); - } - - // Private method to send a DTMF tone - function sendDtmf(handleId, callbacks) { - callbacks = callbacks || {}; - callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; - callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - callbacks.error("Invalid handle"); - return; - } - var config = pluginHandle.webrtcStuff; - if(!config.dtmfSender) { - // Create the DTMF sender the proper way, if possible - if(config.pc) { - var senders = config.pc.getSenders(); - var audioSender = senders.find(function(sender) { - return sender.track && sender.track.kind === 'audio'; - }); - if(!audioSender) { - Janus.warn("Invalid DTMF configuration (no audio track)"); - callbacks.error("Invalid DTMF configuration (no audio track)"); - return; - } - config.dtmfSender = audioSender.dtmf; - if(config.dtmfSender) { - Janus.log("Created DTMF Sender"); - config.dtmfSender.ontonechange = function(tone) { Janus.debug("Sent DTMF tone: " + tone.tone); }; - } - } - if(!config.dtmfSender) { - Janus.warn("Invalid DTMF configuration"); - callbacks.error("Invalid DTMF configuration"); - return; - } - } - var dtmf = callbacks.dtmf; - if(!dtmf) { - Janus.warn("Invalid DTMF parameters"); - callbacks.error("Invalid DTMF parameters"); - return; - } - var tones = dtmf.tones; - if(!tones) { - Janus.warn("Invalid DTMF string"); - callbacks.error("Invalid DTMF string"); - return; - } - var duration = (typeof dtmf.duration === 'number') ? dtmf.duration : 500; // We choose 500ms as the default duration for a tone - var gap = (typeof dtmf.gap === 'number') ? dtmf.gap : 50; // We choose 50ms as the default gap between tones - Janus.debug("Sending DTMF string " + tones + " (duration " + duration + "ms, gap " + gap + "ms)"); - config.dtmfSender.insertDTMF(tones, duration, gap); - callbacks.success(); - } - - // Private method to destroy a plugin handle - function destroyHandle(handleId, callbacks) { - callbacks = callbacks || {}; - callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; - callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; - var noRequest = (callbacks.noRequest === true); - Janus.log("Destroying handle " + handleId + " (only-locally=" + noRequest + ")"); - cleanupWebrtc(handleId); - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || pluginHandle.detached) { - // Plugin was already detached by Janus, calling detach again will return a handle not found error, so just exit here - delete pluginHandles[handleId]; - callbacks.success(); - return; - } - pluginHandle.detached = true; - if(noRequest) { - // We're only removing the handle locally - delete pluginHandles[handleId]; - callbacks.success(); - return; - } - if(!connected) { - Janus.warn("Is the server down? (connected=false)"); - callbacks.error("Is the server down? (connected=false)"); - return; - } - var request = { "janus": "detach", "transaction": Janus.randomString(12) }; - if(pluginHandle.token) - request["token"] = pluginHandle.token; - if(apisecret) - request["apisecret"] = apisecret; - if(websockets) { - request["session_id"] = sessionId; - request["handle_id"] = handleId; - ws.send(JSON.stringify(request)); - delete pluginHandles[handleId]; - callbacks.success(); - return; - } - Janus.httpAPICall(server + "/" + sessionId + "/" + handleId, { - verb: 'POST', - withCredentials: withCredentials, - body: request, - success: function(json) { - Janus.log("Destroyed handle:"); - Janus.debug(json); - if(json["janus"] !== "success") { - Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME - } - delete pluginHandles[handleId]; - callbacks.success(); - }, - error: function(textStatus, errorThrown) { - Janus.error(textStatus + ":", errorThrown); // FIXME - // We cleanup anyway - delete pluginHandles[handleId]; - callbacks.success(); - } - }); - } - - // WebRTC stuff - function streamsDone(handleId, jsep, media, callbacks, stream) { - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - // Close all tracks if the given stream has been created internally - if(!callbacks.stream) { - Janus.stopAllTracks(stream); - } - callbacks.error("Invalid handle"); - return; - } - var config = pluginHandle.webrtcStuff; - Janus.debug("streamsDone:", stream); - if(stream) { - Janus.debug(" -- Audio tracks:", stream.getAudioTracks()); - Janus.debug(" -- Video tracks:", stream.getVideoTracks()); - } - // We're now capturing the new stream: check if we're updating or if it's a new thing - var addTracks = false; - if(!config.myStream || !media.update || (config.streamExternal && !media.replaceAudio && !media.replaceVideo)) { - config.myStream = stream; - addTracks = true; - } else { - // We only need to update the existing stream - if(((!media.update && isAudioSendEnabled(media)) || (media.update && (media.addAudio || media.replaceAudio))) && - stream.getAudioTracks() && stream.getAudioTracks().length) { - config.myStream.addTrack(stream.getAudioTracks()[0]); - if(Janus.unifiedPlan) { - // Use Transceivers - Janus.log((media.replaceAudio ? "Replacing" : "Adding") + " audio track:", stream.getAudioTracks()[0]); - var audioTransceiver = null; - const transceivers = config.pc.getTransceivers(); - if(transceivers && transceivers.length > 0) { - for(const t of transceivers) { - if((t.sender && t.sender.track && t.sender.track.kind === "audio") || - (t.receiver && t.receiver.track && t.receiver.track.kind === "audio")) { - audioTransceiver = t; - break; - } - } - } - if(audioTransceiver && audioTransceiver.sender) { - audioTransceiver.sender.replaceTrack(stream.getAudioTracks()[0]); - } else { - config.pc.addTrack(stream.getAudioTracks()[0], stream); - } - } else { - Janus.log((media.replaceAudio ? "Replacing" : "Adding") + " audio track:", stream.getAudioTracks()[0]); - config.pc.addTrack(stream.getAudioTracks()[0], stream); - } - } - if(((!media.update && isVideoSendEnabled(media)) || (media.update && (media.addVideo || media.replaceVideo))) && - stream.getVideoTracks() && stream.getVideoTracks().length) { - config.myStream.addTrack(stream.getVideoTracks()[0]); - if(Janus.unifiedPlan) { - // Use Transceivers - Janus.log((media.replaceVideo ? "Replacing" : "Adding") + " video track:", stream.getVideoTracks()[0]); - var videoTransceiver = null; - const transceivers = config.pc.getTransceivers(); - if(transceivers && transceivers.length > 0) { - for(const t of transceivers) { - if((t.sender && t.sender.track && t.sender.track.kind === "video") || - (t.receiver && t.receiver.track && t.receiver.track.kind === "video")) { - videoTransceiver = t; - break; - } - } - } - if(videoTransceiver && videoTransceiver.sender) { - videoTransceiver.sender.replaceTrack(stream.getVideoTracks()[0]); - } else { - config.pc.addTrack(stream.getVideoTracks()[0], stream); - } - } else { - Janus.log((media.replaceVideo ? "Replacing" : "Adding") + " video track:", stream.getVideoTracks()[0]); - config.pc.addTrack(stream.getVideoTracks()[0], stream); - } - } - } - // If we still need to create a PeerConnection, let's do that - if(!config.pc) { - var pc_config = {"iceServers": iceServers, "iceTransportPolicy": iceTransportPolicy, "bundlePolicy": bundlePolicy}; - if(Janus.webRTCAdapter.browserDetails.browser === "chrome") { - // For Chrome versions before 72, we force a plan-b semantic, and unified-plan otherwise - pc_config["sdpSemantics"] = (Janus.webRTCAdapter.browserDetails.version < 72) ? "plan-b" : "unified-plan"; - } - var pc_constraints = {}; - if(Janus.webRTCAdapter.browserDetails.browser === "edge") { - // This is Edge, enable BUNDLE explicitly - pc_config.bundlePolicy = "max-bundle"; - } - // Check if a sender or receiver transform has been provided - if(RTCRtpSender && (RTCRtpSender.prototype.createEncodedStreams || - (RTCRtpSender.prototype.createEncodedAudioStreams && - RTCRtpSender.prototype.createEncodedVideoStreams)) && - (callbacks.senderTransforms || callbacks.receiverTransforms)) { - config.senderTransforms = callbacks.senderTransforms; - config.receiverTransforms = callbacks.receiverTransforms; - pc_config["forceEncodedAudioInsertableStreams"] = true; - pc_config["forceEncodedVideoInsertableStreams"] = true; - pc_config["encodedInsertableStreams"] = true; - } - Janus.log("Creating PeerConnection"); - Janus.debug(pc_constraints); - config.pc = new RTCPeerConnection(pc_config, pc_constraints); - Janus.debug(config.pc); - if(config.pc.getStats) { // FIXME - config.volume = {}; - config.bitrate.value = "0 kbits/sec"; - } - Janus.log("Preparing local SDP and gathering candidates (trickle=" + config.trickle + ")"); - config.pc.oniceconnectionstatechange = function() { - if(config.pc) - pluginHandle.iceState(config.pc.iceConnectionState); - }; - config.pc.onicecandidate = function(event) { - if (!event.candidate || - (Janus.webRTCAdapter.browserDetails.browser === 'edge' && event.candidate.candidate.indexOf('endOfCandidates') > 0)) { - Janus.log("End of candidates."); - config.iceDone = true; - if(config.trickle === true) { - // Notify end of candidates - sendTrickleCandidate(handleId, {"completed": true}); - } else { - // No trickle, time to send the complete SDP (including all candidates) - sendSDP(handleId, callbacks); - } - } else { - // JSON.stringify doesn't work on some WebRTC objects anymore - // See https://code.google.com/p/chromium/issues/detail?id=467366 - var candidate = { - "candidate": event.candidate.candidate, - "sdpMid": event.candidate.sdpMid, - "sdpMLineIndex": event.candidate.sdpMLineIndex - }; - if(config.trickle === true) { - // Send candidate - sendTrickleCandidate(handleId, candidate); - } - } - }; - config.pc.ontrack = function(event) { - Janus.log("Handling Remote Track"); - Janus.debug(event); - if(!event.streams) - return; - config.remoteStream = event.streams[0]; - pluginHandle.onremotestream(config.remoteStream); - if(event.track.onended) - return; - if(config.receiverTransforms) { - var receiverStreams = null; - if(RTCRtpSender.prototype.createEncodedStreams) { - receiverStreams = event.receiver.createEncodedStreams(); - } else if(RTCRtpSender.prototype.createAudioEncodedStreams || RTCRtpSender.prototype.createEncodedVideoStreams) { - if(event.track.kind === "audio" && config.receiverTransforms["audio"]) { - receiverStreams = event.receiver.createEncodedAudioStreams(); - } else if(event.track.kind === "video" && config.receiverTransforms["video"]) { - receiverStreams = event.receiver.createEncodedVideoStreams(); - } - } - if(receiverStreams) { - console.log(receiverStreams); - if(receiverStreams.readableStream && receiverStreams.writableStream) { - receiverStreams.readableStream - .pipeThrough(config.receiverTransforms[event.track.kind]) - .pipeTo(receiverStreams.writableStream); - } else if(receiverStreams.readable && receiverStreams.writable) { - receiverStreams.readable - .pipeThrough(config.receiverTransforms[event.track.kind]) - .pipeTo(receiverStreams.writable); - } - } - } - var trackMutedTimeoutId = null; - Janus.log("Adding onended callback to track:", event.track); - event.track.onended = function(ev) { - Janus.log("Remote track removed:", ev); - if(config.remoteStream) { - clearTimeout(trackMutedTimeoutId); - config.remoteStream.removeTrack(ev.target); - pluginHandle.onremotestream(config.remoteStream); - } - }; - event.track.onmute = function(ev) { - Janus.log("Remote track muted:", ev); - if(config.remoteStream && trackMutedTimeoutId == null) { - trackMutedTimeoutId = setTimeout(function() { - Janus.log("Removing remote track"); - if (config.remoteStream) { - config.remoteStream.removeTrack(ev.target); - pluginHandle.onremotestream(config.remoteStream); - } - trackMutedTimeoutId = null; - // Chrome seems to raise mute events only at multiples of 834ms; - // we set the timeout to three times this value (rounded to 840ms) - }, 3 * 840); - } - }; - event.track.onunmute = function(ev) { - Janus.log("Remote track flowing again:", ev); - if(trackMutedTimeoutId != null) { - clearTimeout(trackMutedTimeoutId); - trackMutedTimeoutId = null; - } else { - try { - config.remoteStream.addTrack(ev.target); - pluginHandle.onremotestream(config.remoteStream); - } catch(e) { - Janus.error(e); - } - } - }; - }; - } - if(addTracks && stream) { - Janus.log('Adding local stream'); - var simulcast = (callbacks.simulcast === true || callbacks.simulcast2 === true) && Janus.unifiedPlan; - var svc = callbacks.svc; - stream.getTracks().forEach(function(track) { - Janus.log('Adding local track:', track); - var sender = null; - if((!simulcast && !svc) || track.kind === 'audio') { - sender = config.pc.addTrack(track, stream); - } else if(simulcast) { - Janus.log('Enabling rid-based simulcasting:', track); - let maxBitrates = getMaxBitrates(callbacks.simulcastMaxBitrates); - let tr = config.pc.addTransceiver(track, { - direction: "sendrecv", - streams: [stream], - sendEncodings: callbacks.sendEncodings || [ - { rid: "h", active: true, maxBitrate: maxBitrates.high }, - { rid: "m", active: true, maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2 }, - { rid: "l", active: true, maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4 } - ] - }); - if(tr) - sender = tr.sender; - } else { - Janus.log('Enabling SVC (' + svc + '):', track); - let tr = config.pc.addTransceiver(track, { - direction: "sendrecv", - streams: [stream], - sendEncodings: [ - { scalabilityMode: svc } - ] - }); - if(tr) - sender = tr.sender; - } - // Check if insertable streams are involved - if(sender && config.senderTransforms) { - var senderStreams = null; - if(RTCRtpSender.prototype.createEncodedStreams) { - senderStreams = sender.createEncodedStreams(); - } else if(RTCRtpSender.prototype.createAudioEncodedStreams || RTCRtpSender.prototype.createEncodedVideoStreams) { - if(sender.track.kind === "audio" && config.senderTransforms["audio"]) { - senderStreams = sender.createEncodedAudioStreams(); - } else if(sender.track.kind === "video" && config.senderTransforms["video"]) { - senderStreams = sender.createEncodedVideoStreams(); - } - } - if(senderStreams) { - console.log(senderStreams); - if(senderStreams.readableStream && senderStreams.writableStream) { - senderStreams.readableStream - .pipeThrough(config.senderTransforms[sender.track.kind]) - .pipeTo(senderStreams.writableStream); - } else if(senderStreams.readable && senderStreams.writable) { - senderStreams.readable - .pipeThrough(config.senderTransforms[sender.track.kind]) - .pipeTo(senderStreams.writable); - } - } - } - }); - } - // Any data channel to create? - if(isDataEnabled(media) && !config.dataChannel[Janus.dataChanDefaultLabel]) { - Janus.log("Creating default data channel"); - createDataChannel(handleId, Janus.dataChanDefaultLabel, null, false); - config.pc.ondatachannel = function(event) { - Janus.log("Data channel created by Janus:", event); - createDataChannel(handleId, event.channel.label, event.channel.protocol, event.channel); - }; - } - // If there's a new local stream, let's notify the application - if(config.myStream) { - pluginHandle.onlocalstream(config.myStream); - } - // Create offer/answer now - if(!jsep) { - createOffer(handleId, media, callbacks); - } else { - config.pc.setRemoteDescription(jsep) - .then(function() { - Janus.log("Remote description accepted!"); - config.remoteSdp = jsep.sdp; - // Any trickle candidate we cached? - if(config.candidates && config.candidates.length > 0) { - for(var i = 0; i< config.candidates.length; i++) { - var candidate = config.candidates[i]; - Janus.debug("Adding remote candidate:", candidate); - if(!candidate || candidate.completed === true) { - // end-of-candidates - config.pc.addIceCandidate(Janus.endOfCandidates); - } else { - // New candidate - config.pc.addIceCandidate(candidate); - } - } - config.candidates = []; - } - // Create the answer now - createAnswer(handleId, media, callbacks); - }, callbacks.error); - } - } - - function prepareWebrtc(handleId, offer, callbacks) { - callbacks = callbacks || {}; - callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; - callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : webrtcError; - var jsep = callbacks.jsep; - if(offer && jsep) { - Janus.error("Provided a JSEP to a createOffer"); - callbacks.error("Provided a JSEP to a createOffer"); - return; - } else if(!offer && (!jsep || !jsep.type || !jsep.sdp)) { - Janus.error("A valid JSEP is required for createAnswer"); - callbacks.error("A valid JSEP is required for createAnswer"); - return; - } - /* Check that callbacks.media is a (not null) Object */ - callbacks.media = (typeof callbacks.media === 'object' && callbacks.media) ? callbacks.media : { audio: true, video: true }; - var media = callbacks.media; - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - callbacks.error("Invalid handle"); - return; - } - var config = pluginHandle.webrtcStuff; - config.trickle = isTrickleEnabled(callbacks.trickle); - // Are we updating a session? - if(!config.pc) { - // Nope, new PeerConnection - media.update = false; - media.keepAudio = false; - media.keepVideo = false; - } else { - Janus.log("Updating existing media session"); - media.update = true; - // Check if there's anything to add/remove/replace, or if we - // can go directly to preparing the new SDP offer or answer - if(callbacks.stream) { - // External stream: is this the same as the one we were using before? - if(callbacks.stream !== config.myStream) { - Janus.log("Renegotiation involves a new external stream"); - } - } else { - // Check if there are changes on audio - if(media.addAudio) { - media.keepAudio = false; - media.replaceAudio = false; - media.removeAudio = false; - media.audioSend = true; - if(config.myStream && config.myStream.getAudioTracks() && config.myStream.getAudioTracks().length) { - Janus.error("Can't add audio stream, there already is one"); - callbacks.error("Can't add audio stream, there already is one"); - return; - } - } else if(media.removeAudio) { - media.keepAudio = false; - media.replaceAudio = false; - media.addAudio = false; - media.audioSend = false; - } else if(media.replaceAudio) { - media.keepAudio = false; - media.addAudio = false; - media.removeAudio = false; - media.audioSend = true; - } - if(!config.myStream) { - // No media stream: if we were asked to replace, it's actually an "add" - if(media.replaceAudio) { - media.keepAudio = false; - media.replaceAudio = false; - media.addAudio = true; - media.audioSend = true; - } - if(isAudioSendEnabled(media)) { - media.keepAudio = false; - media.addAudio = true; - } - } else { - if(!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) { - // No audio track: if we were asked to replace, it's actually an "add" - if(media.replaceAudio) { - media.keepAudio = false; - media.replaceAudio = false; - media.addAudio = true; - media.audioSend = true; - } - if(isAudioSendEnabled(media)) { - media.keepAudio = false; - media.addAudio = true; - } - } else { - // We have an audio track: should we keep it as it is? - if(isAudioSendEnabled(media) && - !media.removeAudio && !media.replaceAudio) { - media.keepAudio = true; - } - } - } - // Check if there are changes on video - if(media.addVideo) { - media.keepVideo = false; - media.replaceVideo = false; - media.removeVideo = false; - media.videoSend = true; - if(config.myStream && config.myStream.getVideoTracks() && config.myStream.getVideoTracks().length) { - Janus.error("Can't add video stream, there already is one"); - callbacks.error("Can't add video stream, there already is one"); - return; - } - } else if(media.removeVideo) { - media.keepVideo = false; - media.replaceVideo = false; - media.addVideo = false; - media.videoSend = false; - } else if(media.replaceVideo) { - media.keepVideo = false; - media.addVideo = false; - media.removeVideo = false; - media.videoSend = true; - } - if(!config.myStream) { - // No media stream: if we were asked to replace, it's actually an "add" - if(media.replaceVideo) { - media.keepVideo = false; - media.replaceVideo = false; - media.addVideo = true; - media.videoSend = true; - } - if(isVideoSendEnabled(media)) { - media.keepVideo = false; - media.addVideo = true; - } - } else { - if(!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) { - // No video track: if we were asked to replace, it's actually an "add" - if(media.replaceVideo) { - media.keepVideo = false; - media.replaceVideo = false; - media.addVideo = true; - media.videoSend = true; - } - if(isVideoSendEnabled(media)) { - media.keepVideo = false; - media.addVideo = true; - } - } else { - // We have a video track: should we keep it as it is? - if(isVideoSendEnabled(media) && !media.removeVideo && !media.replaceVideo) { - media.keepVideo = true; - } - } - } - // Data channels can only be added - if(media.addData) { - media.data = true; - } - } - // If we're updating and keeping all tracks, let's skip the getUserMedia part - if((isAudioSendEnabled(media) && media.keepAudio) && - (isVideoSendEnabled(media) && media.keepVideo)) { - pluginHandle.consentDialog(false); - streamsDone(handleId, jsep, media, callbacks, config.myStream); - return; - } - } - // If we're updating, check if we need to remove/replace one of the tracks - if(media.update && (!config.streamExternal || (config.streamExternal && (media.replaceAudio || media.replaceVideo)))) { - if(media.removeAudio || media.replaceAudio) { - if(config.myStream && config.myStream.getAudioTracks() && config.myStream.getAudioTracks().length) { - var at = config.myStream.getAudioTracks()[0]; - Janus.log("Removing audio track:", at); - config.myStream.removeTrack(at); - try { - at.stop(); - } catch(e) {} - } - if(config.pc.getSenders() && config.pc.getSenders().length) { - var ra = true; - if(media.replaceAudio && Janus.unifiedPlan) { - // We can use replaceTrack - ra = false; - } - if(ra) { - for(var asnd of config.pc.getSenders()) { - if(asnd && asnd.track && asnd.track.kind === "audio") { - Janus.log("Removing audio sender:", asnd); - config.pc.removeTrack(asnd); - } - } - } - } - } - if(media.removeVideo || media.replaceVideo) { - if(config.myStream && config.myStream.getVideoTracks() && config.myStream.getVideoTracks().length) { - var vt = config.myStream.getVideoTracks()[0]; - Janus.log("Removing video track:", vt); - config.myStream.removeTrack(vt); - try { - vt.stop(); - } catch(e) {} - } - if(config.pc.getSenders() && config.pc.getSenders().length) { - var rv = true; - if(media.replaceVideo && Janus.unifiedPlan) { - // We can use replaceTrack - rv = false; - } - if(rv) { - for(var vsnd of config.pc.getSenders()) { - if(vsnd && vsnd.track && vsnd.track.kind === "video") { - Janus.log("Removing video sender:", vsnd); - config.pc.removeTrack(vsnd); - } - } - } - } - } - } - // Was a MediaStream object passed, or do we need to take care of that? - if(callbacks.stream) { - var stream = callbacks.stream; - Janus.log("MediaStream provided by the application"); - Janus.debug(stream); - // If this is an update, let's check if we need to release the previous stream - if(media.update && config.myStream && config.myStream !== callbacks.stream && !config.streamExternal && !media.replaceAudio && !media.replaceVideo) { - // We're replacing a stream we captured ourselves with an external one - Janus.stopAllTracks(config.myStream); - config.myStream = null; - } - // Skip the getUserMedia part - config.streamExternal = true; - pluginHandle.consentDialog(false); - streamsDone(handleId, jsep, media, callbacks, stream); - return; - } - if(isAudioSendEnabled(media) || isVideoSendEnabled(media)) { - if(!Janus.isGetUserMediaAvailable()) { - callbacks.error("getUserMedia not available"); - return; - } - var constraints = { mandatory: {}, optional: []}; - pluginHandle.consentDialog(true); - var audioSupport = isAudioSendEnabled(media); - if(audioSupport && media && typeof media.audio === 'object') - audioSupport = media.audio; - var videoSupport = isVideoSendEnabled(media); - if(videoSupport && media) { - var simulcast = (callbacks.simulcast === true || callbacks.simulcast2 === true); - var svc = callbacks.svc; - if((simulcast || svc) && !jsep && !media.video) - media.video = "hires"; - if(media.video && media.video != 'screen' && media.video != 'window') { - if(typeof media.video === 'object') { - videoSupport = media.video; - } else { - var width = 0; - var height = 0; - if(media.video === 'lowres') { - // Small resolution, 4:3 - height = 240; - width = 320; - } else if(media.video === 'lowres-16:9') { - // Small resolution, 16:9 - height = 180; - width = 320; - } else if(media.video === 'hires' || media.video === 'hires-16:9' || media.video === 'hdres') { - // High(HD) resolution is only 16:9 - height = 720; - width = 1280; - } else if(media.video === 'fhdres') { - // Full HD resolution is only 16:9 - height = 1080; - width = 1920; - } else if(media.video === '4kres') { - // 4K resolution is only 16:9 - height = 2160; - width = 3840; - } else if(media.video === 'stdres') { - // Normal resolution, 4:3 - height = 480; - width = 640; - } else if(media.video === 'stdres-16:9') { - // Normal resolution, 16:9 - height = 360; - width = 640; - } else { - Janus.log("Default video setting is stdres 4:3"); - height = 480; - width = 640; - } - Janus.log("Adding media constraint:", media.video); - videoSupport = { - 'height': {'ideal': height}, - 'width': {'ideal': width} - }; - Janus.log("Adding video constraint:", videoSupport); - } - } else if(media.video === 'screen' || media.video === 'window') { - if(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) { - // The new experimental getDisplayMedia API is available, let's use that - // https://groups.google.com/forum/#!topic/discuss-webrtc/Uf0SrR4uxzk - // https://webrtchacks.com/chrome-screensharing-getdisplaymedia/ - constraints.video = {}; - if(media.screenshareFrameRate) { - constraints.video.frameRate = media.screenshareFrameRate; - } - if(media.screenshareHeight) { - constraints.video.height = media.screenshareHeight; - } - if(media.screenshareWidth) { - constraints.video.width = media.screenshareWidth; - } - constraints.audio = media.captureDesktopAudio; - navigator.mediaDevices.getDisplayMedia(constraints) - .then(function(stream) { - pluginHandle.consentDialog(false); - if(isAudioSendEnabled(media) && !media.keepAudio) { - navigator.mediaDevices.getUserMedia({ audio: true, video: false }) - .then(function (audioStream) { - stream.addTrack(audioStream.getAudioTracks()[0]); - streamsDone(handleId, jsep, media, callbacks, stream); - }) - } else { - streamsDone(handleId, jsep, media, callbacks, stream); - } - }, function (error) { - pluginHandle.consentDialog(false); - callbacks.error(error); - }); - return; - } - // We're going to try and use the extension for Chrome 34+, the old approach - // for older versions of Chrome, or the experimental support in Firefox 33+ - const callbackUserMedia = function(error, stream) { - pluginHandle.consentDialog(false); - if(error) { - callbacks.error(error); - } else { - streamsDone(handleId, jsep, media, callbacks, stream); - } - } - const getScreenMedia = function(constraints, gsmCallback, useAudio) { - Janus.log("Adding media constraint (screen capture)"); - Janus.debug(constraints); - navigator.mediaDevices.getUserMedia(constraints) - .then(function(stream) { - if(useAudio) { - navigator.mediaDevices.getUserMedia({ audio: true, video: false }) - .then(function (audioStream) { - stream.addTrack(audioStream.getAudioTracks()[0]); - gsmCallback(null, stream); - }) - } else { - gsmCallback(null, stream); - } - }) - .catch(function(error) { pluginHandle.consentDialog(false); gsmCallback(error); }); - } - if(Janus.webRTCAdapter.browserDetails.browser === 'chrome') { - var chromever = Janus.webRTCAdapter.browserDetails.version; - var maxver = 33; - if(window.navigator.userAgent.match('Linux')) - maxver = 35; // "known" crash in chrome 34 and 35 on linux - if(chromever >= 26 && chromever <= maxver) { - // Chrome 26->33 requires some awkward chrome://flags manipulation - constraints = { - video: { - mandatory: { - googLeakyBucket: true, - maxWidth: window.screen.width, - maxHeight: window.screen.height, - minFrameRate: media.screenshareFrameRate, - maxFrameRate: media.screenshareFrameRate, - chromeMediaSource: 'screen' - } - }, - audio: isAudioSendEnabled(media) && !media.keepAudio - }; - getScreenMedia(constraints, callbackUserMedia); - } else { - // Chrome 34+ requires an extension - Janus.extension.getScreen(function (error, sourceId) { - if (error) { - pluginHandle.consentDialog(false); - return callbacks.error(error); - } - constraints = { - audio: false, - video: { - mandatory: { - chromeMediaSource: 'desktop', - maxWidth: window.screen.width, - maxHeight: window.screen.height, - minFrameRate: media.screenshareFrameRate, - maxFrameRate: media.screenshareFrameRate, - }, - optional: [ - {googLeakyBucket: true}, - {googTemporalLayeredScreencast: true} - ] - } - }; - constraints.video.mandatory.chromeMediaSourceId = sourceId; - getScreenMedia(constraints, callbackUserMedia, - isAudioSendEnabled(media) && !media.keepAudio); - }); - } - } else if(Janus.webRTCAdapter.browserDetails.browser === 'firefox') { - if(Janus.webRTCAdapter.browserDetails.version >= 33) { - // Firefox 33+ has experimental support for screen sharing - constraints = { - video: { - mozMediaSource: media.video, - mediaSource: media.video - }, - audio: isAudioSendEnabled(media) && !media.keepAudio - }; - getScreenMedia(constraints, function (err, stream) { - callbackUserMedia(err, stream); - // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1045810 - if (!err) { - var lastTime = stream.currentTime; - var polly = window.setInterval(function () { - if(!stream) - window.clearInterval(polly); - if(stream.currentTime == lastTime) { - window.clearInterval(polly); - if(stream.onended) { - stream.onended(); - } - } - lastTime = stream.currentTime; - }, 500); - } - }); - } else { - var error = new Error('NavigatorUserMediaError'); - error.name = 'Your version of Firefox does not support screen sharing, please install Firefox 33 (or more recent versions)'; - pluginHandle.consentDialog(false); - callbacks.error(error); - return; - } - } - return; - } - } - // If we got here, we're not screensharing - if(!media || media.video !== 'screen') { - // Check whether all media sources are actually available or not - navigator.mediaDevices.enumerateDevices().then(function(devices) { - var audioExist = devices.some(function(device) { - return device.kind === 'audioinput'; - }), - videoExist = isScreenSendEnabled(media) || devices.some(function(device) { - return device.kind === 'videoinput'; - }); - - // Check whether a missing device is really a problem - var audioSend = isAudioSendEnabled(media); - var videoSend = isVideoSendEnabled(media); - var needAudioDevice = isAudioSendRequired(media); - var needVideoDevice = isVideoSendRequired(media); - if(audioSend || videoSend || needAudioDevice || needVideoDevice) { - // We need to send either audio or video - var haveAudioDevice = audioSend ? audioExist : false; - var haveVideoDevice = videoSend ? videoExist : false; - if(!haveAudioDevice && !haveVideoDevice) { - // FIXME Should we really give up, or just assume recvonly for both? - pluginHandle.consentDialog(false); - callbacks.error('No capture device found'); - return false; - } else if(!haveAudioDevice && needAudioDevice) { - pluginHandle.consentDialog(false); - callbacks.error('Audio capture is required, but no capture device found'); - return false; - } else if(!haveVideoDevice && needVideoDevice) { - pluginHandle.consentDialog(false); - callbacks.error('Video capture is required, but no capture device found'); - return false; - } - } - - var gumConstraints = { - audio: (audioExist && !media.keepAudio) ? audioSupport : false, - video: (videoExist && !media.keepVideo) ? videoSupport : false - }; - Janus.debug("getUserMedia constraints", gumConstraints); - if (!gumConstraints.audio && !gumConstraints.video) { - pluginHandle.consentDialog(false); - streamsDone(handleId, jsep, media, callbacks, stream); - } else { - navigator.mediaDevices.getUserMedia(gumConstraints) - .then(function(stream) { - pluginHandle.consentDialog(false); - streamsDone(handleId, jsep, media, callbacks, stream); - }).catch(function(error) { - pluginHandle.consentDialog(false); - callbacks.error({code: error.code, name: error.name, message: error.message}); - }); - } - }) - .catch(function(error) { - pluginHandle.consentDialog(false); - callbacks.error(error); - }); - } - } else { - // No need to do a getUserMedia, create offer/answer right away - streamsDone(handleId, jsep, media, callbacks); - } - } - - function prepareWebrtcPeer(handleId, callbacks) { - callbacks = callbacks || {}; - callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; - callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : webrtcError; - callbacks.customizeSdp = (typeof callbacks.customizeSdp == "function") ? callbacks.customizeSdp : Janus.noop; - var jsep = callbacks.jsep; - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - callbacks.error("Invalid handle"); - return; - } - var config = pluginHandle.webrtcStuff; - if(jsep) { - if(!config.pc) { - Janus.warn("Wait, no PeerConnection?? if this is an answer, use createAnswer and not handleRemoteJsep"); - callbacks.error("No PeerConnection: if this is an answer, use createAnswer and not handleRemoteJsep"); - return; - } - callbacks.customizeSdp(jsep); - config.pc.setRemoteDescription(jsep) - .then(function() { - Janus.log("Remote description accepted!"); - config.remoteSdp = jsep.sdp; - // Any trickle candidate we cached? - if(config.candidates && config.candidates.length > 0) { - for(var i = 0; i< config.candidates.length; i++) { - var candidate = config.candidates[i]; - Janus.debug("Adding remote candidate:", candidate); - if(!candidate || candidate.completed === true) { - // end-of-candidates - config.pc.addIceCandidate(Janus.endOfCandidates); - } else { - // New candidate - config.pc.addIceCandidate(candidate); - } - } - config.candidates = []; - } - // Done - callbacks.success(); - }, callbacks.error); - } else { - callbacks.error("Invalid JSEP"); - } - } - - function createOffer(handleId, media, callbacks) { - callbacks = callbacks || {}; - callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; - callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; - callbacks.customizeSdp = (typeof callbacks.customizeSdp == "function") ? callbacks.customizeSdp : Janus.noop; - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - callbacks.error("Invalid handle"); - return; - } - var config = pluginHandle.webrtcStuff; - var simulcast = (callbacks.simulcast === true); - if(!simulcast) { - Janus.log("Creating offer (iceDone=" + config.iceDone + ")"); - } else { - Janus.log("Creating offer (iceDone=" + config.iceDone + ", simulcast=" + simulcast + ")"); - } - // https://code.google.com/p/webrtc/issues/detail?id=3508 - var mediaConstraints = {}; - if(Janus.unifiedPlan) { - // We can use Transceivers - var audioTransceiver = null, videoTransceiver = null; - var transceivers = config.pc.getTransceivers(); - if(transceivers && transceivers.length > 0) { - for(var t of transceivers) { - if((t.sender && t.sender.track && t.sender.track.kind === "audio") || - (t.receiver && t.receiver.track && t.receiver.track.kind === "audio")) { - if(!audioTransceiver) { - audioTransceiver = t; - } - continue; - } - if((t.sender && t.sender.track && t.sender.track.kind === "video") || - (t.receiver && t.receiver.track && t.receiver.track.kind === "video")) { - if(!videoTransceiver) { - videoTransceiver = t; - } - continue; - } - } - } - // Handle audio (and related changes, if any) - var audioSend = isAudioSendEnabled(media); - var audioRecv = isAudioRecvEnabled(media); - if(!audioSend && !audioRecv) { - // Audio disabled: have we removed it? - if(media.removeAudio && audioTransceiver) { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection("inactive"); - } else { - audioTransceiver.direction = "inactive"; - } - Janus.log("Setting audio transceiver to inactive:", audioTransceiver); - } - } else { - // Take care of audio m-line - if(audioSend && audioRecv) { - if(audioTransceiver) { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection("sendrecv"); - } else { - audioTransceiver.direction = "sendrecv"; - } - Janus.log("Setting audio transceiver to sendrecv:", audioTransceiver); - } - } else if(audioSend && !audioRecv) { - if(audioTransceiver) { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection("sendonly"); - } else { - audioTransceiver.direction = "sendonly"; - } - Janus.log("Setting audio transceiver to sendonly:", audioTransceiver); - } - } else if(!audioSend && audioRecv) { - if(audioTransceiver) { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection("recvonly"); - } else { - audioTransceiver.direction = "recvonly"; - } - Janus.log("Setting audio transceiver to recvonly:", audioTransceiver); - } else { - // In theory, this is the only case where we might not have a transceiver yet - audioTransceiver = config.pc.addTransceiver("audio", { direction: "recvonly" }); - Janus.log("Adding recvonly audio transceiver:", audioTransceiver); - } - } - } - // Handle video (and related changes, if any) - var videoSend = isVideoSendEnabled(media); - var videoRecv = isVideoRecvEnabled(media); - if(!videoSend && !videoRecv) { - // Video disabled: have we removed it? - if(media.removeVideo && videoTransceiver) { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection("inactive"); - } else { - videoTransceiver.direction = "inactive"; - } - Janus.log("Setting video transceiver to inactive:", videoTransceiver); - } - } else { - // Take care of video m-line - if(videoSend && videoRecv) { - if(videoTransceiver) { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection("sendrecv"); - } else { - videoTransceiver.direction = "sendrecv"; - } - Janus.log("Setting video transceiver to sendrecv:", videoTransceiver); - } - } else if(videoSend && !videoRecv) { - if(videoTransceiver) { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection("sendonly"); - } else { - videoTransceiver.direction = "sendonly"; - } - Janus.log("Setting video transceiver to sendonly:", videoTransceiver); - } - } else if(!videoSend && videoRecv) { - if(videoTransceiver) { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection("recvonly"); - } else { - videoTransceiver.direction = "recvonly"; - } - Janus.log("Setting video transceiver to recvonly:", videoTransceiver); - } else { - // In theory, this is the only case where we might not have a transceiver yet - videoTransceiver = config.pc.addTransceiver("video", { direction: "recvonly" }); - Janus.log("Adding recvonly video transceiver:", videoTransceiver); - } - } - } - } else { - mediaConstraints["offerToReceiveAudio"] = isAudioRecvEnabled(media); - mediaConstraints["offerToReceiveVideo"] = isVideoRecvEnabled(media); - } - var iceRestart = (callbacks.iceRestart === true); - if(iceRestart) { - mediaConstraints["iceRestart"] = true; - } - Janus.debug(mediaConstraints); - // Check if this is Firefox and we've been asked to do simulcasting - var sendVideo = isVideoSendEnabled(media); - if(sendVideo && simulcast && Janus.webRTCAdapter.browserDetails.browser === "firefox") { - // FIXME Based on https://gist.github.com/voluntas/088bc3cc62094730647b - Janus.log("Enabling Simulcasting for Firefox (RID)"); - var sender = config.pc.getSenders().find(function(s) {return s.track && s.track.kind === "video"}); - if(sender) { - var parameters = sender.getParameters(); - if(!parameters) { - parameters = {}; - } - var maxBitrates = getMaxBitrates(callbacks.simulcastMaxBitrates); - parameters.encodings = callbacks.sendEncodings || [ - { rid: "h", active: true, maxBitrate: maxBitrates.high }, - { rid: "m", active: true, maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2 }, - { rid: "l", active: true, maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4 } - ]; - sender.setParameters(parameters); - } - } - config.pc.createOffer(mediaConstraints) - .then(function(offer) { - Janus.debug(offer); - // JSON.stringify doesn't work on some WebRTC objects anymore - // See https://code.google.com/p/chromium/issues/detail?id=467366 - var jsep = { - "type": offer.type, - "sdp": offer.sdp - }; - callbacks.customizeSdp(jsep); - offer.sdp = jsep.sdp; - Janus.log("Setting local description"); - if(sendVideo && simulcast && !Janus.unifiedPlan) { - // We only do simulcast via SDP munging on older versions of Chrome and Safari - if(Janus.webRTCAdapter.browserDetails.browser === "chrome" || - Janus.webRTCAdapter.browserDetails.browser === "safari") { - Janus.log("Enabling Simulcasting for Chrome (SDP munging)"); - offer.sdp = mungeSdpForSimulcasting(offer.sdp); - } - } - config.mySdp = { - type: "offer", - sdp: offer.sdp - }; - config.pc.setLocalDescription(offer) - .catch(callbacks.error); - config.mediaConstraints = mediaConstraints; - if(!config.iceDone && !config.trickle) { - // Don't do anything until we have all candidates - Janus.log("Waiting for all candidates..."); - return; - } - // If transforms are present, notify Janus that the media is end-to-end encrypted - if(config.senderTransforms || config.receiverTransforms) { - offer["e2ee"] = true; - } - callbacks.success(offer); - }, callbacks.error); - } - - function createAnswer(handleId, media, callbacks) { - callbacks = callbacks || {}; - callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; - callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; - callbacks.customizeSdp = (typeof callbacks.customizeSdp == "function") ? callbacks.customizeSdp : Janus.noop; - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - callbacks.error("Invalid handle"); - return; - } - var config = pluginHandle.webrtcStuff; - var simulcast = (callbacks.simulcast === true || callbacks.simulcast2 === true); - if(!simulcast) { - Janus.log("Creating answer (iceDone=" + config.iceDone + ")"); - } else { - Janus.log("Creating answer (iceDone=" + config.iceDone + ", simulcast=" + simulcast + ")"); - } - var mediaConstraints = null; - if(Janus.unifiedPlan) { - // We can use Transceivers - mediaConstraints = {}; - var audioTransceiver = null, videoTransceiver = null; - var transceivers = config.pc.getTransceivers(); - if(transceivers && transceivers.length > 0) { - for(var t of transceivers) { - if((t.sender && t.sender.track && t.sender.track.kind === "audio") || - (t.receiver && t.receiver.track && t.receiver.track.kind === "audio")) { - if(!audioTransceiver) - audioTransceiver = t; - continue; - } - if((t.sender && t.sender.track && t.sender.track.kind === "video") || - (t.receiver && t.receiver.track && t.receiver.track.kind === "video")) { - if(!videoTransceiver) - videoTransceiver = t; - continue; - } - } - } - // Handle audio (and related changes, if any) - var audioSend = isAudioSendEnabled(media); - var audioRecv = isAudioRecvEnabled(media); - if(!audioSend && !audioRecv) { - // Audio disabled: have we removed it? - if(media.removeAudio && audioTransceiver) { - try { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection("inactive"); - } else { - audioTransceiver.direction = "inactive"; - } - Janus.log("Setting audio transceiver to inactive:", audioTransceiver); - } catch(e) { - Janus.error(e); - } - } - } else { - // Take care of audio m-line - if(audioSend && audioRecv) { - if(audioTransceiver) { - try { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection("sendrecv"); - } else { - audioTransceiver.direction = "sendrecv"; - } - Janus.log("Setting audio transceiver to sendrecv:", audioTransceiver); - } catch(e) { - Janus.error(e); - } - } - } else if(audioSend && !audioRecv) { - try { - if(audioTransceiver) { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection("sendonly"); - } else { - audioTransceiver.direction = "sendonly"; - } - Janus.log("Setting audio transceiver to sendonly:", audioTransceiver); - } - } catch(e) { - Janus.error(e); - } - } else if(!audioSend && audioRecv) { - if(audioTransceiver) { - try { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection("recvonly"); - } else { - audioTransceiver.direction = "recvonly"; - } - Janus.log("Setting audio transceiver to recvonly:", audioTransceiver); - } catch(e) { - Janus.error(e); - } - } else { - // In theory, this is the only case where we might not have a transceiver yet - audioTransceiver = config.pc.addTransceiver("audio", { direction: "recvonly" }); - Janus.log("Adding recvonly audio transceiver:", audioTransceiver); - } - } - } - // Handle video (and related changes, if any) - var videoSend = isVideoSendEnabled(media); - var videoRecv = isVideoRecvEnabled(media); - if(!videoSend && !videoRecv) { - // Video disabled: have we removed it? - if(media.removeVideo && videoTransceiver) { - try { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection("inactive"); - } else { - videoTransceiver.direction = "inactive"; - } - Janus.log("Setting video transceiver to inactive:", videoTransceiver); - } catch(e) { - Janus.error(e); - } - } - } else { - // Take care of video m-line - if(videoSend && videoRecv) { - if(videoTransceiver) { - try { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection("sendrecv"); - } else { - videoTransceiver.direction = "sendrecv"; - } - Janus.log("Setting video transceiver to sendrecv:", videoTransceiver); - } catch(e) { - Janus.error(e); - } - } - } else if(videoSend && !videoRecv) { - if(videoTransceiver) { - try { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection("sendonly"); - } else { - videoTransceiver.direction = "sendonly"; - } - Janus.log("Setting video transceiver to sendonly:", videoTransceiver); - } catch(e) { - Janus.error(e); - } - } - } else if(!videoSend && videoRecv) { - if(videoTransceiver) { - try { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection("recvonly"); - } else { - videoTransceiver.direction = "recvonly"; - } - Janus.log("Setting video transceiver to recvonly:", videoTransceiver); - } catch(e) { - Janus.error(e); - } - } else { - // In theory, this is the only case where we might not have a transceiver yet - videoTransceiver = config.pc.addTransceiver("video", { direction: "recvonly" }); - Janus.log("Adding recvonly video transceiver:", videoTransceiver); - } - } - } - } else { - if(Janus.webRTCAdapter.browserDetails.browser === "firefox" || Janus.webRTCAdapter.browserDetails.browser === "edge") { - mediaConstraints = { - offerToReceiveAudio: isAudioRecvEnabled(media), - offerToReceiveVideo: isVideoRecvEnabled(media) - }; - } else { - mediaConstraints = { - mandatory: { - OfferToReceiveAudio: isAudioRecvEnabled(media), - OfferToReceiveVideo: isVideoRecvEnabled(media) - } - }; - } - } - Janus.debug(mediaConstraints); - // Check if this is Firefox and we've been asked to do simulcasting - var sendVideo = isVideoSendEnabled(media); - if(sendVideo && simulcast && Janus.webRTCAdapter.browserDetails.browser === "firefox") { - // FIXME Based on https://gist.github.com/voluntas/088bc3cc62094730647b - Janus.log("Enabling Simulcasting for Firefox (RID)"); - var sender = config.pc.getSenders()[1]; - Janus.log(sender); - var parameters = sender.getParameters(); - Janus.log(parameters); - - var maxBitrates = getMaxBitrates(callbacks.simulcastMaxBitrates); - sender.setParameters({encodings: callbacks.sendEncodings || [ - { rid: "h", active: true, maxBitrate: maxBitrates.high }, - { rid: "m", active: true, maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2}, - { rid: "l", active: true, maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4} - ]}); - } - config.pc.createAnswer(mediaConstraints) - .then(function(answer) { - Janus.debug(answer); - // JSON.stringify doesn't work on some WebRTC objects anymore - // See https://code.google.com/p/chromium/issues/detail?id=467366 - var jsep = { - "type": answer.type, - "sdp": answer.sdp - }; - callbacks.customizeSdp(jsep); - answer.sdp = jsep.sdp; - Janus.log("Setting local description"); - if(sendVideo && simulcast && !Janus.unifiedPlan) { - // We only do simulcast via SDP munging on older versions of Chrome and Safari - if(Janus.webRTCAdapter.browserDetails.browser === "chrome") { - // FIXME Apparently trying to simulcast when answering breaks video in Chrome... - //~ Janus.log("Enabling Simulcasting for Chrome (SDP munging)"); - //~ answer.sdp = mungeSdpForSimulcasting(answer.sdp); - Janus.warn("simulcast=true, but this is an answer, and video breaks in Chrome if we enable it"); - } - } - config.mySdp = { - type: "answer", - sdp: answer.sdp - }; - config.pc.setLocalDescription(answer) - .catch(callbacks.error); - config.mediaConstraints = mediaConstraints; - if(!config.iceDone && !config.trickle) { - // Don't do anything until we have all candidates - Janus.log("Waiting for all candidates..."); - return; - } - // If transforms are present, notify Janus that the media is end-to-end encrypted - if(config.senderTransforms || config.receiverTransforms) { - answer["e2ee"] = true; - } - callbacks.success(answer); - }, callbacks.error); - } - - function sendSDP(handleId, callbacks) { - callbacks = callbacks || {}; - callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; - callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle, not sending anything"); - return; - } - var config = pluginHandle.webrtcStuff; - Janus.log("Sending offer/answer SDP..."); - if(!config.mySdp) { - Janus.warn("Local SDP instance is invalid, not sending anything..."); - return; - } - config.mySdp = { - "type": config.pc.localDescription.type, - "sdp": config.pc.localDescription.sdp - }; - if(config.trickle === false) - config.mySdp["trickle"] = false; - Janus.debug(callbacks); - config.sdpSent = true; - callbacks.success(config.mySdp); - } - - function getVolume(handleId, remote) { - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - return 0; - } - var stream = remote ? "remote" : "local"; - var config = pluginHandle.webrtcStuff; - if(!config.volume[stream]) - config.volume[stream] = { value: 0 }; - // Start getting the volume, if audioLevel in getStats is supported (apparently - // they're only available in Chrome/Safari right now: https://webrtc-stats.callstats.io/) - if(config.pc.getStats && (Janus.webRTCAdapter.browserDetails.browser === "chrome" || - Janus.webRTCAdapter.browserDetails.browser === "safari")) { - if(remote && !config.remoteStream) { - Janus.warn("Remote stream unavailable"); - return 0; - } else if(!remote && !config.myStream) { - Janus.warn("Local stream unavailable"); - return 0; - } - if(!config.volume[stream].timer) { - Janus.log("Starting " + stream + " volume monitor"); - config.volume[stream].timer = setInterval(function() { - config.pc.getStats() - .then(function(stats) { - stats.forEach(function (res) { - if(!res || res.kind !== "audio") - return; - if((remote && !res.remoteSource) || (!remote && res.type !== "media-source")) - return; - config.volume[stream].value = (res.audioLevel ? res.audioLevel : 0); - }); - }); - }, 200); - return 0; // We don't have a volume to return yet - } - return config.volume[stream].value; - } else { - // audioInputLevel and audioOutputLevel seem only available in Chrome? audioLevel - // seems to be available on Chrome and Firefox, but they don't seem to work - Janus.warn("Getting the " + stream + " volume unsupported by browser"); - return 0; - } - } - - function isMuted(handleId, video) { - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - return true; - } - var config = pluginHandle.webrtcStuff; - if(!config.pc) { - Janus.warn("Invalid PeerConnection"); - return true; - } - if(!config.myStream) { - Janus.warn("Invalid local MediaStream"); - return true; - } - if(video) { - // Check video track - if(!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) { - Janus.warn("No video track"); - return true; - } - return !config.myStream.getVideoTracks()[0].enabled; - } else { - // Check audio track - if(!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) { - Janus.warn("No audio track"); - return true; - } - return !config.myStream.getAudioTracks()[0].enabled; - } - } - - function mute(handleId, video, mute) { - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - return false; - } - var config = pluginHandle.webrtcStuff; - if(!config.pc) { - Janus.warn("Invalid PeerConnection"); - return false; - } - if(!config.myStream) { - Janus.warn("Invalid local MediaStream"); - return false; - } - if(video) { - // Mute/unmute video track - if(!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) { - Janus.warn("No video track"); - return false; - } - config.myStream.getVideoTracks()[0].enabled = !mute; - return true; - } else { - // Mute/unmute audio track - if(!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) { - Janus.warn("No audio track"); - return false; - } - config.myStream.getAudioTracks()[0].enabled = !mute; - return true; - } - } - - function getBitrate(handleId) { - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - return "Invalid handle"; - } - var config = pluginHandle.webrtcStuff; - if(!config.pc) - return "Invalid PeerConnection"; - // Start getting the bitrate, if getStats is supported - if(config.pc.getStats) { - if(!config.bitrate.timer) { - Janus.log("Starting bitrate timer (via getStats)"); - config.bitrate.timer = setInterval(function() { - config.pc.getStats() - .then(function(stats) { - stats.forEach(function (res) { - if(!res) - return; - var inStats = false; - // Check if these are statistics on incoming media - if((res.mediaType === "video" || res.id.toLowerCase().indexOf("video") > -1) && - res.type === "inbound-rtp" && res.id.indexOf("rtcp") < 0) { - // New stats - inStats = true; - } else if(res.type == 'ssrc' && res.bytesReceived && - (res.googCodecName === "VP8" || res.googCodecName === "")) { - // Older Chromer versions - inStats = true; - } - // Parse stats now - if(inStats) { - config.bitrate.bsnow = res.bytesReceived; - config.bitrate.tsnow = res.timestamp; - if(config.bitrate.bsbefore === null || config.bitrate.tsbefore === null) { - // Skip this round - config.bitrate.bsbefore = config.bitrate.bsnow; - config.bitrate.tsbefore = config.bitrate.tsnow; - } else { - // Calculate bitrate - var timePassed = config.bitrate.tsnow - config.bitrate.tsbefore; - if(Janus.webRTCAdapter.browserDetails.browser === "safari") - timePassed = timePassed/1000; // Apparently the timestamp is in microseconds, in Safari - var bitRate = Math.round((config.bitrate.bsnow - config.bitrate.bsbefore) * 8 / timePassed); - if(Janus.webRTCAdapter.browserDetails.browser === "safari") - bitRate = parseInt(bitRate/1000); - config.bitrate.value = bitRate + ' kbits/sec'; - //~ Janus.log("Estimated bitrate is " + config.bitrate.value); - config.bitrate.bsbefore = config.bitrate.bsnow; - config.bitrate.tsbefore = config.bitrate.tsnow; - } - } - }); - }); - }, 1000); - return "0 kbits/sec"; // We don't have a bitrate value yet - } - return config.bitrate.value; - } else { - Janus.warn("Getting the video bitrate unsupported by browser"); - return "Feature unsupported by browser"; - } - } - - function webrtcError(error) { - Janus.error("WebRTC error:", error); - } - - function cleanupWebrtc(handleId, hangupRequest) { - Janus.log("Cleaning WebRTC stuff"); - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle) { - // Nothing to clean - return; - } - var config = pluginHandle.webrtcStuff; - if(config) { - if(hangupRequest === true) { - // Send a hangup request (we don't really care about the response) - var request = { "janus": "hangup", "transaction": Janus.randomString(12) }; - if(pluginHandle.token) - request["token"] = pluginHandle.token; - if(apisecret) - request["apisecret"] = apisecret; - Janus.debug("Sending hangup request (handle=" + handleId + "):"); - Janus.debug(request); - if(websockets) { - request["session_id"] = sessionId; - request["handle_id"] = handleId; - ws.send(JSON.stringify(request)); - } else { - Janus.httpAPICall(server + "/" + sessionId + "/" + handleId, { - verb: 'POST', - withCredentials: withCredentials, - body: request - }); - } - } - // Cleanup stack - config.remoteStream = null; - if(config.volume) { - if(config.volume["local"] && config.volume["local"].timer) - clearInterval(config.volume["local"].timer); - if(config.volume["remote"] && config.volume["remote"].timer) - clearInterval(config.volume["remote"].timer); - } - config.volume = {}; - if(config.bitrate.timer) - clearInterval(config.bitrate.timer); - config.bitrate.timer = null; - config.bitrate.bsnow = null; - config.bitrate.bsbefore = null; - config.bitrate.tsnow = null; - config.bitrate.tsbefore = null; - config.bitrate.value = null; - if(!config.streamExternal && config.myStream) { - Janus.log("Stopping local stream tracks"); - Janus.stopAllTracks(config.myStream); - } - config.streamExternal = false; - config.myStream = null; - // Close PeerConnection - try { - config.pc.close(); - } catch(e) { - // Do nothing - } - config.pc = null; - config.candidates = null; - config.mySdp = null; - config.remoteSdp = null; - config.iceDone = false; - config.dataChannel = {}; - config.dtmfSender = null; - config.senderTransforms = null; - config.receiverTransforms = null; - } - pluginHandle.oncleanup(); - } - - // Helper method to munge an SDP to enable simulcasting (Chrome only) - function mungeSdpForSimulcasting(sdp) { - // Let's munge the SDP to add the attributes for enabling simulcasting - // (based on https://gist.github.com/ggarber/a19b4c33510028b9c657) - var lines = sdp.split("\r\n"); - var video = false; - var ssrc = [ -1 ], ssrc_fid = [ -1 ]; - var cname = null, msid = null, mslabel = null, label = null; - var insertAt = -1; - for(let i=0; i -1) { - // We're done, let's add the new attributes here - insertAt = i; - break; - } - } - continue; - } - if(!video) - continue; - var sim = lines[i].match(/a=ssrc-group:SIM (\d+) (\d+) (\d+)/); - if(sim) { - Janus.warn("The SDP already contains a SIM attribute, munging will be skipped"); - return sdp; - } - var fid = lines[i].match(/a=ssrc-group:FID (\d+) (\d+)/); - if(fid) { - ssrc[0] = fid[1]; - ssrc_fid[0] = fid[2]; - lines.splice(i, 1); i--; - continue; - } - if(ssrc[0]) { - var match = lines[i].match('a=ssrc:' + ssrc[0] + ' cname:(.+)') - if(match) { - cname = match[1]; - } - match = lines[i].match('a=ssrc:' + ssrc[0] + ' msid:(.+)') - if(match) { - msid = match[1]; - } - match = lines[i].match('a=ssrc:' + ssrc[0] + ' mslabel:(.+)') - if(match) { - mslabel = match[1]; - } - match = lines[i].match('a=ssrc:' + ssrc[0] + ' label:(.+)') - if(match) { - label = match[1]; - } - if(lines[i].indexOf('a=ssrc:' + ssrc_fid[0]) === 0) { - lines.splice(i, 1); i--; - continue; - } - if(lines[i].indexOf('a=ssrc:' + ssrc[0]) === 0) { - lines.splice(i, 1); i--; - continue; - } - } - if(lines[i].length == 0) { - lines.splice(i, 1); i--; - continue; - } - } - if(ssrc[0] < 0) { - // Couldn't find a FID attribute, let's just take the first video SSRC we find - insertAt = -1; - video = false; - for(let i=0; i -1) { - // We're done, let's add the new attributes here - insertAt = i; - break; - } - } - continue; - } - if(!video) - continue; - if(ssrc[0] < 0) { - var value = lines[i].match(/a=ssrc:(\d+)/); - if(value) { - ssrc[0] = value[1]; - lines.splice(i, 1); i--; - continue; - } - } else { - let match = lines[i].match('a=ssrc:' + ssrc[0] + ' cname:(.+)') - if(match) { - cname = match[1]; - } - match = lines[i].match('a=ssrc:' + ssrc[0] + ' msid:(.+)') - if(match) { - msid = match[1]; - } - match = lines[i].match('a=ssrc:' + ssrc[0] + ' mslabel:(.+)') - if(match) { - mslabel = match[1]; - } - match = lines[i].match('a=ssrc:' + ssrc[0] + ' label:(.+)') - if(match) { - label = match[1]; - } - if(lines[i].indexOf('a=ssrc:' + ssrc_fid[0]) === 0) { - lines.splice(i, 1); i--; - continue; - } - if(lines[i].indexOf('a=ssrc:' + ssrc[0]) === 0) { - lines.splice(i, 1); i--; - continue; - } - } - if(lines[i].length === 0) { - lines.splice(i, 1); i--; - continue; - } - } - } - if(ssrc[0] < 0) { - // Still nothing, let's just return the SDP we were asked to munge - Janus.warn("Couldn't find the video SSRC, simulcasting NOT enabled"); - return sdp; - } - if(insertAt < 0) { - // Append at the end - insertAt = lines.length; - } - // Generate a couple of SSRCs (for retransmissions too) - // Note: should we check if there are conflicts, here? - ssrc[1] = Math.floor(Math.random()*0xFFFFFFFF); - ssrc[2] = Math.floor(Math.random()*0xFFFFFFFF); - ssrc_fid[1] = Math.floor(Math.random()*0xFFFFFFFF); - ssrc_fid[2] = Math.floor(Math.random()*0xFFFFFFFF); - // Add attributes to the SDP - for(var i=0; i # -# # -# 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 . # -# # -# ========================================================================== # diff --git a/testenv/tests/apps/__init__.py b/testenv/tests/apps/__init__.py deleted file mode 100644 index 8d45fdfd..00000000 --- a/testenv/tests/apps/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # diff --git a/testenv/tests/apps/htpasswd/__init__.py b/testenv/tests/apps/htpasswd/__init__.py deleted file mode 100644 index 8d45fdfd..00000000 --- a/testenv/tests/apps/htpasswd/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # diff --git a/testenv/tests/apps/htpasswd/test_main.py b/testenv/tests/apps/htpasswd/test_main.py deleted file mode 100644 index ba45110b..00000000 --- a/testenv/tests/apps/htpasswd/test_main.py +++ /dev/null @@ -1,169 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import os -import 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) diff --git a/testenv/tests/apps/kvmd/__init__.py b/testenv/tests/apps/kvmd/__init__.py deleted file mode 100644 index 8d45fdfd..00000000 --- a/testenv/tests/apps/kvmd/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # diff --git a/testenv/tests/apps/kvmd/test_auth.py b/testenv/tests/apps/kvmd/test_auth.py deleted file mode 100644 index 4fa1c8ae..00000000 --- a/testenv/tests/apps/kvmd/test_auth.py +++ /dev/null @@ -1,223 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import os -import 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() diff --git a/testenv/tests/keyboard/__init__.py b/testenv/tests/keyboard/__init__.py deleted file mode 100644 index 8d45fdfd..00000000 --- a/testenv/tests/keyboard/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # diff --git a/testenv/tests/keyboard/test_keymap.py b/testenv/tests/keyboard/test_keymap.py deleted file mode 100644 index 8c5b3312..00000000 --- a/testenv/tests/keyboard/test_keymap.py +++ /dev/null @@ -1,35 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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"]) diff --git a/testenv/tests/plugins/__init__.py b/testenv/tests/plugins/__init__.py deleted file mode 100644 index 8d45fdfd..00000000 --- a/testenv/tests/plugins/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # diff --git a/testenv/tests/plugins/auth/__init__.py b/testenv/tests/plugins/auth/__init__.py deleted file mode 100644 index 3ddb5061..00000000 --- a/testenv/tests/plugins/auth/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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() diff --git a/testenv/tests/plugins/auth/test_htpasswd.py b/testenv/tests/plugins/auth/test_htpasswd.py deleted file mode 100644 index 12d40b23..00000000 --- a/testenv/tests/plugins/auth/test_htpasswd.py +++ /dev/null @@ -1,54 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import os - -import 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")) diff --git a/testenv/tests/plugins/auth/test_http.py b/testenv/tests/plugins/auth/test_http.py deleted file mode 100644 index 252ad85b..00000000 --- a/testenv/tests/plugins/auth/test_http.py +++ /dev/null @@ -1,79 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -from 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")) diff --git a/testenv/tests/plugins/auth/test_pam.py b/testenv/tests/plugins/auth/test_pam.py deleted file mode 100644 index 26bf11be..00000000 --- a/testenv/tests/plugins/auth/test_pam.py +++ /dev/null @@ -1,93 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import os -import 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)) diff --git a/testenv/tests/test_aiotools.py b/testenv/tests/test_aiotools.py deleted file mode 100644 index 8bb0ccea..00000000 --- a/testenv/tests/test_aiotools.py +++ /dev/null @@ -1,149 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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"] diff --git a/testenv/tests/test_logging.py b/testenv/tests/test_logging.py deleted file mode 100644 index b6f52a38..00000000 --- a/testenv/tests/test_logging.py +++ /dev/null @@ -1,35 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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 diff --git a/testenv/tests/test_yamlconf.py b/testenv/tests/test_yamlconf.py deleted file mode 100644 index 70f9593f..00000000 --- a/testenv/tests/test_yamlconf.py +++ /dev/null @@ -1,42 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import 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" diff --git a/testenv/tests/validators/__init__.py b/testenv/tests/validators/__init__.py deleted file mode 100644 index 8d45fdfd..00000000 --- a/testenv/tests/validators/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # diff --git a/testenv/tests/validators/test_auth.py b/testenv/tests/validators/test_auth.py deleted file mode 100644 index d84e029b..00000000 --- a/testenv/tests/validators/test_auth.py +++ /dev/null @@ -1,130 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -from 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)) diff --git a/testenv/tests/validators/test_basic.py b/testenv/tests/validators/test_basic.py deleted file mode 100644 index 7551e4bb..00000000 --- a/testenv/tests/validators/test_basic.py +++ /dev/null @@ -1,174 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -from 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)) diff --git a/testenv/tests/validators/test_hid.py b/testenv/tests/validators/test_hid.py deleted file mode 100644 index a1031a13..00000000 --- a/testenv/tests/validators/test_hid.py +++ /dev/null @@ -1,98 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -from 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)) diff --git a/testenv/tests/validators/test_hw.py b/testenv/tests/validators/test_hw.py deleted file mode 100644 index b73af65e..00000000 --- a/testenv/tests/validators/test_hw.py +++ /dev/null @@ -1,132 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -from 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)) diff --git a/testenv/tests/validators/test_kvm.py b/testenv/tests/validators/test_kvm.py deleted file mode 100644 index b7ea6365..00000000 --- a/testenv/tests/validators/test_kvm.py +++ /dev/null @@ -1,210 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -from 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)) diff --git a/testenv/tests/validators/test_net.py b/testenv/tests/validators/test_net.py deleted file mode 100644 index a67fbeff..00000000 --- a/testenv/tests/validators/test_net.py +++ /dev/null @@ -1,210 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -from 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)) diff --git a/testenv/tests/validators/test_os.py b/testenv/tests/validators/test_os.py deleted file mode 100644 index 8fb5236b..00000000 --- a/testenv/tests/validators/test_os.py +++ /dev/null @@ -1,159 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import os - -from typing import Any - -import pytest - -from kvmd.validators import ValidatorError -from kvmd.validators.os import valid_abs_path -from kvmd.validators.os import valid_printable_filename -from kvmd.validators.os import valid_unix_mode -from kvmd.validators.os import valid_command - - -# ===== -@pytest.mark.parametrize("arg, retval", [ - ("/..", "/"), - ("/root/..", "/"), - ("/root", "/root"), - ("/f/o/o/b/a/r", "/f/o/o/b/a/r"), - ("~", os.path.abspath(".") + "/~"), - ("/foo~", "/foo~"), - ("/foo/~", "/foo/~"), - (".", os.path.abspath(".")), -]) -def test_ok__valid_abs_path(arg: Any, retval: str) -> None: - assert valid_abs_path(arg) == retval - - -@pytest.mark.parametrize("arg", ["", " ", None]) -def test_fail__valid_abs_path(arg: Any) -> None: - with pytest.raises(ValidatorError): - print(valid_abs_path(arg)) - - -# ===== -@pytest.mark.parametrize("arg, retval", [ - ("/..", "/"), - ("/root/..", "/"), - ("/root", "/root"), - (".", os.path.abspath(".")), -]) -def test_ok__valid_abs_path__dir(arg: Any, retval: str) -> None: - assert valid_abs_path(arg, type="dir") == retval - - -@pytest.mark.parametrize("arg", [ - "/etc/passwd", - "/etc/passwd/", - "~", - "/foo~", - "/foo/~", - "", - None, -]) -def test_fail__valid_abs_path__dir(arg: Any) -> None: - with pytest.raises(ValidatorError): - print(valid_abs_path(arg, type="dir")) - - -# ===== -@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$"), -]) -def test_ok__valid_printable_filename(arg: Any, retval: str) -> None: - assert valid_printable_filename(arg) == retval - - -@pytest.mark.parametrize("arg", [ - ".", - "..", - " ..", - "test/", - "/test", - "../test", - ".test", - "./.", - "../.", - "./..", - "../..", - "/ ..", - ".. /", - "/.. /", - "lost+found", - "", - " ", - None, -]) -def test_fail__valid_printable_filename(arg: Any) -> None: - with pytest.raises(ValidatorError): - valid_printable_filename(arg) - - -# ===== -@pytest.mark.parametrize("arg", [0, 5, "1000"]) -def test_ok__valid_unix_mode(arg: Any) -> None: - value = valid_unix_mode(arg) - assert type(value) is int # pylint: disable=unidiomatic-typecheck - assert value == int(str(value).strip()) - - -@pytest.mark.parametrize("arg", ["test", "", None, -6, "-6", "5.0"]) -def test_fail__valid_unix_mode(arg: Any) -> None: - with pytest.raises(ValidatorError): - print(valid_unix_mode(arg)) - - -# ===== -@pytest.mark.parametrize("arg, retval", [ - (["/bin/true"], ["/bin/true"]), - (["/bin/true", 1, 2, 3], ["/bin/true", "1", "2", "3"]), - ("/bin/true, 1, 2, 3,", ["/bin/true", "1", "2", "3"]), - ("/bin/true", ["/bin/true"]), -]) -def test_ok__valid_command(arg: Any, retval: list[str]) -> None: - assert valid_command(arg) == retval - - -@pytest.mark.parametrize("arg", [ - ["/bin/blahblahblah"], - ["/bin/blahblahblah", 1, 2, 3], - [" "], - [], - "/bin/blahblahblah, 1, 2, 3,", - "/bin/blahblahblah", - " ", -]) -def test_fail__valid_command(arg: Any) -> None: - with pytest.raises(ValidatorError): - print(valid_command(arg)) diff --git a/testenv/tests/validators/test_ugpio.py b/testenv/tests/validators/test_ugpio.py deleted file mode 100644 index be29ea68..00000000 --- a/testenv/tests/validators/test_ugpio.py +++ /dev/null @@ -1,128 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -from typing import Callable -from typing import Any - -import pytest - -from kvmd.validators import ValidatorError -from kvmd.validators.ugpio import valid_ugpio_driver -from kvmd.validators.ugpio import valid_ugpio_channel -from kvmd.validators.ugpio import valid_ugpio_mode -from kvmd.validators.ugpio import valid_ugpio_view_title -from kvmd.validators.ugpio import valid_ugpio_view_table - -from kvmd.plugins.ugpio import UserGpioModes - - -# ===== -@pytest.mark.parametrize("validator", [valid_ugpio_driver, valid_ugpio_channel]) -@pytest.mark.parametrize("arg", [ - "test-", - "glados", - "test", - "_", - "_foo_bar_", - " aix", - "a" * 255, -]) -def test_ok__valid_ugpio_item(validator: Callable[[Any], str], arg: Any) -> None: - assert validator(arg) == arg.strip() - - -@pytest.mark.parametrize("validator", [valid_ugpio_driver, valid_ugpio_channel]) -@pytest.mark.parametrize("arg", [ - "тест", - "-molestia", - "te~st", - "-", - "-foo_bar", - "foo bar", - "a" * 256, - " ", - "", - None, -]) -def test_fail__valid_ugpio_item(validator: Callable[[Any], str], arg: Any) -> None: - with pytest.raises(ValidatorError): - print(validator(arg)) - - -# ===== -@pytest.mark.parametrize("arg", ["foo", " bar", " baz "]) -def test_ok__valid_ugpio_driver_variants(arg: Any) -> None: - value = valid_ugpio_driver(arg, set(["foo", "bar", "baz"])) - assert type(value) is str # pylint: disable=unidiomatic-typecheck - assert value == str(arg).strip() - - -@pytest.mark.parametrize("arg", ["BAR", " ", "", None]) -def test_fail__valid_ugpio_driver_variants(arg: Any) -> None: - with pytest.raises(ValidatorError): - print(valid_ugpio_driver(arg, set(["foo", "bar", "baz"]))) - - -# ===== -@pytest.mark.parametrize("arg", ["Input ", " OUTPUT "]) -def test_ok__valid_ugpio_mode(arg: Any) -> None: - assert valid_ugpio_mode(arg, UserGpioModes.ALL) == arg.strip().lower() - - -@pytest.mark.parametrize("arg", ["test", "", None]) -def test_fail__valid_ugpio_mode(arg: Any) -> None: - with pytest.raises(ValidatorError): - print(valid_ugpio_mode(arg, UserGpioModes.ALL)) - - -# ===== -@pytest.mark.parametrize("arg,retval", [ - ([], []), - ("", ""), - ("ab", "ab"), - ([""], [""]), - ([[]], ["[]"]), - (["a", None], ["a", "None"]), -]) -def test_ok__valid_ugpio_view_title(arg: Any, retval: Any) -> None: - assert valid_ugpio_view_title(arg) == retval - - -# ===== -@pytest.mark.parametrize("arg,retval", [ - ([], []), - ({}, []), - ([[]], [[]]), - ([{}], [[]]), - ([[[]]], [["[]"]]), - ("", []), - ("ab", [["a"], ["b"]]), - ([[1, 2], [None], "ab", {}, [3, 4]], [["1", "2"], ["None"], ["a", "b"], [], ["3", "4"]]), -]) -def test_ok__valid_ugpio_view_table(arg: Any, retval: Any) -> None: - assert valid_ugpio_view_table(arg) == retval - - -@pytest.mark.parametrize("arg", [None, [None], 1]) -def test_fail__valid_ugpio_view_table(arg: Any) -> None: - with pytest.raises(ValidatorError): - print(valid_ugpio_view_table(arg)) diff --git a/testenv/tests/yamlconf/test_merger.py b/testenv/tests/yamlconf/test_merger.py deleted file mode 100644 index 0f876ce9..00000000 --- a/testenv/tests/yamlconf/test_merger.py +++ /dev/null @@ -1,164 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -import pytest - -from kvmd.yamlconf import merger - - -# ===== -def test_simple_override() -> None: - base = {"key1": "value1", "key2": "value2"} - incoming = {"key1": "new_value1"} - merger.yaml_merge(base, incoming) - assert base == {"key1": "new_value1", "key2": "value2"} - - -def test_nested_override() -> None: - base = {"key1": {"nested_key1": "value1"}, "key2": "value2"} - incoming = {"key1": {"nested_key1": "new_value1"}} - merger.yaml_merge(base, incoming) - assert base == {"key1": {"nested_key1": "new_value1"}, "key2": "value2"} - - -def test_dest_none() -> None: - base = None - incoming = {"key1": "value1"} - with pytest.raises(ValueError, match="destination cannot be None"): - merger.yaml_merge(base, incoming) # type: ignore[arg-type] - - -def test_src_none_or_empty() -> None: - base = {"key1": "value1"} - incoming = None - merger.yaml_merge(base, incoming) # type: ignore[arg-type] - assert base == {"key1": "value1"} - - base = {"key1": "value1"} - incoming2: dict = {} - merger.yaml_merge(base, incoming2) - assert base == {"key1": "value1"} - - -def test_merged_new_keys() -> None: - base = {"key1": "value1"} - incoming = {"key2": "value2"} - merger.yaml_merge(base, incoming) - assert base == {"key1": "value1", "key2": "value2"} - - -def test_dest_not_dict() -> None: - base = "I'm not a dict" - incoming = {"key1": "value1"} - with pytest.raises(TypeError, match="object does not support item assignment"): - merger.yaml_merge(base, incoming) # type: ignore[arg-type] - - -def test_src_not_dict() -> None: - base = {"key1": "value1"} - incoming = "I'm not a dict" - with pytest.raises(TypeError, match="string indices must be integers, not 'str'"): - merger.yaml_merge(base, incoming) # type: ignore[arg-type] - - -def test_nested_lists_overwrite() -> None: - base = {"key1": [1, 2, 3]} - incoming = {"key1": ["a", "b", "c"]} - merger.yaml_merge(base, incoming) - assert base == {"key1": ["a", "b", "c"]} - - -def test_same_information_rewrite() -> None: - base = {"key1": "value1", "key2": "value2"} - incoming = {"key1": "value1", "key2": "value2"} - merger.yaml_merge(base, incoming) - assert base == {"key1": "value1", "key2": "value2"} - - -def test_deeply_nested_dictionaries() -> None: - base = {"key1": {"nested_key1": {"deep_nested_key1": "value1"}}, "key2": "value2"} - incoming = {"key1": {"nested_key1": {"deep_nested_key1": "new_value1"}}} - merger.yaml_merge(base, incoming) - assert base == {"key1": {"nested_key1": {"deep_nested_key1": "new_value1"}}, "key2": "value2"} - - -def test_non_dict_values_in_source() -> None: - base = {"key1": "value1", "key2": "value2"} - incoming = {"key1": 123, "key2": ["value3", "value4"]} - merger.yaml_merge(base, incoming) - assert base == {"key1": 123, "key2": ["value3", "value4"]} - - -def test_empty_base() -> None: - base: dict = {} - incoming = {"key1": "value1"} - merger.yaml_merge(base, incoming) - assert base == {"key1": "value1"} - - -def test_none_values_in_source() -> None: - base = {"key1": "value1", "key2": "value2"} - incoming = {"key1": None, "key2": "new_value2"} - merger.yaml_merge(base, incoming) - assert base == {"key1": None, "key2": "new_value2"} - - -def test_key_not_present_in_incoming() -> None: - base = {"key1": "value1", "key2": "value2"} - incoming = {"key3": "value3"} - merger.yaml_merge(base, incoming) - assert base == {"key1": "value1", "key2": "value2", "key3": "value3"} - - -def test_mixed_nested_non_nested_keys() -> None: - base = {"key1": "value1", "key2": {"nested_key1": "value2"}} - incoming = {"key1": "new_value1", "key2": {"nested_key1": "new_value2"}} - merger.yaml_merge(base, incoming) - assert base == {"key1": "new_value1", "key2": {"nested_key1": "new_value2"}} - - -def test_additional_nested_keys_in_incoming() -> None: - base = {"key1": "value1", "key2": {"nested_key1": "value2"}} - incoming = {"key1": "new_value1", "key2": {"nested_key1": "new_value2", "nested_key2": "value3"}} - merger.yaml_merge(base, incoming) - assert base == {"key1": "new_value1", "key2": {"nested_key1": "new_value2", "nested_key2": "value3"}} - - -def test_override_nested_dict_with_non_dict() -> None: - base = {"key1": "value1", "key2": {"nested_key1": "value2"}} - incoming = {"key1": "new_value1", "key2": "new_value2"} - merger.yaml_merge(base, incoming) - assert base == {"key1": "new_value1", "key2": "new_value2"} - - -def test_multiple_value_types() -> None: - base = {"key1": 1, "key2": True, "key3": [1, 2, 3], "key4": {"nested_key1": "value1"}} - incoming = {"key1": 2, "key2": False, "key3": [4, 5, 6], "key4": {"nested_key1": "value2"}} - merger.yaml_merge(base, incoming) - assert base == {"key1": 2, "key2": False, "key3": [4, 5, 6], "key4": {"nested_key1": "value2"}} - - -def test_non_string_keys() -> None: - base: dict = {1: "value1", 2: "value2"} - incoming: dict = {1: "new_value1", 3: "value3"} - merger.yaml_merge(base, incoming) - assert base == {1: "new_value1", 2: "value2", 3: "value3"} diff --git a/testenv/tox.ini b/testenv/tox.ini deleted file mode 100644 index 5ca65f5d..00000000 --- a/testenv/tox.ini +++ /dev/null @@ -1,65 +0,0 @@ -[tox] -envlist = flake8, pylint, mypy, vulture, pytest, eslint, htmlhint, shellcheck -skipsdist = true - -[testenv] -basepython = python3.12 -sitepackages = true -changedir = /src - -[testenv:flake8] -allowlist_externals = bash -commands = bash -c 'flake8 --config=testenv/linters/flake8.ini kvmd testenv/tests *.py' -deps = - flake8 - flake8-quotes - -rrequirements.txt - -[testenv:pylint] -allowlist_externals = bash -commands = bash -c 'pylint -j0 --rcfile=testenv/linters/pylint.ini --output-format=colorized --reports=no kvmd testenv/tests *.py' -deps = - pylint - pytest - pytest-asyncio - aiohttp-basicauth - -rrequirements.txt - -[testenv:mypy] -allowlist_externals = bash -commands = bash -c 'mypy --config-file=testenv/linters/mypy.ini --cache-dir=testenv/.mypy_cache kvmd testenv/tests *.py' -deps = - mypy - -rrequirements.txt - -[testenv:vulture] -allowlist_externals = bash -commands = bash -c 'vulture --ignore-names=_format_P,Plugin --ignore-decorators=@exposed_http,@exposed_ws,@pytest.fixture kvmd testenv/tests *.py testenv/linters/vulture-wl.py' -deps = - vulture - -rrequirements.txt - -[testenv:pytest] -commands = py.test -vv --cov-config=testenv/linters/coverage.ini --cov-report=term-missing --cov=kvmd testenv/tests -setenv = - PYTHONPATH=/src -deps = - pytest - pytest-cov - pytest-mock - pytest-asyncio - pytest-aiohttp - aiohttp-basicauth - -rrequirements.txt - -[testenv:eslint] -allowlist_externals = eslint -commands = eslint --cache-location=/tmp --config=testenv/linters/eslintrc.js --color web/share/js - -[testenv:htmlhint] -allowlist_externals = htmlhint -commands = htmlhint --config=testenv/linters/htmlhint.json web/*.html web/*/*.html - -[testenv:shellcheck] -allowlist_externals = bash -commands = bash -c 'shellcheck --color=always kvmd.install scripts/*' diff --git a/testenv/v2-hdmi-rpi4.override.yaml b/testenv/v2-hdmi-rpi4.override.yaml deleted file mode 100644 index f8a301f1..00000000 --- a/testenv/v2-hdmi-rpi4.override.yaml +++ /dev/null @@ -1,171 +0,0 @@ -kvmd: - server: - unix_mode: 0666 - - atx: - device: /dev/kvmd-gpio - - hid: - keyboard: - device: /dev/null - mouse: - device: /dev/null -# absolute_win98_fix: true -# mouse_alt: -# device: /dev/null - noop: true - - msd: - remount_cmd: /bin/true - - streamer: - desired_fps: 30 - max_fps: 40 - cmd: - - "/usr/bin/ustreamer" - - "--device=/dev/kvmd-video" - - "--quality={quality}" - - "--desired-fps={desired_fps}" - - "--resolution=800x600" - - "--last-as-blank=0" - - "--unix={unix}" - - "--unix-rm" - - "--unix-mode=0666" - - "--exit-on-parent-death" - - "--process-name-prefix={process_name_prefix}" - - "--notify-parent" - - "--no-log-colors" - - gpio: - drivers: - __gpio__: - device: /dev/kvmd-gpio - __v4_locator__: - type: locator - device: /dev/kvmd-gpio - relay: - type: hidrelay - device: /dev/hidraw0 - cmd1: - type: cmd - cmd: [/bin/sleep, 5] - cmd2: - type: cmd - cmd: [/bin/ls, -l] - - scheme: - __v3_usb_breaker__: - pin: 5 - mode: output - initial: true - pulse: - delay: 0 - - __v4_locator__: - driver: __v4_locator__ - pin: 12 - mode: output - pulse: - delay: 0 - - __v4_const1__: - pin: 6 - mode: output - switch: false - pulse: false - - led1: - pin: 19 - mode: input - - led2: - pin: 16 - mode: input - - button1: - pin: 26 - mode: output - switch: false - - button2: - pin: 20 - mode: output - switch: false - - relay1: - pin: 0 - mode: output - initial: null - driver: relay - - relay2: - pin: 1 - mode: output - initial: null - driver: relay - pulse: - delay: 2 - max_delay: 5 - - cmd1: - pin: 0 - mode: output - driver: cmd1 - switch: false - - cmd2: - pin: 0 - mode: output - driver: cmd2 - switch: false - - view: - header: - title: Switches - table: - - ["#Generic GPIO leds"] - - [] - - ["#Test 1:", led1, button1] - - ["#Test 2:", led2, button2|confirm|Testing] - - [] - - ["#HID Relays /dev/hidraw0"] - - [] - - ["#Relay #1:", "relay1|confirm|Boop 0.1"] - - ["#Relay #2:", "relay2|Boop 2.0"] - - [] - - ["#Commands"] - - ["#Cmd #1:", "cmd1|confirm|Run 'sleep 5'"] - - ["#Cmd #2:", "cmd2|Run 'ls -l'"] - -pst: - remount_cmd: /bin/true - -vnc: - keymap: /usr/share/kvmd/keymaps/ru - - auth: - vncauth: - enabled: true - - memsink: - jpeg: - sink: "" - h264: - sink: "" - -otgnet: - commands: - post_start_cmd: - - "/bin/true" - pre_stop_cmd: - - "/bin/true" - -nginx: - http: - port: 8080 - https: - port: 4430 - -janus: - cmd: - - "/bin/true" diff --git a/testenv/v2-hdmiusb-rpi4.override.yaml b/testenv/v2-hdmiusb-rpi4.override.yaml deleted file mode 100644 index cb18b1aa..00000000 --- a/testenv/v2-hdmiusb-rpi4.override.yaml +++ /dev/null @@ -1,78 +0,0 @@ -kvmd: - server: - unix_mode: 0666 - - atx: - type: disabled - - hid: - keyboard: - device: /dev/null - mouse: - device: /dev/null - noop: true - - mouse_alt: - device: /dev/null - - - wol: - mac: 00:00:00:00:00:00 - - msd: - type: otg - remount_cmd: /bin/true - msd_path: /var/lib/kvmd/msd - normalfiles_path: NormalFiles - normalfiles_size: 64 - - streamer: - cmd: - - "/usr/bin/ustreamer" - - "--device=/dev/video0" - - "--persistent" - - "--format=mjpeg" - - "--resolution={resolution}" - - "--desired-fps={desired_fps}" - - "--drop-same-frames=30" - - "--last-as-blank=0" - - "--unix={unix}" - - "--unix-rm" - - "--unix-mode=0666" - - "--exit-on-parent-death" - - "--process-name-prefix={process_name_prefix}" - - "--notify-parent" - - "--no-log-colors" - -pst: - remount_cmd: /bin/true - -vnc: - keymap: /usr/share/kvmd/keymaps/ru - - auth: - vncauth: - enabled: true - - memsink: - jpeg: - sink: "" - h264: - sink: "" - -otgnet: - commands: - post_start_cmd: - - "/bin/true" - pre_stop_cmd: - - "/bin/true" - -nginx: - http: - port: 8080 - https: - port: 4430 - -janus: - cmd: - - "/bin/true" diff --git a/testenv/web.css b/testenv/web.css deleted file mode 100644 index e69de29b..00000000 diff --git a/ustreamer-win/ustreamer-win.py b/ustreamer-win/ustreamer-win.py index 2c894a54..36627b79 100644 --- a/ustreamer-win/ustreamer-win.py +++ b/ustreamer-win/ustreamer-win.py @@ -12,7 +12,6 @@ def configure_logging(): return logging.getLogger(__name__) def get_windows_cameras(logger): - """Retrieve available camera devices on Windows system""" from win32com.client import Dispatch devices = [] try: @@ -31,7 +30,6 @@ def get_windows_cameras(logger): return devices def test_camera(index, logger): - """Test if the camera is available""" try: cap = cv2.VideoCapture(index, cv2.CAP_DSHOW if platform.system() == "Windows" else cv2.CAP_ANY) if cap.isOpened():