mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-11 16:50:28 +08:00
进一步移植:能够打包 exe 运行
This commit is contained in:
parent
5bf2466037
commit
45b394185a
6
.gitignore
vendored
6
.gitignore
vendored
@ -19,7 +19,7 @@
|
||||
*.pyc
|
||||
*.swp
|
||||
/venv/
|
||||
.vscode/settings.j/son
|
||||
.vscode/settings.json
|
||||
kvmd_config/
|
||||
__pycache__/
|
||||
kvmd_data/run/kvmd/*
|
||||
@ -30,3 +30,7 @@ kvmd-launcher.dist
|
||||
kvmd-launcher.onefile-build
|
||||
ustreamer/
|
||||
node_modules/
|
||||
build/
|
||||
*/dist/*
|
||||
*/build/*
|
||||
*.spec
|
||||
366
Makefile
366
Makefile
@ -1,366 +0,0 @@
|
||||
-include config.mk
|
||||
|
||||
TESTENV_IMAGE ?= kvmd-testenv
|
||||
TESTENV_HID ?= /dev/ttyS10
|
||||
TESTENV_VIDEO ?= /dev/video0
|
||||
TESTENV_GPIO ?= /dev/gpiochip0
|
||||
TESTENV_RELAY ?= $(if $(shell ls /dev/hidraw0 2>/dev/null || true),/dev/hidraw0,)
|
||||
|
||||
LIBGPIOD_VERSION ?= 1.6.3
|
||||
|
||||
USTREAMER_MIN_VERSION ?= $(shell grep -o 'ustreamer>=[^"]\+' PKGBUILD | sed 's/ustreamer>=//g')
|
||||
|
||||
DEFAULT_PLATFORM ?= v2-hdmiusb-rpi4
|
||||
|
||||
DOCKER ?= docker
|
||||
|
||||
|
||||
# =====
|
||||
define optbool
|
||||
$(filter $(shell echo $(1) | tr A-Z a-z),yes on 1)
|
||||
endef
|
||||
|
||||
|
||||
# =====
|
||||
all:
|
||||
@ echo "Useful commands:"
|
||||
@ echo " make # Print this help"
|
||||
@ echo " make testenv # Build test environment"
|
||||
@ echo " make tox # Run tests and linters"
|
||||
@ echo " make tox E=pytest # Run selected test environment"
|
||||
@ echo " make gpio # Create gpio mockup"
|
||||
@ echo " make run # Run kvmd"
|
||||
@ echo " make run CMD=... # Run specified command inside kvmd environment"
|
||||
@ echo " make run-cfg # Run kvmd -m"
|
||||
@ echo " make run-ipmi # Run kvmd-ipmi"
|
||||
@ echo " make run-ipmi CMD=... # Run specified command inside kvmd-ipmi environment"
|
||||
@ echo " make run-vnc # Run kvmd-vnc"
|
||||
@ echo " make run-vnc CMD=... # Run specified command inside kvmd-vnc environment"
|
||||
@ echo " make regen # Regen some sources like keymap"
|
||||
@ echo " make bump # Bump minor version"
|
||||
@ echo " make bump V=major # Bump major version"
|
||||
@ echo " make release # Publish the new release (include bump minor)"
|
||||
@ echo " make clean # Remove garbage"
|
||||
@ echo " make clean-all # Remove garbage and test results"
|
||||
@ echo
|
||||
@ echo "Also you can add option NC=1 to rebuild docker test environment"
|
||||
|
||||
|
||||
testenv:
|
||||
$(DOCKER) build \
|
||||
$(if $(call optbool,$(NC)),--no-cache,) \
|
||||
--rm \
|
||||
--tag $(TESTENV_IMAGE) \
|
||||
--build-arg LIBGPIOD_VERSION=$(LIBGPIOD_VERSION) \
|
||||
--build-arg USTREAMER_MIN_VERSION=$(USTREAMER_MIN_VERSION) \
|
||||
-f testenv/Dockerfile .
|
||||
test -d testenv/.ssl || $(DOCKER) run --rm \
|
||||
--volume `pwd`:/src:ro \
|
||||
--volume `pwd`/testenv:/src/testenv:rw \
|
||||
-t $(TESTENV_IMAGE) bash -c " \
|
||||
groupadd kvmd-nginx \
|
||||
&& groupadd kvmd-vnc \
|
||||
&& /src/scripts/kvmd-gencert --do-the-thing \
|
||||
&& /src/scripts/kvmd-gencert --do-the-thing --vnc \
|
||||
&& chown -R root:root /etc/kvmd/{nginx,vnc}/ssl \
|
||||
&& chmod 664 /etc/kvmd/{nginx,vnc}/ssl/* \
|
||||
&& chmod 775 /etc/kvmd/{nginx,vnc}/ssl \
|
||||
&& mkdir /src/testenv/.ssl \
|
||||
&& mv /etc/kvmd/nginx/ssl /src/testenv/.ssl/nginx \
|
||||
&& mv /etc/kvmd/vnc/ssl /src/testenv/.ssl/vnc \
|
||||
"
|
||||
|
||||
|
||||
tox: testenv
|
||||
time $(DOCKER) run --rm \
|
||||
--volume `pwd`:/src:ro \
|
||||
--volume `pwd`/testenv:/src/testenv:rw \
|
||||
--volume `pwd`/testenv/tests:/src/testenv/tests:ro \
|
||||
--volume `pwd`/extras:/usr/share/kvmd/extras:ro \
|
||||
--volume `pwd`/configs:/usr/share/kvmd/configs.default:ro \
|
||||
--volume `pwd`/contrib/keymaps:/usr/share/kvmd/keymaps:ro \
|
||||
-t $(TESTENV_IMAGE) bash -c " \
|
||||
cp -a /src/testenv/.ssl/nginx /etc/kvmd/nginx/ssl \
|
||||
&& cp -a /src/testenv/.ssl/vnc /etc/kvmd/vnc/ssl \
|
||||
&& cp /src/testenv/platform /usr/share/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.yaml /etc/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/*passwd /etc/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.secret /etc/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/main.yaml /etc/kvmd/main.yaml \
|
||||
&& mkdir -p /etc/kvmd/override.d \
|
||||
&& cp /src/testenv/$(if $(P),$(P),$(DEFAULT_PLATFORM)).override.yaml /etc/kvmd/override.yaml \
|
||||
&& cd /src \
|
||||
&& $(if $(CMD),$(CMD),tox -q -c testenv/tox.ini $(if $(E),-e $(E),-p auto)) \
|
||||
"
|
||||
|
||||
|
||||
$(TESTENV_GPIO):
|
||||
test ! -e $(TESTENV_GPIO)
|
||||
sudo modprobe gpio-mockup gpio_mockup_ranges=0,40
|
||||
test -c $(TESTENV_GPIO)
|
||||
|
||||
|
||||
run: testenv $(TESTENV_GPIO)
|
||||
- $(DOCKER) run --rm --name kvmd \
|
||||
--privileged \
|
||||
--volume `pwd`/testenv/run:/run/kvmd:rw \
|
||||
--volume `pwd`/testenv:/testenv:ro \
|
||||
--volume `pwd`/kvmd:/kvmd:ro \
|
||||
--volume `pwd`/testenv/env.py:/kvmd/env.py:ro \
|
||||
--volume `pwd`/web:/usr/share/kvmd/web:ro \
|
||||
--volume `pwd`/extras:/usr/share/kvmd/extras:ro \
|
||||
--volume `pwd`/configs:/usr/share/kvmd/configs.default:ro \
|
||||
--volume `pwd`/contrib/keymaps:/usr/share/kvmd/keymaps:ro \
|
||||
--device $(TESTENV_VIDEO):$(TESTENV_VIDEO) \
|
||||
--device $(TESTENV_GPIO):$(TESTENV_GPIO) \
|
||||
$(if $(TESTENV_RELAY),--device $(TESTENV_RELAY):$(TESTENV_RELAY),) \
|
||||
--publish 8080:8080/tcp \
|
||||
--publish 4430:4430/tcp \
|
||||
-it $(TESTENV_IMAGE) /bin/bash -c " \
|
||||
mkdir -p /tmp/kvmd-nginx \
|
||||
&& mount -t debugfs none /sys/kernel/debug \
|
||||
&& test -d /sys/kernel/debug/gpio-mockup/`basename $(TESTENV_GPIO)`/ || (echo \"Missing GPIO mockup\" && exit 1) \
|
||||
&& (socat PTY,link=$(TESTENV_HID) PTY,link=/dev/ttyS11 &) \
|
||||
&& cp -r /usr/share/kvmd/configs.default/nginx/* /etc/kvmd/nginx \
|
||||
&& cp -a /testenv/.ssl/nginx /etc/kvmd/nginx/ssl \
|
||||
&& cp -a /testenv/.ssl/vnc /etc/kvmd/vnc/ssl \
|
||||
&& cp /testenv/platform /usr/share/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.yaml /etc/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/*passwd /etc/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.secret /etc/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/main/$(if $(P),$(P),$(DEFAULT_PLATFORM)).yaml /etc/kvmd/main.yaml \
|
||||
&& ln -s /testenv/web.css /etc/kvmd/web.css \
|
||||
&& mkdir -p /etc/kvmd/override.d \
|
||||
&& cp /testenv/$(if $(P),$(P),$(DEFAULT_PLATFORM)).override.yaml /etc/kvmd/override.yaml \
|
||||
&& python -m kvmd.apps.ngxmkconf /etc/kvmd/nginx/nginx.conf.mako /etc/kvmd/nginx/nginx.conf \
|
||||
&& nginx -c /etc/kvmd/nginx/nginx.conf -g 'user http; error_log stderr;' \
|
||||
&& ln -s $(TESTENV_VIDEO) /dev/kvmd-video \
|
||||
&& ln -s $(TESTENV_GPIO) /dev/kvmd-gpio \
|
||||
&& $(if $(CMD),$(CMD),python -m kvmd.apps.kvmd --run) \
|
||||
"
|
||||
|
||||
|
||||
run-cfg: testenv
|
||||
- $(DOCKER) run --rm --name kvmd-cfg \
|
||||
--volume `pwd`/testenv/run:/run/kvmd:rw \
|
||||
--volume `pwd`/testenv:/testenv:ro \
|
||||
--volume `pwd`/kvmd:/kvmd:ro \
|
||||
--volume `pwd`/extras:/usr/share/kvmd/extras:ro \
|
||||
--volume `pwd`/configs:/usr/share/kvmd/configs.default:ro \
|
||||
--volume `pwd`/contrib/keymaps:/usr/share/kvmd/keymaps:ro \
|
||||
-it $(TESTENV_IMAGE) /bin/bash -c " \
|
||||
cp -a /testenv/.ssl/nginx /etc/kvmd/nginx/ssl \
|
||||
&& cp -a /testenv/.ssl/vnc /etc/kvmd/vnc/ssl \
|
||||
&& cp /testenv/platform /usr/share/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.yaml /etc/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/*passwd /etc/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.secret /etc/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/main.yaml /etc/kvmd/main.yaml \
|
||||
&& mkdir -p /etc/kvmd/override.d \
|
||||
&& cp /testenv/$(if $(P),$(P),$(DEFAULT_PLATFORM)).override.yaml /etc/kvmd/override.yaml \
|
||||
&& $(if $(CMD),$(CMD),python -m kvmd.apps.kvmd -m) \
|
||||
"
|
||||
|
||||
|
||||
run-ipmi: testenv
|
||||
- $(DOCKER) run --rm --name kvmd-ipmi \
|
||||
--volume `pwd`/testenv/run:/run/kvmd:rw \
|
||||
--volume `pwd`/testenv:/testenv:ro \
|
||||
--volume `pwd`/kvmd:/kvmd:ro \
|
||||
--volume `pwd`/extras:/usr/share/kvmd/extras:ro \
|
||||
--volume `pwd`/configs:/usr/share/kvmd/configs.default:ro \
|
||||
--volume `pwd`/contrib/keymaps:/usr/share/kvmd/keymaps:ro \
|
||||
--publish 6230:623/udp \
|
||||
-it $(TESTENV_IMAGE) /bin/bash -c " \
|
||||
cp -a /testenv/.ssl/nginx /etc/kvmd/nginx/ssl \
|
||||
&& cp -a /testenv/.ssl/vnc /etc/kvmd/vnc/ssl \
|
||||
&& cp /testenv/platform /usr/share/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.yaml /etc/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/*passwd /etc/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.secret /etc/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/main.yaml /etc/kvmd/main.yaml \
|
||||
&& mkdir -p /etc/kvmd/override.d \
|
||||
&& cp /testenv/$(if $(P),$(P),$(DEFAULT_PLATFORM)).override.yaml /etc/kvmd/override.yaml \
|
||||
&& $(if $(CMD),$(CMD),python -m kvmd.apps.ipmi --run) \
|
||||
"
|
||||
|
||||
|
||||
run-vnc: testenv
|
||||
- $(DOCKER) run --rm --name kvmd-vnc \
|
||||
--volume `pwd`/testenv/run:/run/kvmd:rw \
|
||||
--volume `pwd`/testenv:/testenv:ro \
|
||||
--volume `pwd`/kvmd:/kvmd:ro \
|
||||
--volume `pwd`/extras:/usr/share/kvmd/extras:ro \
|
||||
--volume `pwd`/configs:/usr/share/kvmd/configs.default:ro \
|
||||
--volume `pwd`/contrib/keymaps:/usr/share/kvmd/keymaps:ro \
|
||||
--publish 5900:5900/tcp \
|
||||
-it $(TESTENV_IMAGE) /bin/bash -c " \
|
||||
cp -a /testenv/.ssl/nginx /etc/kvmd/nginx/ssl \
|
||||
&& cp -a /testenv/.ssl/vnc /etc/kvmd/vnc/ssl \
|
||||
&& cp /testenv/platform /usr/share/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.yaml /etc/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/*passwd /etc/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.secret /etc/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/main.yaml /etc/kvmd/main.yaml \
|
||||
&& mkdir -p /etc/kvmd/override.d \
|
||||
&& cp /testenv/$(if $(P),$(P),$(DEFAULT_PLATFORM)).override.yaml /etc/kvmd/override.yaml \
|
||||
&& $(if $(CMD),$(CMD),python -m kvmd.apps.vnc --run) \
|
||||
"
|
||||
|
||||
|
||||
regen: keymap pug
|
||||
|
||||
|
||||
keymap: testenv
|
||||
$(DOCKER) run --user `id -u`:`id -g` --rm \
|
||||
--volume `pwd`:/src \
|
||||
-it $(TESTENV_IMAGE) bash -c "cd src \
|
||||
&& ./genmap.py keymap.csv kvmd/keyboard/mappings.py.mako kvmd/keyboard/mappings.py \
|
||||
&& ./genmap.py keymap.csv hid/arduino/lib/drivers/usb-keymap.h.mako hid/arduino/lib/drivers/usb-keymap.h \
|
||||
&& ./genmap.py keymap.csv hid/arduino/lib/drivers-avr/ps2/keymap.h.mako hid/arduino/lib/drivers-avr/ps2/keymap.h \
|
||||
&& ./genmap.py keymap.csv hid/pico/src/ph_usb_keymap.h.mako hid/pico/src/ph_usb_keymap.h \
|
||||
"
|
||||
|
||||
|
||||
pug: testenv
|
||||
$(DOCKER) run --user `id -u`:`id -g` --rm \
|
||||
--volume `pwd`:/src \
|
||||
-it $(TESTENV_IMAGE) bash -c "cd src \
|
||||
&& pug --pretty web/index.pug -o web \
|
||||
&& pug --pretty web/login/index.pug -o web/login \
|
||||
&& pug --pretty web/kvm/index.pug -o web/kvm \
|
||||
&& pug --pretty web/ipmi/index.pug -o web/ipmi \
|
||||
&& pug --pretty web/vnc/index.pug -o web/vnc \
|
||||
"
|
||||
|
||||
|
||||
release:
|
||||
make clean
|
||||
make tox
|
||||
make clean
|
||||
make push
|
||||
make bump V=$(V)
|
||||
make push
|
||||
make clean
|
||||
|
||||
|
||||
bump:
|
||||
bumpversion $(if $(V),$(V),minor)
|
||||
|
||||
|
||||
push:
|
||||
git push
|
||||
git push --tags
|
||||
|
||||
|
||||
clean:
|
||||
rm -rf testenv/run/*.{pid,sock} build site dist pkg src v*.tar.gz *.pkg.tar.{xz,zst} *.egg-info kvmd-*.tar.gz
|
||||
find kvmd testenv/tests -name __pycache__ | xargs rm -rf
|
||||
make -C hid/arduino clean
|
||||
make -C hid/pico clean
|
||||
|
||||
|
||||
clean-all: testenv clean
|
||||
make -C hid/arduino clean-all
|
||||
make -C hid/pico clean-all
|
||||
- $(DOCKER) run --rm \
|
||||
--volume `pwd`:/src \
|
||||
-it $(TESTENV_IMAGE) bash -c "cd src && rm -rf testenv/{.ssl,.tox,.mypy_cache,.coverage}"
|
||||
|
||||
|
||||
.PHONY: testenv
|
||||
|
||||
run-stage-0:
|
||||
$(DOCKER) buildx build -t registry.cn-hangzhou.aliyuncs.com/silentwind/kvmd-stage-0 \
|
||||
--allow security.insecure --progress plain \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
-f build/Dockerfile-stage-0 . \
|
||||
--push
|
||||
$(DOCKER) buildx build -t silentwind0/kvmd-stage-0 \
|
||||
--allow security.insecure --progress plain \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
-f build/Dockerfile-stage-0 . \
|
||||
--push
|
||||
|
||||
run-build-dev:
|
||||
$(DOCKER) buildx build -t registry.cn-hangzhou.aliyuncs.com/silentwind/kvmd:dev \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
--build-arg CACHEBUST=$(date +%s) --allow security.insecure \
|
||||
-f build/Dockerfile . \
|
||||
--push
|
||||
$(DOCKER) buildx build -t silentwind0/kvmd:dev \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
--build-arg CACHEBUST=$(date +%s) --allow security.insecure \
|
||||
-f build/Dockerfile . \
|
||||
--push
|
||||
|
||||
run-build-new:
|
||||
$(DOCKER) buildx build -t registry.cn-hangzhou.aliyuncs.com/silentwind/kvmd:dev \
|
||||
--platform linux/amd64 \
|
||||
--build-arg CACHEBUST=$(date +%s) \
|
||||
-f build/Dockerfile . \
|
||||
--load
|
||||
|
||||
run-build-release:
|
||||
$(DOCKER) buildx build -t registry.cn-hangzhou.aliyuncs.com/silentwind/kvmd \
|
||||
--progress plain \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
--build-arg CACHEBUST=$(date +%s) \
|
||||
-f build/Dockerfile . \
|
||||
--push
|
||||
$(DOCKER) buildx build -t silentwind0/kvmd \
|
||||
--progress plain \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
--build-arg CACHEBUST=$(date +%s) \
|
||||
-f build/Dockerfile . \
|
||||
--push
|
||||
|
||||
run-nogpio: testenv
|
||||
- $(DOCKER) run --rm --name kvmd \
|
||||
--privileged \
|
||||
--volume `pwd`/testenv/run:/run/kvmd:rw \
|
||||
--volume `pwd`/testenv:/testenv:ro \
|
||||
--volume `pwd`/kvmd:/kvmd:ro \
|
||||
--volume `pwd`/testenv/env.py:/kvmd/env.py:ro \
|
||||
--volume `pwd`/web:/usr/share/kvmd/web:ro \
|
||||
--volume `pwd`/extras:/usr/share/kvmd/extras:ro \
|
||||
--volume `pwd`/configs:/usr/share/kvmd/configs.default:ro \
|
||||
--volume `pwd`/contrib/keymaps:/usr/share/kvmd/keymaps:ro \
|
||||
--device $(TESTENV_VIDEO):$(TESTENV_VIDEO) \
|
||||
$(if $(TESTENV_RELAY),--device $(TESTENV_RELAY):$(TESTENV_RELAY),) \
|
||||
--publish 8080:8080/tcp \
|
||||
--publish 4430:4430/tcp \
|
||||
-it $(TESTENV_IMAGE) /bin/bash -c " \
|
||||
mkdir -p /tmp/kvmd-nginx \
|
||||
&& mount -t debugfs none /sys/kernel/debug \
|
||||
&& (socat PTY,link=$(TESTENV_HID) PTY,link=/dev/ttyS11 &) \
|
||||
&& cp -r /usr/share/kvmd/configs.default/nginx/* /etc/kvmd/nginx \
|
||||
&& cp -a /testenv/.ssl/nginx /etc/kvmd/nginx/ssl \
|
||||
&& cp -a /testenv/.ssl/vnc /etc/kvmd/vnc/ssl \
|
||||
&& touch /etc/kvmd/.docker_flag \
|
||||
&& cp /testenv/platform /usr/share/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.yaml /etc/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/*passwd /etc/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.secret /etc/kvmd \
|
||||
&& cp /usr/share/kvmd/configs.default/kvmd/main.yaml /etc/kvmd/main.yaml \
|
||||
&& ln -s /testenv/web.css /etc/kvmd/web.css \
|
||||
&& mkdir -p /etc/kvmd/override.d \
|
||||
&& cp /testenv/$(if $(P),$(P),$(DEFAULT_PLATFORM)).override.yaml /etc/kvmd/override.yaml \
|
||||
&& python -m kvmd.apps.ngxmkconf /etc/kvmd/nginx/nginx.conf.mako /etc/kvmd/nginx/nginx.conf \
|
||||
&& nginx -c /etc/kvmd/nginx/nginx.conf -g 'user http; error_log stderr;' \
|
||||
&& $(if $(CMD),$(CMD),python -m kvmd.apps.kvmd --run) \
|
||||
"
|
||||
nuitka:
|
||||
python3.11 -m nuitka kvmd-launcher.py --standalone --onefile --no-deployment-flag=self-execution --include-module=\
|
||||
kvmd.plugins.auth.htpasswd,kvmd.plugins.auth.http,kvmd.plugins.auth.ldap,\
|
||||
kvmd.plugins.auth.pam,kvmd.plugins.auth.radius,\
|
||||
kvmd.plugins.hid.ch9329,kvmd.plugins.hid.bt,kvmd.plugins.hid.otg,\
|
||||
kvmd.plugins.atx.disabled,kvmd.plugins.atx.gpio,\
|
||||
kvmd.plugins.msd.disabled,kvmd.plugins.msd.otg,\
|
||||
kvmd.plugins.ugpio.gpio,kvmd.plugins.ugpio.wol,kvmd.plugins.ugpio.cmd,\
|
||||
kvmd.plugins.ugpio.ipmi,kvmd.plugins.ugpio.anelpwr,kvmd.plugins.ugpio.cmdret,\
|
||||
kvmd.plugins.ugpio.extron,kvmd.plugins.ugpio.ezcoo,kvmd.plugins.ugpio.hidrelay,\
|
||||
kvmd.plugins.ugpio.hue,kvmd.plugins.ugpio.locator,kvmd.plugins.ugpio.noyito,\
|
||||
kvmd.plugins.ugpio.otgconf,kvmd.plugins.ugpio.pway,kvmd.plugins.ugpio.pwm,\
|
||||
kvmd.plugins.ugpio.servo,kvmd.plugins.ugpio.tesmart,kvmd.plugins.ugpio.xh_hk4401,\
|
||||
passlib.handlers.sha1_crypt,pygments.formatters.terminal
|
||||
@ -1,11 +1,68 @@
|
||||
import multiprocessing
|
||||
import os
|
||||
import sys
|
||||
from kvmd.apps.kvmd import main as kvmd_main
|
||||
|
||||
import fileinput
|
||||
|
||||
# 文件路径
|
||||
file_path = '_internal/kvmd_data/etc/kvmd/kvmd_data/etc/kvmd/override.yaml'
|
||||
|
||||
# 使用fileinput.input进行原地编辑
|
||||
|
||||
|
||||
def resource_path(relative_path):
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
base_path = sys._MEIPASS
|
||||
else:
|
||||
base_path = os.path.abspath(".")
|
||||
return os.path.join(base_path, relative_path)
|
||||
|
||||
def replace_streamer_command(override_config_path):
|
||||
lines_to_replace = [
|
||||
" - \"C:/Users/mofen/miniconda3/python.exe\"\n",
|
||||
" - \"ustreamer-win/ustreamer-win.py\"\n"
|
||||
]
|
||||
new_line = " - \"ustreamer-win.exe\"\n"
|
||||
|
||||
with open(override_config_path, 'r', encoding='utf-8') as file:
|
||||
lines = file.readlines()
|
||||
|
||||
with open(override_config_path, 'w', encoding='utf-8') as file:
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
if lines[i] in lines_to_replace:
|
||||
if i + 1 < len(lines) and lines[i + 1] == lines_to_replace[1]:
|
||||
file.write(new_line)
|
||||
i += 2
|
||||
continue
|
||||
file.write(lines[i])
|
||||
i += 1
|
||||
|
||||
def start():
|
||||
main_config_path = resource_path('kvmd_data/etc/kvmd/main.yaml')
|
||||
override_config_path = resource_path('kvmd_data/etc/kvmd/override.yaml')
|
||||
flag_path = resource_path('kvmd_data/run_flag')
|
||||
|
||||
|
||||
|
||||
if not os.path.exists(flag_path):
|
||||
with fileinput.input(override_config_path, inplace=True) as file:
|
||||
for line in file:
|
||||
updated_line = line.replace('kvmd_data/', '_internal/kvmd_data/')
|
||||
print(updated_line, end='')
|
||||
with open(flag_path, 'w') as flag_file:
|
||||
flag_file.write("1")
|
||||
|
||||
replace_streamer_command(override_config_path)
|
||||
|
||||
custom_argv = [
|
||||
'kvmd',
|
||||
'-c', 'kvmd_data/etc/kvmd/main.yaml',
|
||||
'-c',main_config_path,
|
||||
'--run'
|
||||
]
|
||||
kvmd_main(argv=custom_argv)
|
||||
|
||||
if __name__ == '__main__':
|
||||
multiprocessing.freeze_support()
|
||||
start()
|
||||
@ -53,7 +53,7 @@ class ExtrasInfoSubmanager(BaseInfoSubmanager):
|
||||
await sui.open()
|
||||
except Exception as ex:
|
||||
if not os.path.exists("/etc/kvmd/.docker_flag") or not sys.platform.startswith('linux'):
|
||||
get_logger(0).error("Can't open systemd bus to get extras state: %s", tools.efmt(ex))
|
||||
get_logger(0).error("Can't open systemd bus to get extras state.")
|
||||
sui = None
|
||||
try:
|
||||
extras: dict[str, dict] = {}
|
||||
|
||||
@ -83,9 +83,9 @@ class HwInfoSubmanager(BaseInfoSubmanager):
|
||||
)
|
||||
return {
|
||||
"platform": {
|
||||
"type": "rpi",
|
||||
"type": "windows",
|
||||
"base": base,
|
||||
"serial": serial,
|
||||
"serial": "windows1000000000",
|
||||
**platform, # type: ignore
|
||||
},
|
||||
"health": {
|
||||
@ -124,7 +124,7 @@ class HwInfoSubmanager(BaseInfoSubmanager):
|
||||
self.__dt_cache[name] = (await aiotools.read_file(path)).strip(" \t\r\n\0")
|
||||
except Exception as err:
|
||||
#get_logger(0).warn("Can't read DT %s from %s: %s", name, path, err)
|
||||
return None
|
||||
return "windows"
|
||||
return self.__dt_cache[name]
|
||||
|
||||
async def __read_platform_file(self) -> dict:
|
||||
@ -142,8 +142,8 @@ class HwInfoSubmanager(BaseInfoSubmanager):
|
||||
"board": parsed["PIKVM_BOARD"],
|
||||
}
|
||||
except Exception:
|
||||
get_logger(0).exception("Can't read device model")
|
||||
return {"model": None, "video": None, "board": None}
|
||||
#get_logger(0).exception("Can't read device model")
|
||||
return {"model": "V2", "video": "USB_VIDEO", "board": "Windows"}
|
||||
|
||||
async def __get_cpu_temp(self) -> (float | None):
|
||||
temp_path = f"{env.SYSFS_PREFIX}/sys/class/thermal/thermal_zone0/temp"
|
||||
@ -155,21 +155,9 @@ class HwInfoSubmanager(BaseInfoSubmanager):
|
||||
|
||||
async def __get_cpu_percent(self) -> (float | None):
|
||||
try:
|
||||
st = psutil.cpu_times_percent()
|
||||
user = st.user - st.guest
|
||||
nice = st.nice - st.guest_nice
|
||||
idle_all = st.idle + st.iowait
|
||||
system_all = st.system + st.irq + st.softirq
|
||||
virtual = st.guest + st.guest_nice
|
||||
total = max(1, user + nice + system_all + idle_all + st.steal + virtual)
|
||||
return int(
|
||||
st.nice / total * 100
|
||||
+ st.user / total * 100
|
||||
+ system_all / total * 100
|
||||
+ (st.steal + st.guest) / total * 100
|
||||
)
|
||||
return int(psutil.cpu_percent(interval=1))
|
||||
except Exception as ex:
|
||||
#get_logger(0).error("Can't get CPU percent: %s", ex)
|
||||
get_logger(0).error("Can't get CPU percent: %s", ex)
|
||||
return None
|
||||
|
||||
async def __get_mem(self) -> dict:
|
||||
|
||||
@ -418,7 +418,7 @@ class Streamer: # pylint: disable=too-many-instance-attributes
|
||||
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")
|
||||
logger.exception("Streamer unexpectedly died")
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception:
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
# 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]
|
||||
override: !include [override.yaml]
|
||||
|
||||
logging: !include logging.yaml
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
# will be displayed in the web interface.
|
||||
|
||||
server:
|
||||
host: docker
|
||||
host: windows
|
||||
|
||||
kvm: {
|
||||
base_on: PiKVM,
|
||||
|
||||
@ -40,12 +40,11 @@ kvmd:
|
||||
keymap: kvmd_data/usr/share/kvmd/keymaps/en-us
|
||||
|
||||
msd:
|
||||
#type: otg
|
||||
remount_cmd: /bin/true
|
||||
msd_path: /var/lib/kvmd/msd
|
||||
normalfiles_path: NormalFiles
|
||||
normalfiles_size: 256
|
||||
type: disabled
|
||||
|
||||
log_reader:
|
||||
enabled: false
|
||||
|
||||
ocr:
|
||||
langs:
|
||||
- eng
|
||||
@ -53,7 +52,7 @@ kvmd:
|
||||
|
||||
streamer:
|
||||
resolution:
|
||||
default: 1280x720
|
||||
default: 1920x1080
|
||||
|
||||
forever: true
|
||||
|
||||
|
||||
Binary file not shown.
@ -1,3 +1,3 @@
|
||||
PIKVM_MODEL=docker_model
|
||||
PIKVM_VIDEO=docker_video
|
||||
PIKVM_BOARD=docker_board
|
||||
PIKVM_MODEL=windows_model
|
||||
PIKVM_VIDEO=windows_video
|
||||
PIKVM_BOARD=windows_board
|
||||
|
||||
@ -899,7 +899,7 @@
|
||||
</div>
|
||||
<div id="stream-info"></div>
|
||||
<button class="window-button-exit-full-tab">▼</button>
|
||||
<div class="stream-box-online" id="stream-box"><img id="stream-image" src="/share/png/blank-stream.png">
|
||||
<div class="stream-box-offline" id="stream-box"><img id="stream-image" src="/share/png/blank-stream.png">
|
||||
<video class="hidden" id="stream-video" disablePictureInPicture="true" autoplay playsinline muted></video>
|
||||
<div id="stream-fullscreen-active"></div>
|
||||
</div>
|
||||
|
||||
@ -13,7 +13,7 @@ div(id="stream-window" class="window window-resizable")
|
||||
div(id="stream-info")
|
||||
|
||||
button(class="window-button-exit-full-tab") ▼
|
||||
div(id="stream-box" class="stream-box-online")
|
||||
div(id="stream-box" class="stream-box-offline")
|
||||
img(id="stream-image" src=`${png_dir}/blank-stream.png`)
|
||||
video(id="stream-video" class="hidden" disablePictureInPicture="true" autoplay playsinline muted)
|
||||
div(id="stream-fullscreen-active")
|
||||
|
||||
@ -127,8 +127,9 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) {
|
||||
|
||||
var __checkStream = function() {
|
||||
__findId();
|
||||
|
||||
if (__id.legnth > 0 && __id in __state.stream.clients_stat) {
|
||||
console.log("__state.stream.clients_stat",__state.stream.clients_stat)
|
||||
console.log("__id",__id)
|
||||
if (__id.length > 0 && __id in __state.stream.clients_stat) {
|
||||
__setStreamActive();
|
||||
__stopChecking();
|
||||
|
||||
|
||||
317
quick_start.sh
317
quick_start.sh
@ -1,317 +0,0 @@
|
||||
#!/bin/bash
|
||||
#Install Latest Stable One-KVM Dcoker Release
|
||||
|
||||
DOCKER_IMAGE_PATH="registry.cn-hangzhou.aliyuncs.com/silentwind/kvmd"
|
||||
DOCKER_PORT="-p 8080:8080 -p 4430:4430 -p 5900:5900 -p 623:623"
|
||||
DOCKER_NAME="kvmd"
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
function check_os_architecture(){
|
||||
osCheck=$(uname -a)
|
||||
if [[ $osCheck =~ 'x86_64' ]];then
|
||||
architecture="amd64"
|
||||
elif [[ $osCheck =~ 'arm64' ]] || [[ $osCheck =~ 'aarch64' ]];then
|
||||
architecture="arm64"
|
||||
elif [[ $osCheck =~ 'armv7l' ]];then
|
||||
architecture="armv7l"
|
||||
else
|
||||
echo "暂不支持的系统架构,请参阅官方文档,选择受支持的系统。\n退出程序"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function check_docker_exists() {
|
||||
if command -v docker &> /dev/null; then
|
||||
echo "$(docker -v)"
|
||||
else
|
||||
echo "Docker 未安装,退出程序"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function check_sudo_exists() {
|
||||
if command -v sudo > /dev/null 2>&1; then
|
||||
sudo_command="sudo"
|
||||
else
|
||||
sudo_command=""
|
||||
fi
|
||||
}
|
||||
|
||||
function delete_kvmd_container(){
|
||||
if docker ps -a --format '{{.Names}}' | grep -q '^kvmd$'; then
|
||||
$sudo_command docker stop $DOCKER_NAME
|
||||
$sudo_command docker rm $DOCKER_NAME
|
||||
fi
|
||||
}
|
||||
|
||||
function check_otg_device(){
|
||||
$sudo_command modprobe libcomposite > /dev/null|| echo -e "${YELLOW}libcomposite 内核模块加载失败${NC}"
|
||||
if [[ "$architecture" != "amd64" ]] && [[ -d "/sys/class/udc" ]]; then
|
||||
if [[ "$(ls -A /sys/class/udc)" ]] || [[ "$(ls -A /sys/class/usb_role)" ]]; then
|
||||
otg_devices=$(ls -A /sys/class/udc)
|
||||
otg_status=$(cat /sys/class/usb_role/*/role 2>/dev/null | head -n 1)
|
||||
echo -e "${GREEN}当前系统支持 OTG:$otg_devices OTG 状态:$otg_status${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}当前系统不支持 OTG,退出程序${NC}"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "/sys/kernel/config" ]];then
|
||||
echo -e "${RED}当前系统不支持 configfs 文件系统,退出程序${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function check_video_device(){
|
||||
if ls /dev/video* 1> /dev/null 2>&1; then
|
||||
video_devices=($(ls /dev/video* 2>/dev/null))
|
||||
video_num_devices=${#video_devices[@]}
|
||||
echo -e ""${GREEN}找到视频设备:$(ls -A /dev/video*)${NC}""
|
||||
else
|
||||
echo -e "${RED}未找到任何视频采集设备,退出程序${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function check_repeat_install(){
|
||||
if docker ps -a --format '{{.Names}}' | grep -q '^kvmd$'; then
|
||||
echo -e "${YELLOW}检查到 kvmd 容器已存在,是否删除容器重新部署?${NC}"
|
||||
read -p "y/n: " delete_choice
|
||||
case $delete_choice in
|
||||
y|Y)
|
||||
delete_kvmd_container
|
||||
;;
|
||||
n|N)
|
||||
echo -e "${RED}退出程序${NC}"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}无效的选择,请输入 y 或者 n,退出程序${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
if [[ -d "kvmd_config" ]]; then
|
||||
echo -e "${YELLOW}检查到此前配置文件夹已存在,是否删除此前配置文件夹?${NC}"
|
||||
read -p "y/n: " delete_choice
|
||||
case $delete_choice in
|
||||
y|Y)
|
||||
$sudo_command rm -r kvmd_config
|
||||
;;
|
||||
n|N)
|
||||
echo -e ""
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}无效的选择,请输入 y 或者 n,退出程序${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
function show_main_menu() {
|
||||
echo -e "${BLUE}==============================${NC}"
|
||||
echo -e "${BLUE} One-KVM Docker 版管理 ${NC}"
|
||||
echo -e "${BLUE}==============================${NC}"
|
||||
|
||||
echo " 1. 安装 One-KVM Docker 版"
|
||||
echo ""
|
||||
echo " 2. 卸载 One-KVM Docker 版"
|
||||
echo ""
|
||||
echo " 3. 拉取 One-KVM 最新镜像"
|
||||
echo ""
|
||||
echo " 4. 更多信息"
|
||||
|
||||
echo -e "${BLUE}==============================${NC}"
|
||||
read -p "请输入数字(1-4): " choice
|
||||
while [[ "$choice" != "1" && "$choice" != "2" && "$choice" != "3" && "$choice" != "4" ]]; do
|
||||
echo -e "${RED}无效的选择,请输入1-4${NC}"
|
||||
read -p "请输入数字(1-4): " choice
|
||||
done
|
||||
case $choice in
|
||||
1)
|
||||
check_repeat_install
|
||||
get_hid_info
|
||||
get_video_info
|
||||
get_audio_info
|
||||
get_userinfo
|
||||
get_userenv
|
||||
show_install_info
|
||||
get_install_command
|
||||
execute_command
|
||||
;;
|
||||
2)
|
||||
delete_kvmd_container
|
||||
;;
|
||||
3)
|
||||
$sudo_command docker pull $DOCKER_IMAGE_PATH
|
||||
;;
|
||||
4)
|
||||
echo -e "${BLUE}作者:${NC}\t\t默风SilentWind"
|
||||
echo -e "${BLUE}文档:${NC}\t\thttps://one-kvm.mofeng.run/"
|
||||
echo -e "${BLUE}Github:${NC}\thttps://github.com/mofeng-git/One-KVM"
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}无效的选择,请输入1-4之间的数字,退出程序${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function get_hid_info() {
|
||||
if [[ "$architecture" == "amd64" ]]; then
|
||||
echo -e "${GREEN}使用的 HID 硬件类型:CH9329${NC}"
|
||||
use_hid="CH9329"
|
||||
else
|
||||
echo -e "${GREEN}请选择使用的 HID 硬件类型:${NC}"
|
||||
echo " 1. OTG"
|
||||
echo " 2. CH9329"
|
||||
read -p "请输入数字(1 或 2): " hardware_type
|
||||
while [[ "$hardware_type" != "1" && "$hardware_type" != "2" ]]; do
|
||||
echo -e "${RED}无效的选择,请输入1或2。${NC}"
|
||||
read -p "请输入数字(1 或 2): " hardware_type
|
||||
done
|
||||
if [[ "$hardware_type" == "1" ]]; then
|
||||
use_hid="OTG"
|
||||
else
|
||||
use_hid="CH9329"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$use_hid" == "CH9329" ]]; then
|
||||
if ls /dev/ttyUSB* 1> /dev/null 2>&1; then
|
||||
echo -e ""${GREEN}找到串口设备:$(ls -A /dev/ttyUSB*)${NC}""
|
||||
else
|
||||
echo -e "${RED}未找到任何 USB 串口设备,退出程序${NC}"
|
||||
exit 1
|
||||
fi
|
||||
read -p "请输入 CH9329 硬件的地址(回车使用默认值 /dev/ttyUSB0): " ch9329_address
|
||||
read -p "请输入 CH9329 硬件的波特率(回车使用默认值 9600): " ch9329_serial_rate
|
||||
ch9329_address=${ch9329_address:-/dev/ttyUSB0}
|
||||
ch9329_serial_rate=${ch9329_serial_rate:-9600}
|
||||
fi
|
||||
|
||||
if [[ "$use_hid" == "OTG" ]]; then
|
||||
check_otg_device
|
||||
fi
|
||||
}
|
||||
|
||||
function get_video_info() {
|
||||
check_video_device
|
||||
if [[ "$video_num_devices" == "3" ]]; then
|
||||
video_default_device="/dev/video1"
|
||||
echo -e "${YELLOW}经检测 /dev/video0 可能不可用,建议使用 /dev/video1${NC}"
|
||||
else
|
||||
video_default_device="/dev/video0"
|
||||
fi
|
||||
read -p "请输入视频设备路径(回车使用默认值 $video_default_device): " video_device
|
||||
if [[ -z "$video_device" ]]; then
|
||||
video_device=$video_default_device
|
||||
fi
|
||||
}
|
||||
|
||||
function get_audio_info() {
|
||||
if [[ -d "/dev/snd" ]]; then
|
||||
echo -e ""${GREEN}找到音频设备:$(ls -A /dev/snd)${NC}""
|
||||
read -p "请输入音频设备路径(回车使用默认值 hw:0): " audio_device
|
||||
if [[ -z "$audio_device" ]]; then
|
||||
audio_device="hw:0"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}未找到任何音频采集设备${NC}"
|
||||
audio_device="none"
|
||||
fi
|
||||
}
|
||||
|
||||
function get_userinfo() {
|
||||
read -p "请输入用户名(回车使用默认值 admin): " username
|
||||
read -s -p "请输入密码(回车使用默认值 admin): " password
|
||||
if [[ -z "$username" ]]; then
|
||||
username="admin"
|
||||
fi
|
||||
if [[ -z "$password" ]]; then
|
||||
password="admin"
|
||||
fi
|
||||
}
|
||||
|
||||
function get_userenv() {
|
||||
echo -e "\n"
|
||||
read -p "额外用户环境变量(回车则留空): " userenv
|
||||
}
|
||||
|
||||
function show_install_info() {
|
||||
echo -e "\n\n${BLUE}==============================${NC}"
|
||||
echo -e "${BLUE}安装信息总览:${NC}"
|
||||
if [[ "$use_hid" == "CH9329" ]]; then
|
||||
echo -e "CH9329 设备: \t${GREEN}$ch9329_address${NC} \tCH9329 波特率: \t${GREEN}$ch9329_serial_rate${NC}"
|
||||
fi
|
||||
if [[ "$use_hid" == "OTG" ]]; then
|
||||
echo -e "OTG端口:\t${GREEN}$otg_devices${NC} \tOTG 状态:\t${GREEN}$otg_status${NC}"
|
||||
fi
|
||||
echo -e "视频设备: \t${GREEN}$video_device${NC} \t音频设备: \t${GREEN}$audio_device${NC}"
|
||||
echo -e "用户名: \t${GREEN}$username${NC} \t\t密码: \t${GREEN}$password${NC}"
|
||||
}
|
||||
|
||||
function get_install_command(){
|
||||
local docker_init_command="docker run -itd --name $DOCKER_NAME"
|
||||
local append_command=""
|
||||
local append_env=""
|
||||
|
||||
if [[ "$use_hid" == "CH9329" ]]; then
|
||||
append_command="--device $video_device:/dev/video0 --device $ch9329_address:/dev/ttyUSB0 -v ./kvmd_config:/etc/kvmd"
|
||||
|
||||
if [[ -d "/dev/snd" ]]; then
|
||||
append_command="$append_command --device /dev/snd:/dev/snd -e AUDIONUM=${audio_device:3}"
|
||||
fi
|
||||
append_env="-e USERNAME=$username -e PASSWORD=$password -e CH9329SPEED=$ch9329_serial_rate"
|
||||
docker_command="$sudo_command $docker_init_command $append_command $DOCKER_PORT $append_env $userenv $DOCKER_IMAGE_PATH"
|
||||
else
|
||||
append_command="--privileged=true -v /lib/modules:/lib/modules:ro -v /dev:/dev -v /sys/kernel/config:/sys/kernel/config -v ./kvmd_config:/etc/kvmd"
|
||||
if [[ -d "/dev/snd" ]]; then
|
||||
append_command="$append_command -e AUDIONUM=${audio_device:3}"
|
||||
fi
|
||||
append_env="-e OTG=1 -e USERNAME=$username -e PASSWORD=$password -e VIDEONUM=${video_device:10} -e AUDIONUM=${audio_device:3}"
|
||||
docker_command="$sudo_command $docker_init_command $append_command $DOCKER_PORT $append_env $userenv $DOCKER_IMAGE_PATH"
|
||||
fi
|
||||
echo -e "\n${BLUE}Docker 部署命令:${NC}\n$docker_command"
|
||||
echo -e "${BLUE}==============================${NC}\n"
|
||||
}
|
||||
|
||||
function execute_command(){
|
||||
echo -e "${BLUE}One-KVM 部署中......${NC}"
|
||||
eval "$docker_command"
|
||||
local exit_status=$?
|
||||
if [[ $exit_status -eq 0 ]]; then
|
||||
echo -e "${BLUE}One-KVM 部署成功${NC}"
|
||||
$sudo_command docker update --restart=always $DOCKER_NAME
|
||||
if [[ "$use_hid" == "OTG" ]]; then
|
||||
execute_otg_command
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}One-KVM 部署失败,退出状态码为 $exit_status${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
function execute_otg_command(){
|
||||
$sudo_command echo "device" > /sys/class/usb_role/**/role || echo -e "${YELLOW}OTG 端口切换 device 模式失败${NC}"
|
||||
if grep -q "usb_role" /etc/rc.local; then
|
||||
echo -e ""
|
||||
else
|
||||
$sudo_command sed -i '/^exit 0/i echo device > \/sys\/class\/usb_role\/\*\*\/role' /etc/rc.local
|
||||
$sudo_command chmod +x /etc/rc.local
|
||||
fi
|
||||
if grep -q "libcomposite" /etc/modules.conf; then
|
||||
echo -e ""
|
||||
else
|
||||
$sudo_command echo "libcomposite" >> /etc/modules.conf
|
||||
fi
|
||||
}
|
||||
|
||||
check_os_architecture
|
||||
check_docker_exists
|
||||
check_sudo_exists
|
||||
show_main_menu
|
||||
29
tools/test_video.py
Normal file
29
tools/test_video.py
Normal file
@ -0,0 +1,29 @@
|
||||
# 模块导入
|
||||
import numpy as np
|
||||
import cv2 as cv
|
||||
# 相机捕获
|
||||
cap = cv.VideoCapture(0,cv.CAP_DSHOW)
|
||||
#更改默认参数
|
||||
cap.set(6,cv.VideoWriter.fourcc('M','J','P','G'))# 视频流格式
|
||||
cap.set(5, 30);# 帧率
|
||||
cap.set(3, 1280)# 帧宽
|
||||
cap.set(4, 720)# 帧高
|
||||
# 获取相机宽高以及帧率
|
||||
width = cap.get(3)
|
||||
height = cap.get(4)
|
||||
frame = cap.get(5) #帧率只对视频有效,因此返回值为0
|
||||
#打印信息
|
||||
print(width ,height)
|
||||
# 循环
|
||||
while(True):
|
||||
# 获取一帧图片
|
||||
ret, img = cap.read()
|
||||
# 显示图片
|
||||
cv.imshow('img', img)
|
||||
# 等待键盘事件
|
||||
k = cv.waitKey(1) & 0xFF
|
||||
if k == 27:
|
||||
break
|
||||
#资源释放
|
||||
cap.release()
|
||||
cv.destroyAllWindows()
|
||||
@ -3,9 +3,9 @@ import threading
|
||||
import time
|
||||
import json
|
||||
from collections import deque
|
||||
from typing import List, Optional, Tuple, Union, Dict, Any
|
||||
from typing import List, Optional, Tuple, Dict
|
||||
import uuid
|
||||
|
||||
import aiohttp
|
||||
import cv2
|
||||
import logging
|
||||
import numpy as np
|
||||
@ -13,8 +13,7 @@ from aiohttp import MultipartWriter, web
|
||||
from aiohttp.web_runner import GracefulExit
|
||||
|
||||
class MjpegStream:
|
||||
"""MJPEG video stream class for handling video frames and providing HTTP streaming service"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
@ -26,19 +25,7 @@ class MjpegStream:
|
||||
device_name: str = "Unknown Camera",
|
||||
log_requests: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Initialize MJPEG stream
|
||||
|
||||
Args:
|
||||
name: Stream name
|
||||
size: Video size (width, height)
|
||||
quality: JPEG compression quality (1-100)
|
||||
fps: Target frame rate
|
||||
host: Server host address
|
||||
port: Server port
|
||||
device_name: Camera device name
|
||||
log_requests: Whether to log stream requests
|
||||
"""
|
||||
|
||||
self.name = name.lower().replace(" ", "_")
|
||||
self.size = size
|
||||
self.quality = max(1, min(quality, 100))
|
||||
@ -48,53 +35,58 @@ class MjpegStream:
|
||||
self._device_name = device_name
|
||||
self.log_requests = log_requests
|
||||
|
||||
# Video frame and synchronization
|
||||
self._frame = np.zeros((320, 240, 1), dtype=np.uint8)
|
||||
self._lock = asyncio.Lock()
|
||||
self._byte_frame_window = deque(maxlen=30)
|
||||
self._bandwidth_last_modified_time = time.time()
|
||||
self._is_online = True
|
||||
self._last_frame_time = time.time()
|
||||
self._last_repeat_frame_time = time.time()
|
||||
self._last_fps_update_time = time.time()
|
||||
self._last_frame_data = None
|
||||
self.per_second_fps = 0
|
||||
self.frame_counter = 0
|
||||
|
||||
|
||||
# 设置日志级别为ERROR,以隐藏HTTP请求日志
|
||||
if not self.log_requests:
|
||||
logging.getLogger('aiohttp.access').setLevel(logging.ERROR)
|
||||
|
||||
# Server setup
|
||||
self._app = web.Application()
|
||||
self._app.router.add_route("GET", f"/{self.name}", self._stream_handler)
|
||||
self._app.router.add_route("GET", "/state", self._state_handler)
|
||||
self._app.router.add_route("GET", "/", self._index_handler)
|
||||
self._app.router.add_route("GET", "/snapshot", self._snapshot_handler)
|
||||
self._app.is_running = False
|
||||
self._clients: Dict[str, Dict] = {}
|
||||
self._clients_lock = asyncio.Lock()
|
||||
|
||||
|
||||
def set_frame(self, frame: np.ndarray) -> None:
|
||||
"""Set the current video frame"""
|
||||
self._frame = frame
|
||||
self._last_frame_time = time.time()
|
||||
self._is_online = True
|
||||
|
||||
def get_bandwidth(self) -> float:
|
||||
"""Get current bandwidth usage (bytes/second)"""
|
||||
if time.time() - self._bandwidth_last_modified_time >= 1:
|
||||
self._byte_frame_window.clear()
|
||||
return sum(self._byte_frame_window)
|
||||
|
||||
async def _process_frame(self) -> Tuple[np.ndarray, Dict[str, str]]:
|
||||
"""Process video frame (resize and JPEG encode)"""
|
||||
frame = cv2.resize(
|
||||
self._frame, self.size or (self._frame.shape[1], self._frame.shape[0])
|
||||
)
|
||||
success, encoded = cv2.imencode(
|
||||
".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, self.quality]
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise ValueError("Error encoding frame")
|
||||
|
||||
self._byte_frame_window.append(len(encoded.tobytes()))
|
||||
self._bandwidth_last_modified_time = time.time()
|
||||
|
||||
current_frame_data = encoded.tobytes()
|
||||
current_time = time.time()
|
||||
|
||||
if current_frame_data == self._last_frame_data and current_time - self._last_repeat_frame_time < 1:
|
||||
return None, {}
|
||||
else:
|
||||
self._last_frame_data = current_frame_data
|
||||
self._last_repeat_frame_time = current_time
|
||||
|
||||
# Add KVMD-compatible header information
|
||||
if current_time - self._last_fps_update_time >= 1:
|
||||
self.per_second_fps = self.frame_counter
|
||||
self.frame_counter = 0
|
||||
self._last_fps_update_time = current_time
|
||||
|
||||
self.frame_counter += 1
|
||||
headers = {
|
||||
"X-UStreamer-Online": str(self._is_online).lower(),
|
||||
"X-UStreamer-Width": str(frame.shape[1]),
|
||||
@ -109,61 +101,69 @@ class MjpegStream:
|
||||
return encoded, headers
|
||||
|
||||
async def _stream_handler(self, request: web.Request) -> web.StreamResponse:
|
||||
"""Handle MJPEG stream requests"""
|
||||
client_id = request.query.get("client_id", uuid.uuid4().hex[:8])
|
||||
client_key = request.query.get("key", "0")
|
||||
advance_headers = request.query.get("advance_headers", "0") == "1"
|
||||
|
||||
response = web.StreamResponse(
|
||||
status=200,
|
||||
reason="OK",
|
||||
headers={"Content-Type": "multipart/x-mixed-replace;boundary=frame"}
|
||||
headers={
|
||||
"Content-Type": "multipart/x-mixed-replace;boundary=frame",
|
||||
"Set-Cookie": f"stream_client={client_key}/{client_id}; Path=/; Max-Age=30"
|
||||
}
|
||||
)
|
||||
await response.prepare(request)
|
||||
|
||||
if self.log_requests:
|
||||
print(f"Stream request received: {request.path}")
|
||||
async with self._clients_lock:
|
||||
if client_id not in self._clients:
|
||||
self._clients[client_id] = {
|
||||
"key": client_key,
|
||||
"advance_headers": advance_headers,
|
||||
"extra_headers": False,
|
||||
"zero_data": False,
|
||||
"fps": 0,
|
||||
}
|
||||
|
||||
try:
|
||||
while True:
|
||||
async with self._lock:
|
||||
frame, headers = await self._process_frame()
|
||||
if frame is None:
|
||||
continue
|
||||
|
||||
#Enable workaround for the Chromium/Blink bug https://issues.chromium.org/issues/41199053
|
||||
if advance_headers:
|
||||
headers.pop('Content-Length', None)
|
||||
for k in list(headers.keys()):
|
||||
if k.startswith('X-UStreamer-'):
|
||||
del headers[k]
|
||||
|
||||
with MultipartWriter("image/jpeg", boundary="frame") as mpwriter:
|
||||
part = mpwriter.append(frame.tobytes(), {"Content-Type": "image/jpeg"})
|
||||
for key, value in headers.items():
|
||||
part.headers[key] = value
|
||||
try:
|
||||
await mpwriter.write(response, close_boundary=False)
|
||||
except (ConnectionResetError, ConnectionAbortedError):
|
||||
return web.Response(status=499)
|
||||
await response.write(b"\r\n")
|
||||
self._clients[client_id]["fps"]=self.per_second_fps
|
||||
finally:
|
||||
async with self._clients_lock:
|
||||
if client_id in self._clients:
|
||||
del self._clients[client_id]
|
||||
|
||||
while True:
|
||||
await asyncio.sleep(1 / self.fps)
|
||||
|
||||
# Check if the device is online
|
||||
if time.time() - self._last_frame_time > 5:
|
||||
self._is_online = False
|
||||
|
||||
async with self._lock:
|
||||
frame, headers = await self._process_frame()
|
||||
|
||||
with MultipartWriter("image/jpeg", boundary="frame") as mpwriter:
|
||||
part = mpwriter.append(frame.tobytes(), {"Content-Type": "image/jpeg"})
|
||||
for key, value in headers.items():
|
||||
part.headers[key] = value
|
||||
try:
|
||||
await mpwriter.write(response, close_boundary=False)
|
||||
except (ConnectionResetError, ConnectionAbortedError):
|
||||
return web.Response(status=499)
|
||||
await response.write(b"\r\n")
|
||||
|
||||
async def _state_handler(self, request: web.Request) -> web.Response:
|
||||
"""Handle /state requests and return device status information"""
|
||||
state = {
|
||||
"ok": "true",
|
||||
"result": {
|
||||
"instance_id": "",
|
||||
"encoder": {
|
||||
"type": "CPU",
|
||||
"quality": self.quality
|
||||
},
|
||||
"h264": {
|
||||
"bitrate": 4875,
|
||||
"gop": 60,
|
||||
"online": self._is_online,
|
||||
"fps": self.fps
|
||||
},
|
||||
"sinks": {
|
||||
"jpeg": {
|
||||
"has_clients": False
|
||||
},
|
||||
"h264": {
|
||||
"has_clients": False
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"resolution": {
|
||||
"width": self.size[0] if self.size else self._frame.shape[1],
|
||||
@ -171,21 +171,12 @@ class MjpegStream:
|
||||
},
|
||||
"online": self._is_online,
|
||||
"desired_fps": self.fps,
|
||||
"captured_fps": 0 # You can update this with actual captured fps if needed
|
||||
"captured_fps": self.fps
|
||||
},
|
||||
"stream": {
|
||||
"queued_fps": 2, # Placeholder value, update as needed
|
||||
"clients": 1, # Placeholder value, update as needed
|
||||
"clients_stat": {
|
||||
"70bf63a507f71e47": {
|
||||
"fps": 2, # Placeholder value, update as needed
|
||||
"extra_headers": False,
|
||||
"advance_headers": True,
|
||||
"dual_final_frames": False,
|
||||
"zero_data": False,
|
||||
"key": "tIR9TtuedKIzDYZa" # Placeholder key, update as needed
|
||||
}
|
||||
}
|
||||
"queued_fps": self.fps,
|
||||
"clients": len(self._clients),
|
||||
"clients_stat": self._clients
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -195,18 +186,30 @@ class MjpegStream:
|
||||
)
|
||||
|
||||
async def _index_handler(self, _: web.Request) -> web.Response:
|
||||
"""Handle root path requests and display available streams"""
|
||||
html = f"""
|
||||
<h2>Available Video Streams:</h2>
|
||||
<ul>
|
||||
<li><a href='http://{self._host}:{self._port}/{self.name}'>/{self.name}</a></li>
|
||||
<li><a href='http://{self._host}:{self._port}/state'>/state</a></li>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>uStreamer-Win</title><style>body {{font-family: monospace;}}</style></head>
|
||||
<body>
|
||||
<h3>uStreamer-Win v0.01 </h3>
|
||||
<ul><hr>
|
||||
<li><a href='http://{self._host}:{self._port}/{self.name}'>/{self.name}</a>
|
||||
<br>Get a live stream. </li><hr><br>
|
||||
<li><a href='http://{self._host}:{self._port}/snapshot'>/snapshot</a>
|
||||
<br>Get a current actual image from the server.</li><hr><br>
|
||||
<li><a href='http://{self._host}:{self._port}/state'>/state</a>
|
||||
<br>Get JSON structure with the state of the server.</li><hr><br>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return web.Response(text=html, content_type="text/html")
|
||||
|
||||
async def _snapshot_handler(self, request: web.Request) -> web.Response:
|
||||
async with self._lock:
|
||||
frame, _ = await self._process_frame()
|
||||
return web.Response(body=frame.tobytes(), content_type="image/jpeg")
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start the stream server"""
|
||||
if not self._app.is_running:
|
||||
threading.Thread(target=self._run_server, daemon=True).start()
|
||||
self._app.is_running = True
|
||||
@ -214,8 +217,8 @@ class MjpegStream:
|
||||
else:
|
||||
print("\nServer is already running\n")
|
||||
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop the stream server"""
|
||||
if self._app.is_running:
|
||||
self._app.is_running = False
|
||||
print("\nStopping server...\n")
|
||||
@ -223,7 +226,6 @@ class MjpegStream:
|
||||
print("\nServer is not running\n")
|
||||
|
||||
def _run_server(self) -> None:
|
||||
"""Run the server in a new thread"""
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
runner = web.AppRunner(self._app)
|
||||
|
||||
@ -41,7 +41,6 @@ def test_camera(index, logger):
|
||||
return False
|
||||
|
||||
def find_camera_by_name(camera_name, logger):
|
||||
"""Find device index by camera name"""
|
||||
if platform.system() != "Windows":
|
||||
logger.warning("Finding camera by name is only supported on Windows")
|
||||
return None
|
||||
@ -57,7 +56,6 @@ def find_camera_by_name(camera_name, logger):
|
||||
return None
|
||||
|
||||
def get_first_available_camera(logger):
|
||||
"""Get the first available camera"""
|
||||
for i in range(5):
|
||||
if test_camera(i, logger):
|
||||
return i
|
||||
@ -75,13 +73,10 @@ def parse_arguments():
|
||||
parser.add_argument('--port', type=int, default=8000, help='Server port')
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate arguments
|
||||
if args.quality < 1 or args.quality > 100:
|
||||
raise ValueError("Quality must be between 1 and 100.")
|
||||
if args.fps <= 0:
|
||||
raise ValueError("FPS must be greater than 0.")
|
||||
|
||||
# Parse resolution
|
||||
try:
|
||||
width, height = map(int, args.resolution.split('x'))
|
||||
except ValueError:
|
||||
@ -95,14 +90,9 @@ def parse_arguments():
|
||||
def main():
|
||||
logger = configure_logging()
|
||||
args = parse_arguments()
|
||||
|
||||
# Determine which camera device to use
|
||||
device_index = None
|
||||
|
||||
if args.device_name:
|
||||
if platform.system() != "Windows":
|
||||
logger.error("Specifying camera by name is only supported on Windows")
|
||||
return
|
||||
device_index = find_camera_by_name(args.device_name, logger)
|
||||
if device_index is None:
|
||||
logger.error(f"No available camera found with a name containing '{args.device_name}'")
|
||||
@ -122,23 +112,21 @@ def main():
|
||||
|
||||
# Initialize the camera
|
||||
try:
|
||||
cap = cv2.VideoCapture(device_index, cv2.CAP_DSHOW if platform.system() == "Windows" else cv2.CAP_ANY)
|
||||
cap = cv2.VideoCapture(device_index, cv2.CAP_DSHOW)
|
||||
|
||||
if not cap.isOpened():
|
||||
logger.error(f"Unable to open camera {device_index}")
|
||||
return
|
||||
|
||||
# Set camera parameters
|
||||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, args.width)
|
||||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, args.height)
|
||||
|
||||
# Verify camera settings
|
||||
cap.set(cv2.CAP_PROP_FRAME_COUNT, args.fps)
|
||||
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M','J','P','G'))
|
||||
actual_width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
|
||||
actual_height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
|
||||
if actual_width != args.width or actual_height != args.height:
|
||||
logger.warning(f"Actual resolution ({actual_width}x{actual_height}) does not match requested resolution ({args.width}x{args.height})")
|
||||
|
||||
# Test if we can read frames
|
||||
ret, _ = cap.read()
|
||||
if not ret:
|
||||
logger.error("Unable to read video frames from the camera")
|
||||
@ -155,13 +143,13 @@ def main():
|
||||
try:
|
||||
stream = MjpegStream(
|
||||
name="stream",
|
||||
size=(int(actual_width), int(actual_height)), # Use actual resolution
|
||||
size=(int(actual_width), int(actual_height)),
|
||||
quality=args.quality,
|
||||
fps=args.fps,
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
device_name=args.device_name or f"Camera {device_index}", # Add device name
|
||||
log_requests=False # 设置为False以隐藏HTTP请求日志
|
||||
device_name=args.device_name or f"Camera {device_index}",
|
||||
log_requests=False
|
||||
)
|
||||
stream.start()
|
||||
logger.info(f"Video stream started: http://{args.host}:{args.port}/stream")
|
||||
@ -176,20 +164,11 @@ def main():
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("User interrupt")
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred: {str(e)}")
|
||||
finally:
|
||||
logger.info("Cleaning up resources...")
|
||||
try:
|
||||
stream.stop()
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping the video stream: {str(e)}")
|
||||
try:
|
||||
cap.release()
|
||||
except Exception as e:
|
||||
logger.error(f"Error releasing the camera: {str(e)}")
|
||||
stream.stop()
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
||||
logger.info("Program has exited")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
x
Reference in New Issue
Block a user