重构更新

This commit is contained in:
mofeng-git 2024-06-18 18:29:24 +08:00
parent fdf58ea6f7
commit 99f2a1b09a
51 changed files with 5125 additions and 1895 deletions

93
README.MD Normal file
View File

@ -0,0 +1,93 @@
# KVMD-ARMBIAN
This project supports non-Raspberry Pi device to run pikvm on armbian or Raspberry Pi boards on Raspbian Bookworm
- It also supports x86 pikvm with USB uart + ch9329 serial HID if you use the install-x86.sh script and follow the How to Install PiKVM x86.pdf
- As of 04/21/2024, I have left the PiKVM Discord so if you need assistance, please email me: srepac@kvmnerds.com
- You can also try and see if I'm on my own Discord at https://discord.gg/YaJ87sVznc
# Install
KVMD Install for armbian/raspbian
It supports Allwinner, Amlogic and Rockchip based tv box, tested on phicomm n1, mxq pro 4k, tqc a01.
Chipset needs to support USB OTG feature, lots of old amglogic chipset does not support otg feature, such as s805 and s905.
You should install armbian with debian jammy as jammy has python 3.10 or python 3.11 (starting with kvmd 3.217) which the newer kvmd code is based on.
Then run this script (2x) to install pikvm.
This also works on rpi boards running Raspbian Bookworm.
Install script is a fork from @srepac raspbian pikvm install script.
**NOTE: all commands need to be run as root user.**
Original Script [http://148.135.104.55/RPiKVM/install-pikvm-raspbian.sh]
# Hardware for kvmd-armbian project
* A tv box/arm board that supports otg feature:
- Tests on phicomm n1(Amlogic s905d), mxq pro 4k (rk322x), tqc a01(Allwinner H6).
- If you are using arm board you can remove gpio patch to enable gpio feature.
* Video capture device:
- HDMI to USB dongle (30 RMB On taobao, 10$ on aliexpress.) or USB HDMI loop capture dongle
- cheap hdmi to usb dongle all use physics USB2.0 port, but fake USB3.0(USB 5GBPS, USB3.2GEN1) version supports 720P 60FPS,
usb 2.0 version only supports 720P 30FPS.
* USB-A to USB-A cable or micro-USB to USB-A cable:
- Recommended to cut off usb cable's power line, otherwise it might cause otg disconnect.
## Step 1
- Flash armbian debian [Recommended jammy] for your tv box (If kernel not support otg you should build a kernel enable otg features)
- Flash armbian debian jammy for all others
## Step 2 (skip this step if running on Orange Pi zero and one boards)
- Modify your dtb file to enable otg feature. Change dr_mode from host to peripheral for otg usb port.
- If you use rk322x (rk3228A rk3228B rk3229) series chipset, you can use dtb/4.4/rk332x-box.dtb
- Add the following to your /boot/armbianEnv.txt file
```
overlays=usbhost0 usbhost1 usbhost2 usbhost3
```
**NOTE: Skip steps 1 and 2 if you are running raspbian bookworm on rpi boards**
## Step 3 - Perform part 1 of install
```
apt update && apt upgrade -y
apt install -y git vim make python3-dev gcc
git clone https://github.com/srepac/kvmd-armbian.git
cd kvmd-armbian
./install.sh
```
This will ask you to press ENTER to reboot after part 1 completes.
## Step 4 - Perform part 2 of install
- run install.sh again after reboot os to perform part 2 of install. **NOTE: May require one more reboot if missing /dev/kvmd-hid-[keyboard|mouse]**
```
cd kvmd-armbian
./install.sh
```
- Enjoy
# Updating RPiKVM
Update armbian pikvm anytime in order to take advantage of new features/updates.
- run the update-rpikvm.sh script to perform update
```
wget -O /usr/local/bin/update-rpikvm.sh http://148.135.104.55/RPiKVM/update-rpikvm.sh
update-rpikvm.sh
```
# Tested device
- Phicomm N1
- TQC A01 (Ethernet port not working, only support wireless.)
- RK322x based tvbox (MXQ, V88)
- S905L2 based tvbox
- Orange pi zero (tested by @MrSuicideParrot)
- Orange Pi Zero and One (tested by @srepac)
- Nano Pi Neo, Rock64, and Orange Pi Zero Plus (tested by @srepac)
- Libre Computer Le Potato, La Frite 1GB, Renegade ROC-RK3328-CC and ALL-H3-CC H5 2GB (tested by @srepac)
- Inovato Quadra tv box (tested by @srepac)
- Big Tree Tech CB1+Rpi4 board (WIP by @srepac)
- Orange Pi PC+ and orange pi 3 (tested by @ducs4rs)
- RPi4B + PiKVM V3 HAT on Raspberry Pi OS bookworm (tested by @srepac on 10/19/23)
- RPi4B + BliKVM V3 HAT on Raspberry Pi OS bookworm (tested by @srepac)
- RPiCM4 + BliKVM V2 pcie HAT on Raspberry Pi OS bookworm (tested by @srepac)
- RPiCM4 + Geekworm A8 pcie HAT on Raspberry Pi OS bookworm (tested by @srepac)
- RPiCM4 + Geekworm X650 pcie HAT on Raspberry Pi OS bookworm (tested by @srepac)
- RPiCM4 + Geekworm X635 HAT on Raspberry Pi OS bookworm (tested by @srepac)

View File

@ -6,29 +6,6 @@
One-KVM 是基于经济计算机硬件(目前为玩客云和 X64 兼容机和PiKVM软件的硬件级远程控制项目。KVM over IP 可以远程管理服务器或工作站,实现无侵入式控制,无论被控机为什么操作系统或是否安装了操作系统,具有更广泛的适用性。此项目基于 [PiKVM](https://github.com/pikvm/pikvm),和基于远控软件的远程管理方式不同,无需在被控电脑安装任何软件,实现无侵入式控制。
**功能特性**
主要功能比较TinyPilot 社区版本、PiKVMv3 版本出现在这里仅做比较目的。
| 功能 | One-KVM | TinyPilot 社区版本 | PiKVMv3版本 |
| :------------: | :---------------------: | :----------------: | :----------: |
| HTML5界面语言 | 简体中文 | 英文 | 英文 |
| BIOS控制 | √ | √ | √ |
| 视频捕捉 | √ | √ | √ |
| 音频捕捉 | × | √ | √ |
| 鼠键捕获类型 | OTG CH9329 | OTG | OTG CH9329 |
| 从剪贴板粘贴 | √ | √ | √ |
| OCR识别 | √ | × | √ |
| LAN唤醒 | √ | × | √ |
| VNC支持 | √ | × | √ |
| HDMI环出 | √含HDMI设备初步支持 | × | × |
| 虚拟存储驱动器 | √仅含OTG设备支持 | × | √ |
| ATX开关机 | √仅含GPIO设备支持 | × | √ |
| 板载WiFi | × | √ | √ |
| 视频流格式 | MJPEG H.264(软编码) | MJPEG, H.264 | MJPEG, H.264 |
| 最大视频分辨率 | 1920x1080 | 1920x1080 | 1920x1080 |
### 快速开始
**方式一:直刷 One-KVM 镜像**
@ -57,6 +34,27 @@ docker run -itd -p443:443 -p80:80 --name pikvm-docker --device=/dev/ttyUSB0:/dev
详细内容可以参照 [One-KVM文档](https://one-kvm.mofeng.run/)。
### 功能特性
主要功能比较TinyPilot 社区版本、PiKVMv3 版本出现在这里仅做比较目的。
| 功能 | One-KVM | TinyPilot 社区版本 | PiKVMv3版本 |
| :------------: | :---------------------: | :----------------: | :----------: |
| HTML5界面语言 | 简体中文 | 英文 | 英文 |
| BIOS控制 | √ | √ | √ |
| 视频捕捉 | √ | √ | √ |
| 音频捕捉 | × | √ | √ |
| 鼠键捕获类型 | OTG CH9329 | OTG | OTG CH9329 |
| 从剪贴板粘贴 | √ | √ | √ |
| OCR识别 | √ | × | √ |
| LAN唤醒 | √ | × | √ |
| VNC支持 | √ | × | √ |
| HDMI环出 | √含HDMI设备初步支持 | × | × |
| 虚拟存储驱动器 | √仅含OTG设备支持 | × | √ |
| ATX开关机 | √仅含GPIO设备支持 | × | √ |
| 板载WiFi | × | √ | √ |
| 视频流格式 | MJPEG H.264(软编码) | MJPEG, H.264 | MJPEG, H.264 |
| 最大视频分辨率 | 1920x1080 | 1920x1080 | 1920x1080 |
### 其他
@ -101,3 +99,4 @@ Will
1. [pikvm/pikvm: Open and inexpensive DIY IP-KVM based on Raspberry Pi (github.com)](https://github.com/pikvm/pikvm)
2. [hzyitc/armbian-onecloud: Armbian for onecloud. 玩客云用armbian (github.com)](https://github.com/hzyitc/armbian-onecloud/)
3. [jacobbar/fruity-pikvm: Install Pi-KVM on debian SBCs such as Orange Pi, Banana Pi, Mango Pi, etc (github.com)](https://github.com/jacobbar/fruity-pikvm)
4. [kvmd-armbian/install.sh at master · srepac/kvmd-armbian (github.com)](https://github.com/srepac/kvmd-armbian/blob/master/install.sh)

BIN
aiofiles.tar Normal file

Binary file not shown.

24
armbian/armbian-motd Normal file
View File

@ -0,0 +1,24 @@
#!/bin/sh
if [ -e /etc/update-motd.d/10-armbian-header ]; then /etc/update-motd.d/10-armbian-header; fi
if [ -e /etc/update-motd.d/30-armbian-sysinfo ]; then /etc/update-motd.d/30-armbian-sysinfo; fi
if [ -e /etc/update-motd.d/41-armbian-config ]; then /etc/update-motd.d/41-armbian-config; fi
printf " 欢迎使用 One-KVM基于开源程序 PiKVM 的 IP-KVM 应用
____________________________________________________________________________
要阻止内核消息输出到终端,可以使用命令\"dmesg -n 1\"
要修改默认账户 admin 的密码可使用命令 \"kvmd-htpasswd set admin\"
项目链接:
* https://pikvm.org
* https://github.com/srepac/kvmd-armbian
* https://github.com/mofeng-git/One-KVM
文档链接:
* https://docs.pikvm.org
* https://one-kvm.mofeng.run
"
if [ -e /etc/motd.custom ]; then cat /etc/motd.custom; fi

206
armbian/opt/armbian-sysinfo Normal file
View File

@ -0,0 +1,206 @@
#!/bin/bash
#
# Copyright (c) Authors: http://www.armbian.com/authors
#
# This file is licensed under the terms of the GNU General Public
# License version 2. This program is licensed "as is" without any
# warranty of any kind, whether express or implied.
#
# DO NOT EDIT THIS FILE but add config options to /etc/default/armbian-motd
# generate system information
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
THIS_SCRIPT="sysinfo"
MOTD_DISABLE=""
STORAGE=/dev/sda1
SHOW_IP_PATTERN="^bond.*|^[ewr].*|^br.*|^lt.*|^umts.*|^lan.*"
CPU_TEMP_LIMIT=60
HDD_TEMP_LIMIT=60
AMB_TEMP_LIMIT=40
[[ -f /etc/default/armbian-motd ]] && . /etc/default/armbian-motd
for f in $MOTD_DISABLE; do
[[ $f == $THIS_SCRIPT ]] && exit 0
done
# don't edit below here
function display() {
# $1=name $2=value $3=red_limit $4=minimal_show_limit $5=unit $6=after $7=acs/desc{
# battery red color is opposite, lower number
if [[ "$1" == "Battery" ]]; then local great="<"; else local great=">"; fi
if [[ -n "$2" && "$2" > "0" && (( "${2%.*}" -ge "$4" )) ]]; then
printf "%-14s%s" "$1:"
if awk "BEGIN{exit ! ($2 $great $3)}"; then echo -ne "\e[0;91m $2"; else echo -ne "\e[0;92m $2"; fi
printf "%-1s%s\x1B[0m" "$5"
printf "%-11s%s\t" "$6"
return 1
fi
} # display
function getboardtemp() {
if [ -f /etc/armbianmonitor/datasources/soctemp ]; then
read raw_temp </etc/armbianmonitor/datasources/soctemp 2>/dev/null
if [ ! -z $(echo "$raw_temp" | grep -o "^[1-9][0-9]*\.\?[0-9]*$") ] && (( $(echo "${raw_temp} < 200" |bc -l) )); then
# Allwinner legacy kernels output degree C
board_temp=${raw_temp}
else
board_temp=$(awk '{printf("%d",$1/1000)}' <<<${raw_temp})
fi
elif [ -f /etc/armbianmonitor/datasources/pmictemp ]; then
# fallback to PMIC temperature
board_temp=$(awk '{printf("%d",$1/1000)}' </etc/armbianmonitor/datasources/pmictemp)
fi
} # getboardtemp
function batteryinfo() {
# Battery info for Allwinner
mainline_dir="/sys/power/axp_pmu"
legacy_dir="/sys/class/power_supply"
if [[ -e "$mainline_dir" ]]; then
read status_battery_connected < $mainline_dir/battery/connected 2>/dev/null
if [[ "$status_battery_connected" == "1" ]]; then
read status_battery_charging < $mainline_dir/charger/charging
read status_ac_connect < $mainline_dir/ac/connected
read battery_percent< $mainline_dir/battery/capacity
# dispay charging / percentage
if [[ "$status_ac_connect" == "1" && "$battery_percent" -lt "100" ]]; then
status_battery_text=" charging"
elif [[ "$status_ac_connect" == "1" && "$battery_percent" -eq "100" ]]; then
status_battery_text=" charged"
else
status_battery_text=" discharging"
fi
fi
elif [[ -e "$legacy_dir/axp813-ac" ]]; then
read status_battery_connected < $legacy_dir/axp20x-battery/present
if [[ "$status_battery_connected" == "1" ]]; then
status_battery_text=" "$(awk '{print tolower($0)}' < $legacy_dir/axp20x-battery/status)
read status_ac_connect < $legacy_dir/axp813-ac/present
read battery_percent< $legacy_dir/axp20x-battery/capacity
fi
elif [[ -e "$legacy_dir/battery" ]]; then
if [[ (("$(cat $legacy_dir/battery/voltage_now)" -gt "5" )) ]]; then
status_battery_text=" "$(awk '{print tolower($0)}' < $legacy_dir/battery/status)
read battery_percent <$legacy_dir/battery/capacity
fi
fi
} # batteryinfo
function ambienttemp() {
# define where w1 usually shows up
W1_DIR="/sys/devices/w1_bus_master1/"
if [ -f /etc/armbianmonitor/datasources/ambienttemp ]; then
read raw_temp </etc/armbianmonitor/datasources/ambienttemp 2>/dev/null
amb_temp=$(awk '{printf("%d",$1/1000)}' <<<${raw_temp})
echo $amb_temp
elif [[ -d $W1_DIR && $ONE_WIRE == yes ]]; then
device=$(ls -1 $W1_DIR | grep -Eo '^[0-9]{1,4}' | head -1)
if [[ -n $device ]]; then
if [[ -d ${W1_DIR}${device}/hwmon/hwmon0 ]]; then hwmon=0; else hwmon=1; fi
read raw_temp < ${W1_DIR}${device}/hwmon/hwmon${hwmon}/temp1_input 2>/dev/null
amb_temp=$(awk '{printf("%d",$1/1000)}' <<<${raw_temp})
echo $amb_temp
fi
else
# read ambient temperature from USB device if available
if [[ ! -f /usr/bin/temper ]]; then
echo ""
return
fi
amb_temp=$(temper -c 2>/dev/null)
case ${amb_temp} in
*"find the USB device"*)
echo ""
;;
*)
amb_temp=$(awk '{print $NF}' <<<$amb_temp | sed 's/C//g')
echo -n "scale=1;${amb_temp}/1" | grep -oE "\-?[[:digit:]]+\.[[:digit:]]"
esac
fi
} # ambienttemp
function get_ip_addresses() {
local ips=()
for f in /sys/class/net/*; do
local intf=$(basename $f)
# match only interface names starting with e (Ethernet), br (bridge), w (wireless), r (some Ralink drivers use ra<number> format)
if [[ $intf =~ $SHOW_IP_PATTERN ]]; then
local tmp=$(ip -4 addr show dev $intf | awk '/inet/ {print $2}' | cut -d'/' -f1)
# add both name and IP - can be informative but becomes ugly with long persistent/predictable device names
#[[ -n $tmp ]] && ips+=("$intf: $tmp")
# add IP only
[[ -n $tmp ]] && ips+=("$tmp")
fi
done
echo "${ips[@]}"
} # get_ip_addresses
function storage_info() {
# storage info
RootInfo=$(df -h /)
root_usage=$(awk '/\// {print $(NF-1)}' <<<${RootInfo} | sed 's/%//g')
root_total=$(awk '/\// {print $(NF-4)}' <<<${RootInfo})
StorageInfo=$(df -h $STORAGE 2>/dev/null | grep $STORAGE)
if [[ -n "${StorageInfo}" && ${RootInfo} != *$STORAGE* ]]; then
storage_usage=$(awk '/\// {print $(NF-1)}' <<<${StorageInfo} | sed 's/%//g')
storage_total=$(awk '/\// {print $(NF-4)}' <<<${StorageInfo})
if [[ -n "$(command -v smartctl)" ]]; then
DISK="${STORAGE::-1}"
storage_temp+=$(sudo smartctl -A $DISK 2> /dev/null | grep -i temperature | awk '{print $(NF-2)}')
fi
fi
} # storage_info
# query various systems and send some stuff to the background for overall faster execution.
# Works only with ambienttemp and batteryinfo since A20 is slow enough :)
amb_temp=$(ambienttemp &)
ip_address=$(get_ip_addresses &)
batteryinfo
storage_info
getboardtemp
critical_load=80
# get uptime, logged in users and load in one take
UPTIME=$(LC_ALL=C uptime)
UPT1=${UPTIME#*'up '}
UPT2=${UPT1%'user'*}
users=${UPT2//*','}
users=${users//' '}
time=${UPT2%','*}
time=${time//','}
time=$(echo $time | xargs)
load=${UPTIME#*'load average: '}
load=${load//','}
load=$(echo $load | cut -d" " -f1)
[[ $load == 0.0* ]] && load=0.10
cpucount=$(grep -c processor /proc/cpuinfo)
load=$(awk '{printf("%.0f",($1/$2) * 100)}' <<< "$load $cpucount")
# memory and swap
mem_info=$(LC_ALL=C free -w 2>/dev/null | grep "^Mem" || LC_ALL=C free | grep "^Mem")
memory_usage=$(awk '{printf("%.0f",(($2-($4+$6+$7))/$2) * 100)}' <<<${mem_info})
mem_info=$(echo $mem_info | awk '{print $2}')
memory_total=$(( mem_info / 1024 ))
swap_info=$(LC_ALL=C free -m | grep "^Swap")
swap_usage=$( (awk '/Swap/ { printf("%3.0f", $3/$2*100) }' <<<${swap_info} 2>/dev/null || echo 0) | tr -c -d '[:digit:]')
swap_total=$(awk '{print $(2)}' <<<${swap_info})
if [[ ${memory_total} -gt 1000 ]]; then
memory_total=$(awk '{printf("%.2f",$1/1024)}' <<<${memory_total})"G"
else
memory_total+="M"
fi
if [[ ${swap_total} -gt 500 ]]; then
swap_total=$(awk '{printf("%.2f",$1/1024)}' <<<${swap_total})"G"
else
swap_total+="M"
fi

70
armbian/opt/vcgencmd Normal file
View File

@ -0,0 +1,70 @@
#!/bin/bash
cd `dirname $0`
source armbian-sysinfo
case $1 in
get_throttled) echo "throttled=0x0";;
# measure_temp) echo "temp=$board_temp'C";;
get_config)
case $2 in
total_mem)
NUM=$( free -m | grep Mem: | awk '{print $2}' )
if [ -e /var/log/dmesg ]; then
KB=$( sudo grep 'Memory:' /var/log/dmesg | awk '{print $5}' | cut -d'/' -f2 | sed 's/K//g' | head -1 )
else
KB=""
fi
if [[ "$KB" == "" ]]; then
GB=`echo "( $NUM + 256 ) / 256" | bc`
MB=$( echo "${GB} * 256" | bc )
else
GB=$( echo "($KB + 2048) / 1024 / 256" | bc )
MB=$( echo "${GB} * 256" | bc )
fi
echo "total_mem=$MB"
;;
*)
echo "invalid option";;
esac
;;
measure_clock)
case ${2} in
arm)
# awk is probably overkill....
value=`cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq | awk '{print $1 * 1000}'`
echo 'frequency(45)='${value}
exit
;;
core)
value=0 # TODO / FIXME
echo 'frequency(1)='${value}
exit
;;
# TODO anything else thrown an error/debug
esac
exit
;;
measure_temp)
# awk is probably overkill....
value=`cat /sys/class/thermal/thermal_zone0/temp | awk '{print $1 / 1000}'`
echo 'temp='${value}"'C"
exit
;;
measure_volts)
case ${2} in
core)
value=1 # TODO / FIXME
echo 'volt='${value}'.0000V'
exit
;;
# TODO anything else thrown an error/debug
esac
;;
version)
echo 'Nov 4 2018 16:31:07'
echo 'Copyright (c) 2012 rock64'
echo 'version rock64_TODO (clean) (release)'
exit
;;
esac

View File

@ -0,0 +1,6 @@
# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name
# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names
KERNEL=="video[1-9]*", SUBSYSTEM=="video4linux", PROGRAM="/usr/bin/kvmd-udev-hdmiusb-check rpi4 1-1.4:1.0", ATTR{index}=="0", GROUP="kvmd", SYMLINK+="kvmd-video"
KERNEL=="hidg0", GROUP="kvmd", SYMLINK+="kvmd-hid-keyboard"
KERNEL=="hidg1", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse"
KERNEL=="hidg2", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse-alt"

View File

@ -16,8 +16,13 @@ vnc:
vncauth:
enabled: true # Enable auth via /etc/kvmd/vncpasswd
kvmd:
hid:
mouse_alt:
device: /dev/kvmd-hid-mouse-alt # allow relative mouse mode
msd:
type: disabled
atx:
type: disabled
gpio:
drivers:
wol_server1:
@ -47,7 +52,7 @@ kvmd:
switch: false
view:
header:
title: ATX
title: 电源管理
table:
- ["#电源管理"]
- []
@ -57,8 +62,10 @@ kvmd:
- ["#网络唤醒"]
- ["#被控机设备", wol_server1|网络唤醒]
streamer:
forever: true
#forever: true
cmd_append: [--slowdown]
resolution:
default: 1280x720
desired_fps:
default: 30
max: 60

View File

@ -1,62 +0,0 @@
# Don't touch this file otherwise your device may stop working.
# Use override.yaml to modify required settings.
# You can find a working configuration in /usr/share/kvmd/configs.default/kvmd.
override: !include [override.d, override.yaml]
logging: !include logging.yaml
kvmd:
auth: !include auth.yaml
hid:
type: otg
mouse_alt:
device: /dev/kvmd-hid-mouse-alt
atx:
type: disabled
msd:
type: otg
streamer:
quality: 0
resolution:
default: 1280x720
available:
- 1920x1080
- 1600x1200
- 1360x768
- 1280x1024
- 1280x960
- 1280x720
- 1024x768
- 800x600
- 720x576
- 720x480
- 640x480
cmd:
- "/usr/bin/ustreamer"
- "--device=/dev/kvmd-video"
- "--persistent"
- "--format=mjpeg"
- "--resolution={resolution}"
- "--desired-fps={desired_fps}"
- "--drop-same-frames=30"
- "--last-as-blank=0"
- "--unix={unix}"
- "--unix-rm"
- "--unix-mode=0660"
- "--exit-on-parent-death"
- "--process-name-prefix={process_name_prefix}"
- "--notify-parent"
- "--no-log-colors"
- "--sink=kvmd::ustreamer::jpeg"
- "--sink-mode=0660"
vnc:
memsink:
jpeg:
sink: "kvmd::ustreamer::jpeg"

Binary file not shown.

864
install-x86.sh Normal file
View File

@ -0,0 +1,864 @@
#!/bin/bash
# https://github.com/srepac/kvmd-armbian
# modified by SilentWind 2024-06-17
# modified by xe5700 2021-11-04 xe5700@outlook.com
# modified by NewbieOrange 2021-11-04
# created by @srepac 08/09/2021 srepac@kvmnerds.com
# Scripted Installer of Pi-KVM on x86 (as long as it's running python 3.10 or higher)
#
# *** MSD is disabled by default ***
#
# Mass Storage Device requires the use of a USB thumbdrive or SSD and will need to be added in /etc/fstab
: '
# SAMPLE /etc/fstab entry for USB drive with only one partition formatted as ext4 for the entire drive:
/dev/sda1 /var/lib/kvmd/msd ext4 nodev,nosuid,noexec,ro,errors=remount-ro,data=journal,X-kvmd.otgmsd-root=/var/lib/kvmd/msd,X-kvmd.otgmsd-user=kvmd 0 0
'
# NOTE: This was tested on a new install of raspbian desktop and lite versions, but should also work on an existing install.
#
# Last change 20240526 2345 PDT
VER=3.4
set +x
PIKVMREPO="https://files.pikvm.org/repos/arch/rpi4"
KVMDFILE="kvmd-3.291-1-any.pkg.tar.xz"
KVMDCACHE="/var/cache/kvmd"; mkdir -p $KVMDCACHE
PKGINFO="${KVMDCACHE}/packages.txt"
APP_PATH=$(readlink -f $(dirname $0))
LOGFILE="${KVMDCACHE}/installer.log"; touch $LOGFILE; echo "==== $( date ) ====" >> $LOGFILE
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
echo "usage: $0 [-f] where -f will force re-install new pikvm platform"
exit 1
fi
CWD=`pwd`
WHOAMI=$( whoami )
if [ "$WHOAMI" != "root" ]; then
echo "$WHOAMI, please run script as root."
exit 1
fi
PYTHONVER=$( python3 -V | cut -d' ' -f2 | cut -d'.' -f1,2 )
case $PYTHONVER in
3.10|3.11)
echo "Python $PYTHONVER is supported." | tee -a $LOGFILE
;;
*)
echo "Python $PYTHONVER is NOT supported. Please make sure you have python3.10 or higher installed. Exiting." | tee -a $LOGFILE
exit 1
;;
esac
MAKER=$(tr -d '\0' < /proc/device-tree/model | awk '{print $1}')
gen-ssl-certs() {
cd /etc/kvmd/nginx/ssl
openssl ecparam -out server.key -name prime256v1 -genkey
openssl req -new -x509 -sha256 -nodes -key server.key -out server.crt -days 3650 \
-subj "/C=US/ST=Denial/L=Denial/O=Pi-KVM/OU=Pi-KVM/CN=$(hostname)"
cp server* /etc/kvmd/vnc/ssl/
cd ${APP_PATH}
} # end gen-ssl-certs
create-override() {
if [ $( grep ^kvmd: /etc/kvmd/override.yaml | wc -l ) -eq 0 ]; then
if [[ $( echo $platform | grep usb | wc -l ) -eq 1 ]]; then
cat <<USBOVERRIDE >> /etc/kvmd/override.yaml
kvmd:
hid:
### add entries for use with the ch9329 serial HID
type: ch9329
speed: 9600 # default speed after loading ch9329 plugin is 9600
device: /dev/kvmd-hid
msd:
type: disabled
atx:
type: disabled
streamer:
#forever: true
cmd_append:
- "--slowdown" # so target doesn't have to reboot
resolution:
default: 1280x720
USBOVERRIDE
else
cat <<CSIOVERRIDE >> /etc/kvmd/override.yaml
kvmd:
hid:
### add entries for use with the ch9329 serial HID
type: ch9329
speed: 9600 # default speed after loading ch9329 plugin is 9600
device: /dev/kvmd-hid
msd:
type: disabled
streamer:
#forever: true
cmd_append:
- "--slowdown" # so target doesn't have to reboot
CSIOVERRIDE
fi
fi
} # end create-override
install-python-packages() {
echo "apt 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-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-build
python3-pyotp python3-qrcode python3-serial"
apt 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-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-build\
python3-pyotp python3-qrcode python3-serial >> $LOGFILE
} # end install python-packages
otg-devices() {
modprobe libcomposite
if [ ! -e /sys/kernel/config/usb_gadget/kvmd ]; then
mkdir -p /sys/kernel/config/usb_gadget/kvmd/functions
cd /sys/kernel/config/usb_gadget/kvmd/functions
mkdir hid.usb0 hid.usb1 hid.usb2 mass_storage.usb0
fi
cd ${APP_PATH}
} # end otg-device creation
boot-files() {
# Remove OTG serial (Orange pi zero's kernel not support it)
sed -i '/^g_serial/d' /etc/modules
# /etc/modules required entries for DWC2, HID and I2C
if [[ $( grep -w dwc2 /etc/modules | wc -l ) -eq 0 ]]; then
echo "dwc2" >> /etc/modules
fi
if [[ $( grep -w libcomposite /etc/modules | wc -l ) -eq 0 ]]; then
echo "libcomposite" >> /etc/modules
fi
if [[ $( grep -w i2c-dev /etc/modules | wc -l ) -eq 0 ]]; then
echo "i2c-dev" >> /etc/modules
fi
printf "\n/etc/modules\n\n" | tee -a $LOGFILE
cat /etc/modules | tee -a $LOGFILE
} # end of necessary boot files
get-packages() {
printf "\n\n-> Getting Pi-KVM packages from ${PIKVMREPO}\n\n" | tee -a $LOGFILE
cp -f ${APP_PATH}/kvmd-packages/* ${KVMDCACHE}
} # end get-packages function
get-platform() {
platform="kvmd-platform-v0-hdmiusb-rpi3";
echo
echo "Platform selected -> $platform" | tee -a $LOGFILE
echo
} # end get-platform
install-kvmd-pkgs() {
cd /
INSTLOG="${KVMDCACHE}/installed_ver.txt"; rm -f $INSTLOG
date > $INSTLOG
# uncompress platform package first
i=$( ls ${KVMDCACHE}/${platform}*.tar.xz | grep 3.291 )
# change the log entry to show 3.291 platform installed as we'll be forcing kvmd-3.291 instead of latest/greatest kvmd
_platformver=$( echo $i | sed -e 's/3\.29[2-9]*/3.291/g' -e 's/3\.3[0-9]*/3.291/g' )
echo "-> Extracting package $_platformver into /" | tee -a $INSTLOG
tar xfJ $i
# then uncompress, kvmd-{version}, kvmd-webterm, and janus packages
for i in $( ls ${KVMDCACHE}/*.tar.xz | egrep 'kvmd-[0-9]|webterm' )
do
case $i in
*kvmd-3.29[2-9]*|*kvmd-3.[3-9]*|*kvmd-[45].[1-9]*) # if latest/greatest is 3.292 and higher, then force 3.291 install
echo "*** Force install kvmd 3.291 ***" | tee -a $LOGFILE
i=$KVMDCACHE/$KVMDFILE
;;
*)
;;
esac
echo "-> Extracting package $i into /" >> $INSTLOG
tar xfJ $i
done
# uncompress janus package if /usr/bin/janus doesn't exist
if [ ! -e /usr/bin/janus ]; then
i=$( ls ${KVMDCACHE}/*.tar.xz | egrep janus | grep -v 1x )
echo "-> Extracting package $i into /" >> $INSTLOG
tar xfJ $i
else # confirm that /usr/bin/janus actually runs properly
/usr/bin/janus --version > /dev/null 2>> $LOGFILE
if [ $? -eq 0 ]; then
echo "You have a working valid janus binary." | tee -a $LOGFILE
else # error status code, so uncompress from REPO package
i=$( ls ${KVMDCACHE}/*.tar.xz | egrep janus )
echo "-> Extracting package $i into /" >> $INSTLOG
tar xfJ $i
fi
fi
cd ${APP_PATH}
} # end install-kvmd-pkgs
fix-udevrules() {
# for hdmiusb, replace %b with 1-1.4:1.0 in /etc/udev/rules.d/99-kvmd.rules
sed -i -e 's+\%b+1-1.4:1.0+g' -e 's+ttyAMA0+ttyUSB[0-2]+g' /etc/udev/rules.d/99-kvmd.rules | tee -a $LOGFILE
echo
cat /etc/udev/rules.d/99-kvmd.rules | tee -a $LOGFILE
} # end fix-udevrules
enable-kvmd-svcs() {
# enable KVMD services but don't start them
echo "-> Enabling $SERVICES services, but do not start them." | tee -a $LOGFILE
systemctl enable $SERVICES
} # end enable-kvmd-svcs
build-ustreamer() {
printf "\n\n-> Building ustreamer\n\n" | tee -a $LOGFILE
# Install packages needed for building ustreamer source
echo "apt install -y libevent-dev libjpeg-dev libbsd-dev libgpiod-dev libsystemd-dev janus-dev janus" | tee -a $LOGFILE
apt install -y libevent-dev libjpeg-dev libbsd-dev libgpiod-dev libsystemd-dev janus-dev janus >> $LOGFILE
# fix refcount.h
sed -i -e 's|^#include "refcount.h"$|#include "../refcount.h"|g' /usr/include/janus/plugins/plugin.h
# Download ustreamer source and build it
cd /tmp
unzip ${APP_PATH}/sources/ustreamer-6.12.zip
cd ustreamer-6.12/
#添加WITH_PYTHON=1 使kvmd-vnc正常工作
make WITH_GPIO=1 WITH_SYSTEMD=1 WITH_JANUS=1 WITH_PYTHON=1 -j
#删除 --prefix=$(PREFIX) 修复无法安装pythgon包的问题
sed -i 's/--prefix=\$(PREFIX)//g' python/Makefile
make install WITH_PYTHON=1
# kvmd service is looking for /usr/bin/ustreamer
ln -sf /usr/local/bin/ustreamer* /usr/bin/
# add janus support
mkdir -p /usr/lib/ustreamer/janus
cp /tmp/ustreamer-6.12/janus/libjanus_ustreamer.so /usr/lib/ustreamer/janus
} # end build-ustreamer
install-dependencies() {
echo
echo "-> Installing dependencies for pikvm" | tee -a $LOGFILE
echo "apt install -y make nginx python3 gcc unzip net-tools bc expect v4l-utils iptables vim dos2unix screen tmate nfs-common gpiod ffmpeg dialog iptables dnsmasq git python3-pip tesseract-ocr tesseract-ocr-eng libasound2-dev libsndfile-dev libspeexdsp-dev build-essential libssl-dev libffi-dev lm-sensors" | tee -a $LOGFILE
apt install -y make nginx python3 gcc unzip net-tools bc expect v4l-utils iptables vim dos2unix screen tmate nfs-common gpiod ffmpeg dialog iptables dnsmasq git python3-pip tesseract-ocr tesseract-ocr-eng libasound2-dev libsndfile-dev libspeexdsp-dev build-essential libssl-dev libffi-dev lm-sensors >> $LOGFILE
sed -i -e 's/#port=5353/port=5353/g' /etc/dnsmasq.conf
install-python-packages
echo "-> Make tesseract data link" | tee -a $LOGFILE
ln -sf /usr/share/tesseract-ocr/*/tessdata /usr/share/tessdata
echo "-> Install TTYD" | tee -a $LOGFILE
apt install -y ttyd | tee -a $LOGFILE
/usr/bin/ttyd -v | tee -a $LOGFILE
if [ ! -e /usr/local/bin/gpio ]; then
printf "\n\n-> Building wiringpi from source\n\n" | tee -a $LOGFILE
cd /tmp; rm -rf WiringPi-3.6
unzip ${APP_PATH}/sources/WiringPi-3.6.zip
cd WiringPi-3.6
./build
else
printf "\n\n-> Wiringpi (gpio) is already installed.\n\n" | tee -a $LOGFILE
fi
gpio -v | tee -a $LOGFILE
echo "-> Install ustreamer" | tee -a $LOGFILE
if [ ! -e /usr/bin/ustreamer ]; then
cd /tmp
### required dependent packages for ustreamer ###
build-ustreamer
cd ${APP_PATH}
fi
echo -n "ustreamer version: " | tee -a $LOGFILE
ustreamer -v | tee -a $LOGFILE
ustreamer --features | tee -a $LOGFILE
} # end install-dependencies
python-pkg-dir() {
# debian system python3 no alias
# create quick python script to show where python packages need to go
cat << MYSCRIPT > /tmp/syspath.py
#!$(which python3)
import sys
print (sys.path)
MYSCRIPT
chmod +x /tmp/syspath.py
#PYTHONDIR=$( /tmp/syspath.py | awk -F, '{print $NF}' | cut -d"'" -f2 )
### hardcode path for armbian/raspbian
PYTHONDIR="/usr/lib/python3/dist-packages"
} # end python-pkg-dir
fix-nginx-symlinks() {
# disable default nginx service since we will use kvmd-nginx instead
echo
echo "-> Disabling nginx service, so that we can use kvmd-nginx instead" | tee -a $LOGFILE
systemctl disable --now nginx
# setup symlinks
echo
echo "-> Creating symlinks for use with kvmd python scripts" | tee -a $LOGFILE
if [ ! -e /usr/bin/nginx ]; then ln -sf /usr/sbin/nginx /usr/bin/; fi
if [ ! -e /usr/sbin/python ]; then ln -sf /usr/bin/python3 /usr/sbin/python; fi
if [ ! -e /usr/bin/iptables ]; then ln -sf /usr/sbin/iptables /usr/bin/iptables; fi
if [ ! -e /usr/bin/vcgencmd ]; then ln -sf /opt/vc/bin/* /usr/bin/; chmod +x /opt/vc/bin/*; fi
python-pkg-dir
if [ ! -e $PYTHONDIR/kvmd ]; then
# Debian python版本比 pikvm官方的低一些
# in case new kvmd packages are now using python 3.11
ln -sf /usr/lib/python3.1*/site-packages/kvmd* ${PYTHONDIR}
fi
} # end fix-nginx-symlinks
fix-python-symlinks(){
python-pkg-dir
if [ ! -e $PYTHONDIR/kvmd ]; then
# Debian python版本比 pikvm官方的低一些
ln -sf /usr/lib/python3.1*/site-packages/kvmd* ${PYTHONDIR}
fi
}
apply-custom-patch(){
read -p "Do you want apply old kernel msd patch? [y/n]" answer
case $answer in
n|N|no|No)
echo 'You skipped this patch.'
;;
y|Y|Yes|yes)
./patches/custom/old-kernel-msd/apply.sh
;;
*)
echo "Try again.";;
esac
}
fix-webterm() {
echo
echo "-> Creating kvmd-webterm homedir" | tee -a $LOGFILE
mkdir -p /home/kvmd-webterm
chown kvmd-webterm /home/kvmd-webterm
ls -ld /home/kvmd-webterm | tee -a $LOGFILE
# remove -W option since ttyd installed on raspbian/armbian is 1.6.3 (-W option only works with ttyd 1.7.x)
_ttydver=$( /usr/bin/ttyd -v | awk '{print $NF}' )
case $_ttydver in
1.6*)
echo "ttyd $_ttydver found. Removing -W from /lib/systemd/system/kvmd-webterm.service"
sed -i -e '/-W \\/d' /lib/systemd/system/kvmd-webterm.service
;;
1.7*)
echo "ttyd $_ttydver found. Nothing to do."
;;
esac
# add sudoers entry for kvmd-webterm user to be able to run sudo
echo "kvmd-webterm ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/kvmd-webterm; chmod 440 /etc/sudoers.d/kvmd-webterm
} # end fix-webterm
create-kvmdfix() {
# Create kvmd-fix service and script
cat <<ENDSERVICE > /lib/systemd/system/kvmd-fix.service
[Unit]
Description=KVMD Fixes
After=network.target network-online.target nss-lookup.target
Before=kvmd.service
[Service]
User=root
Type=simple
ExecStart=/usr/bin/kvmd-fix
[Install]
WantedBy=multi-user.target
ENDSERVICE
cat <<SCRIPTEND > /usr/bin/kvmd-fix
#!/bin/bash
# Written by @srepac
# 1. Properly set group ownership of /dev/gpio*
# 2. fix /dev/kvmd-video symlink to point to /dev/video1 (Amglogic Device video0 is not usb device)
#
### These fixes are required in order for kvmd service to start properly
#
set -x
chgrp gpio /dev/gpio*
chmod 660 /dev/gpio*
ls -l /dev/gpio*
udevadm trigger
ls -l /dev/kvmd-video
if [ \$( systemctl | grep kvmd-oled | grep -c activ ) -eq 0 ]; then
echo "kvmd-oled service is not enabled."
exit 0
else
echo "kvmd-oled service is enabled and activated."
fi
### kvmd-oled fix: swap i2c-0 <-> i2c-1 (code is looking for I2C oled on i2c-1)
# pins #1 - 3.3v, #3 - SDA, #5 - SCL, and #9 - GND
i2cget -y 0 0x3c
if [ \$? -eq 0 ]; then
echo "-> Found valid I2C OLED at i2c-0. Applying I2C OLED fix."
cd /dev
# rename i2c-0 -> i2c-9, move i2c-1 to i2c-0, and rename the good i2c-9 to i2c-1
mv i2c-0 i2c-9
mv i2c-1 i2c-0
mv i2c-9 i2c-1
# restart kvmd-oled service
systemctl restart kvmd-oled
else
echo "-> I2C OLED fix already applied and OLED should be showing info."
fi
SCRIPTEND
chmod +x /usr/bin/kvmd-fix
cat << CHRESET > /root/ch_reset.py
#!/usr/bin/python3
import serial
import time
device_path = "/dev/kvmd-hid"
chip = serial.Serial(device_path, 9600, timeout=1)
command = [87, 171, 0, 15, 0]
sum = sum(command) % 256
command.append(sum)
print("Resetting CH9329")
chip.write(serial.to_bytes(command))
time.sleep(2)
data = list(chip.read(5))
print("Initial data:", data)
if data[4] :
more_data = list(chip.read(data[4]))
data.extend(more_data)
print("Output: ", data)
chip.close()
CHRESET
chmod +x /root/ch_reset.py
} # end create-kvmdfix
set-ownership() {
# set proper ownership of password files and kvmd-webterm homedir
cd /etc/kvmd
chown kvmd:kvmd htpasswd
chown kvmd-ipmi:kvmd-ipmi ipmipasswd
chown kvmd-vnc:kvmd-vnc vncpasswd
chown kvmd-webterm /home/kvmd-webterm
# add kvmd user to video group (this is required in order to use CSI bridge with OMX and h264 support)
usermod -a -G video kvmd
# add kvmd user to dialout group (required for xh_hk4401 kvm switch support)
usermod -a -G dialout kvmd
} # end set-ownership
check-kvmd-works() {
echo "-> Checking kvmd -m works before continuing" | tee -a $LOGFILE
kvmd -m
invalid=1
! $NOTCHROOT || while [ $invalid -eq 1 ]; do
#kvmd -m
read -p "Did kvmd -m run properly? [y/n] " answer
case $answer in
n|N|no|No)
echo "Please install missing packages as per the kvmd -m output in another ssh/terminal."
;;
y|Y|Yes|yes)
invalid=0
;;
*)
echo "Try again.";;
esac
done
} # end check-kvmd-works
start-kvmd-svcs() {
#### start the main KVM services in order ####
# 1. nginx is the webserver
# 2. kvmd-otg is for OTG devices (keyboard/mouse, etc..)
# 3. kvmd is the main daemon
systemctl daemon-reload
systemctl restart $SERVICES
} # end start-kvmd-svcs
fix-motd() {
if [ -e /etc/motd ]; then rm /etc/motd; fi
cp armbian/armbian-motd /usr/bin/
chmod +x /usr/bin/armbian-motd
chmod +x /etc/update-motd.d/*
sed -i 's/cat \/etc\/motd/armbian-motd/g' /lib/systemd/system/kvmd-webterm.service
systemctl daemon-reload
# systemctl restart kvmd-webterm
} # end fix-motd
# 安装armbian的包
armbian-packages() {
mkdir -p /opt/vc/bin/
#cd /opt/vc/bin
if [ ! -e /usr/bin/vcgencmd ]; then
# Install vcgencmd for armbian platform
cp -rf armbian/opt/* /opt/vc/bin
else
ln -s /usr/bin/vcgencmd /opt/vc/bin/
fi
#cp -rf armbian/udev /etc/
cd ${APP_PATH}
} # end armbian-packages
fix-nfs-msd() {
NAME="aiofiles.tar"
LOCATION="/usr/lib/python3.11/site-packages"
echo "-> Extracting $NAME into $LOCATION" | tee -a $LOGFILE
tar xvf $NAME -C $LOCATION
echo "-> Renaming original aiofiles and creating symlink to correct aiofiles" | tee -a $LOGFILE
cd /usr/lib/python3/dist-packages
mv aiofiles aiofiles.$(date +%Y%m%d.%H%M)
ln -s $LOCATION/aiofiles .
ls -ld aiofiles* | tail -5
}
apply-x86-mods() {
TARBALL="${APP_PATH}/x86-mods.tar"
if [ -e $TARBALL ]; then
echo "-> Making backup of files that require modification" | tee -a $LOGFILE
for i in $( tar tf $TARBALL ); do
echo "cp $PYTHONDIR/$i $PYTHONDIR/$i.orig" | tee -a $LOGFILE
cp $PYTHONDIR/$i $PYTHONDIR/$i.orig
done
tar tvf $TARBALL
echo "tar xvf $TARBALL -C $PYTHONDIR" | tee -a $LOGFILE
tar xvf $TARBALL -C $PYTHONDIR
for i in $( tar tf $TARBALL ); do
ls -l $PYTHONDIR/$i
done
else
echo "Missing $TARBALL. Please obtain the tar file from @srepac and try again." | tee -a $LOGFILE
fi
}
fix-nginx() {
#set -x
KERNEL=$( uname -r | awk -F\- '{print $1}' )
ARCH=$( uname -r | awk -F\- '{print $NF}' )
echo "KERNEL: $KERNEL ARCH: $ARCH" | tee -a $LOGFILE
case $ARCH in
ARCH) SEARCHKEY=nginx-mainline;;
*) SEARCHKEY="nginx/";;
esac
HTTPSCONF="/etc/kvmd/nginx/listen-https.conf"
echo "HTTPSCONF BEFORE: $HTTPSCONF" | tee -a $LOGFILE
cat $HTTPSCONF | tee -a $LOGFILE
if [[ ! -e /usr/local/bin/pikvm-info || ! -e /tmp/pacmanquery ]]; then
cp -f ${APP_PATH}/pikvm-info /usr/local/bin/pikvm-info
chmod +x /usr/local/bin/pikvm-info
echo "Getting list of packages installed..." | tee -a $LOGFILE
pikvm-info > /dev/null ### this generates /tmp/pacmanquery with list of installed pkgs
fi
NGINXVER=$( grep $SEARCHKEY /tmp/pacmanquery | awk '{print $1}' | cut -d'.' -f1,2 )
echo
echo "NGINX version installed: $NGINXVER" | tee -a $LOGFILE
case $NGINXVER in
1.2[56789]|1.3*|1.4*|1.5*) # nginx version 1.25 and higher
cat << NEW_CONF > $HTTPSCONF
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
NEW_CONF
;;
1.18|*) # nginx version 1.18 and lower
cat << ORIG_CONF > $HTTPSCONF
listen 443 ssl http2;
listen [::]:443 ssl;
ORIG_CONF
;;
esac
echo "HTTPSCONF AFTER: $HTTPSCONF" | tee -a $LOGFILE
cat $HTTPSCONF | tee -a $LOGFILE
set +x
} # end fix-nginx
ocr-fix() { # create function
echo
echo "-> Apply OCR fix..." | tee -a $LOGFILE
# 1. verify that Pillow module is currently running 9.0.x
PILLOWVER=$( pip3 list | grep -i pillow | awk '{print $NF}' )
case $PILLOWVER in
9.*|8.*|7.*) # Pillow running at 9.x and lower
# 2. update Pillow to 10.0.0
pip3 install -U Pillow 2>> $LOGFILE
# 3. check that Pillow module is now running 10.0.0
pip3 list | grep -i pillow | tee -a $LOGFILE
#4. restart kvmd and confirm OCR now works.
systemctl restart kvmd
;;
10.*|11.*|12.*) # Pillow running at 10.x and higher
echo "Already running Pillow $PILLOWVER. Nothing to do." | tee -a $LOGFILE
;;
esac
set +x
echo
} # end ocr-fix
x86-fix-3.256() {
echo "-> Apply x86-fix for 3.256 and higher..." | tee -a $LOGFILE
cd /usr/lib/python3/dist-packages/kvmd/apps/
cp __init__.py __init__.py.$( date +%Y%m%d )
cp -f ${APP_PATH}/patches/__init__.py __init__.py
#mv __init__.py.1 __init__.py
cd /usr/share/kvmd/web/share/js
if [ -e session.js ]; then
cp session.js session.js.$( date +%Y%m%d )
fi
cp -f ${APP_PATH}/patches/session.js session.js
if [ -e session.js.1 ]; then
mv session.js.1 session.js
fi
cd /usr/lib/python3/dist-packages/kvmd/apps/kvmd/info/
cp hw.py hw.py.$( date +%Y%m%d )
#wget --no-check-certificate https://raw.githubusercontent.com/pikvm/kvmd/cec03c4468df87bcdc68f20c2cf51a7998c56ebd/kvmd/apps/kvmd/info/hw.py 2> /dev/null
#mv hw.py.1 hw.py
cp -f ${APP_PATH}/patches/hw.py hw.py
cp -f ${APP_PATH}patches/main.yaml /etc/kvmd/
} # end x86-fix-3.256
x86-fix-3.281() {
echo "-> Apply x86-fix for 3.281 and higher..." | tee -a $LOGFILE
cd /usr/lib/python3/dist-packages/kvmd/apps/
cp __init__.py __init__.py.$( date +%Y%m%d )
cp -f ${APP_PATH}/patches/__init__.py.2 __init__.py
cd /usr/lib/python3/dist-packages/kvmd/apps/kvmd
cp -f ${APP_PATH}/patches/streamer.py.1 streamer.py
} # end x86-fix-3.281
update-logo() {
sed -i -e 's|class="svg-gray"|class="svg-color"|g' /usr/share/kvmd/web/index.html
sed -i -e 's|target="_blank"><img class="svg-gray"|target="_blank"><img class="svg-color"|g' /usr/share/kvmd/web/kvm/index.html
### download opikvm-logo.svg and then overwrite logo.svg
cp -f ${APP_PATH}/opikvm-logo.svg /usr/share/kvmd/web/share/svg/opikvm-logo.svg
cd /usr/share/kvmd/web/share/svg
cp logo.svg logo.svg.old
cp opikvm-logo.svg logo.svg
# change some text in the main html page
#sed -i.bak -e 's/The Open Source KVM over IP/KVM over IP on non-Arch linux OS by @srepac/g' /usr/share/kvmd/web/index.html
#sed -i.bak -e 's/The Open Source KVM over IP/KVM over IP on non-Arch linux OS by @srepac/g' /usr/share/kvmd/web/kvm/index.html
#sed -i.backup -e 's|https://pikvm.org/support|https://discord.gg/YaJ87sVznc|g' /usr/share/kvmd/web/kvm/index.html
#sed -i.backup -e 's|https://pikvm.org/support|https://discord.gg/YaJ87sVznc|g' /usr/share/kvmd/web/index.html
cd
}
### MAIN STARTS HERE ###
# Install is done in two parts
# First part requires a reboot in order to create kvmd users and groups
# Second part will start the necessary kvmd services
# if /etc/kvmd/htpasswd exists, then make a backup
if [ -e /etc/kvmd/htpasswd ]; then cp /etc/kvmd/htpasswd /etc/kvmd/htpasswd.save; fi
### I uploaded all these into github on 05/22/23 -- so just copy them into correct location
cd ${APP_PATH}
cp -rf pistat /usr/local/bin/pistat
cp -rf pi-temp /usr/local/bin/pi-temp
cp -rf pikvm-info /usr/local/bin/pikvm-info
chmod +x /usr/local/bin/pi*
### fix for kvmd 3.230 and higher
ln -sf python3 /usr/bin/python
SERVICES="kvmd-nginx kvmd-webterm kvmd kvmd-fix kvmd-vnc kvmd-ipmi"
# added option to re-install by adding -f parameter (for use as platform switcher)
PYTHON_VERSION=$( python3 -V | awk '{print $2}' | cut -d'.' -f1,2 )
if [[ $( grep kvmd /etc/passwd | wc -l ) -eq 0 || "$1" == "-f" ]]; then
printf "\nRunning part 1 of PiKVM installer script v$VER by @srepac and @SilentWind\n" | tee -a $LOGFILE
get-platform
get-packages
install-kvmd-pkgs
boot-files
create-override
gen-ssl-certs
fix-udevrules
install-dependencies
! $NOTCHROOT || otg-devices
armbian-packages
systemctl disable --now janus ttyd
printf "\nEnd part 1 of PiKVM installer script v$VER by @srepac and @SilentWind\n" >> $LOGFILE
printf "\nReboot is required to create kvmd users and groups.\nPlease re-run this script after reboot to complete the install.\n" | tee -a $LOGFILE
# Fix paste-as-keys if running python 3.7
if [[ $( python3 -V | awk '{print $2}' | cut -d'.' -f1,2 ) == "3.7" ]]; then
sed -i -e 's/reversed//g' /usr/lib/python3.1*/site-packages/kvmd/keyboard/printer.py
fi
### run these to make sure kvmd users are created ###
echo "-> Ensuring KVMD users and groups ..." | tee -a $LOGFILE
systemd-sysusers /usr/lib/sysusers.d/kvmd.conf
systemd-sysusers /usr/lib/sysusers.d/kvmd-webterm.conf
# Ask user to press CTRL+C before reboot or ENTER to proceed with reboot
echo
! $NOTCHROOT || read -p "Press ENTER to continue or CTRL+C to break out of script."
! $NOTCHROOT || reboot
else
printf "\nRunning part 2 of PiKVM installer script v$VER by @srepac and @SilentWind\n" | tee -a $LOGFILE
echo "-> Re-installing janus ..." | tee -a $LOGFILE
apt reinstall -y janus > /dev/null 2>&1
### run these to make sure kvmd users are created ###
echo "-> Ensuring KVMD users and groups ..." | tee -a $LOGFILE
systemd-sysusers /usr/lib/sysusers.d/kvmd.conf
systemd-sysusers /usr/lib/sysusers.d/kvmd-webterm.conf
fix-nginx-symlinks
fix-python-symlinks
fix-webterm
fix-motd
fix-nfs-msd
fix-nginx
ocr-fix
set-ownership
create-kvmdfix
echo "-> Install python3 modules dbus_next and zstandard" | tee -a $LOGFILE
if [[ "$PYTHONVER" == "3.11" ]]; then
apt install -y python3-dbus-next python3-zstandard
else
pip3 install dbus_next zstandard
fi
### additional python pip dependencies for kvmd 3.238 and higher
case $PYTHONVER in
3.10*|3.[987]*)
pip3 install async-lru 2> /dev/null
### Fix for kvmd 3.291 -- only applies to python 3.10 ###
sed -i -e 's|gpiod.EdgeEvent|gpiod.LineEvent|g' /usr/lib/python3/dist-packages/kvmd/aiogp.py
sed -i -e 's|gpiod.line,|gpiod.Line,|g' /usr/lib/python3/dist-packages/kvmd/aiogp.py
;;
3.11*)
pip3 install async-lru --break-system-packages 2> /dev/null
;;
esac
apply-x86-mods
x86-fix-3.256
x86-fix-3.281
check-kvmd-works
enable-kvmd-svcs
update-logo
start-kvmd-svcs
printf "\nCheck kvmd devices\n\n" | tee -a $LOGFILE
ls -l /dev/kvmd* | tee -a $LOGFILE
printf "\nYou should see devices for keyboard, mouse, and video.\n" | tee -a $LOGFILE
printf "\nPoint a browser to https://$(hostname)\nIf it doesn't work, then reboot one last time.\nPlease make sure kvmd services are running after reboot.\n" | tee -a $LOGFILE
fi
cd $CWD
cp -rf web.css /etc/kvmd/web.css
systemctl status $SERVICES | grep Loaded | tee -a $LOGFILE
### fix totp.secret file permissions for use with 2FA
chmod go+r /etc/kvmd/totp.secret
chown kvmd:kvmd /etc/kvmd/totp.secret
### create rw and ro so that /usr/bin/kvmd-bootconfig doesn't fail
touch /usr/local/bin/rw /usr/local/bin/ro
chmod +x /usr/local/bin/rw /usr/local/bin/ro
### update default hostname info in webui to reflect current hostname
sed -i -e "s/localhost.localdomain/`hostname`/g" /etc/kvmd/meta.yaml
### restore htpasswd from previous install, if applies
if [ -e /etc/kvmd/htpasswd.save ]; then cp /etc/kvmd/htpasswd.save /etc/kvmd/htpasswd; fi
### instead of showing # fps dynamic, show REDACTED fps dynamic instead; USELESS fps meter fix
#sed -i -e 's|${__fps}|REDACTED|g' /usr/share/kvmd/web/share/js/kvm/stream_mjpeg.js
### fix kvmd-webterm 0.49 change that changed ttyd to kvmd-ttyd which broke webterm
sed -i -e 's/kvmd-ttyd/ttyd/g' /lib/systemd/system/kvmd-webterm.service
# get rid of this line, otherwise kvmd-nginx won't start properly since the nginx version is not 1.25 and higher
if [ -e /etc/kvmd/nginx/nginx.conf.mako ]; then
sed -i -e '/http2 on;/d' /etc/kvmd/nginx/nginx.conf.mako
fi
systemctl restart kvmd-nginx kvmd-webterm kvmd

884
install.sh Executable file → Normal file
View File

@ -1,108 +1,640 @@
#!/bin/bash
# https://github.com/srepac/kvmd-armbian
# modified by SilentWind 2024-06-17
# modified by xe5700 2021-11-04 xe5700@outlook.com
# modified by NewbieOrange 2021-11-04
# created by @srepac 08/09/2021 srepac@kvmnerds.com
# Scripted Installer of Pi-KVM on Armbian 32-bit and 64-bit (as long as it's running python 3.10 or higher)
#
# *** MSD is disabled by default ***
#
# Mass Storage Device requires the use of a USB thumbdrive or SSD and will need to be added in /etc/fstab
: '
# SAMPLE /etc/fstab entry for USB drive with only one partition formatted as ext4 for the entire drive:
ARCH=$(uname -n)
MACHINE=$(uname -o -s -r -m)
PYVER=$(python3 -V)
CURRENTWD=$PWD
FIND_FILE="/etc/sudoers"
FIND_STR="onecloud_gpio.sh"
/dev/sda1 /var/lib/kvmd/msd ext4 nodev,nosuid,noexec,ro,errors=remount-ro,data=journal,X-kvmd.otgmsd-root=/var/lib/kvmd/msd,X-kvmd.otgmsd-user=kvmd 0 0
#检查架构和Python版本
check_environment(){
echo -e "设备名称:$MACHINE\nPython版本$PYVER"
if [ ! $ARCH = "onecloud" ]; then
echo -e "此脚本暂不支持armv7l架构以外的设备\n退出脚本"
exit
'
# NOTE: This was tested on a new install of raspbian desktop and lite versions, but should also work on an existing install.
#
# Last change 20240526 2345 PDT
VER=3.4
set +x
PIKVMREPO="https://files.pikvm.org/repos/arch/rpi4"
KVMDFILE="kvmd-3.291-1-any.pkg.tar.xz"
KVMDCACHE="/var/cache/kvmd"; mkdir -p $KVMDCACHE
PKGINFO="${KVMDCACHE}/packages.txt"
APP_PATH=$(readlink -f $(dirname $0))
LOGFILE="${KVMDCACHE}/installer.log"; touch $LOGFILE; echo "==== $( date ) ====" >> $LOGFILE
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
echo "usage: $0 [-f] where -f will force re-install new pikvm platform"
exit 1
fi
CWD=`pwd`
WHOAMI=$( whoami )
if [ "$WHOAMI" != "root" ]; then
echo "$WHOAMI, please run script as root."
exit 1
fi
PYTHONVER=$( python3 -V | cut -d' ' -f2 | cut -d'.' -f1,2 )
case $PYTHONVER in
3.10|3.11)
echo "Python $PYTHONVER is supported." | tee -a $LOGFILE
;;
*)
echo "Python $PYTHONVER is NOT supported. Please make sure you have python3.10 or higher installed. Exiting." | tee -a $LOGFILE
exit 1
;;
esac
MAKER=$(tr -d '\0' < /proc/device-tree/model | awk '{print $1}')
gen-ssl-certs() {
cd /etc/kvmd/nginx/ssl
openssl ecparam -out server.key -name prime256v1 -genkey
openssl req -new -x509 -sha256 -nodes -key server.key -out server.crt -days 3650 \
-subj "/C=US/ST=Denial/L=Denial/O=Pi-KVM/OU=Pi-KVM/CN=$(hostname)"
cp server* /etc/kvmd/vnc/ssl/
cd ${APP_PATH}
} # end gen-ssl-certs
create-override() {
if [ $( grep ^kvmd: /etc/kvmd/override.yaml | wc -l ) -eq 0 ]; then
if [[ $( echo $platform | grep usb | wc -l ) -eq 1 ]]; then
cat <<USBOVERRIDE >> /etc/kvmd/override.yaml
kvmd:
hid:
mouse_alt:
device: /dev/kvmd-hid-mouse-alt # allow relative mouse mode
msd:
type: disabled
atx:
type: disabled
streamer:
#forever: true
cmd_append:
- "--slowdown" # so target doesn't have to reboot
resolution:
default: 1280x720
USBOVERRIDE
else
cat <<CSIOVERRIDE >> /etc/kvmd/override.yaml
kvmd:
### disable fan socket check ###
info:
fan:
unix: ''
hid:
mouse_alt:
device: /dev/kvmd-hid-mouse-alt
msd:
type: disabled
streamer:
#forever: true
cmd_append:
- "--slowdown" # so target doesn't have to reboot
CSIOVERRIDE
fi
fi
} # end create-override
install-python-packages() {
echo "apt 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-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-build
python3-pyotp python3-qrcode python3-serial"
apt 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-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-build\
python3-pyotp python3-qrcode python3-serial >> $LOGFILE
} # end install python-packages
otg-devices() {
modprobe libcomposite
if [ ! -e /sys/kernel/config/usb_gadget/kvmd ]; then
mkdir -p /sys/kernel/config/usb_gadget/kvmd/functions
cd /sys/kernel/config/usb_gadget/kvmd/functions
mkdir hid.usb0 hid.usb1 hid.usb2 mass_storage.usb0
fi
cd ${APP_PATH}
} # end otg-device creation
boot-files() {
# Remove OTG serial (Orange pi zero's kernel not support it)
sed -i '/^g_serial/d' /etc/modules
# /etc/modules required entries for DWC2, HID and I2C
if [[ $( grep -w dwc2 /etc/modules | wc -l ) -eq 0 ]]; then
echo "dwc2" >> /etc/modules
fi
if [[ $( grep -w libcomposite /etc/modules | wc -l ) -eq 0 ]]; then
echo "libcomposite" >> /etc/modules
fi
if [[ $( grep -w i2c-dev /etc/modules | wc -l ) -eq 0 ]]; then
echo "i2c-dev" >> /etc/modules
fi
if [[ "$PYVER" != *"3.10"* && $(which python3.10) != *"python"* ]]; then
echo -e "您似乎没有安装 Python 3.10\n退出脚本"
exit
fi
}
printf "\n/etc/modules\n\n" | tee -a $LOGFILE
cat /etc/modules | tee -a $LOGFILE
} # end of necessary boot files
#安装依赖软件
install_dependencies(){
bash <(curl -sSL https://gitee.com/SuperManito/LinuxMirrors/raw/main/ChangeMirrors.sh) --source mirrors.tuna.tsinghua.edu.cn --updata-software false --web-protocol http && echo "换源成功!"
echo -e "正在安装依赖软件......"
apt install -y python3.10 python3-pip python3-dev patch iptables nginx \
tesseract-ocr tesseract-ocr-eng janus libevent-dev libgpiod-dev \
tesseract-ocr-chi-sim libjpeg-dev libfreetype6-dev gcc
}
get-packages() {
printf "\n\n-> Getting Pi-KVM packages from ${PIKVMREPO}\n\n" | tee -a $LOGFILE
cp -f ${APP_PATH}/kvmd-packages/* ${KVMDCACHE}
#安装PiKVM
install_pikvm(){
echo "正在安装PiKVM......"
dpkg -i ./fruity-pikvm_0.2_armhf.deb
systemctl enable kvmd-vnc
systemctl disable nginx kvmd-janus
#rm -f /lib/systemd/system/nginx.service
#rm -f /lib/systemd/system/kvmd-janus.service && systemctl daemon-reload
echo "PiKVM安装成功"
cd $CURRENTWD
cp -f ./patch/onecloud_gpio.sh /usr/bin
chmod +x /usr/bin/onecloud_gpio.sh
echo "GPIO脚本移动成功"
cp -f ./patch/hw.py /usr/local/lib/python3.10/kvmd-packages/kvmd/apps/kvmd/info/
chmod +x /usr/local/lib/python3.10/kvmd-packages/kvmd/apps/kvmd/info/hw.py
cp -f ./config/main.yaml /etc/kvmd/ && cp -f ./config/override.yaml /etc/kvmd/
echo "配置文件替换成功"
kvmd -m
}
} # end get-packages function
#应用补丁
add_patches(){
if [ ! -f `grep -c "$FIND_STR" $FIND_FILE` ]; then
echo kvmd ALL=\(ALL\) NOPASSWD: /usr/bin/onecloud_gpio.sh >> /etc/sudoers
get-platform() {
platform="kvmd-platform-v2-hdmiusb-rpi4";
echo
echo "Platform selected -> $platform" | tee -a $LOGFILE
echo
} # end get-platform
install-kvmd-pkgs() {
cd /
INSTLOG="${KVMDCACHE}/installed_ver.txt"; rm -f $INSTLOG
date > $INSTLOG
# uncompress platform package first
i=$( ls ${KVMDCACHE}/${platform}*.tar.xz ) ### install the most up to date kvmd-platform package
# change the log entry to show 3.291 platform installed as we'll be forcing kvmd-3.291 instead of latest/greatest kvmd
_platformver=$( echo $i | sed -e 's/3\.29[2-9]*/3.291/g' -e 's/3\.3[0-9]*/3.291/g' -e 's/3.2911/3.291/g' -e 's/4\.[0-9].*-/3.291-/g' )
echo "-> Extracting package $_platformver into /" | tee -a $INSTLOG
tar xfJ $i
# then uncompress, kvmd-{version}, kvmd-webterm, and janus packages
for i in $( ls ${KVMDCACHE}/*.tar.xz | egrep 'kvmd-[0-9]|webterm' )
do
case $i in
*kvmd-3.29[2-9]*|*kvmd-3.[3-9]*|*kvmd-[45].[1-9]*) # if latest/greatest is 3.292 and higher, then force 3.291 install
echo "*** Force install kvmd 3.291 ***" | tee -a $LOGFILE
i=$KVMDCACHE/$KVMDFILE
;;
*)
;;
esac
echo "-> Extracting package $i into /" >> $INSTLOG
tar xfJ $i
done
# uncompress janus package if /usr/bin/janus doesn't exist
if [ ! -e /usr/bin/janus ]; then
i=$( ls ${KVMDCACHE}/*.tar.xz | egrep janus | grep -v 1x )
echo "-> Extracting package $i into /" >> $INSTLOG
tar xfJ $i
else # confirm that /usr/bin/janus actually runs properly
/usr/bin/janus --version > /dev/null 2>> $LOGFILE
if [ $? -eq 0 ]; then
echo "You have a working valid janus binary." | tee -a $LOGFILE
else # error status code, so uncompress from REPO package
#i=$( ls ${KVMDCACHE}/*.tar.xz | egrep janus )
#echo "-> Extracting package $i into /" >> $INSTLOG
#tar xfJ $i
apt-get remove janus janus-dev -y >> $LOGFILE
apt-get install janus janus-dev -y >> $LOGFILE
fi
fi
if [ ! -f "/usr/local/lib/python3.10/kvmd-packages/3.198msd.patch" ]; then
cd $CURRENTWD
cp ./patch/3.198msd.patch /usr/local/lib/python3.10/kvmd-packages/ && cd /usr/local/lib/python3.10/kvmd-packages/
patch -s -p0 < 3.198msd.patch
echo "MSD补丁应用成功"
fi
cd ${APP_PATH}
} # end install-kvmd-pkgs
cd $CURRENTWD
cp -f ./patch/chinese.patch /usr/share/kvmd/web/ && cd /usr/share/kvmd/web/
patch -s -p0 < chinese.patch
echo -e "中文补丁应用成功"
pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple/
pip3 install -U Pillow
fix-udevrules() {
# for hdmiusb, replace %b with 1-1.4:1.0 in /etc/udev/rules.d/99-kvmd.rules
sed -i -e 's+\%b+1-1.4:1.0+g' /etc/udev/rules.d/99-kvmd.rules | tee -a $LOGFILE
echo
cat /etc/udev/rules.d/99-kvmd.rules | tee -a $LOGFILE
} # end fix-udevrules
}
enable-kvmd-svcs() {
# enable KVMD services but don't start them
echo "-> Enabling $SERVICES services, but do not start them." | tee -a $LOGFILE
systemctl enable $SERVICES
} # end enable-kvmd-svcs
#设置网页终端欢迎语
fix_motd(){
#cd $CURRENTWD
if [ -e /etc/motd ]; then rm /etc/motd; fi
cat > /usr/bin/armbian-motd << EOF
#!/bin/sh
if [ -e /etc/update-motd.d/10-armbian-header ]; then /etc/update-motd.d/10-armbian-header; fi
if [ -e /etc/update-motd.d/30-armbian-sysinfo ]; then /etc/update-motd.d/30-armbian-sysinfo; fi
build-ustreamer() {
printf "\n\n-> Building ustreamer\n\n" | tee -a $LOGFILE
# Install packages needed for building ustreamer source
echo "apt install -y libevent-dev libjpeg-dev libbsd-dev libgpiod-dev libsystemd-dev janus-dev janus" | tee -a $LOGFILE
apt install -y libevent-dev libjpeg-dev libbsd-dev libgpiod-dev libsystemd-dev janus-dev janus >> $LOGFILE
printf " 欢迎使用 One-KVM基于开源程序 PiKVM 的 IP-KVM 应用
____________________________________________________________________________
# fix refcount.h
sed -i -e 's|^#include "refcount.h"$|#include "../refcount.h"|g' /usr/include/janus/plugins/plugin.h
要修改默认账户 admin 密码可使用 \"kvmd-htpasswd set admin\"
# Download ustreamer source and build it
cd /tmp
unzip ${APP_PATH}/sources/ustreamer-6.12.zip
cd ustreamer-6.12/
#添加WITH_PYTHON=1 使kvmd-vnc正常工作
make WITH_GPIO=1 WITH_SYSTEMD=1 WITH_JANUS=1 WITH_PYTHON=1 -j
#删除 --prefix=$(PREFIX) 修复无法安装pythgon包的问题
sed -i 's/--prefix=\$(PREFIX)//g' python/Makefile
make install WITH_PYTHON=1
# kvmd service is looking for /usr/bin/ustreamer
ln -sf /usr/local/bin/ustreamer* /usr/bin/
帮助链接:
* https://docs.pikvm.org
* https://one-kvm.mofeng.run/
* https://github.com/mofeng-git/One-KVM
"
EOF
chmod +x /usr/bin/armbian-motd /etc/update-motd.d/10-armbian-header /etc/update-motd.d/30-armbian-sysinfo
sed -i 's/cat \/etc\/motd/armbian-motd/g' /lib/systemd/system/kvmd-webterm.service
echo "fixed motd"
}
# add janus support
mkdir -p /usr/lib/ustreamer/janus
cp /tmp/ustreamer-6.12/janus/libjanus_ustreamer.so /usr/lib/ustreamer/janus
} # end build-ustreamer
#玩客云特定配置
onecloud_conf(){
if [ ! $ARCH = "onecloud" ]; then
echo -e "\n"
install-dependencies() {
echo
echo "-> Installing dependencies for pikvm" | tee -a $LOGFILE
echo "apt install -y make nginx python3 gcc unzip net-tools bc expect v4l-utils iptables vim dos2unix screen tmate nfs-common gpiod ffmpeg dialog iptables dnsmasq git python3-pip tesseract-ocr tesseract-ocr-eng libasound2-dev libsndfile-dev libspeexdsp-dev build-essential libssl-dev libffi-dev libevent libevent-core libevent-pthreads" | tee -a $LOGFILE
apt install -y make nginx python3 gcc unzip net-tools bc expect v4l-utils iptables vim dos2unix screen tmate nfs-common gpiod ffmpeg dialog iptables dnsmasq git python3-pip tesseract-ocr tesseract-ocr-eng libasound2-dev libsndfile-dev libspeexdsp-dev build-essential libssl-dev libffi-dev libevent libevent-core libevent-pthreads >> $LOGFILE
sed -i -e 's/#port=5353/port=5353/g' /etc/dnsmasq.conf
install-python-packages
echo "-> Make tesseract data link" | tee -a $LOGFILE
ln -sf /usr/share/tesseract-ocr/*/tessdata /usr/share/tessdata
echo "-> Install TTYD" | tee -a $LOGFILE
apt install -y ttyd | tee -a $LOGFILE
/usr/bin/ttyd -v | tee -a $LOGFILE
if [ ! -e /usr/local/bin/gpio ]; then
printf "\n\n-> Building wiringpi from source\n\n" | tee -a $LOGFILE
cd /tmp; rm -rf WiringPi-3.6
unzip ${APP_PATH}/sources/WiringPi-3.6.zip
cd WiringPi-3.6
./build
else
echo "为玩客云配置开机脚本"
printf "\n\n-> Wiringpi (gpio) is already installed.\n\n" | tee -a $LOGFILE
fi
gpio -v | tee -a $LOGFILE
echo "-> Install ustreamer" | tee -a $LOGFILE
if [ ! -e /usr/bin/ustreamer ]; then
cd /tmp
### required dependent packages for ustreamer ###
build-ustreamer
cd ${APP_PATH}
fi
echo -n "ustreamer version: " | tee -a $LOGFILE
ustreamer -v | tee -a $LOGFILE
ustreamer --features | tee -a $LOGFILE
} # end install-dependencies
python-pkg-dir() {
# debian system python3 no alias
# create quick python script to show where python packages need to go
cat << MYSCRIPT > /tmp/syspath.py
#!$(which python3)
import sys
print (sys.path)
MYSCRIPT
chmod +x /tmp/syspath.py
#PYTHONDIR=$( /tmp/syspath.py | awk -F, '{print $NF}' | cut -d"'" -f2 )
### hardcode path for armbian/raspbian
PYTHONDIR="/usr/lib/python3/dist-packages"
} # end python-pkg-dir
fix-nginx-symlinks() {
# disable default nginx service since we will use kvmd-nginx instead
echo
echo "-> Disabling nginx service, so that we can use kvmd-nginx instead" | tee -a $LOGFILE
systemctl disable --now nginx
# setup symlinks
echo
echo "-> Creating symlinks for use with kvmd python scripts" | tee -a $LOGFILE
if [ ! -e /usr/bin/nginx ]; then ln -sf /usr/sbin/nginx /usr/bin/; fi
if [ ! -e /usr/sbin/python ]; then ln -sf /usr/bin/python3 /usr/sbin/python; fi
if [ ! -e /usr/bin/iptables ]; then ln -sf /usr/sbin/iptables /usr/bin/iptables; fi
if [ ! -e /usr/bin/vcgencmd ]; then ln -sf /opt/vc/bin/* /usr/bin/; chmod +x /opt/vc/bin/*; fi
python-pkg-dir
if [ ! -e $PYTHONDIR/kvmd ]; then
# Debian python版本比 pikvm官方的低一些
# in case new kvmd packages are now using python 3.11
ln -sf /usr/lib/python3.1*/site-packages/kvmd* ${PYTHONDIR}
fi
} # end fix-nginx-symlinks
fix-python-symlinks(){
python-pkg-dir
if [ ! -e $PYTHONDIR/kvmd ]; then
# Debian python版本比 pikvm官方的低一些
ln -sf /usr/lib/python3.1*/site-packages/kvmd* ${PYTHONDIR}
fi
}
apply-custom-patch(){
read -p "Do you want apply old kernel msd patch? [y/n]" answer
case $answer in
n|N|no|No)
echo 'You skipped this patch.'
;;
y|Y|Yes|yes)
./patches/custom/old-kernel-msd/apply.sh
;;
*)
echo "Try again.";;
esac
}
fix-webterm() {
echo
echo "-> Creating kvmd-webterm homedir" | tee -a $LOGFILE
mkdir -p /home/kvmd-webterm
chown kvmd-webterm /home/kvmd-webterm
ls -ld /home/kvmd-webterm | tee -a $LOGFILE
# remove -W option since ttyd installed on raspbian/armbian is 1.6.3 (-W option only works with ttyd 1.7.x)
_ttydver=$( /usr/bin/ttyd -v | awk '{print $NF}' )
case $_ttydver in
1.6*)
echo "ttyd $_ttydver found. Removing -W from /lib/systemd/system/kvmd-webterm.service"
sed -i -e '/-W \\/d' /lib/systemd/system/kvmd-webterm.service
;;
1.7*)
echo "ttyd $_ttydver found. Nothing to do."
;;
esac
# add sudoers entry for kvmd-webterm user to be able to run sudo
echo "kvmd-webterm ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/kvmd-webterm; chmod 440 /etc/sudoers.d/kvmd-webterm
} # end fix-webterm
create-kvmdfix() {
# Create kvmd-fix service and script
cat <<ENDSERVICE > /lib/systemd/system/kvmd-fix.service
[Unit]
Description=KVMD Fixes
After=network.target network-online.target nss-lookup.target
Before=kvmd.service
[Service]
User=root
Type=simple
ExecStart=/usr/bin/kvmd-fix
[Install]
WantedBy=multi-user.target
ENDSERVICE
cat <<SCRIPTEND > /usr/bin/kvmd-fix
#!/bin/bash
# Written by @srepac
# 1. Properly set group ownership of /dev/gpio*
# 2. fix /dev/kvmd-video symlink to point to /dev/video1 (Amglogic Device video0 is not usb device)
#
### These fixes are required in order for kvmd service to start properly
#
set -x
chgrp gpio /dev/gpio*
chmod 660 /dev/gpio*
ls -l /dev/gpio*
udevadm trigger
ls -l /dev/kvmd-video
if [ \$( systemctl | grep kvmd-oled | grep -c activ ) -eq 0 ]; then
echo "kvmd-oled service is not enabled."
exit 0
else
echo "kvmd-oled service is enabled and activated."
fi
### kvmd-oled fix: swap i2c-0 <-> i2c-1 (code is looking for I2C oled on i2c-1)
# pins #1 - 3.3v, #3 - SDA, #5 - SCL, and #9 - GND
i2cget -y 0 0x3c
if [ \$? -eq 0 ]; then
echo "-> Found valid I2C OLED at i2c-0. Applying I2C OLED fix."
cd /dev
# rename i2c-0 -> i2c-9, move i2c-1 to i2c-0, and rename the good i2c-9 to i2c-1
mv i2c-0 i2c-9
mv i2c-1 i2c-0
mv i2c-9 i2c-1
# restart kvmd-oled service
systemctl restart kvmd-oled
else
echo "-> I2C OLED fix already applied and OLED should be showing info."
fi
SCRIPTEND
chmod +x /usr/bin/kvmd-fix
} # end create-kvmdfix
set-ownership() {
# set proper ownership of password files and kvmd-webterm homedir
cd /etc/kvmd
chown kvmd:kvmd htpasswd
chown kvmd-ipmi:kvmd-ipmi ipmipasswd
chown kvmd-vnc:kvmd-vnc vncpasswd
chown kvmd-webterm /home/kvmd-webterm
# add kvmd user to video group (this is required in order to use CSI bridge with OMX and h264 support)
usermod -a -G video kvmd
# add kvmd user to dialout group (required for xh_hk4401 kvm switch support)
usermod -a -G dialout kvmd
} # end set-ownership
check-kvmd-works() {
echo "-> Checking kvmd -m works before continuing" | tee -a $LOGFILE
kvmd -m
invalid=1
! $NOTCHROOT || while [ $invalid -eq 1 ]; do
#kvmd -m
read -p "Did kvmd -m run properly? [y/n] " answer
case $answer in
n|N|no|No)
echo "Please install missing packages as per the kvmd -m output in another ssh/terminal."
;;
y|Y|Yes|yes)
invalid=0
;;
*)
echo "Try again.";;
esac
done
} # end check-kvmd-works
start-kvmd-svcs() {
#### start the main KVM services in order ####
# 1. nginx is the webserver
# 2. kvmd-otg is for OTG devices (keyboard/mouse, etc..)
# 3. kvmd is the main daemon
systemctl daemon-reload
systemctl restart $SERVICES
} # end start-kvmd-svcs
fix-motd() {
if [ -e /etc/motd ]; then rm /etc/motd; fi
cp armbian/armbian-motd /usr/bin/
chmod +x /usr/bin/armbian-motd
chmod +x /etc/update-motd.d/*
sed -i 's/cat \/etc\/motd/armbian-motd/g' /lib/systemd/system/kvmd-webterm.service
systemctl daemon-reload
# systemctl restart kvmd-webterm
} # end fix-motd
# 安装armbian的包
armbian-packages() {
mkdir -p /opt/vc/bin/
#cd /opt/vc/bin
if [ ! -e /usr/bin/vcgencmd ]; then
# Install vcgencmd for armbian platform
cp -rf armbian/opt/* /opt/vc/bin
else
ln -s /usr/bin/vcgencmd /opt/vc/bin/
fi
#cp -rf armbian/udev /etc/
cd ${APP_PATH}
} # end armbian-packages
fix-nfs-msd() {
NAME="aiofiles.tar"
LOCATION="/usr/lib/python3.11/site-packages"
echo "-> Extracting $NAME into $LOCATION" | tee -a $LOGFILE
tar xvf $NAME -C $LOCATION
echo "-> Renaming original aiofiles and creating symlink to correct aiofiles" | tee -a $LOGFILE
cd /usr/lib/python3/dist-packages
mv aiofiles aiofiles.$(date +%Y%m%d.%H%M)
ln -s $LOCATION/aiofiles .
ls -ld aiofiles* | tail -5
}
fix-nginx() {
#set -x
KERNEL=$( uname -r | awk -F\- '{print $1}' )
ARCH=$( uname -r | awk -F\- '{print $NF}' )
echo "KERNEL: $KERNEL ARCH: $ARCH" | tee -a $LOGFILE
case $ARCH in
ARCH) SEARCHKEY=nginx-mainline;;
*) SEARCHKEY="nginx/";;
esac
HTTPSCONF="/etc/kvmd/nginx/listen-https.conf"
echo "HTTPSCONF BEFORE: $HTTPSCONF" | tee -a $LOGFILE
cat $HTTPSCONF | tee -a $LOGFILE
if [[ ! -e /usr/local/bin/pikvm-info || ! -e /tmp/pacmanquery ]]; then
cp -f ${APP_PATH}/pikvm-info /usr/local/bin/pikvm-info
chmod +x /usr/local/bin/pikvm-info
echo "Getting list of packages installed..." | tee -a $LOGFILE
pikvm-info > /dev/null ### this generates /tmp/pacmanquery with list of installed pkgs
fi
NGINXVER=$( grep $SEARCHKEY /tmp/pacmanquery | awk '{print $1}' | cut -d'.' -f1,2 )
echo
echo "NGINX version installed: $NGINXVER" | tee -a $LOGFILE
case $NGINXVER in
1.2[56789]|1.3*|1.4*|1.5*) # nginx version 1.25 and higher
cat << NEW_CONF > $HTTPSCONF
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
NEW_CONF
;;
1.18|*) # nginx version 1.18 and lower
cat << ORIG_CONF > $HTTPSCONF
listen 443 ssl http2;
listen [::]:443 ssl;
ORIG_CONF
;;
esac
echo "HTTPSCONF AFTER: $HTTPSCONF" | tee -a $LOGFILE
cat $HTTPSCONF | tee -a $LOGFILE
set +x
} # end fix-nginx
ocr-fix() { # create function
echo
echo "-> Apply OCR fix..." | tee -a $LOGFILE
set -x
# 1. verify that Pillow module is currently running 9.0.x
PILLOWVER=$( grep -i pillow $PIP3LIST | awk '{print $NF}' )
case $PILLOWVER in
9.*|8.*|7.*) # Pillow running at 9.x and lower
# 2. update Pillow to 10.0.0
pip3 install -U Pillow 2>> $LOGFILE
# 3. check that Pillow module is now running 10.0.0
pip3 list | grep -i pillow | tee -a $LOGFILE
#4. restart kvmd and confirm OCR now works.
systemctl restart kvmd
;;
10.*|11.*|12.*) # Pillow running at 10.x and higher
echo "Already running Pillow $PILLOWVER. Nothing to do." | tee -a $LOGFILE
;;
esac
set +x
echo
} # end ocr-fix
async-lru-fix() {
echo
echo "-> Ensuring async-lru is installed with version 2.x ..." | tee -a $LOGFILE
pip3 install async-lru 2> /dev/null
PIP3LIST="/tmp/pip3.list"; /bin/rm -f $PIP3LIST
pip3 list 2> /dev/null > $PIP3LIST
ASYNCLRUVER=$( grep -i 'async[-_]lru' $PIP3LIST | awk '{print $NF}' )
echo "ASYNC-LRU version: $ASYNCLRUVER"
case $ASYNCLRUVER in
2.*) echo "Nothing to do. aync-lru is already running $ASYNCLRUVER" | tee -a $LOFILE;;
1.*|*) pip3 install -U async_lru --break-system-packages | tee -a $LOGFILE;; # raspbian bookworm only installs 1.0.x, this forces 2.0.x
esac
} # end async-lru-fix
#fix for onecloud
onecloud_conf(){
if [ "$(hostname)" == "onecloud" ]; then
pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
apt install -y cpufrequtils
echo "-->Add /etc/rc.local and skip usbburning for onecloud"
cat <<EOF >/etc/rc.local
#!/bin/bash
echo "default-on" >/sys/class/leds/onecloud\:green\:alive/trigger
@ -110,26 +642,178 @@ echo "none" >/sys/class/leds/onecloud\:red\:alive/trigger
echo "none" >/sys/class/leds/onecloud\:blue\:alive/trigger
cpufreq-set -d 1200MHz -u 1200MHz
echo device > /sys/class/usb_role/c9040000.usb-role-switch/role
systemctl disable kvmd
systemctl disable kvmd && systemctl stop kvmd
systemctl start kvmd
exit 0
EOF
sed -i '97c #' /usr/lib/python3.11/site-packages/kvmd/apps/kvmd/info/hw.py
sed -i '106c #' /usr/lib/python3.11/site-packages/kvmd/apps/kvmd/info/hw.py
cp -f ./patches/onecloud_gpio.sh /usr/bin && chmod +x /usr/bin/onecloud_gpio.sh
cp -f ${APP_PATH}/conf/override-onecloud.yaml /etc/kvmd/override.yaml
#如果在CHROOT环境需设置NOTCHROOT=false
! $NOTCHROOT || gzip -dc ./patch/Boot_SkipUSBBurning.gz | dd of=/dev/mmcblk1 bs=512 seek=1 count=32767
! $NOTCHROOT || gzip -dc ${APP_PATH}/patches/Boot_SkipUSBBurning.gz | dd of=/dev/mmcblk1 bs=512 seek=1 count=32767
echo -e "\n"
if [ ! -f `grep -c "onecloud_gpio.sh" /etc/sudoers` ]; then
echo kvmd ALL=\(ALL\) NOPASSWD: /usr/bin/onecloud_gpio.sh >> /etc/sudoers
fi
fi
}
#打印完成信息
show_info(){
echo -e "安装结束重启之后即可开始使用One-KVM"
/usr/bin/armbian-motd
update-logo() {
sed -i -e 's|class="svg-gray"|class="svg-color"|g' /usr/share/kvmd/web/index.html
sed -i -e 's|target="_blank"><img class="svg-gray"|target="_blank"><img class="svg-color"|g' /usr/share/kvmd/web/kvm/index.html
### download opikvm-logo.svg and then overwrite logo.svg
cp -f ${APP_PATH}/opikvm-logo.svg /usr/share/kvmd/web/share/svg/opikvm-logo.svg
cd /usr/share/kvmd/web/share/svg
cp logo.svg logo.svg.old
cp opikvm-logo.svg logo.svg
# change some text in the main html page
#sed -i.bak -e 's/The Open Source KVM over IP/KVM over IP on non-Arch linux OS by @srepac/g' /usr/share/kvmd/web/index.html
#sed -i.bak -e 's/The Open Source KVM over IP/KVM over IP on non-Arch linux OS by @srepac/g' /usr/share/kvmd/web/kvm/index.html
#sed -i.backup -e 's|https://pikvm.org/support|https://discord.gg/YaJ87sVznc|g' /usr/share/kvmd/web/kvm/index.html
#sed -i.backup -e 's|https://pikvm.org/support|https://discord.gg/YaJ87sVznc|g' /usr/share/kvmd/web/index.html
cd
}
check_environment
install_dependencies
install_pikvm
add_patches
fix_motd
onecloud_conf
show_info
### MAIN STARTS HERE ###
# Install is done in two parts
# First part requires a reboot in order to create kvmd users and groups
# Second part will start the necessary kvmd services
# if /etc/kvmd/htpasswd exists, then make a backup
if [ -e /etc/kvmd/htpasswd ]; then cp /etc/kvmd/htpasswd /etc/kvmd/htpasswd.save; fi
### I uploaded all these into github on 05/22/23 -- so just copy them into correct location
cd ${APP_PATH}
cp -rf pistat /usr/local/bin/pistat
cp -rf pi-temp /usr/local/bin/pi-temp
cp -rf pikvm-info /usr/local/bin/pikvm-info
chmod +x /usr/local/bin/pi*
### fix for kvmd 3.230 and higher
ln -sf python3 /usr/bin/python
SERVICES="kvmd-nginx kvmd-webterm kvmd-otg kvmd kvmd-fix kvmd-vnc kvmd-ipmi"
# added option to re-install by adding -f parameter (for use as platform switcher)
PYTHON_VERSION=$( python3 -V | awk '{print $2}' | cut -d'.' -f1,2 )
if [[ $( grep kvmd /etc/passwd | wc -l ) -eq 0 || "$1" == "-f" ]]; then
printf "\nRunning part 1 of PiKVM installer script v$VER by @srepac and @SilentWind\n" | tee -a $LOGFILE
get-platform
get-packages
install-kvmd-pkgs
boot-files
create-override
gen-ssl-certs
fix-udevrules
install-dependencies
! $NOTCHROOT || otg-devices
armbian-packages
onecloud_conf #set for onecloud
systemctl disable --now janus ttyd
printf "\nEnd part 1 of PiKVM installer script v$VER by @srepac and @SilentWind\n" >> $LOGFILE
printf "\nReboot is required to create kvmd users and groups.\nPlease re-run this script after reboot to complete the install.\n" | tee -a $LOGFILE
# Fix paste-as-keys if running python 3.7
if [[ $( python3 -V | awk '{print $2}' | cut -d'.' -f1,2 ) == "3.7" ]]; then
sed -i -e 's/reversed//g' /usr/lib/python3.1*/site-packages/kvmd/keyboard/printer.py
fi
### run these to make sure kvmd users are created ###
echo "-> Ensuring KVMD users and groups ..." | tee -a $LOGFILE
systemd-sysusers /usr/lib/sysusers.d/kvmd.conf
systemd-sysusers /usr/lib/sysusers.d/kvmd-webterm.conf
# Ask user to press CTRL+C before reboot or ENTER to proceed with reboot
echo
! $NOTCHROOT || read -p "Press ENTER to continue or CTRL+C to break out of script."
! $NOTCHROOT || reboot
else
printf "\nRunning part 2 of PiKVM installer script v$VER by @srepac and @SilentWind\n" | tee -a $LOGFILE
echo "-> Re-installing janus ..." | tee -a $LOGFILE
apt reinstall -y janus > /dev/null 2>&1
### run these to make sure kvmd users are created ###
echo "-> Ensuring KVMD users and groups ..." | tee -a $LOGFILE
systemd-sysusers /usr/lib/sysusers.d/kvmd.conf
systemd-sysusers /usr/lib/sysusers.d/kvmd-webterm.conf
fix-nginx-symlinks
fix-python-symlinks
fix-webterm
fix-motd
fix-nfs-msd
fix-nginx
async-lru-fix
ocr-fix
set-ownership
create-kvmdfix
echo "-> Install python3 modules dbus_next and zstandard" | tee -a $LOGFILE
if [[ "$PYTHONVER" == "3.11" ]]; then
apt install -y python3-dbus-next python3-zstandard
else
pip3 install dbus_next zstandard
fi
### additional python pip dependencies for kvmd 3.238 and higher
case $PYTHONVER in
3.10*|3.[987]*)
pip3 install async-lru 2> /dev/null
### Fix for kvmd 3.291 -- only applies to python 3.10 ###
sed -i -e 's|gpiod.EdgeEvent|gpiod.LineEvent|g' /usr/lib/python3/dist-packages/kvmd/aiogp.py
sed -i -e 's|gpiod.line,|gpiod.Line,|g' /usr/lib/python3/dist-packages/kvmd/aiogp.py
;;
3.11*)
pip3 install async-lru --break-system-packages 2> /dev/null
;;
esac
check-kvmd-works
enable-kvmd-svcs
update-logo
start-kvmd-svcs
printf "\nCheck kvmd devices\n\n" | tee -a $LOGFILE
ls -l /dev/kvmd* | tee -a $LOGFILE
printf "\nYou should see devices for keyboard, mouse, and video.\n" | tee -a $LOGFILE
printf "\nPoint a browser to https://$(hostname)\nIf it doesn't work, then reboot one last time.\nPlease make sure kvmd services are running after reboot.\n" | tee -a $LOGFILE
fi
cd $CWD
cp -rf web.css /etc/kvmd/web.css
systemctl status $SERVICES | grep Loaded | tee -a $LOGFILE
### fix totp.secret file permissions for use with 2FA
chmod go+r /etc/kvmd/totp.secret
chown kvmd:kvmd /etc/kvmd/totp.secret
### create rw and ro so that /usr/bin/kvmd-bootconfig doesn't fail
touch /usr/local/bin/rw /usr/local/bin/ro
chmod +x /usr/local/bin/rw /usr/local/bin/ro
### update default hostname info in webui to reflect current hostname
sed -i -e "s/localhost.localdomain/`hostname`/g" /etc/kvmd/meta.yaml
### restore htpasswd from previous install, if applies
if [ -e /etc/kvmd/htpasswd.save ]; then cp /etc/kvmd/htpasswd.save /etc/kvmd/htpasswd; fi
### instead of showing # fps dynamic, show REDACTED fps dynamic instead; USELESS fps meter fix
#sed -i -e 's|${__fps}|REDACTED|g' /usr/share/kvmd/web/share/js/kvm/stream_mjpeg.js
### fix kvmd-webterm 0.49 change that changed ttyd to kvmd-ttyd which broke webterm
sed -i -e 's/kvmd-ttyd/ttyd/g' /lib/systemd/system/kvmd-webterm.service
# get rid of this line, otherwise kvmd-nginx won't start properly since the nginx version is not 1.25 and higher
if [ -e /etc/kvmd/nginx/nginx.conf.mako ]; then
sed -i -e '/http2 on;/d' /etc/kvmd/nginx/nginx.conf.mako
fi
systemctl restart kvmd-nginx kvmd-webterm kvmd

Binary file not shown.

Binary file not shown.

Binary file not shown.

9
kvmd_display_install.sh Executable file → Normal file
View File

@ -21,16 +21,19 @@ RestartSec=3
AmbientCapabilities=CAP_NET_RAW
LimitNOFILE=65536
UMask=0117
ExecStart=/usr/share/kvmd/display_when_ustream_exists.sh
ExecStart=/usr/share/kvmd/display.sh
TimeoutStopSec=10
KillMode=mixed
[Install]
WantedBy=multi-user.target
EOF
cp -f ./patch/stream.sh /usr/share/kvmd/ && cp -f ./patch/stream_when_ustream_exists.sh /usr/share/kvmd/ && chmod +x /usr/share/kvmd/stream.sh /usr/share/kvmd/stream_when_ustream_exists.sh
cp -f ./patches/display.sh /usr/share/kvmd/ && chmod +x /usr/share/kvmd/display.sh
#启动服务
systemctl enable kvmd-display && systemctl start kvmd-display
systemctl daemon-reload
! $NOTCHROOT || systemctl enable kvmd-display
! $NOTCHROOT || systemctl start kvmd-display
echo "配置完成"
}
kvmd_display

16
kvmd_h264_install.sh Executable file → Normal file
View File

@ -49,15 +49,19 @@ EOF
#修改原有kvmd代码和配置文件
sed -i '17s/.*/ExecStart=\/usr\/bin\/janus --disable-colors --configs-folder=\/etc\/kvmd\/janus2/' /lib/systemd/system/kvmd-janus-static.service
sed -i 's/janus.plugin.ustreamer/janus.plugin.streaming/' /usr/share/kvmd/web/share/js/kvm/stream_janus.js
sed -i '293c \/\/' /usr/share/kvmd/web/share/js/kvm/stream_janus.js
sed -i '324c \/\/' /usr/share/kvmd/web/share/js/kvm/stream_janus.js
sed -i 's/request\": \"watch\", \"p/request\": \"watch\", \"id\" : 1, \"p/' /usr/share/kvmd/web/share/js/kvm/stream_janus.js
#补全网页JS文件并添加相应脚本
mkdir /usr/share/janus/javascript/
cp -f ./web/adapter.js /usr/share/janus/javascript/ && cp -f ./web/janus.js /usr/share/janus/javascript/
cp -f ./patch/stream.sh /usr/share/kvmd/ && cp -f ./patch/stream_when_ustream_exists.sh /usr/share/kvmd/ && chmod +x /usr/share/kvmd/stream.sh /usr/share/kvmd/stream_when_ustream_exists.sh
if [ ! -e /usr/share/janus/javascript/adapter.js ]; then
mkdir /usr/share/janus/javascript/
cp -f ./patches/adapter.js /usr/share/janus/javascript/ && cp -f ./patches/janus.js /usr/share/janus/javascript/
fi
cp -f ./patches/stream.sh /usr/share/kvmd/ && cp -f ./patches/stream_when_ustream_exists.sh /usr/share/kvmd/ && chmod +x /usr/share/kvmd/stream.sh /usr/share/kvmd/stream_when_ustream_exists.sh
#启动服务
systemctl enable kvmd-ffmpeg && systemctl enable kvmd-janus-static
systemctl start kvmd-ffmpeg && systemctl start kvmd-janus-static
systemctl daemon-reload
! $NOTCHROOT || systemctl enable kvmd-ffmpeg && systemctl enable kvmd-janus-static
! $NOTCHROOT || systemctl start kvmd-ffmpeg && systemctl start kvmd-janus-static
echo "配置完成"
}
kvmd_ffmpeg_h-264

49
opikvm-logo.svg Normal file
View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by Microsoft Visio, SVG Export opikvm-logo.svg Page-1 -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events"
xmlns:v="http://schemas.microsoft.com/visio/2003/SVGExtensions/" width="6.44656in" height="1.88949in"
viewBox="0 0 464.153 136.043" xml:space="preserve" color-interpolation-filters="sRGB" class="st2">
<v:documentProperties v:langID="1033" v:viewMarkup="false"/>
<style type="text/css">
<![CDATA[
.st1 {fill:#dc7a0e;stroke:none;stroke-linecap:butt;stroke-width:0.0283465}
.st2 {fill:none;fill-rule:evenodd;font-size:12px;overflow:visible;stroke-linecap:square;stroke-miterlimit:3}
]]>
</style>
<g v:mID="0" v:index="1" v:groupContext="foregroundPage">
<title>Page-1</title>
<v:pageProperties v:drawingScale="1" v:pageScale="1" v:drawingUnits="19" v:shadowOffsetX="9" v:shadowOffsetY="-9"/>
<g id="shape2-1" v:mID="2" v:groupContext="shape" transform="translate(0.0283465,-0.0283465)">
<title>path7</title>
<path d="M12.87 0.06 L451.23 0.06 C458.31 0.06 464.1 5.85 464.1 12.92 L464.1 123.18 C464.1 130.25 458.31 136.04 451.23
136.04 L12.87 136.04 C5.79 136.04 0 130.25 0 123.18 L0 12.92 C0 5.85 5.79 0.06 12.87 0.06 ZM203.52 64.5
L240.18 110.34 C240.88 111.21 241.03 112.33 240.56 113.35 C240.08 114.36 239.11 114.97 237.99 114.97 L214.83
114.97 C213.95 114.97 213.18 114.62 212.64 113.93 L204.76 103.99 L178.52 70.87 L178.52 112.14 C178.52 113.7
177.24 114.97 175.68 114.97 L157.86 114.97 C156.3 114.97 155.02 113.7 155.02 112.14 L155.02 23.96 C155.02
22.4 156.3 21.13 157.86 21.13 L175.68 21.13 C177.24 21.13 178.52 22.4 178.52 23.96 L178.52 62.1 L205.35
28.23 L210.15 22.18 C210.7 21.49 211.47 21.13 212.35 21.13 L232.26 21.13 C233.38 21.13 234.34 21.74 234.82
22.76 C235.3 23.77 235.14 24.89 234.44 25.77 L203.52 64.5 ZM245.12 30.92 L251.29 38.69 L282.84 78.38 L316.61
37.11 L328.71 22.31 C329.49 21.36 330.72 21.03 331.87 21.44 C333.02 21.86 333.74 22.91 333.74 24.13 L333.75
52.44 C333.75 53.11 333.53 53.7 333.11 54.22 L285.14 112.93 C284.58 113.61 283.81 113.98 282.93 113.97 C282.04
113.97 281.28 113.6 280.73 112.91 L229.61 48.81 C228.79 47.78 228.77 46.35 229.58 45.31 L240.66 30.95 C241.21
30.24 241.98 29.86 242.88 29.85 C243.78 29.85 244.56 30.22 245.12 30.92 ZM117.3 112.14 L117.3 56.32 C117.3
54.76 118.57 53.49 120.13 53.49 L137.96 53.49 C139.52 53.49 140.79 54.76 140.79 56.33 C140.79 96.9 140.79
73.79 140.79 112.14 C140.79 113.7 139.52 114.97 137.96 114.97 L120.13 114.97 C118.57 114.97 117.3 113.7
117.3 112.14 ZM120.13 21.13 L137.96 21.13 C139.52 21.13 140.79 22.4 140.79 23.96 L140.79 41.79 C140.79 43.35
139.52 44.62 137.96 44.62 C125.46 44.62 131.82 44.62 120.13 44.62 C118.57 44.62 117.3 43.35 117.3 41.79
L117.3 23.96 C117.3 22.4 118.57 21.13 120.13 21.13 ZM52.71 83.71 L52.71 112.14 C52.71 113.7 51.44 114.97
49.88 114.97 L31.91 114.97 C30.35 114.97 29.08 113.7 29.08 112.14 L29.08 23.96 C29.08 22.4 30.35 21.13 31.91
21.13 C52.01 21.13 74.47 21.13 94.57 21.13 C99.25 21.13 103.07 24.95 103.07 29.63 C103.07 46.41 103.07 58.43
103.07 75.2 C103.07 79.89 99.25 83.71 94.57 83.71 C81.4 83.71 65.88 83.71 52.71 83.71 ZM79.57 65.45 L79.57
38.58 L52.71 38.58 L52.71 65.45 L79.57 65.45 ZM342.63 112.14 L342.63 23.96 C342.63 22.4 343.9 21.13 345.47
21.13 L361.99 21.13 C362.88 21.13 363.65 21.5 364.2 22.2 L388.85 53.12 L413.5 22.2 C414.06 21.5 414.83 21.13
415.72 21.13 L432.24 21.13 C433.8 21.13 435.08 22.4 435.08 23.96 L435.08 112.14 C435.08 113.7 433.8 114.97
432.24 114.97 L414.69 114.97 C413.13 114.97 411.86 113.7 411.86 112.14 L411.86 56.9 L391.06 82.73 C390.5
83.42 389.74 83.79 388.85 83.79 C387.97 83.79 387.2 83.42 386.65 82.73 L365.85 56.9 L365.85 112.14 C365.85
113.7 364.58 114.97 363.01 114.97 L345.47 114.97 C343.9 114.97 342.63 113.7 342.63 112.14 Z" class="st1"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,43 +0,0 @@
#!/bin/bash
# 存储 stream.sh 的进程 ID
b_pid=""
cleanup() {
echo "Received SIGINT. Terminating the process group..."
[ -n "$b_pid" ] && pkill -9 -g $b_pid # 终止整个进程组
exit 0
}
# 捕获 SIGINT 信号
trap cleanup SIGINT
while true; do
# 检测是否有包含 "ustreamer" 的进程
if pgrep -f "/usr/bin/ustreamer " > /dev/null; then
# 如果存在,但是 stream.sh 进程不存在,执行 stream.sh 并记录其进程 ID
if [ -z "$b_pid" ]; then
echo "Found a process with 'ustreamer' in the command. Executing stream.sh in the background..."
setsid /usr/share/kvmd/diaplay.sh &
b_pid=$(ps -o pgid= $!)
echo "stream.sh started with PID: $b_pid"
else
echo "Process with 'ustreamer' is already running. Skipping..."
fi
else
# 如果不存在 "ustreamer" 进程,但是 stream.sh 进程存在,终止 stream.sh 并清除进程 ID
if [ -n "$b_pid" ]; then
echo "No process with 'ustreamer' found. Terminating stream.sh (PID: $b_pid)..."
pkill -9 -g $b_pid
b_pid=""
else
echo "No process with 'ustreamer' found. Waiting for the next check..."
fi
fi
# 等待一段时间,可以根据需要调整等待的时间间隔
sleep 1
done

769
patches/__init__.py Normal file
View File

@ -0,0 +1,769 @@
# ========================================================================== #
# #
# KVMD - The main PiKVM daemon. #
# #
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
# #
# ========================================================================== #
import sys
import os
import functools
import argparse
import logging
import logging.config
import pygments
import pygments.lexers.data
import pygments.formatters
from .. import tools
from ..mouse import MouseRange
from ..plugins import UnknownPluginError
from ..plugins.auth import get_auth_service_class
from ..plugins.hid import get_hid_class
from ..plugins.atx import get_atx_class
from ..plugins.msd import get_msd_class
from ..plugins.ugpio import UserGpioModes
from ..plugins.ugpio import BaseUserGpioDriver
from ..plugins.ugpio import get_ugpio_driver_class
from ..yamlconf import ConfigError
from ..yamlconf import manual_validated
from ..yamlconf import make_config
from ..yamlconf import Section
from ..yamlconf import Option
from ..yamlconf import build_raw_from_options
from ..yamlconf.dumper import make_config_dump
from ..yamlconf.loader import load_yaml_file
from ..yamlconf.merger import yaml_merge
from ..validators.basic import valid_stripped_string
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_int_f0
from ..validators.basic import valid_int_f1
from ..validators.basic import valid_float_f0
from ..validators.basic import valid_float_f01
from ..validators.basic import valid_string_list
from ..validators.auth import valid_user
from ..validators.auth import valid_users_list
from ..validators.os import valid_abs_path
from ..validators.os import valid_abs_file
from ..validators.os import valid_abs_dir
from ..validators.os import valid_unix_mode
from ..validators.os import valid_options
from ..validators.os import valid_command
from ..validators.net import valid_ip_or_host
from ..validators.net import valid_net
from ..validators.net import valid_port
from ..validators.net import valid_ports_list
from ..validators.net import valid_mac
from ..validators.net import valid_ssl_ciphers
from ..validators.hid import valid_hid_key
from ..validators.hid import valid_hid_mouse_output
from ..validators.hid import valid_hid_mouse_move
from ..validators.kvm import valid_stream_quality
from ..validators.kvm import valid_stream_fps
from ..validators.kvm import valid_stream_resolution
from ..validators.kvm import valid_stream_h264_bitrate
from ..validators.kvm import valid_stream_h264_gop
from ..validators.ugpio import valid_ugpio_driver
from ..validators.ugpio import valid_ugpio_channel
from ..validators.ugpio import valid_ugpio_mode
from ..validators.ugpio import valid_ugpio_view_title
from ..validators.ugpio import valid_ugpio_view_table
from ..validators.hw import valid_tty_speed
from ..validators.hw import valid_otg_gadget
from ..validators.hw import valid_otg_id
from ..validators.hw import valid_otg_ethernet
# =====
def init(
prog: (str | None)=None,
description: (str | None)=None,
add_help: bool=True,
check_run: bool=False,
cli_logging: bool=False,
argv: (list[str] | None)=None,
**load: bool,
) -> tuple[argparse.ArgumentParser, list[str], Section]:
argv = (argv or sys.argv)
assert len(argv) > 0
parser = argparse.ArgumentParser(
prog=(prog or argv[0]),
description=description,
add_help=add_help,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("-c", "--config", default="/etc/kvmd/main.yaml", type=valid_abs_file,
help="Set config file path", metavar="<file>")
parser.add_argument("-o", "--set-options", default=[], nargs="+",
help="Override config options list (like sec/sub/opt=value)", metavar="<k=v>",)
parser.add_argument("-m", "--dump-config", action="store_true",
help="View current configuration (include all overrides)")
if check_run:
parser.add_argument("--run", dest="run", action="store_true",
help="Run the service")
(options, remaining) = parser.parse_known_args(argv)
if options.dump_config:
_dump_config(_init_config(
config_path=options.config,
override_options=options.set_options,
load_auth=True,
load_hid=True,
load_atx=True,
load_msd=True,
load_gpio=True,
))
raise SystemExit()
config = _init_config(options.config, options.set_options, **load)
logging.captureWarnings(True)
logging.config.dictConfig(config.logging)
if cli_logging:
logging.getLogger().handlers[0].setFormatter(logging.Formatter(
"-- {levelname:>7} -- {message}",
style="{",
))
if check_run and not options.run:
raise SystemExit(
"To prevent accidental startup, you must specify the --run option to start.\n"
"Try the --help option to find out what this service does.\n"
"Make sure you understand exactly what you are doing!"
)
return (parser, remaining, config)
# =====
def _init_config(config_path: str, override_options: list[str], **load_flags: bool) -> Section:
config_path = os.path.expanduser(config_path)
try:
raw_config: dict = load_yaml_file(config_path)
except Exception as err:
raise SystemExit(f"ConfigError: Can't read config file {config_path!r}:\n{tools.efmt(err)}")
if not isinstance(raw_config, dict):
raise SystemExit(f"ConfigError: Top-level of the file {config_path!r} must be a dictionary")
scheme = _get_config_scheme()
try:
yaml_merge(raw_config, (raw_config.pop("override", {}) or {}))
yaml_merge(raw_config, build_raw_from_options(override_options), "raw CLI options")
_patch_raw(raw_config)
config = make_config(raw_config, scheme)
if _patch_dynamic(raw_config, config, scheme, **load_flags):
config = make_config(raw_config, scheme)
return config
except (ConfigError, UnknownPluginError) as err:
raise SystemExit(f"ConfigError: {err}")
def _patch_raw(raw_config: dict) -> None: # pylint: disable=too-many-branches
if isinstance(raw_config.get("otg"), dict):
for (old, new) in [
("msd", "msd"),
("acm", "serial"),
("drives", "drives"),
]:
if old in raw_config["otg"]:
if not isinstance(raw_config["otg"].get("devices"), dict):
raw_config["otg"]["devices"] = {}
raw_config["otg"]["devices"][new] = raw_config["otg"].pop(old)
if isinstance(raw_config.get("kvmd"), dict) and isinstance(raw_config["kvmd"].get("wol"), dict):
if not isinstance(raw_config["kvmd"].get("gpio"), dict):
raw_config["kvmd"]["gpio"] = {}
for section in ["drivers", "scheme"]:
if not isinstance(raw_config["kvmd"]["gpio"].get(section), dict):
raw_config["kvmd"]["gpio"][section] = {}
raw_config["kvmd"]["gpio"]["drivers"]["__wol__"] = {
"type": "wol",
**raw_config["kvmd"].pop("wol"),
}
raw_config["kvmd"]["gpio"]["scheme"]["__wol__"] = {
"driver": "__wol__",
"pin": 0,
"mode": "output",
"switch": False,
}
if isinstance(raw_config.get("kvmd"), dict) and isinstance(raw_config["kvmd"].get("streamer"), dict):
streamer_config = raw_config["kvmd"]["streamer"]
desired_fps = streamer_config.get("desired_fps")
if desired_fps is not None and not isinstance(desired_fps, dict):
streamer_config["desired_fps"] = {"default": desired_fps}
max_fps = streamer_config.get("max_fps")
if max_fps is not None:
if not isinstance(streamer_config.get("desired_fps"), dict):
streamer_config["desired_fps"] = {}
streamer_config["desired_fps"]["max"] = max_fps
del streamer_config["max_fps"]
resolution = streamer_config.get("resolution")
if resolution is not None and not isinstance(resolution, dict):
streamer_config["resolution"] = {"default": resolution}
available_resolutions = streamer_config.get("available_resolutions")
if available_resolutions is not None:
if not isinstance(streamer_config.get("resolution"), dict):
streamer_config["resolution"] = {}
streamer_config["resolution"]["available"] = available_resolutions
del streamer_config["available_resolutions"]
def _patch_dynamic( # pylint: disable=too-many-locals
raw_config: dict,
config: Section,
scheme: dict,
load_auth: bool=False,
load_hid: bool=False,
load_atx: bool=False,
load_msd: bool=False,
load_gpio: bool=False,
) -> bool:
rebuild = False
if load_auth:
scheme["kvmd"]["auth"]["internal"].update(get_auth_service_class(config.kvmd.auth.internal.type).get_plugin_options())
if config.kvmd.auth.external.type:
scheme["kvmd"]["auth"]["external"].update(get_auth_service_class(config.kvmd.auth.external.type).get_plugin_options())
rebuild = True
for (load, section, get_class) in [
(load_hid, "hid", get_hid_class),
(load_atx, "atx", get_atx_class),
(load_msd, "msd", get_msd_class),
]:
if load:
scheme["kvmd"][section].update(get_class(getattr(config.kvmd, section).type).get_plugin_options())
rebuild = True
if load_gpio:
driver: str
drivers: dict[str, type[BaseUserGpioDriver]] = {} # Name to drivers
for (driver, params) in { # type: ignore
"__gpio__": {},
**tools.rget(raw_config, "kvmd", "gpio", "drivers"),
}.items():
with manual_validated(driver, "kvmd", "gpio", "drivers", "<key>"):
driver = valid_ugpio_driver(driver)
driver_type = valid_stripped_string_not_empty(params.get("type", "gpio"))
driver_class = get_ugpio_driver_class(driver_type)
drivers[driver] = driver_class
scheme["kvmd"]["gpio"]["drivers"][driver] = {
"type": Option(driver_type, type=valid_stripped_string_not_empty),
**driver_class.get_plugin_options()
}
path = ("kvmd", "gpio", "scheme")
for (channel, params) in tools.rget(raw_config, *path).items():
with manual_validated(channel, *path, "<key>"):
channel = valid_ugpio_channel(channel)
driver = params.get("driver", "__gpio__")
with manual_validated(driver, *path, channel, "driver"):
driver = valid_ugpio_driver(driver, set(drivers))
mode: str = params.get("mode", "")
with manual_validated(mode, *path, channel, "mode"):
mode = valid_ugpio_mode(mode, drivers[driver].get_modes())
if params.get("pulse") == False: # noqa: E712 # pylint: disable=singleton-comparison
params["pulse"] = {"delay": 0}
scheme["kvmd"]["gpio"]["scheme"][channel] = {
"driver": Option("__gpio__", type=functools.partial(valid_ugpio_driver, variants=set(drivers))),
"pin": Option(None, type=drivers[driver].get_pin_validator()),
"mode": Option("", type=functools.partial(valid_ugpio_mode, variants=drivers[driver].get_modes())),
"inverted": Option(False, type=valid_bool),
**({
"busy_delay": Option(0.2, type=valid_float_f01),
"initial": Option(False, type=(lambda arg: (valid_bool(arg) if arg is not None else None))),
"switch": Option(True, type=valid_bool),
"pulse": { # type: ignore
"delay": Option(0.1, type=valid_float_f0),
"min_delay": Option(0.1, type=valid_float_f01),
"max_delay": Option(0.1, type=valid_float_f01),
},
} if mode == UserGpioModes.OUTPUT else { # input
"debounce": Option(0.1, type=valid_float_f0),
})
}
rebuild = True
return rebuild
def _dump_config(config: Section) -> None:
dump = make_config_dump(config)
if sys.stdout.isatty():
dump = pygments.highlight(
dump,
pygments.lexers.data.YamlLexer(),
pygments.formatters.TerminalFormatter(bg="dark"), # pylint: disable=no-member
)
print(dump)
def _get_config_scheme() -> dict:
return {
"logging": Option({}),
"kvmd": {
"server": {
"unix": Option("/run/kvmd/kvmd.sock", type=valid_abs_path, unpack_as="unix_path"),
"unix_rm": Option(True, type=valid_bool),
"unix_mode": Option(0o660, type=valid_unix_mode),
"heartbeat": Option(15.0, type=valid_float_f01),
"access_log_format": Option("[%P / %{X-Real-IP}i] '%r' => %s; size=%b ---"
" referer='%{Referer}i'; user_agent='%{User-Agent}i'"),
},
"auth": {
"enabled": Option(True, type=valid_bool),
"internal": {
"type": Option("htpasswd"),
"force_users": Option([], type=valid_users_list),
# Dynamic content
},
"external": {
"type": Option("", type=valid_stripped_string),
# Dynamic content
},
"totp": {
"secret": {
"file": Option("/etc/kvmd/totp.secret", type=valid_abs_path, if_empty=""),
},
},
},
"info": { # Accessed via global config, see kvmd/info for details
"meta": Option("/etc/kvmd/meta.yaml", type=valid_abs_file),
"extras": Option("/usr/share/kvmd/extras", type=valid_abs_dir),
"hw": {
"vcgencmd_cmd": Option(["/opt/vc/bin/vcgencmd"], type=valid_command),
"ignore_past": Option(False, type=valid_bool),
"state_poll": Option(10.0, type=valid_float_f01),
},
"fan": {
"daemon": Option("kvmd-fan", type=valid_stripped_string),
"unix": Option("", type=valid_abs_path, if_empty="", unpack_as="unix_path"),
"timeout": Option(5.0, type=valid_float_f01),
"state_poll": Option(5.0, type=valid_float_f01),
},
},
"log_reader": {
"enabled": Option(True, type=valid_bool),
},
"prometheus": {
"auth": {
"enabled": Option(True, type=valid_bool),
},
},
"hid": {
"type": Option("", type=valid_stripped_string_not_empty),
"keymap": Option("/usr/share/kvmd/keymaps/en-us", type=valid_abs_file),
"ignore_keys": Option([], type=functools.partial(valid_string_list, subval=valid_hid_key)),
"mouse_x_range": {
"min": Option(MouseRange.MIN, type=valid_hid_mouse_move),
"max": Option(MouseRange.MAX, type=valid_hid_mouse_move),
},
"mouse_y_range": {
"min": Option(MouseRange.MIN, type=valid_hid_mouse_move),
"max": Option(MouseRange.MAX, type=valid_hid_mouse_move),
},
# Dynamic content
},
"atx": {
"type": Option("", type=valid_stripped_string_not_empty),
# Dynamic content
},
"msd": {
"type": Option("", type=valid_stripped_string_not_empty),
# Dynamic content
},
"streamer": {
"forever": Option(False, type=valid_bool),
"reset_delay": Option(1.0, type=valid_float_f0),
"shutdown_delay": Option(10.0, type=valid_float_f01),
"state_poll": Option(1.0, type=valid_float_f01),
"quality": Option(80, type=valid_stream_quality, if_empty=0),
"resolution": {
"default": Option("", type=valid_stream_resolution, if_empty="", unpack_as="resolution"),
"available": Option(
[],
type=functools.partial(valid_string_list, subval=valid_stream_resolution),
unpack_as="available_resolutions",
),
},
"desired_fps": {
"default": Option(40, type=valid_stream_fps, unpack_as="desired_fps"),
"min": Option(0, type=valid_stream_fps, unpack_as="desired_fps_min"),
"max": Option(70, type=valid_stream_fps, unpack_as="desired_fps_max"),
},
"h264_bitrate": {
"default": Option(0, type=valid_stream_h264_bitrate, if_empty=0, unpack_as="h264_bitrate"),
"min": Option(25, type=valid_stream_h264_bitrate, unpack_as="h264_bitrate_min"),
"max": Option(20000, type=valid_stream_h264_bitrate, unpack_as="h264_bitrate_max"),
},
"h264_gop": {
"default": Option(30, type=valid_stream_h264_gop, unpack_as="h264_gop"),
"min": Option(0, type=valid_stream_h264_gop, unpack_as="h264_gop_min"),
"max": Option(60, type=valid_stream_h264_gop, unpack_as="h264_gop_max"),
},
"unix": Option("/run/kvmd/ustreamer.sock", type=valid_abs_path, unpack_as="unix_path"),
"timeout": Option(2.0, type=valid_float_f01),
"process_name_prefix": Option("kvmd/streamer"),
"cmd": Option(["/bin/true"], type=valid_command),
"cmd_remove": Option([], type=valid_options),
"cmd_append": Option([], type=valid_options),
},
"ocr": {
"langs": Option(["eng"], type=valid_string_list, unpack_as="default_langs"),
"tessdata": Option("/usr/share/tessdata", type=valid_stripped_string_not_empty, unpack_as="data_dir_path")
},
"snapshot": {
"idle_interval": Option(0.0, type=valid_float_f0),
"live_interval": Option(0.0, type=valid_float_f0),
"wakeup_key": Option("", type=valid_hid_key, if_empty=""),
"wakeup_move": Option(0, type=valid_hid_mouse_move),
"online_delay": Option(5.0, type=valid_float_f0),
"retries": Option(10, type=valid_int_f1),
"retries_delay": Option(3.0, type=valid_float_f01),
},
"gpio": {
"state_poll": Option(0.1, type=valid_float_f01),
"drivers": {}, # Dynamic content
"scheme": {}, # Dymanic content
"view": {
"header": {
"title": Option("GPIO", type=valid_ugpio_view_title),
},
"table": Option([], type=valid_ugpio_view_table),
},
},
},
"pst": {
"server": {
"unix": Option("/run/kvmd/pst.sock", type=valid_abs_path, unpack_as="unix_path"),
"unix_rm": Option(True, type=valid_bool),
"unix_mode": Option(0o660, type=valid_unix_mode),
"heartbeat": Option(15.0, type=valid_float_f01),
"access_log_format": Option("[%P / %{X-Real-IP}i] '%r' => %s; size=%b ---"
" referer='%{Referer}i'; user_agent='%{User-Agent}i'"),
},
"ro_retries_delay": Option(10.0, type=valid_float_f01),
"ro_cleanup_delay": Option(3.0, type=valid_float_f01),
"remount_cmd": Option([
"/usr/bin/sudo", "--non-interactive",
"/usr/bin/kvmd-helper-pst-remount", "{mode}",
], type=valid_command),
},
"otg": {
"vendor_id": Option(0x1D6B, type=valid_otg_id), # Linux Foundation
"product_id": Option(0x0104, type=valid_otg_id), # Multifunction Composite Gadget
"manufacturer": Option("PiKVM", type=valid_stripped_string),
"product": Option("Composite KVM Device", type=valid_stripped_string),
"serial": Option("CAFEBABE", type=valid_stripped_string, if_none=None),
"device_version": Option(-1, type=functools.partial(valid_number, min=-1, max=0xFFFF)),
"usb_version": Option(0x0200, type=valid_otg_id),
"max_power": Option(250, type=functools.partial(valid_number, min=50, max=500)),
"remote_wakeup": Option(False, type=valid_bool),
"gadget": Option("kvmd", type=valid_otg_gadget),
"config": Option("PiKVM device", type=valid_stripped_string_not_empty),
"udc": Option("", type=valid_stripped_string),
"init_delay": Option(3.0, type=valid_float_f01),
"user": Option("kvmd", type=valid_user),
"meta": Option("/run/kvmd/otg", type=valid_abs_path),
"devices": {
"hid": {
"keyboard": {
"start": Option(True, type=valid_bool),
},
"mouse": {
"start": Option(True, type=valid_bool),
},
},
"msd": {
"start": Option(True, type=valid_bool),
"default": {
"stall": Option(False, type=valid_bool),
"cdrom": Option(True, type=valid_bool),
"rw": Option(False, type=valid_bool),
"removable": Option(True, type=valid_bool),
"fua": Option(True, type=valid_bool),
},
},
"serial": {
"enabled": Option(False, type=valid_bool),
"start": Option(True, type=valid_bool),
},
"ethernet": {
"enabled": Option(False, type=valid_bool),
"start": Option(True, type=valid_bool),
"driver": Option("ecm", type=valid_otg_ethernet),
"host_mac": Option("", type=valid_mac, if_empty=""),
"kvm_mac": Option("", type=valid_mac, if_empty=""),
},
"drives": {
"enabled": Option(False, type=valid_bool),
"start": Option(True, type=valid_bool),
"count": Option(1, type=valid_int_f1),
"default": {
"stall": Option(False, type=valid_bool),
"cdrom": Option(False, type=valid_bool),
"rw": Option(True, type=valid_bool),
"removable": Option(True, type=valid_bool),
"fua": Option(True, type=valid_bool),
},
},
},
},
"otgnet": {
"iface": {
"net": Option("172.30.30.0/24", type=functools.partial(valid_net, v6=False)),
"ip_cmd": Option(["/usr/bin/ip"], type=valid_command),
},
"firewall": {
"allow_icmp": Option(True, type=valid_bool),
"allow_tcp": Option([], type=valid_ports_list),
"allow_udp": Option([67], type=valid_ports_list),
"forward_iface": Option("", type=valid_stripped_string),
"iptables_cmd": Option(["/usr/sbin/iptables", "--wait=5"], type=valid_command),
},
"commands": {
"pre_start_cmd": Option(["/bin/true", "pre-start"], type=valid_command),
"pre_start_cmd_remove": Option([], type=valid_options),
"pre_start_cmd_append": Option([], type=valid_options),
"post_start_cmd": Option([
"/usr/bin/systemd-run",
"--unit=kvmd-otgnet-dnsmasq",
"/usr/sbin/dnsmasq",
"--conf-file=/dev/null",
"--pid-file",
"--user=dnsmasq",
"--interface={iface}",
"--port=0",
"--dhcp-range={dhcp_ip_begin},{dhcp_ip_end},24h",
"--dhcp-leasefile=/run/kvmd/dnsmasq.lease",
"--dhcp-option={dhcp_option_3}",
"--dhcp-option=6",
"--keep-in-foreground",
], type=valid_command),
"post_start_cmd_remove": Option([], type=valid_options),
"post_start_cmd_append": Option([], type=valid_options),
"pre_stop_cmd": Option([
"/usr/bin/systemctl",
"stop",
"kvmd-otgnet-dnsmasq",
], type=valid_command),
"pre_stop_cmd_remove": Option([], type=valid_options),
"pre_stop_cmd_append": Option([], type=valid_options),
"post_stop_cmd": Option(["/bin/true", "post-stop"], type=valid_command),
"post_stop_cmd_remove": Option([], type=valid_options),
"post_stop_cmd_append": Option([], type=valid_options),
},
},
"ipmi": {
"server": {
"host": Option("::", type=valid_ip_or_host),
"port": Option(623, type=valid_port),
"timeout": Option(10.0, type=valid_float_f01),
},
"kvmd": {
"unix": Option("/run/kvmd/kvmd.sock", type=valid_abs_path, unpack_as="unix_path"),
"timeout": Option(5.0, type=valid_float_f01),
},
"auth": {
"file": Option("/etc/kvmd/ipmipasswd", type=valid_abs_file, unpack_as="path"),
},
"sol": {
"device": Option("", type=valid_abs_path, if_empty="", unpack_as="sol_device_path"),
"speed": Option(115200, type=valid_tty_speed, unpack_as="sol_speed"),
"select_timeout": Option(0.1, type=valid_float_f01, unpack_as="sol_select_timeout"),
"proxy_port": Option(0, type=valid_port, unpack_as="sol_proxy_port"),
},
},
"vnc": {
"desired_fps": Option(30, type=valid_stream_fps),
"mouse_output": Option("usb", type=valid_hid_mouse_output),
"keymap": Option("/usr/share/kvmd/keymaps/en-us", type=valid_abs_file),
"server": {
"host": Option("::", type=valid_ip_or_host),
"port": Option(5900, type=valid_port),
"max_clients": Option(10, type=valid_int_f1),
"no_delay": Option(True, type=valid_bool),
"keepalive": {
"enabled": Option(True, type=valid_bool, unpack_as="keepalive_enabled"),
"idle": Option(10, type=functools.partial(valid_number, min=1, max=3600), unpack_as="keepalive_idle"),
"interval": Option(3, type=functools.partial(valid_number, min=1, max=60), unpack_as="keepalive_interval"),
"count": Option(3, type=functools.partial(valid_number, min=1, max=10), unpack_as="keepalive_count"),
},
"tls": {
"ciphers": Option("ALL:@SECLEVEL=0", type=valid_ssl_ciphers, if_empty=""),
"timeout": Option(30.0, type=valid_float_f01),
"x509": {
"cert": Option("/etc/kvmd/vnc/ssl/server.crt", type=valid_abs_file, if_empty=""),
"key": Option("/etc/kvmd/vnc/ssl/server.key", type=valid_abs_file, if_empty=""),
},
},
},
"kvmd": {
"unix": Option("/run/kvmd/kvmd.sock", type=valid_abs_path, unpack_as="unix_path"),
"timeout": Option(5.0, type=valid_float_f01),
},
"streamer": {
"unix": Option("/run/kvmd/ustreamer.sock", type=valid_abs_path, unpack_as="unix_path"),
"timeout": Option(5.0, type=valid_float_f01),
},
"memsink": {
"jpeg": {
"sink": Option("", unpack_as="obj"),
"lock_timeout": Option(1.0, type=valid_float_f01),
"wait_timeout": Option(1.0, type=valid_float_f01),
"drop_same_frames": Option(1.0, type=valid_float_f0),
},
"h264": {
"sink": Option("", unpack_as="obj"),
"lock_timeout": Option(1.0, type=valid_float_f01),
"wait_timeout": Option(1.0, type=valid_float_f01),
"drop_same_frames": Option(0.0, type=valid_float_f0),
},
},
"auth": {
"vncauth": {
"enabled": Option(False, type=valid_bool),
"file": Option("/etc/kvmd/vncpasswd", type=valid_abs_file, unpack_as="path"),
},
"vencrypt": {
"enabled": Option(True, type=valid_bool, unpack_as="vencrypt_enabled"),
},
},
},
"janus": {
"stun": {
"host": Option("stun.l.google.com", type=valid_ip_or_host, unpack_as="stun_host"),
"port": Option(19302, type=valid_port, unpack_as="stun_port"),
"timeout": Option(5.0, type=valid_float_f01, unpack_as="stun_timeout"),
"retries": Option(5, type=valid_int_f1, unpack_as="stun_retries"),
"retries_delay": Option(5.0, type=valid_float_f01, unpack_as="stun_retries_delay"),
},
"check": {
"interval": Option(10.0, type=valid_float_f01, unpack_as="check_interval"),
"retries": Option(5, type=valid_int_f1, unpack_as="check_retries"),
"retries_delay": Option(5.0, type=valid_float_f01, unpack_as="check_retries_delay"),
},
"cmd": Option([
"/usr/bin/janus",
"--disable-colors",
"--plugins-folder=/usr/lib/ustreamer/janus",
"--configs-folder=/etc/kvmd/janus",
"--interface={src_ip}",
"{o_stun_server}",
], type=valid_command),
"cmd_remove": Option([], type=valid_options),
"cmd_append": Option([], type=valid_options),
},
"watchdog": {
"rtc": Option(0, type=valid_int_f0),
"timeout": Option(300, type=valid_int_f1),
"interval": Option(30, type=valid_int_f1),
},
}

777
patches/__init__.py.2 Normal file
View File

@ -0,0 +1,777 @@
# ========================================================================== #
# #
# KVMD - The main PiKVM daemon. #
# #
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
# #
# ========================================================================== #
import sys
import os
import functools
import argparse
import logging
import logging.config
import pygments
import pygments.lexers.data
import pygments.formatters
from .. import tools
from ..mouse import MouseRange
from ..plugins import UnknownPluginError
from ..plugins.auth import get_auth_service_class
from ..plugins.hid import get_hid_class
from ..plugins.atx import get_atx_class
from ..plugins.msd import get_msd_class
from ..plugins.ugpio import UserGpioModes
from ..plugins.ugpio import BaseUserGpioDriver
from ..plugins.ugpio import get_ugpio_driver_class
from ..yamlconf import ConfigError
from ..yamlconf import manual_validated
from ..yamlconf import make_config
from ..yamlconf import Section
from ..yamlconf import Option
from ..yamlconf import build_raw_from_options
from ..yamlconf.dumper import make_config_dump
from ..yamlconf.loader import load_yaml_file
from ..yamlconf.merger import yaml_merge
from ..validators.basic import valid_stripped_string
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_int_f0
from ..validators.basic import valid_int_f1
from ..validators.basic import valid_float_f0
from ..validators.basic import valid_float_f01
from ..validators.basic import valid_string_list
from ..validators.auth import valid_user
from ..validators.auth import valid_users_list
from ..validators.os import valid_abs_path
from ..validators.os import valid_abs_file
from ..validators.os import valid_abs_dir
from ..validators.os import valid_unix_mode
from ..validators.os import valid_options
from ..validators.os import valid_command
from ..validators.net import valid_ip_or_host
from ..validators.net import valid_net
from ..validators.net import valid_port
from ..validators.net import valid_ports_list
from ..validators.net import valid_mac
from ..validators.net import valid_ssl_ciphers
from ..validators.hid import valid_hid_key
from ..validators.hid import valid_hid_mouse_output
from ..validators.hid import valid_hid_mouse_move
from ..validators.kvm import valid_stream_quality
from ..validators.kvm import valid_stream_fps
from ..validators.kvm import valid_stream_resolution
from ..validators.kvm import valid_stream_h264_bitrate
from ..validators.kvm import valid_stream_h264_gop
from ..validators.ugpio import valid_ugpio_driver
from ..validators.ugpio import valid_ugpio_channel
from ..validators.ugpio import valid_ugpio_mode
from ..validators.ugpio import valid_ugpio_view_title
from ..validators.ugpio import valid_ugpio_view_table
from ..validators.hw import valid_tty_speed
from ..validators.hw import valid_otg_gadget
from ..validators.hw import valid_otg_id
from ..validators.hw import valid_otg_ethernet
# =====
def init(
prog: (str | None)=None,
description: (str | None)=None,
add_help: bool=True,
check_run: bool=False,
cli_logging: bool=False,
argv: (list[str] | None)=None,
**load: bool,
) -> tuple[argparse.ArgumentParser, list[str], Section]:
argv = (argv or sys.argv)
assert len(argv) > 0
parser = argparse.ArgumentParser(
prog=(prog or argv[0]),
description=description,
add_help=add_help,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("-c", "--config", default="/etc/kvmd/main.yaml", type=valid_abs_file,
help="Set config file path", metavar="<file>")
parser.add_argument("-o", "--set-options", default=[], nargs="+",
help="Override config options list (like sec/sub/opt=value)", metavar="<k=v>",)
parser.add_argument("-m", "--dump-config", action="store_true",
help="View current configuration (include all overrides)")
if check_run:
parser.add_argument("--run", dest="run", action="store_true",
help="Run the service")
(options, remaining) = parser.parse_known_args(argv)
if options.dump_config:
_dump_config(_init_config(
config_path=options.config,
override_options=options.set_options,
load_auth=True,
load_hid=True,
load_atx=True,
load_msd=True,
load_gpio=True,
))
raise SystemExit()
config = _init_config(options.config, options.set_options, **load)
logging.captureWarnings(True)
logging.config.dictConfig(config.logging)
if cli_logging:
logging.getLogger().handlers[0].setFormatter(logging.Formatter(
"-- {levelname:>7} -- {message}",
style="{",
))
if check_run and not options.run:
raise SystemExit(
"To prevent accidental startup, you must specify the --run option to start.\n"
"Try the --help option to find out what this service does.\n"
"Make sure you understand exactly what you are doing!"
)
return (parser, remaining, config)
# =====
def _init_config(config_path: str, override_options: list[str], **load_flags: bool) -> Section:
config_path = os.path.expanduser(config_path)
try:
raw_config: dict = load_yaml_file(config_path)
except Exception as err:
raise SystemExit(f"ConfigError: Can't read config file {config_path!r}:\n{tools.efmt(err)}")
if not isinstance(raw_config, dict):
raise SystemExit(f"ConfigError: Top-level of the file {config_path!r} must be a dictionary")
scheme = _get_config_scheme()
try:
yaml_merge(raw_config, (raw_config.pop("override", {}) or {}))
yaml_merge(raw_config, build_raw_from_options(override_options), "raw CLI options")
_patch_raw(raw_config)
config = make_config(raw_config, scheme)
if _patch_dynamic(raw_config, config, scheme, **load_flags):
config = make_config(raw_config, scheme)
return config
except (ConfigError, UnknownPluginError) as err:
raise SystemExit(f"ConfigError: {err}")
def _patch_raw(raw_config: dict) -> None: # pylint: disable=too-many-branches
if isinstance(raw_config.get("otg"), dict):
for (old, new) in [
("msd", "msd"),
("acm", "serial"),
("drives", "drives"),
]:
if old in raw_config["otg"]:
if not isinstance(raw_config["otg"].get("devices"), dict):
raw_config["otg"]["devices"] = {}
raw_config["otg"]["devices"][new] = raw_config["otg"].pop(old)
if isinstance(raw_config.get("kvmd"), dict) and isinstance(raw_config["kvmd"].get("wol"), dict):
if not isinstance(raw_config["kvmd"].get("gpio"), dict):
raw_config["kvmd"]["gpio"] = {}
for section in ["drivers", "scheme"]:
if not isinstance(raw_config["kvmd"]["gpio"].get(section), dict):
raw_config["kvmd"]["gpio"][section] = {}
raw_config["kvmd"]["gpio"]["drivers"]["__wol__"] = {
"type": "wol",
**raw_config["kvmd"].pop("wol"),
}
raw_config["kvmd"]["gpio"]["scheme"]["__wol__"] = {
"driver": "__wol__",
"pin": 0,
"mode": "output",
"switch": False,
}
if isinstance(raw_config.get("kvmd"), dict) and isinstance(raw_config["kvmd"].get("streamer"), dict):
streamer_config = raw_config["kvmd"]["streamer"]
desired_fps = streamer_config.get("desired_fps")
if desired_fps is not None and not isinstance(desired_fps, dict):
streamer_config["desired_fps"] = {"default": desired_fps}
max_fps = streamer_config.get("max_fps")
if max_fps is not None:
if not isinstance(streamer_config.get("desired_fps"), dict):
streamer_config["desired_fps"] = {}
streamer_config["desired_fps"]["max"] = max_fps
del streamer_config["max_fps"]
resolution = streamer_config.get("resolution")
if resolution is not None and not isinstance(resolution, dict):
streamer_config["resolution"] = {"default": resolution}
available_resolutions = streamer_config.get("available_resolutions")
if available_resolutions is not None:
if not isinstance(streamer_config.get("resolution"), dict):
streamer_config["resolution"] = {}
streamer_config["resolution"]["available"] = available_resolutions
del streamer_config["available_resolutions"]
def _patch_dynamic( # pylint: disable=too-many-locals
raw_config: dict,
config: Section,
scheme: dict,
load_auth: bool=False,
load_hid: bool=False,
load_atx: bool=False,
load_msd: bool=False,
load_gpio: bool=False,
) -> bool:
rebuild = False
if load_auth:
scheme["kvmd"]["auth"]["internal"].update(get_auth_service_class(config.kvmd.auth.internal.type).get_plugin_options())
if config.kvmd.auth.external.type:
scheme["kvmd"]["auth"]["external"].update(get_auth_service_class(config.kvmd.auth.external.type).get_plugin_options())
rebuild = True
for (load, section, get_class) in [
(load_hid, "hid", get_hid_class),
(load_atx, "atx", get_atx_class),
(load_msd, "msd", get_msd_class),
]:
if load:
scheme["kvmd"][section].update(get_class(getattr(config.kvmd, section).type).get_plugin_options())
rebuild = True
if load_gpio:
driver: str
drivers: dict[str, type[BaseUserGpioDriver]] = {} # Name to drivers
for (driver, params) in { # type: ignore
"__gpio__": {},
**tools.rget(raw_config, "kvmd", "gpio", "drivers"),
}.items():
with manual_validated(driver, "kvmd", "gpio", "drivers", "<key>"):
driver = valid_ugpio_driver(driver)
driver_type = valid_stripped_string_not_empty(params.get("type", "gpio"))
driver_class = get_ugpio_driver_class(driver_type)
drivers[driver] = driver_class
scheme["kvmd"]["gpio"]["drivers"][driver] = {
"type": Option(driver_type, type=valid_stripped_string_not_empty),
**driver_class.get_plugin_options()
}
path = ("kvmd", "gpio", "scheme")
for (channel, params) in tools.rget(raw_config, *path).items():
with manual_validated(channel, *path, "<key>"):
channel = valid_ugpio_channel(channel)
driver = params.get("driver", "__gpio__")
with manual_validated(driver, *path, channel, "driver"):
driver = valid_ugpio_driver(driver, set(drivers))
mode: str = params.get("mode", "")
with manual_validated(mode, *path, channel, "mode"):
mode = valid_ugpio_mode(mode, drivers[driver].get_modes())
if params.get("pulse") == False: # noqa: E712 # pylint: disable=singleton-comparison
params["pulse"] = {"delay": 0}
scheme["kvmd"]["gpio"]["scheme"][channel] = {
"driver": Option("__gpio__", type=functools.partial(valid_ugpio_driver, variants=set(drivers))),
"pin": Option(None, type=drivers[driver].get_pin_validator()),
"mode": Option("", type=functools.partial(valid_ugpio_mode, variants=drivers[driver].get_modes())),
"inverted": Option(False, type=valid_bool),
**({
"busy_delay": Option(0.2, type=valid_float_f01),
"initial": Option(False, type=(lambda arg: (valid_bool(arg) if arg is not None else None))),
"switch": Option(True, type=valid_bool),
"pulse": { # type: ignore
"delay": Option(0.1, type=valid_float_f0),
"min_delay": Option(0.1, type=valid_float_f01),
"max_delay": Option(0.1, type=valid_float_f01),
},
} if mode == UserGpioModes.OUTPUT else { # input
"debounce": Option(0.1, type=valid_float_f0),
})
}
rebuild = True
return rebuild
def _dump_config(config: Section) -> None:
dump = make_config_dump(config)
if sys.stdout.isatty():
dump = pygments.highlight(
dump,
pygments.lexers.data.YamlLexer(),
pygments.formatters.TerminalFormatter(bg="dark"), # pylint: disable=no-member
)
print(dump)
def _get_config_scheme() -> dict:
return {
"logging": Option({}),
"kvmd": {
"server": {
"unix": Option("/run/kvmd/kvmd.sock", type=valid_abs_path, unpack_as="unix_path"),
"unix_rm": Option(True, type=valid_bool),
"unix_mode": Option(0o660, type=valid_unix_mode),
"heartbeat": Option(15.0, type=valid_float_f01),
"access_log_format": Option("[%P / %{X-Real-IP}i] '%r' => %s; size=%b ---"
" referer='%{Referer}i'; user_agent='%{User-Agent}i'"),
},
"auth": {
"enabled": Option(True, type=valid_bool),
"internal": {
"type": Option("htpasswd"),
"force_users": Option([], type=valid_users_list),
# Dynamic content
},
"external": {
"type": Option("", type=valid_stripped_string),
# Dynamic content
},
"totp": {
"secret": {
"file": Option("/etc/kvmd/totp.secret", type=valid_abs_path, if_empty=""),
},
},
},
"info": { # Accessed via global config, see kvmd/info for details
"meta": Option("/etc/kvmd/meta.yaml", type=valid_abs_file),
"extras": Option("/usr/share/kvmd/extras", type=valid_abs_dir),
"hw": {
"vcgencmd_cmd": Option(["/usr/bin/vcgencmd"], type=valid_command),
"ignore_past": Option(False, type=valid_bool),
"state_poll": Option(10.0, type=valid_float_f01),
},
"fan": {
"daemon": Option("kvmd-fan", type=valid_stripped_string),
"unix": Option("", type=valid_abs_path, if_empty="", unpack_as="unix_path"),
"timeout": Option(5.0, type=valid_float_f01),
"state_poll": Option(5.0, type=valid_float_f01),
},
},
"log_reader": {
"enabled": Option(True, type=valid_bool),
},
"prometheus": {
"auth": {
"enabled": Option(True, type=valid_bool),
},
},
"hid": {
"type": Option("", type=valid_stripped_string_not_empty),
"keymap": Option("/usr/share/kvmd/keymaps/en-us", type=valid_abs_file),
"ignore_keys": Option([], type=functools.partial(valid_string_list, subval=valid_hid_key)),
"mouse_x_range": {
"min": Option(MouseRange.MIN, type=valid_hid_mouse_move),
"max": Option(MouseRange.MAX, type=valid_hid_mouse_move),
},
"mouse_y_range": {
"min": Option(MouseRange.MIN, type=valid_hid_mouse_move),
"max": Option(MouseRange.MAX, type=valid_hid_mouse_move),
},
# Dynamic content
},
"atx": {
"type": Option("", type=valid_stripped_string_not_empty),
# Dynamic content
},
"msd": {
"type": Option("", type=valid_stripped_string_not_empty),
# Dynamic content
},
"streamer": {
"forever": Option(False, type=valid_bool),
"reset_delay": Option(1.0, type=valid_float_f0),
"shutdown_delay": Option(10.0, type=valid_float_f01),
"state_poll": Option(1.0, type=valid_float_f01),
"quality": Option(80, type=valid_stream_quality, if_empty=0),
"resolution": {
"default": Option("", type=valid_stream_resolution, if_empty="", unpack_as="resolution"),
"available": Option(
[],
type=functools.partial(valid_string_list, subval=valid_stream_resolution),
unpack_as="available_resolutions",
),
},
"desired_fps": {
"default": Option(40, type=valid_stream_fps, unpack_as="desired_fps"),
"min": Option(0, type=valid_stream_fps, unpack_as="desired_fps_min"),
"max": Option(70, type=valid_stream_fps, unpack_as="desired_fps_max"),
},
"h264_bitrate": {
"default": Option(0, type=valid_stream_h264_bitrate, if_empty=0, unpack_as="h264_bitrate"),
"min": Option(25, type=valid_stream_h264_bitrate, unpack_as="h264_bitrate_min"),
"max": Option(20000, type=valid_stream_h264_bitrate, unpack_as="h264_bitrate_max"),
},
"h264_gop": {
"default": Option(30, type=valid_stream_h264_gop, unpack_as="h264_gop"),
"min": Option(0, type=valid_stream_h264_gop, unpack_as="h264_gop_min"),
"max": Option(60, type=valid_stream_h264_gop, unpack_as="h264_gop_max"),
},
"unix": Option("/run/kvmd/ustreamer.sock", type=valid_abs_path, unpack_as="unix_path"),
"timeout": Option(2.0, type=valid_float_f01),
"process_name_prefix": Option("kvmd/streamer"),
"pre_start_cmd": Option(["/bin/true", "pre-start"], type=valid_command),
"pre_start_cmd_remove": Option([], type=valid_options),
"pre_start_cmd_append": Option([], type=valid_options),
"cmd": Option(["/bin/true"], type=valid_command),
"cmd_remove": Option([], type=valid_options),
"cmd_append": Option([], type=valid_options),
"post_stop_cmd": Option(["/bin/true", "post-stop"], type=valid_command),
"post_stop_cmd_remove": Option([], type=valid_options),
"post_stop_cmd_append": Option([], type=valid_options),
},
"ocr": {
"langs": Option(["eng"], type=valid_string_list, unpack_as="default_langs"),
"tessdata": Option("/usr/share/tessdata", type=valid_stripped_string_not_empty, unpack_as="data_dir_path")
},
"snapshot": {
"idle_interval": Option(0.0, type=valid_float_f0),
"live_interval": Option(0.0, type=valid_float_f0),
"wakeup_key": Option("", type=valid_hid_key, if_empty=""),
"wakeup_move": Option(0, type=valid_hid_mouse_move),
"online_delay": Option(5.0, type=valid_float_f0),
"retries": Option(10, type=valid_int_f1),
"retries_delay": Option(3.0, type=valid_float_f01),
},
"gpio": {
"state_poll": Option(0.1, type=valid_float_f01),
"drivers": {}, # Dynamic content
"scheme": {}, # Dymanic content
"view": {
"header": {
"title": Option("GPIO", type=valid_ugpio_view_title),
},
"table": Option([], type=valid_ugpio_view_table),
},
},
},
"pst": {
"server": {
"unix": Option("/run/kvmd/pst.sock", type=valid_abs_path, unpack_as="unix_path"),
"unix_rm": Option(True, type=valid_bool),
"unix_mode": Option(0o660, type=valid_unix_mode),
"heartbeat": Option(15.0, type=valid_float_f01),
"access_log_format": Option("[%P / %{X-Real-IP}i] '%r' => %s; size=%b ---"
" referer='%{Referer}i'; user_agent='%{User-Agent}i'"),
},
"ro_retries_delay": Option(10.0, type=valid_float_f01),
"ro_cleanup_delay": Option(3.0, type=valid_float_f01),
"remount_cmd": Option([
"/usr/bin/sudo", "--non-interactive",
"/usr/bin/kvmd-helper-pst-remount", "{mode}",
], type=valid_command),
},
"otg": {
"vendor_id": Option(0x1D6B, type=valid_otg_id), # Linux Foundation
"product_id": Option(0x0104, type=valid_otg_id), # Multifunction Composite Gadget
"manufacturer": Option("PiKVM", type=valid_stripped_string),
"product": Option("Composite KVM Device", type=valid_stripped_string),
"serial": Option("CAFEBABE", type=valid_stripped_string, if_none=None),
"device_version": Option(-1, type=functools.partial(valid_number, min=-1, max=0xFFFF)),
"usb_version": Option(0x0200, type=valid_otg_id),
"max_power": Option(250, type=functools.partial(valid_number, min=50, max=500)),
"remote_wakeup": Option(False, type=valid_bool),
"gadget": Option("kvmd", type=valid_otg_gadget),
"config": Option("PiKVM device", type=valid_stripped_string_not_empty),
"udc": Option("", type=valid_stripped_string),
"init_delay": Option(3.0, type=valid_float_f01),
"user": Option("kvmd", type=valid_user),
"meta": Option("/run/kvmd/otg", type=valid_abs_path),
"devices": {
"hid": {
"keyboard": {
"start": Option(True, type=valid_bool),
},
"mouse": {
"start": Option(True, type=valid_bool),
},
},
"msd": {
"start": Option(True, type=valid_bool),
"default": {
"stall": Option(False, type=valid_bool),
"cdrom": Option(True, type=valid_bool),
"rw": Option(False, type=valid_bool),
"removable": Option(True, type=valid_bool),
"fua": Option(True, type=valid_bool),
},
},
"serial": {
"enabled": Option(False, type=valid_bool),
"start": Option(True, type=valid_bool),
},
"ethernet": {
"enabled": Option(False, type=valid_bool),
"start": Option(True, type=valid_bool),
"driver": Option("ecm", type=valid_otg_ethernet),
"host_mac": Option("", type=valid_mac, if_empty=""),
"kvm_mac": Option("", type=valid_mac, if_empty=""),
},
"drives": {
"enabled": Option(False, type=valid_bool),
"start": Option(True, type=valid_bool),
"count": Option(1, type=valid_int_f1),
"default": {
"stall": Option(False, type=valid_bool),
"cdrom": Option(False, type=valid_bool),
"rw": Option(True, type=valid_bool),
"removable": Option(True, type=valid_bool),
"fua": Option(True, type=valid_bool),
},
},
},
},
"otgnet": {
"iface": {
"net": Option("172.30.30.0/24", type=functools.partial(valid_net, v6=False)),
"ip_cmd": Option(["/usr/bin/ip"], type=valid_command),
},
"firewall": {
"allow_icmp": Option(True, type=valid_bool),
"allow_tcp": Option([], type=valid_ports_list),
"allow_udp": Option([67], type=valid_ports_list),
"forward_iface": Option("", type=valid_stripped_string),
"iptables_cmd": Option(["/usr/sbin/iptables", "--wait=5"], type=valid_command),
},
"commands": {
"pre_start_cmd": Option(["/bin/true", "pre-start"], type=valid_command),
"pre_start_cmd_remove": Option([], type=valid_options),
"pre_start_cmd_append": Option([], type=valid_options),
"post_start_cmd": Option([
"/usr/bin/systemd-run",
"--unit=kvmd-otgnet-dnsmasq",
"/usr/sbin/dnsmasq",
"--conf-file=/dev/null",
"--pid-file",
"--user=dnsmasq",
"--interface={iface}",
"--port=0",
"--dhcp-range={dhcp_ip_begin},{dhcp_ip_end},24h",
"--dhcp-leasefile=/run/kvmd/dnsmasq.lease",
"--dhcp-option={dhcp_option_3}",
"--dhcp-option=6",
"--keep-in-foreground",
], type=valid_command),
"post_start_cmd_remove": Option([], type=valid_options),
"post_start_cmd_append": Option([], type=valid_options),
"pre_stop_cmd": Option([
"/usr/bin/systemctl",
"stop",
"kvmd-otgnet-dnsmasq",
], type=valid_command),
"pre_stop_cmd_remove": Option([], type=valid_options),
"pre_stop_cmd_append": Option([], type=valid_options),
"post_stop_cmd": Option(["/bin/true", "post-stop"], type=valid_command),
"post_stop_cmd_remove": Option([], type=valid_options),
"post_stop_cmd_append": Option([], type=valid_options),
},
},
"ipmi": {
"server": {
"host": Option("::", type=valid_ip_or_host),
"port": Option(623, type=valid_port),
"timeout": Option(10.0, type=valid_float_f01),
},
"kvmd": {
"unix": Option("/run/kvmd/kvmd.sock", type=valid_abs_path, unpack_as="unix_path"),
"timeout": Option(5.0, type=valid_float_f01),
},
"auth": {
"file": Option("/etc/kvmd/ipmipasswd", type=valid_abs_file, unpack_as="path"),
},
"sol": {
"device": Option("", type=valid_abs_path, if_empty="", unpack_as="sol_device_path"),
"speed": Option(115200, type=valid_tty_speed, unpack_as="sol_speed"),
"select_timeout": Option(0.1, type=valid_float_f01, unpack_as="sol_select_timeout"),
"proxy_port": Option(0, type=valid_port, unpack_as="sol_proxy_port"),
},
},
"vnc": {
"desired_fps": Option(30, type=valid_stream_fps),
"mouse_output": Option("usb", type=valid_hid_mouse_output),
"keymap": Option("/usr/share/kvmd/keymaps/en-us", type=valid_abs_file),
"server": {
"host": Option("::", type=valid_ip_or_host),
"port": Option(5900, type=valid_port),
"max_clients": Option(10, type=valid_int_f1),
"no_delay": Option(True, type=valid_bool),
"keepalive": {
"enabled": Option(True, type=valid_bool, unpack_as="keepalive_enabled"),
"idle": Option(10, type=functools.partial(valid_number, min=1, max=3600), unpack_as="keepalive_idle"),
"interval": Option(3, type=functools.partial(valid_number, min=1, max=60), unpack_as="keepalive_interval"),
"count": Option(3, type=functools.partial(valid_number, min=1, max=10), unpack_as="keepalive_count"),
},
"tls": {
"ciphers": Option("ALL:@SECLEVEL=0", type=valid_ssl_ciphers, if_empty=""),
"timeout": Option(30.0, type=valid_float_f01),
"x509": {
"cert": Option("/etc/kvmd/vnc/ssl/server.crt", type=valid_abs_file, if_empty=""),
"key": Option("/etc/kvmd/vnc/ssl/server.key", type=valid_abs_file, if_empty=""),
},
},
},
"kvmd": {
"unix": Option("/run/kvmd/kvmd.sock", type=valid_abs_path, unpack_as="unix_path"),
"timeout": Option(5.0, type=valid_float_f01),
},
"streamer": {
"unix": Option("/run/kvmd/ustreamer.sock", type=valid_abs_path, unpack_as="unix_path"),
"timeout": Option(5.0, type=valid_float_f01),
},
"memsink": {
"jpeg": {
"sink": Option("", unpack_as="obj"),
"lock_timeout": Option(1.0, type=valid_float_f01),
"wait_timeout": Option(1.0, type=valid_float_f01),
"drop_same_frames": Option(1.0, type=valid_float_f0),
},
"h264": {
"sink": Option("", unpack_as="obj"),
"lock_timeout": Option(1.0, type=valid_float_f01),
"wait_timeout": Option(1.0, type=valid_float_f01),
"drop_same_frames": Option(0.0, type=valid_float_f0),
},
},
"auth": {
"vncauth": {
"enabled": Option(False, type=valid_bool),
"file": Option("/etc/kvmd/vncpasswd", type=valid_abs_file, unpack_as="path"),
},
"vencrypt": {
"enabled": Option(True, type=valid_bool, unpack_as="vencrypt_enabled"),
},
},
},
"janus": {
"stun": {
"host": Option("stun.l.google.com", type=valid_ip_or_host, unpack_as="stun_host"),
"port": Option(19302, type=valid_port, unpack_as="stun_port"),
"timeout": Option(5.0, type=valid_float_f01, unpack_as="stun_timeout"),
"retries": Option(5, type=valid_int_f1, unpack_as="stun_retries"),
"retries_delay": Option(5.0, type=valid_float_f01, unpack_as="stun_retries_delay"),
},
"check": {
"interval": Option(10.0, type=valid_float_f01, unpack_as="check_interval"),
"retries": Option(5, type=valid_int_f1, unpack_as="check_retries"),
"retries_delay": Option(5.0, type=valid_float_f01, unpack_as="check_retries_delay"),
},
"cmd": Option([
"/usr/bin/janus",
"--disable-colors",
"--plugins-folder=/usr/lib/ustreamer/janus",
"--configs-folder=/etc/kvmd/janus",
"--interface={src_ip}",
"{o_stun_server}",
], type=valid_command),
"cmd_remove": Option([], type=valid_options),
"cmd_append": Option([], type=valid_options),
},
"watchdog": {
"rtc": Option(0, type=valid_int_f0),
"timeout": Option(300, type=valid_int_f1),
"interval": Option(30, type=valid_int_f1),
},
}

View File

@ -1,9 +1,27 @@
diff -ruN kvmd/aiohelpers.py kvmd/aiohelpers.py
--- kvmd/aiohelpers.py 2023-01-30 03:25:23.556804000 +0700
+++ kvmd/aiohelpers.py 2023-01-30 08:12:21.773899000 +0700
@@ -38,11 +38,26 @@
From 3d137882ac38ac046b7d09cada1883b304b04319 Mon Sep 17 00:00:00 2001
From: xe5700 <9338143+xe5700@users.noreply.github.com>
Date: Fri, 20 May 2022 18:34:21 +0800
Subject: [PATCH] Revert force eject feature to unlock helper
---
kvmd/aiohelpers.py | 31 ++++++++++++-----
kvmd/apps/otg/__init__.py | 3 +-
kvmd/apps/otgmsd/__init__.py | 25 +++++++++++++-
kvmd/helpers/unlock/__init__.py | 58 ++++++++++++++++++++++++++++++++
kvmd/helpers/unlock/__main__.py | 24 +++++++++++++
kvmd/plugins/msd/otg/__init__.py | 19 ++++++++---
kvmd/plugins/msd/otg/drive.py | 5 +--
7 files changed, 145 insertions(+), 20 deletions(-)
create mode 100644 kvmd/helpers/unlock/__init__.py
create mode 100644 kvmd/helpers/unlock/__main__.py
diff --git a/kvmd/aiohelpers.py b/kvmd/aiohelpers.py
index 6357764c..37a5d4b9 100644
--- a/kvmd/aiohelpers.py
+++ b/kvmd/aiohelpers.py
@@ -40,11 +40,26 @@ async def remount(name: str, base_cmd: List[str], rw: bool) -> bool:
]
logger.info("Remounting %s storage to %s: %s ...", name, mode.upper(), tools.cmdfmt(cmd))
logger.info("Remounting %s storage to %s: %s ...", name, mode.upper(), cmd)
try:
- proc = await aioproc.log_process(cmd, logger)
- if proc.returncode != 0:
@ -19,7 +37,7 @@ diff -ruN kvmd/aiohelpers.py kvmd/aiohelpers.py
+ raise
+
+
+async def unlock_drive(base_cmd: list[str]) -> None:
+async def unlock_drive(base_cmd: List[str]) -> None:
+ logger = get_logger(0)
+ logger.info("Unlocking the drive ...")
+ try:
@ -30,16 +48,17 @@ diff -ruN kvmd/aiohelpers.py kvmd/aiohelpers.py
+
+
+# =====
+async def _run_helper(cmd: list[str]) -> None:
+async def _run_helper(cmd: List[str]) -> None:
+ logger = get_logger(0)
+ logger.info("Executing helper %s ...", cmd)
+ proc = await aioproc.log_process(cmd, logger)
+ if proc.returncode != 0:
+ logger.error(f"Error while helper execution: pid={proc.pid}; retcode={proc.returncode}")
diff -ruN kvmd/apps/otg/__init__.py kvmd/apps/otg/__init__.py
--- kvmd/apps/otg/__init__.py 2022-12-22 10:01:48.000000000 +0700
+++ kvmd/apps/otg/__init__.py 2023-01-30 03:51:51.331539000 +0700
@@ -182,7 +182,6 @@
+ raise MsdError(f"Error while helper execution: pid={proc.pid}; retcode={proc.returncode}")
diff --git a/kvmd/apps/otg/__init__.py b/kvmd/apps/otg/__init__.py
index cbf7a197..d0ed0554 100644
--- a/kvmd/apps/otg/__init__.py
+++ b/kvmd/apps/otg/__init__.py
@@ -182,7 +182,6 @@ class _GadgetConfig:
_chown(join(func_path, "lun.0/cdrom"), user)
_chown(join(func_path, "lun.0/ro"), user)
_chown(join(func_path, "lun.0/file"), user)
@ -47,7 +66,7 @@ diff -ruN kvmd/apps/otg/__init__.py kvmd/apps/otg/__init__.py
_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)
@@ -291,7 +290,7 @@
@@ -291,7 +290,7 @@ def _cmd_stop(config: Section) -> None:
logger.info("Disabling gadget %r ...", config.otg.gadget)
_write(join(gadget_path, "UDC"), "\n")
@ -56,21 +75,27 @@ diff -ruN kvmd/apps/otg/__init__.py kvmd/apps/otg/__init__.py
profile_path = join(gadget_path, usb.G_PROFILE)
for func in os.listdir(profile_path):
diff -ruN kvmd/apps/otgmsd/__init__.py kvmd/apps/otgmsd/__init__.py
--- kvmd/apps/otgmsd/__init__.py 2022-12-22 10:01:48.000000000 +0700
+++ kvmd/apps/otgmsd/__init__.py 2023-01-30 04:35:09.702576000 +0700
@@ -21,8 +21,10 @@
diff --git a/kvmd/apps/otgmsd/__init__.py b/kvmd/apps/otgmsd/__init__.py
index f57b3107..78f8e3c7 100644
--- a/kvmd/apps/otgmsd/__init__.py
+++ b/kvmd/apps/otgmsd/__init__.py
@@ -21,12 +21,15 @@
import os
+import signal
import errno
import argparse
+import psutil
from typing import List
from typing import Optional
+import psutil
+
from ...validators.basic import valid_bool
from ...validators.basic import valid_int_f0
@@ -53,6 +55,21 @@
from ...validators.os import valid_abs_file
@@ -56,6 +59,21 @@ def _set_param(gadget: str, instance: int, param: str, value: str) -> None:
raise
@ -90,18 +115,18 @@ diff -ruN kvmd/apps/otgmsd/__init__.py kvmd/apps/otgmsd/__init__.py
+
+
# =====
def main(argv: (list[str] | None)=None) -> None:
def main(argv: Optional[List[str]]=None) -> None:
(parent_parser, argv, config) = init(
@@ -77,7 +94,7 @@
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")
@@ -70,6 +88,8 @@ def main(argv: Optional[List[str]]=None) -> None:
)
parser.add_argument("-i", "--instance", default=0, type=valid_int_f0,
metavar="<N>", help="Drive instance (0 for KVMD drive)")
+ parser.add_argument("--unlock", action="store_true",
+ help="Send SIGUSR1 to MSD kernel thread")
options = parser.parse_args(argv[1:])
if config.kvmd.msd.type != "otg":
@@ -87,8 +104,11 @@
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,
@@ -89,8 +109,11 @@ def main(argv: Optional[List[str]]=None) -> None:
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))
@ -114,9 +139,11 @@ diff -ruN kvmd/apps/otgmsd/__init__.py kvmd/apps/otgmsd/__init__.py
if options.set_cdrom is not None:
set_param("cdrom", str(int(options.set_cdrom)))
diff -ruN kvmd/helpers/unlock/__init__.py kvmd/helpers/unlock/__init__.py
--- kvmd/helpers/unlock/__init__.py 1970-01-01 07:00:00.000000000 +0700
+++ kvmd/helpers/unlock/__init__.py 2023-01-30 04:04:07.000000000 +0700
diff --git a/kvmd/helpers/unlock/__init__.py b/kvmd/helpers/unlock/__init__.py
new file mode 100644
index 00000000..140e0e7c
--- /dev/null
+++ b/kvmd/helpers/unlock/__init__.py
@@ -0,0 +1,58 @@
+# ========================================================================== #
+# #
@ -176,9 +203,11 @@ diff -ruN kvmd/helpers/unlock/__init__.py kvmd/helpers/unlock/__init__.py
+ if len(sys.argv) != 2 or sys.argv[1] != "unlock":
+ raise SystemExit(f"Usage: {sys.argv[0]} [unlock]")
+ _unlock()
diff -ruN kvmd/helpers/unlock/__main__.py kvmd/helpers/unlock/__main__.py
--- kvmd/helpers/unlock/__main__.py 1970-01-01 07:00:00.000000000 +0700
+++ kvmd/helpers/unlock/__main__.py 2023-01-30 04:04:07.000000000 +0700
diff --git a/kvmd/helpers/unlock/__main__.py b/kvmd/helpers/unlock/__main__.py
new file mode 100644
index 00000000..3849d1b9
--- /dev/null
+++ b/kvmd/helpers/unlock/__main__.py
@@ -0,0 +1,24 @@
+# ========================================================================== #
+# #
@ -204,10 +233,87 @@ diff -ruN kvmd/helpers/unlock/__main__.py kvmd/helpers/unlock/__main__.py
+
+from . import main
+main()
diff -ruN kvmd/plugins/msd/otg/drive.py kvmd/plugins/msd/otg/drive.py
--- kvmd/plugins/msd/otg/drive.py 2022-12-22 10:01:48.000000000 +0700
+++ kvmd/plugins/msd/otg/drive.py 2023-01-30 06:31:13.923959000 +0700
@@ -51,10 +51,7 @@
diff --git a/kvmd/plugins/msd/otg/__init__.py b/kvmd/plugins/msd/otg/__init__.py
index 409b899a..1342c6b4 100644
--- a/kvmd/plugins/msd/otg/__init__.py
+++ b/kvmd/plugins/msd/otg/__init__.py
@@ -140,6 +140,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
storage_path: str,
remount_cmd: List[str],
+ unlock_cmd: List[str],
initial: Dict,
@@ -154,6 +155,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
self.__meta_path = os.path.join(self.__storage_path, "meta")
self.__remount_cmd = remount_cmd
+ self.__unlock_cmd = unlock_cmd
self.__initial_image: str = initial["image"]
self.__initial_cdrom: bool = initial["cdrom"]
@@ -178,10 +180,8 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
"storage": Option("/var/lib/kvmd/msd", type=valid_abs_dir, unpack_as="storage_path"),
- "remount_cmd": Option([
- "/usr/bin/sudo", "--non-interactive",
- "/usr/bin/kvmd-helper-otgmsd-remount", "{mode}",
- ], type=valid_command),
+ "remount_cmd": Option([*sudo, "/usr/bin/kvmd-helper-otgmsd-remount", "{mode}"], type=valid_command),
+ "unlock_cmd": Option([*sudo, "/usr/bin/kvmd-helper-otgmsd-unlock", "unlock"], type=valid_command),
"initial": {
"image": Option("", type=valid_printable_filename, if_empty=""),
@@ -241,6 +241,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
async def reset(self) -> None:
async with self.__state.busy(check_online=False):
try:
+ await self.__unlock_drive()
self.__drive.set_image_path("")
self.__drive.set_rw_flag(False)
self.__drive.set_cdrom_flag(False)
@@ -290,12 +291,15 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
if not os.path.exists(self.__state.vd.image.path):
raise MsdUnknownImageError()
+ await self.__unlock_drive()
self.__drive.set_cdrom_flag(self.__state.vd.cdrom)
self.__drive.set_image_path(self.__state.vd.image.path)
else:
if not (self.__state.vd.connected or self.__drive.get_image_path()):
raise MsdDisconnectedError()
+
+ await self.__unlock_drive()
self.__drive.set_image_path("")
self.__state.vd.connected = connected
@@ -474,6 +478,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
if os.path.exists(path):
logger.info("Setting up initial image %r ...", self.__initial_image)
try:
+ await self.__unlock_drive()
self.__drive.set_cdrom_flag(self.__initial_cdrom)
self.__drive.set_image_path(path)
except Exception:
@@ -541,4 +546,8 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
async def __remount_storage(self, rw: bool) -> None:
if not (await aiohelpers.remount("MSD", self.__remount_cmd, rw)):
- raise MsdError("Can't execute remount helper")
+ pass
+ #raise MsdError("Can't execute remount helper")
+
+ async def __unlock_drive(self) -> None:
+ await helpers.unlock_drive(self.__unlock_cmd)
\ No newline at end of file
diff --git a/kvmd/plugins/msd/otg/drive.py b/kvmd/plugins/msd/otg/drive.py
index 11af7f81..ee54e5e9 100644
--- a/kvmd/plugins/msd/otg/drive.py
+++ b/kvmd/plugins/msd/otg/drive.py
@@ -53,10 +53,7 @@ class Drive:
# =====
def set_image_path(self, path: str) -> None:
@ -219,78 +325,6 @@ diff -ruN kvmd/plugins/msd/otg/drive.py kvmd/plugins/msd/otg/drive.py
def get_image_path(self) -> str:
return self.__get_param("file")
diff -ruN kvmd/plugins/msd/otg/__init__.py kvmd/plugins/msd/otg/__init__.py
--- kvmd/plugins/msd/otg/__init__.py 2023-02-02 09:42:28.021418683 +0700
+++ kvmd/plugins/msd/otg/__init__.py 2023-02-02 09:50:38.774955045 +0700
@@ -129,6 +129,7 @@
sync_chunk_size: int,
--
2.34.1.windows.1
remount_cmd: list[str],
+ unlock_cmd: list[str],
initial: dict,
@@ -140,6 +141,7 @@
self.__sync_chunk_size = sync_chunk_size
self.__remount_cmd = remount_cmd
+ self.__unlock_cmd = unlock_cmd
self.__initial_image: str = initial["image"]
self.__initial_cdrom: bool = initial["cdrom"]
@@ -169,6 +171,11 @@
"/usr/bin/kvmd-helper-otgmsd-remount", "{mode}",
], type=valid_command),
+ "unlock_cmd": Option([
+ "/usr/bin/sudo", "--non-interactive",
+ "/usr/bin/kvmd-helper-otgmsd-unlock", "unlock",
+ ], type=valid_command),
+
"initial": {
"image": Option("", type=valid_printable_filename, if_empty=""),
"cdrom": Option(False, type=valid_bool),
@@ -230,6 +237,7 @@
async def reset(self) -> None:
async with self.__state.busy(check_online=False):
try:
+ await self.__unlock_drive()
self.__drive.set_image_path("")
self.__drive.set_cdrom_flag(False)
self.__drive.set_rw_flag(False)
@@ -286,7 +294,7 @@
raise MsdUnknownImageError()
assert self.__state.vd.image.in_storage
-
+ await self.__unlock_drive()
self.__drive.set_rw_flag(self.__state.vd.rw)
self.__drive.set_cdrom_flag(self.__state.vd.cdrom)
if self.__state.vd.rw:
@@ -294,6 +302,7 @@
self.__drive.set_image_path(self.__state.vd.image.path)
else:
+ await self.__unlock_drive()
self.__state_check_connected()
self.__drive.set_image_path("")
await self.__remount_rw(False, fatal=False)
@@ -499,6 +508,7 @@
if image.exists():
logger.info("Setting up initial image %r ...", self.__initial_image)
try:
+ await self.__unlock_drive()
self.__drive.set_rw_flag(False)
self.__drive.set_cdrom_flag(self.__initial_cdrom)
self.__drive.set_image_path(image.path)
@@ -531,5 +541,8 @@
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")
+ pass
+ #raise MsdError("Can't execute remount helper")
+
+ async def __unlock_drive(self) -> None:
+ await aiohelpers.unlock_drive(self.__unlock_cmd)

View File

@ -0,0 +1,11 @@
#!/bin/bash
APP_PATH=$(readlink -f $(dirname $0))
echo "-> Apply patches"
cd /usr/lib/python3.10/site-packages/
git apply ${APP_PATH}/*.patch
cd ${APP_PATH}
echo "-> Add otgmsd unlock link"
cp kvmd-helper-otgmsd-unlock /usr/bin/
echo "-> Add sudoer"
echo "kvmd ALL=(ALL) NOPASSWD: /usr/bin/kvmd-helper-otgmsd-unlock" >> /etc/sudoers.d/99_kvmd
echo "-> Apply old kernel msd patch done."

View File

@ -0,0 +1,7 @@
#!/usr/sbin/python
# KVMD-ARMBIAN
from kvmd.helpers.unlock import main
if __name__ == "__main__":
main()

0
patch/display.sh → patches/display.sh Executable file → Normal file
View File

View File

@ -2,7 +2,7 @@
# #
# KVMD - The main PiKVM daemon. #
# #
# Copyright (C) 2018-2022 Maxim Devaev <mdevaev@gmail.com> #
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
@ -31,7 +31,7 @@ from ....logging import get_logger
from .... import env
from .... import tools
from .... import aiofs
from .... import aiotools
from .... import aioproc
from .base import BaseInfoSubmanager
@ -46,33 +46,36 @@ class HwInfoSubmanager(BaseInfoSubmanager):
def __init__(
self,
vcgencmd_cmd: list[str],
ignore_past: bool,
state_poll: float,
) -> None:
self.__vcgencmd_cmd = vcgencmd_cmd
self.__ignore_past = ignore_past
self.__state_poll = state_poll
self.__dt_cache: dict[str, str] = {}
async def get_state(self) -> dict:
(model, cpu_temp, throttling) = await asyncio.gather(
self.__read_dt_file("model"),
self.__get_cpu_temp(),
self.__get_throttling(),
)
return {
"platform": {
"type": "rpi",
"base": model,
"serial": "0000000000000000",
},
"health": {
"temp": {
"cpu": cpu_temp,
},
"throttling": throttling,
},
}
#async def get_state(self) -> dict:
# (model, serial, cpu_temp, throttling) = await asyncio.gather(
# self.__read_dt_file("model"),
# self.__read_dt_file("serial-number"),
# self.__get_cpu_temp(),
# self.__get_throttling(),
# )
# return {
# "platform": {
# "type": "rpi",
# "base": model,
# "serial": serial,
# },
# "health": {
# "temp": {
# "cpu": cpu_temp,
# },
# "throttling": throttling,
# },
# }
async def poll_state(self) -> AsyncGenerator[dict, None]:
prev_state: dict = {}
@ -89,7 +92,7 @@ class HwInfoSubmanager(BaseInfoSubmanager):
if name not in self.__dt_cache:
path = os.path.join(f"{env.PROCFS_PREFIX}/proc/device-tree", name)
try:
self.__dt_cache[name] = (await aiofs.read(path)).strip(" \t\r\n\0")
self.__dt_cache[name] = (await aiotools.read_file(path)).strip(" \t\r\n\0")
except Exception as err:
get_logger(0).error("Can't read DT %s from %s: %s", name, path, err)
return None
@ -98,8 +101,9 @@ class HwInfoSubmanager(BaseInfoSubmanager):
async def __get_cpu_temp(self) -> (float | None):
temp_path = f"{env.SYSFS_PREFIX}/sys/class/thermal/thermal_zone0/temp"
try:
return int((await aiofs.read(temp_path)).strip()) / 1000
return int((await aiotools.read_file(temp_path)).strip()) / 1000
except Exception as err:
get_logger(0).error("Can't read CPU temp from %s: %s", temp_path, err)
return None
async def __get_throttling(self) -> (dict | None):
@ -125,6 +129,7 @@ class HwInfoSubmanager(BaseInfoSubmanager):
"past": bool(flags & (1 << 18)),
},
},
"ignore_past": self.__ignore_past,
}
return None

417
patches/session.js Normal file
View File

@ -0,0 +1,417 @@
/*****************************************************************************
# #
# KVMD - The main PiKVM daemon. #
# #
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
# #
*****************************************************************************/
"use strict";
import {tools, $} from "../tools.js";
import {wm} from "../wm.js";
import {Recorder} from "./recorder.js";
import {Hid} from "./hid.js";
import {Atx} from "./atx.js";
import {Msd} from "./msd.js";
import {Streamer} from "./stream.js";
import {Gpio} from "./gpio.js";
import {Ocr} from "./ocr.js";
export function Session() {
// var self = this;
/************************************************************************/
var __ws = null;
var __ping_timer = null;
var __missed_heartbeats = 0;
var __streamer = new Streamer();
var __recorder = new Recorder();
var __hid = new Hid(__streamer.getGeometry, __recorder);
var __atx = new Atx(__recorder);
var __msd = new Msd();
var __gpio = new Gpio(__recorder);
var __ocr = new Ocr(__streamer.getGeometry);
var __info_hw_state = null;
var __info_fan_state = null;
var __init__ = function() {
__startSession();
};
/************************************************************************/
var __setAboutInfoMeta = function(state) {
if (state !== null) {
let text = JSON.stringify(state, undefined, 4).replace(/ /g, "&nbsp;").replace(/\n/g, "<br>");
$("about-meta").innerHTML = `
<span class="code-comment">// The PiKVM metadata.<br>
// You can get this JSON using handle <a target="_blank" href="/api/info?fields=meta">/api/info?fields=meta</a>.<br>
// In the standard configuration this data<br>
// is specified in the file /etc/kvmd/meta.yaml.</span><br>
<br>
${text}
`;
if (state.server && state.server.host) {
$("kvmd-meta-server-host").innerHTML = `Server: ${state.server.host}`;
document.title = `PiKVM Session: ${state.server.host}`;
} else {
$("kvmd-meta-server-host").innerHTML = "";
document.title = "PiKVM Session";
}
// Don't use this option, it may be removed in any time
if (state.web && state.web.confirm_session_exit === false) {
window.onbeforeunload = null; // See main.js
}
}
};
var __setAboutInfoHw = function(state) {
if (state.health.throttling !== null) {
let flags = state.health.throttling.parsed_flags;
let ignore_past = state.health.throttling.ignore_past;
let undervoltage = (flags.undervoltage.now || (flags.undervoltage.past && !ignore_past));
let freq_capped = (flags.freq_capped.now || (flags.freq_capped.past && !ignore_past));
tools.hidden.setVisible($("hw-health-dropdown"), (undervoltage || freq_capped));
$("hw-health-undervoltage-led").className = (undervoltage ? (flags.undervoltage.now ? "led-red" : "led-yellow") : "hidden");
$("hw-health-overheating-led").className = (freq_capped ? (flags.freq_capped.now ? "led-red" : "led-yellow") : "hidden");
tools.hidden.setVisible($("hw-health-message-undervoltage"), undervoltage);
tools.hidden.setVisible($("hw-health-message-overheating"), freq_capped);
}
__info_hw_state = state;
__renderAboutInfoHardware();
};
var __setAboutInfoFan = function(state) {
let failed = false;
let failed_past = false;
if (state.monitored) {
if (state.state === null) {
failed = true;
} else {
if (!state.state.fan.ok) {
failed = true;
} else if (state.state.fan.last_fail_ts >= 0) {
failed = true;
failed_past = true;
}
}
}
tools.hidden.setVisible($("fan-health-dropdown"), failed);
$("fan-health-led").className = (failed ? (failed_past ? "led-yellow" : "led-red") : "hidden");
__info_fan_state = state;
__renderAboutInfoHardware();
};
var __renderAboutInfoHardware = function() {
let html = "";
if (__info_hw_state !== null) {
html += `
Platform:
${__formatPlatform(__info_hw_state.platform)}
<hr>
Temperature:
${__formatTemp(__info_hw_state.health.temp)}
<hr>
Throttling:
${__formatThrottling(__info_hw_state.health.throttling)}
`;
}
if (__info_fan_state !== null) {
if (html.length > 0) {
html += "<hr>";
}
html += `
Fan:
${__formatFan(__info_fan_state)}
`;
}
$("about-hardware").innerHTML = html;
};
var __formatPlatform = function(state) {
return __formatUl([["Base", state.base], ["Serial", state.serial]]);
};
var __formatFan = function(state) {
if (!state.monitored) {
return __formatUl([["Status", "Not monitored"]]);
} else if (state.state === null) {
return __formatUl([["Status", __colored("red", "Not available")]]);
} else {
state = state.state;
let pairs = [
["Status", (state.fan.ok ? __colored("green", "Ok") : __colored("red", "Failed"))],
["Desired speed", `${state.fan.speed}%`],
["PWM", `${state.fan.pwm}`],
];
if (state.hall.available) {
pairs.push(["RPM", __colored((state.fan.ok ? "green" : "red"), state.hall.rpm)]);
}
return __formatUl(pairs);
}
};
var __formatTemp = function(temp) {
let pairs = [];
for (let field of Object.keys(temp).sort()) {
pairs.push([field.toUpperCase(), `${temp[field]}&deg;C`]);
}
return __formatUl(pairs);
};
var __formatThrottling = function(throttling) {
if (throttling !== null) {
let pairs = [];
for (let field of Object.keys(throttling.parsed_flags).sort()) {
let flags = throttling.parsed_flags[field];
let key = tools.upperFirst(field).replace("_", " ");
let value = (flags["now"] ? __colored("red", "RIGHT NOW") : __colored("green", "No"));
if (!throttling.ignore_past) {
value += "; " + (flags["past"] ? __colored("red", "In the past") : __colored("green", "Never"));
}
pairs.push([key, value]);
}
return __formatUl(pairs);
} else {
return "NO DATA";
}
};
var __colored = function(color, text) {
return `<font color="${color}">${text}</font>`;
};
var __setAboutInfoSystem = function(state) {
$("about-version").innerHTML = `
KVMD: <span class="code-comment">${state.kvmd.version}</span><br>
<hr>
Streamer: <span class="code-comment">${state.streamer.version} (${state.streamer.app})</span>
${__formatStreamerFeatures(state.streamer.features)}
<hr>
${state.kernel.system} kernel:
${__formatUname(state.kernel)}
`;
$("kvmd-version-kvmd").innerHTML = state.kvmd.version;
$("kvmd-version-streamer").innerHTML = state.streamer.version;
};
var __formatStreamerFeatures = function(features) {
let pairs = [];
for (let field of Object.keys(features).sort()) {
pairs.push([field, (features[field] ? "Yes" : "No")]);
}
return __formatUl(pairs);
};
var __formatUname = function(kernel) {
let pairs = [];
for (let field of Object.keys(kernel).sort()) {
if (field !== "system") {
pairs.push([tools.upperFirst(field), kernel[field]]);
}
}
return __formatUl(pairs);
};
var __formatUl = function(pairs) {
let text = "<ul>";
for (let pair of pairs) {
text += `<li>${pair[0]}: <span class="code-comment">${pair[1]}</span></li>`;
}
return text + "</ul>";
};
var __setExtras = function(state) {
let show_hook = null;
let close_hook = null;
let has_webterm = (state.webterm && (state.webterm.enabled || state.webterm.started));
if (has_webterm) {
let path = "/" + state.webterm.path;
show_hook = function() {
tools.info("Terminal opened: ", path);
$("webterm-iframe").src = path;
};
close_hook = function() {
tools.info("Terminal closed");
$("webterm-iframe").src = "";
};
}
tools.feature.setEnabled($("system-tool-webterm"), has_webterm);
$("webterm-window").show_hook = show_hook;
$("webterm-window").close_hook = close_hook;
__streamer.setJanusEnabled(
(state.janus && (state.janus.enabled || state.janus.started))
|| (state.janus_static && (state.janus_static.enabled || state.janus_static.started))
);
};
var __startSession = function() {
$("link-led").className = "led-yellow";
$("link-led").title = "Connecting...";
let http = tools.makeRequest("GET", "/api/auth/check", function() {
if (http.readyState === 4) {
if (http.status === 200) {
__ws = new WebSocket(`${tools.is_https ? "wss" : "ws"}://${location.host}/api/ws`);
__ws.sendHidEvent = (event) => __sendHidEvent(__ws, event.event_type, event.event);
__ws.onopen = __wsOpenHandler;
__ws.onmessage = __wsMessageHandler;
__ws.onerror = __wsErrorHandler;
__ws.onclose = __wsCloseHandler;
} else if (http.status === 401 || http.status === 403) {
window.onbeforeunload = () => null;
wm.error("Unexpected logout occured, please login again").then(function() {
document.location.href = "/login";
});
} else {
__wsCloseHandler(null);
}
}
});
};
var __ascii_encoder = new TextEncoder("ascii");
var __sendHidEvent = function(ws, event_type, event) {
if (event_type == "key") {
let data = __ascii_encoder.encode("\x01\x00" + event.key);
data[1] = (event.state ? 1 : 0);
ws.send(data);
} else if (event_type == "mouse_button") {
let data = __ascii_encoder.encode("\x02\x00" + event.button);
data[1] = (event.state ? 1 : 0);
ws.send(data);
} else if (event_type == "mouse_move") {
let data = new Uint8Array([
3,
(event.to.x >> 8) & 0xFF, event.to.x & 0xFF,
(event.to.y >> 8) & 0xFF, event.to.y & 0xFF,
]);
ws.send(data);
} else if (event_type == "mouse_relative" || event_type == "mouse_wheel") {
let data;
if (Array.isArray(event.delta)) {
data = new Int8Array(2 + event.delta.length * 2);
let index = 0;
for (let delta of event.delta) {
data[index + 2] = delta["x"];
data[index + 3] = delta["y"];
index += 2;
}
} else {
data = new Int8Array([0, 0, event.delta.x, event.delta.y]);
}
data[0] = (event_type == "mouse_relative" ? 4 : 5);
data[1] = (event.squash ? 1 : 0);
ws.send(data);
}
};
var __wsOpenHandler = function(event) {
tools.debug("Session: socket opened:", event);
$("link-led").className = "led-green";
$("link-led").title = "Connected";
__recorder.setSocket(__ws);
__hid.setSocket(__ws);
__missed_heartbeats = 0;
__ping_timer = setInterval(__pingServer, 1000);
};
var __wsMessageHandler = function(event) {
// tools.debug("Session: received socket data:", event.data);
let data = JSON.parse(event.data);
switch (data.event_type) {
case "pong": __missed_heartbeats = 0; break;
case "info_meta_state": __setAboutInfoMeta(data.event); break;
case "info_hw_state": __setAboutInfoHw(data.event); break;
case "info_fan_state": __setAboutInfoFan(data.event); break;
case "info_system_state": __setAboutInfoSystem(data.event); break;
case "info_extras_state": __setExtras(data.event); break;
case "gpio_model_state": __gpio.setModel(data.event); break;
case "gpio_state": __gpio.setState(data.event); break;
case "hid_keymaps_state": __hid.setKeymaps(data.event); break;
case "hid_state": __hid.setState(data.event); break;
case "atx_state": __atx.setState(data.event); break;
case "msd_state": __msd.setState(data.event); break;
case "streamer_state": __streamer.setState(data.event); break;
case "streamer_ocr_state": __ocr.setState(data.event); break;
}
};
var __wsErrorHandler = function(event) {
tools.error("Session: socket error:", event);
if (__ws) {
__ws.onclose = null;
__ws.close();
__wsCloseHandler(null);
}
};
var __wsCloseHandler = function(event) {
tools.debug("Session: socket closed:", event);
$("link-led").className = "led-gray";
if (__ping_timer) {
clearInterval(__ping_timer);
__ping_timer = null;
}
__ocr.setState(null);
__gpio.setState(null);
__hid.setSocket(null);
__recorder.setSocket(null);
__atx.setState(null);
__msd.setState(null);
__streamer.setState(null);
__ws = null;
setTimeout(function() {
$("link-led").className = "led-yellow";
setTimeout(__startSession, 500);
}, 500);
};
var __pingServer = function() {
try {
__missed_heartbeats += 1;
if (__missed_heartbeats >= 15) {
throw new Error("Too many missed heartbeats");
}
__ws.send("{\"event_type\": \"ping\", \"event\": {}}");
} catch (err) {
__wsErrorHandler(err.message);
}
};
__init__();
}

482
patches/streamer.py.1 Normal file
View File

@ -0,0 +1,482 @@
# ========================================================================== #
# #
# KVMD - The main PiKVM daemon. #
# #
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
# #
# ========================================================================== #
import io
import signal
import asyncio
import asyncio.subprocess
import dataclasses
import functools
from typing import AsyncGenerator
from typing import Any
import aiohttp
from PIL import Image as PilImage
from ...logging import get_logger
from ... import tools
from ... import aiotools
from ... import aioproc
from ... import htclient
# =====
@dataclasses.dataclass(frozen=True)
class StreamerSnapshot:
online: bool
width: int
height: int
headers: tuple[tuple[str, str], ...]
data: bytes
async def make_preview(self, max_width: int, max_height: int, quality: int) -> bytes:
assert max_width >= 0
assert max_height >= 0
assert quality > 0
if max_width == 0 and max_height == 0:
max_width = self.width // 5
max_height = self.height // 5
else:
max_width = min((max_width or self.width), self.width)
max_height = min((max_height or self.height), self.height)
if (max_width, max_height) == (self.width, self.height):
return self.data
return (await aiotools.run_async(self.__inner_make_preview, max_width, max_height, quality))
@functools.lru_cache(maxsize=1)
def __inner_make_preview(self, max_width: int, max_height: int, quality: int) -> bytes:
with io.BytesIO(self.data) as snapshot_bio:
with io.BytesIO() as preview_bio:
with PilImage.open(snapshot_bio) as image:
image.thumbnail((max_width, max_height), PilImage.Resampling.LANCZOS)
image.save(preview_bio, format="jpeg", quality=quality)
return preview_bio.getvalue()
class _StreamerParams:
__DESIRED_FPS = "desired_fps"
__QUALITY = "quality"
__RESOLUTION = "resolution"
__AVAILABLE_RESOLUTIONS = "available_resolutions"
__H264_BITRATE = "h264_bitrate"
__H264_GOP = "h264_gop"
def __init__( # pylint: disable=too-many-arguments
self,
quality: int,
resolution: str,
available_resolutions: list[str],
desired_fps: int,
desired_fps_min: int,
desired_fps_max: int,
h264_bitrate: int,
h264_bitrate_min: int,
h264_bitrate_max: int,
h264_gop: int,
h264_gop_min: int,
h264_gop_max: int,
) -> None:
self.__has_quality = bool(quality)
self.__has_resolution = bool(resolution)
self.__has_h264 = bool(h264_bitrate)
self.__params: dict = {self.__DESIRED_FPS: min(max(desired_fps, desired_fps_min), desired_fps_max)}
self.__limits: dict = {self.__DESIRED_FPS: {"min": desired_fps_min, "max": desired_fps_max}}
if self.__has_quality:
self.__params[self.__QUALITY] = quality
if self.__has_resolution:
self.__params[self.__RESOLUTION] = resolution
self.__limits[self.__AVAILABLE_RESOLUTIONS] = available_resolutions
if self.__has_h264:
self.__params[self.__H264_BITRATE] = min(max(h264_bitrate, h264_bitrate_min), h264_bitrate_max)
self.__limits[self.__H264_BITRATE] = {"min": h264_bitrate_min, "max": h264_bitrate_max}
self.__params[self.__H264_GOP] = min(max(h264_gop, h264_gop_min), h264_gop_max)
self.__limits[self.__H264_GOP] = {"min": h264_gop_min, "max": h264_gop_max}
def get_features(self) -> dict:
return {
self.__QUALITY: self.__has_quality,
self.__RESOLUTION: self.__has_resolution,
"h264": self.__has_h264,
}
def get_limits(self) -> dict:
limits = dict(self.__limits)
if self.__has_resolution:
limits[self.__AVAILABLE_RESOLUTIONS] = list(limits[self.__AVAILABLE_RESOLUTIONS])
return limits
def get_params(self) -> dict:
return dict(self.__params)
def set_params(self, params: dict) -> None:
new_params = dict(self.__params)
if self.__QUALITY in params and self.__has_quality:
new_params[self.__QUALITY] = min(max(params[self.__QUALITY], 1), 100)
if self.__RESOLUTION in params and self.__has_resolution:
if params[self.__RESOLUTION] in self.__limits[self.__AVAILABLE_RESOLUTIONS]:
new_params[self.__RESOLUTION] = params[self.__RESOLUTION]
for (key, enabled) in [
(self.__DESIRED_FPS, True),
(self.__H264_BITRATE, self.__has_h264),
(self.__H264_GOP, self.__has_h264),
]:
if key in params and enabled:
if self.__check_limits_min_max(key, params[key]):
new_params[key] = params[key]
self.__params = new_params
def __check_limits_min_max(self, key: str, value: int) -> bool:
return (self.__limits[key]["min"] <= value <= self.__limits[key]["max"])
class Streamer: # pylint: disable=too-many-instance-attributes
def __init__( # pylint: disable=too-many-arguments,too-many-locals
self,
reset_delay: float,
shutdown_delay: float,
state_poll: float,
unix_path: str,
timeout: float,
process_name_prefix: str,
pre_start_cmd: list[str],
pre_start_cmd_remove: list[str],
pre_start_cmd_append: list[str],
cmd: list[str],
cmd_remove: list[str],
cmd_append: list[str],
post_stop_cmd: list[str],
post_stop_cmd_remove: list[str],
post_stop_cmd_append: list[str],
**params_kwargs: Any,
) -> None:
self.__reset_delay = reset_delay
self.__shutdown_delay = shutdown_delay
self.__state_poll = state_poll
self.__unix_path = unix_path
self.__timeout = timeout
self.__process_name_prefix = process_name_prefix
self.__pre_start_cmd = tools.build_cmd(pre_start_cmd, pre_start_cmd_remove, pre_start_cmd_append)
self.__cmd = tools.build_cmd(cmd, cmd_remove, cmd_append)
self.__post_stop_cmd = tools.build_cmd(post_stop_cmd, post_stop_cmd_remove, post_stop_cmd_append)
self.__params = _StreamerParams(**params_kwargs)
self.__stop_task: (asyncio.Task | None) = None
self.__stop_wip = False
self.__streamer_task: (asyncio.Task | None) = None
self.__streamer_proc: (asyncio.subprocess.Process | None) = None # pylint: disable=no-member
self.__http_session: (aiohttp.ClientSession | None) = None
self.__snapshot: (StreamerSnapshot | None) = None
self.__notifier = aiotools.AioNotifier()
# =====
@aiotools.atomic_fg
async def ensure_start(self, reset: bool) -> None:
if not self.__streamer_task or self.__stop_task:
logger = get_logger(0)
if self.__stop_task:
if not self.__stop_wip:
self.__stop_task.cancel()
await asyncio.gather(self.__stop_task, return_exceptions=True)
logger.info("Streamer stop cancelled")
return
else:
await asyncio.gather(self.__stop_task, return_exceptions=True)
if reset and self.__reset_delay > 0:
logger.info("Waiting %.2f seconds for reset delay ...", self.__reset_delay)
await asyncio.sleep(self.__reset_delay)
logger.info("Starting streamer ...")
await self.__inner_start()
@aiotools.atomic_fg
async def ensure_stop(self, immediately: bool) -> None:
if self.__streamer_task:
logger = get_logger(0)
if immediately:
if self.__stop_task:
if not self.__stop_wip:
self.__stop_task.cancel()
await asyncio.gather(self.__stop_task, return_exceptions=True)
logger.info("Stopping streamer immediately ...")
await self.__inner_stop()
else:
await asyncio.gather(self.__stop_task, return_exceptions=True)
else:
logger.info("Stopping streamer immediately ...")
await self.__inner_stop()
elif not self.__stop_task:
async def delayed_stop() -> None:
try:
await asyncio.sleep(self.__shutdown_delay)
self.__stop_wip = True
logger.info("Stopping streamer after delay ...")
await self.__inner_stop()
finally:
self.__stop_task = None
self.__stop_wip = False
logger.info("Planning to stop streamer in %.2f seconds ...", self.__shutdown_delay)
self.__stop_task = asyncio.create_task(delayed_stop())
def is_working(self) -> bool:
# Запущено и не планирует останавливаться
return bool(self.__streamer_task and not self.__stop_task)
# =====
def set_params(self, params: dict) -> None:
assert not self.__streamer_task
return self.__params.set_params(params)
def get_params(self) -> dict:
return self.__params.get_params()
# =====
async def get_state(self) -> dict:
streamer_state = None
if self.__streamer_task:
session = self.__ensure_http_session()
try:
async with session.get(self.__make_url("state")) as response:
htclient.raise_not_200(response)
streamer_state = (await response.json())["result"]
except (aiohttp.ClientConnectionError, aiohttp.ServerConnectionError):
pass
except Exception:
get_logger().exception("Invalid streamer response from /state")
snapshot: (dict | None) = None
if self.__snapshot:
snapshot = dataclasses.asdict(self.__snapshot)
del snapshot["headers"]
del snapshot["data"]
return {
"limits": self.__params.get_limits(),
"params": self.__params.get_params(),
"snapshot": {"saved": snapshot},
"streamer": streamer_state,
"features": self.__params.get_features(),
}
async def poll_state(self) -> AsyncGenerator[dict, None]:
def signal_handler(*_: Any) -> None:
get_logger(0).info("Got SIGUSR2, checking the stream state ...")
self.__notifier.notify()
get_logger(0).info("Installing SIGUSR2 streamer handler ...")
asyncio.get_event_loop().add_signal_handler(signal.SIGUSR2, signal_handler)
waiter_task: (asyncio.Task | None) = None
prev_state: dict = {}
while True:
state = await self.get_state()
if state != prev_state:
yield state
prev_state = state
if waiter_task is None:
waiter_task = asyncio.create_task(self.__notifier.wait())
if waiter_task in (await aiotools.wait_first(
asyncio.ensure_future(asyncio.sleep(self.__state_poll)),
waiter_task,
))[0]:
waiter_task = None
# =====
async def take_snapshot(self, save: bool, load: bool, allow_offline: bool) -> (StreamerSnapshot | None):
if load:
return self.__snapshot
else:
logger = get_logger()
session = self.__ensure_http_session()
try:
async with session.get(self.__make_url("snapshot")) as response:
htclient.raise_not_200(response)
online = (response.headers["X-UStreamer-Online"] == "true")
if online or allow_offline:
snapshot = StreamerSnapshot(
online=online,
width=int(response.headers["X-UStreamer-Width"]),
height=int(response.headers["X-UStreamer-Height"]),
headers=tuple(
(key, value)
for (key, value) in tools.sorted_kvs(dict(response.headers))
if key.lower().startswith("x-ustreamer-") or key.lower() in [
"x-timestamp",
"access-control-allow-origin",
"cache-control",
"pragma",
"expires",
]
),
data=bytes(await response.read()),
)
if save:
self.__snapshot = snapshot
self.__notifier.notify()
return snapshot
logger.error("Stream is offline, no signal or so")
except (aiohttp.ClientConnectionError, aiohttp.ServerConnectionError) as err:
logger.error("Can't connect to streamer: %s", tools.efmt(err))
except Exception:
logger.exception("Invalid streamer response from /snapshot")
return None
def remove_snapshot(self) -> None:
self.__snapshot = None
# =====
@aiotools.atomic_fg
async def cleanup(self) -> None:
await self.ensure_stop(immediately=True)
if self.__http_session:
await self.__http_session.close()
self.__http_session = None
# =====
def __ensure_http_session(self) -> aiohttp.ClientSession:
if not self.__http_session:
kwargs: dict = {
"headers": {"User-Agent": htclient.make_user_agent("KVMD")},
"connector": aiohttp.UnixConnector(path=self.__unix_path),
"timeout": aiohttp.ClientTimeout(total=self.__timeout),
}
self.__http_session = aiohttp.ClientSession(**kwargs)
return self.__http_session
def __make_url(self, handle: str) -> str:
assert not handle.startswith("/"), handle
return f"http://localhost:0/{handle}"
# =====
@aiotools.atomic_fg
async def __inner_start(self) -> None:
assert not self.__streamer_task
await self.__run_hook("PRE-START-CMD", self.__pre_start_cmd)
self.__streamer_task = asyncio.create_task(self.__streamer_task_loop())
@aiotools.atomic_fg
async def __inner_stop(self) -> None:
assert self.__streamer_task
self.__streamer_task.cancel()
await asyncio.gather(self.__streamer_task, return_exceptions=True)
await self.__kill_streamer_proc()
await self.__run_hook("POST-STOP-CMD", self.__post_stop_cmd)
self.__streamer_task = None
# =====
async def __streamer_task_loop(self) -> None: # pylint: disable=too-many-branches
logger = get_logger(0)
while True: # pylint: disable=too-many-nested-blocks
try:
await self.__start_streamer_proc()
assert self.__streamer_proc is not None
await aioproc.log_stdout_infinite(self.__streamer_proc, logger)
raise RuntimeError("Streamer unexpectedly died")
except asyncio.CancelledError:
break
except Exception:
if self.__streamer_proc:
logger.exception("Unexpected streamer error: pid=%d", self.__streamer_proc.pid)
else:
logger.exception("Can't start streamer")
await self.__kill_streamer_proc()
await asyncio.sleep(1)
def __make_cmd(self, cmd: list[str]) -> list[str]:
return [
part.format(
unix=self.__unix_path,
process_name_prefix=self.__process_name_prefix,
**self.__params.get_params(),
)
for part in cmd
]
async def __run_hook(self, name: str, cmd: list[str]) -> None:
logger = get_logger()
cmd = self.__make_cmd(cmd)
logger.info("%s: %s", name, tools.cmdfmt(cmd))
try:
await aioproc.log_process(cmd, logger, prefix=name)
except Exception as err:
logger.exception("Can't execute command: %s", err)
async def __start_streamer_proc(self) -> None:
assert self.__streamer_proc is None
cmd = self.__make_cmd(self.__cmd)
self.__streamer_proc = await aioproc.run_process(cmd)
get_logger(0).info("Started streamer pid=%d: %s", self.__streamer_proc.pid, tools.cmdfmt(cmd))
async def __kill_streamer_proc(self) -> None:
if self.__streamer_proc:
await aioproc.kill_process(self.__streamer_proc, 1, get_logger(0))
self.__streamer_proc = None

47
pi-temp Normal file
View File

@ -0,0 +1,47 @@
#!/bin/bash
# Script: pi-temp.sh
# This should work on both arch and ubuntu/debian
# Purpose: Display the ARM CPU and GPU temperatures of Raspberry Pi 2/3/4/Zero
# Edited by: srepac <srepac@kvmnerds.com>
# convert C to F and show both temps in output
# -------------------------------------------------------
# install basic calculator for conversions
if [ `which bc | wc -l` -le 0 ]; then
sudo apt-get install bc -y
fi
MODEL=$(tr -d '\0' </proc/device-tree/model)
# code section to get Pi RAM size
RAMGB=$( sudo vcgencmd get_config total_mem | awk -F= '{NUM = $2} END {
if ( NUM < 1024 ) print NUM "MB";
else print (NUM / 1024) "GB"; }' )
AVAIL=$( free -m | grep Mem | awk '{print $NF}' )
printf "$MODEL ${RAMGB} Hostname: $(hostname)\n$(date) Load average:$(uptime | awk -F: '{print $NF}') Avail RAM: ${AVAIL}MB\n"
GPUTEMP=`sudo $(which vcgencmd) measure_temp | awk -F= '{print $2}' | sed "s/'C//g"`
CPU=$(</sys/class/thermal/thermal_zone0/temp)
cpuC=`echo "scale=3; $CPU / 1000" | bc`
cpuF=`echo "scale=2; 9/5 * ${cpuC} + 32" | bc`
gpuF=`echo "scale=2; 9/5 * ${GPUTEMP} + 32" | bc`
#echo "$(date) @ $(hostname)"
echo "-------------------------------------------"
printf "CPU => ${cpuC}'C\t${cpuF}'F\n"
printf "GPU => ${GPUTEMP}'C\t${gpuF}'F\n"
MHZ=`sudo cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_cur_freq`
#echo "CPU0 MHZ: $((MHZ/1000))"
#echo "CPU (All) MHz: "
count=0
for i in `sudo cat /sys/devices/system/cpu/cpu*/cpufreq/cpuinfo_cur_freq`; do
cpu=`echo "scale=2; $i / 1000" | bc`
echo "CPU${count} MHz: $cpu"
count=`expr $count + 1`
done
sudo vcgencmd measure_volts | sed 's/volt=/vCore /g'
sudo vcgencmd get_throttled

51
pikvm-info Normal file
View File

@ -0,0 +1,51 @@
#!/bin/bash
# Put this file into /usr/local/bin then you can run pikvm-info anywhere while logged in to Pi-KVM terminal/ssh
#
uptime
NAME=$( egrep ^NAME= /etc/os-release | cut -d'"' -f2 )
echo "Host OS: $NAME `uname -r` `uname -m`"
# pistat is built-in to pi-kvm (/usr/local/bin)
pistat && echo
printf "%-32s\t%s\n" "Version" "Package-Name" "---------------------------------" "-----------------------------"
get-packages() {
if [[ $( echo $NAME | cut -d' ' -f1 ) == "Arch" ]]; then
if [ ! -e $TMPFILE ]; then
pacman -Q | awk '{print $2, $1}' > $TMPFILE
fi
else
KVMDPLAT=$( grep kvmd-platform /var/cache/kvmd/installed_ver.txt | awk '{print $4}' | awk -F\/ '{print $NF}' | tail -1 )
if [[ "$KVMDPLAT" != "" ]]; then
KVMDVER=$( echo $KVMDPLAT | cut -d'-' -f6 )
PLATFORM=$( echo $KVMDPLAT | cut -d'-' -f1,2,3,4,5 )
printf "%-32s\t%s\n" $KVMDVER $PLATFORM
fi
USTREAMVER=$( /usr/bin/ustreamer -v )
printf "%-32s\t%s\n" $USTREAMVER "ustreamer"
#JANUSVER=$( /usr/bin/janus -V | tail -1 | awk '{print $NF}' )
JANUSVER=$( /usr/bin/janus -V | egrep 'version|janus' | awk '{print $NF}' | sed -e 's|(||g' -e 's|)||g' )
printf "%-32s\t%s\n" $JANUSVER "janus"
TTYDVER=$( ttyd -v | awk '{print $NF}' )
printf "%-32s\t%s\n" $TTYDVER "ttyd"
if [ ! -e $TMPFILE ]; then
apt list 2> /dev/null | egrep 'upgradable|installed' | grep -v ^$ | awk '{print $2, $1}' > $TMPFILE
fi
fi
} # end get-packages
TMPFILE="/tmp/pacmanquery"
get-packages
PACKAGES="janus pikvm kvmd ustreamer nginx wpa wireless python3/stable python$ firmware raspberrypi tailscale"
for PKG in $( echo $PACKAGES ); do
if [ $(grep -w $PKG $TMPFILE | wc -l) -gt 0 ]; then
printf "%-32s\t%s\n" $(grep -w $PKG $TMPFILE | sed 's/-[1-9]+//g' | awk -F\/ '{print $1}' )
fi
done | sort -u | sort -k2

110
pistat Normal file
View File

@ -0,0 +1,110 @@
#!/bin/bash
# Copy this into /usr/local/bin/pistat
# This version will show Pi RAM size and will work in Raspbian also
# sample output: Raspberry Pi 4 Model B 1.2 4GB
if [[ $( whoami ) == "root" ]]; then
VCGENCMD="/usr/bin/vcgencmd"
else
VCGENCMD="sudo /usr/bin/vcgencmd"
fi
if [ -t 1 ]; then
color_comment=$(echo -e '\e[1;30m')
color_value=$(echo -e '\e[1;35m')
color_ok=$(echo -e '\e[1;32m')
color_fail=$(echo -e '\e[1;31m')
color_reset=$(echo -e '\e[0m')
else
color_comment=""
color_value=""
color_ok=""
color_fail=""
color_reset=""
fi
no="${color_ok}no${color_reset}"
yes="${color_fail}yes${color_reset}"
# code section to get Pi RAM size
RAMGB=$( $VCGENCMD get_config total_mem | awk -F= '{NUM = $2} END { if ( NUM < 1024 ) print NUM "MB"; else print (NUM / 1024) "GB"; }' )
if [ -f /proc/device-tree/model ]; then
echo "${color_reset}# $(tr -d '\0' < /proc/device-tree/model) ${RAMGB}${color_reset}"
else
RAM=$( echo "( $( free -m | grep Mem: | awk '{print $2}' ) + 340 ) / 1024" | bc )
TMPFILE="/tmp/dmidecode.system"; rm -f $TMPFILE
dmidecode -t system > $TMPFILE
echo "${color_reset}# $(grep -i version $TMPFILE | cut -d':' -f2 | sed 's/^ //g') $(grep Product $TMPFILE | cut -d: -f2 | cut -d\( -f1 | sed 's/^ //g') ${RAM}GB${color_reset}"
fi
if [ -e /proc/device-tree/serial-number ]; then
echo "${color_reset}Serial number: $(tr -d '\0' < /proc/device-tree/serial-number) ${color_reset}"
fi
echo
if [ -e /sys/class/thermal/thermal_zone0/temp ]; then
echo "CPU temp: ${color_value}$(echo "scale=2; $(</sys/class/thermal/thermal_zone0/temp) / 1000" | bc -l)'C${color_reset}"
gpu_temp=$($VCGENCMD measure_temp | cut -d'=' -f2 | cut -d"'" -f1 )
else
CPUTEMP=$(sensors 2> /dev/null | egrep -A 2 'coretemp|k10temp' | egrep '^temp|^Core' | awk '{print $2}' | sed -e 's/°C//g' -e 's/+//g')
echo "CPU temp: ${color_value}${CPUTEMP}'C${color_reset}"
gpu_temp=$(sensors 2> /dev/null | grep -A 2 radeon | grep ^temp | awk '{print $2}' | sed -e 's/°C//g' -e 's/+//g')
fi
echo "GPU temp: ${color_value}${gpu_temp}'C${color_reset}"
flags=$($VCGENCMD get_throttled)
flags="${flags#*=}"
echo
echo "Throttled flags: ${color_value}${flags}${color_reset}"
echo
echo -n "Throttled now: "
((($flags&0x4)!=0)) && echo "${yes}" || echo "${no}"
echo -n "Throttled past: "
((($flags&0x40000)!=0)) && echo "${yes}" || echo "${no}"
echo
echo -n "Undervoltage now: "
((($flags&0x1)!=0)) && echo "${yes}" || echo "${no}"
echo -n "Undervoltage past: "
((($flags&0x10000)!=0)) && echo "${yes}" || echo "${no}"
echo
echo -n "Frequency capped now: "
((($flags&0x2)!=0)) && echo "${yes}" || echo "${no}"
echo -n "Frequency capped past: "
((($flags&0x20000)!=0)) && echo "${yes}" || echo "${no}"
# =====
# https://stackoverflow.com/questions/13889659/read-a-file-by-bytes-in-bash
read8() {
local _r8_var=${1:-OUTBIN} _r8_car LANG=C IFS=
read -r -d '' -n 1 _r8_car
printf -v $_r8_var %d "'"$_r8_car
}
read16() {
local _r16_var=${1:-OUTBIN} _r16_lb _r16_hb
read8 _r16_hb && read8 _r16_lb
printf -v $_r16_var %d $(( _r16_hb<<8 | _r16_lb ))
}
read32() {
local _r32_var=${1:-OUTBIN} _r32_lw _r32_hw
read16 _r32_hw && read16 _r32_lw
printf -v $_r32_var %d $(( _r32_hw<<16| _r32_lw ))
}
power=/proc/device-tree/chosen/power
if [ -f $power/max_current ]; then
read32 value < $power/max_current
echo
echo "Max power supply current:" ${color_value}$(bc <<< "scale=1; $value / 1000")A${color_reset}
fi
if [ -f $power/usb_max_current_enable ]; then
read32 value < $power/usb_max_current_enable
echo "USB max current enabled: " ${color_value}$(test $value -ne 0 && echo yes || echo no)${color_reset}
fi
if [ -f $power/usb_over_current_detected ]; then
read32 value < $power/usb_over_current_detected
echo "USB overcurrent detected:" $(test $value -ne 0 && echo $yes || echo $no)
fi

BIN
sources/WiringPi-3.6.zip Normal file

Binary file not shown.

BIN
sources/ustreamer-6.12.zip Normal file

Binary file not shown.

112
uninstall-pikvm.sh Normal file
View File

@ -0,0 +1,112 @@
#!/bin/bash
# Script written by @srepac as requested by @Mark Jim
# This script perform uninstall of pikvm from raspbian. This performs the following:
#
# 1. Stop/disable kvmd services
# 2. Remove the main kvmd package files based on what was installed (see /var/cache/kvmd/installed_ver.txt file)
#
# CAVEATS:
# 1. Script does not remove /usr/bin/ttyd (webterm), /usr/bin/ustreamer, /usr/bin/ustreamer-dump
# 2. Script does not remove /usr/bin/janus (webrtc) and all its dependent files also
# 3. Script does not remove any directories; you may end up with empty directories
###
# CHANGELOG:
# 1.0 20220218 created script
# 1.1 20220220 confirm uninstall and add -f option to perform destructive commands
# 1.2 20220225 restore original /etc/motd
# 2.0 20220225 save custom configs for possible restores during re-install later
VER=2.0
save-configs() { ### save config files inside /etc/kvmd in case user re-installs pikvm later
if [[ $f_flag -eq 1 ]]; then
printf "\n-> Saving config files\n"
# Save passwd files used by PiKVM
cp /etc/kvmd/htpasswd /etc/kvmd/htpasswd.save
cp /etc/kvmd/ipmipasswd /etc/kvmd/ipmipasswd.save
cp /etc/kvmd/vncpasswd /etc/kvmd/vncpasswd.save
# Save webUI name and overrides
cp /etc/kvmd/meta.yaml /etc/kvmd/meta.yaml.save
cp /etc/kvmd/override.yaml /etc/kvmd/override.yaml.save
cp /etc/kvmd/web.css /etc/kvmd/web.css.save
# Save Janus configs
#cp /etc/kvmd/janus/janus.cfg /etc/kvmd/janus/janus.cfg.save
# Save sudoers.d/99_kvmd
cp /etc/sudoers.d/99_kvmd /etc/sudoers.d/99_kvmd.save
cp /etc/sudoers.d/custom_commands /etc/sudoers.d/custom_commands.save
fi
}
stop-disable-kvmd() {
#for i in $( systemctl | grep kvmd | grep -v var | awk '{print $1}')
for i in $( systemctl | grep kvmd | grep -v var | awk '$1||$2 ~ /kvmd/ {print $2, $1}' | sed 's/loaded //g' | cut -d' ' -f1 )
do
echo "-> Stopping/disabling ${i} ..."
if [[ $f_flag -eq 1 ]]; then systemctl disable --now $i; fi
done
} # end stop-disable-kvmd
# Determine what kvmd version was installed last
remove-kvmd-package() {
printf "\nProceeding to remove kvmd package files\n" | tee -a $LOGFILE
#KVMDVER=$( egrep 'kvmd-[0-9]' $INSTLOG | awk '{print $4}' | cut -d'-' -f2 | tail -1 )
KVMDVER=$( pikvm-info | grep kvmd-platform | awk '{print $1}' )
KVMDPKG="kvmd-${KVMDVER}"
echo "Uninstalling ${KVMDPKG} from this system."
for file in $( tar tvfJ /var/cache/kvmd/${KVMDPKG}* | awk '{print $NF}' | grep -v '/$' )
do
echo "-> Deleting /$file ..."
if [[ $f_flag -eq 1 ]]; then rm /$file; fi
done
} # end remove-kvmd-package
restore-motd() {
if [[ $f_flag -eq 1 ]]; then
if [ -e /etc/motd.orig ]; then cp -f /etc/motd.orig /etc/motd; fi
fi
cat /etc/motd
} # end restore-motd
are-you-sure() {
invalidinput=1
while [ $invalidinput -eq 1 ]; do
read -p "Uninstall PiKVM from this system. Are you sure? [y/n] " SURE
case $SURE in
Y|y) invalidinput=0 ;;
N|n) echo "Exiting."; exit 0 ;;
*) echo "Invalid input. try again."; invalidinput=1 ;;
esac
done
} # end are-you-sure fn
### MAIN STARTS HERE ###
if [ -e /usr/local/bin/rw ]; then rw; fi
mkdir -p /var/cache/kvmd # create directory in case it hasn't been created yet (e.g. installer hasn't been run)
export INSTLOG="/var/cache/kvmd/installed_ver.txt"
export LOGFILE="/var/cache/kvmd/uninstall.log"; rm -f $LOGFILE
if [ ! -e $INSTLOG ]; then
echo "Install log missing. Nothing to do." | tee -a $LOGFILE
exit 1
fi
if [[ "$1" == "-f" ]]; then
printf "\n*** Actually perform destructive commands option set.\n\n"
f_flag=1
else
printf "\n*** Only SHOWING what will be performed. Re-run with -f to actually perform destructive commands.\n\n"
f_flag=0
fi
are-you-sure
save-configs | tee -a $LOGFILE
stop-disable-kvmd | tee -a $LOGFILE
restore-motd | tee -a $LOGFILE
remove-kvmd-package | tee -a $LOGFILE
if [ -e /usr/local/bin/ro ]; then ro; fi

13
web.css Normal file
View File

@ -0,0 +1,13 @@
/* Here you can customize the Web UI */
div.stream-box-mouse-dot { /* required for kvmd 3.305+ */
cursor: crosshair !important; /* crosshair instead of blue blob */
}
div.stream-box-mouse-enabled {
cursor: crosshair !important; /* crosshair instead of blue blob */
}
div.window.window-full-tab {
border: 0px !important; /* removes window-full-tab border by adding !important to override priority, the border cause jiggling again due to window-full-tab getting overrided by window-active border styling */
}
div#stream-window.window-active:fullscreen div#stream-box div#stream-fullscreen-active {
box-shadow: none; /* removes fullscreen ~2px border */
}

BIN
x86-mods.tar Normal file

Binary file not shown.