mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-14 02:00:32 +08:00
Compare commits
285 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96a6e7edcd | ||
|
|
50c3e6a32a | ||
|
|
c8305cc65d | ||
|
|
aae4e936db | ||
|
|
45a04f7570 | ||
|
|
53ba69f4aa | ||
|
|
53229a9055 | ||
|
|
f97df0d830 | ||
|
|
8ed5e4abc3 | ||
|
|
1e727ddc1b | ||
|
|
da84a6d09f | ||
|
|
9c35c68eda | ||
|
|
651f9a4f4e | ||
|
|
7777f5e490 | ||
|
|
3ab5e2b431 | ||
|
|
65874c6b43 | ||
|
|
67b943c151 | ||
|
|
593de19df5 | ||
|
|
5296e61281 | ||
|
|
1729badc55 | ||
|
|
9373790f37 | ||
|
|
edb9112435 | ||
|
|
0328163a9e | ||
|
|
0c9d94e1c5 | ||
|
|
d4bd94cb8a | ||
|
|
e7c891353b | ||
|
|
3f8a9e3b2c | ||
|
|
4d4f528178 | ||
|
|
201c615ce2 | ||
|
|
8cc9e22c91 | ||
|
|
892d2b6f41 | ||
|
|
30dd4290ab | ||
|
|
f900c4bb5a | ||
|
|
6299f04127 | ||
|
|
08551e737e | ||
|
|
bbef7bb5c4 | ||
|
|
b94cc14e2a | ||
|
|
ecc27c2be7 | ||
|
|
ccdfd52b75 | ||
|
|
7ccac8bc9e | ||
|
|
6f4cf12c69 | ||
|
|
916a0483b4 | ||
|
|
c262db4a18 | ||
|
|
0b4d83dc93 | ||
|
|
16878dc7ff | ||
|
|
f80e063495 | ||
|
|
d411affca4 | ||
|
|
04b13b1215 | ||
|
|
bdd97c5ea3 | ||
|
|
fafd790b3e | ||
|
|
432c61fd91 | ||
|
|
10fbd0611f | ||
|
|
e87942a5a9 | ||
|
|
19d1c52ac4 | ||
|
|
2c056ca3e3 | ||
|
|
caf3533872 | ||
|
|
187c713424 | ||
|
|
c8d1dcca30 | ||
|
|
0809ab4878 | ||
|
|
678744ce91 | ||
|
|
bd5e17da4b | ||
|
|
fd7bcbd88a | ||
|
|
cfbb6f1be7 | ||
|
|
4a0029bab7 | ||
|
|
6002dfd9c7 | ||
|
|
42efb73c98 | ||
|
|
9b5b6f6152 | ||
|
|
dc7f38a1b6 | ||
|
|
e5cee0ec5e | ||
|
|
776b93cab6 | ||
|
|
43eada0fef | ||
|
|
ec994f4518 | ||
|
|
70c5b9fc4b | ||
|
|
296b1f3bda | ||
|
|
263e252db7 | ||
|
|
9b433a909a | ||
|
|
0cf6f183c8 | ||
|
|
cf6addeb0f | ||
|
|
d57c3c66cd | ||
|
|
49638ed896 | ||
|
|
fbf5e52b0f | ||
|
|
6bdda82822 | ||
|
|
1142cc9d65 | ||
|
|
1b5df61f61 | ||
|
|
b4b1fb8d9a | ||
|
|
f22e05ac88 | ||
|
|
6661efe61d | ||
|
|
a68f860b8e | ||
|
|
e8498858bb | ||
|
|
8b5c87c893 | ||
|
|
824955fb83 | ||
|
|
8560a46f17 | ||
|
|
d4b4cdc492 | ||
|
|
687cea3658 | ||
|
|
12c7566581 | ||
|
|
0e3c821863 | ||
|
|
a5e226e168 | ||
|
|
fe1f821715 | ||
|
|
b28275b042 | ||
|
|
4e4ea9fcea | ||
|
|
735c2e6395 | ||
|
|
f25e5ef2b4 | ||
|
|
0d8b7fd3aa | ||
|
|
91312dd4be | ||
|
|
5bff6cadd4 | ||
|
|
5d2c275f13 | ||
|
|
2a928a4a38 | ||
|
|
37e8aa2cec | ||
|
|
54cb364c2e | ||
|
|
007371d30b | ||
|
|
517e79fd65 | ||
|
|
86f73844dd | ||
|
|
e04381555c | ||
|
|
82f45cd1fd | ||
|
|
2c36d86075 | ||
|
|
6df1e55ffc | ||
|
|
659e8f9169 | ||
|
|
38981a4108 | ||
|
|
10fb78abe6 | ||
|
|
97ea7de7d3 | ||
|
|
56d0d3aa8a | ||
|
|
92f635cdf8 | ||
|
|
4a2c642c49 | ||
|
|
1642ce73a0 | ||
|
|
64c83be0a4 | ||
|
|
6f971a7c54 | ||
|
|
1e3c90e94a | ||
|
|
09884c54c0 | ||
|
|
cd2a801eae | ||
|
|
183a6c2553 | ||
|
|
310b23edad | ||
|
|
625b2aa970 | ||
|
|
741e94f2fd | ||
|
|
ce3af61510 | ||
|
|
bf8761baa9 | ||
|
|
8e2bc47cd3 | ||
|
|
65d1cfd827 | ||
|
|
d7963f3271 | ||
|
|
c3eed7c497 | ||
|
|
70ca478a78 | ||
|
|
49fb9a6f92 | ||
|
|
bd9f5bf9ee | ||
|
|
193eaa48c8 | ||
|
|
47614a5724 | ||
|
|
791e047a6b | ||
|
|
818ff6321e | ||
|
|
53980c0e68 | ||
|
|
1195a9e3be | ||
|
|
18122eff82 | ||
|
|
6910cebc00 | ||
|
|
3b39fcefd5 | ||
|
|
3f309077f8 | ||
|
|
ed447a7cc2 | ||
|
|
93d60ac932 | ||
|
|
39c13d31f3 | ||
|
|
8b97eed743 | ||
|
|
191eb4b430 | ||
|
|
ac240e141b | ||
|
|
af51d79502 | ||
|
|
c551b9ff57 | ||
|
|
df8898684f | ||
|
|
5273199e0b | ||
|
|
eb0fb04b72 | ||
|
|
cfdf225d10 | ||
|
|
76ca81bbfd | ||
|
|
ed7b2e5b33 | ||
|
|
c80532fb73 | ||
|
|
9875d4686f | ||
|
|
1b822c19ff | ||
|
|
1356187771 | ||
|
|
8fb4bc6be7 | ||
|
|
09eb5ebc2f | ||
|
|
bc880009c1 | ||
|
|
3268c62bf3 | ||
|
|
21c83e6fca | ||
|
|
8f19d40566 | ||
|
|
32425c1903 | ||
|
|
6005ed38b9 | ||
|
|
bb0656c0cb | ||
|
|
8d7f89e8f1 | ||
|
|
a65cd7feb5 | ||
|
|
d630e24aa0 | ||
|
|
46ef5fd46b | ||
|
|
c8cf06ee8c | ||
|
|
79d4d99f37 | ||
|
|
0437f487b5 | ||
|
|
59eff99dcc | ||
|
|
334b9f7d7b | ||
|
|
6dea594380 | ||
|
|
fd5196a2ce | ||
|
|
b7715b731e | ||
|
|
7d7edb1c03 | ||
|
|
69d254d80e | ||
|
|
e011a98288 | ||
|
|
63a1933342 | ||
|
|
ebbd55ee17 | ||
|
|
a92a6f2811 | ||
|
|
3d58f6dd21 | ||
|
|
50022e7353 | ||
|
|
1624b0cbf8 | ||
|
|
fa2630250c | ||
|
|
7e185d2ad9 | ||
|
|
16a1dbd9ed | ||
|
|
e66edd45e2 | ||
|
|
86774dfa4e | ||
|
|
866eb2a2c6 | ||
|
|
1984a245e9 | ||
|
|
04209e2a6b | ||
|
|
71617cc62a | ||
|
|
45ff6cb7c7 | ||
|
|
ff4f04d936 | ||
|
|
49695247a5 | ||
|
|
87f78990a5 | ||
|
|
b86f4cd437 | ||
|
|
413fce72ec | ||
|
|
842238009e | ||
|
|
2c4f7f1458 | ||
|
|
ba5df47c97 | ||
|
|
20a7206b0f | ||
|
|
70d134a2ff | ||
|
|
8391b7a467 | ||
|
|
2bdd349fbf | ||
|
|
5014e82177 | ||
|
|
1566f026de | ||
|
|
878bc03a80 | ||
|
|
41e6502904 | ||
|
|
ec9c12ffcc | ||
|
|
9fdb861048 | ||
|
|
97dbc17771 | ||
|
|
e7d4f7fe8c | ||
|
|
1cb5c11239 | ||
|
|
72ef037959 | ||
|
|
182aa0e374 | ||
|
|
876ff22bd8 | ||
|
|
a01ef562a1 | ||
|
|
362b88e92c | ||
|
|
7f6b0a814d | ||
|
|
b3d1291039 | ||
|
|
6a08fab818 | ||
|
|
02740aef37 | ||
|
|
dd3f4c16e3 | ||
|
|
30a82efea4 | ||
|
|
ccbe455ada | ||
|
|
1d0f441cc4 | ||
|
|
8c7f86ac83 | ||
|
|
4b67208cab | ||
|
|
a3e398a1d5 | ||
|
|
c66c97afd4 | ||
|
|
83c352a900 | ||
|
|
de4f1903aa | ||
|
|
800d2724b8 | ||
|
|
dc1c6c0fcf | ||
|
|
4c9c98c6ab | ||
|
|
6ffaa8d6bd | ||
|
|
97b405297b | ||
|
|
302e7c2877 | ||
|
|
75a4aa0736 | ||
|
|
c3dc5b9553 | ||
|
|
79b7788480 | ||
|
|
05519f403f | ||
|
|
c49d712f17 | ||
|
|
375a345820 | ||
|
|
a7c3cdc1ea | ||
|
|
abbd65a9a0 | ||
|
|
ba28f03575 | ||
|
|
ad019f8476 | ||
|
|
0afc81f56c | ||
|
|
84ec99b332 | ||
|
|
54f6d93f63 | ||
|
|
94fe2226f1 | ||
|
|
beb5d541b0 | ||
|
|
1c179da857 | ||
|
|
c8df621172 | ||
|
|
1899902860 | ||
|
|
4800f9e486 | ||
|
|
73238e18e9 | ||
|
|
b51ea5e374 | ||
|
|
13fff8a88c | ||
|
|
9436bb029d | ||
|
|
430a3848f7 | ||
|
|
3b5e539012 | ||
|
|
d1a12f1f6a | ||
|
|
697ef549b9 | ||
|
|
4039ae0483 | ||
|
|
06812231c1 |
@ -1,7 +1,7 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
commit = True
|
commit = True
|
||||||
tag = True
|
tag = True
|
||||||
current_version = 4.49
|
current_version = 4.94
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)(\.(?P<patch>\d+)(\-(?P<release>[a-z]+))?)?
|
parse = (?P<major>\d+)\.(?P<minor>\d+)(\.(?P<patch>\d+)(\-(?P<release>[a-z]+))?)?
|
||||||
serialize =
|
serialize =
|
||||||
{major}.{minor}
|
{major}.{minor}
|
||||||
|
|||||||
36
.github/ISSUE_TEMPLATE/bug-反馈.md
vendored
Normal file
36
.github/ISSUE_TEMPLATE/bug-反馈.md
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
name: BUG 反馈
|
||||||
|
about: 反馈你所遇到的软件 BUG 或其他错误
|
||||||
|
title: "[BUG]"
|
||||||
|
labels: BUG
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Bug 反馈**
|
||||||
|
|
||||||
|
**问题描述**
|
||||||
|
请清晰描述您遇到的问题。例如:软件无法启动、特定功能报错或表现异常等。
|
||||||
|
|
||||||
|
**复现步骤**
|
||||||
|
请提供可复现此问题的详细步骤:
|
||||||
|
1. 前往 '...'
|
||||||
|
2. 点击 '....'
|
||||||
|
3. 滚动到 '....'
|
||||||
|
4. 发现错误
|
||||||
|
|
||||||
|
**日志信息**
|
||||||
|
如果程序崩溃或报错,请在此处粘贴相关的日志。
|
||||||
|
- **整合包镜像**: `systemctl status kvmd` 或 `journalctl -xeu kvmd`
|
||||||
|
- **Docker 镜像**: `docker logs kvmd`
|
||||||
|
|
||||||
|
**系统环境**
|
||||||
|
- **运行方式**: (例如:整合包镜像 / Docker)
|
||||||
|
- **镜像版本**: (Docker 镜像请提供版本号)
|
||||||
|
- **操作系统**: (例如:Debian 12)
|
||||||
|
|
||||||
|
**尝试过的解决方法**
|
||||||
|
请简要描述您为解决此问题已尝试过的方法及其结果。如果未尝试,可留空。
|
||||||
|
|
||||||
|
**补充信息**
|
||||||
|
可以附加截图、录屏或其他有助于理解问题的信息。
|
||||||
25
.github/ISSUE_TEMPLATE/功能请求与设备适配.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/功能请求与设备适配.md
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
name: 功能请求与设备适配
|
||||||
|
about: 请求新的功能或适配新的平台
|
||||||
|
title: "[功能/适配]"
|
||||||
|
labels: 特性
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**功能描述**
|
||||||
|
请详细描述您期望的新功能应该是什么样子。
|
||||||
|
- **对于新功能**:它应该如何工作?有哪些关键特性?
|
||||||
|
- **对于新平台适配**:请提供该平台的具体信息(如设备型号、系统版本、相关链接等)。
|
||||||
|
|
||||||
|
**期望的效果**
|
||||||
|
当该功能实现或平台适配完成后,您期望达到怎样的理想效果?可以像下面这样列出关键点:
|
||||||
|
- [ ] 用户可以...
|
||||||
|
- [ ] 系统能够...
|
||||||
|
- [ ] 解决了之前的...问题
|
||||||
|
|
||||||
|
**我能提供的帮助**
|
||||||
|
为了让这个想法更快成为现实,您可以提供哪些帮助?没有则填写无。
|
||||||
|
- [ ] 我可以参与后续的功能测试
|
||||||
|
- [ ] 我可以提供(临时的)远程调试环境(如 SSH、远程桌面)
|
||||||
|
- [ ] 其他:...
|
||||||
23
.github/workflows/arduino-hid.yml
vendored
23
.github/workflows/arduino-hid.yml
vendored
@ -1,23 +0,0 @@
|
|||||||
name: Arduino HID CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
container:
|
|
||||||
image: python
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Prepare platformio
|
|
||||||
run: pip install platformio
|
|
||||||
|
|
||||||
- name: Build all
|
|
||||||
run: make -C hid/arduino _build_all
|
|
||||||
210
.github/workflows/build_img.yaml
vendored
Normal file
210
.github/workflows/build_img.yaml
vendored
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
name: Build One-KVM Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
device_target:
|
||||||
|
description: 'Target device to build'
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- onecloud
|
||||||
|
- onecloud-pro
|
||||||
|
- cumebox2
|
||||||
|
- chainedbox
|
||||||
|
- vm
|
||||||
|
- e900v22c
|
||||||
|
- octopus-flanet
|
||||||
|
- orangepi-zero
|
||||||
|
- oec-turbo
|
||||||
|
- all
|
||||||
|
create_release:
|
||||||
|
description: 'Create GitHub Release'
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
release_name:
|
||||||
|
description: 'Custom release name (optional)'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
env:
|
||||||
|
BUILD_DATE: ""
|
||||||
|
GIT_SHA: ""
|
||||||
|
RELEASE_TAG: ""
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
container:
|
||||||
|
image: node:18
|
||||||
|
options: --user root --privileged
|
||||||
|
env:
|
||||||
|
TZ: Asia/Shanghai
|
||||||
|
volumes:
|
||||||
|
- /dev:/dev
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Inject TURN config (optional)
|
||||||
|
if: ${{ env.TURN_HOST != '' }}
|
||||||
|
run: |
|
||||||
|
mkdir -p configs/kvmd/override.d
|
||||||
|
cat > configs/kvmd/override.d/turn.yaml <<EOF
|
||||||
|
janus:
|
||||||
|
stun:
|
||||||
|
host: ${TURN_HOST}
|
||||||
|
port: ${TURN_PORT}
|
||||||
|
local_ice_servers:
|
||||||
|
- urls:
|
||||||
|
- "stun:${TURN_HOST}:${TURN_PORT}"
|
||||||
|
- "turn:${TURN_HOST}:${TURN_PORT}?transport=udp"
|
||||||
|
- "turn:${TURN_HOST}:${TURN_PORT}?transport=tcp"
|
||||||
|
username: "${TURN_USER}"
|
||||||
|
credential: "${TURN_PASS}"
|
||||||
|
EOF
|
||||||
|
env:
|
||||||
|
TURN_HOST: ${{ secrets.TURN_HOST }}
|
||||||
|
TURN_PORT: ${{ secrets.TURN_PORT }}
|
||||||
|
TURN_USER: ${{ secrets.TURN_USER }}
|
||||||
|
TURN_PASS: ${{ secrets.TURN_PASS }}
|
||||||
|
|
||||||
|
- name: Set build environment
|
||||||
|
id: build_env
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
BUILD_DATE=$(date +%y%m%d-%H%M)
|
||||||
|
# 使用 GitHub 提供的环境变量避免 Git 权限问题
|
||||||
|
GIT_SHA="${GITHUB_SHA:0:7}"
|
||||||
|
GIT_BRANCH="${GITHUB_REF_NAME}"
|
||||||
|
|
||||||
|
echo "BUILD_DATE=$BUILD_DATE" >> $GITHUB_ENV
|
||||||
|
echo "GIT_SHA=$GIT_SHA" >> $GITHUB_ENV
|
||||||
|
echo "GIT_BRANCH=$GIT_BRANCH" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# 生成唯一但不创建新分支的标识符
|
||||||
|
RELEASE_TAG="build-$BUILD_DATE-${{ github.event.inputs.device_target }}-$GIT_SHA"
|
||||||
|
echo "RELEASE_TAG=$RELEASE_TAG" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "Build environment:"
|
||||||
|
echo "- Date: $BUILD_DATE"
|
||||||
|
echo "- Git SHA: $GIT_SHA"
|
||||||
|
echo "- Git Branch: $GIT_BRANCH"
|
||||||
|
echo "- Release Tag: $RELEASE_TAG"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
sudo tzdata docker.io qemu-utils qemu-user-static binfmt-support parted e2fsprogs \
|
||||||
|
curl tar python3 python3-pip rsync git android-sdk-libsparse-utils coreutils zerofree wget \
|
||||||
|
file tree
|
||||||
|
apt-get clean
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime
|
||||||
|
echo $TZ > /etc/timezone
|
||||||
|
update-binfmts --enable
|
||||||
|
env:
|
||||||
|
DEBIAN_FRONTEND: noninteractive
|
||||||
|
|
||||||
|
- name: Build image
|
||||||
|
id: build
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
echo "=== Build Configuration ==="
|
||||||
|
echo "Target: ${{ github.event.inputs.device_target }}"
|
||||||
|
echo "Build Date: $BUILD_DATE"
|
||||||
|
echo "Git SHA: $GIT_SHA"
|
||||||
|
echo "Git Branch: $GIT_BRANCH"
|
||||||
|
echo "Output Directory: ${{ github.workspace }}/output"
|
||||||
|
echo "=========================="
|
||||||
|
|
||||||
|
mkdir -p "${{ github.workspace }}/output"
|
||||||
|
chmod +x build/build_img.sh
|
||||||
|
|
||||||
|
echo "Starting build process..."
|
||||||
|
if bash build/build_img.sh ${{ github.event.inputs.device_target }}; then
|
||||||
|
echo "BUILD_SUCCESS=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Build completed successfully!"
|
||||||
|
else
|
||||||
|
echo "BUILD_SUCCESS=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "Build failed!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
CI_PROJECT_DIR: ${{ github.workspace }}
|
||||||
|
GITHUB_ACTIONS: true
|
||||||
|
OUTPUTDIR: ${{ github.workspace }}/output
|
||||||
|
|
||||||
|
- name: Collect build artifacts
|
||||||
|
id: artifacts
|
||||||
|
run: |
|
||||||
|
cd "${{ github.workspace }}/output"
|
||||||
|
|
||||||
|
echo "=== Build Artifacts ==="
|
||||||
|
if [ -d "${{ github.workspace }}/output" ]; then
|
||||||
|
find . -name "*.xz" | head -20
|
||||||
|
|
||||||
|
# 统计xz文件信息
|
||||||
|
ARTIFACT_COUNT=$(find . -name "*.xz" | wc -l)
|
||||||
|
TOTAL_SIZE=$(du -sh . | cut -f1)
|
||||||
|
|
||||||
|
echo "ARTIFACT_COUNT=$ARTIFACT_COUNT" >> $GITHUB_OUTPUT
|
||||||
|
echo "TOTAL_SIZE=$TOTAL_SIZE" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "No output directory found!"
|
||||||
|
echo "ARTIFACT_COUNT=0" >> $GITHUB_OUTPUT
|
||||||
|
echo "TOTAL_SIZE=0" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
echo "======================"
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
if: steps.build.outputs.BUILD_SUCCESS == 'true' && github.event.inputs.create_release == 'true'
|
||||||
|
id: release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
tag_name: ${{ env.RELEASE_TAG }}
|
||||||
|
name: ${{ github.event.inputs.release_name || format('One-KVM {0} 构建镜像 ({1})', github.event.inputs.device_target, env.BUILD_DATE) }}
|
||||||
|
body: |
|
||||||
|
## 📦 GitHub Actions 镜像构建
|
||||||
|
|
||||||
|
### 构建信息
|
||||||
|
- **目标设备**: `${{ github.event.inputs.device_target }}`
|
||||||
|
- **构建时间**: `${{ env.BUILD_DATE }}`
|
||||||
|
- **Git 提交**: `${{ env.GIT_SHA }}` (分支: `${{ env.GIT_BRANCH }}`)
|
||||||
|
- **构建环境**: GitHub Actions (Ubuntu 22.04)
|
||||||
|
- **工作流ID**: `${{ github.run_id }}`
|
||||||
|
|
||||||
|
files: ${{ github.workspace }}/output/*.xz
|
||||||
|
prerelease: true
|
||||||
|
make_latest: false
|
||||||
|
generate_release_notes: false
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "## 📋 构建摘要" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 项目 | 值 |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|------|-----|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **目标设备** | \`${{ github.event.inputs.device_target }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **构建时间** | \`${{ env.BUILD_DATE }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **Git SHA** | \`${{ env.GIT_SHA }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **Git 分支** | \`${{ env.GIT_BRANCH }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **构建状态** | ${{ steps.build.outputs.BUILD_SUCCESS == 'true' && '✅ 成功' || '❌ 失败' }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if [ "${{ steps.build.outputs.BUILD_SUCCESS }}" = "true" ]; then
|
||||||
|
echo "| **构建产物** | ${{ steps.artifacts.outputs.ARTIFACT_COUNT || '0' }} 个文件 (${{ steps.artifacts.outputs.TOTAL_SIZE || '0' }}) |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "${{ github.event.inputs.create_release }}" = "true" ]; then
|
||||||
|
echo "| **Release** | [${{ env.RELEASE_TAG }}](${{ steps.release.outputs.url }}) |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
240
.github/workflows/docker-build.yaml
vendored
Normal file
240
.github/workflows/docker-build.yaml
vendored
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
name: Build and Push Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
build_type:
|
||||||
|
description: 'Build type'
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- stage-0
|
||||||
|
- dev
|
||||||
|
- release
|
||||||
|
version:
|
||||||
|
description: 'Version tag (for main image)'
|
||||||
|
required: false
|
||||||
|
default: 'latest'
|
||||||
|
type: string
|
||||||
|
platforms:
|
||||||
|
description: 'Target platforms'
|
||||||
|
required: false
|
||||||
|
default: 'linux/amd64,linux/arm64,linux/arm/v7'
|
||||||
|
type: string
|
||||||
|
enable_aliyun:
|
||||||
|
description: 'Push to Aliyun Registry'
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOCKERHUB_REGISTRY: docker.io
|
||||||
|
ALIYUN_REGISTRY: registry.cn-hangzhou.aliyuncs.com
|
||||||
|
STAGE0_IMAGE: kvmd-stage-0
|
||||||
|
MAIN_IMAGE: kvmd
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-stage-0:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
if: github.event.inputs.build_type == 'stage-0'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Inject TURN config (optional)
|
||||||
|
if: ${{ env.TURN_HOST != '' }}
|
||||||
|
run: |
|
||||||
|
mkdir -p configs/kvmd/override.d
|
||||||
|
cat > configs/kvmd/override.d/turn.yaml <<EOF
|
||||||
|
janus:
|
||||||
|
stun:
|
||||||
|
host: ${TURN_HOST}
|
||||||
|
port: ${TURN_PORT}
|
||||||
|
local_ice_servers:
|
||||||
|
- urls:
|
||||||
|
- "stun:${TURN_HOST}:${TURN_PORT}"
|
||||||
|
- "turn:${TURN_HOST}:${TURN_PORT}?transport=udp"
|
||||||
|
- "turn:${TURN_HOST}:${TURN_PORT}?transport=tcp"
|
||||||
|
username: "${TURN_USER}"
|
||||||
|
credential: "${TURN_PASS}"
|
||||||
|
EOF
|
||||||
|
env:
|
||||||
|
TURN_HOST: ${{ secrets.TURN_HOST }}
|
||||||
|
TURN_PORT: ${{ secrets.TURN_PORT }}
|
||||||
|
TURN_USER: ${{ secrets.TURN_USER }}
|
||||||
|
TURN_PASS: ${{ secrets.TURN_PASS }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
driver: docker-container
|
||||||
|
platforms: ${{ github.event.inputs.platforms }}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
with:
|
||||||
|
platforms: all
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to Aliyun Registry
|
||||||
|
if: github.event.inputs.enable_aliyun == 'true'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.ALIYUN_REGISTRY }}
|
||||||
|
username: ${{ secrets.ALIYUN_USERNAME }}
|
||||||
|
password: ${{ secrets.ALIYUN_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
silentwind0/${{ env.STAGE0_IMAGE }}
|
||||||
|
${{ github.event.inputs.enable_aliyun == 'true' && format('{0}/silentwind/{1}', env.ALIYUN_REGISTRY, env.STAGE0_IMAGE) || '' }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest
|
||||||
|
type=raw,value=latest-{{date 'YYYYMMDD-HHmmss'}}
|
||||||
|
type=sha,prefix={{branch}}-
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.title=One-KVM Stage-0 Base Image
|
||||||
|
org.opencontainers.image.description=Base image for One-KVM build environment
|
||||||
|
org.opencontainers.image.vendor=One-KVM Project
|
||||||
|
|
||||||
|
- name: Build and push stage-0 image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./build/Dockerfile-stage-0
|
||||||
|
platforms: ${{ github.event.inputs.platforms }}
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha,scope=stage-0
|
||||||
|
cache-to: type=gha,mode=max,scope=stage-0
|
||||||
|
provenance: false
|
||||||
|
sbom: false
|
||||||
|
allow: security.insecure
|
||||||
|
|
||||||
|
build-main:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
if: github.event.inputs.build_type != 'stage-0'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Inject TURN config (optional)
|
||||||
|
if: ${{ env.TURN_HOST != '' }}
|
||||||
|
run: |
|
||||||
|
mkdir -p configs/kvmd/override.d
|
||||||
|
cat > configs/kvmd/override.d/turn.yaml <<EOF
|
||||||
|
janus:
|
||||||
|
stun:
|
||||||
|
host: ${TURN_HOST}
|
||||||
|
port: ${TURN_PORT}
|
||||||
|
local_ice_servers:
|
||||||
|
- urls:
|
||||||
|
- "stun:${TURN_HOST}:${TURN_PORT}"
|
||||||
|
- "turn:${TURN_HOST}:${TURN_PORT}?transport=udp"
|
||||||
|
- "turn:${TURN_HOST}:${TURN_PORT}?transport=tcp"
|
||||||
|
username: "${TURN_USER}"
|
||||||
|
credential: "${TURN_PASS}"
|
||||||
|
EOF
|
||||||
|
env:
|
||||||
|
TURN_HOST: ${{ secrets.TURN_HOST }}
|
||||||
|
TURN_PORT: ${{ secrets.TURN_PORT }}
|
||||||
|
TURN_USER: ${{ secrets.TURN_USER }}
|
||||||
|
TURN_PASS: ${{ secrets.TURN_PASS }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
driver: docker-container
|
||||||
|
platforms: ${{ github.event.inputs.platforms }}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
with:
|
||||||
|
platforms: all
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to Aliyun Registry
|
||||||
|
if: github.event.inputs.enable_aliyun == 'true'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.ALIYUN_REGISTRY }}
|
||||||
|
username: ${{ secrets.ALIYUN_USERNAME }}
|
||||||
|
password: ${{ secrets.ALIYUN_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Set version tag
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event.inputs.build_type }}" == "dev" ]]; then
|
||||||
|
echo "tag=dev" >> $GITHUB_OUTPUT
|
||||||
|
elif [[ "${{ github.event.inputs.build_type }}" == "release" ]]; then
|
||||||
|
echo "tag=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
silentwind0/${{ env.MAIN_IMAGE }}
|
||||||
|
${{ github.event.inputs.enable_aliyun == 'true' && format('{0}/silentwind/{1}', env.ALIYUN_REGISTRY, env.MAIN_IMAGE) || '' }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ steps.version.outputs.tag }}
|
||||||
|
type=raw,value=${{ steps.version.outputs.tag }}-{{date 'YYYYMMDD-HHmmss'}}
|
||||||
|
type=sha,prefix={{branch}}-
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.title=One-KVM
|
||||||
|
org.opencontainers.image.description=DIY IP-KVM solution based on PiKVM
|
||||||
|
org.opencontainers.image.vendor=One-KVM Project
|
||||||
|
org.opencontainers.image.version=${{ steps.version.outputs.tag }}
|
||||||
|
|
||||||
|
- name: Build and push main image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./build/Dockerfile
|
||||||
|
platforms: ${{ github.event.inputs.platforms }}
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha,scope=main
|
||||||
|
cache-to: type=gha,mode=max,scope=main
|
||||||
|
provenance: false
|
||||||
|
sbom: false
|
||||||
|
|
||||||
|
- name: Build summary
|
||||||
|
run: |
|
||||||
|
echo "## Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **Build Type**: ${{ github.event.inputs.build_type }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **Version Tag**: ${{ steps.version.outputs.tag }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **Platforms**: ${{ github.event.inputs.platforms }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **Aliyun Enabled**: ${{ github.event.inputs.enable_aliyun }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **Tags**:" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${{ steps.meta.outputs.tags }}" | sed 's/^/ - /' >> $GITHUB_STEP_SUMMARY
|
||||||
41
.github/workflows/pico-hid-release.yml
vendored
41
.github/workflows/pico-hid-release.yml
vendored
@ -1,41 +0,0 @@
|
|||||||
name: Pico HID Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Installing deps ...
|
|
||||||
run: sudo apt-get install cmake gcc-arm-none-eabi build-essential
|
|
||||||
|
|
||||||
- name: Building ...
|
|
||||||
run: make -C hid/pico all
|
|
||||||
|
|
||||||
- name: Releasing ...
|
|
||||||
id: create_release
|
|
||||||
uses: actions/create-release@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
tag_name: ${{ github.ref }}
|
|
||||||
release_name: Release ${{ github.ref }}
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
|
|
||||||
- name: Uploading firmware ...
|
|
||||||
id: upload-release-asset
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: ./hid/pico/hid.uf2
|
|
||||||
asset_name: pico-hid.uf2
|
|
||||||
asset_content_type: application/octet-stream
|
|
||||||
20
.github/workflows/pico-hid.yml
vendored
20
.github/workflows/pico-hid.yml
vendored
@ -1,20 +0,0 @@
|
|||||||
name: Pico HID CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Installing deps ...
|
|
||||||
run: sudo apt-get install cmake gcc-arm-none-eabi build-essential
|
|
||||||
|
|
||||||
- name: Running tests ...
|
|
||||||
run: make -C hid/pico all
|
|
||||||
20
.github/workflows/tox.yml
vendored
20
.github/workflows/tox.yml
vendored
@ -1,20 +0,0 @@
|
|||||||
name: TOX CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Building testenv ...
|
|
||||||
run: make testenv
|
|
||||||
|
|
||||||
- name: Running tests ...
|
|
||||||
run: make tox CMD="tox -c testenv/tox.ini"
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -21,3 +21,4 @@
|
|||||||
/venv/
|
/venv/
|
||||||
.vscode/settings.j/son
|
.vscode/settings.j/son
|
||||||
kvmd_config/
|
kvmd_config/
|
||||||
|
CLAUDE.md
|
||||||
|
|||||||
11
Makefile
11
Makefile
@ -4,7 +4,8 @@ TESTENV_IMAGE ?= kvmd-testenv
|
|||||||
TESTENV_HID ?= /dev/ttyS10
|
TESTENV_HID ?= /dev/ttyS10
|
||||||
TESTENV_VIDEO ?= /dev/video0
|
TESTENV_VIDEO ?= /dev/video0
|
||||||
TESTENV_GPIO ?= /dev/gpiochip0
|
TESTENV_GPIO ?= /dev/gpiochip0
|
||||||
TESTENV_RELAY ?= $(if $(shell ls /dev/hidraw0 2>/dev/null || true),/dev/hidraw0,)
|
TESTENV_RELAY ?=
|
||||||
|
#TESTENV_RELAY ?= $(if $(shell ls /dev/hidraw0 2>/dev/null || true),/dev/hidraw0,)
|
||||||
|
|
||||||
LIBGPIOD_VERSION ?= 1.6.3
|
LIBGPIOD_VERSION ?= 1.6.3
|
||||||
|
|
||||||
@ -28,6 +29,8 @@ all:
|
|||||||
@ echo " make testenv # Build test environment"
|
@ echo " make testenv # Build test environment"
|
||||||
@ echo " make tox # Run tests and linters"
|
@ echo " make tox # Run tests and linters"
|
||||||
@ echo " make tox E=pytest # Run selected test environment"
|
@ echo " make tox E=pytest # Run selected test environment"
|
||||||
|
@ echo " make tox-local # Run tests and linters locally (no Docker)"
|
||||||
|
@ echo " make tox-local E=flake8 # Run selected test locally"
|
||||||
@ echo " make gpio # Create gpio mockup"
|
@ echo " make gpio # Create gpio mockup"
|
||||||
@ echo " make run # Run kvmd"
|
@ echo " make run # Run kvmd"
|
||||||
@ echo " make run CMD=... # Run specified command inside kvmd environment"
|
@ echo " make run CMD=... # Run specified command inside kvmd environment"
|
||||||
@ -96,9 +99,13 @@ tox: testenv
|
|||||||
"
|
"
|
||||||
|
|
||||||
|
|
||||||
|
tox-local:
|
||||||
|
@./check-code.sh $(if $(E),$(E),all)
|
||||||
|
|
||||||
|
|
||||||
$(TESTENV_GPIO):
|
$(TESTENV_GPIO):
|
||||||
test ! -e $(TESTENV_GPIO)
|
test ! -e $(TESTENV_GPIO)
|
||||||
sudo modprobe gpio-mockup gpio_mockup_ranges=0,40
|
sudo modprobe gpio_mockup gpio_mockup_ranges=0,40
|
||||||
test -c $(TESTENV_GPIO)
|
test -c $(TESTENV_GPIO)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
18
PKGBUILD
18
PKGBUILD
@ -39,7 +39,7 @@ for _variant in "${_variants[@]}"; do
|
|||||||
pkgname+=(kvmd-platform-$_platform-$_board)
|
pkgname+=(kvmd-platform-$_platform-$_board)
|
||||||
done
|
done
|
||||||
pkgbase=kvmd
|
pkgbase=kvmd
|
||||||
pkgver=4.49
|
pkgver=4.94
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="The main PiKVM daemon"
|
pkgdesc="The main PiKVM daemon"
|
||||||
url="https://github.com/pikvm/kvmd"
|
url="https://github.com/pikvm/kvmd"
|
||||||
@ -53,6 +53,8 @@ depends=(
|
|||||||
python-aiofiles
|
python-aiofiles
|
||||||
python-async-lru
|
python-async-lru
|
||||||
python-passlib
|
python-passlib
|
||||||
|
# python-bcrypt is needed for passlib
|
||||||
|
python-bcrypt
|
||||||
python-pyotp
|
python-pyotp
|
||||||
python-qrcode
|
python-qrcode
|
||||||
python-periphery
|
python-periphery
|
||||||
@ -66,7 +68,7 @@ depends=(
|
|||||||
python-dbus
|
python-dbus
|
||||||
python-dbus-next
|
python-dbus-next
|
||||||
python-pygments
|
python-pygments
|
||||||
python-pyghmi
|
"python-pyghmi>=1.6.0-2"
|
||||||
python-pam
|
python-pam
|
||||||
python-pillow
|
python-pillow
|
||||||
python-xlib
|
python-xlib
|
||||||
@ -80,6 +82,7 @@ depends=(
|
|||||||
python-luma-oled
|
python-luma-oled
|
||||||
python-pyusb
|
python-pyusb
|
||||||
python-pyudev
|
python-pyudev
|
||||||
|
python-evdev
|
||||||
"libgpiod>=2.1"
|
"libgpiod>=2.1"
|
||||||
freetype2
|
freetype2
|
||||||
"v4l-utils>=1.22.1-1"
|
"v4l-utils>=1.22.1-1"
|
||||||
@ -94,7 +97,7 @@ depends=(
|
|||||||
certbot
|
certbot
|
||||||
platform-io-access
|
platform-io-access
|
||||||
raspberrypi-utils
|
raspberrypi-utils
|
||||||
"ustreamer>=6.26"
|
"ustreamer>=6.37"
|
||||||
|
|
||||||
# Systemd UDEV bug
|
# Systemd UDEV bug
|
||||||
"systemd>=248.3-2"
|
"systemd>=248.3-2"
|
||||||
@ -120,7 +123,7 @@ depends=(
|
|||||||
# fsck for /boot
|
# fsck for /boot
|
||||||
dosfstools
|
dosfstools
|
||||||
|
|
||||||
# pgrep for kvmd-udev-restart-pass
|
# pgrep for kvmd-udev-restart-pass, sysctl for kvmd-otgnet
|
||||||
procps-ng
|
procps-ng
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
@ -163,7 +166,9 @@ package_kvmd() {
|
|||||||
|
|
||||||
install -Dm755 -t "$pkgdir/usr/bin" scripts/kvmd-{bootconfig,gencert,certbot}
|
install -Dm755 -t "$pkgdir/usr/bin" scripts/kvmd-{bootconfig,gencert,certbot}
|
||||||
|
|
||||||
install -Dm644 -t "$pkgdir/usr/lib/systemd/system" configs/os/services/*
|
install -dm755 "$pkgdir/usr/lib/systemd/system"
|
||||||
|
cp -rd configs/os/services -T "$pkgdir/usr/lib/systemd/system"
|
||||||
|
|
||||||
install -DTm644 configs/os/sysusers.conf "$pkgdir/usr/lib/sysusers.d/kvmd.conf"
|
install -DTm644 configs/os/sysusers.conf "$pkgdir/usr/lib/sysusers.d/kvmd.conf"
|
||||||
install -DTm644 configs/os/tmpfiles.conf "$pkgdir/usr/lib/tmpfiles.d/kvmd.conf"
|
install -DTm644 configs/os/tmpfiles.conf "$pkgdir/usr/lib/tmpfiles.d/kvmd.conf"
|
||||||
|
|
||||||
@ -198,6 +203,7 @@ package_kvmd() {
|
|||||||
mkdir -p "$pkgdir/etc/kvmd/override.d"
|
mkdir -p "$pkgdir/etc/kvmd/override.d"
|
||||||
|
|
||||||
mkdir -p "$pkgdir/var/lib/kvmd/"{msd,pst}
|
mkdir -p "$pkgdir/var/lib/kvmd/"{msd,pst}
|
||||||
|
chmod 1775 "$pkgdir/var/lib/kvmd/pst"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -210,7 +216,7 @@ for _variant in "${_variants[@]}"; do
|
|||||||
cd \"kvmd-\$pkgver\"
|
cd \"kvmd-\$pkgver\"
|
||||||
|
|
||||||
pkgdesc=\"PiKVM platform configs - $_platform for $_board\"
|
pkgdesc=\"PiKVM platform configs - $_platform for $_board\"
|
||||||
depends=(kvmd=$pkgver-$pkgrel \"linux-rpi-pikvm>=6.6.45-10\" \"raspberrypi-bootloader-pikvm>=20240818-1\")
|
depends=(kvmd=$pkgver-$pkgrel \"linux-rpi-pikvm>=6.6.45-13\" \"raspberrypi-bootloader-pikvm>=20240818-1\")
|
||||||
|
|
||||||
backup=(
|
backup=(
|
||||||
etc/sysctl.d/99-kvmd.conf
|
etc/sysctl.d/99-kvmd.conf
|
||||||
|
|||||||
307
README.en.md
Normal file
307
README.en.md
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
<div align="center">
|
||||||
|
<img src="https://github.com/mofeng-git/Build-Armbian/assets/62919083/add9743a-0987-4e8a-b2cb-62121f236582" alt="One-KVM Logo" width="300">
|
||||||
|
<h1>One-KVM</h1>
|
||||||
|
<p><strong>DIY IP-KVM solution based on PiKVM</strong></p>
|
||||||
|
|
||||||
|
[](https://github.com/mofeng-git/One-KVM/stargazers)
|
||||||
|
[](https://github.com/mofeng-git/One-KVM/network/members)
|
||||||
|
[](https://github.com/mofeng-git/One-KVM/issues)
|
||||||
|
[](https://github.com/mofeng-git/One-KVM/blob/master/LICENSE)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://one-kvm.mofeng.run">📖 Documentation</a> •
|
||||||
|
<a href="https://kvmd-demo.mofeng.run">🚀 Live Demo</a> •
|
||||||
|
<a href="#quick-start">⚡ Quick Start</a> •
|
||||||
|
<a href="#features">📊 Features</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
[简体中文](README.md) | English
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Table of Contents
|
||||||
|
|
||||||
|
- [Overview](#project-overview)
|
||||||
|
- [Features](#features)
|
||||||
|
- [Quick Start](#quick-start)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
- [Others](#others)
|
||||||
|
|
||||||
|
## 📖 Project Overview
|
||||||
|
|
||||||
|
**One-KVM** is a DIY IP-KVM solution built upon the open-source [PiKVM](https://github.com/pikvm/pikvm) project. It uses cost-effective hardware to provide BIOS-level remote management for servers and workstations.
|
||||||
|
|
||||||
|
### Use Cases
|
||||||
|
|
||||||
|
- **Home lab management** – Remotely manage servers and development devices
|
||||||
|
- **Server maintenance** – Perform system maintenance without physical access
|
||||||
|
- **System recovery** – Troubleshoot boot and BIOS/UEFI issues remotely
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 📊 Features
|
||||||
|
|
||||||
|
### Core Capabilities
|
||||||
|
|
||||||
|
| Feature | Description | Benefit |
|
||||||
|
|------|------|------|
|
||||||
|
| **Non-intrusive** | No software/driver required on the target machine | OS-agnostic; access BIOS/UEFI |
|
||||||
|
| **Cost-effective** | Leverages affordable hardware (TV boxes, dev boards) | Lower cost for KVM-over-IP |
|
||||||
|
| **Extendable** | Added utilities on top of PiKVM | Docker, recording, Chinese UI |
|
||||||
|
| **Deployment** | Supports Docker and prebuilt images | Preconfigured images for specific devices |
|
||||||
|
|
||||||
|
### Limitations
|
||||||
|
|
||||||
|
This project is maintained by an individual with limited resources and no commercial plan.
|
||||||
|
|
||||||
|
- No built-in free NAT punching/tunneling service
|
||||||
|
- No 24×7 technical support
|
||||||
|
- No guarantee on stability/compliance; use at your own risk
|
||||||
|
- User experience is optimized, but basic technical skills are still required
|
||||||
|
|
||||||
|
### Feature Comparison
|
||||||
|
|
||||||
|
> 💡 **Note:** The table below compares One-KVM with other PiKVM-based projects for reference only. If there are omissions or inaccuracies, please open an issue to help improve it.
|
||||||
|
|
||||||
|
| Feature | One-KVM | PiKVM | ArmKVM | BLIKVM |
|
||||||
|
|:--------:|:-------:|:-----:|:------:|:------:|
|
||||||
|
| Simplified Chinese WebUI | ✅ | ❌ | ✅ | ✅ |
|
||||||
|
| Remote video stream | MJPEG/H.264 | MJPEG/H.264 | MJPEG/H.264 | MJPEG/H.264 |
|
||||||
|
| H.264 encoding | CPU | GPU | Unknown | GPU |
|
||||||
|
| Remote audio | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Remote mouse/keyboard | OTG/CH9329 | OTG/CH9329/Pico/Bluetooth | OTG | OTG |
|
||||||
|
| VNC control | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| ATX power control | GPIO/USB relay | GPIO | GPIO | GPIO |
|
||||||
|
| Virtual drive mounting | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Web terminal | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Docker deployment | ✅ | ❌ | ❌ | ❌ |
|
||||||
|
| Commercial offering | ❌ | ✅ | ✅ | ✅ |
|
||||||
|
|
||||||
|
## ⚡ Quick Start
|
||||||
|
|
||||||
|
### Method 1: Docker (Recommended)
|
||||||
|
|
||||||
|
The Docker variant supports OTG or CH9329 as virtual HID and runs on Linux for amd64/arm64/armv7.
|
||||||
|
|
||||||
|
#### One-liner Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sSL https://one-kvm.mofeng.run/quick_start.sh -o quick_start.sh && bash quick_start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Manual Deployment
|
||||||
|
|
||||||
|
It is recommended to use the `--net=host` network mode for better WOL functionality and WebRTC communication support.
|
||||||
|
|
||||||
|
Docker host network mode:
|
||||||
|
|
||||||
|
Port 8080: HTTP Web service
|
||||||
|
Port 4430: HTTPS Web service
|
||||||
|
Port 5900: VNC service
|
||||||
|
Port 623: IPMI service
|
||||||
|
Ports 20000-40000: WebRTC communication port range for low-latency video
|
||||||
|
Port 9 (UDP): Wake-on-LAN (WOL)
|
||||||
|
|
||||||
|
Docker host mode:
|
||||||
|
|
||||||
|
**Using OTG as virtual HID:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo docker run --name kvmd -itd --privileged=true \
|
||||||
|
-v /lib/modules:/lib/modules:ro -v /dev:/dev \
|
||||||
|
-v /sys/kernel/config:/sys/kernel/config -e OTG=1 \
|
||||||
|
--net=host \
|
||||||
|
silentwind0/kvmd
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using CH9329 as virtual HID:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo docker run --name kvmd -itd \
|
||||||
|
--device /dev/video0:/dev/video0 \
|
||||||
|
--device /dev/ttyUSB0:/dev/ttyUSB0 \
|
||||||
|
--net=host \
|
||||||
|
silentwind0/kvmd
|
||||||
|
```
|
||||||
|
|
||||||
|
Docker bridge mode:
|
||||||
|
|
||||||
|
**Using OTG as virtual HID:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo docker run --name kvmd -itd --privileged=true \
|
||||||
|
-v /lib/modules:/lib/modules:ro -v /dev:/dev \
|
||||||
|
-v /sys/kernel/config:/sys/kernel/config -e OTG=1 \
|
||||||
|
-p 8080:8080 -p 4430:4430 -p 5900:5900 -p 623:623 \
|
||||||
|
silentwind0/kvmd
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using CH9329 as virtual HID:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo docker run --name kvmd -itd \
|
||||||
|
--device /dev/video0:/dev/video0 \
|
||||||
|
--device /dev/ttyUSB0:/dev/ttyUSB0 \
|
||||||
|
-p 8080:8080 -p 4430:4430 -p 5900:5900 -p 623:623 \
|
||||||
|
silentwind0/kvmd
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 2: Flash Prebuilt One-KVM Images
|
||||||
|
|
||||||
|
Preconfigured images are provided for specific hardware platforms to simplify deployment and enable out-of-the-box experience.
|
||||||
|
|
||||||
|
#### Download
|
||||||
|
|
||||||
|
**GitHub:**
|
||||||
|
- **GitHub Releases:** [https://github.com/mofeng-git/One-KVM/releases](https://github.com/mofeng-git/One-KVM/releases)
|
||||||
|
|
||||||
|
**Other mirrors:**
|
||||||
|
- **No-login mirror:** [https://pan.huang1111.cn/s/mxkx3T1](https://pan.huang1111.cn/s/mxkx3T1)
|
||||||
|
- **Baidu Netdisk:** [https://pan.baidu.com/s/166-2Y8PBF4SbHXFkGmFJYg?pwd=o9aj](https://pan.baidu.com/s/166-2Y8PBF4SbHXFkGmFJYg?pwd=o9aj) (code: o9aj)
|
||||||
|
|
||||||
|
#### Supported Hardware Platforms
|
||||||
|
|
||||||
|
| Firmware | Codename | Hardware | Latest | Status |
|
||||||
|
|:--------:|:--------:|:--------:|:------:|:----:|
|
||||||
|
| OneCloud | Onecloud | USB capture card, OTG | 241018 | ✅ |
|
||||||
|
| CumeBox 2 | Cumebox2 | USB capture card, OTG | 241004 | ✅ |
|
||||||
|
| Vmare | Vmare-uefi | USB capture card, CH9329 | 241004 | ✅ |
|
||||||
|
| VirtualBox | Virtualbox-uefi | USB capture card, CH9329 | 241004 | ✅ |
|
||||||
|
| s905l3a Generic | E900v22c | USB capture card, OTG | 241004 | ✅ |
|
||||||
|
| Chainedbox | Chainedbox | USB capture card, OTG | 241004 | ✅ |
|
||||||
|
| Loongson 2K0300 | 2k0300 | USB capture card, CH9329 | 241025 | ✅ |
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
Contributions of all kinds are welcome!
|
||||||
|
|
||||||
|
### How to Contribute
|
||||||
|
|
||||||
|
1. **Fork this repo**
|
||||||
|
2. **Create a feature branch:** `git checkout -b feature/AmazingFeature`
|
||||||
|
3. **Commit your changes:** `git commit -m 'Add some AmazingFeature'`
|
||||||
|
4. **Push to the branch:** `git push origin feature/AmazingFeature`
|
||||||
|
5. **Open a Pull Request**
|
||||||
|
|
||||||
|
### Report Issues
|
||||||
|
|
||||||
|
If you find bugs or have suggestions:
|
||||||
|
1. Open an issue via [GitHub Issues](https://github.com/mofeng-git/One-KVM/issues)
|
||||||
|
2. Provide detailed error logs and reproduction steps
|
||||||
|
3. Include your hardware and system information
|
||||||
|
|
||||||
|
### Sponsorship
|
||||||
|
|
||||||
|
This project builds upon many great open-source projects and requires considerable time for testing and maintenance. If you find it helpful, consider supporting via **[Afdian](https://afdian.com/a/silentwind)**.
|
||||||
|
|
||||||
|
#### Thanks
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Click to view the thank-you list</strong></summary>
|
||||||
|
|
||||||
|
- 浩龙的电子嵌入式之路
|
||||||
|
|
||||||
|
- Tsuki
|
||||||
|
|
||||||
|
- H_xiaoming
|
||||||
|
|
||||||
|
- 0蓝蓝0
|
||||||
|
|
||||||
|
- fairybl
|
||||||
|
|
||||||
|
- Will
|
||||||
|
|
||||||
|
- 浩龙的电子嵌入式之路
|
||||||
|
|
||||||
|
- 自.知
|
||||||
|
|
||||||
|
- 观棋不语٩ ི۶
|
||||||
|
|
||||||
|
- 爱发电用户_a57a4
|
||||||
|
|
||||||
|
- 爱发电用户_2c769
|
||||||
|
|
||||||
|
- 霜序
|
||||||
|
|
||||||
|
- 远方(闲鱼用户名:小远技术店铺)
|
||||||
|
|
||||||
|
- 爱发电用户_399fc
|
||||||
|
|
||||||
|
- 斐斐の
|
||||||
|
|
||||||
|
- 爱发电用户_09451
|
||||||
|
|
||||||
|
- 超高校级的錆鱼
|
||||||
|
|
||||||
|
- 爱发电用户_08cff
|
||||||
|
|
||||||
|
- guoke
|
||||||
|
|
||||||
|
- mgt
|
||||||
|
|
||||||
|
- 姜沢掵
|
||||||
|
|
||||||
|
- ui_beam
|
||||||
|
|
||||||
|
- 爱发电用户_c0dd7
|
||||||
|
|
||||||
|
- 爱发电用户_dnjK
|
||||||
|
|
||||||
|
- 忍者胖猪
|
||||||
|
|
||||||
|
- 永遠の願い
|
||||||
|
|
||||||
|
- 爱发电用户_GBrF
|
||||||
|
|
||||||
|
- 爱发电用户_fd65c
|
||||||
|
|
||||||
|
- 爱发电用户_vhNa
|
||||||
|
|
||||||
|
- 爱发电用户_Xu6S
|
||||||
|
|
||||||
|
- moss
|
||||||
|
|
||||||
|
- woshididi
|
||||||
|
|
||||||
|
- 爱发电用户_a0fd1
|
||||||
|
|
||||||
|
- 爱发电用户_f6bH
|
||||||
|
|
||||||
|
- 码农
|
||||||
|
|
||||||
|
- 爱发电用户_6639f
|
||||||
|
|
||||||
|
- jeron
|
||||||
|
|
||||||
|
- 爱发电用户_CN7y
|
||||||
|
|
||||||
|
- 爱发电用户_Up6w
|
||||||
|
|
||||||
|
- 爱发电用户_e3202
|
||||||
|
|
||||||
|
- ......
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
#### Sponsors
|
||||||
|
|
||||||
|
This project is supported by the following sponsors:
|
||||||
|
|
||||||
|
**CDN & Security:**
|
||||||
|
- **[Tencent EdgeOne](https://edgeone.ai/zh?from=github)** – CDN acceleration and security protection
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**File Storage:**
|
||||||
|
- **[Huang1111公益计划](https://pan.huang1111.cn/s/mxkx3T1)** – No-login download service
|
||||||
|
|
||||||
|
## 📚 Others
|
||||||
|
|
||||||
|
### Open-source Projects Used
|
||||||
|
|
||||||
|
This project is built upon the following excellent open-source projects:
|
||||||
|
|
||||||
|
- [PiKVM](https://github.com/pikvm/pikvm) – Open-source DIY IP-KVM solution
|
||||||
|
|
||||||
|
|
||||||
342
README.md
342
README.md
@ -1,61 +1,136 @@
|
|||||||
<h3 align=center><img src="https://github.com/mofeng-git/Build-Armbian/assets/62919083/add9743a-0987-4e8a-b2cb-62121f236582" alt="logo" width="300"><br></h3>
|
<div align="center">
|
||||||
<h3 align=center><a href="https://github.com/mofeng-git/One-KVM/blob/master/README.md">简体中文</a> </h3>
|
<img src="https://github.com/mofeng-git/Build-Armbian/assets/62919083/add9743a-0987-4e8a-b2cb-62121f236582" alt="One-KVM Logo" width="300">
|
||||||
<p align=right> </p>
|
<h1>One-KVM</h1>
|
||||||
|
<p><strong>基于 PiKVM 的 DIY IP-KVM 解决方案</strong></p>
|
||||||
|
|
||||||
|
<p><a href="README.md">简体中文</a> | <a href="README.en.md">English</a></p>
|
||||||
|
|
||||||
|
[](https://github.com/mofeng-git/One-KVM/stargazers)
|
||||||
|
[](https://github.com/mofeng-git/One-KVM/network/members)
|
||||||
|
[](https://github.com/mofeng-git/One-KVM/issues)
|
||||||
|
[](https://github.com/mofeng-git/One-KVM/blob/master/LICENSE)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://docs.one-kvm.cn">📖 详细文档</a> •
|
||||||
|
<a href="https://demo.one-kvm.cn/">🚀 在线演示</a> •
|
||||||
|
<a href="#快速开始">⚡ 快速开始</a> •
|
||||||
|
<a href="#功能介绍">📊 功能介绍</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
### 介绍
|
---
|
||||||
|
|
||||||
One-KVM 是基于廉价计算机硬件和 [PiKVM]((https://github.com/pikvm/pikvm)) 软件二次开发的 BIOS 级远程控制项目。可以实现远程管理服务器或工作站,无需在被控机安装软件调整设置,实现无侵入式控制,适用范围广泛。
|
## 📋 目录
|
||||||
|
|
||||||
使用文档:[https://one-kvm.mofeng.run](https://one-kvm.mofeng.run)
|
- [项目概述](#项目概述)
|
||||||
|
- [功能介绍](#功能介绍)
|
||||||
|
- [快速开始](#快速开始)
|
||||||
|
- [贡献指南](#贡献指南)
|
||||||
|
- [其他](#其他)
|
||||||
|
|
||||||
演示网站:[https://kvmd-demo.mofeng.run](https://kvmd-demo.mofeng.run)
|
## 📖 项目概述
|
||||||
|
|
||||||

|
**One-KVM** 是基于开源 [PiKVM](https://github.com/pikvm/pikvm) 项目进行二次开发的 DIY IP-KVM 解决方案。该方案利用成本较低的硬件设备,实现 BIOS 级别的远程服务器或工作站管理功能。
|
||||||
|
|
||||||
### 软件功能
|
> 本项目目前并无适配树莓派的计划。这是因为树莓派平台本质上属于 PiKVM 官方硬件生态和盈利的一部分。我们非常尊重和感谢上游项目 PiKVM ,因此 One-KVM 的设备适配主要聚焦于补充性场景,尽量避免与 PiKVM 官方产品产生重叠,以支持其可持续发展。
|
||||||
|
|
||||||
表格仅为 One-KVM 与其他基于 PiKVM 的项目的功能对比,无不良导向,如有错漏请联系更正。
|
### 应用场景
|
||||||
|
|
||||||
| 功能 | One-KVM | PiKVM | ArmKVM | BLIKVM |
|
- **家庭实验室主机管理** - 远程管理服务器和开发设备
|
||||||
| :-------------------: | :-------------: | :-----------------------: | :---------: | :---------: |
|
- **服务器远程维护** - 无需物理接触即可进行系统维护
|
||||||
| 系统开源 | √ | √ | √ | √ |
|
- **系统故障处理** - 远程解决系统启动和 BIOS 相关问题
|
||||||
| 简体中文 WebUI | √ | x | √ | √ |
|
|
||||||
| 远程视频流 | MJPEG/H.264 | MJPEG/H.264 | MJPEG/H.264 | MJPEG/H.264 |
|
|
||||||
| H.264 视频编码 | CPU | GPU | 未知 | GPU |
|
|
||||||
| 远程音频流 | √ | √ | √ | √ |
|
|
||||||
| 远程鼠键控制 | OTG/CH9329 | OTG/CH9329/Pico/Bluetooth | OTG | OTG |
|
|
||||||
| VNC 控制 | √ | √ | √ | √ |
|
|
||||||
| ATX 电源控制 | GPIO/USB 继电器 | GPIO | GPIO | GPIO |
|
|
||||||
| 虚拟存储驱动器挂载 | √ | √ | √ | √ |
|
|
||||||
| 2.2G 以上 CD-ROM 挂载 | x | x | √ | √ |
|
|
||||||
| WOL 远程唤醒 | √ | √ | √ | √ |
|
|
||||||
| 网页剪切板 | √ | √ | √ | √ |
|
|
||||||
| OCR 文字识别 | √ | √ | √ | √ |
|
|
||||||
| 网页终端 | √ | √ | √ | √ |
|
|
||||||
| 网络串口终端 | x | x | √ | √ |
|
|
||||||
| HDMI 切换器支持 | √ | √ | √ | √ |
|
|
||||||
| 视频录制 | √ | x | x | x |
|
|
||||||
| Docker 部署 | √ | x | x | x |
|
|
||||||
| 官方商业化成品 | x | √ | √ | √ |
|
|
||||||
| 技术支持 | √ | √ | √ | √ |
|
|
||||||
|
|
||||||
### 快速开始
|

|
||||||
|
|
||||||
更多详细内容可以查阅 [One-KVM文档](https://one-kvm.mofeng.run/)。
|
## 📊 功能介绍
|
||||||
|
|
||||||
**方式一:Docker 镜像部署(推荐)**
|
### 核心特性
|
||||||
|
|
||||||
Docker 版本可以使用 OTG 或 CH9329 作为虚拟 HID ,支持 amd64、arm64、armv7 架构的 Linux 系统安装。
|
| 特性 | 描述 | 优势 |
|
||||||
|
|------|------|------|
|
||||||
|
| **无侵入性** | 无需在目标机器上安装软件或驱动 | 不依赖操作系统,可访问 BIOS/UEFI 设置 |
|
||||||
|
| **成本效益** | 利用常见硬件设备(如电视盒子、开发板等) | 降低 KVM over IP 的实现成本 |
|
||||||
|
| **功能扩展** | 在 PiKVM 基础上增加实用功能 | Docker 部署、视频录制、中文界面 |
|
||||||
|
| **部署方式** | 支持 Docker 部署和硬件整合包 | 为特定硬件平台提供预配置方案 |
|
||||||
|
|
||||||
**脚本部署**
|
### 项目限制
|
||||||
|
|
||||||
|
本项目为个人维护的开源项目,资源有限,无商业运营计划
|
||||||
|
|
||||||
|
- 不提供内置免费内网穿透服务,相关问题请自行解决
|
||||||
|
- 不提供24×7小时技术支持服务
|
||||||
|
- 不承诺系统稳定性和合规性,使用风险需自行承担
|
||||||
|
- 尽力优化用户体验,但仍需要一定的技术基础
|
||||||
|
|
||||||
|
### 功能对比
|
||||||
|
|
||||||
|
> 💡 **说明:** 以下表格展示了 One-KVM 与其他基于 PiKVM 项目的功能对比,仅供参考。如有遗漏或错误,欢迎联系更正。
|
||||||
|
|
||||||
|
| 功能特性 | One-KVM | PiKVM | ArmKVM | BLIKVM |
|
||||||
|
|:--------:|:-------:|:-----:|:------:|:------:|
|
||||||
|
| 简体中文 WebUI | ✅ | ❌ | ✅ | ✅ |
|
||||||
|
| 远程视频流 | MJPEG/H.264 | MJPEG/H.264 | MJPEG/H.264 | MJPEG/H.264 |
|
||||||
|
| H.264 视频编码 | CPU/GPU | GPU | 未知 | GPU |
|
||||||
|
| 远程音频流 | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| 远程鼠键控制 | OTG/CH9329 | OTG/CH9329/Pico/Bluetooth | OTG | OTG |
|
||||||
|
| VNC 控制 | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| ATX 电源控制 | GPIO/USB 继电器 | GPIO | GPIO | GPIO |
|
||||||
|
| 虚拟存储驱动器挂载 | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| 网页终端 | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Docker 部署 | ✅ | ❌ | ❌ | ❌ |
|
||||||
|
| 商业化运营 | ❌ | ✅ | ✅ | ✅ |
|
||||||
|
|
||||||
|
## ⚡ 快速开始
|
||||||
|
|
||||||
|
### 方式一:Docker 镜像部署(推荐)
|
||||||
|
|
||||||
|
Docker 版本支持 OTG 或 CH9329 作为虚拟 HID,兼容 amd64、arm64、armv7 架构的 Linux 系统。
|
||||||
|
|
||||||
|
#### 一键脚本部署
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sSL https://one-kvm.mofeng.run/quick_start.sh -o quick_start.sh && bash quick_start.sh
|
curl -sSL https://docs.one-kvm.cn/quick_start.sh -o quick_start.sh && bash quick_start.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
**手动部署**
|
#### 手动部署
|
||||||
|
|
||||||
|
推荐使用 --net=host 网络模式以获得更好的 wol 功能和 webrtc 通信支持。
|
||||||
|
|
||||||
|
docker host 网络模式:
|
||||||
|
|
||||||
|
端口 8080:HTTP Web 服务
|
||||||
|
端口 4430:HTTPS Web 服务
|
||||||
|
端口 5900:VNC 服务
|
||||||
|
端口 623:IPMI 服务
|
||||||
|
端口 20000-40000:WebRTC 通信端口范围,用于低延迟视频传输
|
||||||
|
端口 9(UDP):Wake-on-LAN(WOL)唤醒功能
|
||||||
|
|
||||||
|
docker host 模式:
|
||||||
|
|
||||||
|
**使用 OTG 作为虚拟 HID:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo docker run --name kvmd -itd --privileged=true \
|
||||||
|
-v /lib/modules:/lib/modules:ro -v /dev:/dev \
|
||||||
|
-v /sys/kernel/config:/sys/kernel/config -e OTG=1 \
|
||||||
|
--net=host \
|
||||||
|
silentwind0/kvmd
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用 CH9329 作为虚拟 HID:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo docker run --name kvmd -itd \
|
||||||
|
--device /dev/video0:/dev/video0 \
|
||||||
|
--device /dev/ttyUSB0:/dev/ttyUSB0 \
|
||||||
|
--net=host \
|
||||||
|
silentwind0/kvmd
|
||||||
|
```
|
||||||
|
|
||||||
|
docker bridge 模式:
|
||||||
|
|
||||||
|
**使用 OTG 作为虚拟 HID:**
|
||||||
|
|
||||||
如果使用 OTG 作为虚拟 HID,可以使用如下部署命令:
|
|
||||||
```bash
|
```bash
|
||||||
sudo docker run --name kvmd -itd --privileged=true \
|
sudo docker run --name kvmd -itd --privileged=true \
|
||||||
-v /lib/modules:/lib/modules:ro -v /dev:/dev \
|
-v /lib/modules:/lib/modules:ro -v /dev:/dev \
|
||||||
@ -64,89 +139,190 @@ sudo docker run --name kvmd -itd --privileged=true \
|
|||||||
silentwind0/kvmd
|
silentwind0/kvmd
|
||||||
```
|
```
|
||||||
|
|
||||||
如果使用 CH9329 作为虚拟 HID,可以使用如下部署命令:
|
**使用 CH9329 作为虚拟 HID:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo docker run --name kvmd -itd \
|
sudo docker run --name kvmd -itd \
|
||||||
--device /dev/video0:/dev/video0 \
|
--device /dev/video0:/dev/video0 \
|
||||||
--device /dev/ttyUSB0:/dev/ttyUSB0 \
|
--device /dev/ttyUSB0:/dev/ttyUSB0 \
|
||||||
--device /dev/snd:/dev/snd \
|
|
||||||
-p 8080:8080 -p 4430:4430 -p 5900:5900 -p 623:623 \
|
-p 8080:8080 -p 4430:4430 -p 5900:5900 -p 623:623 \
|
||||||
silentwind0/kvmd
|
silentwind0/kvmd
|
||||||
```
|
```
|
||||||
|
|
||||||
**方式二:直刷 One-KVM 整合包**
|
### 方式二:直刷 One-KVM 整合包
|
||||||
|
|
||||||
对于部分平台硬件,本项目制作了深度适配的 One-KVM 打包镜像,开箱即用,刷好后启动设备就可以开始使用 One-KVM。免费 One-KVM 整合包也可以在本项目 Releases 页可以找到。
|
针对特定硬件平台,提供了预配置的 One-KVM 打包镜像,简化部署流程,实现开箱即用。
|
||||||
|
|
||||||
| 整合包适配概况 | | | |
|
#### 固件下载
|
||||||
| :-------------: | :-------------: | :-------------: | :-------------: |
|
|
||||||
| **固件型号** | **固件代号** | **硬件情况** | **最新版本** |
|
|
||||||
| 玩客云 | Onecloud | USB 采集卡、OTG | 241018 |
|
|
||||||
| 私家云二代 | Cumebox2 | USB 采集卡、OTG | 241004 |
|
|
||||||
| Vmare | Vmare-uefi | USB 采集卡、CH9329 | 241004 |
|
|
||||||
| Virtualbox | Virtualbox-uefi | USB 采集卡、CH9329 | 241004 |
|
|
||||||
| s905l3a 通用包 | E900v22c | USB 采集卡、OTG | 241004 |
|
|
||||||
| 我家云 | Chainedbox | USB 采集卡、OTG | 241004 |
|
|
||||||
| 龙芯久久派 | 2k0300 | USB 采集卡、CH9329 | 241025 |
|
|
||||||
|
|
||||||
### 赞助方式
|
**GitHub 下载:**
|
||||||
|
- **GitHub Releases:** [https://github.com/mofeng-git/One-KVM/releases](https://github.com/mofeng-git/One-KVM/releases)
|
||||||
|
|
||||||
这个项目基于众多开源项目二次开发,作者为此花费了大量的时间和精力进行测试和维护。若此项目对您有用,您可以考虑通过 **[为爱发电](https://afdian.com/a/silentwind)** 赞助一笔小钱支持作者。作者将能有更多的金钱来测试和维护 One-KVM 的各种配置,并在项目上投入更多的时间和精力。
|
**其他下载方式:**
|
||||||
|
- **免登录高速下载:** [http://sd1.files.one-kvm.cn/](http://sd1.files.one-kvm.cn/)(由群友赞助,支持直链,接入 EdgeOne CDN,建议使用多线程下载工具下载获取最高速度)
|
||||||
|
- **免登录下载:** [https://pan.huang1111.cn/s/mxkx3T1](https://pan.huang1111.cn/s/mxkx3T1) (由 Huang1111公益计划 提供)
|
||||||
|
- **百度网盘:** [https://pan.baidu.com/s/166-2Y8PBF4SbHXFkGmFJYg?pwd=o9aj](https://pan.baidu.com/s/166-2Y8PBF4SbHXFkGmFJYg?pwd=o9aj) (提取码:o9aj)
|
||||||
|
|
||||||
**感谢名单**
|
#### 支持的硬件平台
|
||||||
|
|
||||||
|
| 固件型号 | 固件代号 | 硬件配置 | 最新版本 | 状态 |
|
||||||
|
|:--------:|:--------:|:--------:|:--------:|:----:|
|
||||||
|
| 玩客云 | Onecloud | USB 采集卡、OTG | 241018 | ✅ |
|
||||||
|
| 私家云二代 | Cumebox2 | USB 采集卡、OTG | 241004 | ✅ |
|
||||||
|
| Vmare | Vmare-uefi | USB 采集卡、CH9329 | 241004 | ✅ |
|
||||||
|
| Virtualbox | Virtualbox-uefi | USB 采集卡、CH9329 | 241004 | ✅ |
|
||||||
|
| s905l3a 通用包 | E900v22c | USB 采集卡、OTG | 241004 | ✅ |
|
||||||
|
| 我家云 | Chainedbox | USB 采集卡、OTG | 241004 | ✅ |
|
||||||
|
| 龙芯久久派 | 2k0300 | USB 采集卡、CH9329 | 241025 | ❌ |
|
||||||
|
|
||||||
|
### 报告问题
|
||||||
|
|
||||||
|
如果您发现了问题,请:
|
||||||
|
1. 使用 [GitHub Issues](https://github.com/mofeng-git/One-KVM/issues) 报告
|
||||||
|
2. 提供详细的错误信息和复现步骤
|
||||||
|
3. 包含您的硬件配置和系统信息
|
||||||
|
|
||||||
|
### 赞助支持
|
||||||
|
|
||||||
|
本项目基于多个优秀开源项目进行二次开发,作者投入了大量时间进行测试和维护。如果您觉得这个项目有价值,欢迎通过 **[为爱发电](https://afdian.com/a/silentwind)** 支持项目发展。
|
||||||
|
|
||||||
|
#### 感谢名单
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
<summary><strong>点击查看感谢名单</strong></summary>
|
||||||
|
|
||||||
浩龙的电子嵌入式之路(赞助)
|
- 浩龙的电子嵌入式之路
|
||||||
|
|
||||||
Tsuki(赞助)
|
- Tsuki
|
||||||
|
|
||||||
H_xiaoming
|
- H_xiaoming
|
||||||
|
|
||||||
0蓝蓝0
|
- 0蓝蓝0
|
||||||
|
|
||||||
fairybl
|
- fairybl
|
||||||
|
|
||||||
Will
|
- Will
|
||||||
|
|
||||||
浩龙的电子嵌入式之路
|
- 浩龙的电子嵌入式之路
|
||||||
|
|
||||||
自.知
|
- 自.知
|
||||||
|
|
||||||
观棋不语٩ ི۶
|
- 观棋不语٩ ི۶
|
||||||
|
|
||||||
爱发电用户_a57a4
|
- 爱发电用户_a57a4
|
||||||
|
|
||||||
爱发电用户_2c769
|
- 爱发电用户_2c769
|
||||||
|
|
||||||
霜序
|
- 霜序
|
||||||
|
|
||||||
[远方](https://runyf.cn/)(闲鱼用户名:小远技术店铺)
|
- 远方(闲鱼用户名:小远技术店铺)
|
||||||
|
|
||||||
爱发电用户_399fc
|
- 爱发电用户_399fc
|
||||||
|
|
||||||
[斐斐の](https://www.mmuaa.com/)
|
- 斐斐の
|
||||||
|
|
||||||
爱发电用户_09451
|
- 爱发电用户_09451
|
||||||
|
|
||||||
超高校级的錆鱼
|
- 超高校级的錆鱼
|
||||||
|
|
||||||
爱发电用户_08cff
|
- 爱发电用户_08cff
|
||||||
|
|
||||||
guoke
|
- guoke
|
||||||
|
|
||||||
mgt
|
- mgt
|
||||||
|
|
||||||
|
- 姜沢掵
|
||||||
|
|
||||||
|
- ui_beam
|
||||||
|
|
||||||
|
- 爱发电用户_c0dd7
|
||||||
|
|
||||||
|
- 爱发电用户_dnjK
|
||||||
|
|
||||||
|
- 忍者胖猪
|
||||||
|
|
||||||
|
- 永遠の願い
|
||||||
|
|
||||||
|
- 爱发电用户_GBrF
|
||||||
|
|
||||||
|
- 爱发电用户_fd65c
|
||||||
|
|
||||||
|
- 爱发电用户_vhNa
|
||||||
|
|
||||||
|
- 爱发电用户_Xu6S
|
||||||
|
|
||||||
|
- moss
|
||||||
|
|
||||||
|
- woshididi
|
||||||
|
|
||||||
|
- 爱发电用户_a0fd1
|
||||||
|
|
||||||
|
- 爱发电用户_f6bH
|
||||||
|
|
||||||
|
- 码农
|
||||||
|
|
||||||
|
- 爱发电用户_6639f
|
||||||
|
|
||||||
|
- jeron
|
||||||
|
|
||||||
|
- 爱发电用户_CN7y
|
||||||
|
|
||||||
|
- 爱发电用户_Up6w
|
||||||
|
|
||||||
|
- 爱发电用户_e3202
|
||||||
|
|
||||||
|
- 一语念白
|
||||||
|
|
||||||
|
- 云边
|
||||||
|
|
||||||
|
- 爱发电用户_5a711
|
||||||
|
|
||||||
|
- 爱发电用户_9a706
|
||||||
|
|
||||||
|
- T0m9ir1SUKI
|
||||||
|
|
||||||
|
- 爱发电用户_56d52
|
||||||
|
|
||||||
|
- 爱发电用户_3N6F
|
||||||
|
|
||||||
|
- DUSK
|
||||||
|
|
||||||
|
- 飘零
|
||||||
|
|
||||||
|
- .
|
||||||
|
|
||||||
|
- 饭太稀
|
||||||
|
|
||||||
|
- 葱
|
||||||
|
|
||||||
|
- ......
|
||||||
|
|
||||||
......
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
本项目使用了下列开源项目:
|
#### 赞助商
|
||||||
1. [pikvm/pikvm: Open and inexpensive DIY IP-KVM based on Raspberry Pi (github.com)](https://github.com/pikvm/pikvm)
|
|
||||||
|
|
||||||
### 项目状态
|
本项目得到以下赞助商的支持:
|
||||||
|
|
||||||
[](https://star-history.com/#mofeng-git/One-KVM&Date)
|
**CDN 加速及安全防护:**
|
||||||
|
- **[Tencent EdgeOne](https://edgeone.ai/zh?from=github)** - 提供 CDN 加速及安全防护服务
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
**文件存储服务:**
|
||||||
|
- **[Huang1111公益计划](https://pan.huang1111.cn/s/mxkx3T1)** - 提供免登录下载服务
|
||||||
|
|
||||||
|
**云服务商**
|
||||||
|
|
||||||
|
- **[林枫云](https://www.dkdun.cn)** - 赞助了本项目宁波大带宽服务器
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
林枫云主营国内外地域的精品线路业务服务器、高主频游戏服务器和大带宽服务器。
|
||||||
|
|
||||||
|
## 📚 其他
|
||||||
|
|
||||||
|
### 使用的开源项目
|
||||||
|
|
||||||
|
本项目基于以下优秀开源项目进行二次开发:
|
||||||
|
|
||||||
|
- [PiKVM](https://github.com/pikvm/pikvm) - 开源的 DIY IP-KVM 解决方案
|
||||||
|
|||||||
@ -4,13 +4,17 @@ FROM python:3.11.11-slim-bookworm
|
|||||||
|
|
||||||
LABEL maintainer="mofeng654321@hotmail.com"
|
LABEL maintainer="mofeng654321@hotmail.com"
|
||||||
|
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
COPY --from=builder /tmp/lib/* /tmp/lib/
|
COPY --from=builder /tmp/lib/* /tmp/lib/
|
||||||
COPY --from=builder /tmp/ustreamer/ustreamer /tmp/ustreamer/ustreamer-dump /usr/bin/janus /usr/bin/
|
COPY --from=builder /tmp/ustreamer/ustreamer /tmp/ustreamer/ustreamer-dump /usr/bin/janus /usr/bin/
|
||||||
COPY --from=builder /tmp/wheel/*.whl /tmp/wheel/
|
COPY --from=builder /tmp/wheel/*.whl /tmp/wheel/
|
||||||
COPY --from=builder /tmp/ustreamer/libjanus_ustreamer.so /usr/lib/ustreamer/janus/
|
COPY --from=builder /tmp/ustreamer/libjanus_ustreamer.so /usr/lib/ustreamer/janus/
|
||||||
COPY --from=builder /usr/lib/janus/transports/* /usr/lib/janus/transports/
|
COPY --from=builder /usr/lib/janus/transports/* /usr/lib/janus/transports/
|
||||||
|
COPY --from=builder /tmp/arm64-libs.tar.gz* /tmp/
|
||||||
ARG TARGETARCH
|
RUN if [ ${TARGETARCH} = arm64 ] && [ -f /tmp/arm64-libs.tar.gz ]; then \
|
||||||
|
cd / && tar -xzf /tmp/arm64-libs.tar.gz && rm -f /tmp/arm64-libs.tar.gz; \
|
||||||
|
fi
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
@ -41,7 +45,39 @@ RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/' /etc/apt/sources.lis
|
|||||||
libwebsockets17 \
|
libwebsockets17 \
|
||||||
libnss3 \
|
libnss3 \
|
||||||
libasound2 \
|
libasound2 \
|
||||||
|
libdrm2 \
|
||||||
|
libx264-164 \
|
||||||
|
libyuv0 \
|
||||||
nano \
|
nano \
|
||||||
|
unzip \
|
||||||
|
&& case ${TARGETARCH} in \
|
||||||
|
amd64) \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
libavcodec59 libavformat59 libavutil57 \
|
||||||
|
libswscale6 libavfilter8 libavdevice59 \
|
||||||
|
ffmpeg vainfo \
|
||||||
|
libva2 libva-drm2 libva-x11-2 \
|
||||||
|
mesa-va-drivers mesa-vdpau-drivers \
|
||||||
|
intel-media-va-driver i965-va-driver \
|
||||||
|
;; \
|
||||||
|
arm) \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
libavcodec59 libavformat59 libavutil57 \
|
||||||
|
libswscale6 libavfilter8 libavdevice59 \
|
||||||
|
v4l-utils libv4l-0 \
|
||||||
|
;; \
|
||||||
|
arm64) \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
v4l-utils libv4l-0 libavutil57 \
|
||||||
|
libstdc++6 libavcodec59 libavformat59 \
|
||||||
|
libswscale6 libavfilter8 libavdevice59 \
|
||||||
|
libva2 libva-drm2 libva-x11-2 \
|
||||||
|
libvdpau1 ocl-icd-libopencl1 \
|
||||||
|
;; \
|
||||||
|
*) \
|
||||||
|
echo "Unsupported architecture: ${TARGETARCH}" && exit 1 \
|
||||||
|
;; \
|
||||||
|
esac \
|
||||||
&& cp /tmp/lib/* /lib/*-linux-*/ \
|
&& cp /tmp/lib/* /lib/*-linux-*/ \
|
||||||
&& pip install --no-cache-dir --root-user-action=ignore --disable-pip-version-check /tmp/wheel/*.whl \
|
&& pip install --no-cache-dir --root-user-action=ignore --disable-pip-version-check /tmp/wheel/*.whl \
|
||||||
&& pip install --no-cache-dir --root-user-action=ignore --disable-pip-version-check pyfatfs \
|
&& pip install --no-cache-dir --root-user-action=ignore --disable-pip-version-check pyfatfs \
|
||||||
@ -51,6 +87,18 @@ RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/' /etc/apt/sources.lis
|
|||||||
fi \
|
fi \
|
||||||
&& curl https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.$ARCH -L -o /usr/local/bin/ttyd \
|
&& curl https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.$ARCH -L -o /usr/local/bin/ttyd \
|
||||||
&& chmod +x /usr/local/bin/ttyd \
|
&& chmod +x /usr/local/bin/ttyd \
|
||||||
|
&& mkdir -p /tmp/gostc && cd /tmp/gostc \
|
||||||
|
&& case ${TARGETARCH} in \
|
||||||
|
amd64) GOSTC_ARCH=amd64_v1 ;; \
|
||||||
|
arm) GOSTC_ARCH=arm_7 ;; \
|
||||||
|
arm64) GOSTC_ARCH=arm64_v8.0 ;; \
|
||||||
|
*) echo "Unsupported architecture for gostc: ${TARGETARCH}" && exit 1 ;; \
|
||||||
|
esac \
|
||||||
|
&& curl -L https://github.com/mofeng-git/gostc-open/releases/download/v2.0.8-beta.2/gostc_linux_${GOSTC_ARCH}.tar.gz -o gostc.tar.gz \
|
||||||
|
&& tar -xzf gostc.tar.gz \
|
||||||
|
&& mv gostc /usr/bin/ \
|
||||||
|
&& chmod +x /usr/bin/gostc \
|
||||||
|
&& cd / && rm -rf /tmp/gostc \
|
||||||
&& adduser kvmd --gecos "" --disabled-password \
|
&& adduser kvmd --gecos "" --disabled-password \
|
||||||
&& ln -sf /usr/share/tesseract-ocr/*/tessdata /usr/share/tessdata \
|
&& ln -sf /usr/share/tesseract-ocr/*/tessdata /usr/share/tessdata \
|
||||||
&& mkdir -p /etc/kvmd_backup/override.d \
|
&& mkdir -p /etc/kvmd_backup/override.d \
|
||||||
@ -62,9 +110,12 @@ RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/' /etc/apt/sources.lis
|
|||||||
/run/kvmd \
|
/run/kvmd \
|
||||||
/tmp/kvmd-nginx \
|
/tmp/kvmd-nginx \
|
||||||
&& touch /run/kvmd/ustreamer.sock \
|
&& touch /run/kvmd/ustreamer.sock \
|
||||||
|
&& groupadd kvmd-selfauth \
|
||||||
|
&& usermod -a -G kvmd-selfauth root \
|
||||||
&& apt clean \
|
&& apt clean \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& rm -rf /tmp/lib /tmp/wheel
|
&& rm -rf /tmp/lib /tmp/wheel \
|
||||||
|
&& ustreamer -v
|
||||||
|
|
||||||
COPY testenv/fakes/vcgencmd scripts/kvmd* /usr/bin/
|
COPY testenv/fakes/vcgencmd scripts/kvmd* /usr/bin/
|
||||||
COPY extras/ /usr/share/kvmd/extras/
|
COPY extras/ /usr/share/kvmd/extras/
|
||||||
|
|||||||
@ -47,6 +47,35 @@ RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/' /etc/apt/sources.lis
|
|||||||
libspeex-dev \
|
libspeex-dev \
|
||||||
libspeexdsp-dev \
|
libspeexdsp-dev \
|
||||||
libusb-1.0-0-dev \
|
libusb-1.0-0-dev \
|
||||||
|
libldap2-dev \
|
||||||
|
libsasl2-dev \
|
||||||
|
libdrm-dev \
|
||||||
|
mesa-va-drivers \
|
||||||
|
mesa-vdpau-drivers \
|
||||||
|
v4l-utils \
|
||||||
|
libv4l-dev \
|
||||||
|
ffmpeg \
|
||||||
|
libavcodec-dev \
|
||||||
|
libavformat-dev \
|
||||||
|
libavutil-dev \
|
||||||
|
libswscale-dev \
|
||||||
|
libavfilter-dev \
|
||||||
|
libavdevice-dev \
|
||||||
|
&& if [ ${TARGETARCH} != arm ] && [ ${TARGETARCH} != arm64 ]; then \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
vainfo \
|
||||||
|
libva-dev \
|
||||||
|
libva-drm2 \
|
||||||
|
libva-x11-2 \
|
||||||
|
intel-media-va-driver \
|
||||||
|
i965-va-driver; \
|
||||||
|
fi \
|
||||||
|
&& if [ ${TARGETARCH} = arm64 ]; then \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
ninja-build \
|
||||||
|
zlib1g-dev \
|
||||||
|
libswresample-dev; \
|
||||||
|
fi \
|
||||||
&& apt clean \
|
&& apt clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
@ -70,50 +99,104 @@ RUN --security=insecure pip config set global.index-url https://pypi.tuna.tsingh
|
|||||||
more-itertools multidict netifaces packaging passlib pillow ply psutil \
|
more-itertools multidict netifaces packaging passlib pillow ply psutil \
|
||||||
pycparser pyelftools pyghmi pygments pyparsing pyotp qrcode requests \
|
pycparser pyelftools pyghmi pygments pyparsing pyotp qrcode requests \
|
||||||
semantic-version setproctitle six spidev tabulate urllib3 wrapt xlib \
|
semantic-version setproctitle six spidev tabulate urllib3 wrapt xlib \
|
||||||
yarl pyserial pyyaml zstandard supervisor pyfatfs
|
yarl pyserial pyyaml zstandard supervisor pyfatfs pyserial python-periphery \
|
||||||
|
python-ldap python-pam pyrad pyudev pyusb luma.oled pyserial-asyncio \
|
||||||
|
&& rm -rf /root/.cache/pip/* /tmp/pip-* \
|
||||||
|
&& if [ ${TARGETARCH} = arm ]; then \
|
||||||
|
umount /root/.cargo 2>/dev/null || true \
|
||||||
|
&& rm -rf /root/.cargo /root/rustup-init.sh; \
|
||||||
|
fi
|
||||||
|
|
||||||
# 编译安装 libnice、libsrtp、libwebsockets 和 janus-gateway
|
# 编译 python evdev库
|
||||||
RUN git clone --depth=1 https://gitlab.freedesktop.org/libnice/libnice /tmp/libnice \
|
RUN git clone --depth=1 https://github.com/gvalkov/python-evdev.git /tmp/python-evdev \
|
||||||
|
&& cd /tmp/python-evdev \
|
||||||
|
&& python3 setup.py bdist_wheel --dist-dir /tmp/wheel/ \
|
||||||
|
&& rm -rf /tmp/python-evdev
|
||||||
|
|
||||||
|
# 编译安装 libnice、libsrtp、libwebsockets 和 janus-gateway(显式 Release 与按架构优化)
|
||||||
|
RUN export COMMON_CFLAGS='-O2 -pipe -fPIC -fstack-protector-strong -D_FORTIFY_SOURCE=2' \
|
||||||
|
&& if [ "${TARGETARCH}" = arm64 ]; then export CFLAGS="$COMMON_CFLAGS -march=armv8-a"; \
|
||||||
|
elif [ "${TARGETARCH}" = arm ]; then export CFLAGS="$COMMON_CFLAGS -march=armv7-a -mfpu=neon-vfpv4 -mfloat-abi=hard -mtune=cortex-a7"; \
|
||||||
|
else export CFLAGS="$COMMON_CFLAGS -march=x86-64 -mtune=generic"; fi \
|
||||||
|
&& export CXXFLAGS="$CFLAGS" LDFLAGS="-Wl,-O1 -Wl,--as-needed" \
|
||||||
|
&& git clone --depth=1 https://gitlab.freedesktop.org/libnice/libnice /tmp/libnice \
|
||||||
&& cd /tmp/libnice \
|
&& cd /tmp/libnice \
|
||||||
&& meson --prefix=/usr build && ninja -C build && ninja -C build install \
|
&& meson setup build --prefix=/usr --buildtype=release -Doptimization=2 -Dc_args="$CFLAGS" -Dcpp_args="$CXXFLAGS" \
|
||||||
|
&& ninja -C build && ninja -C build install \
|
||||||
&& rm -rf /tmp/libnice \
|
&& rm -rf /tmp/libnice \
|
||||||
&& curl https://github.com/cisco/libsrtp/archive/v2.2.0.tar.gz -L -o /tmp/libsrtp-2.2.0.tar.gz \
|
&& curl https://github.com/cisco/libsrtp/archive/v2.2.0.tar.gz -L -o /tmp/libsrtp-2.2.0.tar.gz \
|
||||||
&& cd /tmp \
|
&& cd /tmp \
|
||||||
&& tar xf libsrtp-2.2.0.tar.gz \
|
&& tar xf libsrtp-2.2.0.tar.gz \
|
||||||
&& cd libsrtp-2.2.0 \
|
&& cd libsrtp-2.2.0 \
|
||||||
&& ./configure --prefix=/usr --enable-openssl \
|
&& CFLAGS="$CFLAGS" CXXFLAGS="$CXXFLAGS" ./configure --prefix=/usr --enable-openssl \
|
||||||
&& make shared_library && make install \
|
&& make shared_library -j$(nproc) && make install \
|
||||||
&& cd /tmp \
|
&& cd /tmp \
|
||||||
&& rm -rf /tmp/libsrtp* \
|
&& rm -rf /tmp/libsrtp* \
|
||||||
&& git clone --depth=1 https://libwebsockets.org/repo/libwebsockets /tmp/libwebsockets \
|
&& git clone --depth=1 https://github.com/warmcat/libwebsockets /tmp/libwebsockets \
|
||||||
&& cd /tmp/libwebsockets \
|
&& cd /tmp/libwebsockets \
|
||||||
&& mkdir build && cd build \
|
&& mkdir build && cd build \
|
||||||
&& cmake -DLWS_MAX_SMP=1 -DLWS_WITHOUT_EXTENSIONS=0 -DCMAKE_INSTALL_PREFIX:PATH=/usr -DCMAKE_C_FLAGS="-fpic" .. \
|
&& cmake -DLWS_MAX_SMP=1 -DLWS_WITHOUT_EXTENSIONS=0 -DCMAKE_INSTALL_PREFIX:PATH=/usr \
|
||||||
&& make && make install \
|
-DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS_RELEASE="$CFLAGS -fPIC" -DCMAKE_CXX_FLAGS_RELEASE="$CXXFLAGS -fPIC" .. \
|
||||||
|
&& make -j$(nproc) && make install \
|
||||||
&& cd /tmp \
|
&& cd /tmp \
|
||||||
&& rm -rf /tmp/libwebsockets \
|
&& rm -rf /tmp/libwebsockets \
|
||||||
&& git clone --depth=1 https://github.com/meetecho/janus-gateway.git /tmp/janus-gateway \
|
&& git clone --depth=1 https://github.com/meetecho/janus-gateway.git /tmp/janus-gateway \
|
||||||
&& cd /tmp/janus-gateway \
|
&& cd /tmp/janus-gateway \
|
||||||
&& sh autogen.sh \
|
&& sh autogen.sh \
|
||||||
&& ./configure --enable-static --enable-websockets --enable-plugin-audiobridge \
|
&& CFLAGS="$CFLAGS" CXXFLAGS="$CXXFLAGS" ./configure --enable-static --enable-websockets --enable-plugin-audiobridge \
|
||||||
--disable-data-channels --disable-rabbitmq --disable-mqtt --disable-all-plugins \
|
--disable-data-channels --disable-rabbitmq --disable-mqtt --disable-all-plugins \
|
||||||
--disable-all-loggers --prefix=/usr \
|
--disable-all-loggers --prefix=/usr \
|
||||||
&& make && make install \
|
&& make -j$(nproc) && make install \
|
||||||
&& cd /tmp \
|
&& cd /tmp \
|
||||||
&& rm -rf /tmp/janus-gateway
|
&& rm -rf /tmp/janus-gateway
|
||||||
|
|
||||||
# 编译 ustreamer
|
# 编译 Rockchip MPP、RGA(仅 arm64,显式 Release 与按架构优化)
|
||||||
|
RUN if [ ${TARGETARCH} = arm64 ]; then \
|
||||||
|
export COMMON_CFLAGS='-O2 -pipe -fPIC -fstack-protector-strong -D_FORTIFY_SOURCE=2' \
|
||||||
|
&& export CFLAGS="$COMMON_CFLAGS -march=armv8-a" \
|
||||||
|
&& export CXXFLAGS="$CFLAGS" \
|
||||||
|
&& git clone --depth=1 https://github.com/rockchip-linux/mpp.git /tmp/rkmpp \
|
||||||
|
&& mkdir -p /tmp/rkmpp/rkmpp_build && cd /tmp/rkmpp/rkmpp_build \
|
||||||
|
&& cmake -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DBUILD_TEST=OFF \
|
||||||
|
-DCMAKE_C_FLAGS_RELEASE="$CFLAGS" -DCMAKE_CXX_FLAGS_RELEASE="$CXXFLAGS" .. \
|
||||||
|
&& make -j$(nproc) \
|
||||||
|
&& make install \
|
||||||
|
&& git clone -b jellyfin-rga --depth=1 https://github.com/nyanmisaka/rk-mirrors.git /tmp/rkrga \
|
||||||
|
&& cd /tmp/ \
|
||||||
|
&& meson setup rkrga rkrga_build --prefix=/usr --libdir=lib --buildtype=release -Doptimization=2 \
|
||||||
|
-Dc_args="$CFLAGS" -Dcpp_args="$CXXFLAGS -fpermissive" -Dlibdrm=false -Dlibrga_demo=false \
|
||||||
|
&& meson configure rkrga_build > /dev/null \
|
||||||
|
&& ninja -C rkrga_build install \
|
||||||
|
&& rm -rf /tmp/rkmpp /tmp/rkrga; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 编译 ustreamer(按架构优化)
|
||||||
RUN sed --in-place --expression 's|^#include "refcount.h"$|#include "../refcount.h"|g' /usr/include/janus/plugins/plugin.h \
|
RUN sed --in-place --expression 's|^#include "refcount.h"$|#include "../refcount.h"|g' /usr/include/janus/plugins/plugin.h \
|
||||||
&& git clone --depth=1 https://github.com/mofeng-git/ustreamer /tmp/ustreamer \
|
&& git clone --depth=1 https://github.com/mofeng-git/ustreamer /tmp/ustreamer \
|
||||||
&& sed -i '68s/-Wl,-Bstatic//' /tmp/ustreamer/src/Makefile \
|
&& export COMMON_CFLAGS='-O2 -pipe -fPIC -fstack-protector-strong -D_FORTIFY_SOURCE=2' \
|
||||||
&& make -j WITH_PYTHON=1 WITH_JANUS=1 WITH_LIBX264=1 -C /tmp/ustreamer \
|
&& if [ "${TARGETARCH}" = arm64 ]; then export CFLAGS="$COMMON_CFLAGS -march=armv8-a"; \
|
||||||
|
elif [ "${TARGETARCH}" = arm ]; then export CFLAGS="$COMMON_CFLAGS -march=armv7-a -mfpu=neon-vfpv4 -mfloat-abi=hard -mtune=cortex-a7"; \
|
||||||
|
else export CFLAGS="$COMMON_CFLAGS -march=x86-64 -mtune=generic"; fi \
|
||||||
|
&& export CXXFLAGS="$CFLAGS" \
|
||||||
|
&& if [ ${TARGETARCH} = arm64 ]; then \
|
||||||
|
make -j$(nproc) CFLAGS="$CFLAGS" WITH_PYTHON=1 WITH_JANUS=1 WITH_FFMPEG=1 WITH_MPP=1 WITH_DRM=1 -C /tmp/ustreamer; \
|
||||||
|
else \
|
||||||
|
make -j$(nproc) CFLAGS="$CFLAGS" WITH_PYTHON=1 WITH_JANUS=1 WITH_FFMPEG=1 WITH_DRM=1 -C /tmp/ustreamer; \
|
||||||
|
fi \
|
||||||
&& /tmp/ustreamer/ustreamer -v \
|
&& /tmp/ustreamer/ustreamer -v \
|
||||||
|
&& /tmp/ustreamer/ustreamer-dump -v \
|
||||||
&& cp /tmp/ustreamer/python/dist/*.whl /tmp/wheel/
|
&& cp /tmp/ustreamer/python/dist/*.whl /tmp/wheel/
|
||||||
|
|
||||||
# 复制必要的库文件
|
# 复制必要的库文件
|
||||||
RUN mkdir /tmp/lib \
|
RUN mkdir /tmp/lib \
|
||||||
&& cd /lib/*-linux-*/ \
|
&& cd /lib/*-linux-*/ \
|
||||||
&& cp libevent_core-*.so.7 libbsd.so.0 libevent_pthreads-*.so.7 libspeexdsp.so.1 \
|
&& cp libevent_core-*.so.* libbsd.so.* libevent_pthreads-*.so.* libspeexdsp.so.* \
|
||||||
libevent-*.so.7 libjpeg.so.62 libx264.so.164 libyuv.so.0 libnice.so.10 \
|
libevent-*.so.* libjpeg.so.* libyuv.so.* libnice.so.* \
|
||||||
/usr/lib/libsrtp2.so.1 /usr/lib/libwebsockets.so.19 \
|
/tmp/lib/ \
|
||||||
/tmp/lib/
|
&& find /usr/lib -name "libsrtp2.so.*" -exec cp {} /tmp/lib/ \; \
|
||||||
|
&& find /usr/lib -name "libwebsockets.so.*" -exec cp {} /tmp/lib/ \; \
|
||||||
|
&& [ "${TARGETARCH}" = "arm64" ] && \
|
||||||
|
find /usr/lib -name "libsw*.so.*" -exec cp {} /tmp/lib/ \; && \
|
||||||
|
find /usr/lib -name "libpostproc.so.*" -exec cp {} /tmp/lib/ \; && \
|
||||||
|
find /usr/lib -name "librockchip*" -exec cp {} /tmp/lib/ \; && \
|
||||||
|
find /usr/lib -name "librga.so.*" -exec cp {} /tmp/lib/ \; || true
|
||||||
|
|||||||
538
build/build_img.sh
Normal file → Executable file
538
build/build_img.sh
Normal file → Executable file
@ -1,368 +1,208 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# ========================================================================== #
|
|
||||||
# #
|
|
||||||
# KVMD - The main PiKVM daemon. #
|
|
||||||
# #
|
|
||||||
# Copyright (C) 2023-2025 SilentWind <mofeng654321@hotmail.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/>. #
|
|
||||||
# #
|
|
||||||
# ========================================================================== #
|
|
||||||
|
|
||||||
SRCPATH=/mnt/nas/src
|
# --- 配置 ---
|
||||||
BOOTFS=/tmp/bootfs
|
# 允许通过环境变量覆盖默认路径
|
||||||
ROOTFS=/tmp/rootfs
|
SRCPATH="${SRCPATH:-/mnt/src}"
|
||||||
OUTPUTDIR=/mnt/nas/src/output
|
BOOTFS="${BOOTFS:-/tmp/bootfs}"
|
||||||
LOOPDEV=/dev/loop10
|
ROOTFS="${ROOTFS:-/tmp/rootfs}"
|
||||||
DATE=240303
|
OUTPUTDIR="${OUTPUTDIR:-/mnt/output}"
|
||||||
|
TMPDIR="${TMPDIR:-$SRCPATH/tmp}"
|
||||||
|
|
||||||
|
# 远程文件下载配置
|
||||||
|
REMOTE_PREFIX="${REMOTE_PREFIX:-https://files.mofeng.run/src}"
|
||||||
|
|
||||||
export LC_ALL=C
|
export LC_ALL=C
|
||||||
|
|
||||||
write_meta() {
|
# 全局变量
|
||||||
sudo chroot --userspec "root:root" $ROOTFS bash -c "sed -i 's/localhost.localdomain/$1/g' /etc/kvmd/meta.yaml"
|
LOOPDEV=""
|
||||||
}
|
ROOTFS_MOUNTED=0
|
||||||
|
BOOTFS_MOUNTED=0
|
||||||
|
PROC_MOUNTED=0
|
||||||
|
SYS_MOUNTED=0
|
||||||
|
DEV_MOUNTED=0
|
||||||
|
DOCKER_CONTAINER_NAME="to_build_rootfs_$$"
|
||||||
|
PREBUILT_DIR="/tmp/prebuilt_binaries"
|
||||||
|
|
||||||
mount_rootfs() {
|
# --- 引入模块化脚本 ---
|
||||||
mkdir $ROOTFS $SRCPATH/tmp/rootfs
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
sudo mount $LOOPDEV $ROOTFS || exit -1
|
source "$SCRIPT_DIR/functions/common.sh"
|
||||||
sudo mount -t proc proc $ROOTFS/proc || exit -1
|
source "$SCRIPT_DIR/functions/devices.sh"
|
||||||
sudo mount -t sysfs sys $ROOTFS/sys || exit -1
|
source "$SCRIPT_DIR/functions/install.sh"
|
||||||
sudo mount -o bind /dev $ROOTFS/dev || exit -1
|
source "$SCRIPT_DIR/functions/packaging.sh"
|
||||||
}
|
|
||||||
|
|
||||||
umount_rootfs() {
|
# 获取日期与Git版本
|
||||||
sudo umount $ROOTFS/sys
|
GIT_COMMIT_ID=$(get_git_commit_id)
|
||||||
sudo umount $ROOTFS/dev
|
DATE=$(date +%y%m%d)
|
||||||
sudo umount $ROOTFS/proc
|
if [ -n "$GIT_COMMIT_ID" ]; then
|
||||||
sudo umount $ROOTFS
|
DATE="${DATE}-${GIT_COMMIT_ID}"
|
||||||
sudo zerofree $LOOPDEV
|
fi
|
||||||
sudo losetup -d $LOOPDEV
|
|
||||||
sudo docker rm to_build_rootfs
|
|
||||||
sudo rm -rf $SRCPATH/tmp/rootfs/*
|
|
||||||
}
|
|
||||||
|
|
||||||
parpare_dns() {
|
# --- 注册清理函数 ---
|
||||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
# 在脚本退出、收到错误信号、中断信号、终止信号时执行 cleanup
|
||||||
mkdir -p /run/systemd/resolve/ \
|
trap cleanup EXIT ERR INT TERM
|
||||||
&& touch /run/systemd/resolve/stub-resolv.conf \
|
|
||||||
&& printf '%s\n' 'nameserver 1.1.1.1' 'nameserver 1.0.0.1' > /etc/resolv.conf \
|
|
||||||
&& bash <(curl -sSL https://gitee.com/SuperManito/LinuxMirrors/raw/main/ChangeMirrors.sh) \
|
|
||||||
--source mirrors.tuna.tsinghua.edu.cn --updata-software false --web-protocol http "
|
|
||||||
}
|
|
||||||
|
|
||||||
delete_armbain_verify(){
|
# --- 构建流程函数 ---
|
||||||
sudo chroot --userspec "root:root" $ROOTFS bash -c "echo 'deb http://mirrors.ustc.edu.cn/armbian bullseye main bullseye-utils bullseye-desktop' > /etc/apt/sources.list.d/armbian.list "
|
|
||||||
}
|
|
||||||
|
|
||||||
config_file() {
|
build_target() {
|
||||||
|
local target="$1"
|
||||||
sudo mkdir -p $ROOTFS/etc/kvmd/override.d $ROOTFS/etc/kvmd/vnc $ROOTFS/var/lib/kvmd/msd $ROOTFS/opt/vc/bin $ROOTFS/usr/share/kvmd $ROOTFS/One-KVM \
|
local build_time=$(date "+%Y-%m-%d %H:%M:%S")
|
||||||
$ROOTFS/usr/share/janus/javascript $ROOTFS/usr/lib/ustreamer/janus $ROOTFS/run/kvmd $ROOTFS/var/lib/kvmd/msd/images $ROOTFS/var/lib/kvmd/msd/meta \
|
echo "=================================================="
|
||||||
$ROOTFS/tmp/wheel/ $ROOTFS/usr/lib/janus/transports/ $ROOTFS/usr/lib/janus/loggers
|
echo "信息:构建目标: $target"
|
||||||
sudo rsync -a --exclude={src,.github} . $ROOTFS/One-KVM
|
echo "信息:构建时间: $build_time"
|
||||||
sudo cp -r configs/kvmd/* configs/nginx configs/janus $ROOTFS/etc/kvmd
|
echo "=================================================="
|
||||||
sudo cp -r web extras contrib/keymaps $ROOTFS/usr/share/kvmd
|
|
||||||
sudo cp testenv/fakes/vcgencmd $ROOTFS/usr/bin/
|
|
||||||
sudo cp -r testenv/js/* $ROOTFS/usr/share/janus/javascript/
|
|
||||||
sudo cp build/platform/$1 $ROOTFS/usr/share/kvmd/platform
|
|
||||||
if [ -f "$SRCPATH/image/$1/rc.local" ]; then
|
|
||||||
sudo cp $SRCPATH/image/$1/rc.local $ROOTFS/etc/
|
|
||||||
fi
|
|
||||||
|
|
||||||
sudo docker pull --platform linux/$2 registry.cn-hangzhou.aliyuncs.com/silentwind/kvmd-stage-0
|
# 设置全局变量,供后续函数使用
|
||||||
sudo docker create --name to_build_rootfs registry.cn-hangzhou.aliyuncs.com/silentwind/kvmd-stage-0
|
TARGET_DEVICE_NAME="$target"
|
||||||
sudo docker export to_build_rootfs | sudo tar -xvf - -C $SRCPATH/tmp/rootfs
|
NEED_PREPARE_DNS=false # 默认不需要准备 DNS
|
||||||
sudo cp $SRCPATH/tmp/rootfs/tmp/lib/* $ROOTFS/lib/*-linux-*/
|
|
||||||
sudo cp $SRCPATH/tmp/rootfs/tmp/ustreamer/ustreamer $SRCPATH/tmp/rootfs/tmp/ustreamer/ustreamer-dump $SRCPATH/tmp/rootfs/usr/bin/janus $ROOTFS/usr/bin/
|
|
||||||
sudo cp $SRCPATH/tmp/rootfs/tmp/ustreamer/janus/libjanus_ustreamer.so $ROOTFS/usr/lib/ustreamer/janus/
|
|
||||||
sudo cp $SRCPATH/tmp/rootfs/tmp/wheel/*.whl $ROOTFS/tmp/wheel/
|
|
||||||
sudo cp $SRCPATH/tmp/rootfs/usr/lib/janus/transports/* $ROOTFS/usr/lib/janus/transports/
|
|
||||||
|
|
||||||
sudo mv $ROOTFS/etc/apt/apt.conf.d/50apt-file.conf{,.disabled}
|
case "$target" in
|
||||||
}
|
onecloud)
|
||||||
|
onecloud_rootfs
|
||||||
pack_img() {
|
local arch="armhf"
|
||||||
sudo mv $SRCPATH/tmp/rootfs.img $OUTPUTDIR/One-KVM_by-SilentWind_$1_$DATE.img
|
local device_type="gpio-onecloud"
|
||||||
if [ "$1" = "Vm" ]; then
|
local network_type="systemd-networkd"
|
||||||
sudo qemu-img convert -f raw -O vmdk $OUTPUTDIR/One-KVM_by-SilentWind_Vm_$DATE.img $OUTPUTDIR/One-KVM_by-SilentWind_Vmare-uefi_$DATE.vmdk
|
|
||||||
sudo qemu-img convert -f raw -O vdi $OUTPUTDIR/One-KVM_by-SilentWind_Vm_$DATE.img $OUTPUTDIR/One-KVM_by-SilentWind_Virtualbox-uefi_$DATE.vdi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
onecloud_rootfs() {
|
|
||||||
$SRCPATH/image/onecloud/AmlImg_v0.3.1_linux_amd64 unpack $SRCPATH/image/onecloud/Armbian_by-SilentWind_24.5.0-trunk_Onecloud_bookworm_legacy_5.9.0-rc7_minimal.burn.img $SRCPATH/tmp
|
|
||||||
simg2img $SRCPATH/tmp/6.boot.PARTITION.sparse $SRCPATH/tmp/bootfs.img
|
|
||||||
simg2img $SRCPATH/tmp/7.rootfs.PARTITION.sparse $SRCPATH/tmp/rootfs.img
|
|
||||||
mkdir $BOOTFS
|
|
||||||
sudo losetup $LOOPDEV $SRCPATH/tmp/bootfs.img || exit -1
|
|
||||||
sudo mount $LOOPDEV $BOOTFS
|
|
||||||
sudo cp $SRCPATH/image/onecloud/meson8b-onecloud-fix.dtb $BOOTFS/dtb/meson8b-onecloud.dtb
|
|
||||||
sudo umount $BOOTFS
|
|
||||||
sudo losetup -d $LOOPDEV
|
|
||||||
dd if=/dev/zero of=/tmp/add.img bs=1M count=256 && cat /tmp/add.img >> $SRCPATH/tmp/rootfs.img && rm /tmp/add.img
|
|
||||||
e2fsck -f $SRCPATH/tmp/rootfs.img && resize2fs $SRCPATH/tmp/rootfs.img
|
|
||||||
sudo losetup $LOOPDEV $SRCPATH/tmp/rootfs.img
|
|
||||||
}
|
|
||||||
|
|
||||||
cumebox2_rootfs() {
|
|
||||||
cp $SRCPATH/image/cumebox2/Armbian_24.8.1_Khadas-vim1_bookworm_current_6.6.47_minimal.img $SRCPATH/tmp/rootfs.img
|
|
||||||
dd if=/dev/zero of=/tmp/add.img bs=1M count=1500 && cat /tmp/add.img >> $SRCPATH/tmp/rootfs.img && rm /tmp/add.img
|
|
||||||
sudo parted -s $SRCPATH/tmp/rootfs.img resizepart 1 100% || exit -1
|
|
||||||
sudo losetup --offset $((8192*512)) $LOOPDEV $SRCPATH/tmp/rootfs.img || exit -1
|
|
||||||
sudo e2fsck -f $LOOPDEV && sudo resize2fs $LOOPDEV
|
|
||||||
}
|
|
||||||
|
|
||||||
chainedbox_rootfs_and_fix_dtb() {
|
|
||||||
cp $SRCPATH/image/chainedbox/Armbian_24.11.0_rockchip_chainedbox_bookworm_6.1.112_server_2024.10.02_add800m.img $SRCPATH/tmp/rootfs.img
|
|
||||||
mkdir $BOOTFS
|
|
||||||
sudo losetup --offset $((32768*512)) $LOOPDEV $SRCPATH/tmp/rootfs.img || exit -1
|
|
||||||
sudo mount $LOOPDEV $BOOTFS
|
|
||||||
sudo cp $SRCPATH/image/chainedbox/rk3328-l1pro-1296mhz-fix.dtb $BOOTFS/dtb/rockchip/rk3328-l1pro-1296mhz.dtb
|
|
||||||
sudo umount $BOOTFS
|
|
||||||
sudo losetup -d $LOOPDEV
|
|
||||||
sudo losetup --offset $((1081344*512)) $LOOPDEV $SRCPATH/tmp/rootfs.img
|
|
||||||
}
|
|
||||||
|
|
||||||
vm_rootfs() {
|
|
||||||
cp $SRCPATH/image/vm/Armbian_24.8.1_Uefi-x86_bookworm_current_6.6.47_minimal_add1g.img $SRCPATH/tmp/rootfs.img
|
|
||||||
sudo losetup --offset $((540672*512)) $LOOPDEV $SRCPATH/tmp/rootfs.img || exit -1
|
|
||||||
}
|
|
||||||
|
|
||||||
e900v22c_rootfs() {
|
|
||||||
cp $SRCPATH/image/e900v22c/Armbian_23.08.0_amlogic_s905l3a_bookworm_5.15.123_server_2023.08.01.img $SRCPATH/tmp/rootfs.img
|
|
||||||
dd if=/dev/zero of=/tmp/add.img bs=1M count=400 && cat /tmp/add.img >> $SRCPATH/tmp/rootfs.img && rm /tmp/add.img
|
|
||||||
sudo parted -s $SRCPATH/tmp/rootfs.img resizepart 2 100% || exit -1
|
|
||||||
sudo losetup --offset $((532480*512)) $LOOPDEV $SRCPATH/tmp/rootfs.img || exit -1
|
|
||||||
sudo e2fsck -f $LOOPDEV && sudo resize2fs $LOOPDEV
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
octopus-flanet_rootfs() {
|
|
||||||
cp $SRCPATH/image/octopus-flanet/Armbian_24.11.0_amlogic_s912_bookworm_6.1.114_server_2024.11.01.img $SRCPATH/tmp/rootfs.img
|
|
||||||
mkdir $BOOTFS
|
|
||||||
sudo losetup --offset $((8192*512)) $LOOPDEV $SRCPATH/tmp/rootfs.img || exit -1
|
|
||||||
sudo mount $LOOPDEV $BOOTFS
|
|
||||||
sudo sed -i "s/meson-gxm-octopus-planet.dtb/meson-gxm-khadas-vim2.dtb/g" $BOOTFS/uEnv.txt
|
|
||||||
sudo umount $BOOTFS
|
|
||||||
sudo losetup -d $LOOPDEV
|
|
||||||
dd if=/dev/zero of=/tmp/add.img bs=1M count=400 && cat /tmp/add.img >> $SRCPATH/tmp/rootfs.img && rm /tmp/add.img
|
|
||||||
sudo parted -s $SRCPATH/tmp/rootfs.img resizepart 2 100% || exit -1
|
|
||||||
sudo losetup --offset $((1056768*512)) $LOOPDEV $SRCPATH/tmp/rootfs.img || exit -1
|
|
||||||
sudo e2fsck -f $LOOPDEV && sudo resize2fs $LOOPDEV
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
config_cumebox2_file() {
|
|
||||||
sudo mkdir $ROOTFS/etc/oled
|
|
||||||
sudo cp $SRCPATH/image/cumebox2/v-fix.dtb $ROOTFS/boot/dtb/amlogic/meson-gxl-s905x-khadas-vim.dtb
|
|
||||||
sudo cp $SRCPATH/image/cumebox2/ssd $ROOTFS/usr/bin/
|
|
||||||
sudo cp $SRCPATH/image/cumebox2/config.json $ROOTFS/etc/oled/config.json
|
|
||||||
}
|
|
||||||
|
|
||||||
config_octopus-flanet_file() {
|
|
||||||
sudo cp $SRCPATH/image/octopus-flanet/model_database.conf $ROOTFS/etc/model_database.conf
|
|
||||||
}
|
|
||||||
|
|
||||||
instal_one-kvm() {
|
|
||||||
#$1 arch; $2 deivce: "gpio" or "video1"; $3 network: "systemd-networkd",default is network-manager
|
|
||||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
|
||||||
df -h \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt install -y --no-install-recommends libxkbcommon-x11-0 nginx tesseract-ocr tesseract-ocr-eng tesseract-ocr-chi-sim iptables \
|
|
||||||
curl kmod libmicrohttpd12 libjansson4 libssl3 libsofia-sip-ua0 libglib2.0-0 libopus0 libogg0 libcurl4 libconfig9 python3-pip \
|
|
||||||
&& apt clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* "
|
|
||||||
|
|
||||||
if [ "$3" = "systemd-networkd" ]; then
|
|
||||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
|
||||||
echo -e '[Match]\nName=eth0\n\n[Network]\nDHCP=yes\n\n[Link]\nMACAddress=B6:AE:B3:21:42:0C' > /etc/systemd/network/99-eth0.network \
|
|
||||||
&& systemctl mask NetworkManager \
|
|
||||||
&& systemctl unmask systemd-networkd \
|
|
||||||
&& systemctl enable systemd-networkd systemd-resolved "
|
|
||||||
fi
|
|
||||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
|
||||||
pip3 install --no-cache-dir --break-system-packages /tmp/wheel/*.whl \
|
|
||||||
&& pip3 cache purge \
|
|
||||||
&& rm -r /tmp/wheel "
|
|
||||||
|
|
||||||
#pip3 install --target=/usr/lib/python3/dist-packages --break-system-packages pyfatfs -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
|
|
||||||
|
|
||||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
|
||||||
cd /One-KVM \
|
|
||||||
&& python3 setup.py install \
|
|
||||||
&& bash scripts/kvmd-gencert --do-the-thing \
|
|
||||||
&& bash scripts/kvmd-gencert --do-the-thing --vnc \
|
|
||||||
&& kvmd-nginx-mkconf /etc/kvmd/nginx/nginx.conf.mako /etc/kvmd/nginx/nginx.conf \
|
|
||||||
&& kvmd -m "
|
|
||||||
|
|
||||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
|
||||||
cat /One-KVM/configs/os/sudoers/v2-hdmiusb >> /etc/sudoers \
|
|
||||||
&& cat /One-KVM/configs/os/udev/v2-hdmiusb-rpi4.rules > /etc/udev/rules.d/99-kvmd.rules \
|
|
||||||
&& echo 'libcomposite' >> /etc/modules \
|
|
||||||
&& mv /usr/local/bin/kvmd* /usr/bin \
|
|
||||||
&& cp /One-KVM/configs/os/services/* /etc/systemd/system/ \
|
|
||||||
&& cp /One-KVM/configs/os/tmpfiles.conf /usr/lib/tmpfiles.d/ \
|
|
||||||
&& mv /etc/kvmd/supervisord.conf /etc/supervisord.conf \
|
|
||||||
&& chmod +x /etc/update-motd.d/* \
|
|
||||||
&& echo 'kvmd ALL=(ALL) NOPASSWD: /etc/kvmd/custom_atx/gpio.sh' >> /etc/sudoers \
|
|
||||||
&& echo 'kvmd ALL=(ALL) NOPASSWD: /etc/kvmd/custom_atx/usbrelay_hid.sh' >> /etc/sudoers \
|
|
||||||
&& systemd-sysusers /One-KVM/configs/os/sysusers.conf \
|
|
||||||
&& systemd-sysusers /One-KVM/configs/os/kvmd-webterm.conf \
|
|
||||||
&& ln -sf /usr/share/tesseract-ocr/*/tessdata /usr/share/tessdata \
|
|
||||||
&& sed -i 's/8080/80/g' /etc/kvmd/override.yaml \
|
|
||||||
&& sed -i 's/4430/443/g' /etc/kvmd/override.yaml \
|
|
||||||
&& chown kvmd -R /var/lib/kvmd/msd/ \
|
|
||||||
&& systemctl enable kvmd kvmd-otg kvmd-nginx kvmd-vnc kvmd-ipmi kvmd-webterm kvmd-janus kvmd-media \
|
|
||||||
&& systemctl disable nginx \
|
|
||||||
&& rm -r /One-KVM "
|
|
||||||
|
|
||||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
|
||||||
curl https://gh.llkk.cc/https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.$1 -L -o /usr/bin/ttyd \
|
|
||||||
&& chmod +x /usr/bin/ttyd \
|
|
||||||
&& mkdir -p /home/kvmd-webterm \
|
|
||||||
&& chown kvmd-webterm /home/kvmd-webterm "
|
|
||||||
|
|
||||||
if [ "$1" = "x86_64" ]; then
|
|
||||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
|
||||||
systemctl disable kvmd-otg \
|
|
||||||
&& sed -i '2c ATX=USBRELAY_HID' /etc/kvmd/atx.sh \
|
|
||||||
&& sed -i 's/device: \/dev\/ttyUSB0/device: \/dev\/kvmd-hid/g' /etc/kvmd/override.yaml "
|
|
||||||
else
|
|
||||||
if [ "$2" = "gpio" ]; then
|
|
||||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
|
||||||
sed -i '2c ATX=GPIO' /etc/kvmd/atx.sh \
|
|
||||||
&& sed -i 's/SHUTDOWNPIN/gpiochip1 7/g' /etc/kvmd/custom_atx/gpio.sh \
|
|
||||||
&& sed -i 's/REBOOTPIN/gpiochip0 11/g' /etc/kvmd/custom_atx/gpio.sh "
|
|
||||||
else
|
|
||||||
sudo chroot --userspec "root:root" $ROOTFS sed -i '2c ATX=USBRELAY_HID' /etc/kvmd/atx.sh
|
|
||||||
|
|
||||||
fi
|
|
||||||
if [ "$2" = "video1" ]; then
|
|
||||||
sudo chroot --userspec "root:root" $ROOTFS sed -i 's/\/dev\/video0/\/dev\/video1/g' /etc/kvmd/override.yaml
|
|
||||||
fi
|
|
||||||
sudo chroot --userspec "root:root" $ROOTFS bash -c " \
|
|
||||||
sed -i 's/ch9329/otg/g' /etc/kvmd/override.yaml \
|
|
||||||
&& sed -i 's/device: \/dev\/ttyUSB0//g' /etc/kvmd/override.yaml \
|
|
||||||
&& sed -i 's/#type: otg/type: otg/g' /etc/kvmd/override.yaml "
|
|
||||||
fi
|
|
||||||
|
|
||||||
sudo chroot --userspec "root:root" $ROOTFS bash -c "df -h"
|
|
||||||
}
|
|
||||||
|
|
||||||
pack_img_onecloud() {
|
|
||||||
sudo rm $SRCPATH/tmp/7.rootfs.PARTITION.sparse
|
|
||||||
sudo img2simg $SRCPATH/tmp/rootfs.img $SRCPATH/tmp/7.rootfs.PARTITION.sparse
|
|
||||||
sudo $SRCPATH/image/onecloud/AmlImg_v0.3.1_linux_amd64 pack $OUTPUTDIR/One-KVM_by-SilentWind_Onecloud_$DATE.burn.img $SRCPATH/tmp/
|
|
||||||
sudo rm $SRCPATH/tmp/*
|
|
||||||
}
|
|
||||||
|
|
||||||
#build function
|
|
||||||
|
|
||||||
onecloud() {
|
|
||||||
onecloud_rootfs
|
|
||||||
mount_rootfs
|
|
||||||
config_file "onecloud" "arm"
|
|
||||||
instal_one-kvm armhf gpio systemd-networkd
|
|
||||||
write_meta "onecloud"
|
|
||||||
umount_rootfs
|
|
||||||
pack_img_onecloud
|
|
||||||
}
|
|
||||||
|
|
||||||
cumebox2() {
|
|
||||||
cumebox2_rootfs
|
|
||||||
mount_rootfs
|
|
||||||
config_file "cumebox2" "aarch64"
|
|
||||||
config_cumebox2_file
|
|
||||||
parpare_dns
|
|
||||||
instal_one-kvm aarch64 video1
|
|
||||||
write_meta "cumebox2"
|
|
||||||
umount_rootfs
|
|
||||||
pack_img "Cumebox2"
|
|
||||||
}
|
|
||||||
|
|
||||||
chainedbox() {
|
|
||||||
chainedbox_rootfs_and_fix_dtb
|
|
||||||
mount_rootfs
|
|
||||||
config_file "chainedbox" "aarch64"
|
|
||||||
parpare_dns
|
|
||||||
instal_one-kvm aarch64 video1
|
|
||||||
write_meta "chainedbox"
|
|
||||||
umount_rootfs
|
|
||||||
pack_img "Chainedbox"
|
|
||||||
}
|
|
||||||
|
|
||||||
vm() {
|
|
||||||
vm_rootfs
|
|
||||||
mount_rootfs
|
|
||||||
config_file "vm" "amd64"
|
|
||||||
parpare_dns
|
|
||||||
instal_one-kvm x86_64
|
|
||||||
write_meta "vm"
|
|
||||||
umount_rootfs
|
|
||||||
pack_img "Vm"
|
|
||||||
}
|
|
||||||
|
|
||||||
e900v22c() {
|
|
||||||
e900v22c_rootfs
|
|
||||||
mount_rootfs
|
|
||||||
config_file "e900v22c" "aarch64"
|
|
||||||
instal_one-kvm aarch64 video1
|
|
||||||
write_meta "e900v22c"
|
|
||||||
umount_rootfs
|
|
||||||
pack_img "E900v22c"
|
|
||||||
}
|
|
||||||
|
|
||||||
octopus_flanet() {
|
|
||||||
octopus-flanet_rootfs
|
|
||||||
mount_rootfs
|
|
||||||
config_file "octopus-flanet" "aarch64"
|
|
||||||
config_octopus-flanet_file
|
|
||||||
parpare_dns
|
|
||||||
instal_one-kvm aarch64 video1
|
|
||||||
write_meta "octopus-flanet"
|
|
||||||
umount_rootfs
|
|
||||||
pack_img "Octopus-Flanet"
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ "$1" = "all" ]; then
|
|
||||||
onecloud
|
|
||||||
cumebox2
|
|
||||||
chainedbox
|
|
||||||
vm
|
|
||||||
e900v22c
|
|
||||||
octopus_flanet
|
|
||||||
else
|
|
||||||
case $1 in
|
|
||||||
onecloud)
|
|
||||||
onecloud
|
|
||||||
;;
|
;;
|
||||||
cumebox2)
|
cumebox2)
|
||||||
cumebox2
|
cumebox2_rootfs
|
||||||
|
local arch="aarch64"
|
||||||
|
local device_type="video1"
|
||||||
|
local network_type="" # 默认 NetworkManager
|
||||||
|
NEED_PREPARE_DNS=true
|
||||||
;;
|
;;
|
||||||
chainedbox)
|
chainedbox)
|
||||||
chainedbox
|
chainedbox_rootfs_and_fix_dtb
|
||||||
|
local arch="aarch64"
|
||||||
|
local device_type="video1"
|
||||||
|
local network_type=""
|
||||||
|
NEED_PREPARE_DNS=true
|
||||||
;;
|
;;
|
||||||
vm)
|
vm)
|
||||||
vm
|
vm_rootfs
|
||||||
|
local arch="amd64"
|
||||||
|
local device_type=""
|
||||||
|
local network_type=""
|
||||||
|
NEED_PREPARE_DNS=true
|
||||||
;;
|
;;
|
||||||
e900v22c)
|
e900v22c)
|
||||||
e900v22c
|
e900v22c_rootfs
|
||||||
|
local arch="aarch64"
|
||||||
|
local device_type="video1"
|
||||||
|
local network_type=""
|
||||||
|
NEED_PREPARE_DNS=true
|
||||||
;;
|
;;
|
||||||
octopus-flanet)
|
octopus-flanet)
|
||||||
octopus_flanet
|
octopus_flanet_rootfs
|
||||||
|
local arch="aarch64"
|
||||||
|
local device_type="video1"
|
||||||
|
local network_type=""
|
||||||
|
NEED_PREPARE_DNS=true
|
||||||
;;
|
;;
|
||||||
*)
|
onecloud-pro)
|
||||||
echo "Do no thing."
|
onecloud_pro_rootfs
|
||||||
|
local arch="aarch64"
|
||||||
|
local device_type="gpio-onecloud-pro video1"
|
||||||
|
local network_type="systemd-networkd"
|
||||||
|
NEED_PREPARE_DNS=true
|
||||||
|
;;
|
||||||
|
orangepi-zero)
|
||||||
|
orangepizero_rootfs
|
||||||
|
local arch="armhf"
|
||||||
|
local device_type=""
|
||||||
|
local network_type=""
|
||||||
|
NEED_PREPARE_DNS=true
|
||||||
|
;;
|
||||||
|
oec-turbo)
|
||||||
|
oec_turbo_rootfs
|
||||||
|
local arch="aarch64"
|
||||||
|
local device_type="vpu"
|
||||||
|
local network_type=""
|
||||||
|
NEED_PREPARE_DNS=true
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "错误:未知或不支持的目标 '$target'" >&2
|
||||||
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
fi
|
|
||||||
|
mount_rootfs
|
||||||
|
|
||||||
|
install_and_configure_kvmd "$arch" "$device_type" "$network_type"
|
||||||
|
|
||||||
|
write_meta "$target"
|
||||||
|
|
||||||
|
unmount_all
|
||||||
|
|
||||||
|
case "$target" in
|
||||||
|
onecloud)
|
||||||
|
pack_img_onecloud
|
||||||
|
;;
|
||||||
|
vm)
|
||||||
|
pack_img "Vm"
|
||||||
|
;;
|
||||||
|
cumebox2)
|
||||||
|
pack_img "Cumebox2"
|
||||||
|
;;
|
||||||
|
chainedbox)
|
||||||
|
pack_img "Chainedbox"
|
||||||
|
;;
|
||||||
|
e900v22c)
|
||||||
|
pack_img "E900v22c"
|
||||||
|
;;
|
||||||
|
octopus-flanet)
|
||||||
|
pack_img "Octopus-Flanet"
|
||||||
|
;;
|
||||||
|
onecloud-pro)
|
||||||
|
pack_img "Onecloud-Pro"
|
||||||
|
;;
|
||||||
|
orangepi-zero)
|
||||||
|
pack_img "Orangepi-Zero"
|
||||||
|
;;
|
||||||
|
oec-turbo)
|
||||||
|
pack_img "OEC-Turbo"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "错误:未知的打包类型 for '$target'" >&2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 在 GitHub Actions 环境中清理下载的文件
|
||||||
|
cleanup_downloaded_files
|
||||||
|
|
||||||
|
echo "=================================================="
|
||||||
|
echo "信息:目标 $target 构建完成!"
|
||||||
|
echo "=================================================="
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 主逻辑 ---
|
||||||
|
|
||||||
|
# 检查是否提供了目标参数
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "用法: $0 <target|all>"
|
||||||
|
echo "可用目标: onecloud, cumebox2, chainedbox, vm, e900v22c, octopus-flanet, onecloud-pro, orangepi-zero, oec-turbo"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 设置脚本立即退出模式
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
# 检查必要的外部工具
|
||||||
|
check_required_tools "$1"
|
||||||
|
|
||||||
|
# 执行构建
|
||||||
|
if [ "$1" = "all" ]; then
|
||||||
|
echo "信息:开始构建所有目标..."
|
||||||
|
build_target "onecloud"
|
||||||
|
build_target "cumebox2"
|
||||||
|
build_target "chainedbox"
|
||||||
|
build_target "vm"
|
||||||
|
build_target "e900v22c"
|
||||||
|
build_target "octopus-flanet"
|
||||||
|
build_target "onecloud-pro"
|
||||||
|
build_target "orangepi-zero"
|
||||||
|
build_target "oec-turbo"
|
||||||
|
echo "信息:所有目标构建完成。"
|
||||||
|
else
|
||||||
|
build_target "$1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
313
build/functions/common.sh
Executable file
313
build/functions/common.sh
Executable file
@ -0,0 +1,313 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# --- 辅助函数 ---
|
||||||
|
|
||||||
|
# 获取 Git 提交 ID
|
||||||
|
get_git_commit_id() {
|
||||||
|
if git rev-parse --is-inside-work-tree &>/dev/null; then
|
||||||
|
git rev-parse --short HEAD 2>/dev/null || echo ""
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 查找并设置一个可用的 loop 设备
|
||||||
|
find_loop_device() {
|
||||||
|
echo "信息:查找可用的 loop 设备..."
|
||||||
|
# 只使用 --find 来获取设备名
|
||||||
|
LOOPDEV=$(sudo losetup --find)
|
||||||
|
if [[ -z "$LOOPDEV" || ! -e "$LOOPDEV" ]]; then
|
||||||
|
echo "错误:再次尝试后仍无法找到可用的 loop 设备。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "信息:找到可用 loop 设备名:$LOOPDEV"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查并创建目录
|
||||||
|
ensure_dir() {
|
||||||
|
if [[ ! -d "$1" ]]; then
|
||||||
|
echo "信息:创建目录 $1 ..."
|
||||||
|
sudo mkdir -p "$1" || { echo "错误:创建目录 $1 失败" >&2; exit 1; }
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 执行 chroot 命令
|
||||||
|
run_in_chroot() {
|
||||||
|
echo "信息:在 chroot 环境 ($ROOTFS) 中执行命令..."
|
||||||
|
sudo chroot --userspec "root:root" "$ROOTFS" bash -ec "$1" || { echo "错误:在 chroot 环境中执行命令失败" >&2; exit 1; }
|
||||||
|
echo "信息:chroot 命令执行完成。"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 清理函数 ---
|
||||||
|
cleanup() {
|
||||||
|
echo "信息:执行清理操作..."
|
||||||
|
# 尝试卸载 chroot 环境下的挂载点
|
||||||
|
if [[ "$DEV_MOUNTED" -eq 1 ]]; then
|
||||||
|
echo "信息:卸载 $ROOTFS/dev ..."
|
||||||
|
sudo umount "$ROOTFS/dev" || echo "警告:卸载 $ROOTFS/dev 失败,可能已被卸载"
|
||||||
|
DEV_MOUNTED=0
|
||||||
|
fi
|
||||||
|
if [[ "$SYS_MOUNTED" -eq 1 ]]; then
|
||||||
|
echo "信息:卸载 $ROOTFS/sys ..."
|
||||||
|
sudo umount "$ROOTFS/sys" || echo "警告:卸载 $ROOTFS/sys 失败,可能已被卸载"
|
||||||
|
SYS_MOUNTED=0
|
||||||
|
fi
|
||||||
|
if [[ "$PROC_MOUNTED" -eq 1 ]]; then
|
||||||
|
echo "信息:卸载 $ROOTFS/proc ..."
|
||||||
|
sudo umount "$ROOTFS/proc" || echo "警告:卸载 $ROOTFS/proc 失败,可能已被卸载"
|
||||||
|
PROC_MOUNTED=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 尝试卸载主根文件系统
|
||||||
|
if [[ "$ROOTFS_MOUNTED" -eq 1 && -d "$ROOTFS" ]]; then
|
||||||
|
echo "信息:卸载 $ROOTFS ..."
|
||||||
|
sudo umount "$ROOTFS" || sudo umount -l "$ROOTFS" || echo "警告:卸载 $ROOTFS 失败"
|
||||||
|
ROOTFS_MOUNTED=0
|
||||||
|
fi
|
||||||
|
# 尝试卸载引导文件系统 (如果使用)
|
||||||
|
if [[ "$BOOTFS_MOUNTED" -eq 1 && -d "$BOOTFS" ]]; then
|
||||||
|
echo "信息:卸载 $BOOTFS ..."
|
||||||
|
sudo umount "$BOOTFS" || sudo umount -l "$BOOTFS" || echo "警告:卸载 $BOOTFS 失败"
|
||||||
|
BOOTFS_MOUNTED=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 尝试分离 loop 设备
|
||||||
|
if [[ -n "$LOOPDEV" && -b "$LOOPDEV" ]]; then
|
||||||
|
echo "信息:尝试 zerofree $LOOPDEV ..."
|
||||||
|
sudo zerofree "$LOOPDEV" || echo "警告:zerofree $LOOPDEV 失败,可能文件系统不支持或未干净卸载"
|
||||||
|
echo "信息:分离 loop 设备 $LOOPDEV ..."
|
||||||
|
sudo losetup -d "$LOOPDEV" || echo "警告:分离 $LOOPDEV 失败"
|
||||||
|
LOOPDEV=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 尝试删除 Docker 容器
|
||||||
|
echo "信息:检查并删除 Docker 容器 $DOCKER_CONTAINER_NAME ..."
|
||||||
|
if sudo docker ps -a --format '{{.Names}}' | grep -q "^${DOCKER_CONTAINER_NAME}$"; then
|
||||||
|
sudo docker rm -f "$DOCKER_CONTAINER_NAME" || echo "警告:删除 Docker 容器 $DOCKER_CONTAINER_NAME 失败"
|
||||||
|
else
|
||||||
|
echo "信息:Docker 容器 $DOCKER_CONTAINER_NAME 不存在或已被删除。"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 清理临时目录和挂载点目录
|
||||||
|
echo "信息:清理临时文件和目录..."
|
||||||
|
sudo rm -rf "$PREBUILT_DIR"
|
||||||
|
# 只删除挂载点目录本身
|
||||||
|
if [[ -d "$ROOTFS" ]]; then
|
||||||
|
sudo rmdir "$ROOTFS" || echo "警告:删除目录 $ROOTFS 失败,可能非空"
|
||||||
|
fi
|
||||||
|
if [[ -d "$BOOTFS" ]]; then
|
||||||
|
sudo rmdir "$BOOTFS" || echo "警告:删除目录 $BOOTFS 失败,可能非空"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "信息:清理完成。"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 在打包镜像前调用此函数,确保干净卸载所有挂载点和loop设备
|
||||||
|
unmount_all() {
|
||||||
|
echo "信息:执行卸载操作,准备打包..."
|
||||||
|
# 卸载 chroot 环境下的挂载点
|
||||||
|
if [[ "$DEV_MOUNTED" -eq 1 ]]; then
|
||||||
|
echo "信息:卸载 $ROOTFS/dev ..."
|
||||||
|
sudo umount "$ROOTFS/dev" || echo "警告:卸载 $ROOTFS/dev 失败,可能已被卸载"
|
||||||
|
DEV_MOUNTED=0
|
||||||
|
fi
|
||||||
|
if [[ "$SYS_MOUNTED" -eq 1 ]]; then
|
||||||
|
echo "信息:卸载 $ROOTFS/sys ..."
|
||||||
|
sudo umount "$ROOTFS/sys" || echo "警告:卸载 $ROOTFS/sys 失败,可能已被卸载"
|
||||||
|
SYS_MOUNTED=0
|
||||||
|
fi
|
||||||
|
if [[ "$PROC_MOUNTED" -eq 1 ]]; then
|
||||||
|
echo "信息:卸载 $ROOTFS/proc ..."
|
||||||
|
sudo umount "$ROOTFS/proc" || echo "警告:卸载 $ROOTFS/proc 失败,可能已被卸载"
|
||||||
|
PROC_MOUNTED=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 卸载主根文件系统
|
||||||
|
if [[ "$ROOTFS_MOUNTED" -eq 1 && -d "$ROOTFS" ]]; then
|
||||||
|
echo "信息:卸载 $ROOTFS ..."
|
||||||
|
sudo umount "$ROOTFS" || sudo umount -l "$ROOTFS" || echo "警告:卸载 $ROOTFS 失败"
|
||||||
|
ROOTFS_MOUNTED=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 尝试分离 loop 设备前执行 zerofree(如果文件系统支持)
|
||||||
|
if [[ -n "$LOOPDEV" && -b "$LOOPDEV" ]]; then
|
||||||
|
echo "信息:尝试 zerofree $LOOPDEV ..."
|
||||||
|
sudo zerofree "$LOOPDEV" || echo "警告:zerofree $LOOPDEV 失败,可能文件系统不支持或未干净卸载"
|
||||||
|
echo "信息:分离 loop 设备 $LOOPDEV ..."
|
||||||
|
sudo losetup -d "$LOOPDEV" || echo "警告:分离 $LOOPDEV 失败"
|
||||||
|
LOOPDEV=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo rm -rf "$PREBUILT_DIR"
|
||||||
|
|
||||||
|
echo "信息:卸载操作完成,可以安全打包镜像。"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 挂载根文件系统
|
||||||
|
mount_rootfs() {
|
||||||
|
echo "信息:挂载根文件系统到 $ROOTFS ..."
|
||||||
|
ensure_dir "$ROOTFS"
|
||||||
|
sudo mount "$LOOPDEV" "$ROOTFS" || { echo "错误:挂载 $LOOPDEV 到 $ROOTFS 失败" >&2; exit 1; }
|
||||||
|
ROOTFS_MOUNTED=1
|
||||||
|
|
||||||
|
echo "信息:挂载 proc, sys, dev 到 chroot 环境..."
|
||||||
|
ensure_dir "$ROOTFS/proc"
|
||||||
|
sudo mount -t proc proc "$ROOTFS/proc" || { echo "错误:挂载 proc 到 $ROOTFS/proc 失败" >&2; exit 1; }
|
||||||
|
PROC_MOUNTED=1
|
||||||
|
|
||||||
|
ensure_dir "$ROOTFS/sys"
|
||||||
|
sudo mount -t sysfs sys "$ROOTFS/sys" || { echo "错误:挂载 sys 到 $ROOTFS/sys 失败" >&2; exit 1; }
|
||||||
|
SYS_MOUNTED=1
|
||||||
|
|
||||||
|
ensure_dir "$ROOTFS/dev"
|
||||||
|
sudo mount -o bind /dev "$ROOTFS/dev" || { echo "错误:绑定挂载 /dev 到 $ROOTFS/dev 失败" >&2; exit 1; }
|
||||||
|
DEV_MOUNTED=1
|
||||||
|
echo "信息:根文件系统及虚拟文件系统挂载完成。"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 设置元数据
|
||||||
|
write_meta() {
|
||||||
|
local hostname="$1"
|
||||||
|
echo "信息:在 chroot 环境中设置主机名/元数据为 $hostname ..."
|
||||||
|
run_in_chroot "sed -i 's/localhost.localdomain/$hostname/g' /etc/kvmd/meta.yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检测是否在 GitHub Actions 环境中
|
||||||
|
is_github_actions() {
|
||||||
|
[[ -n "$GITHUB_ACTIONS" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 记录下载的文件列表(仅在 GitHub Actions 环境中)
|
||||||
|
DOWNLOADED_FILES_LIST="/tmp/downloaded_files.txt"
|
||||||
|
|
||||||
|
# 自动下载文件函数
|
||||||
|
download_file_if_missing() {
|
||||||
|
local file_path="$1"
|
||||||
|
local relative_path=""
|
||||||
|
|
||||||
|
# 如果文件已存在,直接返回
|
||||||
|
if [[ -f "$file_path" ]]; then
|
||||||
|
echo "信息:文件已存在: $file_path"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 计算相对于 SRCPATH 的路径
|
||||||
|
if [[ "$file_path" == "$SRCPATH"/* ]]; then
|
||||||
|
relative_path="${file_path#$SRCPATH/}"
|
||||||
|
else
|
||||||
|
echo "错误:文件路径 $file_path 不在 SRCPATH ($SRCPATH) 下" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "信息:文件不存在,尝试下载: $file_path"
|
||||||
|
echo "信息:相对路径: $relative_path"
|
||||||
|
|
||||||
|
# 确保目标目录存在
|
||||||
|
local target_dir="$(dirname "$file_path")"
|
||||||
|
ensure_dir "$target_dir"
|
||||||
|
|
||||||
|
# 首先尝试直接下载
|
||||||
|
local remote_url="${REMOTE_PREFIX}/${relative_path}"
|
||||||
|
echo "信息:尝试下载: $remote_url"
|
||||||
|
|
||||||
|
if curl -f -L -o "$file_path" "$remote_url" 2>/dev/null; then
|
||||||
|
echo "信息:下载成功: $file_path"
|
||||||
|
# 在 GitHub Actions 环境中记录下载的文件
|
||||||
|
if is_github_actions; then
|
||||||
|
echo "$file_path" >> "$DOWNLOADED_FILES_LIST"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 如果直接下载失败,尝试添加 .xz 后缀
|
||||||
|
echo "信息:直接下载失败,尝试 .xz 压缩版本..."
|
||||||
|
local xz_url="${remote_url}.xz"
|
||||||
|
local xz_file="${file_path}.xz"
|
||||||
|
|
||||||
|
if curl -f -L -o "$xz_file" "$xz_url" 2>/dev/null; then
|
||||||
|
echo "信息:下载 .xz 文件成功,正在解压..."
|
||||||
|
if xz -d "$xz_file"; then
|
||||||
|
echo "信息:解压成功: $file_path"
|
||||||
|
# 在 GitHub Actions 环境中记录下载的文件
|
||||||
|
if is_github_actions; then
|
||||||
|
echo "$file_path" >> "$DOWNLOADED_FILES_LIST"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "错误:解压 .xz 文件失败" >&2
|
||||||
|
rm -f "$xz_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "错误:无法下载文件 $file_path (尝试了原始版本和 .xz 版本)" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 下载 rc.local 文件
|
||||||
|
download_rc_local() {
|
||||||
|
local platform_id="$1"
|
||||||
|
local rc_local_path="$SRCPATH/image/$platform_id/rc.local"
|
||||||
|
local relative_path="image/$platform_id/rc.local"
|
||||||
|
local remote_url="$REMOTE_PREFIX/$relative_path"
|
||||||
|
|
||||||
|
echo "信息:检查是否需要下载 rc.local 文件 ($platform_id)..."
|
||||||
|
|
||||||
|
# 如果本地文件不存在,尝试下载
|
||||||
|
if [ ! -f "$rc_local_path" ]; then
|
||||||
|
echo "信息:本地 rc.local 文件不存在,尝试从远程下载..."
|
||||||
|
ensure_dir "$(dirname "$rc_local_path")"
|
||||||
|
|
||||||
|
if curl -sSL --fail "$remote_url" -o "$rc_local_path"; then
|
||||||
|
echo "信息:成功下载 rc.local 文件:$remote_url"
|
||||||
|
# 在 GitHub Actions 环境中记录下载的文件
|
||||||
|
if is_github_actions; then
|
||||||
|
echo "$rc_local_path" >> "$DOWNLOADED_FILES_LIST"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "信息:远程 rc.local 文件不存在或下载失败:$remote_url"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "信息:使用本地 rc.local 文件:$rc_local_path"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 清理下载的文件(仅在 GitHub Actions 环境中)
|
||||||
|
cleanup_downloaded_files() {
|
||||||
|
if is_github_actions && [[ -f "$DOWNLOADED_FILES_LIST" ]]; then
|
||||||
|
echo "信息:清理 GitHub Actions 环境中下载的文件..."
|
||||||
|
while IFS= read -r file_path; do
|
||||||
|
if [[ -f "$file_path" ]]; then
|
||||||
|
echo "信息:删除下载的文件: $file_path"
|
||||||
|
rm -f "$file_path"
|
||||||
|
fi
|
||||||
|
done < "$DOWNLOADED_FILES_LIST"
|
||||||
|
rm -f "$DOWNLOADED_FILES_LIST"
|
||||||
|
echo "信息:下载文件清理完成"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查必要的外部工具
|
||||||
|
check_required_tools() {
|
||||||
|
local required_tools="sudo docker losetup mount umount parted e2fsck resize2fs qemu-img curl tar python3 pip3 rsync git simg2img img2simg dd cat rm mkdir mv cp sed chmod chown ln grep printf id xz"
|
||||||
|
|
||||||
|
for cmd in $required_tools; do
|
||||||
|
if ! command -v "$cmd" &> /dev/null; then
|
||||||
|
echo "错误:必需的命令 '$cmd' 未找到。请安装相应软件包。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 检查特定工具 (如果脚本中使用了)
|
||||||
|
if ! command -v "$SRCPATH/image/onecloud/AmlImg_v0.3.1_linux_amd64" &> /dev/null && [[ "$1" == "onecloud" || "$1" == "all" ]]; then
|
||||||
|
if [ -f "$SRCPATH/image/onecloud/AmlImg_v0.3.1_linux_amd64" ]; then
|
||||||
|
echo "信息:找到 AmlImg 工具,尝试设置执行权限..."
|
||||||
|
sudo chmod +x "$SRCPATH/image/onecloud/AmlImg_v0.3.1_linux_amd64" || echo "警告:设置 AmlImg 执行权限失败"
|
||||||
|
else
|
||||||
|
echo "错误:构建 onecloud 需要 '$SRCPATH/image/onecloud/AmlImg_v0.3.1_linux_amd64',但未找到。" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
453
build/functions/devices.sh
Executable file
453
build/functions/devices.sh
Executable file
@ -0,0 +1,453 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# --- 设备特定的 Rootfs 准备函数 ---
|
||||||
|
|
||||||
|
onecloud_rootfs() {
|
||||||
|
local unpacker="$SRCPATH/image/onecloud/AmlImg_v0.3.1_linux_amd64"
|
||||||
|
local source_image="$SRCPATH/image/onecloud/Armbian_by-SilentWind_24.5.0-trunk_Onecloud_bookworm_legacy_5.9.0-rc7_minimal_support-dvd-emulation.burn.img"
|
||||||
|
local bootfs_img="$TMPDIR/bootfs.img"
|
||||||
|
local rootfs_img="$TMPDIR/rootfs.img"
|
||||||
|
local bootfs_sparse="$TMPDIR/6.boot.PARTITION.sparse"
|
||||||
|
local rootfs_sparse="$TMPDIR/7.rootfs.PARTITION.sparse"
|
||||||
|
local bootfs_loopdev="" # 存储 bootfs 使用的 loop 设备
|
||||||
|
local add_size_mb=600
|
||||||
|
|
||||||
|
echo "信息:准备 Onecloud Rootfs..."
|
||||||
|
ensure_dir "$TMPDIR"
|
||||||
|
ensure_dir "$BOOTFS"
|
||||||
|
|
||||||
|
# 自动下载 AmlImg 工具(如果不存在)
|
||||||
|
download_file_if_missing "$unpacker" || { echo "错误:下载 AmlImg 工具失败" >&2; exit 1; }
|
||||||
|
sudo chmod +x "$unpacker" || { echo "错误:设置 AmlImg 工具执行权限失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
# 自动下载源镜像文件(如果不存在)
|
||||||
|
download_file_if_missing "$source_image" || { echo "错误:下载 Onecloud 原始镜像失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:解包 Onecloud burn 镜像..."
|
||||||
|
sudo "$unpacker" unpack "$source_image" "$TMPDIR" || { echo "错误:解包失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:转换 bootfs 和 rootfs sparse 镜像到 raw 格式..."
|
||||||
|
sudo simg2img "$bootfs_sparse" "$bootfs_img" || { echo "错误:转换 bootfs sparse 镜像失败" >&2; exit 1; }
|
||||||
|
sudo simg2img "$rootfs_sparse" "$rootfs_img" || { echo "错误:转换 rootfs sparse 镜像失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:挂载 bootfs 并修复 DTB..."
|
||||||
|
find_loop_device # 查找一个 loop 设备给 bootfs
|
||||||
|
bootfs_loopdev="$LOOPDEV" # 保存这个设备名
|
||||||
|
echo "信息:将 $bootfs_img 关联到 $bootfs_loopdev..."
|
||||||
|
sudo losetup "$bootfs_loopdev" "$bootfs_img" || { echo "错误:关联 bootfs 镜像到 $bootfs_loopdev 失败" >&2; exit 1; }
|
||||||
|
sudo mount "$bootfs_loopdev" "$BOOTFS" || { echo "错误:挂载 bootfs ($bootfs_loopdev) 失败" >&2; exit 1; }
|
||||||
|
BOOTFS_MOUNTED=1
|
||||||
|
|
||||||
|
# 自动下载 DTB 文件(如果不存在)
|
||||||
|
local dtb_file="$SRCPATH/image/onecloud/meson8b-onecloud-fix.dtb"
|
||||||
|
download_file_if_missing "$dtb_file" || { echo "错误:下载 Onecloud DTB 文件失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
sudo cp "$dtb_file" "$BOOTFS/dtb/meson8b-onecloud.dtb" || { echo "错误:复制修复后的 DTB 文件失败" >&2; exit 1; }
|
||||||
|
sudo umount "$BOOTFS" || { echo "警告:卸载 bootfs ($BOOTFS) 失败" >&2; BOOTFS_MOUNTED=0; } # 卸载失败不应中断流程
|
||||||
|
BOOTFS_MOUNTED=0
|
||||||
|
echo "信息:分离 bootfs loop 设备 $bootfs_loopdev..."
|
||||||
|
sudo losetup -d "$bootfs_loopdev" || { echo "警告:分离 bootfs loop 设备 $bootfs_loopdev 失败" >&2; }
|
||||||
|
# bootfs_loopdev 对应的设备现在是空闲的
|
||||||
|
|
||||||
|
echo "信息:扩展 rootfs 镜像 (${add_size_mb}MB)..."
|
||||||
|
sudo dd if=/dev/zero bs=1M count="$add_size_mb" >> "$rootfs_img" || { echo "错误:扩展 rootfs 镜像失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:检查并调整 rootfs 文件系统大小 (在文件上)..."
|
||||||
|
# 注意:e2fsck/resize2fs 现在直接操作镜像文件,而不是 loop 设备
|
||||||
|
sudo e2fsck -f -y "$rootfs_img" || { echo "警告:e2fsck 检查 rootfs 镜像文件失败" >&2; exit 1; }
|
||||||
|
sudo resize2fs "$rootfs_img" || { echo "错误:resize2fs 调整 rootfs 镜像文件大小失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:设置 rootfs loop 设备..."
|
||||||
|
find_loop_device # 重新查找一个可用的 loop 设备 (可能是刚才释放的那个)
|
||||||
|
echo "信息:将 $rootfs_img 关联到 $LOOPDEV..."
|
||||||
|
sudo losetup "$LOOPDEV" "$rootfs_img" || { echo "错误:关联 rootfs 镜像到 $LOOPDEV 失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:Onecloud Rootfs 准备完成。 Loop 设备 $LOOPDEV 已关联 $rootfs_img"
|
||||||
|
}
|
||||||
|
|
||||||
|
cumebox2_rootfs() {
|
||||||
|
local source_image="$SRCPATH/image/cumebox2/Armbian_24.8.1_Khadas-vim1_bookworm_current_6.6.47_minimal.img"
|
||||||
|
local target_image="$TMPDIR/rootfs.img"
|
||||||
|
local offset=$((8192 * 512))
|
||||||
|
local add_size_mb=900
|
||||||
|
|
||||||
|
echo "信息:准备 Cumebox2 Rootfs..."
|
||||||
|
ensure_dir "$TMPDIR"
|
||||||
|
|
||||||
|
# 自动下载源镜像文件(如果不存在)
|
||||||
|
download_file_if_missing "$source_image" || { echo "错误:下载 Cumebox2 原始镜像失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
cp "$source_image" "$target_image" || { echo "错误:复制 Cumebox2 原始镜像失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:扩展镜像文件 (${add_size_mb}MB)..."
|
||||||
|
sudo dd if=/dev/zero bs=1M count="$add_size_mb" >> "$target_image" || { echo "错误:扩展镜像文件失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:调整镜像分区大小..."
|
||||||
|
sudo parted -s "$target_image" resizepart 1 100% || { echo "错误:使用 parted 调整分区大小失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:设置带偏移量的 loop 设备..."
|
||||||
|
find_loop_device # 查找设备名
|
||||||
|
echo "信息:将 $target_image (偏移 $offset) 关联到 $LOOPDEV..."
|
||||||
|
sudo losetup --offset "$offset" "$LOOPDEV" "$target_image" || { echo "错误:设置带偏移量的 loop 设备 $LOOPDEV 失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:检查并调整文件系统大小 (在 loop 设备上)..."
|
||||||
|
sudo e2fsck -f -y "$LOOPDEV" || { echo "警告:e2fsck 检查 $LOOPDEV 失败" >&2; exit 1; }
|
||||||
|
sudo resize2fs "$LOOPDEV" || { echo "错误:resize2fs 调整 $LOOPDEV 大小失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:Cumebox2 Rootfs 准备完成,loop 设备 $LOOPDEV 已就绪。"
|
||||||
|
}
|
||||||
|
|
||||||
|
chainedbox_rootfs_and_fix_dtb() {
|
||||||
|
local source_image="$SRCPATH/image/chainedbox/Armbian_24.11.0_rockchip_chainedbox_bookworm_6.1.112_server_2024.10.02_add800m.img"
|
||||||
|
local target_image="$TMPDIR/rootfs.img"
|
||||||
|
local boot_offset=$((32768 * 512))
|
||||||
|
local rootfs_offset=$((1081344 * 512))
|
||||||
|
local bootfs_loopdev=""
|
||||||
|
|
||||||
|
echo "信息:准备 Chainedbox Rootfs 并修复 DTB..."
|
||||||
|
ensure_dir "$TMPDIR"; ensure_dir "$BOOTFS"
|
||||||
|
|
||||||
|
# 自动下载源镜像文件(如果不存在)
|
||||||
|
download_file_if_missing "$source_image" || { echo "错误:下载 Chainedbox 原始镜像失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
cp "$source_image" "$target_image" || { echo "错误:复制 Chainedbox 原始镜像失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:挂载 boot 分区并修复 DTB..."
|
||||||
|
find_loop_device # 找 loop 给 boot
|
||||||
|
bootfs_loopdev="$LOOPDEV"
|
||||||
|
echo "信息:将 $target_image (偏移 $boot_offset) 关联到 $bootfs_loopdev..."
|
||||||
|
sudo losetup --offset "$boot_offset" "$bootfs_loopdev" "$target_image" || { echo "错误:设置 boot 分区 loop 设备 $bootfs_loopdev 失败" >&2; exit 1; }
|
||||||
|
sudo mount "$bootfs_loopdev" "$BOOTFS" || { echo "错误:挂载 boot 分区 ($bootfs_loopdev) 失败" >&2; exit 1; }
|
||||||
|
BOOTFS_MOUNTED=1
|
||||||
|
|
||||||
|
# 自动下载 DTB 文件(如果不存在)
|
||||||
|
local dtb_file="$SRCPATH/image/chainedbox/rk3328-l1pro-1296mhz-fix.dtb"
|
||||||
|
download_file_if_missing "$dtb_file" || { echo "错误:下载 Chainedbox DTB 文件失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
sudo cp "$dtb_file" "$BOOTFS/dtb/rockchip/rk3328-l1pro-1296mhz.dtb" || { echo "错误:复制修复后的 DTB 文件失败" >&2; exit 1; }
|
||||||
|
sudo umount "$BOOTFS" || { echo "警告:卸载 boot 分区 ($BOOTFS) 失败" >&2; BOOTFS_MOUNTED=0; }
|
||||||
|
BOOTFS_MOUNTED=0
|
||||||
|
echo "信息:分离 boot loop 设备 $bootfs_loopdev..."
|
||||||
|
sudo losetup -d "$bootfs_loopdev" || { echo "警告:分离 boot 分区 loop 设备 $bootfs_loopdev 失败" >&2; }
|
||||||
|
|
||||||
|
echo "信息:设置 rootfs 分区的 loop 设备..."
|
||||||
|
find_loop_device # 找 loop 给 rootfs
|
||||||
|
echo "信息:将 $target_image (偏移 $rootfs_offset) 关联到 $LOOPDEV..."
|
||||||
|
sudo losetup --offset "$rootfs_offset" "$LOOPDEV" "$target_image" || { echo "错误:设置 rootfs 分区 loop 设备 $LOOPDEV 失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:Chainedbox Rootfs 准备完成,loop 设备 $LOOPDEV 已就绪。"
|
||||||
|
}
|
||||||
|
|
||||||
|
vm_rootfs() {
|
||||||
|
local source_image="$SRCPATH/image/vm/Armbian_25.2.1_Uefi-x86_bookworm_current_6.12.13_minimal.img"
|
||||||
|
local target_image="$TMPDIR/rootfs.img"
|
||||||
|
local offset=$((540672 * 512))
|
||||||
|
|
||||||
|
echo "信息:准备 Vm Rootfs..."
|
||||||
|
ensure_dir "$TMPDIR"
|
||||||
|
|
||||||
|
# 自动下载源镜像文件(如果不存在)
|
||||||
|
download_file_if_missing "$source_image" || { echo "错误:下载 Vm 原始镜像失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
cp "$source_image" "$target_image" || { echo "错误:复制 Vm 原始镜像失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:设置带偏移量的 loop 设备..."
|
||||||
|
find_loop_device # 查找设备名
|
||||||
|
echo "信息:将 $target_image (偏移 $offset) 关联到 $LOOPDEV..."
|
||||||
|
sudo losetup --offset "$offset" "$LOOPDEV" "$target_image" || { echo "错误:设置带偏移量的 loop 设备 $LOOPDEV 失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:Vm Rootfs 准备完成,loop 设备 $LOOPDEV 已就绪。"
|
||||||
|
}
|
||||||
|
|
||||||
|
e900v22c_rootfs() {
|
||||||
|
local source_image="$SRCPATH/image/e900v22c/Armbian_23.08.0_amlogic_s905l3a_bookworm_5.15.123_server_2023.08.01.img"
|
||||||
|
local target_image="$TMPDIR/rootfs.img"
|
||||||
|
local offset=$((532480 * 512))
|
||||||
|
local add_size_mb=600
|
||||||
|
|
||||||
|
echo "信息:准备 E900V22C Rootfs..."
|
||||||
|
ensure_dir "$TMPDIR"
|
||||||
|
|
||||||
|
# 自动下载源镜像文件(如果不存在)
|
||||||
|
download_file_if_missing "$source_image" || { echo "错误:下载 E900V22C 原始镜像失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
cp "$source_image" "$target_image" || { echo "错误:复制 E900V22C 原始镜像失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:扩展镜像文件 (${add_size_mb}MB)..."
|
||||||
|
sudo dd if=/dev/zero bs=1M count="$add_size_mb" >> "$target_image" || { echo "错误:扩展镜像文件失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:调整镜像分区大小 (分区 2)..."
|
||||||
|
sudo parted -s "$target_image" resizepart 2 100% || { echo "错误:使用 parted 调整分区 2 大小失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:设置带偏移量的 loop 设备..."
|
||||||
|
find_loop_device # 查找设备名
|
||||||
|
echo "信息:将 $target_image (偏移 $offset) 关联到 $LOOPDEV..."
|
||||||
|
sudo losetup --offset "$offset" "$LOOPDEV" "$target_image" || { echo "错误:设置带偏移量的 loop 设备 $LOOPDEV 失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:检查并调整文件系统大小 (在 loop 设备上)..."
|
||||||
|
sudo e2fsck -f -y "$LOOPDEV" || { echo "警告:e2fsck 检查 $LOOPDEV 失败" >&2; exit 1; }
|
||||||
|
sudo resize2fs "$LOOPDEV" || { echo "错误:resize2fs 调整 $LOOPDEV 大小失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:E900V22C Rootfs 准备完成,loop 设备 $LOOPDEV 已就绪。"
|
||||||
|
}
|
||||||
|
|
||||||
|
octopus_flanet_rootfs() {
|
||||||
|
local source_image="$SRCPATH/image/octopus-flanet/Armbian_25.05.0_amlogic_s912_bookworm_6.1.129_server_2025.03.02.img"
|
||||||
|
local target_image="$TMPDIR/rootfs.img"
|
||||||
|
local boot_offset=$((8192 * 512))
|
||||||
|
local rootfs_offset=$((1056768 * 512))
|
||||||
|
local add_size_mb=600
|
||||||
|
local bootfs_loopdev=""
|
||||||
|
|
||||||
|
echo "信息:准备 Octopus-Planet Rootfs..."
|
||||||
|
ensure_dir "$TMPDIR"; ensure_dir "$BOOTFS"
|
||||||
|
|
||||||
|
# 自动下载源镜像文件(如果不存在)
|
||||||
|
download_file_if_missing "$source_image" || { echo "错误:下载 Octopus-Planet 原始镜像失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
cp "$source_image" "$target_image" || { echo "错误:复制 Octopus-Planet 原始镜像失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:挂载 boot 分区并修改 uEnv.txt (使用 VIM2 DTB)..."
|
||||||
|
find_loop_device # 找 loop 给 boot
|
||||||
|
bootfs_loopdev="$LOOPDEV"
|
||||||
|
echo "信息:将 $target_image (偏移 $boot_offset) 关联到 $bootfs_loopdev..."
|
||||||
|
sudo losetup --offset "$boot_offset" "$bootfs_loopdev" "$target_image" || { echo "错误:设置 boot 分区 loop 设备 $bootfs_loopdev 失败" >&2; exit 1; }
|
||||||
|
sudo mount "$bootfs_loopdev" "$BOOTFS" || { echo "错误:挂载 boot 分区 ($bootfs_loopdev) 失败" >&2; exit 1; }
|
||||||
|
BOOTFS_MOUNTED=1
|
||||||
|
|
||||||
|
# 自动下载 Octopus-Planet 相关文件
|
||||||
|
local dtb_file="$SRCPATH/image/octopus-flanet/meson-gxm-octopus-planet.dtb"
|
||||||
|
download_file_if_missing "$dtb_file" || echo "警告:下载 Octopus-Planet DTB 失败"
|
||||||
|
sudo cp "$dtb_file" "$BOOTFS/dtb/amlogic/meson-gxm-octopus-planet.dtb" || echo "警告:复制 Octopus-Planet DTB 失败"
|
||||||
|
|
||||||
|
sudo sed -i "s/meson-gxm-octopus-planet.dtb/meson-gxm-khadas-vim2.dtb/g" "$BOOTFS/uEnv.txt" || { echo "错误:修改 uEnv.txt 失败" >&2; exit 1; }
|
||||||
|
sudo umount "$BOOTFS" || { echo "警告:卸载 boot 分区 ($BOOTFS) 失败" >&2; BOOTFS_MOUNTED=0; }
|
||||||
|
BOOTFS_MOUNTED=0
|
||||||
|
echo "信息:分离 boot loop 设备 $bootfs_loopdev..."
|
||||||
|
sudo losetup -d "$bootfs_loopdev" || { echo "警告:分离 boot 分区 loop 设备 $bootfs_loopdev 失败" >&2; }
|
||||||
|
|
||||||
|
echo "信息:调整镜像分区大小 (分区 2)..."
|
||||||
|
sudo parted -s "$target_image" resizepart 2 100% || { echo "错误:使用 parted 调整分区 2 大小失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:设置 rootfs 分区的 loop 设备..."
|
||||||
|
find_loop_device # 找 loop 给 rootfs
|
||||||
|
echo "信息:将 $target_image (偏移 $rootfs_offset) 关联到 $LOOPDEV..."
|
||||||
|
sudo losetup --offset "$rootfs_offset" "$LOOPDEV" "$target_image" || { echo "错误:设置 rootfs 分区 loop 设备 $LOOPDEV 失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:检查并调整文件系统大小 (在 loop 设备上)..."
|
||||||
|
sudo e2fsck -f -y "$LOOPDEV" || { echo "警告:e2fsck 检查 $LOOPDEV 失败" >&2; exit 1; }
|
||||||
|
sudo resize2fs "$LOOPDEV" || { echo "错误:resize2fs 调整 $LOOPDEV 大小失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:Octopus-Planet Rootfs 准备完成,loop 设备 $LOOPDEV 已就绪。"
|
||||||
|
}
|
||||||
|
|
||||||
|
onecloud_pro_rootfs() {
|
||||||
|
local source_image="$SRCPATH/image/onecloud-pro/Armbian-by-SilentWind_24.5.0_amlogic_Onecloud-Pro_jammy_6.6.28_server.img"
|
||||||
|
local target_image="$TMPDIR/rootfs.img"
|
||||||
|
local boot_offset=$((8192 * 512))
|
||||||
|
local rootfs_offset=$((1056768 * 512))
|
||||||
|
local add_size_mb=600
|
||||||
|
local bootfs_loopdev=""
|
||||||
|
|
||||||
|
echo "信息:准备 Octopus-Planet Rootfs..."
|
||||||
|
ensure_dir "$TMPDIR"; ensure_dir "$BOOTFS"
|
||||||
|
|
||||||
|
# 自动下载源镜像文件(如果不存在)
|
||||||
|
download_file_if_missing "$source_image" || { echo "错误:下载 Octopus-Planet 原始镜像失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
cp "$source_image" "$target_image" || { echo "错误:复制 Octopus-Planet 原始镜像失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:调整镜像分区大小 (分区 2)..."
|
||||||
|
sudo parted -s "$target_image" resizepart 2 100% || { echo "错误:使用 parted 调整分区 2 大小失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:设置 rootfs 分区的 loop 设备..."
|
||||||
|
find_loop_device # 找 loop 给 rootfs
|
||||||
|
echo "信息:将 $target_image (偏移 $rootfs_offset) 关联到 $LOOPDEV..."
|
||||||
|
sudo losetup --offset "$rootfs_offset" "$LOOPDEV" "$target_image" || { echo "错误:设置 rootfs 分区 loop 设备 $LOOPDEV 失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:检查并调整文件系统大小 (在 loop 设备上)..."
|
||||||
|
sudo e2fsck -f -y "$LOOPDEV" || { echo "警告:e2fsck 检查 $LOOPDEV 失败" >&2; exit 1; }
|
||||||
|
sudo resize2fs "$LOOPDEV" || { echo "错误:resize2fs 调整 $LOOPDEV 大小失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:Octopus-Planet Rootfs 准备完成,loop 设备 $LOOPDEV 已就绪。"
|
||||||
|
}
|
||||||
|
|
||||||
|
orangepizero_rootfs() {
|
||||||
|
local source_image="$SRCPATH/image/orangepi-zero/Armbian_community_25.11.0-trunk.208_Orangepizero_bookworm_current_6.12.47_minimal.img"
|
||||||
|
local target_image="$TMPDIR/rootfs.img"
|
||||||
|
local offset=$((8192 * 512))
|
||||||
|
local add_size_mb=600
|
||||||
|
|
||||||
|
echo "信息:准备 Orange Pi Zero Rootfs..."
|
||||||
|
ensure_dir "$TMPDIR"
|
||||||
|
|
||||||
|
echo "信息:下载或使用本地 Orange Pi Zero 原始镜像..."
|
||||||
|
download_file_if_missing "$source_image" || { echo "错误:下载 Orange Pi Zero 原始镜像失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
cp "$source_image" "$target_image" || { echo "错误:复制 Orange Pi Zero 原始镜像失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:扩展镜像文件 (${add_size_mb}MB)..."
|
||||||
|
sudo dd if=/dev/zero bs=1M count="$add_size_mb" >> "$target_image" || { echo "错误:扩展镜像文件失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:调整镜像分区大小..."
|
||||||
|
sudo parted -s "$target_image" resizepart 1 100% || { echo "错误:使用 parted 调整分区大小失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
find_loop_device
|
||||||
|
sudo losetup -P "$LOOPDEV" "$target_image" || { echo "错误:设置 loop 设备失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:检查并调整文件系统大小..."
|
||||||
|
sudo e2fsck -y -f "${LOOPDEV}p1" || { echo "错误:文件系统检查失败" >&2; exit 1; }
|
||||||
|
sudo resize2fs "${LOOPDEV}p1" || { echo "错误:调整文件系统大小失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
# 重新设置 LOOPDEV 为分区
|
||||||
|
sudo losetup -d "$LOOPDEV"
|
||||||
|
sudo losetup "$LOOPDEV" "$target_image" -o "$offset" || { echo "错误:重新设置 loop 设备失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:Orange Pi Zero Rootfs 准备完成。"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 特定设备的文件配置函数 ---
|
||||||
|
|
||||||
|
config_cumebox2_files() {
|
||||||
|
echo "信息:为 Cumebox2 配置特定文件 (OLED, DTB)..."
|
||||||
|
ensure_dir "$ROOTFS/etc/oled"
|
||||||
|
|
||||||
|
# 自动下载 Cumebox2 相关文件(如果不存在)
|
||||||
|
local dtb_file="$SRCPATH/image/cumebox2/v-fix.dtb"
|
||||||
|
local ssd_file="$SRCPATH/image/cumebox2/ssd"
|
||||||
|
local config_file="$SRCPATH/image/cumebox2/config.json"
|
||||||
|
|
||||||
|
download_file_if_missing "$dtb_file" || echo "警告:下载 Cumebox2 DTB 失败"
|
||||||
|
download_file_if_missing "$ssd_file" || echo "警告:下载 Cumebox2 ssd 脚本失败"
|
||||||
|
download_file_if_missing "$config_file" || echo "警告:下载 Cumebox2 配置文件失败"
|
||||||
|
|
||||||
|
sudo cp "$dtb_file" "$ROOTFS/boot/dtb/amlogic/meson-gxl-s905x-khadas-vim.dtb" || echo "警告:复制 Cumebox2 DTB 失败"
|
||||||
|
sudo cp "$ssd_file" "$ROOTFS/usr/bin/" || echo "警告:复制 Cumebox2 ssd 脚本失败"
|
||||||
|
sudo chmod +x "$ROOTFS/usr/bin/ssd" || echo "警告:设置 ssd 脚本执行权限失败"
|
||||||
|
sudo cp "$config_file" "$ROOTFS/etc/oled/config.json" || echo "警告:复制 OLED 配置文件失败"
|
||||||
|
}
|
||||||
|
|
||||||
|
config_octopus_flanet_files() {
|
||||||
|
echo "信息:为 Octopus-Planet 配置特定文件 (model_database.conf)..."
|
||||||
|
|
||||||
|
# 自动下载 Octopus-Planet 相关文件(如果不存在)
|
||||||
|
local config_file="$SRCPATH/image/octopus-flanet/model_database.conf"
|
||||||
|
|
||||||
|
download_file_if_missing "$config_file" || echo "警告:下载 Octopus-Planet 配置文件失败"
|
||||||
|
|
||||||
|
sudo cp "$config_file" "$ROOTFS/etc/model_database.conf" || echo "警告:复制 model_database.conf 失败"
|
||||||
|
|
||||||
|
echo "信息:为 Octopus-Planet 添加 DRM 设备支持..."
|
||||||
|
run_in_chroot "sed -i \"/--device=\\/dev\\/video0/a\\ - \\\"--drm-device=/dev/dri/card0\\\"\" /etc/kvmd/override.yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
config_orangepi_zero_files() {
|
||||||
|
echo "信息:配置 Orange Pi Zero 特定文件..."
|
||||||
|
|
||||||
|
# 清空 modules.conf 文件,避免加载不必要的模块
|
||||||
|
run_in_chroot "echo 'libcomposite' > /etc/modules-load.d/modules.conf"
|
||||||
|
|
||||||
|
echo "信息:Orange Pi Zero 特定配置完成。"
|
||||||
|
}
|
||||||
|
|
||||||
|
config_onecloud_pro_files() {
|
||||||
|
echo "信息:配置 Onecloud Pro 特定文件..."
|
||||||
|
|
||||||
|
echo "信息:为 Onecloud Pro 添加 DRM 设备支持..."
|
||||||
|
run_in_chroot "sed -i \"/--device=\\/dev\\/video0/a\\ - \\\"--drm-device=/dev/dri/card0\\\"\" /etc/kvmd/override.yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
config_onecloud_files() {
|
||||||
|
echo "信息:配置 Onecloud 特定文件..."
|
||||||
|
|
||||||
|
echo "信息:为 Onecloud 添加 DRM 设备支持..."
|
||||||
|
run_in_chroot "sed -i \"/--device=\\/dev\\/video0/a\\ - \\\"--drm-device=/dev/dri/card1\\\"\" /etc/kvmd/override.yaml"
|
||||||
|
|
||||||
|
echo "信息:Onecloud 特定配置完成。"
|
||||||
|
}
|
||||||
|
|
||||||
|
oec_turbo_rootfs() {
|
||||||
|
local source_image="$SRCPATH/image/oec-turbo/Flash_Armbian_25.05.0_rockchip_efused-wxy-oec_bookworm_6.1.99_server_2025.03.20.img"
|
||||||
|
local target_image="$TMPDIR/rootfs.img"
|
||||||
|
local rootfs_offset=$((1409024 * 512)) # 根据分区7的起始扇区计算
|
||||||
|
|
||||||
|
echo "信息:准备 OEC-Turbo Rootfs (Debian 12)..."
|
||||||
|
ensure_dir "$TMPDIR"
|
||||||
|
|
||||||
|
echo "信息:下载或使用本地 OEC-Turbo 原始镜像..."
|
||||||
|
download_file_if_missing "$source_image" || { echo "错误:下载 OEC-Turbo 原始镜像失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
cp "$source_image" "$target_image" || { echo "错误:复制 OEC-Turbo 原始镜像失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
find_loop_device
|
||||||
|
# 设置 loop 设备指向 rootfs 分区 (分区7)
|
||||||
|
sudo losetup "$LOOPDEV" "$target_image" -o "$rootfs_offset" || { echo "错误:设置 loop 设备失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:OEC-Turbo Rootfs 准备完成,loop 设备 $LOOPDEV 已就绪。"
|
||||||
|
}
|
||||||
|
|
||||||
|
config_oec_turbo_files() {
|
||||||
|
echo "信息:配置 OEC-Turbo 特定文件..."
|
||||||
|
|
||||||
|
# 替换 override.yaml 中的硬件编码配置,启用 RK MPP 硬件编码
|
||||||
|
echo "信息:配置 VPU 硬件编码支持..."
|
||||||
|
run_in_chroot "sed -i 's/--h264-hwenc=disabled/--h264-hwenc=rkmpp/g' /etc/kvmd/override.yaml"
|
||||||
|
|
||||||
|
echo "信息:配置 udev 规则以授权 kvmd 组访问硬件设备..."
|
||||||
|
run_in_chroot "cat > /etc/udev/rules.d/99-kvmd-hw-access.rules <<'EOF'
|
||||||
|
# Generic hardware access for kvmd
|
||||||
|
# Safe on all platforms — rules only apply if device exists
|
||||||
|
|
||||||
|
# Rockchip MPP (rkmpp)
|
||||||
|
KERNEL==\"mpp_service\", GROUP=\"kvmd\", MODE=\"0660\"
|
||||||
|
|
||||||
|
# DMA-Heap (used by modern MPP)
|
||||||
|
SUBSYSTEM==\"dma_heap\", KERNEL==\"system\", GROUP=\"kvmd\", MODE=\"0660\"
|
||||||
|
SUBSYSTEM==\"dma_heap\", KERNEL==\"system-uncached\", GROUP=\"kvmd\", MODE=\"0660\"
|
||||||
|
SUBSYSTEM==\"dma_heap\", KERNEL==\"reserved\", GROUP=\"kvmd\", MODE=\"0660\"
|
||||||
|
|
||||||
|
# Optional legacy Rockchip devices
|
||||||
|
KERNEL==\"rkvdec\", GROUP=\"kvmd\", MODE=\"0660\"
|
||||||
|
KERNEL==\"rkvenc\", GROUP=\"kvmd\", MODE=\"0660\"
|
||||||
|
KERNEL==\"rga\", GROUP=\"kvmd\", MODE=\"0660\"
|
||||||
|
|
||||||
|
EOF"
|
||||||
|
|
||||||
|
# 替换 DTB 文件
|
||||||
|
replace_oec_turbo_dtb
|
||||||
|
|
||||||
|
echo "信息:OEC-Turbo 特定配置完成。"
|
||||||
|
}
|
||||||
|
|
||||||
|
replace_oec_turbo_dtb() {
|
||||||
|
local dtb_source="$SRCPATH/image/oec-turbo/rk3566-onething-oec-box.dtb"
|
||||||
|
local target_image="$TMPDIR/rootfs.img"
|
||||||
|
local boot_offset=$((360448 * 512)) # boot 分区6的偏移
|
||||||
|
local boot_mount="$TMPDIR/oec_boot_mount"
|
||||||
|
local dtb_target_path="dtb/rockchip/rk3566-onething-oec-box.dtb"
|
||||||
|
local boot_loopdev=""
|
||||||
|
|
||||||
|
echo "信息:替换 OEC-Turbo DTB 文件..."
|
||||||
|
|
||||||
|
if [ ! -f "$dtb_source" ]; then
|
||||||
|
echo "信息:尝试下载 DTB 文件..."
|
||||||
|
download_file_if_missing "$dtb_source"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "信息:为 boot 分区查找独立的 loop 设备..."
|
||||||
|
# 查找一个新的loop设备用于boot分区
|
||||||
|
boot_loopdev=$(losetup -f)
|
||||||
|
ensure_dir "$boot_mount"
|
||||||
|
|
||||||
|
losetup -o "$boot_offset" "$boot_loopdev" "$target_image"
|
||||||
|
mount "$boot_loopdev" "$boot_mount"
|
||||||
|
|
||||||
|
# 确保目标目录存在并复制 DTB 文件
|
||||||
|
mkdir -p "$boot_mount/$(dirname "$dtb_target_path")"
|
||||||
|
cp "$dtb_source" "$boot_mount/$dtb_target_path"
|
||||||
|
echo "信息:DTB 文件替换成功: $dtb_target_path"
|
||||||
|
|
||||||
|
umount "$boot_mount"
|
||||||
|
losetup -d "$boot_loopdev"
|
||||||
|
rmdir "$boot_mount"
|
||||||
|
}
|
||||||
386
build/functions/install.sh
Executable file
386
build/functions/install.sh
Executable file
@ -0,0 +1,386 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# --- 预准备 ---
|
||||||
|
|
||||||
|
prepare_dns_and_mirrors() {
|
||||||
|
echo "信息:在 chroot 环境中准备 DNS 和更换软件源..."
|
||||||
|
run_in_chroot "
|
||||||
|
mkdir -p /run/systemd/resolve/ \\
|
||||||
|
&& touch /run/systemd/resolve/stub-resolv.conf \\
|
||||||
|
&& printf '%s\\n' 'nameserver 1.1.1.1' 'nameserver 1.0.0.1' > /etc/resolv.conf \\
|
||||||
|
&& echo '信息:尝试更换镜像源...' \\
|
||||||
|
&& bash <(curl -sSL https://gitee.com/SuperManito/LinuxMirrors/raw/main/ChangeMirrors.sh) \\
|
||||||
|
--source mirrors.ustc.edu.cn --upgrade-software false --web-protocol http || echo '警告:更换镜像源脚本执行失败,可能网络不通或脚本已更改'
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_armbian_verify(){
|
||||||
|
echo "信息:在 chroot 环境中修改 Armbian 软件源..."
|
||||||
|
run_in_chroot "echo 'deb http://mirrors.ustc.edu.cn/armbian bullseye main bullseye-utils bullseye-desktop' > /etc/apt/sources.list.d/armbian.list"
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_external_binaries() {
|
||||||
|
local platform="$1" # linux/armhf or linux/amd64 or linux/aarch64
|
||||||
|
# 如果在 GitHub Actions 环境下,使用 silentwind0/kvmd-stage-0,否则用阿里云镜像
|
||||||
|
if is_github_actions; then
|
||||||
|
local docker_image="silentwind0/kvmd-stage-0"
|
||||||
|
else
|
||||||
|
local docker_image="registry.cn-hangzhou.aliyuncs.com/silentwind/kvmd-stage-0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "信息:准备外部预编译二进制文件 (平台: $platform)..."
|
||||||
|
ensure_dir "$PREBUILT_DIR"
|
||||||
|
|
||||||
|
echo "信息:拉取 Docker 镜像 $docker_image (平台: $platform)..."
|
||||||
|
sudo docker pull --platform "$platform" "$docker_image" || { echo "错误:拉取 Docker 镜像 $docker_image 失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:创建 Docker 容器 $DOCKER_CONTAINER_NAME ..."
|
||||||
|
sudo docker create --name "$DOCKER_CONTAINER_NAME" "$docker_image" || { echo "错误:创建 Docker 容器 $DOCKER_CONTAINER_NAME 失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:从 Docker 容器导出文件到 $PREBUILT_DIR ..."
|
||||||
|
sudo docker export "$DOCKER_CONTAINER_NAME" | sudo tar -xf - -C "$PREBUILT_DIR" || { echo "错误:导出并解压 Docker 容器内容失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:预编译二进制文件准备完成,存放于 $PREBUILT_DIR"
|
||||||
|
|
||||||
|
# 删除 Docker 容器
|
||||||
|
sudo docker rm -f "$DOCKER_CONTAINER_NAME" || { echo "错误:删除 Docker 容器 $DOCKER_CONTAINER_NAME 失败" >&2; exit 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
config_base_files() {
|
||||||
|
local platform_id="$1" # e.g., "onecloud", "cumebox2"
|
||||||
|
echo "信息:配置基础文件和目录结构 ($platform_id)..."
|
||||||
|
|
||||||
|
echo "信息:创建 KVMD 相关目录..."
|
||||||
|
ensure_dir "$ROOTFS/etc/kvmd/override.d"
|
||||||
|
ensure_dir "$ROOTFS/etc/kvmd/vnc"
|
||||||
|
ensure_dir "$ROOTFS/var/lib/kvmd/msd/images"
|
||||||
|
ensure_dir "$ROOTFS/var/lib/kvmd/msd/meta"
|
||||||
|
ensure_dir "$ROOTFS/opt/vc/bin"
|
||||||
|
ensure_dir "$ROOTFS/usr/share/kvmd"
|
||||||
|
ensure_dir "$ROOTFS/One-KVM"
|
||||||
|
ensure_dir "$ROOTFS/usr/share/janus/javascript"
|
||||||
|
ensure_dir "$ROOTFS/usr/lib/ustreamer/janus"
|
||||||
|
ensure_dir "$ROOTFS/run/kvmd"
|
||||||
|
ensure_dir "$ROOTFS/tmp/wheel/"
|
||||||
|
ensure_dir "$ROOTFS/usr/lib/janus/transports/"
|
||||||
|
ensure_dir "$ROOTFS/usr/lib/janus/loggers"
|
||||||
|
|
||||||
|
echo "信息:复制 One-KVM 源码..."
|
||||||
|
sudo rsync -a --exclude={.git,.github,output,tmp} . "$ROOTFS/One-KVM/" || { echo "错误:复制 One-KVM 源码失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:复制配置文件..."
|
||||||
|
sudo cp -r configs/kvmd/* configs/nginx configs/janus "$ROOTFS/etc/kvmd/"
|
||||||
|
sudo cp -r web extras contrib/keymaps "$ROOTFS/usr/share/kvmd/"
|
||||||
|
sudo cp testenv/fakes/vcgencmd "$ROOTFS/usr/bin/"
|
||||||
|
sudo cp -r testenv/js/* "$ROOTFS/usr/share/janus/javascript/"
|
||||||
|
sudo cp "build/platform/$platform_id" "$ROOTFS/usr/share/kvmd/platform" || { echo "错误:复制平台文件 build/platform/$platform_id 失败" >&2; exit 1; }
|
||||||
|
sudo cp scripts/kvmd-gencert scripts/kvmd-bootconfig scripts/kvmd-certbot scripts/kvmd-udev-hdmiusb-check scripts/kvmd-udev-restart-pass build/scripts/kvmd-firstrun.sh "$ROOTFS/usr/bin/"
|
||||||
|
sudo chmod +x "$ROOTFS/usr/bin/kvmd-gencert" "$ROOTFS/usr/bin/kvmd-bootconfig" "$ROOTFS/usr/bin/kvmd-certbot" "$ROOTFS/usr/bin/kvmd-udev-hdmiusb-check" "$ROOTFS/usr/bin/kvmd-udev-restart-pass" "$ROOTFS/usr/bin/kvmd-firstrun.sh"
|
||||||
|
|
||||||
|
# 尝试下载或使用本地 rc.local 文件
|
||||||
|
download_rc_local "$platform_id" || echo "信息:rc.local 文件不存在,跳过"
|
||||||
|
if [ -f "$SRCPATH/image/$platform_id/rc.local" ]; then
|
||||||
|
echo "信息:复制设备特定的 rc.local 文件..."
|
||||||
|
sudo cp "$SRCPATH/image/$platform_id/rc.local" "$ROOTFS/etc/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "信息:从预编译目录复制二进制文件和库..."
|
||||||
|
sudo cp "$PREBUILT_DIR/tmp/lib/"* "$ROOTFS/lib/"*-linux-*/ || echo "警告:复制 /tmp/lib/* 失败,可能源目录或目标目录不存在或不匹配"
|
||||||
|
sudo cp "$PREBUILT_DIR/tmp/ustreamer/ustreamer" "$PREBUILT_DIR/tmp/ustreamer/ustreamer-dump" "$PREBUILT_DIR/usr/bin/janus" "$ROOTFS/usr/bin/" || { echo "错误:复制 ustreamer/janus 二进制文件失败" >&2; exit 1; }
|
||||||
|
sudo cp "$PREBUILT_DIR/tmp/ustreamer/janus/libjanus_ustreamer.so" "$ROOTFS/usr/lib/ustreamer/janus/" || { echo "错误:复制 libjanus_ustreamer.so 失败" >&2; exit 1; }
|
||||||
|
sudo cp "$PREBUILT_DIR/tmp/wheel/"*.whl "$ROOTFS/tmp/wheel/" || { echo "错误:复制 Python wheel 文件失败" >&2; exit 1; }
|
||||||
|
sudo cp "$PREBUILT_DIR/usr/lib/janus/transports/"* "$ROOTFS/usr/lib/janus/transports/" || { echo "错误:复制 Janus transports 失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
# 禁用 apt-file
|
||||||
|
if [ -f "$ROOTFS/etc/apt/apt.conf.d/50apt-file.conf" ]; then
|
||||||
|
echo "信息:禁用 apt-file 配置..."
|
||||||
|
sudo mv "$ROOTFS/etc/apt/apt.conf.d/50apt-file.conf" "$ROOTFS/etc/apt/apt.conf.d/50apt-file.conf.disabled"
|
||||||
|
fi
|
||||||
|
echo "信息:基础文件配置完成。"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- KVMD 安装与配置 ---
|
||||||
|
|
||||||
|
install_base_packages() {
|
||||||
|
echo "信息:在 chroot 环境中更新源并安装基础软件包..."
|
||||||
|
run_in_chroot "
|
||||||
|
apt-get update && \\
|
||||||
|
apt install -y --no-install-recommends \\
|
||||||
|
libxkbcommon-x11-0 nginx tesseract-ocr tesseract-ocr-eng tesseract-ocr-chi-sim \\
|
||||||
|
iptables network-manager curl kmod libmicrohttpd12 libjansson4 libssl3 \\
|
||||||
|
libsofia-sip-ua0 libglib2.0-0 libopus0 libogg0 libcurl4 libconfig9 \\
|
||||||
|
python3-pip net-tools libavcodec59 libavformat59 libavutil57 libswscale6 \\
|
||||||
|
libavfilter8 libavdevice59 v4l-utils libv4l-0 nano unzip dnsmasq python3-systemd && \\
|
||||||
|
apt clean && \\
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_network() {
|
||||||
|
local network_type="$1" # "systemd-networkd" or others (default network-manager)
|
||||||
|
if [ "$network_type" = "systemd-networkd" ]; then
|
||||||
|
echo "信息:在 chroot 环境中配置 systemd-networkd..."
|
||||||
|
|
||||||
|
# onecloud 与 onecloud-pro 均启用基于 SN 的 MAC 地址生成
|
||||||
|
if [ "$TARGET_DEVICE_NAME" = "onecloud" ] || [ "$TARGET_DEVICE_NAME" = "onecloud-pro" ]; then
|
||||||
|
echo "信息:为 ${TARGET_DEVICE_NAME} 平台配置基于 SN 的 MAC 地址生成机制..."
|
||||||
|
|
||||||
|
# 复制MAC地址生成脚本
|
||||||
|
sudo cp "$SCRIPT_DIR/scripts/generate-random-mac.sh" "$ROOTFS/usr/local/bin/"
|
||||||
|
sudo chmod +x "$ROOTFS/usr/local/bin/generate-random-mac.sh"
|
||||||
|
|
||||||
|
# 复制systemd服务文件
|
||||||
|
sudo cp "$SCRIPT_DIR/services/kvmd-generate-mac.service" "$ROOTFS/etc/systemd/system/"
|
||||||
|
|
||||||
|
# 创建初始网络配置文件(不包含MAC地址,将由脚本生成)
|
||||||
|
run_in_chroot "
|
||||||
|
echo -e '[Match]\\nName=eth0\\n\\n[Network]\\nDHCP=yes' > /etc/systemd/network/99-eth0.network && \\
|
||||||
|
systemctl mask NetworkManager && \\
|
||||||
|
systemctl unmask systemd-networkd && \\
|
||||||
|
systemctl enable systemd-networkd systemd-resolved && \\
|
||||||
|
systemctl enable kvmd-generate-mac.service
|
||||||
|
"
|
||||||
|
echo "信息:${TARGET_DEVICE_NAME} 基于 SN 的 MAC 地址生成机制配置完成"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "信息:使用默认的网络管理器 (NetworkManager)..."
|
||||||
|
# 可能需要确保 NetworkManager 是启用的 (通常默认是)
|
||||||
|
run_in_chroot "systemctl enable NetworkManager"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_python_deps() {
|
||||||
|
echo "信息:在 chroot 环境中安装 Python 依赖 (wheels)..."
|
||||||
|
run_in_chroot "
|
||||||
|
pip3 install --no-cache-dir --break-system-packages /tmp/wheel/*.whl && \\
|
||||||
|
pip3 cache purge && \\
|
||||||
|
rm -rf /tmp/wheel
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_kvmd_core() {
|
||||||
|
echo "信息:在 chroot 环境中安装和配置 KVMD 核心..."
|
||||||
|
|
||||||
|
# 复制KVMD首次运行脚本和服务
|
||||||
|
echo "信息:配置KVMD首次运行初始化服务..."
|
||||||
|
sudo cp "build/services/kvmd-firstrun.service" "$ROOTFS/etc/systemd/system/"
|
||||||
|
|
||||||
|
# 安装KVMD但不执行需要在首次运行时完成的操作
|
||||||
|
run_in_chroot "
|
||||||
|
cd /One-KVM && \\
|
||||||
|
python3 setup.py install && \\
|
||||||
|
systemctl enable kvmd-firstrun.service
|
||||||
|
"
|
||||||
|
|
||||||
|
echo "信息:KVMD核心安装完成,证书生成等初始化操作将在首次开机时执行"
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_system() {
|
||||||
|
echo "信息:在 chroot 环境中配置系统级设置 (sudoers, udev, services)..."
|
||||||
|
run_in_chroot "
|
||||||
|
cat /One-KVM/configs/os/sudoers/v2-hdmiusb >> /etc/sudoers && \\
|
||||||
|
cat /One-KVM/configs/os/udev/v2-hdmiusb-rpi4.rules > /etc/udev/rules.d/99-kvmd.rules && \\
|
||||||
|
echo 'libcomposite' >> /etc/modules && \\
|
||||||
|
echo 'net.ipv4.ip_forward = 1' > /etc/sysctl.d/99-kvmd-extra.conf && \\
|
||||||
|
mv /usr/local/bin/kvmd* /usr/bin/ || echo '信息:/usr/local/bin/kvmd* 未找到或移动失败,可能已在/usr/bin' && \\
|
||||||
|
cp -r /One-KVM/configs/os/services/* /etc/systemd/system/ && \\
|
||||||
|
cp /One-KVM/configs/os/tmpfiles.conf /usr/lib/tmpfiles.d/ && \\
|
||||||
|
chmod +x /etc/update-motd.d/* || echo '警告:chmod /etc/update-motd.d/* 失败' && \\
|
||||||
|
echo 'kvmd ALL=(ALL) NOPASSWD: /etc/kvmd/custom_atx/gpio.sh' >> /etc/sudoers && \\
|
||||||
|
echo 'kvmd ALL=(ALL) NOPASSWD: /etc/kvmd/custom_atx/usbrelay_hid.sh' >> /etc/sudoers && \\
|
||||||
|
systemd-sysusers /One-KVM/configs/os/sysusers.conf && \\
|
||||||
|
systemd-sysusers /One-KVM/configs/os/kvmd-webterm.conf && \\
|
||||||
|
ln -sf /usr/share/tesseract-ocr/*/tessdata /usr/share/tessdata || echo '警告:创建 tesseract 链接失败' && \\
|
||||||
|
sed -i 's/8080/80/g' /etc/kvmd/override.yaml && \\
|
||||||
|
sed -i 's/4430/443/g' /etc/kvmd/override.yaml && \\
|
||||||
|
chown kvmd -R /var/lib/kvmd/msd/ && \\
|
||||||
|
rm /etc/resolv.conf && \\
|
||||||
|
printf '%s\\n' 'nameserver 1.1.1.1' 'nameserver 1.0.0.1' > /etc/resolv.conf && \
|
||||||
|
systemctl enable dnsmasq kvmd kvmd-otg kvmd-nginx kvmd-vnc kvmd-ipmi kvmd-webterm kvmd-janus kvmd-media kvmd-gostc && \\
|
||||||
|
systemctl disable nginx systemd-resolved && \\
|
||||||
|
rm -rf /One-KVM
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_webterm() {
|
||||||
|
local arch="$1" # armhf, aarch64, x86_64
|
||||||
|
local ttyd_arch="$arch"
|
||||||
|
|
||||||
|
if [ "$arch" = "armhf" ]; then
|
||||||
|
ttyd_arch="armhf"
|
||||||
|
elif [ "$arch" = "amd64" ]; then
|
||||||
|
ttyd_arch="x86_64"
|
||||||
|
elif [ "$arch" = "aarch64" ]; then
|
||||||
|
ttyd_arch="aarch64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "信息:在 chroot 环境中下载并安装 ttyd ($ttyd_arch)..."
|
||||||
|
run_in_chroot "
|
||||||
|
curl -L https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.${ttyd_arch} -o /usr/bin/ttyd && \\
|
||||||
|
chmod +x /usr/bin/ttyd && \\
|
||||||
|
mkdir -p /home/kvmd-webterm && \\
|
||||||
|
chown kvmd-webterm /home/kvmd-webterm
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_gostc() {
|
||||||
|
local arch="$1" # armhf, aarch64, x86_64
|
||||||
|
local gostc_arch="$arch"
|
||||||
|
local gostc_version="v2.0.8-beta.2"
|
||||||
|
|
||||||
|
# 根据架构映射下载文件名
|
||||||
|
case "$arch" in
|
||||||
|
armhf) gostc_arch="arm_7" ;;
|
||||||
|
aarch64) gostc_arch="arm64_v8.0" ;;
|
||||||
|
x86_64|amd64) gostc_arch="amd64_v1" ;;
|
||||||
|
*) echo "错误:不支持的架构 $arch"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "信息:在 chroot 环境中下载并安装 gostc ($gostc_arch)..."
|
||||||
|
run_in_chroot "
|
||||||
|
mkdir -p /tmp/gostc && cd /tmp/gostc && \\
|
||||||
|
curl -L https://github.com/mofeng-git/gostc-open/releases/download/${gostc_version}/gostc_linux_${gostc_arch}.tar.gz -o gostc.tar.gz && \\
|
||||||
|
tar -xzf gostc.tar.gz && \\
|
||||||
|
mv gostc /usr/bin/ && \\
|
||||||
|
chmod +x /usr/bin/gostc && \\
|
||||||
|
cd / && rm -rf /tmp/gostc
|
||||||
|
"
|
||||||
|
|
||||||
|
echo "信息:创建 gostc systemd 服务文件..."
|
||||||
|
run_in_chroot "
|
||||||
|
cat > /etc/systemd/system/kvmd-gostc.service << 'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=基于FRP开发的内网穿透 客户端/节点
|
||||||
|
ConditionFileIsExecutable=/usr/bin/gostc
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
StartLimitInterval=5
|
||||||
|
StartLimitBurst=10
|
||||||
|
ExecStart=/usr/bin/gostc \"-web-addr\" \"0.0.0.0:18080\"
|
||||||
|
WorkingDirectory=/usr/bin
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
EnvironmentFile=-/etc/sysconfig/gostc
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
"
|
||||||
|
|
||||||
|
echo "信息:gostc 安装和配置完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_kvmd_tweaks() {
|
||||||
|
local arch="$1" # armhf, aarch64, x86_64
|
||||||
|
local device_type="$2" # "gpio" or "video1" or other
|
||||||
|
local atx_setting=""
|
||||||
|
local hid_setting=""
|
||||||
|
|
||||||
|
echo "信息:根据架构 ($arch) 和设备类型 ($device_type) 调整 KVMD 配置..."
|
||||||
|
|
||||||
|
if [ "$arch" = "x86_64" ] || [ "$arch" = "amd64" ]; then
|
||||||
|
echo "信息:目标平台为 x86_64/amd64 架构,禁用 OTG,设置 ATX 为 USBRELAY_HID..."
|
||||||
|
run_in_chroot "
|
||||||
|
systemctl disable kvmd-otg && \\
|
||||||
|
sed -i 's/^ATX=.*/ATX=USBRELAY_HID/' /etc/kvmd/atx.sh && \\
|
||||||
|
sed -i 's/device: \/dev\/ttyUSB0/device: \/dev\/kvmd-hid/g' /etc/kvmd/override.yaml
|
||||||
|
"
|
||||||
|
else
|
||||||
|
echo "信息::目标平台为 ARM 架构 ($arch)..."
|
||||||
|
# ARM 架构,配置 HID 为 OTG
|
||||||
|
hid_setting="otg"
|
||||||
|
run_in_chroot "
|
||||||
|
sed -i 's/#type: otg/type: otg/g' /etc/kvmd/override.yaml && \\
|
||||||
|
sed -i 's/device: \/dev\/ttyUSB0/#device: \/dev\/ttyUSB0/g' /etc/kvmd/override.yaml # 注释掉 ttyUSB0
|
||||||
|
"
|
||||||
|
echo "信息:设置 HID 为 $hid_setting"
|
||||||
|
run_in_chroot "sed -i 's/type: ch9329/type: $hid_setting/g' /etc/kvmd/override.yaml"
|
||||||
|
|
||||||
|
|
||||||
|
# 根据 device_type 配置 ATX
|
||||||
|
if [[ "$device_type" == *"gpio-onecloud-pro"* ]]; then
|
||||||
|
echo "信息:电源控制设备类型为 gpio,设置 ATX 为 GPIO 并配置引脚..."
|
||||||
|
atx_setting="GPIO"
|
||||||
|
run_in_chroot "
|
||||||
|
sed -i 's/^ATX=.*/ATX=GPIO/' /etc/kvmd/atx.sh && \\
|
||||||
|
sed -i 's/SHUTDOWNPIN/gpiochip0 7/g' /etc/kvmd/custom_atx/gpio.sh && \\
|
||||||
|
sed -i 's/REBOOTPIN/gpiochip0 11/g' /etc/kvmd/custom_atx/gpio.sh
|
||||||
|
"
|
||||||
|
elif [[ "$device_type" == *"gpio-onecloud"* ]]; then
|
||||||
|
echo "信息:电源控制设备类型为 gpio,设置 ATX 为 GPIO 并配置引脚..."
|
||||||
|
atx_setting="GPIO"
|
||||||
|
run_in_chroot "
|
||||||
|
sed -i 's/^ATX=.*/ATX=GPIO/' /etc/kvmd/atx.sh && \\
|
||||||
|
sed -i 's/SHUTDOWNPIN/gpiochip1 7/g' /etc/kvmd/custom_atx/gpio.sh && \\
|
||||||
|
sed -i 's/REBOOTPIN/gpiochip0 11/g' /etc/kvmd/custom_atx/gpio.sh
|
||||||
|
"
|
||||||
|
else
|
||||||
|
echo "信息:电源控制设备类型不是 gpio ($device_type),设置 ATX 为 USBRELAY_HID..."
|
||||||
|
atx_setting="USBRELAY_HID"
|
||||||
|
run_in_chroot "sed -i 's/^ATX=.*/ATX=USBRELAY_HID/' /etc/kvmd/atx.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 配置视频设备
|
||||||
|
if [[ "$device_type" == *"video1"* ]]; then
|
||||||
|
echo "信息:视频设备类型为 video1,设置视频设备为 /dev/video1..."
|
||||||
|
run_in_chroot "sed -i 's|/dev/video0|/dev/video1|g' /etc/kvmd/override.yaml"
|
||||||
|
elif [[ "$device_type" == *"video1"* ]]; then
|
||||||
|
echo "信息:视频设备类型为 kvmd-video,设置视频设备为 /dev/kvmd-video..."
|
||||||
|
run_in_chroot "sed -i 's|/dev/video0|/dev/kvmd-video|g' /etc/kvmd/override.yaml"
|
||||||
|
else
|
||||||
|
echo "信息:使用默认视频设备 /dev/video0..."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "信息:KVMD 配置调整完成。"
|
||||||
|
|
||||||
|
run_in_chroot "apt remove -y --purge systemd-resolved"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 整体安装流程 ---
|
||||||
|
install_and_configure_kvmd() {
|
||||||
|
local arch="$1" # 架构: armhf, aarch64, x86_64/amd64
|
||||||
|
local device_type="$2" # 设备特性: "gpio", "video1", "" (空或其他)
|
||||||
|
local network_type="$3" # 网络配置: "systemd-networkd", "" (默认 network-manager)
|
||||||
|
local host_arch="" # Docker 平台架构: arm, aarch64, amd64
|
||||||
|
|
||||||
|
# 映射架构名称
|
||||||
|
case "$arch" in
|
||||||
|
armhf) host_arch="arm" ;;
|
||||||
|
aarch64) host_arch="arm64" ;; # docker aarch64 平台名是 arm64
|
||||||
|
x86_64|amd64) host_arch="amd64"; arch="x86_64" ;; # 统一内部使用 x86_64
|
||||||
|
*) echo "错误:不支持的架构 $arch"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
|
||||||
|
prepare_external_binaries "linux/$host_arch"
|
||||||
|
config_base_files "$TARGET_DEVICE_NAME" # 使用全局变量传递设备名
|
||||||
|
|
||||||
|
# 特定设备的额外文件配置 (如果存在)
|
||||||
|
# 将设备名中的连字符转换为下划线以匹配函数名
|
||||||
|
local device_func_name="${TARGET_DEVICE_NAME//-/_}"
|
||||||
|
if declare -f "config_${device_func_name}_files" > /dev/null; then
|
||||||
|
echo "信息:执行特定设备的文件配置函数 config_${device_func_name}_files ..."
|
||||||
|
"config_${device_func_name}_files"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 某些镜像可能需要准备DNS和换源
|
||||||
|
if [[ "$NEED_PREPARE_DNS" = true ]]; then
|
||||||
|
prepare_dns_and_mirrors
|
||||||
|
fi
|
||||||
|
# 可选:强制使用特定armbian源
|
||||||
|
# delete_armbian_verify
|
||||||
|
|
||||||
|
# 执行安装步骤
|
||||||
|
install_base_packages
|
||||||
|
configure_network "$network_type"
|
||||||
|
install_python_deps
|
||||||
|
configure_kvmd_core
|
||||||
|
install_gostc "$arch" # 安装 gostc
|
||||||
|
configure_system
|
||||||
|
install_webterm "$arch" # 传递原始架构名给ttyd下载
|
||||||
|
apply_kvmd_tweaks "$arch" "$device_type"
|
||||||
|
|
||||||
|
run_in_chroot "df -h" # 显示最终磁盘使用情况
|
||||||
|
echo "信息:One-KVM 安装和配置完成。"
|
||||||
|
}
|
||||||
105
build/functions/packaging.sh
Executable file
105
build/functions/packaging.sh
Executable file
@ -0,0 +1,105 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# --- 压缩函数 ---
|
||||||
|
|
||||||
|
# 压缩镜像文件(仅在 GitHub Actions 环境中)
|
||||||
|
compress_image_file() {
|
||||||
|
local file_path="$1"
|
||||||
|
|
||||||
|
if is_github_actions && [[ -f "$file_path" ]]; then
|
||||||
|
echo "信息:压缩镜像文件: $file_path"
|
||||||
|
if xz -9 -vv "$file_path"; then
|
||||||
|
echo "信息:压缩完成: ${file_path}.xz"
|
||||||
|
else
|
||||||
|
echo "警告:压缩文件 $file_path 失败"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 打包函数 ---
|
||||||
|
|
||||||
|
pack_img() {
|
||||||
|
local device_name_friendly="$1" # e.g., "Vm", "Cumebox2"
|
||||||
|
local target_img_name="One-KVM_by-SilentWind_${device_name_friendly}_${DATE}.img"
|
||||||
|
local source_img="$TMPDIR/rootfs.img"
|
||||||
|
|
||||||
|
echo "信息:开始打包镜像 ($device_name_friendly)..."
|
||||||
|
ensure_dir "$OUTPUTDIR"
|
||||||
|
|
||||||
|
# 确保在打包前已经正确卸载了所有挂载点和loop设备
|
||||||
|
if [[ "$ROOTFS_MOUNTED" -eq 1 || "$DEV_MOUNTED" -eq 1 || "$SYS_MOUNTED" -eq 1 || "$PROC_MOUNTED" -eq 1 || -n "$LOOPDEV" && -b "$LOOPDEV" ]]; then
|
||||||
|
echo "警告:发现未卸载的挂载点或loop设备,尝试再次卸载..."
|
||||||
|
unmount_all
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "信息:移动镜像文件 $source_img 到 $OUTPUTDIR/$target_img_name ..."
|
||||||
|
sudo mv "$source_img" "$OUTPUTDIR/$target_img_name" || { echo "错误:移动镜像文件失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
if [ "$device_name_friendly" = "Vm" ]; then
|
||||||
|
echo "信息:为 Vm 目标转换镜像格式 (vmdk, vdi)..."
|
||||||
|
local raw_img="$OUTPUTDIR/$target_img_name"
|
||||||
|
local vmdk_img="$OUTPUTDIR/One-KVM_by-SilentWind_Vmare-uefi_${DATE}.vmdk"
|
||||||
|
local vdi_img="$OUTPUTDIR/One-KVM_by-SilentWind_Virtualbox-uefi_${DATE}.vdi"
|
||||||
|
|
||||||
|
echo "信息:转换为 VMDK..."
|
||||||
|
sudo qemu-img convert -f raw -O vmdk "$raw_img" "$vmdk_img" || echo "警告:转换为 VMDK 失败"
|
||||||
|
echo "信息:转换为 VDI..."
|
||||||
|
sudo qemu-img convert -f raw -O vdi "$raw_img" "$vdi_img" || echo "警告:转换为 VDI 失败"
|
||||||
|
|
||||||
|
# 在 GitHub Actions 环境中压缩 VM 镜像文件
|
||||||
|
if is_github_actions; then
|
||||||
|
echo "信息:在 GitHub Actions 环境中压缩 VM 镜像文件..."
|
||||||
|
compress_image_file "$raw_img"
|
||||||
|
compress_image_file "$vmdk_img"
|
||||||
|
compress_image_file "$vdi_img"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# 在 GitHub Actions 环境中压缩镜像文件
|
||||||
|
if is_github_actions; then
|
||||||
|
echo "信息:在 GitHub Actions 环境中压缩镜像文件..."
|
||||||
|
compress_image_file "$OUTPUTDIR/$target_img_name"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "信息:镜像打包完成: $OUTPUTDIR/$target_img_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
pack_img_onecloud() {
|
||||||
|
local target_img_name="One-KVM_by-SilentWind_Onecloud_${DATE}.burn.img"
|
||||||
|
local rootfs_raw_img="$TMPDIR/rootfs.img"
|
||||||
|
local rootfs_sparse_img="$TMPDIR/7.rootfs.PARTITION.sparse"
|
||||||
|
local aml_packer="$SRCPATH/image/onecloud/AmlImg_v0.3.1_linux_amd64"
|
||||||
|
|
||||||
|
echo "信息:开始为 Onecloud 打包 burn 镜像..."
|
||||||
|
ensure_dir "$OUTPUTDIR"
|
||||||
|
|
||||||
|
# 确保在打包前已经正确卸载了所有挂载点和loop设备
|
||||||
|
if [[ "$ROOTFS_MOUNTED" -eq 1 || "$DEV_MOUNTED" -eq 1 || "$SYS_MOUNTED" -eq 1 || "$PROC_MOUNTED" -eq 1 || -n "$LOOPDEV" && -b "$LOOPDEV" ]]; then
|
||||||
|
echo "警告:发现未卸载的挂载点或loop设备,尝试再次卸载..."
|
||||||
|
unmount_all
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 自动下载 AmlImg 工具(如果不存在)
|
||||||
|
download_file_if_missing "$aml_packer" || { echo "错误:下载 AmlImg 工具失败" >&2; exit 1; }
|
||||||
|
sudo chmod +x "$aml_packer" || { echo "错误:设置 AmlImg 工具执行权限失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:将 raw rootfs 转换为 sparse image..."
|
||||||
|
# 先删除可能存在的旧 sparse 文件
|
||||||
|
sudo rm -f "$rootfs_sparse_img"
|
||||||
|
sudo img2simg "$rootfs_raw_img" "$rootfs_sparse_img" || { echo "错误:img2simg 转换失败" >&2; exit 1; }
|
||||||
|
sudo rm "$rootfs_raw_img" # 删除 raw 文件,因为它已被转换
|
||||||
|
|
||||||
|
echo "信息:使用 AmlImg 工具打包..."
|
||||||
|
sudo "$aml_packer" pack "$OUTPUTDIR/$target_img_name" "$TMPDIR/" || { echo "错误:AmlImg 打包失败" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "信息:清理 Onecloud 临时文件..."
|
||||||
|
sudo rm -f "$TMPDIR/6.boot.PARTITION.sparse" "$TMPDIR/7.rootfs.PARTITION.sparse" "$TMPDIR/dts.img"
|
||||||
|
|
||||||
|
# 在 GitHub Actions 环境中压缩 Onecloud 镜像文件
|
||||||
|
if is_github_actions; then
|
||||||
|
echo "信息:在 GitHub Actions 环境中压缩 Onecloud 镜像文件..."
|
||||||
|
compress_image_file "$OUTPUTDIR/$target_img_name"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "信息:Onecloud burn 镜像打包完成: $OUTPUTDIR/$target_img_name"
|
||||||
|
}
|
||||||
@ -71,8 +71,9 @@ if [ ! -f /etc/kvmd/.init_flag ]; then
|
|||||||
|
|
||||||
# 设置用户名和密码
|
# 设置用户名和密码
|
||||||
if [ ! -z "$USERNAME" ] && [ ! -z "$PASSWORD" ]; then
|
if [ ! -z "$USERNAME" ] && [ ! -z "$PASSWORD" ]; then
|
||||||
|
# 设置自定义用户名和密码
|
||||||
if python -m kvmd.apps.htpasswd del admin \
|
if python -m kvmd.apps.htpasswd del admin \
|
||||||
&& echo "$PASSWORD" | python -m kvmd.apps.htpasswd set -i "$USERNAME" \
|
&& echo "$PASSWORD" | python -m kvmd.apps.htpasswd add -i "$USERNAME" \
|
||||||
&& echo "$PASSWORD -> $USERNAME:$PASSWORD" > /etc/kvmd/vncpasswd \
|
&& echo "$PASSWORD -> $USERNAME:$PASSWORD" > /etc/kvmd/vncpasswd \
|
||||||
&& echo "$USERNAME:$PASSWORD -> $USERNAME:$PASSWORD" > /etc/kvmd/ipmipasswd; then
|
&& echo "$USERNAME:$PASSWORD -> $USERNAME:$PASSWORD" > /etc/kvmd/ipmipasswd; then
|
||||||
log_info "用户凭据设置成功"
|
log_info "用户凭据设置成功"
|
||||||
@ -80,6 +81,16 @@ if [ ! -f /etc/kvmd/.init_flag ]; then
|
|||||||
log_error "用户凭据设置失败"
|
log_error "用户凭据设置失败"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
elif [ ! -z "$PASSWORD" ] && [ -z "$USERNAME" ]; then
|
||||||
|
# 只设置密码,保持admin用户名
|
||||||
|
if echo "$PASSWORD" | python -m kvmd.apps.htpasswd set -i "admin" \
|
||||||
|
&& echo "$PASSWORD -> admin:$PASSWORD" > /etc/kvmd/vncpasswd \
|
||||||
|
&& echo "admin:$PASSWORD -> admin:$PASSWORD" > /etc/kvmd/ipmipasswd; then
|
||||||
|
log_info "admin 用户密码设置成功"
|
||||||
|
else
|
||||||
|
log_error "admin 用户密码设置失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
log_warn "未设置 USERNAME 和 PASSWORD 环境变量,使用默认值(admin/admin)"
|
log_warn "未设置 USERNAME 和 PASSWORD 环境变量,使用默认值(admin/admin)"
|
||||||
fi
|
fi
|
||||||
@ -109,7 +120,7 @@ if [ ! -f /etc/kvmd/.init_flag ]; then
|
|||||||
log_info "已禁用 WebTerm 功能"
|
log_info "已禁用 WebTerm 功能"
|
||||||
rm -r /usr/share/kvmd/extras/webterm
|
rm -r /usr/share/kvmd/extras/webterm
|
||||||
else
|
else
|
||||||
cat >> /etc/supervisord.conf << EOF
|
cat >> /etc/kvmd/supervisord.conf << EOF
|
||||||
|
|
||||||
[program:kvmd-webterm]
|
[program:kvmd-webterm]
|
||||||
command=/usr/local/bin/ttyd --interface=/run/kvmd/ttyd.sock --port=0 --writable /bin/bash -c '/etc/kvmd/armbain-motd; bash'
|
command=/usr/local/bin/ttyd --interface=/run/kvmd/ttyd.sock --port=0 --writable /bin/bash -c '/etc/kvmd/armbain-motd; bash'
|
||||||
@ -125,14 +136,14 @@ EOF
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$NOWEBTERMWRITE" == "1" ]; then
|
if [ "$NOWEBTERMWRITE" == "1" ]; then
|
||||||
sed -i "s/--writable//g" /etc/supervisord.conf
|
sed -i "s/--writable//g" /etc/kvmd/supervisord.conf
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$NOVNC" == "1" ]; then
|
if [ "$NOVNC" == "1" ]; then
|
||||||
log_info "已禁用 VNC 功能"
|
log_info "已禁用 VNC 功能"
|
||||||
rm -r /usr/share/kvmd/extras/vnc
|
rm -r /usr/share/kvmd/extras/vnc
|
||||||
else
|
else
|
||||||
cat >> /etc/supervisord.conf << EOF
|
cat >> /etc/kvmd/supervisord.conf << EOF
|
||||||
|
|
||||||
[program:kvmd-vnc]
|
[program:kvmd-vnc]
|
||||||
command=python -m kvmd.apps.vnc --run
|
command=python -m kvmd.apps.vnc --run
|
||||||
@ -148,10 +159,10 @@ EOF
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$NOIPMI" == "1" ]; then
|
if [ "$NOIPMI" == "1" ]; then
|
||||||
log_info "已禁用IPMI功能"
|
log_info "已禁用 IPMI 功能"
|
||||||
rm -r /usr/share/kvmd/extras/ipmi
|
rm -r /usr/share/kvmd/extras/ipmi
|
||||||
else
|
else
|
||||||
cat >> /etc/supervisord.conf << EOF
|
cat >> /etc/kvmd/supervisord.conf << EOF
|
||||||
|
|
||||||
[program:kvmd-ipmi]
|
[program:kvmd-ipmi]
|
||||||
command=python -m kvmd.apps.ipmi --run
|
command=python -m kvmd.apps.ipmi --run
|
||||||
@ -166,11 +177,30 @@ redirect_stderr=true
|
|||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$NOGOSTC" == "1" ]; then
|
||||||
|
log_info "已禁用 GOSTC 功能"
|
||||||
|
rm -rf /usr/share/kvmd/extras/gostc
|
||||||
|
else
|
||||||
|
cat >> /etc/kvmd/supervisord.conf << EOF
|
||||||
|
|
||||||
|
[program:kvmd-gostc]
|
||||||
|
command=/usr/bin/gostc -web-addr 127.0.0.1:18080
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
startsecs=5
|
||||||
|
priority=300
|
||||||
|
stopasgroup=true
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes = 0
|
||||||
|
redirect_stderr=true
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
#switch OTG config
|
#switch OTG config
|
||||||
if [ "$OTG" == "1" ]; then
|
if [ "$OTG" == "1" ]; then
|
||||||
log_info "已启用 OTG 功能"
|
log_info "已启用 OTG 功能"
|
||||||
sed -i "s/ch9329/otg/g" /etc/kvmd/override.yaml
|
sed -i "s/ch9329/otg/g" /etc/kvmd/override.yaml
|
||||||
sed -i "s/device: \/dev\/ttyUSB0//g" /etc/kvmd/override.yaml
|
sed -i "s|device: /dev/ttyUSB0||g" /etc/kvmd/override.yaml
|
||||||
if [ "$NOMSD" == 1 ]; then
|
if [ "$NOMSD" == 1 ]; then
|
||||||
log_info "已禁用 MSD 功能"
|
log_info "已禁用 MSD 功能"
|
||||||
else
|
else
|
||||||
@ -179,8 +209,8 @@ EOF
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -z "$VIDEONUM" ]; then
|
if [ ! -z "$VIDEONUM" ]; then
|
||||||
if sed -i "s/\/dev\/video0/\/dev\/video$VIDEONUM/g" /etc/kvmd/override.yaml && \
|
if sed -i "s|/dev/video0|/dev/video$VIDEONUM|g" /etc/kvmd/override.yaml && \
|
||||||
sed -i "s/\/dev\/video0/\/dev\/video$VIDEONUM/g" /etc/kvmd/janus/janus.plugin.ustreamer.jcfg; then
|
sed -i "s|/dev/video0|/dev/video$VIDEONUM|g" /etc/kvmd/janus/janus.plugin.ustreamer.jcfg; then
|
||||||
log_info "视频设备已设置为 /dev/video$VIDEONUM"
|
log_info "视频设备已设置为 /dev/video$VIDEONUM"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@ -197,6 +227,12 @@ EOF
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "$CH9329NUM" ]; then
|
||||||
|
if sed -i "s|/dev/ttyUSB0|/dev/ttyUSB$CH9329NUM|g" /etc/kvmd/override.yaml; then
|
||||||
|
log_info "CH9329 串口设备已设置为 $CH9329NUM"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [ ! -z "$CH9329TIMEOUT" ]; then
|
if [ ! -z "$CH9329TIMEOUT" ]; then
|
||||||
if sed -i "s/read_timeout: 0.3/read_timeout: $CH9329TIMEOUT/g" /etc/kvmd/override.yaml; then
|
if sed -i "s/read_timeout: 0.3/read_timeout: $CH9329TIMEOUT/g" /etc/kvmd/override.yaml; then
|
||||||
log_info "CH9329 超时已设置为 $CH9329TIMEOUT 秒"
|
log_info "CH9329 超时已设置为 $CH9329TIMEOUT 秒"
|
||||||
@ -210,11 +246,31 @@ EOF
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -z "$VIDEOFORMAT" ]; then
|
if [ ! -z "$VIDEOFORMAT" ]; then
|
||||||
if sed -i "s/format=mjpeg/format=$VIDFORMAT/g" /etc/kvmd/override.yaml; then
|
if sed -i "s/--format=mjpeg/--format=$VIDEOFORMAT/g" /etc/kvmd/override.yaml; then
|
||||||
log_info "视频输入格式已设置为 $VIDFORMAT"
|
log_info "视频输入格式已设置为 $VIDEOFORMAT"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "$HWENCODER" ]; then
|
||||||
|
if sed -i "s/--h264-hwenc=disabled/--h264-hwenc=$HWENCODER/g" /etc/kvmd/override.yaml; then
|
||||||
|
log_info "硬件编码器已设置为 $HWENCODER"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 设置WEB端口
|
||||||
|
if [ ! -z "$HTTPPORT" ]; then
|
||||||
|
if sed -i "s/port: 8080/port: $HTTPPORT/g" /etc/kvmd/override.yaml; then
|
||||||
|
log_info "HTTP 端口已设置为 $HTTPPORT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "$HTTPSPORT" ]; then
|
||||||
|
if sed -i "s/port: 4430/port: $HTTPSPORT/g" /etc/kvmd/override.yaml; then
|
||||||
|
log_info "HTTPS 端口已设置为 $HTTPSPORT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
touch /etc/kvmd/.init_flag
|
touch /etc/kvmd/.init_flag
|
||||||
log_info "初始化配置完成"
|
log_info "初始化配置完成"
|
||||||
fi
|
fi
|
||||||
@ -241,4 +297,4 @@ if [ "$OTG" == "1" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
log_info "One-KVM 配置文件准备完成,正在启动服务..."
|
log_info "One-KVM 配置文件准备完成,正在启动服务..."
|
||||||
exec supervisord -c /etc/supervisord.conf
|
exec supervisord -c /etc/kvmd/supervisord.conf
|
||||||
|
|||||||
3
build/platform/oec-turbo
Normal file
3
build/platform/oec-turbo
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
PIKVM_MODEL=v2_model
|
||||||
|
PIKVM_VIDEO=usb_video
|
||||||
|
PIKVM_BOARD=oec-turbo
|
||||||
3
build/platform/onecloud-pro
Normal file
3
build/platform/onecloud-pro
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
PIKVM_MODEL=v2_model
|
||||||
|
PIKVM_VIDEO=usb_video
|
||||||
|
PIKVM_BOARD=onecloud-pro
|
||||||
3
build/platform/orangepi-zero
Normal file
3
build/platform/orangepi-zero
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
PIKVM_MODEL=v2_model
|
||||||
|
PIKVM_VIDEO=usb_video
|
||||||
|
PIKVM_BOARD=orangepi-zero
|
||||||
21
build/record.txt
Normal file
21
build/record.txt
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
wget https://github.com/hzyitc/AmlImg/releases/download/v0.3.1/AmlImg_v0.3.1_linux_amd64 -O /mnt/src/image/onecloud/AmlImg_v0.3.1_linux_amd64
|
||||||
|
chmod +x /mnt/src/image/onecloud/AmlImg_v0.3.1_linux_amd64
|
||||||
|
|
||||||
|
|
||||||
|
#!/bin/bash
|
||||||
|
# 文件映射脚本
|
||||||
|
# 本地目录前缀:/mnt
|
||||||
|
# 远程URL前缀:https://files.mofeng.run
|
||||||
|
|
||||||
|
LOCAL_PREFIX="/mnt"
|
||||||
|
REMOTE_PREFIX="https://files.mofeng.run"
|
||||||
|
|
||||||
|
# 文件相对路径
|
||||||
|
REL_PATH="src/image/onecloud/Armbian_by-SilentWind_24.5.0-trunk_Onecloud_bookworm_legacy_5.9.0-rc7_minimal_support-dvd-emulation.burn.img"
|
||||||
|
|
||||||
|
LOCAL_FILE="$LOCAL_PREFIX/$REL_PATH"
|
||||||
|
REMOTE_URL="$REMOTE_PREFIX/$REL_PATH"
|
||||||
|
|
||||||
|
echo "下载 $REMOTE_URL 到 $LOCAL_FILE"
|
||||||
|
mkdir -p "$(dirname "$LOCAL_FILE")"
|
||||||
|
wget -O "$LOCAL_FILE" "$REMOTE_URL"
|
||||||
122
build/scripts/generate-random-mac.sh
Normal file
122
build/scripts/generate-random-mac.sh
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 为玩客云/玩客云Pro 平台生成 MAC 地址的一次性脚本
|
||||||
|
# 此脚本在首次开机时执行,为 eth0 网卡生成并应用基于 SN 的 MAC 地址,失败时回退到随机 MAC
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
NETWORK_CONFIG="/etc/systemd/network/99-eth0.network"
|
||||||
|
LOCK_FILE="/var/lib/kvmd/.mac-generated"
|
||||||
|
PLATFORM_FILE="/usr/share/kvmd/platform"
|
||||||
|
EFUSE_SYSFS_PATH=""
|
||||||
|
SN_PREFIX=""
|
||||||
|
SN_EXPECTED_LENGTH=13
|
||||||
|
|
||||||
|
# 按平台设置 EFUSE 与 SN 参数;未知平台时按 efuse 路径探测
|
||||||
|
detect_platform_params() {
|
||||||
|
local platform=""
|
||||||
|
if [ -f "$PLATFORM_FILE" ]; then
|
||||||
|
platform=$(tr -d '\n' < "$PLATFORM_FILE")
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$platform" in
|
||||||
|
onecloud)
|
||||||
|
EFUSE_SYSFS_PATH="/sys/bus/nvmem/devices/meson8b-efuse0/nvmem"
|
||||||
|
SN_PREFIX="OCP"
|
||||||
|
;;
|
||||||
|
onecloud-pro)
|
||||||
|
EFUSE_SYSFS_PATH="/sys/devices/platform/efuse/efuse0/nvmem"
|
||||||
|
SN_PREFIX="ODC"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -z "$EFUSE_SYSFS_PATH" ] || [ -z "$SN_PREFIX" ]; then
|
||||||
|
if [ -e "/sys/devices/platform/efuse/efuse0/nvmem" ]; then
|
||||||
|
EFUSE_SYSFS_PATH="/sys/devices/platform/efuse/efuse0/nvmem"
|
||||||
|
SN_PREFIX="ODC"
|
||||||
|
elif [ -e "/sys/bus/nvmem/devices/meson8b-efuse0/nvmem" ]; then
|
||||||
|
EFUSE_SYSFS_PATH="/sys/bus/nvmem/devices/meson8b-efuse0/nvmem"
|
||||||
|
SN_PREFIX="OCP"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查是否已经执行过
|
||||||
|
if [ -f "$LOCK_FILE" ]; then
|
||||||
|
echo "MAC地址已经生成过,跳过执行"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 生成MAC地址函数
|
||||||
|
generate_random_mac() {
|
||||||
|
detect_platform_params
|
||||||
|
# 尝试根据 SN 生成唯一 MAC 地址
|
||||||
|
if [ -f "$EFUSE_SYSFS_PATH" ]; then
|
||||||
|
sn_offset=$(grep --binary-files=text -boP "$SN_PREFIX" "$EFUSE_SYSFS_PATH" | head -n1 | cut -d: -f1)
|
||||||
|
if [ -n "$sn_offset" ]; then
|
||||||
|
sn=$(cat "$EFUSE_SYSFS_PATH" | dd bs=1 skip="$sn_offset" count="$SN_EXPECTED_LENGTH" 2>/dev/null)
|
||||||
|
if [ ${#sn} -eq $SN_EXPECTED_LENGTH ]; then
|
||||||
|
echo "S/N: $sn" >&2 # 输出到 stderr,避免干扰返回值
|
||||||
|
# 使用 SN 的 SHA-256 哈希生成后 5 字节(避免多余管道)
|
||||||
|
sn_hash=$(printf %s "$sn" | sha256sum | cut -d' ' -f1)
|
||||||
|
# 直接用 Bash 子串获取哈希末 10 个字符并插入分隔符
|
||||||
|
mac_hex=${sn_hash: -10}
|
||||||
|
mac_suffix=$(printf "%s:%s:%s:%s:%s" "${mac_hex:0:2}" "${mac_hex:2:2}" "${mac_hex:4:2}" "${mac_hex:6:2}" "${mac_hex:8:2}")
|
||||||
|
printf "02:%s\n" "$mac_suffix"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 若 SN 获取失败,回退到随机逻辑
|
||||||
|
echo "警告: 无法获取 SN,回退到随机 MAC 生成" >&2
|
||||||
|
printf "02:%02x:%02x:%02x:%02x:%02x\n" \
|
||||||
|
$((RANDOM % 256)) \
|
||||||
|
$((RANDOM % 256)) \
|
||||||
|
$((RANDOM % 256)) \
|
||||||
|
$((RANDOM % 256)) \
|
||||||
|
$((RANDOM % 256))
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "正在生成基于 SN 的 MAC 地址..."
|
||||||
|
|
||||||
|
# 生成新的MAC地址
|
||||||
|
NEW_MAC=$(generate_random_mac)
|
||||||
|
echo "生成的MAC地址: $NEW_MAC"
|
||||||
|
|
||||||
|
# 验证 MAC 地址格式
|
||||||
|
if ! [[ $NEW_MAC =~ ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$ ]]; then
|
||||||
|
echo "错误: 生成的 MAC 地址格式无效: $NEW_MAC"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 备份原配置文件
|
||||||
|
if [ -f "$NETWORK_CONFIG" ]; then
|
||||||
|
cp "$NETWORK_CONFIG" "${NETWORK_CONFIG}.backup"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 更新网络配置文件
|
||||||
|
cat > "$NETWORK_CONFIG" << EOF
|
||||||
|
[Match]
|
||||||
|
Name=eth0
|
||||||
|
|
||||||
|
[Network]
|
||||||
|
DHCP=yes
|
||||||
|
|
||||||
|
[Link]
|
||||||
|
MACAddress=$NEW_MAC
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "已更新网络配置文件: $NETWORK_CONFIG"
|
||||||
|
|
||||||
|
# 创建锁定文件,防止重复执行
|
||||||
|
mkdir -p "$(dirname "$LOCK_FILE")"
|
||||||
|
echo "MAC地址生成时间: $(date)" > "$LOCK_FILE"
|
||||||
|
|
||||||
|
# 禁用此服务,确保只运行一次
|
||||||
|
systemctl disable kvmd-generate-mac.service
|
||||||
|
|
||||||
|
echo "MAC地址生成完成: $NEW_MAC"
|
||||||
|
echo "服务已自动禁用,下次开机不会再执行"
|
||||||
|
|
||||||
|
exit 0
|
||||||
34
build/scripts/kvmd-firstrun.sh
Normal file
34
build/scripts/kvmd-firstrun.sh
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# KVMD首次运行初始化脚本
|
||||||
|
# 在首次开机时执行KVMD服务启动前的必要初始化操作
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
LOCK_FILE="/var/lib/kvmd/.kvmd-firstrun-completed"
|
||||||
|
|
||||||
|
# 检查是否已经执行过
|
||||||
|
[ -f "$LOCK_FILE" ] && { echo "[KVMD-FirstRun] 初始化已完成,跳过执行"; exit 0; }
|
||||||
|
|
||||||
|
echo "[KVMD-FirstRun] 开始KVMD首次运行初始化..."
|
||||||
|
|
||||||
|
# 1. 生成KVMD主证书
|
||||||
|
echo "[KVMD-FirstRun] 生成KVMD主证书..."
|
||||||
|
kvmd-gencert --do-the-thing
|
||||||
|
|
||||||
|
# 2. 生成VNC证书
|
||||||
|
echo "[KVMD-FirstRun] 生成VNC证书..."
|
||||||
|
kvmd-gencert --do-the-thing --vnc
|
||||||
|
|
||||||
|
# 3. 生成nginx配置文件
|
||||||
|
echo "[KVMD-FirstRun] 生成nginx配置文件..."
|
||||||
|
kvmd-nginx-mkconf /etc/kvmd/nginx/nginx.conf.mako /etc/kvmd/nginx/nginx.conf || echo "[KVMD-FirstRun] 警告: nginx配置生成失败"
|
||||||
|
|
||||||
|
# 创建锁定文件
|
||||||
|
mkdir -p "$(dirname "$LOCK_FILE")"
|
||||||
|
echo "KVMD首次运行初始化完成 - $(date)" > "$LOCK_FILE"
|
||||||
|
|
||||||
|
# 禁用服务
|
||||||
|
systemctl disable kvmd-firstrun.service || echo "[KVMD-FirstRun] 警告: 服务禁用失败"
|
||||||
|
|
||||||
|
echo "[KVMD-FirstRun] 初始化完成!"
|
||||||
26
build/services/kvmd-firstrun.service
Normal file
26
build/services/kvmd-firstrun.service
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=KVMD First Run Initialization (One-time)
|
||||||
|
Documentation=https://github.com/your-repo/One-KVM
|
||||||
|
Before=kvmd.service
|
||||||
|
Before=kvmd-nginx.service
|
||||||
|
Before=kvmd-otg.service
|
||||||
|
Before=kvmd-vnc.service
|
||||||
|
Before=kvmd-ipmi.service
|
||||||
|
Before=kvmd-webterm.service
|
||||||
|
Before=kvmd-janus.service
|
||||||
|
Before=kvmd-media.service
|
||||||
|
After=local-fs.target
|
||||||
|
After=network.target
|
||||||
|
Wants=local-fs.target
|
||||||
|
ConditionPathExists=!/var/lib/kvmd/.kvmd-firstrun-completed
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/kvmd-firstrun.sh
|
||||||
|
RemainAfterExit=yes
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
TimeoutStartSec=300
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
18
build/services/kvmd-generate-mac.service
Normal file
18
build/services/kvmd-generate-mac.service
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Generate Random MAC Address for OneCloud (One-time)
|
||||||
|
Documentation=https://github.com/your-repo/One-KVM
|
||||||
|
Before=systemd-networkd.service
|
||||||
|
Before=network-pre.target
|
||||||
|
Wants=network-pre.target
|
||||||
|
After=local-fs.target
|
||||||
|
ConditionPathExists=!/var/lib/kvmd/.mac-generated
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/generate-random-mac.sh
|
||||||
|
RemainAfterExit=yes
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
82
check-code.sh
Executable file
82
check-code.sh
Executable file
@ -0,0 +1,82 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 本地代码质量检查脚本
|
||||||
|
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
echo "🔍 运行代码质量检查..."
|
||||||
|
|
||||||
|
# 检查参数,如果有参数则只运行指定的检查
|
||||||
|
CHECK_TYPE="${1:-all}"
|
||||||
|
|
||||||
|
run_flake8() {
|
||||||
|
echo "📝 运行 flake8 代码风格检查..."
|
||||||
|
flake8 --config=testenv/linters/flake8.ini kvmd testenv/tests *.py
|
||||||
|
}
|
||||||
|
|
||||||
|
run_pylint() {
|
||||||
|
echo "🔎 运行 pylint 代码质量分析..."
|
||||||
|
pylint -j0 --rcfile=testenv/linters/pylint.ini --output-format=colorized --reports=no kvmd testenv/tests *.py || true
|
||||||
|
}
|
||||||
|
|
||||||
|
run_mypy() {
|
||||||
|
echo "🔧 运行 mypy 类型检查..."
|
||||||
|
mypy --config-file=testenv/linters/mypy.ini --cache-dir=testenv/.mypy_cache kvmd testenv/tests *.py || true
|
||||||
|
}
|
||||||
|
|
||||||
|
run_vulture() {
|
||||||
|
echo "💀 运行 vulture 死代码检测..."
|
||||||
|
vulture --ignore-names=_format_P,Plugin --ignore-decorators=@exposed_http,@exposed_ws,@pytest.fixture kvmd testenv/tests *.py testenv/linters/vulture-wl.py || true
|
||||||
|
}
|
||||||
|
|
||||||
|
run_eslint() {
|
||||||
|
echo "📜 运行 eslint JavaScript检查..."
|
||||||
|
if command -v eslint >/dev/null 2>&1; then
|
||||||
|
eslint --cache-location=/tmp --config=testenv/linters/eslintrc.js --color web/share/js || true
|
||||||
|
else
|
||||||
|
echo "⚠️ eslint 未安装,跳过"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_htmlhint() {
|
||||||
|
echo "📄 运行 htmlhint HTML检查..."
|
||||||
|
if command -v htmlhint >/dev/null 2>&1; then
|
||||||
|
htmlhint --config=testenv/linters/htmlhint.json web/*.html web/*/*.html || true
|
||||||
|
else
|
||||||
|
echo "⚠️ htmlhint 未安装,跳过"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_shellcheck() {
|
||||||
|
echo "🐚 运行 shellcheck Shell脚本检查..."
|
||||||
|
if command -v shellcheck >/dev/null 2>&1; then
|
||||||
|
shellcheck --color=always kvmd.install scripts/* || true
|
||||||
|
else
|
||||||
|
echo "⚠️ shellcheck 未安装,跳过"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$CHECK_TYPE" in
|
||||||
|
flake8) run_flake8 ;;
|
||||||
|
pylint) run_pylint ;;
|
||||||
|
mypy) run_mypy ;;
|
||||||
|
vulture) run_vulture ;;
|
||||||
|
eslint) run_eslint ;;
|
||||||
|
htmlhint) run_htmlhint ;;
|
||||||
|
shellcheck) run_shellcheck ;;
|
||||||
|
all)
|
||||||
|
run_flake8
|
||||||
|
run_pylint
|
||||||
|
run_mypy
|
||||||
|
run_vulture
|
||||||
|
run_eslint
|
||||||
|
run_htmlhint
|
||||||
|
run_shellcheck
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "用法: $0 [flake8|pylint|mypy|vulture|eslint|htmlhint|shellcheck|all]"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "✅ 代码质量检查完成!"
|
||||||
@ -1,5 +1,5 @@
|
|||||||
general: {
|
general: {
|
||||||
debug_level = 2
|
debug_level = 4
|
||||||
}
|
}
|
||||||
nat: {
|
nat: {
|
||||||
nice_debug = false
|
nice_debug = false
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
video: {
|
video: {
|
||||||
sink = "kvmd::ustreamer::h264"
|
sink = "kvmd::ustreamer::h264"
|
||||||
}
|
}
|
||||||
audio: {
|
acap: {
|
||||||
device = "hw:0"
|
device = "hw:0,0"
|
||||||
tc358743 = "/dev/video0"
|
tc358743 = "/dev/video0"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
# #
|
# #
|
||||||
# ========================================================================== #
|
# ========================================================================== #
|
||||||
|
|
||||||
|
ATX=USBRELAY_HID
|
||||||
echo $ATX
|
echo $ATX
|
||||||
case $ATX in
|
case $ATX in
|
||||||
GPIO)
|
GPIO)
|
||||||
@ -31,4 +32,4 @@ case $ATX in
|
|||||||
*)
|
*)
|
||||||
echo "No thing."
|
echo "No thing."
|
||||||
exit -1
|
exit -1
|
||||||
esac
|
esac
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
admin:$apr1$.6mu9N8n$xOuGesr4JZZkdiZo/j318.
|
admin:{SSHA512}3zSmw/L9zIkpQdX5bcy6HntTxltAzTuGNP6NjHRRgOcNZkA0K+Lsrj3QplO9Gr3BA5MYVVki9rAVnFNCcIdtYC6FkLJWCmHs
|
||||||
|
|||||||
@ -1,14 +1,11 @@
|
|||||||
# This file describes the credentials for IPMI users. The first pair separated by colon
|
# This file describes the credentials for IPMI users in format "login:password",
|
||||||
# is the login and password with which the user can access to IPMI. The second pair
|
# one per line. The passwords are NOT encrypted.
|
||||||
# is the name and password with which the user can access to KVMD API. The arrow is used
|
|
||||||
# as a separator and shows the direction of user registration in the system.
|
|
||||||
#
|
#
|
||||||
# WARNING! IPMI protocol is completely unsafe by design. In short, the authentication
|
# WARNING! IPMI protocol is completely unsafe by design. In short, the authentication
|
||||||
# process for IPMI 2.0 mandates that the server send a salted SHA1 or MD5 hash of the
|
# process for IPMI 2.0 mandates that the server send a salted SHA1 or MD5 hash of the
|
||||||
# requested user's password to the client, prior to the client authenticating. Never use
|
# requested user's password to the client, prior to the client authenticating.
|
||||||
# the same passwords for KVMD and IPMI users. This default configuration is shown here
|
|
||||||
# for example only.
|
|
||||||
#
|
#
|
||||||
# And even better not to use IPMI. Instead, you can directly use KVMD API via curl.
|
# NEVER use the same passwords for KVMD and IPMI users.
|
||||||
|
# This default configuration is shown here just for the example only.
|
||||||
|
|
||||||
admin:admin -> admin:admin
|
admin:admin
|
||||||
|
|||||||
97
configs/kvmd/main/v4mini-hdmi-rpi4.yaml
Normal file
97
configs/kvmd/main/v4mini-hdmi-rpi4.yaml
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
info:
|
||||||
|
hw:
|
||||||
|
ignore_past: true
|
||||||
|
fan:
|
||||||
|
unix: /run/kvmd/fan.sock
|
||||||
|
|
||||||
|
hid:
|
||||||
|
type: otg
|
||||||
|
|
||||||
|
atx:
|
||||||
|
type: gpio
|
||||||
|
power_led_pin: 4
|
||||||
|
hdd_led_pin: 5
|
||||||
|
power_switch_pin: 23
|
||||||
|
reset_switch_pin: 27
|
||||||
|
|
||||||
|
msd:
|
||||||
|
type: otg
|
||||||
|
|
||||||
|
streamer:
|
||||||
|
h264_bitrate:
|
||||||
|
default: 5000
|
||||||
|
cmd:
|
||||||
|
- "/usr/bin/ustreamer"
|
||||||
|
- "--device=/dev/kvmd-video"
|
||||||
|
- "--persistent"
|
||||||
|
- "--dv-timings"
|
||||||
|
- "--format=uyvy"
|
||||||
|
- "--buffers=6"
|
||||||
|
- "--encoder=m2m-image"
|
||||||
|
- "--workers=3"
|
||||||
|
- "--quality={quality}"
|
||||||
|
- "--desired-fps={desired_fps}"
|
||||||
|
- "--drop-same-frames=30"
|
||||||
|
- "--unix={unix}"
|
||||||
|
- "--unix-rm"
|
||||||
|
- "--unix-mode=0660"
|
||||||
|
- "--exit-on-parent-death"
|
||||||
|
- "--process-name-prefix={process_name_prefix}"
|
||||||
|
- "--notify-parent"
|
||||||
|
- "--no-log-colors"
|
||||||
|
- "--jpeg-sink=kvmd::ustreamer::jpeg"
|
||||||
|
- "--jpeg-sink-mode=0660"
|
||||||
|
- "--h264-sink=kvmd::ustreamer::h264"
|
||||||
|
- "--h264-sink-mode=0660"
|
||||||
|
- "--h264-bitrate={h264_bitrate}"
|
||||||
|
- "--h264-gop={h264_gop}"
|
||||||
|
|
||||||
|
gpio:
|
||||||
|
drivers:
|
||||||
|
__v4_locator__:
|
||||||
|
type: locator
|
||||||
|
|
||||||
|
scheme:
|
||||||
|
__v3_usb_breaker__:
|
||||||
|
pin: 22
|
||||||
|
mode: output
|
||||||
|
initial: true
|
||||||
|
pulse: false
|
||||||
|
|
||||||
|
__v4_locator__:
|
||||||
|
driver: __v4_locator__
|
||||||
|
pin: 12
|
||||||
|
mode: output
|
||||||
|
pulse: false
|
||||||
|
|
||||||
|
__v4_const1__:
|
||||||
|
pin: 6
|
||||||
|
mode: output
|
||||||
|
initial: false
|
||||||
|
switch: false
|
||||||
|
pulse: false
|
||||||
|
|
||||||
|
|
||||||
|
media:
|
||||||
|
memsink:
|
||||||
|
h264:
|
||||||
|
sink: "kvmd::ustreamer::h264"
|
||||||
|
|
||||||
|
|
||||||
|
vnc:
|
||||||
|
memsink:
|
||||||
|
jpeg:
|
||||||
|
sink: "kvmd::ustreamer::jpeg"
|
||||||
|
h264:
|
||||||
|
sink: "kvmd::ustreamer::h264"
|
||||||
@ -17,8 +17,6 @@ kvmd:
|
|||||||
|
|
||||||
hid:
|
hid:
|
||||||
type: otg
|
type: otg
|
||||||
mouse_alt:
|
|
||||||
device: /dev/kvmd-hid-mouse-alt
|
|
||||||
|
|
||||||
atx:
|
atx:
|
||||||
type: gpio
|
type: gpio
|
||||||
|
|||||||
@ -4,11 +4,11 @@
|
|||||||
# will be displayed in the web interface.
|
# will be displayed in the web interface.
|
||||||
|
|
||||||
server:
|
server:
|
||||||
host: localhost.localdomain
|
host: "@auto"
|
||||||
|
|
||||||
kvm: {
|
kvm: {
|
||||||
base_on: PiKVM,
|
base_on: "PiKVM",
|
||||||
app_name: One-KVM,
|
app_name: "One-KVM",
|
||||||
main_version: 241204,
|
main_version: "241204",
|
||||||
author: SilentWind
|
author: "SilentWind"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,7 +37,7 @@ kvmd:
|
|||||||
forever: true
|
forever: true
|
||||||
|
|
||||||
desired_fps:
|
desired_fps:
|
||||||
default: 30
|
default: 60
|
||||||
max: 60
|
max: 60
|
||||||
|
|
||||||
h264_bitrate:
|
h264_bitrate:
|
||||||
@ -48,7 +48,7 @@ kvmd:
|
|||||||
- "--device=/dev/video0"
|
- "--device=/dev/video0"
|
||||||
- "--persistent"
|
- "--persistent"
|
||||||
- "--format=mjpeg"
|
- "--format=mjpeg"
|
||||||
- "--encoder=LIBX264-VIDEO"
|
- "--encoder=FFMPEG-VIDEO"
|
||||||
- "--resolution={resolution}"
|
- "--resolution={resolution}"
|
||||||
- "--desired-fps={desired_fps}"
|
- "--desired-fps={desired_fps}"
|
||||||
- "--drop-same-frames=30"
|
- "--drop-same-frames=30"
|
||||||
@ -66,7 +66,7 @@ kvmd:
|
|||||||
- "--jpeg-sink-mode=0660"
|
- "--jpeg-sink-mode=0660"
|
||||||
- "--h264-bitrate={h264_bitrate}"
|
- "--h264-bitrate={h264_bitrate}"
|
||||||
- "--h264-gop={h264_gop}"
|
- "--h264-gop={h264_gop}"
|
||||||
- "--h264-preset=ultrafast"
|
- "--h264-hwenc=disabled"
|
||||||
- "--slowdown"
|
- "--slowdown"
|
||||||
gpio:
|
gpio:
|
||||||
drivers:
|
drivers:
|
||||||
@ -157,10 +157,6 @@ media:
|
|||||||
|
|
||||||
jpeg:
|
jpeg:
|
||||||
sink: 'kvmd::ustreamer::jpeg'
|
sink: 'kvmd::ustreamer::jpeg'
|
||||||
janus:
|
|
||||||
stun:
|
|
||||||
host: stun.cloudflare.com
|
|
||||||
port: 3478
|
|
||||||
|
|
||||||
otgnet:
|
otgnet:
|
||||||
commands:
|
commands:
|
||||||
@ -168,6 +164,9 @@ otgnet:
|
|||||||
- "/bin/true"
|
- "/bin/true"
|
||||||
pre_stop_cmd:
|
pre_stop_cmd:
|
||||||
- "/bin/true"
|
- "/bin/true"
|
||||||
|
sysctl_cmd:
|
||||||
|
#- "/usr/sbin/sysctl"
|
||||||
|
- "/bin/true"
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
http:
|
http:
|
||||||
|
|||||||
@ -63,4 +63,3 @@ stopasgroup=true
|
|||||||
stdout_logfile=/dev/stdout
|
stdout_logfile=/dev/stdout
|
||||||
stdout_logfile_maxbytes = 0
|
stdout_logfile_maxbytes = 0
|
||||||
redirect_stderr=true
|
redirect_stderr=true
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,9 @@
|
|||||||
# This file describes the credentials for VNCAuth. The left part before arrow is a passphrase
|
# This file contains passwords for the legacy VNCAuth, one per line.
|
||||||
# for VNCAuth. The right part is username and password with which the user can access to KVMD API.
|
# The passwords are NOT encrypted.
|
||||||
# The arrow is used as a separator and shows the relationship of user registrations on the system.
|
|
||||||
#
|
#
|
||||||
# Never use the same passwords for VNC and IPMI users. This default configuration is shown here
|
# WARNING! The VNCAuth method is NOT secure and should not be used at all.
|
||||||
# for example only.
|
# But we support it for compatibility with some clients.
|
||||||
#
|
#
|
||||||
# If this file does not contain any entries, VNCAuth will be disabled and you will only be able
|
# NEVER use the same passwords for KVMD, IPMI and VNCAuth users.
|
||||||
# to login in using your KVMD username and password using VeNCrypt methods.
|
|
||||||
|
|
||||||
# pa$$phr@se -> admin:password
|
|
||||||
admin -> admin:admin
|
admin -> admin:admin
|
||||||
|
|||||||
@ -24,6 +24,7 @@ location @login {
|
|||||||
|
|
||||||
location /login {
|
location /login {
|
||||||
root /usr/share/kvmd/web;
|
root /usr/share/kvmd/web;
|
||||||
|
include /etc/kvmd/nginx/loc-nocache.conf;
|
||||||
auth_request off;
|
auth_request off;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +66,7 @@ location /api/hid/print {
|
|||||||
proxy_pass http://kvmd;
|
proxy_pass http://kvmd;
|
||||||
include /etc/kvmd/nginx/loc-proxy.conf;
|
include /etc/kvmd/nginx/loc-proxy.conf;
|
||||||
include /etc/kvmd/nginx/loc-bigpost.conf;
|
include /etc/kvmd/nginx/loc-bigpost.conf;
|
||||||
|
proxy_read_timeout 7d;
|
||||||
auth_request off;
|
auth_request off;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,2 @@
|
|||||||
limit_rate 6250k;
|
|
||||||
limit_rate_after 50k;
|
|
||||||
client_max_body_size 0;
|
client_max_body_size 0;
|
||||||
proxy_request_buffering off;
|
proxy_request_buffering off;
|
||||||
|
|||||||
@ -39,9 +39,9 @@ http {
|
|||||||
% if https_enabled:
|
% if https_enabled:
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen ${http_port};
|
listen ${http_ipv4}:${http_port};
|
||||||
% if ipv6_enabled:
|
% if ipv6_enabled:
|
||||||
listen [::]:${http_port};
|
listen [${http_ipv6}]:${http_port};
|
||||||
% endif
|
% endif
|
||||||
include /etc/kvmd/nginx/certbot.ctx-server.conf;
|
include /etc/kvmd/nginx/certbot.ctx-server.conf;
|
||||||
location / {
|
location / {
|
||||||
@ -54,9 +54,9 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen ${https_port} ssl http2;
|
listen ${https_ipv4}:${https_port} ssl;
|
||||||
% if ipv6_enabled:
|
% if ipv6_enabled:
|
||||||
listen [::]:${https_port} ssl http2;
|
listen [${https_ipv6}]:${https_port} ssl;
|
||||||
% endif
|
% endif
|
||||||
include /etc/kvmd/nginx/ssl.conf;
|
include /etc/kvmd/nginx/ssl.conf;
|
||||||
include /etc/kvmd/nginx/kvmd.ctx-server.conf;
|
include /etc/kvmd/nginx/kvmd.ctx-server.conf;
|
||||||
@ -66,9 +66,9 @@ http {
|
|||||||
% else:
|
% else:
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen ${http_port};
|
listen ${http_ipv4}:${http_port};
|
||||||
% if ipv6_enabled:
|
% if ipv6_enabled:
|
||||||
listen [::]:${http_port};
|
listen [${http_ipv6}]:${http_port};
|
||||||
% endif
|
% endif
|
||||||
include /etc/kvmd/nginx/certbot.ctx-server.conf;
|
include /etc/kvmd/nginx/certbot.ctx-server.conf;
|
||||||
include /etc/kvmd/nginx/kvmd.ctx-server.conf;
|
include /etc/kvmd/nginx/kvmd.ctx-server.conf;
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
initramfs initramfs-linux.img followkernel
|
initramfs initramfs-linux.img followkernel
|
||||||
|
|
||||||
hdmi_force_hotplug=1
|
hdmi_force_hotplug=1
|
||||||
gpu_mem=128
|
gpu_mem=192
|
||||||
enable_uart=1
|
enable_uart=1
|
||||||
dtoverlay=disable-bt
|
dtoverlay=disable-bt
|
||||||
|
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
s/rootwait/rootwait cma=128M/g
|
s/rootwait/rootwait cma=192M/g
|
||||||
|
|||||||
16
configs/os/services/kvmd-localhid.service
Normal file
16
configs/os/services/kvmd-localhid.service
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=PiKVM - Local HID to KVMD proxy
|
||||||
|
After=kvmd.service systemd-udevd.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=kvmd-localhid
|
||||||
|
Group=kvmd-localhid
|
||||||
|
Type=simple
|
||||||
|
Restart=always
|
||||||
|
RestartSec=3
|
||||||
|
|
||||||
|
ExecStart=/usr/bin/kvmd-localhid --run
|
||||||
|
TimeoutStopSec=3
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@ -1,6 +1,6 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=One-KVM - The main daemon
|
Description=One-KVM - The main daemon
|
||||||
After=network.target network-online.target nss-lookup.target
|
After=network.target network-online.target nss-lookup.target rc-local.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
User=kvmd
|
User=kvmd
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
# Fix https://github.com/pikvm/pikvm/issues/1514:
|
||||||
|
# Wait for any single network interface, not all configured ones
|
||||||
|
# (Rationale: when user configures Wi-Fi via pikvm.txt or otherwise,
|
||||||
|
# we do not delete the Ethernet config, which means it will remain active
|
||||||
|
# regardless of whether the user ever intended to use Ethernet.)
|
||||||
|
[Service]
|
||||||
|
ExecStart=
|
||||||
|
ExecStart=/usr/lib/systemd/systemd-networkd-wait-online --any
|
||||||
@ -1,8 +1,10 @@
|
|||||||
g kvmd - -
|
g kvmd - -
|
||||||
|
g kvmd-selfauth - -
|
||||||
g kvmd-media - -
|
g kvmd-media - -
|
||||||
g kvmd-pst - -
|
g kvmd-pst - -
|
||||||
g kvmd-ipmi - -
|
g kvmd-ipmi - -
|
||||||
g kvmd-vnc - -
|
g kvmd-vnc - -
|
||||||
|
g kvmd-localhid - -
|
||||||
g kvmd-nginx - -
|
g kvmd-nginx - -
|
||||||
g kvmd-janus - -
|
g kvmd-janus - -
|
||||||
g kvmd-certbot - -
|
g kvmd-certbot - -
|
||||||
@ -12,6 +14,7 @@ u kvmd-media - "PiKVM - The media proxy"
|
|||||||
u kvmd-pst - "PiKVM - Persistent storage" -
|
u kvmd-pst - "PiKVM - Persistent storage" -
|
||||||
u kvmd-ipmi - "PiKVM - IPMI to KVMD proxy" -
|
u kvmd-ipmi - "PiKVM - IPMI to KVMD proxy" -
|
||||||
u kvmd-vnc - "PiKVM - VNC to KVMD/Streamer proxy" -
|
u kvmd-vnc - "PiKVM - VNC to KVMD/Streamer proxy" -
|
||||||
|
u kvmd-localhid - "PiKVM - Local HID to KVMD proxy" -
|
||||||
u kvmd-nginx - "PiKVM - HTTP entrypoint" -
|
u kvmd-nginx - "PiKVM - HTTP entrypoint" -
|
||||||
u kvmd-janus - "PiKVM - Janus WebRTC Gateway" -
|
u kvmd-janus - "PiKVM - Janus WebRTC Gateway" -
|
||||||
u kvmd-certbot - "PiKVM - Certbot-Renew for KVMD-Nginx"
|
u kvmd-certbot - "PiKVM - Certbot-Renew for KVMD-Nginx"
|
||||||
@ -29,10 +32,16 @@ m kvmd-media kvmd
|
|||||||
m kvmd-pst kvmd
|
m kvmd-pst kvmd
|
||||||
|
|
||||||
m kvmd-ipmi kvmd
|
m kvmd-ipmi kvmd
|
||||||
|
m kvmd-ipmi kvmd-selfauth
|
||||||
|
|
||||||
m kvmd-vnc kvmd
|
m kvmd-vnc kvmd
|
||||||
|
m kvmd-vnc kvmd-selfauth
|
||||||
m kvmd-vnc kvmd-certbot
|
m kvmd-vnc kvmd-certbot
|
||||||
|
|
||||||
|
m kvmd-localhid input
|
||||||
|
m kvmd-localhid kvmd
|
||||||
|
m kvmd-localhid kvmd-selfauth
|
||||||
|
|
||||||
m kvmd-janus kvmd
|
m kvmd-janus kvmd
|
||||||
m kvmd-janus audio
|
m kvmd-janus audio
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,15 @@
|
|||||||
# Here are described some bindings for PiKVM devices.
|
# Here are described some bindings for PiKVM devices.
|
||||||
# Do not edit this file.
|
# Do not edit this file.
|
||||||
KERNEL=="ttyACM[0-9]*", SUBSYSTEM=="tty", SUBSYSTEMS=="usb", ATTRS{idVendor}=="1209", ATTRS{idProduct}=="eda3", SYMLINK+="kvmd-hid-bridge"
|
|
||||||
KERNEL=="ttyACM[0-9]*", SUBSYSTEM=="tty", SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="1080", SYMLINK+="kvmd-switch"
|
ACTION!="remove", KERNEL=="ttyACM[0-9]*", SUBSYSTEM=="tty", SUBSYSTEMS=="usb", ATTRS{idVendor}=="1209", ATTRS{idProduct}=="eda3", SYMLINK+="kvmd-hid-bridge"
|
||||||
|
ACTION!="remove", KERNEL=="ttyACM[0-9]*", SUBSYSTEM=="tty", SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="1080", SYMLINK+="kvmd-switch"
|
||||||
|
|
||||||
|
# Disable USB autosuspend for critical devices
|
||||||
|
ACTION!="remove", SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="eda3", GOTO="kvmd-usb"
|
||||||
|
ACTION!="remove", SUBSYSTEM=="usb", ATTR{idVendor}=="2e8a", ATTR{idProduct}=="1080", GOTO="kvmd-usb"
|
||||||
|
GOTO="end"
|
||||||
|
|
||||||
|
LABEL="kvmd-usb"
|
||||||
|
ATTR{power/control}="on", ATTR{power/autosuspend_delay_ms}="-1"
|
||||||
|
|
||||||
|
LABEL="end"
|
||||||
|
|||||||
@ -4,3 +4,4 @@ KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", PROGRAM="/usr/bin/kvmd-udev-hdm
|
|||||||
KERNEL=="hidg0", GROUP="kvmd", SYMLINK+="kvmd-hid-keyboard"
|
KERNEL=="hidg0", GROUP="kvmd", SYMLINK+="kvmd-hid-keyboard"
|
||||||
KERNEL=="hidg1", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse"
|
KERNEL=="hidg1", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse"
|
||||||
KERNEL=="hidg2", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse-alt"
|
KERNEL=="hidg2", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse-alt"
|
||||||
|
KERNEL=="ttyUSB0", GROUP="kvmd", SYMLINK+="kvmd-hid"
|
||||||
|
|||||||
1663
contrib/keymaps/en-us-colemak
Normal file
1663
contrib/keymaps/en-us-colemak
Normal file
File diff suppressed because it is too large
Load Diff
@ -49,13 +49,15 @@ oneeighth 0x03 shift altgr
|
|||||||
quotedbl 0x04
|
quotedbl 0x04
|
||||||
3 0x04 shift
|
3 0x04 shift
|
||||||
numbersign 0x04 altgr
|
numbersign 0x04 altgr
|
||||||
sterling 0x04 shift altgr
|
# KVMD
|
||||||
|
#sterling 0x04 shift altgr
|
||||||
|
|
||||||
# evdev 5 (0x5), QKeyCode "4", number 0x5
|
# evdev 5 (0x5), QKeyCode "4", number 0x5
|
||||||
apostrophe 0x05
|
apostrophe 0x05
|
||||||
4 0x05 shift
|
4 0x05 shift
|
||||||
braceleft 0x05 altgr
|
braceleft 0x05 altgr
|
||||||
dollar 0x05 shift altgr
|
# KVMD
|
||||||
|
#dollar 0x05 shift altgr
|
||||||
|
|
||||||
# evdev 6 (0x6), QKeyCode "5", number 0x6
|
# evdev 6 (0x6), QKeyCode "5", number 0x6
|
||||||
parenleft 0x06
|
parenleft 0x06
|
||||||
@ -91,7 +93,8 @@ plusminus 0x0a shift altgr
|
|||||||
agrave 0x0b
|
agrave 0x0b
|
||||||
0 0x0b shift
|
0 0x0b shift
|
||||||
at 0x0b altgr
|
at 0x0b altgr
|
||||||
degree 0x0b shift altgr
|
# KVMD
|
||||||
|
#degree 0x0b shift altgr
|
||||||
|
|
||||||
# evdev 12 (0xc), QKeyCode "minus", number 0xc
|
# evdev 12 (0xc), QKeyCode "minus", number 0xc
|
||||||
parenright 0x0c
|
parenright 0x0c
|
||||||
@ -122,7 +125,8 @@ AE 0x10 shift altgr
|
|||||||
z 0x11
|
z 0x11
|
||||||
Z 0x11 shift
|
Z 0x11 shift
|
||||||
guillemotleft 0x11 altgr
|
guillemotleft 0x11 altgr
|
||||||
less 0x11 shift altgr
|
#KVMD
|
||||||
|
#less 0x11 shift altgr
|
||||||
|
|
||||||
# evdev 18 (0x12), QKeyCode "e", number 0x12
|
# evdev 18 (0x12), QKeyCode "e", number 0x12
|
||||||
e 0x12
|
e 0x12
|
||||||
@ -200,7 +204,8 @@ Greek_OMEGA 0x1e shift altgr
|
|||||||
s 0x1f
|
s 0x1f
|
||||||
S 0x1f shift
|
S 0x1f shift
|
||||||
ssharp 0x1f altgr
|
ssharp 0x1f altgr
|
||||||
section 0x1f shift altgr
|
# KVMD
|
||||||
|
#section 0x1f shift altgr
|
||||||
|
|
||||||
# evdev 32 (0x20), QKeyCode "d", number 0x20
|
# evdev 32 (0x20), QKeyCode "d", number 0x20
|
||||||
d 0x20
|
d 0x20
|
||||||
@ -247,7 +252,8 @@ Lstroke 0x26 shift altgr
|
|||||||
# evdev 39 (0x27), QKeyCode "semicolon", number 0x27
|
# evdev 39 (0x27), QKeyCode "semicolon", number 0x27
|
||||||
m 0x27
|
m 0x27
|
||||||
M 0x27 shift
|
M 0x27 shift
|
||||||
mu 0x27 altgr
|
# KVMD
|
||||||
|
#mu 0x27 altgr
|
||||||
masculine 0x27 shift altgr
|
masculine 0x27 shift altgr
|
||||||
|
|
||||||
# evdev 40 (0x28), QKeyCode "apostrophe", number 0x28
|
# evdev 40 (0x28), QKeyCode "apostrophe", number 0x28
|
||||||
@ -280,7 +286,8 @@ Lstroke 0x2c shift altgr
|
|||||||
x 0x2d
|
x 0x2d
|
||||||
X 0x2d shift
|
X 0x2d shift
|
||||||
guillemotright 0x2d altgr
|
guillemotright 0x2d altgr
|
||||||
greater 0x2d shift altgr
|
# KVMD
|
||||||
|
#greater 0x2d shift altgr
|
||||||
|
|
||||||
# evdev 46 (0x2e), QKeyCode "c", number 0x2e
|
# evdev 46 (0x2e), QKeyCode "c", number 0x2e
|
||||||
c 0x2e
|
c 0x2e
|
||||||
|
|||||||
6
extras/gostc/manifest.yaml
Normal file
6
extras/gostc/manifest.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
name: GOSTC
|
||||||
|
description: GOSTC Server
|
||||||
|
icon: share/svg/gostc.svg
|
||||||
|
path: extras/gostc
|
||||||
|
daemon: kvmd-gostc
|
||||||
|
place: 11
|
||||||
7
extras/gostc/nginx.ctx-server.conf
Normal file
7
extras/gostc/nginx.ctx-server.conf
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
location /extras/gostc {
|
||||||
|
proxy_pass http://127.0.0.1:18080;
|
||||||
|
include /etc/kvmd/nginx/loc-proxy.conf;
|
||||||
|
include /etc/kvmd/nginx/loc-websocket.conf;
|
||||||
|
include /etc/kvmd/nginx/loc-login.conf;
|
||||||
|
include /etc/kvmd/nginx/loc-nocache.conf;
|
||||||
|
}
|
||||||
@ -69,9 +69,10 @@ class _X11Key:
|
|||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class _KeyMapping:
|
class _KeyMapping:
|
||||||
web_name: str
|
web_name: str
|
||||||
|
evdev_name: str
|
||||||
mcu_code: int
|
mcu_code: int
|
||||||
usb_key: _UsbKey
|
usb_key: _UsbKey
|
||||||
ps2_key: _Ps2Key
|
ps2_key: (_Ps2Key | None)
|
||||||
at1_code: int
|
at1_code: int
|
||||||
x11_keys: set[_X11Key]
|
x11_keys: set[_X11Key]
|
||||||
|
|
||||||
@ -107,7 +108,9 @@ def _parse_usb_key(key: str) -> _UsbKey:
|
|||||||
return _UsbKey(code, is_modifier)
|
return _UsbKey(code, is_modifier)
|
||||||
|
|
||||||
|
|
||||||
def _parse_ps2_key(key: str) -> _Ps2Key:
|
def _parse_ps2_key(key: str) -> (_Ps2Key | None):
|
||||||
|
if ":" not in key:
|
||||||
|
return None
|
||||||
(code_type, raw_code) = key.split(":")
|
(code_type, raw_code) = key.split(":")
|
||||||
return _Ps2Key(
|
return _Ps2Key(
|
||||||
code=int(raw_code, 16),
|
code=int(raw_code, 16),
|
||||||
@ -122,6 +125,7 @@ def _read_keymap_csv(path: str) -> list[_KeyMapping]:
|
|||||||
if len(row) >= 6:
|
if len(row) >= 6:
|
||||||
keymap.append(_KeyMapping(
|
keymap.append(_KeyMapping(
|
||||||
web_name=row["web_name"],
|
web_name=row["web_name"],
|
||||||
|
evdev_name=row["evdev_name"],
|
||||||
mcu_code=int(row["mcu_code"]),
|
mcu_code=int(row["mcu_code"]),
|
||||||
usb_key=_parse_usb_key(row["usb_key"]),
|
usb_key=_parse_usb_key(row["usb_key"]),
|
||||||
ps2_key=_parse_ps2_key(row["ps2_key"]),
|
ps2_key=_parse_ps2_key(row["ps2_key"]),
|
||||||
@ -150,6 +154,7 @@ def main() -> None:
|
|||||||
|
|
||||||
# Fields list:
|
# Fields list:
|
||||||
# - Web
|
# - Web
|
||||||
|
# - Linux/evdev
|
||||||
# - MCU code
|
# - MCU code
|
||||||
# - USB code (^ for the modifier mask)
|
# - USB code (^ for the modifier mask)
|
||||||
# - PS/2 key
|
# - PS/2 key
|
||||||
|
|||||||
@ -24,8 +24,8 @@ upload:
|
|||||||
bash -ex -c " \
|
bash -ex -c " \
|
||||||
current=`cat .current`; \
|
current=`cat .current`; \
|
||||||
if [ '$($@_CURRENT)' == 'spi' ] || [ '$($@_CURRENT)' == 'aum' ]; then \
|
if [ '$($@_CURRENT)' == 'spi' ] || [ '$($@_CURRENT)' == 'aum' ]; then \
|
||||||
gpioset 0 25=1; \
|
gpioset -c gpiochip0 -t 30ms,0 25=1; \
|
||||||
gpioset 0 25=0; \
|
gpioset -c gpiochip0 -t 30ms,0 25=0; \
|
||||||
fi \
|
fi \
|
||||||
"
|
"
|
||||||
platformio run --environment '$($@_CURRENT)' --project-conf 'platformio-$($@_CONFIG).ini' --target upload
|
platformio run --environment '$($@_CURRENT)' --project-conf 'platformio-$($@_CONFIG).ini' --target upload
|
||||||
|
|||||||
@ -2,6 +2,7 @@ programmer
|
|||||||
id = "rpi";
|
id = "rpi";
|
||||||
desc = "RPi SPI programmer";
|
desc = "RPi SPI programmer";
|
||||||
type = "linuxspi";
|
type = "linuxspi";
|
||||||
|
prog_modes = PM_ISP;
|
||||||
reset = 25;
|
reset = 25;
|
||||||
baudrate = 400000;
|
baudrate = 400000;
|
||||||
;
|
;
|
||||||
|
|||||||
@ -148,5 +148,8 @@ void keymapPs2(uint8_t code, Ps2KeyType *ps2_type, uint8_t *ps2_code) {
|
|||||||
case 109: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 19; return; // KanaMode
|
case 109: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 19; return; // KanaMode
|
||||||
case 110: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 100; return; // Convert
|
case 110: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 100; return; // Convert
|
||||||
case 111: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 103; return; // NonConvert
|
case 111: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 103; return; // NonConvert
|
||||||
|
case 112: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 35; return; // AudioVolumeMute
|
||||||
|
case 113: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 50; return; // AudioVolumeUp
|
||||||
|
case 114: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 33; return; // AudioVolumeDown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,9 @@ void keymapPs2(uint8_t code, Ps2KeyType *ps2_type, uint8_t *ps2_code) {
|
|||||||
|
|
||||||
switch (code) {
|
switch (code) {
|
||||||
% for km in sorted(keymap, key=operator.attrgetter("mcu_code")):
|
% for km in sorted(keymap, key=operator.attrgetter("mcu_code")):
|
||||||
|
% if km.ps2_key is not None:
|
||||||
case ${km.mcu_code}: *ps2_type = PS2_KEY_TYPE_${km.ps2_key.type.upper()}; *ps2_code = ${km.ps2_key.code}; return; // ${km.web_name}
|
case ${km.mcu_code}: *ps2_type = PS2_KEY_TYPE_${km.ps2_key.type.upper()}; *ps2_code = ${km.ps2_key.code}; return; // ${km.web_name}
|
||||||
|
% endif
|
||||||
% endfor
|
% endfor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -136,6 +136,10 @@ uint8_t keymapUsb(uint8_t code) {
|
|||||||
case 109: return 136; // KanaMode
|
case 109: return 136; // KanaMode
|
||||||
case 110: return 138; // Convert
|
case 110: return 138; // Convert
|
||||||
case 111: return 139; // NonConvert
|
case 111: return 139; // NonConvert
|
||||||
|
case 112: return 127; // AudioVolumeMute
|
||||||
|
case 113: return 128; // AudioVolumeUp
|
||||||
|
case 114: return 129; // AudioVolumeDown
|
||||||
|
case 115: return 111; // F20
|
||||||
default: return 0;
|
default: return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,8 +82,6 @@ build_flags =
|
|||||||
-DCDC_DISABLED
|
-DCDC_DISABLED
|
||||||
upload_protocol = custom
|
upload_protocol = custom
|
||||||
upload_flags =
|
upload_flags =
|
||||||
-C
|
|
||||||
$PROJECT_PACKAGES_DIR/tool-avrdude/avrdude.conf
|
|
||||||
-C
|
-C
|
||||||
+avrdude-rpi.conf
|
+avrdude-rpi.conf
|
||||||
-P
|
-P
|
||||||
|
|||||||
@ -28,11 +28,14 @@ define libdep
|
|||||||
endef
|
endef
|
||||||
.pico-sdk:
|
.pico-sdk:
|
||||||
$(call libdep,pico-sdk,raspberrypi/pico-sdk,6a7db34ff63345a7badec79ebea3aaef1712f374)
|
$(call libdep,pico-sdk,raspberrypi/pico-sdk,6a7db34ff63345a7badec79ebea3aaef1712f374)
|
||||||
|
.pico-sdk.patches: .pico-sdk
|
||||||
|
patch -d .pico-sdk -p1 < patches/pico-sdk.patch
|
||||||
|
touch .pico-sdk.patches
|
||||||
.tinyusb:
|
.tinyusb:
|
||||||
$(call libdep,tinyusb,hathach/tinyusb,d713571cd44f05d2fc72efc09c670787b74106e0)
|
$(call libdep,tinyusb,hathach/tinyusb,d713571cd44f05d2fc72efc09c670787b74106e0)
|
||||||
.ps2x2pico:
|
.ps2x2pico:
|
||||||
$(call libdep,ps2x2pico,No0ne/ps2x2pico,26ce89d597e598bb0ac636622e064202d91a9efc)
|
$(call libdep,ps2x2pico,No0ne/ps2x2pico,26ce89d597e598bb0ac636622e064202d91a9efc)
|
||||||
deps: .pico-sdk .tinyusb .ps2x2pico
|
deps: .pico-sdk .pico-sdk.patches .tinyusb .ps2x2pico
|
||||||
|
|
||||||
|
|
||||||
.PHONY: deps
|
.PHONY: deps
|
||||||
|
|||||||
10
hid/pico/patches/pico-sdk.patch
Normal file
10
hid/pico/patches/pico-sdk.patch
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
diff --git a/tools/pioasm/CMakeLists.txt b/tools/pioasm/CMakeLists.txt
|
||||||
|
index 322408a..fc8e4b8 100644
|
||||||
|
--- a/tools/pioasm/CMakeLists.txt
|
||||||
|
+++ b/tools/pioasm/CMakeLists.txt
|
||||||
|
@@ -1,4 +1,4 @@
|
||||||
|
-cmake_minimum_required(VERSION 3.4)
|
||||||
|
+cmake_minimum_required(VERSION 3.5)
|
||||||
|
project(pioasm CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 11)
|
||||||
@ -138,6 +138,10 @@ inline u8 ph_usb_keymap(u8 key) {
|
|||||||
case 109: return 136; // KanaMode
|
case 109: return 136; // KanaMode
|
||||||
case 110: return 138; // Convert
|
case 110: return 138; // Convert
|
||||||
case 111: return 139; // NonConvert
|
case 111: return 139; // NonConvert
|
||||||
|
case 112: return 127; // AudioVolumeMute
|
||||||
|
case 113: return 128; // AudioVolumeUp
|
||||||
|
case 114: return 129; // AudioVolumeDown
|
||||||
|
case 115: return 111; // F20
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
228
keymap.csv
228
keymap.csv
@ -1,112 +1,116 @@
|
|||||||
web_name,mcu_code,usb_key,ps2_key,at1_code,x11_names
|
web_name,evdev_name,mcu_code,usb_key,ps2_key,at1_code,x11_names
|
||||||
KeyA,1,0x04,reg:0x1c,0x1e,"^XK_A,XK_a"
|
KeyA,KEY_A,1,0x04,reg:0x1c,0x1e,"^XK_A,XK_a"
|
||||||
KeyB,2,0x05,reg:0x32,0x30,"^XK_B,XK_b"
|
KeyB,KEY_B,2,0x05,reg:0x32,0x30,"^XK_B,XK_b"
|
||||||
KeyC,3,0x06,reg:0x21,0x2e,"^XK_C,XK_c"
|
KeyC,KEY_C,3,0x06,reg:0x21,0x2e,"^XK_C,XK_c"
|
||||||
KeyD,4,0x07,reg:0x23,0x20,"^XK_D,XK_d"
|
KeyD,KEY_D,4,0x07,reg:0x23,0x20,"^XK_D,XK_d"
|
||||||
KeyE,5,0x08,reg:0x24,0x12,"^XK_E,XK_e"
|
KeyE,KEY_E,5,0x08,reg:0x24,0x12,"^XK_E,XK_e"
|
||||||
KeyF,6,0x09,reg:0x2b,0x21,"^XK_F,XK_f"
|
KeyF,KEY_F,6,0x09,reg:0x2b,0x21,"^XK_F,XK_f"
|
||||||
KeyG,7,0x0a,reg:0x34,0x22,"^XK_G,XK_g"
|
KeyG,KEY_G,7,0x0a,reg:0x34,0x22,"^XK_G,XK_g"
|
||||||
KeyH,8,0x0b,reg:0x33,0x23,"^XK_H,XK_h"
|
KeyH,KEY_H,8,0x0b,reg:0x33,0x23,"^XK_H,XK_h"
|
||||||
KeyI,9,0x0c,reg:0x43,0x17,"^XK_I,XK_i"
|
KeyI,KEY_I,9,0x0c,reg:0x43,0x17,"^XK_I,XK_i"
|
||||||
KeyJ,10,0x0d,reg:0x3b,0x24,"^XK_J,XK_j"
|
KeyJ,KEY_J,10,0x0d,reg:0x3b,0x24,"^XK_J,XK_j"
|
||||||
KeyK,11,0x0e,reg:0x42,0x25,"^XK_K,XK_k"
|
KeyK,KEY_K,11,0x0e,reg:0x42,0x25,"^XK_K,XK_k"
|
||||||
KeyL,12,0x0f,reg:0x4b,0x26,"^XK_L,XK_l"
|
KeyL,KEY_L,12,0x0f,reg:0x4b,0x26,"^XK_L,XK_l"
|
||||||
KeyM,13,0x10,reg:0x3a,0x32,"^XK_M,XK_m"
|
KeyM,KEY_M,13,0x10,reg:0x3a,0x32,"^XK_M,XK_m"
|
||||||
KeyN,14,0x11,reg:0x31,0x31,"^XK_N,XK_n"
|
KeyN,KEY_N,14,0x11,reg:0x31,0x31,"^XK_N,XK_n"
|
||||||
KeyO,15,0x12,reg:0x44,0x18,"^XK_O,XK_o"
|
KeyO,KEY_O,15,0x12,reg:0x44,0x18,"^XK_O,XK_o"
|
||||||
KeyP,16,0x13,reg:0x4d,0x19,"^XK_P,XK_p"
|
KeyP,KEY_P,16,0x13,reg:0x4d,0x19,"^XK_P,XK_p"
|
||||||
KeyQ,17,0x14,reg:0x15,0x10,"^XK_Q,XK_q"
|
KeyQ,KEY_Q,17,0x14,reg:0x15,0x10,"^XK_Q,XK_q"
|
||||||
KeyR,18,0x15,reg:0x2d,0x13,"^XK_R,XK_r"
|
KeyR,KEY_R,18,0x15,reg:0x2d,0x13,"^XK_R,XK_r"
|
||||||
KeyS,19,0x16,reg:0x1b,0x1f,"^XK_S,XK_s"
|
KeyS,KEY_S,19,0x16,reg:0x1b,0x1f,"^XK_S,XK_s"
|
||||||
KeyT,20,0x17,reg:0x2c,0x14,"^XK_T,XK_t"
|
KeyT,KEY_T,20,0x17,reg:0x2c,0x14,"^XK_T,XK_t"
|
||||||
KeyU,21,0x18,reg:0x3c,0x16,"^XK_U,XK_u"
|
KeyU,KEY_U,21,0x18,reg:0x3c,0x16,"^XK_U,XK_u"
|
||||||
KeyV,22,0x19,reg:0x2a,0x2f,"^XK_V,XK_v"
|
KeyV,KEY_V,22,0x19,reg:0x2a,0x2f,"^XK_V,XK_v"
|
||||||
KeyW,23,0x1a,reg:0x1d,0x11,"^XK_W,XK_w"
|
KeyW,KEY_W,23,0x1a,reg:0x1d,0x11,"^XK_W,XK_w"
|
||||||
KeyX,24,0x1b,reg:0x22,0x2d,"^XK_X,XK_x"
|
KeyX,KEY_X,24,0x1b,reg:0x22,0x2d,"^XK_X,XK_x"
|
||||||
KeyY,25,0x1c,reg:0x35,0x15,"^XK_Y,XK_y"
|
KeyY,KEY_Y,25,0x1c,reg:0x35,0x15,"^XK_Y,XK_y"
|
||||||
KeyZ,26,0x1d,reg:0x1a,0x2c,"^XK_Z,XK_z"
|
KeyZ,KEY_Z,26,0x1d,reg:0x1a,0x2c,"^XK_Z,XK_z"
|
||||||
Digit1,27,0x1e,reg:0x16,0x02,"XK_1,^XK_exclam"
|
Digit1,KEY_1,27,0x1e,reg:0x16,0x02,"XK_1,^XK_exclam"
|
||||||
Digit2,28,0x1f,reg:0x1e,0x03,"XK_2,^XK_at"
|
Digit2,KEY_2,28,0x1f,reg:0x1e,0x03,"XK_2,^XK_at"
|
||||||
Digit3,29,0x20,reg:0x26,0x04,"XK_3,^XK_numbersign"
|
Digit3,KEY_3,29,0x20,reg:0x26,0x04,"XK_3,^XK_numbersign"
|
||||||
Digit4,30,0x21,reg:0x25,0x05,"XK_4,^XK_dollar"
|
Digit4,KEY_4,30,0x21,reg:0x25,0x05,"XK_4,^XK_dollar"
|
||||||
Digit5,31,0x22,reg:0x2e,0x06,"XK_5,^XK_percent"
|
Digit5,KEY_5,31,0x22,reg:0x2e,0x06,"XK_5,^XK_percent"
|
||||||
Digit6,32,0x23,reg:0x36,0x07,"XK_6,^XK_asciicircum"
|
Digit6,KEY_6,32,0x23,reg:0x36,0x07,"XK_6,^XK_asciicircum"
|
||||||
Digit7,33,0x24,reg:0x3d,0x08,"XK_7,^XK_ampersand"
|
Digit7,KEY_7,33,0x24,reg:0x3d,0x08,"XK_7,^XK_ampersand"
|
||||||
Digit8,34,0x25,reg:0x3e,0x09,"XK_8,^XK_asterisk"
|
Digit8,KEY_8,34,0x25,reg:0x3e,0x09,"XK_8,^XK_asterisk"
|
||||||
Digit9,35,0x26,reg:0x46,0x0a,"XK_9,^XK_parenleft"
|
Digit9,KEY_9,35,0x26,reg:0x46,0x0a,"XK_9,^XK_parenleft"
|
||||||
Digit0,36,0x27,reg:0x45,0x0b,"XK_0,^XK_parenright"
|
Digit0,KEY_0,36,0x27,reg:0x45,0x0b,"XK_0,^XK_parenright"
|
||||||
Enter,37,0x28,reg:0x5a,0x1c,XK_Return
|
Enter,KEY_ENTER,37,0x28,reg:0x5a,0x1c,XK_Return
|
||||||
Escape,38,0x29,reg:0x76,0x01,XK_Escape
|
Escape,KEY_ESC,38,0x29,reg:0x76,0x01,XK_Escape
|
||||||
Backspace,39,0x2a,reg:0x66,0x0e,XK_BackSpace
|
Backspace,KEY_BACKSPACE,39,0x2a,reg:0x66,0x0e,XK_BackSpace
|
||||||
Tab,40,0x2b,reg:0x0d,0x0f,XK_Tab
|
Tab,KEY_TAB,40,0x2b,reg:0x0d,0x0f,XK_Tab
|
||||||
Space,41,0x2c,reg:0x29,0x39,XK_space
|
Space,KEY_SPACE,41,0x2c,reg:0x29,0x39,XK_space
|
||||||
Minus,42,0x2d,reg:0x4e,0x0c,"XK_minus,^XK_underscore"
|
Minus,KEY_MINUS,42,0x2d,reg:0x4e,0x0c,"XK_minus,^XK_underscore"
|
||||||
Equal,43,0x2e,reg:0x55,0x0d,"XK_equal,^XK_plus"
|
Equal,KEY_EQUAL,43,0x2e,reg:0x55,0x0d,"XK_equal,^XK_plus"
|
||||||
BracketLeft,44,0x2f,reg:0x54,0x1a,"XK_bracketleft,^XK_braceleft"
|
BracketLeft,KEY_LEFTBRACE,44,0x2f,reg:0x54,0x1a,"XK_bracketleft,^XK_braceleft"
|
||||||
BracketRight,45,0x30,reg:0x5b,0x1b,"XK_bracketright,^XK_braceright"
|
BracketRight,KEY_RIGHTBRACE,45,0x30,reg:0x5b,0x1b,"XK_bracketright,^XK_braceright"
|
||||||
Backslash,46,0x31,reg:0x5d,0x2b,"XK_backslash,^XK_bar"
|
Backslash,KEY_BACKSLASH,46,0x31,reg:0x5d,0x2b,"XK_backslash,^XK_bar"
|
||||||
Semicolon,47,0x33,reg:0x4c,0x27,"XK_semicolon,^XK_colon"
|
Semicolon,KEY_SEMICOLON,47,0x33,reg:0x4c,0x27,"XK_semicolon,^XK_colon"
|
||||||
Quote,48,0x34,reg:0x52,0x28,"XK_apostrophe,^XK_quotedbl"
|
Quote,KEY_APOSTROPHE,48,0x34,reg:0x52,0x28,"XK_apostrophe,^XK_quotedbl"
|
||||||
Backquote,49,0x35,reg:0x0e,0x29,"XK_grave,^XK_asciitilde"
|
Backquote,KEY_GRAVE,49,0x35,reg:0x0e,0x29,"XK_grave,^XK_asciitilde"
|
||||||
Comma,50,0x36,reg:0x41,0x33,"XK_comma,^XK_less"
|
Comma,KEY_COMMA,50,0x36,reg:0x41,0x33,"XK_comma,^XK_less"
|
||||||
Period,51,0x37,reg:0x49,0x34,"XK_period,^XK_greater"
|
Period,KEY_DOT,51,0x37,reg:0x49,0x34,"XK_period,^XK_greater"
|
||||||
Slash,52,0x38,reg:0x4a,0x35,"XK_slash,^XK_question"
|
Slash,KEY_SLASH,52,0x38,reg:0x4a,0x35,"XK_slash,^XK_question"
|
||||||
CapsLock,53,0x39,reg:0x58,0x3a,XK_Caps_Lock
|
CapsLock,KEY_CAPSLOCK,53,0x39,reg:0x58,0x3a,XK_Caps_Lock
|
||||||
F1,54,0x3a,reg:0x05,0x3b,XK_F1
|
F1,KEY_F1,54,0x3a,reg:0x05,0x3b,XK_F1
|
||||||
F2,55,0x3b,reg:0x06,0x3c,XK_F2
|
F2,KEY_F2,55,0x3b,reg:0x06,0x3c,XK_F2
|
||||||
F3,56,0x3c,reg:0x04,0x3d,XK_F3
|
F3,KEY_F3,56,0x3c,reg:0x04,0x3d,XK_F3
|
||||||
F4,57,0x3d,reg:0x0c,0x3e,XK_F4
|
F4,KEY_F4,57,0x3d,reg:0x0c,0x3e,XK_F4
|
||||||
F5,58,0x3e,reg:0x03,0x3f,XK_F5
|
F5,KEY_F5,58,0x3e,reg:0x03,0x3f,XK_F5
|
||||||
F6,59,0x3f,reg:0x0b,0x40,XK_F6
|
F6,KEY_F6,59,0x3f,reg:0x0b,0x40,XK_F6
|
||||||
F7,60,0x40,reg:0x83,0x41,XK_F7
|
F7,KEY_F7,60,0x40,reg:0x83,0x41,XK_F7
|
||||||
F8,61,0x41,reg:0x0a,0x42,XK_F8
|
F8,KEY_F8,61,0x41,reg:0x0a,0x42,XK_F8
|
||||||
F9,62,0x42,reg:0x01,0x43,XK_F9
|
F9,KEY_F9,62,0x42,reg:0x01,0x43,XK_F9
|
||||||
F10,63,0x43,reg:0x09,0x44,XK_F10
|
F10,KEY_F10,63,0x43,reg:0x09,0x44,XK_F10
|
||||||
F11,64,0x44,reg:0x78,0x57,XK_F11
|
F11,KEY_F11,64,0x44,reg:0x78,0x57,XK_F11
|
||||||
F12,65,0x45,reg:0x07,0x58,XK_F12
|
F12,KEY_F12,65,0x45,reg:0x07,0x58,XK_F12
|
||||||
PrintScreen,66,0x46,print:0xff,0x54,XK_Sys_Req
|
PrintScreen,KEY_SYSRQ,66,0x46,print:0xff,0x54,XK_Sys_Req
|
||||||
Insert,67,0x49,spec:0x70,0xe052,XK_Insert
|
Insert,KEY_INSERT,67,0x49,spec:0x70,0xe052,XK_Insert
|
||||||
Home,68,0x4a,spec:0x6c,0xe047,XK_Home
|
Home,KEY_HOME,68,0x4a,spec:0x6c,0xe047,XK_Home
|
||||||
PageUp,69,0x4b,spec:0x7d,0xe049,XK_Page_Up
|
PageUp,KEY_PAGEUP,69,0x4b,spec:0x7d,0xe049,XK_Page_Up
|
||||||
Delete,70,0x4c,spec:0x71,0xe053,XK_Delete
|
Delete,KEY_DELETE,70,0x4c,spec:0x71,0xe053,XK_Delete
|
||||||
End,71,0x4d,spec:0x69,0xe04f,XK_End
|
End,KEY_END,71,0x4d,spec:0x69,0xe04f,XK_End
|
||||||
PageDown,72,0x4e,spec:0x7a,0xe051,XK_Page_Down
|
PageDown,KEY_PAGEDOWN,72,0x4e,spec:0x7a,0xe051,XK_Page_Down
|
||||||
ArrowRight,73,0x4f,spec:0x74,0xe04d,XK_Right
|
ArrowRight,KEY_RIGHT,73,0x4f,spec:0x74,0xe04d,XK_Right
|
||||||
ArrowLeft,74,0x50,spec:0x6b,0xe04b,XK_Left
|
ArrowLeft,KEY_LEFT,74,0x50,spec:0x6b,0xe04b,XK_Left
|
||||||
ArrowDown,75,0x51,spec:0x72,0xe050,XK_Down
|
ArrowDown,KEY_DOWN,75,0x51,spec:0x72,0xe050,XK_Down
|
||||||
ArrowUp,76,0x52,spec:0x75,0xe048,XK_Up
|
ArrowUp,KEY_UP,76,0x52,spec:0x75,0xe048,XK_Up
|
||||||
ControlLeft,77,^0x01,reg:0x14,0x1d,XK_Control_L
|
ControlLeft,KEY_LEFTCTRL,77,^0x01,reg:0x14,0x1d,XK_Control_L
|
||||||
ShiftLeft,78,^0x02,reg:0x12,0x2a,XK_Shift_L
|
ShiftLeft,KEY_LEFTSHIFT,78,^0x02,reg:0x12,0x2a,XK_Shift_L
|
||||||
AltLeft,79,^0x04,reg:0x11,0x38,XK_Alt_L
|
AltLeft,KEY_LEFTALT,79,^0x04,reg:0x11,0x38,XK_Alt_L
|
||||||
MetaLeft,80,^0x08,spec:0x1f,0xe05b,"XK_Meta_L,XK_Super_L"
|
MetaLeft,KEY_LEFTMETA,80,^0x08,spec:0x1f,0xe05b,"XK_Meta_L,XK_Super_L"
|
||||||
ControlRight,81,^0x10,spec:0x14,0xe01d,XK_Control_R
|
ControlRight,KEY_RIGHTCTRL,81,^0x10,spec:0x14,0xe01d,XK_Control_R
|
||||||
ShiftRight,82,^0x20,reg:0x59,0x36,XK_Shift_R
|
ShiftRight,KEY_RIGHTSHIFT,82,^0x20,reg:0x59,0x36,XK_Shift_R
|
||||||
AltRight,83,^0x40,spec:0x11,0xe038,"XK_Alt_R,XK_ISO_Level3_Shift"
|
AltRight,KEY_RIGHTALT,83,^0x40,spec:0x11,0xe038,"XK_Alt_R,XK_ISO_Level3_Shift"
|
||||||
MetaRight,84,^0x80,spec:0x27,0xe05c,"XK_Meta_R,XK_Super_R"
|
MetaRight,KEY_RIGHTMETA,84,^0x80,spec:0x27,0xe05c,"XK_Meta_R,XK_Super_R"
|
||||||
Pause,85,0x48,pause:0xff,0xe046,XK_Pause
|
Pause,KEY_PAUSE,85,0x48,pause:0xff,0xe046,XK_Pause
|
||||||
ScrollLock,86,0x47,reg:0x7e,0x46,XK_Scroll_Lock
|
ScrollLock,KEY_SCROLLLOCK,86,0x47,reg:0x7e,0x46,XK_Scroll_Lock
|
||||||
NumLock,87,0x53,reg:0x77,0x45,XK_Num_Lock
|
NumLock,KEY_NUMLOCK,87,0x53,reg:0x77,0x45,XK_Num_Lock
|
||||||
ContextMenu,88,0x65,spec:0x2f,0xe05d,XK_Menu
|
ContextMenu,KEY_CONTEXT_MENU,88,0x65,spec:0x2f,0xe05d,XK_Menu
|
||||||
NumpadDivide,89,0x54,spec:0x4a,0xe035,XK_KP_Divide
|
NumpadDivide,KEY_KPSLASH,89,0x54,spec:0x4a,0xe035,XK_KP_Divide
|
||||||
NumpadMultiply,90,0x55,reg:0x7c,0x37,XK_multiply
|
NumpadMultiply,KEY_KPASTERISK,90,0x55,reg:0x7c,0x37,XK_multiply
|
||||||
NumpadSubtract,91,0x56,reg:0x7b,0x4a,XK_KP_Subtract
|
NumpadSubtract,KEY_KPMINUS,91,0x56,reg:0x7b,0x4a,XK_KP_Subtract
|
||||||
NumpadAdd,92,0x57,reg:0x79,0x4e,XK_KP_Add
|
NumpadAdd,KEY_KPPLUS,92,0x57,reg:0x79,0x4e,XK_KP_Add
|
||||||
NumpadEnter,93,0x58,spec:0x5a,0xe01c,XK_KP_Enter
|
NumpadEnter,KEY_KPENTER,93,0x58,spec:0x5a,0xe01c,XK_KP_Enter
|
||||||
Numpad1,94,0x59,reg:0x69,0x4f,XK_KP_1
|
Numpad1,KEY_KP1,94,0x59,reg:0x69,0x4f,XK_KP_1
|
||||||
Numpad2,95,0x5a,reg:0x72,0x50,XK_KP_2
|
Numpad2,KEY_KP2,95,0x5a,reg:0x72,0x50,XK_KP_2
|
||||||
Numpad3,96,0x5b,reg:0x7a,0x51,XK_KP_3
|
Numpad3,KEY_KP3,96,0x5b,reg:0x7a,0x51,XK_KP_3
|
||||||
Numpad4,97,0x5c,reg:0x6b,0x4b,XK_KP_4
|
Numpad4,KEY_KP4,97,0x5c,reg:0x6b,0x4b,XK_KP_4
|
||||||
Numpad5,98,0x5d,reg:0x73,0x4c,XK_KP_5
|
Numpad5,KEY_KP5,98,0x5d,reg:0x73,0x4c,XK_KP_5
|
||||||
Numpad6,99,0x5e,reg:0x74,0x4d,XK_KP_6
|
Numpad6,KEY_KP6,99,0x5e,reg:0x74,0x4d,XK_KP_6
|
||||||
Numpad7,100,0x5f,reg:0x6c,0x47,XK_KP_7
|
Numpad7,KEY_KP7,100,0x5f,reg:0x6c,0x47,XK_KP_7
|
||||||
Numpad8,101,0x60,reg:0x75,0x48,XK_KP_8
|
Numpad8,KEY_KP8,101,0x60,reg:0x75,0x48,XK_KP_8
|
||||||
Numpad9,102,0x61,reg:0x7d,0x49,XK_KP_9
|
Numpad9,KEY_KP9,102,0x61,reg:0x7d,0x49,XK_KP_9
|
||||||
Numpad0,103,0x62,reg:0x70,0x52,XK_KP_0
|
Numpad0,KEY_KP0,103,0x62,reg:0x70,0x52,XK_KP_0
|
||||||
NumpadDecimal,104,0x63,reg:0x71,0x53,XK_KP_Decimal
|
NumpadDecimal,KEY_KPDOT,104,0x63,reg:0x71,0x53,XK_KP_Decimal
|
||||||
Power,105,0x66,spec:0x5e,0xe05e,XK_XF86_Sleep
|
Power,KEY_POWER,105,0x66,spec:0x5e,0xe05e,XK_XF86_Sleep
|
||||||
IntlBackslash,106,0x64,reg:0x61,0x56,""
|
IntlBackslash,KEY_102ND,106,0x64,reg:0x61,0x56,
|
||||||
IntlYen,107,0x89,reg:0x6a,0x7d,""
|
IntlYen,KEY_YEN,107,0x89,reg:0x6a,0x7d,
|
||||||
IntlRo,108,0x87,reg:0x51,0x73,""
|
IntlRo,KEY_RO,108,0x87,reg:0x51,0x73,
|
||||||
KanaMode,109,0x88,reg:0x13,0x70,""
|
KanaMode,KEY_KATAKANA,109,0x88,reg:0x13,0x70,
|
||||||
Convert,110,0x8a,reg:0x64,0x79,""
|
Convert,KEY_HENKAN,110,0x8a,reg:0x64,0x79,
|
||||||
NonConvert,111,0x8b,reg:0x67,0x7b,""
|
NonConvert,KEY_MUHENKAN,111,0x8b,reg:0x67,0x7b,
|
||||||
|
AudioVolumeMute,KEY_MUTE,112,0x7f,spec:0x23,0xe020,
|
||||||
|
AudioVolumeUp,KEY_VOLUMEUP,113,0x80,spec:0x32,0xe030,
|
||||||
|
AudioVolumeDown,KEY_VOLUMEDOWN,114,0x81,spec:0x21,0xe02e,
|
||||||
|
F20,KEY_F20,115,0x6f,,0x5a,
|
||||||
|
|||||||
|
@ -112,6 +112,13 @@ EOF
|
|||||||
cp /usr/share/kvmd/configs.default/janus/janus.plugin.ustreamer.jcfg /etc/kvmd/janus || true
|
cp /usr/share/kvmd/configs.default/janus/janus.plugin.ustreamer.jcfg /etc/kvmd/janus || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "$(vercmp "$2" 4.60)" -lt 0 ]]; then
|
||||||
|
if grep -q "^dtoverlay=vc4-kms-v3d" /boot/config.txt; then
|
||||||
|
sed -i -e "s/cma=128M/cma=192M/g" /boot/cmdline.txt || true
|
||||||
|
sed -i -e "s/^gpu_mem=128/gpu_mem=192/g" /boot/config.txt || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Some update deletes /etc/motd, WTF
|
# Some update deletes /etc/motd, WTF
|
||||||
# shellcheck disable=SC2015,SC2166
|
# shellcheck disable=SC2015,SC2166
|
||||||
[ ! -f /etc/motd -a -f /etc/motd.pacsave ] && mv /etc/motd.pacsave /etc/motd || true
|
[ ! -f /etc/motd -a -f /etc/motd.pacsave ] && mv /etc/motd.pacsave /etc/motd || true
|
||||||
|
|||||||
@ -20,4 +20,4 @@
|
|||||||
# ========================================================================== #
|
# ========================================================================== #
|
||||||
|
|
||||||
|
|
||||||
__version__ = "4.49"
|
__version__ = "4.94"
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import threading
|
import threading
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
import typing
|
||||||
|
|
||||||
import gpiod
|
import gpiod
|
||||||
|
|
||||||
@ -101,10 +102,10 @@ class AioReader: # pylint: disable=too-many-instance-attributes
|
|||||||
if line_req.wait_edge_events(1):
|
if line_req.wait_edge_events(1):
|
||||||
new: dict[int, bool] = {}
|
new: dict[int, bool] = {}
|
||||||
for event in line_req.read_edge_events():
|
for event in line_req.read_edge_events():
|
||||||
(pin, value) = self.__parse_event(event)
|
(pin, state) = self.__parse_event(event)
|
||||||
new[pin] = value
|
new[pin] = state
|
||||||
for (pin, value) in new.items():
|
for (pin, state) in new.items():
|
||||||
self.__values[pin].set(value)
|
self.__values[pin].set(state)
|
||||||
else: # Timeout
|
else: # Timeout
|
||||||
# XXX: Лимит был актуален для 1.6. Надо проверить, поменялось ли это в 2.x.
|
# XXX: Лимит был актуален для 1.6. Надо проверить, поменялось ли это в 2.x.
|
||||||
# Размер буфера ядра - 16 эвентов на линии. При превышении этого числа,
|
# Размер буфера ядра - 16 эвентов на линии. При превышении этого числа,
|
||||||
@ -114,11 +115,12 @@ class AioReader: # pylint: disable=too-many-instance-attributes
|
|||||||
self.__values[pin].set(bool(value.value)) # type: ignore
|
self.__values[pin].set(bool(value.value)) # type: ignore
|
||||||
|
|
||||||
def __parse_event(self, event: gpiod.EdgeEvent) -> tuple[int, bool]:
|
def __parse_event(self, event: gpiod.EdgeEvent) -> tuple[int, bool]:
|
||||||
if event.event_type == event.Type.RISING_EDGE:
|
match event.event_type:
|
||||||
return (event.line_offset, True)
|
case event.Type.RISING_EDGE:
|
||||||
elif event.event_type == event.Type.FALLING_EDGE:
|
return (event.line_offset, True)
|
||||||
return (event.line_offset, False)
|
case event.Type.FALLING_EDGE:
|
||||||
raise RuntimeError(f"Invalid event {event} type: {event.type}")
|
return (event.line_offset, False)
|
||||||
|
typing.assert_never(event.event_type)
|
||||||
|
|
||||||
|
|
||||||
class _DebouncedValue:
|
class _DebouncedValue:
|
||||||
|
|||||||
@ -211,6 +211,18 @@ async def wait_first(*aws: asyncio.Task) -> tuple[set[asyncio.Task], set[asyncio
|
|||||||
return (await asyncio.wait(list(aws), return_when=asyncio.FIRST_COMPLETED))
|
return (await asyncio.wait(list(aws), return_when=asyncio.FIRST_COMPLETED))
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
async def spawn_and_follow(*coros: Coroutine) -> None:
|
||||||
|
tasks: list[asyncio.Task] = list(map(asyncio.create_task, coros))
|
||||||
|
try:
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
except Exception:
|
||||||
|
for task in tasks:
|
||||||
|
task.cancel()
|
||||||
|
await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
async def close_writer(writer: asyncio.StreamWriter) -> bool:
|
async def close_writer(writer: asyncio.StreamWriter) -> bool:
|
||||||
closing = writer.is_closing()
|
closing = writer.is_closing()
|
||||||
|
|||||||
@ -65,6 +65,7 @@ from ..validators.basic import valid_string_list
|
|||||||
|
|
||||||
from ..validators.auth import valid_user
|
from ..validators.auth import valid_user
|
||||||
from ..validators.auth import valid_users_list
|
from ..validators.auth import valid_users_list
|
||||||
|
from ..validators.auth import valid_expire
|
||||||
|
|
||||||
from ..validators.os import valid_abs_path
|
from ..validators.os import valid_abs_path
|
||||||
from ..validators.os import valid_abs_file
|
from ..validators.os import valid_abs_file
|
||||||
@ -73,12 +74,14 @@ from ..validators.os import valid_unix_mode
|
|||||||
from ..validators.os import valid_options
|
from ..validators.os import valid_options
|
||||||
from ..validators.os import valid_command
|
from ..validators.os import valid_command
|
||||||
|
|
||||||
|
from ..validators.net import valid_ip
|
||||||
from ..validators.net import valid_ip_or_host
|
from ..validators.net import valid_ip_or_host
|
||||||
from ..validators.net import valid_net
|
from ..validators.net import valid_net
|
||||||
from ..validators.net import valid_port
|
from ..validators.net import valid_port
|
||||||
from ..validators.net import valid_ports_list
|
from ..validators.net import valid_ports_list
|
||||||
from ..validators.net import valid_mac
|
from ..validators.net import valid_mac
|
||||||
from ..validators.net import valid_ssl_ciphers
|
from ..validators.net import valid_ssl_ciphers
|
||||||
|
from ..validators.net import valid_ice_servers
|
||||||
|
|
||||||
from ..validators.hid import valid_hid_key
|
from ..validators.hid import valid_hid_key
|
||||||
from ..validators.hid import valid_hid_mouse_output
|
from ..validators.hid import valid_hid_mouse_output
|
||||||
@ -190,6 +193,14 @@ def _init_config(config_path: str, override_options: list[str], **load_flags: bo
|
|||||||
|
|
||||||
|
|
||||||
def _patch_raw(raw_config: dict) -> None: # pylint: disable=too-many-branches
|
def _patch_raw(raw_config: dict) -> None: # pylint: disable=too-many-branches
|
||||||
|
for (sub, cmd) in [("iface", "ip_cmd"), ("firewall", "iptables_cmd")]:
|
||||||
|
if isinstance(raw_config.get("otgnet"), dict):
|
||||||
|
if isinstance(raw_config["otgnet"].get(sub), dict):
|
||||||
|
if raw_config["otgnet"][sub].get(cmd):
|
||||||
|
raw_config["otgnet"].setdefault("commands", {})
|
||||||
|
raw_config["otgnet"]["commands"][cmd] = raw_config["otgnet"][sub][cmd]
|
||||||
|
del raw_config["otgnet"][sub][cmd]
|
||||||
|
|
||||||
if isinstance(raw_config.get("otg"), dict):
|
if isinstance(raw_config.get("otg"), dict):
|
||||||
for (old, new) in [
|
for (old, new) in [
|
||||||
("msd", "msd"),
|
("msd", "msd"),
|
||||||
@ -357,6 +368,12 @@ def _get_config_scheme() -> dict:
|
|||||||
|
|
||||||
"auth": {
|
"auth": {
|
||||||
"enabled": Option(True, type=valid_bool),
|
"enabled": Option(True, type=valid_bool),
|
||||||
|
"expire": Option(0, type=valid_expire),
|
||||||
|
|
||||||
|
"usc": {
|
||||||
|
"users": Option([], type=valid_users_list), # PiKVM username has a same regex as a UNIX username
|
||||||
|
"groups": Option(["kvmd-selfauth"], type=valid_users_list), # groupname has a same regex as a username
|
||||||
|
},
|
||||||
|
|
||||||
"internal": {
|
"internal": {
|
||||||
"type": Option("htpasswd"),
|
"type": Option("htpasswd"),
|
||||||
@ -457,7 +474,7 @@ def _get_config_scheme() -> dict:
|
|||||||
|
|
||||||
"unix": Option("/run/kvmd/ustreamer.sock", type=valid_abs_path, unpack_as="unix_path"),
|
"unix": Option("/run/kvmd/ustreamer.sock", type=valid_abs_path, unpack_as="unix_path"),
|
||||||
"timeout": Option(2.0, type=valid_float_f01),
|
"timeout": Option(2.0, type=valid_float_f01),
|
||||||
"snapshot_timeout": Option(1.0, type=valid_float_f01), # error_delay * 3 + 1
|
"snapshot_timeout": Option(5.0, type=valid_float_f01), # error_delay * 3 + 1
|
||||||
|
|
||||||
"process_name_prefix": Option("kvmd/streamer"),
|
"process_name_prefix": Option("kvmd/streamer"),
|
||||||
|
|
||||||
@ -504,8 +521,9 @@ def _get_config_scheme() -> dict:
|
|||||||
},
|
},
|
||||||
|
|
||||||
"switch": {
|
"switch": {
|
||||||
"device": Option("/dev/kvmd-switch", type=valid_abs_path, unpack_as="device_path"),
|
"device": Option("/dev/kvmd-switch", type=valid_abs_path, unpack_as="device_path"),
|
||||||
"default_edid": Option("/etc/kvmd/switch-edid.hex", type=valid_abs_path, unpack_as="default_edid_path"),
|
"default_edid": Option("/etc/kvmd/switch-edid.hex", type=valid_abs_path, unpack_as="default_edid_path"),
|
||||||
|
"ignore_hpd_on_top": Option(False, type=valid_bool),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -558,15 +576,15 @@ def _get_config_scheme() -> dict:
|
|||||||
"vendor_id": Option(0x1D6B, type=valid_otg_id), # Linux Foundation
|
"vendor_id": Option(0x1D6B, type=valid_otg_id), # Linux Foundation
|
||||||
"product_id": Option(0x0104, type=valid_otg_id), # Multifunction Composite Gadget
|
"product_id": Option(0x0104, type=valid_otg_id), # Multifunction Composite Gadget
|
||||||
"manufacturer": Option("PiKVM", type=valid_stripped_string),
|
"manufacturer": Option("PiKVM", type=valid_stripped_string),
|
||||||
"product": Option("Composite KVM Device", type=valid_stripped_string),
|
"product": Option("PiKVM Composite Device", type=valid_stripped_string),
|
||||||
"serial": Option("CAFEBABE", type=valid_stripped_string, if_none=None),
|
"serial": Option("CAFEBABE", type=valid_stripped_string, if_none=None),
|
||||||
|
"config": Option("", type=valid_stripped_string),
|
||||||
"device_version": Option(-1, type=functools.partial(valid_number, min=-1, max=0xFFFF)),
|
"device_version": Option(-1, type=functools.partial(valid_number, min=-1, max=0xFFFF)),
|
||||||
"usb_version": Option(0x0200, type=valid_otg_id),
|
"usb_version": Option(0x0200, type=valid_otg_id),
|
||||||
"max_power": Option(250, type=functools.partial(valid_number, min=50, max=500)),
|
"max_power": Option(250, type=functools.partial(valid_number, min=50, max=500)),
|
||||||
"remote_wakeup": Option(True, type=valid_bool),
|
"remote_wakeup": Option(True, type=valid_bool),
|
||||||
|
|
||||||
"gadget": Option("kvmd", type=valid_otg_gadget),
|
"gadget": Option("kvmd", type=valid_otg_gadget),
|
||||||
"config": Option("PiKVM device", type=valid_stripped_string_not_empty),
|
|
||||||
"udc": Option("", type=valid_stripped_string),
|
"udc": Option("", type=valid_stripped_string),
|
||||||
"endpoints": Option(9, type=valid_int_f0),
|
"endpoints": Option(9, type=valid_int_f0),
|
||||||
"init_delay": Option(3.0, type=valid_float_f01),
|
"init_delay": Option(3.0, type=valid_float_f01),
|
||||||
@ -657,8 +675,7 @@ def _get_config_scheme() -> dict:
|
|||||||
|
|
||||||
"otgnet": {
|
"otgnet": {
|
||||||
"iface": {
|
"iface": {
|
||||||
"net": Option("172.30.30.0/24", type=functools.partial(valid_net, v6=False)),
|
"net": Option("172.30.30.0/24", type=functools.partial(valid_net, v6=False)),
|
||||||
"ip_cmd": Option(["/usr/bin/ip"], type=valid_command),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"firewall": {
|
"firewall": {
|
||||||
@ -666,10 +683,13 @@ def _get_config_scheme() -> dict:
|
|||||||
"allow_tcp": Option([], type=valid_ports_list),
|
"allow_tcp": Option([], type=valid_ports_list),
|
||||||
"allow_udp": Option([67], type=valid_ports_list),
|
"allow_udp": Option([67], type=valid_ports_list),
|
||||||
"forward_iface": Option("", type=valid_stripped_string),
|
"forward_iface": Option("", type=valid_stripped_string),
|
||||||
"iptables_cmd": Option(["/usr/sbin/iptables", "--wait=5"], type=valid_command),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"commands": {
|
"commands": {
|
||||||
|
"ip_cmd": Option(["/usr/bin/ip"], type=valid_command),
|
||||||
|
"iptables_cmd": Option(["/usr/sbin/iptables", "--wait=5"], type=valid_command),
|
||||||
|
"sysctl_cmd": Option(["/usr/sbin/sysctl"], type=valid_command),
|
||||||
|
|
||||||
"pre_start_cmd": Option(["/bin/true", "pre-start"], type=valid_command),
|
"pre_start_cmd": Option(["/bin/true", "pre-start"], type=valid_command),
|
||||||
"pre_start_cmd_remove": Option([], type=valid_options),
|
"pre_start_cmd_remove": Option([], type=valid_options),
|
||||||
"pre_start_cmd_append": Option([], type=valid_options),
|
"pre_start_cmd_append": Option([], type=valid_options),
|
||||||
@ -734,7 +754,7 @@ def _get_config_scheme() -> dict:
|
|||||||
"desired_fps": Option(30, type=valid_stream_fps),
|
"desired_fps": Option(30, type=valid_stream_fps),
|
||||||
"mouse_output": Option("usb", type=valid_hid_mouse_output),
|
"mouse_output": Option("usb", type=valid_hid_mouse_output),
|
||||||
"keymap": Option("/usr/share/kvmd/keymaps/en-us", type=valid_abs_file),
|
"keymap": Option("/usr/share/kvmd/keymaps/en-us", type=valid_abs_file),
|
||||||
"allow_cut_after": Option(3.0, type=valid_float_f0),
|
"scroll_rate": Option(4, type=functools.partial(valid_number, min=1, max=30)),
|
||||||
|
|
||||||
"server": {
|
"server": {
|
||||||
"host": Option("", type=valid_ip_or_host, if_empty=""),
|
"host": Option("", type=valid_ip_or_host, if_empty=""),
|
||||||
@ -786,8 +806,8 @@ def _get_config_scheme() -> dict:
|
|||||||
|
|
||||||
"auth": {
|
"auth": {
|
||||||
"vncauth": {
|
"vncauth": {
|
||||||
"enabled": Option(False, type=valid_bool),
|
"enabled": Option(False, type=valid_bool, unpack_as="vncpass_enabled"),
|
||||||
"file": Option("/etc/kvmd/vncpasswd", type=valid_abs_file, unpack_as="path"),
|
"file": Option("/etc/kvmd/vncpasswd", type=valid_abs_file, unpack_as="vncpass_path"),
|
||||||
},
|
},
|
||||||
"vencrypt": {
|
"vencrypt": {
|
||||||
"enabled": Option(True, type=valid_bool, unpack_as="vencrypt_enabled"),
|
"enabled": Option(True, type=valid_bool, unpack_as="vencrypt_enabled"),
|
||||||
@ -795,13 +815,24 @@ def _get_config_scheme() -> dict:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"localhid": {
|
||||||
|
"kvmd": {
|
||||||
|
"unix": Option("/run/kvmd/kvmd.sock", type=valid_abs_path, unpack_as="unix_path"),
|
||||||
|
"timeout": Option(5.0, type=valid_float_f01),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
"nginx": {
|
"nginx": {
|
||||||
"http": {
|
"http": {
|
||||||
"port": Option(80, type=valid_port),
|
"ipv4": Option("0.0.0.0", type=functools.partial(valid_ip, v6=False)),
|
||||||
|
"ipv6": Option("::", type=functools.partial(valid_ip, v4=False)),
|
||||||
|
"port": Option(80, type=valid_port),
|
||||||
},
|
},
|
||||||
"https": {
|
"https": {
|
||||||
"enabled": Option(True, type=valid_bool),
|
"enabled": Option(True, type=valid_bool),
|
||||||
"port": Option(443, type=valid_port),
|
"ipv4": Option("0.0.0.0", type=functools.partial(valid_ip, v6=False)),
|
||||||
|
"ipv6": Option("::", type=functools.partial(valid_ip, v4=False)),
|
||||||
|
"port": Option(443, type=valid_port),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -830,6 +861,7 @@ def _get_config_scheme() -> dict:
|
|||||||
], type=valid_command),
|
], type=valid_command),
|
||||||
"cmd_remove": Option([], type=valid_options),
|
"cmd_remove": Option([], type=valid_options),
|
||||||
"cmd_append": Option([], type=valid_options),
|
"cmd_append": Option([], type=valid_options),
|
||||||
|
"local_ice_servers": Option([], type=valid_ice_servers, unpack_as="ice_servers"),
|
||||||
},
|
},
|
||||||
|
|
||||||
"watchdog": {
|
"watchdog": {
|
||||||
|
|||||||
@ -61,6 +61,33 @@ def _print_edid(edid: Edid) -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _find_out2_edid_path() -> str:
|
||||||
|
card = os.path.basename(os.readlink("/dev/dri/by-path/platform-gpu-card"))
|
||||||
|
path = f"/sys/devices/platform/gpu/drm/{card}/{card}-HDMI-A-2"
|
||||||
|
with open(os.path.join(path, "status")) as file:
|
||||||
|
if file.read().startswith("d"):
|
||||||
|
raise SystemExit("No display found")
|
||||||
|
return os.path.join(path, "edid")
|
||||||
|
|
||||||
|
|
||||||
|
def _adopt_out2_ids(dest: Edid) -> None:
|
||||||
|
src = Edid.from_file(_find_out2_edid_path())
|
||||||
|
dest.set_monitor_name(src.get_monitor_name())
|
||||||
|
try:
|
||||||
|
dest.get_monitor_serial()
|
||||||
|
except EdidNoBlockError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
ser = src.get_monitor_serial()
|
||||||
|
except EdidNoBlockError:
|
||||||
|
ser = "{:08X}".format(src.get_serial())
|
||||||
|
dest.set_monitor_serial(ser)
|
||||||
|
dest.set_mfc_id(src.get_mfc_id())
|
||||||
|
dest.set_product_id(src.get_product_id())
|
||||||
|
dest.set_serial(src.get_serial())
|
||||||
|
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
def main(argv: (list[str] | None)=None) -> None: # pylint: disable=too-many-branches,too-many-statements
|
def main(argv: (list[str] | None)=None) -> None: # pylint: disable=too-many-branches,too-many-statements
|
||||||
# (parent_parser, argv, _) = init(
|
# (parent_parser, argv, _) = init(
|
||||||
@ -89,6 +116,10 @@ def main(argv: (list[str] | None)=None) -> None: # pylint: disable=too-many-bra
|
|||||||
help="Import the specified bin/hex EDID to the [--edid] file as a hex text", metavar="<file>")
|
help="Import the specified bin/hex EDID to the [--edid] file as a hex text", metavar="<file>")
|
||||||
parser.add_argument("--import-preset", choices=presets,
|
parser.add_argument("--import-preset", choices=presets,
|
||||||
help="Restore default EDID or choose the preset", metavar=f"{{ {' | '.join(presets)} }}",)
|
help="Restore default EDID or choose the preset", metavar=f"{{ {' | '.join(presets)} }}",)
|
||||||
|
parser.add_argument("--import-display-ids", action="store_true",
|
||||||
|
help="On PiKVM V4, import and adopt IDs from a physical display connected to the OUT2 port")
|
||||||
|
parser.add_argument("--import-display", action="store_true",
|
||||||
|
help="On PiKVM V4, import full EDID from a physical display connected to the OUT2 port")
|
||||||
parser.add_argument("--set-audio", type=valid_bool,
|
parser.add_argument("--set-audio", type=valid_bool,
|
||||||
help="Enable or disable audio", metavar="<yes|no>")
|
help="Enable or disable audio", metavar="<yes|no>")
|
||||||
parser.add_argument("--set-mfc-id",
|
parser.add_argument("--set-mfc-id",
|
||||||
@ -120,6 +151,9 @@ def main(argv: (list[str] | None)=None) -> None: # pylint: disable=too-many-bra
|
|||||||
imp = f"_{imp}"
|
imp = f"_{imp}"
|
||||||
options.imp = os.path.join(options.presets_path, f"{imp}.hex")
|
options.imp = os.path.join(options.presets_path, f"{imp}.hex")
|
||||||
|
|
||||||
|
if options.import_display:
|
||||||
|
options.imp = _find_out2_edid_path()
|
||||||
|
|
||||||
orig_edid_path = options.edid_path
|
orig_edid_path = options.edid_path
|
||||||
if options.imp:
|
if options.imp:
|
||||||
options.export_hex = options.edid_path
|
options.export_hex = options.edid_path
|
||||||
@ -128,6 +162,10 @@ def main(argv: (list[str] | None)=None) -> None: # pylint: disable=too-many-bra
|
|||||||
edid = Edid.from_file(options.edid_path)
|
edid = Edid.from_file(options.edid_path)
|
||||||
changed = False
|
changed = False
|
||||||
|
|
||||||
|
if options.import_display_ids:
|
||||||
|
_adopt_out2_ids(edid)
|
||||||
|
changed = True
|
||||||
|
|
||||||
for cmd in dir(Edid):
|
for cmd in dir(Edid):
|
||||||
if cmd.startswith("set_"):
|
if cmd.startswith("set_"):
|
||||||
value = getattr(options, cmd)
|
value = getattr(options, cmd)
|
||||||
|
|||||||
@ -30,27 +30,27 @@ import argparse
|
|||||||
|
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
|
||||||
import passlib.apache
|
|
||||||
|
|
||||||
from ...yamlconf import Section
|
from ...yamlconf import Section
|
||||||
|
|
||||||
from ...validators import ValidatorError
|
from ...validators import ValidatorError
|
||||||
from ...validators.auth import valid_user
|
from ...validators.auth import valid_user
|
||||||
from ...validators.auth import valid_passwd
|
from ...validators.auth import valid_passwd
|
||||||
|
|
||||||
|
from ...crypto import KvmdHtpasswdFile
|
||||||
|
|
||||||
from .. import init
|
from .. import init
|
||||||
|
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
def _get_htpasswd_path(config: Section) -> str:
|
def _get_htpasswd_path(config: Section) -> str:
|
||||||
if config.kvmd.auth.internal.type != "htpasswd":
|
if config.kvmd.auth.internal.type != "htpasswd":
|
||||||
raise SystemExit(f"Error: KVMD internal auth not using 'htpasswd'"
|
raise SystemExit(f"Error: KVMD internal auth does not use 'htpasswd'"
|
||||||
f" (now configured {config.kvmd.auth.internal.type!r})")
|
f" (now configured {config.kvmd.auth.internal.type!r})")
|
||||||
return config.kvmd.auth.internal.file
|
return config.kvmd.auth.internal.file
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def _get_htpasswd_for_write(config: Section) -> Generator[passlib.apache.HtpasswdFile, None, None]:
|
def _get_htpasswd_for_write(config: Section) -> Generator[KvmdHtpasswdFile, None, None]:
|
||||||
path = _get_htpasswd_path(config)
|
path = _get_htpasswd_path(config)
|
||||||
(tmp_fd, tmp_path) = tempfile.mkstemp(
|
(tmp_fd, tmp_path) = tempfile.mkstemp(
|
||||||
prefix=f".{os.path.basename(path)}.",
|
prefix=f".{os.path.basename(path)}.",
|
||||||
@ -65,7 +65,7 @@ def _get_htpasswd_for_write(config: Section) -> Generator[passlib.apache.Htpassw
|
|||||||
os.fchmod(tmp_fd, st.st_mode)
|
os.fchmod(tmp_fd, st.st_mode)
|
||||||
finally:
|
finally:
|
||||||
os.close(tmp_fd)
|
os.close(tmp_fd)
|
||||||
htpasswd = passlib.apache.HtpasswdFile(tmp_path)
|
htpasswd = KvmdHtpasswdFile(tmp_path)
|
||||||
yield htpasswd
|
yield htpasswd
|
||||||
htpasswd.save()
|
htpasswd.save()
|
||||||
os.rename(tmp_path, path)
|
os.rename(tmp_path, path)
|
||||||
@ -96,28 +96,55 @@ def _print_invalidate_tip(prepend_nl: bool) -> None:
|
|||||||
|
|
||||||
# ====
|
# ====
|
||||||
def _cmd_list(config: Section, _: argparse.Namespace) -> None:
|
def _cmd_list(config: Section, _: argparse.Namespace) -> None:
|
||||||
for user in sorted(passlib.apache.HtpasswdFile(_get_htpasswd_path(config)).users()):
|
for user in sorted(KvmdHtpasswdFile(_get_htpasswd_path(config)).users()):
|
||||||
print(user)
|
print(user)
|
||||||
|
|
||||||
|
|
||||||
def _cmd_set(config: Section, options: argparse.Namespace) -> None:
|
def _change_user(config: Section, options: argparse.Namespace, create: bool) -> None:
|
||||||
with _get_htpasswd_for_write(config) as htpasswd:
|
with _get_htpasswd_for_write(config) as htpasswd:
|
||||||
|
assert options.user == options.user.strip()
|
||||||
|
assert options.user
|
||||||
|
|
||||||
has_user = (options.user in htpasswd.users())
|
has_user = (options.user in htpasswd.users())
|
||||||
|
if create:
|
||||||
|
if has_user:
|
||||||
|
raise SystemExit(f"The user {options.user!r} is already exists")
|
||||||
|
else:
|
||||||
|
if not has_user:
|
||||||
|
raise SystemExit(f"The user {options.user!r} is not exist")
|
||||||
|
|
||||||
if options.read_stdin:
|
if options.read_stdin:
|
||||||
passwd = valid_passwd(input())
|
passwd = valid_passwd(input())
|
||||||
else:
|
else:
|
||||||
passwd = valid_passwd(getpass.getpass("Password: ", stream=sys.stderr))
|
passwd = valid_passwd(getpass.getpass("Password: ", stream=sys.stderr))
|
||||||
if valid_passwd(getpass.getpass("Repeat: ", stream=sys.stderr)) != passwd:
|
if valid_passwd(getpass.getpass("Repeat: ", stream=sys.stderr)) != passwd:
|
||||||
raise SystemExit("Sorry, passwords do not match")
|
raise SystemExit("Sorry, passwords do not match")
|
||||||
|
|
||||||
htpasswd.set_password(options.user, passwd)
|
htpasswd.set_password(options.user, passwd)
|
||||||
|
|
||||||
if has_user and not options.quiet:
|
if has_user and not options.quiet:
|
||||||
_print_invalidate_tip(True)
|
_print_invalidate_tip(True)
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_add(config: Section, options: argparse.Namespace) -> None:
|
||||||
|
_change_user(config, options, create=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_set(config: Section, options: argparse.Namespace) -> None:
|
||||||
|
_change_user(config, options, create=False)
|
||||||
|
|
||||||
|
|
||||||
def _cmd_delete(config: Section, options: argparse.Namespace) -> None:
|
def _cmd_delete(config: Section, options: argparse.Namespace) -> None:
|
||||||
with _get_htpasswd_for_write(config) as htpasswd:
|
with _get_htpasswd_for_write(config) as htpasswd:
|
||||||
|
assert options.user == options.user.strip()
|
||||||
|
assert options.user
|
||||||
|
|
||||||
has_user = (options.user in htpasswd.users())
|
has_user = (options.user in htpasswd.users())
|
||||||
|
if not has_user:
|
||||||
|
raise SystemExit(f"The user {options.user!r} is not exist")
|
||||||
|
|
||||||
htpasswd.delete(options.user)
|
htpasswd.delete(options.user)
|
||||||
|
|
||||||
if has_user and not options.quiet:
|
if has_user and not options.quiet:
|
||||||
_print_invalidate_tip(False)
|
_print_invalidate_tip(False)
|
||||||
|
|
||||||
@ -138,19 +165,25 @@ def main(argv: (list[str] | None)=None) -> None:
|
|||||||
parser.set_defaults(cmd=(lambda *_: parser.print_help()))
|
parser.set_defaults(cmd=(lambda *_: parser.print_help()))
|
||||||
subparsers = parser.add_subparsers()
|
subparsers = parser.add_subparsers()
|
||||||
|
|
||||||
cmd_list_parser = subparsers.add_parser("list", help="List users")
|
sub = subparsers.add_parser("list", help="List users")
|
||||||
cmd_list_parser.set_defaults(cmd=_cmd_list)
|
sub.set_defaults(cmd=_cmd_list)
|
||||||
|
|
||||||
cmd_set_parser = subparsers.add_parser("set", help="Create user or change password")
|
sub = subparsers.add_parser("add", help="Add user")
|
||||||
cmd_set_parser.add_argument("user", type=valid_user)
|
sub.add_argument("user", type=valid_user)
|
||||||
cmd_set_parser.add_argument("-i", "--read-stdin", action="store_true", help="Read password from stdin")
|
sub.add_argument("-i", "--read-stdin", action="store_true", help="Read password from stdin")
|
||||||
cmd_set_parser.add_argument("-q", "--quiet", action="store_true", help="Don't show invalidation note")
|
sub.add_argument("-q", "--quiet", action="store_true", help="Don't show invalidation note")
|
||||||
cmd_set_parser.set_defaults(cmd=_cmd_set)
|
sub.set_defaults(cmd=_cmd_add)
|
||||||
|
|
||||||
cmd_delete_parser = subparsers.add_parser("del", help="Delete user")
|
sub = subparsers.add_parser("set", help="Change user's password")
|
||||||
cmd_delete_parser.add_argument("user", type=valid_user)
|
sub.add_argument("user", type=valid_user)
|
||||||
cmd_delete_parser.add_argument("-q", "--quiet", action="store_true", help="Don't show invalidation note")
|
sub.add_argument("-i", "--read-stdin", action="store_true", help="Read password from stdin")
|
||||||
cmd_delete_parser.set_defaults(cmd=_cmd_delete)
|
sub.add_argument("-q", "--quiet", action="store_true", help="Don't show invalidation note")
|
||||||
|
sub.set_defaults(cmd=_cmd_set)
|
||||||
|
|
||||||
|
sub = subparsers.add_parser("del", help="Delete user")
|
||||||
|
sub.add_argument("user", type=valid_user)
|
||||||
|
sub.add_argument("-q", "--quiet", action="store_true", help="Don't show invalidation note")
|
||||||
|
sub.set_defaults(cmd=_cmd_delete)
|
||||||
|
|
||||||
options = parser.parse_args(argv[1:])
|
options = parser.parse_args(argv[1:])
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -20,7 +20,13 @@
|
|||||||
# ========================================================================== #
|
# ========================================================================== #
|
||||||
|
|
||||||
|
|
||||||
import dataclasses
|
import threading
|
||||||
|
import functools
|
||||||
|
import time
|
||||||
|
|
||||||
|
from ...logging import get_logger
|
||||||
|
|
||||||
|
from ... import tools
|
||||||
|
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
@ -29,60 +35,42 @@ class IpmiPasswdError(Exception):
|
|||||||
super().__init__(f"Syntax error at {path}:{lineno}: {msg}")
|
super().__init__(f"Syntax error at {path}:{lineno}: {msg}")
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
|
||||||
class IpmiUserCredentials:
|
|
||||||
ipmi_user: str
|
|
||||||
ipmi_passwd: str
|
|
||||||
kvmd_user: str
|
|
||||||
kvmd_passwd: str
|
|
||||||
|
|
||||||
|
|
||||||
class IpmiAuthManager:
|
class IpmiAuthManager:
|
||||||
def __init__(self, path: str) -> None:
|
def __init__(self, path: str) -> None:
|
||||||
self.__path = path
|
self.__path = path
|
||||||
with open(path) as file:
|
self.__lock = threading.Lock()
|
||||||
self.__credentials = self.__parse_passwd_file(file.read().split("\n"))
|
|
||||||
|
|
||||||
def __contains__(self, ipmi_user: str) -> bool:
|
def get(self, user: str) -> (str | None):
|
||||||
return (ipmi_user in self.__credentials)
|
creds = self.__get_credentials(int(time.time()))
|
||||||
|
return creds.get(user)
|
||||||
|
|
||||||
def __getitem__(self, ipmi_user: str) -> str:
|
@functools.lru_cache(maxsize=1)
|
||||||
return self.__credentials[ipmi_user].ipmi_passwd
|
def __get_credentials(self, ts: int) -> dict[str, str]:
|
||||||
|
_ = ts
|
||||||
|
with self.__lock:
|
||||||
|
try:
|
||||||
|
return self.__read_credentials()
|
||||||
|
except Exception as ex:
|
||||||
|
get_logger().error("%s", tools.efmt(ex))
|
||||||
|
return {}
|
||||||
|
|
||||||
def get_credentials(self, ipmi_user: str) -> IpmiUserCredentials:
|
def __read_credentials(self) -> dict[str, str]:
|
||||||
return self.__credentials[ipmi_user]
|
with open(self.__path) as file:
|
||||||
|
creds: dict[str, str] = {}
|
||||||
|
for (lineno, line) in tools.passwds_splitted(file.read()):
|
||||||
|
if " -> " in line: # Compatibility with old ipmipasswd file format
|
||||||
|
line = line.split(" -> ", 1)[0]
|
||||||
|
|
||||||
def __parse_passwd_file(self, lines: list[str]) -> dict[str, IpmiUserCredentials]:
|
if ":" not in line:
|
||||||
credentials: dict[str, IpmiUserCredentials] = {}
|
raise IpmiPasswdError(self.__path, lineno, "Missing ':' operator")
|
||||||
for (lineno, line) in enumerate(lines):
|
|
||||||
if len(line.strip()) == 0 or line.lstrip().startswith("#"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if " -> " not in line:
|
(user, passwd) = line.split(":", 1)
|
||||||
raise IpmiPasswdError(self.__path, lineno, "Missing ' -> ' operator")
|
user = user.strip()
|
||||||
|
if len(user) == 0:
|
||||||
|
raise IpmiPasswdError(self.__path, lineno, "Empty IPMI user")
|
||||||
|
|
||||||
(left, right) = map(str.lstrip, line.split(" -> ", 1))
|
if user in creds:
|
||||||
for (name, pair) in [("left", left), ("right", right)]:
|
raise IpmiPasswdError(self.__path, lineno, f"Found duplicating user {user!r}")
|
||||||
if ":" not in pair:
|
|
||||||
raise IpmiPasswdError(self.__path, lineno, f"Missing ':' operator in {name} credentials")
|
|
||||||
|
|
||||||
(ipmi_user, ipmi_passwd) = left.split(":")
|
creds[user] = passwd
|
||||||
ipmi_user = ipmi_user.strip()
|
return creds
|
||||||
if len(ipmi_user) == 0:
|
|
||||||
raise IpmiPasswdError(self.__path, lineno, "Empty IPMI user (left)")
|
|
||||||
|
|
||||||
(kvmd_user, kvmd_passwd) = right.split(":")
|
|
||||||
kvmd_user = kvmd_user.strip()
|
|
||||||
if len(kvmd_user) == 0:
|
|
||||||
raise IpmiPasswdError(self.__path, lineno, "Empty KVMD user (left)")
|
|
||||||
|
|
||||||
if ipmi_user in credentials:
|
|
||||||
raise IpmiPasswdError(self.__path, lineno, f"Found duplicating user {ipmi_user!r} (left)")
|
|
||||||
|
|
||||||
credentials[ipmi_user] = IpmiUserCredentials(
|
|
||||||
ipmi_user=ipmi_user,
|
|
||||||
ipmi_passwd=ipmi_passwd,
|
|
||||||
kvmd_user=kvmd_user,
|
|
||||||
kvmd_passwd=kvmd_passwd,
|
|
||||||
)
|
|
||||||
return credentials
|
|
||||||
|
|||||||
@ -70,7 +70,6 @@ class IpmiServer(BaseIpmiServer): # pylint: disable=too-many-instance-attribute
|
|||||||
|
|
||||||
super().__init__(authdata=auth_manager, address=host, port=port)
|
super().__init__(authdata=auth_manager, address=host, port=port)
|
||||||
|
|
||||||
self.__auth_manager = auth_manager
|
|
||||||
self.__kvmd = kvmd
|
self.__kvmd = kvmd
|
||||||
|
|
||||||
self.__host = host
|
self.__host = host
|
||||||
@ -165,11 +164,10 @@ class IpmiServer(BaseIpmiServer): # pylint: disable=too-many-instance-attribute
|
|||||||
def __make_request(self, session: IpmiServerSession, name: str, func_path: str, **kwargs): # type: ignore
|
def __make_request(self, session: IpmiServerSession, name: str, func_path: str, **kwargs): # type: ignore
|
||||||
async def runner(): # type: ignore
|
async def runner(): # type: ignore
|
||||||
logger = get_logger(0)
|
logger = get_logger(0)
|
||||||
credentials = self.__auth_manager.get_credentials(session.username.decode())
|
logger.info("[%s]: Performing request %s from IPMI user %r ...",
|
||||||
logger.info("[%s]: Performing request %s from user %r (IPMI) as %r (KVMD)",
|
session.sockaddr[0], name, session.username.decode())
|
||||||
session.sockaddr[0], name, credentials.ipmi_user, credentials.kvmd_user)
|
|
||||||
try:
|
try:
|
||||||
async with self.__kvmd.make_session(credentials.kvmd_user, credentials.kvmd_passwd) as kvmd_session:
|
async with self.__kvmd.make_session() as kvmd_session:
|
||||||
func = functools.reduce(getattr, func_path.split("."), kvmd_session)
|
func = functools.reduce(getattr, func_path.split("."), kvmd_session)
|
||||||
return (await func(**kwargs))
|
return (await func(**kwargs))
|
||||||
except (aiohttp.ClientError, asyncio.TimeoutError) as ex:
|
except (aiohttp.ClientError, asyncio.TimeoutError) as ex:
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import asyncio
|
|||||||
import asyncio.subprocess
|
import asyncio.subprocess
|
||||||
import socket
|
import socket
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import netifaces
|
import netifaces
|
||||||
|
|
||||||
@ -21,6 +23,7 @@ class _Netcfg:
|
|||||||
nat_type: StunNatType = dataclasses.field(default=StunNatType.ERROR)
|
nat_type: StunNatType = dataclasses.field(default=StunNatType.ERROR)
|
||||||
src_ip: str = dataclasses.field(default="")
|
src_ip: str = dataclasses.field(default="")
|
||||||
ext_ip: str = dataclasses.field(default="")
|
ext_ip: str = dataclasses.field(default="")
|
||||||
|
stun_host: str = dataclasses.field(default="")
|
||||||
stun_ip: str = dataclasses.field(default="")
|
stun_ip: str = dataclasses.field(default="")
|
||||||
stun_port: int = dataclasses.field(default=0)
|
stun_port: int = dataclasses.field(default=0)
|
||||||
|
|
||||||
@ -42,6 +45,7 @@ class JanusRunner: # pylint: disable=too-many-instance-attributes
|
|||||||
cmd: list[str],
|
cmd: list[str],
|
||||||
cmd_remove: list[str],
|
cmd_remove: list[str],
|
||||||
cmd_append: list[str],
|
cmd_append: list[str],
|
||||||
|
ice_servers: list[dict[str, Any]],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
self.__stun = Stun(stun_host, stun_port, stun_timeout, stun_retries, stun_retries_delay)
|
self.__stun = Stun(stun_host, stun_port, stun_timeout, stun_retries, stun_retries_delay)
|
||||||
@ -51,6 +55,7 @@ class JanusRunner: # pylint: disable=too-many-instance-attributes
|
|||||||
self.__check_retries_delay = check_retries_delay
|
self.__check_retries_delay = check_retries_delay
|
||||||
|
|
||||||
self.__cmd = tools.build_cmd(cmd, cmd_remove, cmd_append)
|
self.__cmd = tools.build_cmd(cmd, cmd_remove, cmd_append)
|
||||||
|
self.__ice_servers = ice_servers
|
||||||
|
|
||||||
self.__janus_task: (asyncio.Task | None) = None
|
self.__janus_task: (asyncio.Task | None) = None
|
||||||
self.__janus_proc: (asyncio.subprocess.Process | None) = None # pylint: disable=no-member
|
self.__janus_proc: (asyncio.subprocess.Process | None) = None # pylint: disable=no-member
|
||||||
@ -172,10 +177,25 @@ class JanusRunner: # pylint: disable=too-many-instance-attributes
|
|||||||
part.format(**placeholders)
|
part.format(**placeholders)
|
||||||
for part in cmd
|
for part in cmd
|
||||||
]
|
]
|
||||||
self.__janus_proc = await aioproc.run_process(cmd)
|
env = {}
|
||||||
|
ice_payload = self.__build_ice_payload(netcfg)
|
||||||
|
if ice_payload:
|
||||||
|
env["JANUS_USTREAMER_WEB_ICE_URL"] = ice_payload
|
||||||
|
self.__janus_proc = await aioproc.run_process(cmd=cmd, env=env or None)
|
||||||
get_logger(0).info("Started Janus pid=%d: %s", self.__janus_proc.pid, tools.cmdfmt(cmd))
|
get_logger(0).info("Started Janus pid=%d: %s", self.__janus_proc.pid, tools.cmdfmt(cmd))
|
||||||
|
|
||||||
async def __kill_janus_proc(self) -> None:
|
async def __kill_janus_proc(self) -> None:
|
||||||
if self.__janus_proc:
|
if self.__janus_proc:
|
||||||
await aioproc.kill_process(self.__janus_proc, 5, get_logger(0))
|
await aioproc.kill_process(self.__janus_proc, 5, get_logger(0))
|
||||||
self.__janus_proc = None
|
self.__janus_proc = None
|
||||||
|
|
||||||
|
def __build_ice_payload(self, netcfg: _Netcfg) -> (str | None):
|
||||||
|
if self.__ice_servers:
|
||||||
|
try:
|
||||||
|
return f"json:{json.dumps(self.__ice_servers, ensure_ascii=False)}"
|
||||||
|
except Exception as ex: # pragma: no cover
|
||||||
|
get_logger(0).error("Can't encode ICE servers: %s", tools.efmt(ex))
|
||||||
|
return None
|
||||||
|
if netcfg.stun_host and netcfg.stun_port:
|
||||||
|
return f"stun:{netcfg.stun_host}:{netcfg.stun_port}"
|
||||||
|
return None
|
||||||
|
|||||||
@ -30,6 +30,7 @@ class StunInfo:
|
|||||||
nat_type: StunNatType
|
nat_type: StunNatType
|
||||||
src_ip: str
|
src_ip: str
|
||||||
ext_ip: str
|
ext_ip: str
|
||||||
|
stun_host: str
|
||||||
stun_ip: str
|
stun_ip: str
|
||||||
stun_port: int
|
stun_port: int
|
||||||
|
|
||||||
@ -102,6 +103,7 @@ class Stun:
|
|||||||
nat_type=nat_type,
|
nat_type=nat_type,
|
||||||
src_ip=src_ip,
|
src_ip=src_ip,
|
||||||
ext_ip=ext_ip,
|
ext_ip=ext_ip,
|
||||||
|
stun_host=self.__host,
|
||||||
stun_ip=self.__stun_ip,
|
stun_ip=self.__stun_ip,
|
||||||
stun_port=self.__port,
|
stun_port=self.__port,
|
||||||
)
|
)
|
||||||
@ -134,7 +136,12 @@ class Stun:
|
|||||||
return (StunNatType.FULL_CONE_NAT, resp)
|
return (StunNatType.FULL_CONE_NAT, resp)
|
||||||
|
|
||||||
if first.changed is None:
|
if first.changed is None:
|
||||||
raise RuntimeError(f"Changed addr is None: {first}")
|
get_logger(0).warning(
|
||||||
|
"STUN server %s:%d responded without CHANGED-ADDRESS; skipping NAT type detection",
|
||||||
|
self.__host,
|
||||||
|
self.__port,
|
||||||
|
)
|
||||||
|
return (StunNatType.ERROR, first)
|
||||||
resp = await self.__make_request("Change request [ext_ip != src_ip]", first.changed, b"")
|
resp = await self.__make_request("Change request [ext_ip != src_ip]", first.changed, b"")
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
return (StunNatType.CHANGED_ADDR_ERROR, resp)
|
return (StunNatType.CHANGED_ADDR_ERROR, resp)
|
||||||
|
|||||||
@ -76,14 +76,17 @@ def main(argv: (list[str] | None)=None) -> None:
|
|||||||
KvmdServer(
|
KvmdServer(
|
||||||
auth_manager=AuthManager(
|
auth_manager=AuthManager(
|
||||||
enabled=config.auth.enabled,
|
enabled=config.auth.enabled,
|
||||||
|
expire=config.auth.expire,
|
||||||
|
usc_users=config.auth.usc.users,
|
||||||
|
usc_groups=config.auth.usc.groups,
|
||||||
unauth_paths=([] if config.prometheus.auth.enabled else ["/export/prometheus/metrics"]),
|
unauth_paths=([] if config.prometheus.auth.enabled else ["/export/prometheus/metrics"]),
|
||||||
|
|
||||||
internal_type=config.auth.internal.type,
|
int_type=config.auth.internal.type,
|
||||||
internal_kwargs=config.auth.internal._unpack(ignore=["type", "force_users"]),
|
int_kwargs=config.auth.internal._unpack(ignore=["type", "force_users"]),
|
||||||
force_internal_users=config.auth.internal.force_users,
|
force_int_users=config.auth.internal.force_users,
|
||||||
|
|
||||||
external_type=config.auth.external.type,
|
ext_type=config.auth.external.type,
|
||||||
external_kwargs=(config.auth.external._unpack(ignore=["type"]) if config.auth.external.type else {}),
|
ext_kwargs=(config.auth.external._unpack(ignore=["type"]) if config.auth.external.type else {}),
|
||||||
|
|
||||||
totp_secret_path=config.auth.totp.secret.file,
|
totp_secret_path=config.auth.totp.secret.file,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -31,9 +31,11 @@ from ....htserver import HttpExposed
|
|||||||
from ....htserver import exposed_http
|
from ....htserver import exposed_http
|
||||||
from ....htserver import make_json_response
|
from ....htserver import make_json_response
|
||||||
from ....htserver import set_request_auth_info
|
from ....htserver import set_request_auth_info
|
||||||
|
from ....htserver import get_request_unix_credentials
|
||||||
|
|
||||||
from ....validators.auth import valid_user
|
from ....validators.auth import valid_user
|
||||||
from ....validators.auth import valid_passwd
|
from ....validators.auth import valid_passwd
|
||||||
|
from ....validators.auth import valid_expire
|
||||||
from ....validators.auth import valid_auth_token
|
from ....validators.auth import valid_auth_token
|
||||||
|
|
||||||
from ..auth import AuthManager
|
from ..auth import AuthManager
|
||||||
@ -43,39 +45,64 @@ from ..auth import AuthManager
|
|||||||
_COOKIE_AUTH_TOKEN = "auth_token"
|
_COOKIE_AUTH_TOKEN = "auth_token"
|
||||||
|
|
||||||
|
|
||||||
async def check_request_auth(auth_manager: AuthManager, exposed: HttpExposed, req: Request) -> None:
|
async def _check_xhdr(auth_manager: AuthManager, _: HttpExposed, req: Request) -> bool:
|
||||||
if auth_manager.is_auth_required(exposed):
|
user = req.headers.get("X-KVMD-User", "")
|
||||||
user = req.headers.get("X-KVMD-User", "")
|
if user:
|
||||||
|
user = valid_user(user)
|
||||||
|
passwd = req.headers.get("X-KVMD-Passwd", "")
|
||||||
|
set_request_auth_info(req, f"{user} (xhdr)")
|
||||||
|
if (await auth_manager.authorize(user, valid_passwd(passwd))):
|
||||||
|
return True
|
||||||
|
raise ForbiddenError()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_token(auth_manager: AuthManager, _: HttpExposed, req: Request) -> bool:
|
||||||
|
token = req.cookies.get(_COOKIE_AUTH_TOKEN, "")
|
||||||
|
if token:
|
||||||
|
user = auth_manager.check(valid_auth_token(token))
|
||||||
if user:
|
if user:
|
||||||
user = valid_user(user)
|
|
||||||
passwd = req.headers.get("X-KVMD-Passwd", "")
|
|
||||||
set_request_auth_info(req, f"{user} (xhdr)")
|
|
||||||
if not (await auth_manager.authorize(user, valid_passwd(passwd))):
|
|
||||||
raise ForbiddenError()
|
|
||||||
return
|
|
||||||
|
|
||||||
token = req.cookies.get(_COOKIE_AUTH_TOKEN, "")
|
|
||||||
if token:
|
|
||||||
user = auth_manager.check(valid_auth_token(token)) # type: ignore
|
|
||||||
if not user:
|
|
||||||
set_request_auth_info(req, "- (token)")
|
|
||||||
raise ForbiddenError()
|
|
||||||
set_request_auth_info(req, f"{user} (token)")
|
set_request_auth_info(req, f"{user} (token)")
|
||||||
return
|
return True
|
||||||
|
set_request_auth_info(req, "- (token)")
|
||||||
|
raise ForbiddenError()
|
||||||
|
return False
|
||||||
|
|
||||||
basic_auth = req.headers.get("Authorization", "")
|
|
||||||
if basic_auth and basic_auth[:6].lower() == "basic ":
|
|
||||||
try:
|
|
||||||
(user, passwd) = base64.b64decode(basic_auth[6:]).decode("utf-8").split(":")
|
|
||||||
except Exception:
|
|
||||||
raise UnauthorizedError()
|
|
||||||
user = valid_user(user)
|
|
||||||
set_request_auth_info(req, f"{user} (basic)")
|
|
||||||
if not (await auth_manager.authorize(user, valid_passwd(passwd))):
|
|
||||||
raise ForbiddenError()
|
|
||||||
return
|
|
||||||
|
|
||||||
|
async def _check_basic(auth_manager: AuthManager, _: HttpExposed, req: Request) -> bool:
|
||||||
|
basic_auth = req.headers.get("Authorization", "")
|
||||||
|
if basic_auth and basic_auth[:6].lower() == "basic ":
|
||||||
|
try:
|
||||||
|
(user, passwd) = base64.b64decode(basic_auth[6:]).decode("utf-8").split(":")
|
||||||
|
except Exception:
|
||||||
|
raise UnauthorizedError()
|
||||||
|
user = valid_user(user)
|
||||||
|
set_request_auth_info(req, f"{user} (basic)")
|
||||||
|
if (await auth_manager.authorize(user, valid_passwd(passwd))):
|
||||||
|
return True
|
||||||
|
raise ForbiddenError()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_usc(auth_manager: AuthManager, exposed: HttpExposed, req: Request) -> bool:
|
||||||
|
if exposed.allow_usc:
|
||||||
|
creds = get_request_unix_credentials(req)
|
||||||
|
if creds is not None:
|
||||||
|
user = auth_manager.check_unix_credentials(creds)
|
||||||
|
if user:
|
||||||
|
set_request_auth_info(req, f"{user}[{creds.uid}] (unix)")
|
||||||
|
return True
|
||||||
raise UnauthorizedError()
|
raise UnauthorizedError()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def check_request_auth(auth_manager: AuthManager, exposed: HttpExposed, req: Request) -> None:
|
||||||
|
if not auth_manager.is_auth_required(exposed):
|
||||||
|
return
|
||||||
|
for checker in [_check_xhdr, _check_token, _check_basic, _check_usc]:
|
||||||
|
if (await checker(auth_manager, exposed, req)):
|
||||||
|
return
|
||||||
|
raise UnauthorizedError()
|
||||||
|
|
||||||
|
|
||||||
class AuthApi:
|
class AuthApi:
|
||||||
@ -84,26 +111,28 @@ class AuthApi:
|
|||||||
|
|
||||||
# =====
|
# =====
|
||||||
|
|
||||||
@exposed_http("POST", "/auth/login", auth_required=False)
|
@exposed_http("POST", "/auth/login", auth_required=False, allow_usc=False)
|
||||||
async def __login_handler(self, req: Request) -> Response:
|
async def __login_handler(self, req: Request) -> Response:
|
||||||
if self.__auth_manager.is_auth_enabled():
|
if self.__auth_manager.is_auth_enabled():
|
||||||
credentials = await req.post()
|
credentials = await req.post()
|
||||||
token = await self.__auth_manager.login(
|
token = await self.__auth_manager.login(
|
||||||
user=valid_user(credentials.get("user", "")),
|
user=valid_user(credentials.get("user", "")),
|
||||||
passwd=valid_passwd(credentials.get("passwd", "")),
|
passwd=valid_passwd(credentials.get("passwd", "")),
|
||||||
|
expire=valid_expire(credentials.get("expire", "0")),
|
||||||
)
|
)
|
||||||
if token:
|
if token:
|
||||||
return make_json_response(set_cookies={_COOKIE_AUTH_TOKEN: token})
|
return make_json_response(set_cookies={_COOKIE_AUTH_TOKEN: token})
|
||||||
raise ForbiddenError()
|
raise ForbiddenError()
|
||||||
return make_json_response()
|
return make_json_response()
|
||||||
|
|
||||||
@exposed_http("POST", "/auth/logout")
|
@exposed_http("POST", "/auth/logout", allow_usc=False)
|
||||||
async def __logout_handler(self, req: Request) -> Response:
|
async def __logout_handler(self, req: Request) -> Response:
|
||||||
if self.__auth_manager.is_auth_enabled():
|
if self.__auth_manager.is_auth_enabled():
|
||||||
token = valid_auth_token(req.cookies.get(_COOKIE_AUTH_TOKEN, ""))
|
token = valid_auth_token(req.cookies.get(_COOKIE_AUTH_TOKEN, ""))
|
||||||
self.__auth_manager.logout(token)
|
self.__auth_manager.logout(token)
|
||||||
return make_json_response()
|
return make_json_response()
|
||||||
|
|
||||||
@exposed_http("GET", "/auth/check")
|
# XXX: This handle is used for access control so it should NEVER allow access by socket credentials
|
||||||
|
@exposed_http("GET", "/auth/check", allow_usc=False)
|
||||||
async def __check_handler(self, _: Request) -> Response:
|
async def __check_handler(self, _: Request) -> Response:
|
||||||
return make_json_response()
|
return make_json_response()
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import re
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -57,7 +58,7 @@ class ExportApi:
|
|||||||
async def __get_prometheus_metrics(self) -> str:
|
async def __get_prometheus_metrics(self) -> str:
|
||||||
(atx_state, info_state, gpio_state) = await asyncio.gather(*[
|
(atx_state, info_state, gpio_state) = await asyncio.gather(*[
|
||||||
self.__atx.get_state(),
|
self.__atx.get_state(),
|
||||||
self.__info_manager.get_state(["hw", "fan"]),
|
self.__info_manager.get_state(["health", "fan"]),
|
||||||
self.__user_gpio.get_state(),
|
self.__user_gpio.get_state(),
|
||||||
])
|
])
|
||||||
rows: list[str] = []
|
rows: list[str] = []
|
||||||
@ -68,10 +69,11 @@ class ExportApi:
|
|||||||
for mode in sorted(UserGpioModes.ALL):
|
for mode in sorted(UserGpioModes.ALL):
|
||||||
for (channel, ch_state) in gpio_state["state"][f"{mode}s"].items(): # type: ignore
|
for (channel, ch_state) in gpio_state["state"][f"{mode}s"].items(): # type: ignore
|
||||||
if not channel.startswith("__"): # Hide special GPIOs
|
if not channel.startswith("__"): # Hide special GPIOs
|
||||||
|
channel = re.sub(r"[^\w]", "_", channel)
|
||||||
for key in ["online", "state"]:
|
for key in ["online", "state"]:
|
||||||
self.__append_prometheus_rows(rows, ch_state["state"], f"pikvm_gpio_{mode}_{key}_{channel}")
|
self.__append_prometheus_rows(rows, ch_state["state"], f"pikvm_gpio_{mode}_{key}_{channel}")
|
||||||
|
|
||||||
self.__append_prometheus_rows(rows, info_state["hw"]["health"], "pikvm_hw") # type: ignore
|
self.__append_prometheus_rows(rows, info_state["health"], "pikvm_hw") # type: ignore
|
||||||
self.__append_prometheus_rows(rows, info_state["fan"], "pikvm_fan")
|
self.__append_prometheus_rows(rows, info_state["fan"], "pikvm_fan")
|
||||||
|
|
||||||
return "\n".join(rows)
|
return "\n".join(rows)
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
import os
|
import os
|
||||||
import stat
|
import stat
|
||||||
import functools
|
import functools
|
||||||
|
import itertools
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
@ -31,8 +32,11 @@ from typing import Callable
|
|||||||
from aiohttp.web import Request
|
from aiohttp.web import Request
|
||||||
from aiohttp.web import Response
|
from aiohttp.web import Response
|
||||||
|
|
||||||
|
from ....keyboard.mappings import WEB_TO_EVDEV
|
||||||
from ....keyboard.keysym import build_symmap
|
from ....keyboard.keysym import build_symmap
|
||||||
from ....keyboard.printer import text_to_web_keys
|
from ....keyboard.printer import text_to_evdev_keys
|
||||||
|
|
||||||
|
from ....mouse import MOUSE_TO_EVDEV
|
||||||
|
|
||||||
from ....htserver import exposed_http
|
from ....htserver import exposed_http
|
||||||
from ....htserver import exposed_ws
|
from ....htserver import exposed_ws
|
||||||
@ -43,7 +47,9 @@ from ....plugins.hid import BaseHid
|
|||||||
|
|
||||||
from ....validators import raise_error
|
from ....validators import raise_error
|
||||||
from ....validators.basic import valid_bool
|
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_f0
|
||||||
|
from ....validators.basic import valid_string_list
|
||||||
from ....validators.os import valid_printable_filename
|
from ....validators.os import valid_printable_filename
|
||||||
from ....validators.hid import valid_hid_keyboard_output
|
from ....validators.hid import valid_hid_keyboard_output
|
||||||
from ....validators.hid import valid_hid_mouse_output
|
from ....validators.hid import valid_hid_mouse_output
|
||||||
@ -97,6 +103,11 @@ class HidApi:
|
|||||||
await self.__hid.reset()
|
await self.__hid.reset()
|
||||||
return make_json_response()
|
return make_json_response()
|
||||||
|
|
||||||
|
@exposed_http("GET", "/hid/inactivity")
|
||||||
|
async def __inactivity_handler(self, _: Request) -> Response:
|
||||||
|
secs = self.__hid.get_inactivity_seconds()
|
||||||
|
return make_json_response({"inactivity": secs})
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
|
|
||||||
async def get_keymaps(self) -> dict: # Ugly hack to generate hid_keymaps_state (see server.py)
|
async def get_keymaps(self) -> dict: # Ugly hack to generate hid_keymaps_state (see server.py)
|
||||||
@ -119,15 +130,26 @@ class HidApi:
|
|||||||
@exposed_http("POST", "/hid/print")
|
@exposed_http("POST", "/hid/print")
|
||||||
async def __print_handler(self, req: Request) -> Response:
|
async def __print_handler(self, req: Request) -> Response:
|
||||||
text = await req.text()
|
text = await req.text()
|
||||||
limit = int(valid_int_f0(req.query.get("limit", 1024)))
|
limit = valid_int_f0(req.query.get("limit", 1024))
|
||||||
if limit > 0:
|
if limit > 0:
|
||||||
text = text[:limit]
|
text = text[:limit]
|
||||||
symmap = self.__ensure_symmap(req.query.get("keymap", self.__default_keymap_name))
|
symmap = self.__ensure_symmap(req.query.get("keymap", self.__default_keymap_name))
|
||||||
slow = valid_bool(req.query.get("slow", False))
|
slow = valid_bool(req.query.get("slow", False))
|
||||||
await self.__hid.send_key_events(text_to_web_keys(text, symmap), no_ignore_keys=True, slow=slow)
|
delay = float(valid_number(
|
||||||
|
arg=req.query.get("delay", (0.02 if slow else 0)),
|
||||||
|
min=0,
|
||||||
|
max=5,
|
||||||
|
type=float,
|
||||||
|
name="keys delay",
|
||||||
|
))
|
||||||
|
await self.__hid.send_key_events(
|
||||||
|
keys=text_to_evdev_keys(text, symmap),
|
||||||
|
no_ignore_keys=True,
|
||||||
|
delay=delay,
|
||||||
|
)
|
||||||
return make_json_response()
|
return make_json_response()
|
||||||
|
|
||||||
def __ensure_symmap(self, keymap_name: str) -> dict[int, dict[int, str]]:
|
def __ensure_symmap(self, keymap_name: str) -> dict[int, dict[int, int]]:
|
||||||
keymap_name = valid_printable_filename(keymap_name, "keymap")
|
keymap_name = valid_printable_filename(keymap_name, "keymap")
|
||||||
path = os.path.join(self.__keymaps_dir_path, keymap_name)
|
path = os.path.join(self.__keymaps_dir_path, keymap_name)
|
||||||
try:
|
try:
|
||||||
@ -139,7 +161,7 @@ class HidApi:
|
|||||||
return self.__inner_ensure_symmap(path, st.st_mtime)
|
return self.__inner_ensure_symmap(path, st.st_mtime)
|
||||||
|
|
||||||
@functools.lru_cache(maxsize=10)
|
@functools.lru_cache(maxsize=10)
|
||||||
def __inner_ensure_symmap(self, path: str, mod_ts: int) -> dict[int, dict[int, str]]:
|
def __inner_ensure_symmap(self, path: str, mod_ts: int) -> dict[int, dict[int, int]]:
|
||||||
_ = mod_ts # For LRU
|
_ = mod_ts # For LRU
|
||||||
return build_symmap(path)
|
return build_symmap(path)
|
||||||
|
|
||||||
@ -148,9 +170,12 @@ class HidApi:
|
|||||||
@exposed_ws(1)
|
@exposed_ws(1)
|
||||||
async def __ws_bin_key_handler(self, _: WsSession, data: bytes) -> None:
|
async def __ws_bin_key_handler(self, _: WsSession, data: bytes) -> None:
|
||||||
try:
|
try:
|
||||||
key = valid_hid_key(data[1:].decode("ascii"))
|
|
||||||
state = bool(data[0] & 0b01)
|
state = bool(data[0] & 0b01)
|
||||||
finish = bool(data[0] & 0b10)
|
finish = bool(data[0] & 0b10)
|
||||||
|
if data[0] & 0b10000000:
|
||||||
|
key = struct.unpack(">H", data[1:])[0]
|
||||||
|
else:
|
||||||
|
key = WEB_TO_EVDEV[valid_hid_key(data[1:33].decode("ascii"))]
|
||||||
except Exception:
|
except Exception:
|
||||||
return
|
return
|
||||||
self.__hid.send_key_event(key, state, finish)
|
self.__hid.send_key_event(key, state, finish)
|
||||||
@ -158,7 +183,11 @@ class HidApi:
|
|||||||
@exposed_ws(2)
|
@exposed_ws(2)
|
||||||
async def __ws_bin_mouse_button_handler(self, _: WsSession, data: bytes) -> None:
|
async def __ws_bin_mouse_button_handler(self, _: WsSession, data: bytes) -> None:
|
||||||
try:
|
try:
|
||||||
button = valid_hid_mouse_button(data[1:].decode("ascii"))
|
state = bool(data[0] & 0b01)
|
||||||
|
if data[0] & 0b10000000:
|
||||||
|
button = struct.unpack(">H", data[1:])[0]
|
||||||
|
else:
|
||||||
|
button = MOUSE_TO_EVDEV[valid_hid_mouse_button(data[1:33].decode("ascii"))]
|
||||||
state = bool(data[0] & 0b01)
|
state = bool(data[0] & 0b01)
|
||||||
except Exception:
|
except Exception:
|
||||||
return
|
return
|
||||||
@ -199,7 +228,7 @@ class HidApi:
|
|||||||
@exposed_ws("key")
|
@exposed_ws("key")
|
||||||
async def __ws_key_handler(self, _: WsSession, event: dict) -> None:
|
async def __ws_key_handler(self, _: WsSession, event: dict) -> None:
|
||||||
try:
|
try:
|
||||||
key = valid_hid_key(event["key"])
|
key = WEB_TO_EVDEV[valid_hid_key(event["key"])]
|
||||||
state = valid_bool(event["state"])
|
state = valid_bool(event["state"])
|
||||||
finish = valid_bool(event.get("finish", False))
|
finish = valid_bool(event.get("finish", False))
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -209,7 +238,7 @@ class HidApi:
|
|||||||
@exposed_ws("mouse_button")
|
@exposed_ws("mouse_button")
|
||||||
async def __ws_mouse_button_handler(self, _: WsSession, event: dict) -> None:
|
async def __ws_mouse_button_handler(self, _: WsSession, event: dict) -> None:
|
||||||
try:
|
try:
|
||||||
button = valid_hid_mouse_button(event["button"])
|
button = MOUSE_TO_EVDEV[valid_hid_mouse_button(event["button"])]
|
||||||
state = valid_bool(event["state"])
|
state = valid_bool(event["state"])
|
||||||
except Exception:
|
except Exception:
|
||||||
return
|
return
|
||||||
@ -246,9 +275,22 @@ class HidApi:
|
|||||||
|
|
||||||
# =====
|
# =====
|
||||||
|
|
||||||
|
@exposed_http("POST", "/hid/events/send_shortcut")
|
||||||
|
async def __events_send_shortcut_handler(self, req: Request) -> Response:
|
||||||
|
shortcut = valid_string_list(req.query.get("keys"), subval=valid_hid_key)
|
||||||
|
if shortcut:
|
||||||
|
press = [WEB_TO_EVDEV[key] for key in shortcut]
|
||||||
|
release = list(reversed(press))
|
||||||
|
seq = [
|
||||||
|
*zip(press, itertools.repeat(True)),
|
||||||
|
*zip(release, itertools.repeat(False)),
|
||||||
|
]
|
||||||
|
await self.__hid.send_key_events(seq, no_ignore_keys=True, delay=0.05)
|
||||||
|
return make_json_response()
|
||||||
|
|
||||||
@exposed_http("POST", "/hid/events/send_key")
|
@exposed_http("POST", "/hid/events/send_key")
|
||||||
async def __events_send_key_handler(self, req: Request) -> Response:
|
async def __events_send_key_handler(self, req: Request) -> Response:
|
||||||
key = valid_hid_key(req.query.get("key"))
|
key = WEB_TO_EVDEV[valid_hid_key(req.query.get("key"))]
|
||||||
if "state" in req.query:
|
if "state" in req.query:
|
||||||
state = valid_bool(req.query["state"])
|
state = valid_bool(req.query["state"])
|
||||||
finish = valid_bool(req.query.get("finish", False))
|
finish = valid_bool(req.query.get("finish", False))
|
||||||
@ -259,7 +301,7 @@ class HidApi:
|
|||||||
|
|
||||||
@exposed_http("POST", "/hid/events/send_mouse_button")
|
@exposed_http("POST", "/hid/events/send_mouse_button")
|
||||||
async def __events_send_mouse_button_handler(self, req: Request) -> Response:
|
async def __events_send_mouse_button_handler(self, req: Request) -> Response:
|
||||||
button = valid_hid_mouse_button(req.query.get("button"))
|
button = MOUSE_TO_EVDEV[valid_hid_mouse_button(req.query.get("button"))]
|
||||||
if "state" in req.query:
|
if "state" in req.query:
|
||||||
state = valid_bool(req.query["state"])
|
state = valid_bool(req.query["state"])
|
||||||
self.__hid.send_mouse_button_event(button, state)
|
self.__hid.send_mouse_button_event(button, state)
|
||||||
|
|||||||
@ -45,7 +45,10 @@ class InfoApi:
|
|||||||
|
|
||||||
def __valid_info_fields(self, req: Request) -> list[str]:
|
def __valid_info_fields(self, req: Request) -> list[str]:
|
||||||
available = self.__info_manager.get_subs()
|
available = self.__info_manager.get_subs()
|
||||||
|
available.add("hw")
|
||||||
|
default = set(available)
|
||||||
|
default.remove("health")
|
||||||
return sorted(valid_info_fields(
|
return sorted(valid_info_fields(
|
||||||
arg=req.query.get("fields", ",".join(available)),
|
arg=req.query.get("fields", ",".join(default)),
|
||||||
variants=available,
|
variants=(available),
|
||||||
) or available)
|
) or available)
|
||||||
|
|||||||
@ -52,17 +52,15 @@ class LogApi:
|
|||||||
raise LogReaderDisabledError()
|
raise LogReaderDisabledError()
|
||||||
seek = valid_log_seek(req.query.get("seek", 0))
|
seek = valid_log_seek(req.query.get("seek", 0))
|
||||||
follow = valid_bool(req.query.get("follow", False))
|
follow = valid_bool(req.query.get("follow", False))
|
||||||
response = await start_streaming(req, "text/plain")
|
resp = await start_streaming(req, "text/plain")
|
||||||
try:
|
try:
|
||||||
async for record in self.__log_reader.poll_log(seek, follow):
|
async for record in self.__log_reader.poll_log(seek, follow):
|
||||||
await response.write(("[%s %s] --- %s" % (
|
await resp.write(("[%s %s] --- %s" % (
|
||||||
record["dt"].strftime("%Y-%m-%d %H:%M:%S"),
|
record["dt"].strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
record["service"],
|
record["service"],
|
||||||
record["msg"],
|
record["msg"],
|
||||||
)).encode("utf-8") + b"\r\n")
|
)).encode("utf-8") + b"\r\n")
|
||||||
except Exception as e:
|
except Exception as exception:
|
||||||
if record is None:
|
await resp.write(f"Module systemd.journal is unavailable.\n{exception}".encode("utf-8"))
|
||||||
record = e
|
return resp
|
||||||
await response.write(f"Module systemd.journal is unavailable.\n{record}".encode("utf-8"))
|
return resp
|
||||||
return response
|
|
||||||
return response
|
|
||||||
|
|||||||
@ -84,7 +84,7 @@ class MsdApi:
|
|||||||
async def __set_connected_handler(self, req: Request) -> Response:
|
async def __set_connected_handler(self, req: Request) -> Response:
|
||||||
await self.__msd.set_connected(valid_bool(req.query.get("connected")))
|
await self.__msd.set_connected(valid_bool(req.query.get("connected")))
|
||||||
return make_json_response()
|
return make_json_response()
|
||||||
|
|
||||||
@exposed_http("POST", "/msd/make_image")
|
@exposed_http("POST", "/msd/make_image")
|
||||||
async def __set_zipped_handler(self, req: Request) -> Response:
|
async def __set_zipped_handler(self, req: Request) -> Response:
|
||||||
await self.__msd.make_image(valid_bool(req.query.get("zipped")))
|
await self.__msd.make_image(valid_bool(req.query.get("zipped")))
|
||||||
@ -133,10 +133,10 @@ class MsdApi:
|
|||||||
src = compressed()
|
src = compressed()
|
||||||
size = -1
|
size = -1
|
||||||
|
|
||||||
response = await start_streaming(req, "application/octet-stream", size, name + suffix)
|
resp = await start_streaming(req, "application/octet-stream", size, name + suffix)
|
||||||
async for chunk in src:
|
async for chunk in src:
|
||||||
await response.write(chunk)
|
await resp.write(chunk)
|
||||||
return response
|
return resp
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
|
|
||||||
@ -166,11 +166,11 @@ class MsdApi:
|
|||||||
|
|
||||||
name = ""
|
name = ""
|
||||||
size = written = 0
|
size = written = 0
|
||||||
response: (StreamResponse | None) = None
|
resp: (StreamResponse | None) = None
|
||||||
|
|
||||||
async def stream_write_info() -> None:
|
async def stream_write_info() -> None:
|
||||||
assert response is not None
|
assert resp is not None
|
||||||
await stream_json(response, self.__make_write_info(name, size, written))
|
await stream_json(resp, self.__make_write_info(name, size, written))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with htclient.download(
|
async with htclient.download(
|
||||||
@ -190,7 +190,7 @@ class MsdApi:
|
|||||||
get_logger(0).info("Downloading image %r as %r to MSD ...", url, name)
|
get_logger(0).info("Downloading image %r as %r to MSD ...", url, name)
|
||||||
async with self.__msd.write_image(name, size, remove_incomplete) as writer:
|
async with self.__msd.write_image(name, size, remove_incomplete) as writer:
|
||||||
chunk_size = writer.get_chunk_size()
|
chunk_size = writer.get_chunk_size()
|
||||||
response = await start_streaming(req, "application/x-ndjson")
|
resp = await start_streaming(req, "application/x-ndjson")
|
||||||
await stream_write_info()
|
await stream_write_info()
|
||||||
last_report_ts = 0
|
last_report_ts = 0
|
||||||
async for chunk in remote.content.iter_chunked(chunk_size):
|
async for chunk in remote.content.iter_chunked(chunk_size):
|
||||||
@ -201,12 +201,12 @@ class MsdApi:
|
|||||||
last_report_ts = now
|
last_report_ts = now
|
||||||
|
|
||||||
await stream_write_info()
|
await stream_write_info()
|
||||||
return response
|
return resp
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
if response is not None:
|
if resp is not None:
|
||||||
await stream_write_info()
|
await stream_write_info()
|
||||||
await stream_json_exception(response, ex)
|
await stream_json_exception(resp, ex)
|
||||||
elif isinstance(ex, aiohttp.ClientError):
|
elif isinstance(ex, aiohttp.ClientError):
|
||||||
return make_json_exception(ex, 400)
|
return make_json_exception(ex, 400)
|
||||||
raise
|
raise
|
||||||
|
|||||||
@ -102,14 +102,26 @@ class RedfishApi:
|
|||||||
"Actions": {
|
"Actions": {
|
||||||
"#ComputerSystem.Reset": {
|
"#ComputerSystem.Reset": {
|
||||||
"ResetType@Redfish.AllowableValues": list(self.__actions),
|
"ResetType@Redfish.AllowableValues": list(self.__actions),
|
||||||
"target": "/redfish/v1/Systems/0/Actions/ComputerSystem.Reset"
|
"target": "/redfish/v1/Systems/0/Actions/ComputerSystem.Reset",
|
||||||
|
},
|
||||||
|
"#ComputerSystem.SetDefaultBootOrder": { # https://github.com/pikvm/pikvm/issues/1525
|
||||||
|
"target": "/redfish/v1/Systems/0/Actions/ComputerSystem.SetDefaultBootOrder",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"Id": "0",
|
"Id": "0",
|
||||||
"HostName": host,
|
"HostName": host,
|
||||||
"PowerState": ("On" if atx_state["leds"]["power"] else "Off"), # type: ignore
|
"PowerState": ("On" if atx_state["leds"]["power"] else "Off"), # type: ignore
|
||||||
|
"Boot": {
|
||||||
|
"BootSourceOverrideEnabled": "Disabled",
|
||||||
|
"BootSourceOverrideTarget": None,
|
||||||
|
},
|
||||||
}, wrap_result=False)
|
}, wrap_result=False)
|
||||||
|
|
||||||
|
@exposed_http("PATCH", "/redfish/v1/Systems/0")
|
||||||
|
async def __patch_handler(self, _: Request) -> Response:
|
||||||
|
# https://github.com/pikvm/pikvm/issues/1525
|
||||||
|
return Response(body=None, status=204)
|
||||||
|
|
||||||
@exposed_http("POST", "/redfish/v1/Systems/0/Actions/ComputerSystem.Reset")
|
@exposed_http("POST", "/redfish/v1/Systems/0/Actions/ComputerSystem.Reset")
|
||||||
async def __power_handler(self, req: Request) -> Response:
|
async def __power_handler(self, req: Request) -> Response:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -28,6 +28,7 @@ from ....htserver import make_json_response
|
|||||||
|
|
||||||
from ....validators.basic import valid_bool
|
from ....validators.basic import valid_bool
|
||||||
from ....validators.basic import valid_int_f0
|
from ....validators.basic import valid_int_f0
|
||||||
|
from ....validators.basic import valid_float_f0
|
||||||
from ....validators.basic import valid_stripped_string_not_empty
|
from ....validators.basic import valid_stripped_string_not_empty
|
||||||
from ....validators.kvm import valid_atx_power_action
|
from ....validators.kvm import valid_atx_power_action
|
||||||
from ....validators.kvm import valid_atx_button
|
from ....validators.kvm import valid_atx_button
|
||||||
@ -52,9 +53,19 @@ class SwitchApi:
|
|||||||
async def __state_handler(self, _: Request) -> Response:
|
async def __state_handler(self, _: Request) -> Response:
|
||||||
return make_json_response(await self.__switch.get_state())
|
return make_json_response(await self.__switch.get_state())
|
||||||
|
|
||||||
|
@exposed_http("POST", "/switch/set_active_prev")
|
||||||
|
async def __set_active_prev_handler(self, _: Request) -> Response:
|
||||||
|
await self.__switch.set_active_prev()
|
||||||
|
return make_json_response()
|
||||||
|
|
||||||
|
@exposed_http("POST", "/switch/set_active_next")
|
||||||
|
async def __set_active_next_handler(self, _: Request) -> Response:
|
||||||
|
await self.__switch.set_active_next()
|
||||||
|
return make_json_response()
|
||||||
|
|
||||||
@exposed_http("POST", "/switch/set_active")
|
@exposed_http("POST", "/switch/set_active")
|
||||||
async def __set_active_port_handler(self, req: Request) -> Response:
|
async def __set_active_port_handler(self, req: Request) -> Response:
|
||||||
port = valid_int_f0(req.query.get("port"))
|
port = valid_float_f0(req.query.get("port"))
|
||||||
await self.__switch.set_active_port(port)
|
await self.__switch.set_active_port(port)
|
||||||
return make_json_response()
|
return make_json_response()
|
||||||
|
|
||||||
@ -62,7 +73,7 @@ class SwitchApi:
|
|||||||
async def __set_beacon_handler(self, req: Request) -> Response:
|
async def __set_beacon_handler(self, req: Request) -> Response:
|
||||||
on = valid_bool(req.query.get("state"))
|
on = valid_bool(req.query.get("state"))
|
||||||
if "port" in req.query:
|
if "port" in req.query:
|
||||||
port = valid_int_f0(req.query.get("port"))
|
port = valid_float_f0(req.query.get("port"))
|
||||||
await self.__switch.set_port_beacon(port, on)
|
await self.__switch.set_port_beacon(port, on)
|
||||||
elif "uplink" in req.query:
|
elif "uplink" in req.query:
|
||||||
unit = valid_int_f0(req.query.get("uplink"))
|
unit = valid_int_f0(req.query.get("uplink"))
|
||||||
@ -74,11 +85,12 @@ class SwitchApi:
|
|||||||
|
|
||||||
@exposed_http("POST", "/switch/set_port_params")
|
@exposed_http("POST", "/switch/set_port_params")
|
||||||
async def __set_port_params(self, req: Request) -> Response:
|
async def __set_port_params(self, req: Request) -> Response:
|
||||||
port = valid_int_f0(req.query.get("port"))
|
port = valid_float_f0(req.query.get("port"))
|
||||||
params = {
|
params = {
|
||||||
param: validator(req.query.get(param))
|
param: validator(req.query.get(param))
|
||||||
for (param, validator) in [
|
for (param, validator) in [
|
||||||
("edid_id", (lambda arg: valid_switch_edid_id(arg, allow_default=True))),
|
("edid_id", (lambda arg: valid_switch_edid_id(arg, allow_default=True))),
|
||||||
|
("dummy", valid_bool),
|
||||||
("name", valid_switch_port_name),
|
("name", valid_switch_port_name),
|
||||||
("atx_click_power_delay", valid_switch_atx_click_delay),
|
("atx_click_power_delay", valid_switch_atx_click_delay),
|
||||||
("atx_click_power_long_delay", valid_switch_atx_click_delay),
|
("atx_click_power_long_delay", valid_switch_atx_click_delay),
|
||||||
@ -142,7 +154,7 @@ class SwitchApi:
|
|||||||
|
|
||||||
@exposed_http("POST", "/switch/atx/power")
|
@exposed_http("POST", "/switch/atx/power")
|
||||||
async def __power_handler(self, req: Request) -> Response:
|
async def __power_handler(self, req: Request) -> Response:
|
||||||
port = valid_int_f0(req.query.get("port"))
|
port = valid_float_f0(req.query.get("port"))
|
||||||
action = valid_atx_power_action(req.query.get("action"))
|
action = valid_atx_power_action(req.query.get("action"))
|
||||||
await ({
|
await ({
|
||||||
"on": self.__switch.atx_power_on,
|
"on": self.__switch.atx_power_on,
|
||||||
@ -154,7 +166,7 @@ class SwitchApi:
|
|||||||
|
|
||||||
@exposed_http("POST", "/switch/atx/click")
|
@exposed_http("POST", "/switch/atx/click")
|
||||||
async def __click_handler(self, req: Request) -> Response:
|
async def __click_handler(self, req: Request) -> Response:
|
||||||
port = valid_int_f0(req.query.get("port"))
|
port = valid_float_f0(req.query.get("port"))
|
||||||
button = valid_atx_button(req.query.get("button"))
|
button = valid_atx_button(req.query.get("button"))
|
||||||
await ({
|
await ({
|
||||||
"power": self.__switch.atx_click_power,
|
"power": self.__switch.atx_click_power,
|
||||||
|
|||||||
@ -20,6 +20,12 @@
|
|||||||
# ========================================================================== #
|
# ========================================================================== #
|
||||||
|
|
||||||
|
|
||||||
|
import pwd
|
||||||
|
import grp
|
||||||
|
import dataclasses
|
||||||
|
import time
|
||||||
|
import datetime
|
||||||
|
|
||||||
import secrets
|
import secrets
|
||||||
import pyotp
|
import pyotp
|
||||||
|
|
||||||
@ -31,48 +37,79 @@ from ...plugins.auth import BaseAuthService
|
|||||||
from ...plugins.auth import get_auth_service_class
|
from ...plugins.auth import get_auth_service_class
|
||||||
|
|
||||||
from ...htserver import HttpExposed
|
from ...htserver import HttpExposed
|
||||||
|
from ...htserver import RequestUnixCredentials
|
||||||
|
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
class AuthManager:
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class _Session:
|
||||||
|
user: str
|
||||||
|
expire_ts: int
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
assert self.user == self.user.strip()
|
||||||
|
assert self.user
|
||||||
|
assert self.expire_ts >= 0
|
||||||
|
|
||||||
|
|
||||||
|
class AuthManager: # pylint: disable=too-many-arguments,too-many-instance-attributes
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
|
expire: int,
|
||||||
|
usc_users: list[str],
|
||||||
|
usc_groups: list[str],
|
||||||
unauth_paths: list[str],
|
unauth_paths: list[str],
|
||||||
|
|
||||||
internal_type: str,
|
int_type: str,
|
||||||
internal_kwargs: dict,
|
int_kwargs: dict,
|
||||||
force_internal_users: list[str],
|
force_int_users: list[str],
|
||||||
|
|
||||||
external_type: str,
|
ext_type: str,
|
||||||
external_kwargs: dict,
|
ext_kwargs: dict,
|
||||||
|
|
||||||
totp_secret_path: str,
|
totp_secret_path: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
|
logger = get_logger(0)
|
||||||
|
|
||||||
self.__enabled = enabled
|
self.__enabled = enabled
|
||||||
if not enabled:
|
if not enabled:
|
||||||
get_logger().warning("AUTHORIZATION IS DISABLED")
|
logger.warning("AUTHORIZATION IS DISABLED")
|
||||||
|
|
||||||
|
assert expire >= 0
|
||||||
|
self.__expire = expire
|
||||||
|
if expire > 0:
|
||||||
|
logger.info("Maximum user session time is limited: %s",
|
||||||
|
self.__format_seconds(expire))
|
||||||
|
|
||||||
|
self.__usc_uids = self.__load_usc_uids(usc_users, usc_groups)
|
||||||
|
if self.__usc_uids:
|
||||||
|
logger.info("Selfauth UNIX socket access is allowed for users: %s",
|
||||||
|
list(self.__usc_uids.values()))
|
||||||
|
|
||||||
self.__unauth_paths = frozenset(unauth_paths) # To speed up
|
self.__unauth_paths = frozenset(unauth_paths) # To speed up
|
||||||
for path in self.__unauth_paths:
|
if self.__unauth_paths:
|
||||||
get_logger().warning("Authorization is disabled for API %r", path)
|
logger.info("Authorization is disabled for APIs: %s",
|
||||||
|
list(self.__unauth_paths))
|
||||||
|
|
||||||
self.__internal_service: (BaseAuthService | None) = None
|
self.__int_service: (BaseAuthService | None) = None
|
||||||
if enabled:
|
if enabled:
|
||||||
self.__internal_service = get_auth_service_class(internal_type)(**internal_kwargs)
|
self.__int_service = get_auth_service_class(int_type)(**int_kwargs)
|
||||||
get_logger().info("Using internal auth service %r", self.__internal_service.get_plugin_name())
|
logger.info("Using internal auth service %r",
|
||||||
|
self.__int_service.get_plugin_name())
|
||||||
|
|
||||||
self.__force_internal_users = force_internal_users
|
self.__force_int_users = force_int_users
|
||||||
|
|
||||||
self.__external_service: (BaseAuthService | None) = None
|
self.__ext_service: (BaseAuthService | None) = None
|
||||||
if enabled and external_type:
|
if enabled and ext_type:
|
||||||
self.__external_service = get_auth_service_class(external_type)(**external_kwargs)
|
self.__ext_service = get_auth_service_class(ext_type)(**ext_kwargs)
|
||||||
get_logger().info("Using external auth service %r", self.__external_service.get_plugin_name())
|
logger.info("Using external auth service %r",
|
||||||
|
self.__ext_service.get_plugin_name())
|
||||||
|
|
||||||
self.__totp_secret_path = totp_secret_path
|
self.__totp_secret_path = totp_secret_path
|
||||||
|
|
||||||
self.__tokens: dict[str, str] = {} # {token: user}
|
self.__sessions: dict[str, _Session] = {} # {token: session}
|
||||||
|
|
||||||
def is_auth_enabled(self) -> bool:
|
def is_auth_enabled(self) -> bool:
|
||||||
return self.__enabled
|
return self.__enabled
|
||||||
@ -88,7 +125,8 @@ class AuthManager:
|
|||||||
assert user == user.strip()
|
assert user == user.strip()
|
||||||
assert user
|
assert user
|
||||||
assert self.__enabled
|
assert self.__enabled
|
||||||
assert self.__internal_service
|
assert self.__int_service
|
||||||
|
logger = get_logger(0)
|
||||||
|
|
||||||
if self.__totp_secret_path:
|
if self.__totp_secret_path:
|
||||||
with open(self.__totp_secret_path) as file:
|
with open(self.__totp_secret_path) as file:
|
||||||
@ -96,60 +134,150 @@ class AuthManager:
|
|||||||
if secret:
|
if secret:
|
||||||
code = passwd[-6:]
|
code = passwd[-6:]
|
||||||
if not pyotp.TOTP(secret).verify(code, valid_window=1):
|
if not pyotp.TOTP(secret).verify(code, valid_window=1):
|
||||||
get_logger().error("Got access denied for user %r by TOTP", user)
|
logger.error("Got access denied for user %r by TOTP", user)
|
||||||
return False
|
return False
|
||||||
passwd = passwd[:-6]
|
passwd = passwd[:-6]
|
||||||
|
|
||||||
if user not in self.__force_internal_users and self.__external_service:
|
if user not in self.__force_int_users and self.__ext_service:
|
||||||
service = self.__external_service
|
service = self.__ext_service
|
||||||
else:
|
else:
|
||||||
service = self.__internal_service
|
service = self.__int_service
|
||||||
|
|
||||||
|
pname = service.get_plugin_name()
|
||||||
ok = (await service.authorize(user, passwd))
|
ok = (await service.authorize(user, passwd))
|
||||||
if ok:
|
if ok:
|
||||||
get_logger().info("Authorized user %r via auth service %r", user, service.get_plugin_name())
|
logger.info("Authorized user %r via auth service %r", user, pname)
|
||||||
else:
|
else:
|
||||||
get_logger().error("Got access denied for user %r from auth service %r", user, service.get_plugin_name())
|
logger.error("Got access denied for user %r from auth service %r", user, pname)
|
||||||
return ok
|
return ok
|
||||||
|
|
||||||
async def login(self, user: str, passwd: str) -> (str | None):
|
async def login(self, user: str, passwd: str, expire: int) -> (str | None):
|
||||||
assert user == user.strip()
|
assert user == user.strip()
|
||||||
assert user
|
assert user
|
||||||
|
assert expire >= 0
|
||||||
assert self.__enabled
|
assert self.__enabled
|
||||||
|
|
||||||
if (await self.authorize(user, passwd)):
|
if (await self.authorize(user, passwd)):
|
||||||
token = self.__make_new_token()
|
token = self.__make_new_token()
|
||||||
self.__tokens[token] = user
|
session = _Session(
|
||||||
get_logger().info("Logged in user %r", user)
|
user=user,
|
||||||
|
expire_ts=self.__make_expire_ts(expire),
|
||||||
|
)
|
||||||
|
self.__sessions[token] = session
|
||||||
|
get_logger(0).info("Logged in user %r; expire=%s, sessions_now=%d",
|
||||||
|
session.user,
|
||||||
|
self.__format_expire_ts(session.expire_ts),
|
||||||
|
self.__get_sessions_number(session.user))
|
||||||
return token
|
return token
|
||||||
else:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def __make_new_token(self) -> str:
|
def __make_new_token(self) -> str:
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
token = secrets.token_hex(32)
|
token = secrets.token_hex(32)
|
||||||
if token not in self.__tokens:
|
if token not in self.__sessions:
|
||||||
return token
|
return token
|
||||||
raise AssertionError("Can't generate new unique token")
|
raise RuntimeError("Can't generate new unique token")
|
||||||
|
|
||||||
|
def __make_expire_ts(self, expire: int) -> int:
|
||||||
|
assert expire >= 0
|
||||||
|
assert self.__expire >= 0
|
||||||
|
|
||||||
|
if expire == 0:
|
||||||
|
# The user requested infinite session: apply global expire.
|
||||||
|
# It will allow this (0) or set a limit.
|
||||||
|
expire = self.__expire
|
||||||
|
else:
|
||||||
|
# The user wants a limited session
|
||||||
|
if self.__expire > 0:
|
||||||
|
# If we have a global limit, override the user limit
|
||||||
|
assert expire > 0
|
||||||
|
expire = min(expire, self.__expire)
|
||||||
|
|
||||||
|
if expire > 0:
|
||||||
|
return (self.__get_now_ts() + expire)
|
||||||
|
|
||||||
|
assert expire == 0
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def __get_now_ts(self) -> int:
|
||||||
|
return int(time.monotonic())
|
||||||
|
|
||||||
|
def __format_expire_ts(self, expire_ts: int) -> str:
|
||||||
|
if expire_ts > 0:
|
||||||
|
seconds = expire_ts - self.__get_now_ts()
|
||||||
|
return f"[{self.__format_seconds(seconds)}]"
|
||||||
|
return "INF"
|
||||||
|
|
||||||
|
def __format_seconds(self, seconds: int) -> str:
|
||||||
|
return str(datetime.timedelta(seconds=seconds))
|
||||||
|
|
||||||
|
def __get_sessions_number(self, user: str) -> int:
|
||||||
|
return sum(
|
||||||
|
1
|
||||||
|
for session in self.__sessions.values()
|
||||||
|
if session.user == user
|
||||||
|
)
|
||||||
|
|
||||||
def logout(self, token: str) -> None:
|
def logout(self, token: str) -> None:
|
||||||
assert self.__enabled
|
assert self.__enabled
|
||||||
if token in self.__tokens:
|
if token in self.__sessions:
|
||||||
user = self.__tokens[token]
|
user = self.__sessions[token].user
|
||||||
count = 0
|
count = 0
|
||||||
for (r_token, r_user) in list(self.__tokens.items()):
|
for (key_t, session) in list(self.__sessions.items()):
|
||||||
if r_user == user:
|
if session.user == user:
|
||||||
count += 1
|
count += 1
|
||||||
del self.__tokens[r_token]
|
del self.__sessions[key_t]
|
||||||
get_logger().info("Logged out user %r (%d)", user, count)
|
get_logger(0).info("Logged out user %r; sessions_closed=%d", user, count)
|
||||||
|
|
||||||
def check(self, token: str) -> (str | None):
|
def check(self, token: str) -> (str | None):
|
||||||
assert self.__enabled
|
assert self.__enabled
|
||||||
return self.__tokens.get(token)
|
session = self.__sessions.get(token)
|
||||||
|
if session is not None:
|
||||||
|
if session.expire_ts <= 0:
|
||||||
|
# Infinite session
|
||||||
|
return session.user
|
||||||
|
else:
|
||||||
|
# Limited session
|
||||||
|
if self.__get_now_ts() < session.expire_ts:
|
||||||
|
return session.user
|
||||||
|
else:
|
||||||
|
del self.__sessions[token]
|
||||||
|
get_logger(0).info("The session of user %r is expired; sessions_left=%d",
|
||||||
|
session.user,
|
||||||
|
self.__get_sessions_number(session.user))
|
||||||
|
return None
|
||||||
|
|
||||||
@aiotools.atomic_fg
|
@aiotools.atomic_fg
|
||||||
async def cleanup(self) -> None:
|
async def cleanup(self) -> None:
|
||||||
if self.__enabled:
|
if self.__enabled:
|
||||||
assert self.__internal_service
|
assert self.__int_service
|
||||||
await self.__internal_service.cleanup()
|
await self.__int_service.cleanup()
|
||||||
if self.__external_service:
|
if self.__ext_service:
|
||||||
await self.__external_service.cleanup()
|
await self.__ext_service.cleanup()
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
|
def __load_usc_uids(self, users: list[str], groups: list[str]) -> dict[int, str]:
|
||||||
|
uids: dict[int, str] = {}
|
||||||
|
|
||||||
|
pwds: dict[str, int] = {}
|
||||||
|
for pw in pwd.getpwall():
|
||||||
|
assert pw.pw_name == pw.pw_name.strip()
|
||||||
|
assert pw.pw_name
|
||||||
|
pwds[pw.pw_name] = pw.pw_uid
|
||||||
|
if pw.pw_name in users:
|
||||||
|
uids[pw.pw_uid] = pw.pw_name
|
||||||
|
|
||||||
|
for gr in grp.getgrall():
|
||||||
|
if gr.gr_name in groups:
|
||||||
|
for member in gr.gr_mem:
|
||||||
|
if member in pwds:
|
||||||
|
uid = pwds[member]
|
||||||
|
uids[uid] = member
|
||||||
|
|
||||||
|
return uids
|
||||||
|
|
||||||
|
def check_unix_credentials(self, creds: RequestUnixCredentials) -> (str | None):
|
||||||
|
assert self.__enabled
|
||||||
|
return self.__usc_uids.get(creds.uid)
|
||||||
|
|||||||
@ -31,7 +31,7 @@ from .auth import AuthInfoSubmanager
|
|||||||
from .system import SystemInfoSubmanager
|
from .system import SystemInfoSubmanager
|
||||||
from .meta import MetaInfoSubmanager
|
from .meta import MetaInfoSubmanager
|
||||||
from .extras import ExtrasInfoSubmanager
|
from .extras import ExtrasInfoSubmanager
|
||||||
from .hw import HwInfoSubmanager
|
from .health import HealthInfoSubmanager
|
||||||
from .fan import FanInfoSubmanager
|
from .fan import FanInfoSubmanager
|
||||||
|
|
||||||
|
|
||||||
@ -39,11 +39,11 @@ from .fan import FanInfoSubmanager
|
|||||||
class InfoManager:
|
class InfoManager:
|
||||||
def __init__(self, config: Section) -> None:
|
def __init__(self, config: Section) -> None:
|
||||||
self.__subs: dict[str, BaseInfoSubmanager] = {
|
self.__subs: dict[str, BaseInfoSubmanager] = {
|
||||||
"system": SystemInfoSubmanager(config.kvmd.streamer.cmd),
|
"system": SystemInfoSubmanager(config.kvmd.info.hw.platform, config.kvmd.streamer.cmd),
|
||||||
"auth": AuthInfoSubmanager(config.kvmd.auth.enabled),
|
"auth": AuthInfoSubmanager(config.kvmd.auth.enabled),
|
||||||
"meta": MetaInfoSubmanager(config.kvmd.info.meta),
|
"meta": MetaInfoSubmanager(config.kvmd.info.meta),
|
||||||
"extras": ExtrasInfoSubmanager(config),
|
"extras": ExtrasInfoSubmanager(config),
|
||||||
"hw": HwInfoSubmanager(**config.kvmd.info.hw._unpack()),
|
"health": HealthInfoSubmanager(**config.kvmd.info.hw._unpack(ignore="platform")),
|
||||||
"fan": FanInfoSubmanager(**config.kvmd.info.fan._unpack()),
|
"fan": FanInfoSubmanager(**config.kvmd.info.fan._unpack()),
|
||||||
}
|
}
|
||||||
self.__queue: "asyncio.Queue[tuple[str, (dict | None)]]" = asyncio.Queue()
|
self.__queue: "asyncio.Queue[tuple[str, (dict | None)]]" = asyncio.Queue()
|
||||||
@ -52,12 +52,29 @@ class InfoManager:
|
|||||||
return set(self.__subs)
|
return set(self.__subs)
|
||||||
|
|
||||||
async def get_state(self, fields: (list[str] | None)=None) -> dict:
|
async def get_state(self, fields: (list[str] | None)=None) -> dict:
|
||||||
fields = (fields or list(self.__subs))
|
fields_set = set(fields or list(self.__subs))
|
||||||
return dict(zip(fields, await asyncio.gather(*[
|
|
||||||
|
hw = ("hw" in fields_set) # Old for compatible
|
||||||
|
system = ("system" in fields_set)
|
||||||
|
if hw:
|
||||||
|
fields_set.remove("hw")
|
||||||
|
fields_set.add("health")
|
||||||
|
fields_set.add("system")
|
||||||
|
|
||||||
|
state = dict(zip(fields_set, await asyncio.gather(*[
|
||||||
self.__subs[field].get_state()
|
self.__subs[field].get_state()
|
||||||
for field in fields
|
for field in fields_set
|
||||||
])))
|
])))
|
||||||
|
|
||||||
|
if hw:
|
||||||
|
state["hw"] = {
|
||||||
|
"health": state.pop("health"),
|
||||||
|
"platform": (state["system"] or {}).pop("platform"), # {} makes mypy happy
|
||||||
|
}
|
||||||
|
if not system:
|
||||||
|
state.pop("system")
|
||||||
|
return state
|
||||||
|
|
||||||
async def trigger_state(self) -> None:
|
async def trigger_state(self) -> None:
|
||||||
await asyncio.gather(*[
|
await asyncio.gather(*[
|
||||||
sub.trigger_state()
|
sub.trigger_state()
|
||||||
@ -70,7 +87,7 @@ class InfoManager:
|
|||||||
# - auth -- Partial
|
# - auth -- Partial
|
||||||
# - meta -- Partial, nullable
|
# - meta -- Partial, nullable
|
||||||
# - extras -- Partial, nullable
|
# - extras -- Partial, nullable
|
||||||
# - hw -- Partial
|
# - health -- Partial
|
||||||
# - fan -- Partial
|
# - fan -- Partial
|
||||||
# ===========================
|
# ===========================
|
||||||
|
|
||||||
|
|||||||
@ -34,7 +34,6 @@ from ....yamlconf.loader import load_yaml_file
|
|||||||
|
|
||||||
from .... import tools
|
from .... import tools
|
||||||
from .... import aiotools
|
from .... import aiotools
|
||||||
from .... import env
|
|
||||||
|
|
||||||
from .. import sysunit
|
from .. import sysunit
|
||||||
|
|
||||||
|
|||||||
@ -99,9 +99,9 @@ class FanInfoSubmanager(BaseInfoSubmanager):
|
|||||||
async def __get_fan_state(self) -> (dict | None):
|
async def __get_fan_state(self) -> (dict | None):
|
||||||
try:
|
try:
|
||||||
async with self.__make_http_session() as session:
|
async with self.__make_http_session() as session:
|
||||||
async with session.get("http://localhost/state") as response:
|
async with session.get("http://localhost/state") as resp:
|
||||||
htclient.raise_not_200(response)
|
htclient.raise_not_200(resp)
|
||||||
return (await response.json())["result"]
|
return (await resp.json())["result"]
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
get_logger(0).error("Can't read fan state: %s", ex)
|
get_logger(0).error("Can't read fan state: %s", ex)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -20,7 +20,6 @@
|
|||||||
# ========================================================================== #
|
# ========================================================================== #
|
||||||
|
|
||||||
|
|
||||||
import os
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
@ -45,59 +44,41 @@ _RetvalT = TypeVar("_RetvalT")
|
|||||||
|
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
class HwInfoSubmanager(BaseInfoSubmanager):
|
class HealthInfoSubmanager(BaseInfoSubmanager):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
platform_path: str,
|
|
||||||
vcgencmd_cmd: list[str],
|
vcgencmd_cmd: list[str],
|
||||||
ignore_past: bool,
|
ignore_past: bool,
|
||||||
state_poll: float,
|
state_poll: float,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
self.__platform_path = platform_path
|
|
||||||
self.__vcgencmd_cmd = vcgencmd_cmd
|
self.__vcgencmd_cmd = vcgencmd_cmd
|
||||||
self.__ignore_past = ignore_past
|
self.__ignore_past = ignore_past
|
||||||
self.__state_poll = state_poll
|
self.__state_poll = state_poll
|
||||||
|
|
||||||
self.__dt_cache: dict[str, str] = {}
|
|
||||||
|
|
||||||
self.__notifier = aiotools.AioNotifier()
|
self.__notifier = aiotools.AioNotifier()
|
||||||
|
|
||||||
async def get_state(self) -> dict:
|
async def get_state(self) -> dict:
|
||||||
(
|
(
|
||||||
base,
|
|
||||||
serial,
|
|
||||||
platform,
|
|
||||||
throttling,
|
throttling,
|
||||||
cpu_percent,
|
cpu_percent,
|
||||||
cpu_temp,
|
cpu_temp,
|
||||||
mem,
|
mem,
|
||||||
) = await asyncio.gather(
|
) = await asyncio.gather(
|
||||||
self.__read_dt_file("model", upper=False),
|
|
||||||
self.__read_dt_file("serial-number", upper=True),
|
|
||||||
self.__read_platform_file(),
|
|
||||||
self.__get_throttling(),
|
self.__get_throttling(),
|
||||||
self.__get_cpu_percent(),
|
self.__get_cpu_percent(),
|
||||||
self.__get_cpu_temp(),
|
self.__get_cpu_temp(),
|
||||||
self.__get_mem(),
|
self.__get_mem(),
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"platform": {
|
"temp": {
|
||||||
"type": "rpi",
|
"cpu": cpu_temp,
|
||||||
"base": base,
|
|
||||||
"serial": serial,
|
|
||||||
**platform, # type: ignore
|
|
||||||
},
|
},
|
||||||
"health": {
|
"cpu": {
|
||||||
"temp": {
|
"percent": cpu_percent,
|
||||||
"cpu": cpu_temp,
|
|
||||||
},
|
|
||||||
"cpu": {
|
|
||||||
"percent": cpu_percent,
|
|
||||||
},
|
|
||||||
"mem": mem,
|
|
||||||
"throttling": throttling,
|
|
||||||
},
|
},
|
||||||
|
"mem": mem,
|
||||||
|
"throttling": throttling,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def trigger_state(self) -> None:
|
async def trigger_state(self) -> None:
|
||||||
@ -115,42 +96,12 @@ class HwInfoSubmanager(BaseInfoSubmanager):
|
|||||||
|
|
||||||
# =====
|
# =====
|
||||||
|
|
||||||
async def __read_dt_file(self, name: str, upper: bool) -> (str | None):
|
|
||||||
if name not in self.__dt_cache:
|
|
||||||
path = os.path.join(f"{env.PROCFS_PREFIX}/proc/device-tree", name)
|
|
||||||
if not os.path.exists(path):
|
|
||||||
path = os.path.join(f"{env.PROCFS_PREFIX}/etc/kvmd/hw_info/", name)
|
|
||||||
try:
|
|
||||||
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 self.__dt_cache[name]
|
|
||||||
|
|
||||||
async def __read_platform_file(self) -> dict:
|
|
||||||
try:
|
|
||||||
text = await aiotools.read_file(self.__platform_path)
|
|
||||||
parsed: dict[str, str] = {}
|
|
||||||
for row in text.split("\n"):
|
|
||||||
row = row.strip()
|
|
||||||
if row:
|
|
||||||
(key, value) = row.split("=", 1)
|
|
||||||
parsed[key.strip()] = value.strip()
|
|
||||||
return {
|
|
||||||
"model": parsed["PIKVM_MODEL"],
|
|
||||||
"video": parsed["PIKVM_VIDEO"],
|
|
||||||
"board": parsed["PIKVM_BOARD"],
|
|
||||||
}
|
|
||||||
except Exception:
|
|
||||||
get_logger(0).exception("Can't read device model")
|
|
||||||
return {"model": None, "video": None, "board": None}
|
|
||||||
|
|
||||||
async def __get_cpu_temp(self) -> (float | None):
|
async def __get_cpu_temp(self) -> (float | None):
|
||||||
temp_path = f"{env.SYSFS_PREFIX}/sys/class/thermal/thermal_zone0/temp"
|
temp_path = f"{env.SYSFS_PREFIX}/sys/class/thermal/thermal_zone0/temp"
|
||||||
try:
|
try:
|
||||||
return int((await aiotools.read_file(temp_path)).strip()) / 1000
|
return int((await aiotools.read_file(temp_path)).strip()) / 1000
|
||||||
except Exception as err:
|
except Exception:
|
||||||
#get_logger(0).warn("Can't read CPU temp from %s: %s", temp_path, err)
|
# get_logger(0).warn("Can't read CPU temp from %s: %s", temp_path, err)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def __get_cpu_percent(self) -> (float | None):
|
async def __get_cpu_percent(self) -> (float | None):
|
||||||
@ -20,6 +20,8 @@
|
|||||||
# ========================================================================== #
|
# ========================================================================== #
|
||||||
|
|
||||||
|
|
||||||
|
import socket
|
||||||
|
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
from ....logging import get_logger
|
from ....logging import get_logger
|
||||||
@ -39,7 +41,10 @@ class MetaInfoSubmanager(BaseInfoSubmanager):
|
|||||||
|
|
||||||
async def get_state(self) -> (dict | None):
|
async def get_state(self) -> (dict | None):
|
||||||
try:
|
try:
|
||||||
return ((await aiotools.run_async(load_yaml_file, self.__meta_path)) or {})
|
meta = ((await aiotools.run_async(load_yaml_file, self.__meta_path)) or {})
|
||||||
|
if meta["server"]["host"] == "@auto":
|
||||||
|
meta["server"]["host"] = socket.getfqdn()
|
||||||
|
return meta
|
||||||
except Exception:
|
except Exception:
|
||||||
get_logger(0).exception("Can't parse meta")
|
get_logger(0).exception("Can't parse meta")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -28,6 +28,7 @@ from typing import AsyncGenerator
|
|||||||
|
|
||||||
from ....logging import get_logger
|
from ....logging import get_logger
|
||||||
|
|
||||||
|
from .... import env
|
||||||
from .... import aiotools
|
from .... import aiotools
|
||||||
from .... import aioproc
|
from .... import aioproc
|
||||||
|
|
||||||
@ -38,12 +39,30 @@ from .base import BaseInfoSubmanager
|
|||||||
|
|
||||||
# =====
|
# =====
|
||||||
class SystemInfoSubmanager(BaseInfoSubmanager):
|
class SystemInfoSubmanager(BaseInfoSubmanager):
|
||||||
def __init__(self, streamer_cmd: list[str]) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
platform_path: str,
|
||||||
|
streamer_cmd: list[str],
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
self.__platform_path = platform_path
|
||||||
self.__streamer_cmd = streamer_cmd
|
self.__streamer_cmd = streamer_cmd
|
||||||
|
|
||||||
|
self.__dt_cache: dict[str, str] = {}
|
||||||
self.__notifier = aiotools.AioNotifier()
|
self.__notifier = aiotools.AioNotifier()
|
||||||
|
|
||||||
async def get_state(self) -> dict:
|
async def get_state(self) -> dict:
|
||||||
streamer_info = await self.__get_streamer_info()
|
(
|
||||||
|
base,
|
||||||
|
serial,
|
||||||
|
pl,
|
||||||
|
streamer_info,
|
||||||
|
) = await asyncio.gather(
|
||||||
|
self.__read_dt_file("model", upper=False),
|
||||||
|
self.__read_dt_file("serial-number", upper=True),
|
||||||
|
self.__read_platform_file(),
|
||||||
|
self.__get_streamer_info(),
|
||||||
|
)
|
||||||
uname_info = platform.uname() # Uname using the internal cache
|
uname_info = platform.uname() # Uname using the internal cache
|
||||||
return {
|
return {
|
||||||
"kvmd": {"version": __version__},
|
"kvmd": {"version": __version__},
|
||||||
@ -52,6 +71,12 @@ class SystemInfoSubmanager(BaseInfoSubmanager):
|
|||||||
field: getattr(uname_info, field)
|
field: getattr(uname_info, field)
|
||||||
for field in ["system", "release", "version", "machine"]
|
for field in ["system", "release", "version", "machine"]
|
||||||
},
|
},
|
||||||
|
"platform": {
|
||||||
|
"type": "rpi",
|
||||||
|
"base": base,
|
||||||
|
"serial": serial,
|
||||||
|
**pl, # type: ignore
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
async def trigger_state(self) -> None:
|
async def trigger_state(self) -> None:
|
||||||
@ -64,6 +89,35 @@ class SystemInfoSubmanager(BaseInfoSubmanager):
|
|||||||
|
|
||||||
# =====
|
# =====
|
||||||
|
|
||||||
|
async def __read_dt_file(self, name: str, upper: bool) -> (str | None):
|
||||||
|
if name not in self.__dt_cache:
|
||||||
|
path = os.path.join(f"{env.PROCFS_PREFIX}/proc/device-tree", name)
|
||||||
|
try:
|
||||||
|
value = (await aiotools.read_file(path)).strip(" \t\r\n\0")
|
||||||
|
self.__dt_cache[name] = (value.upper() if upper else value)
|
||||||
|
except Exception as ex:
|
||||||
|
get_logger(0).error("Can't read DT %s from %s: %s", name, path, ex)
|
||||||
|
return None
|
||||||
|
return self.__dt_cache[name]
|
||||||
|
|
||||||
|
async def __read_platform_file(self) -> dict:
|
||||||
|
try:
|
||||||
|
text = await aiotools.read_file(self.__platform_path)
|
||||||
|
parsed: dict[str, str] = {}
|
||||||
|
for row in text.split("\n"):
|
||||||
|
row = row.strip()
|
||||||
|
if row:
|
||||||
|
(key, value) = row.split("=", 1)
|
||||||
|
parsed[key.strip()] = value.strip()
|
||||||
|
return {
|
||||||
|
"model": parsed["PIKVM_MODEL"],
|
||||||
|
"video": parsed["PIKVM_VIDEO"],
|
||||||
|
"board": parsed["PIKVM_BOARD"],
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
get_logger(0).exception("Can't read device model")
|
||||||
|
return {"model": None, "video": None, "board": None}
|
||||||
|
|
||||||
async def __get_streamer_info(self) -> dict:
|
async def __get_streamer_info(self) -> dict:
|
||||||
version = ""
|
version = ""
|
||||||
features: dict[str, bool] = {}
|
features: dict[str, bool] = {}
|
||||||
|
|||||||
@ -29,13 +29,11 @@ import time
|
|||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
from xmlrpc.client import ServerProxy
|
from xmlrpc.client import ServerProxy
|
||||||
|
|
||||||
from ...logging import get_logger
|
|
||||||
|
|
||||||
us_systemd_journal = True
|
us_systemd_journal = True
|
||||||
try:
|
try:
|
||||||
import systemd.journal
|
import systemd.journal
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import supervisor.xmlrpc
|
|
||||||
us_systemd_journal = False
|
us_systemd_journal = False
|
||||||
|
|
||||||
|
|
||||||
@ -43,14 +41,14 @@ except ImportError:
|
|||||||
class LogReader:
|
class LogReader:
|
||||||
async def poll_log(self, seek: int, follow: bool) -> AsyncGenerator[dict, None]:
|
async def poll_log(self, seek: int, follow: bool) -> AsyncGenerator[dict, None]:
|
||||||
if us_systemd_journal:
|
if us_systemd_journal:
|
||||||
reader = systemd.journal.Reader() # type: ignore
|
reader = systemd.journal.Reader() # type: ignore
|
||||||
reader.this_boot()
|
reader.this_boot()
|
||||||
# XXX: Из-за смены ID машины в bootconfig это не работает при первой загрузке.
|
# XXX: Из-за смены ID машины в bootconfig это не работает при первой загрузке.
|
||||||
# reader.this_machine()
|
# reader.this_machine()
|
||||||
reader.log_level(systemd.journal.LOG_DEBUG) # type: ignore
|
reader.log_level(systemd.journal.LOG_DEBUG) # type: ignore
|
||||||
services = set(
|
services = set(
|
||||||
service
|
service
|
||||||
for service in systemd.journal.Reader().query_unique("_SYSTEMD_UNIT") # type: ignore
|
for service in systemd.journal.Reader().query_unique("_SYSTEMD_UNIT") # type: ignore
|
||||||
if re.match(r"kvmd(-\w+)*\.service", service)
|
if re.match(r"kvmd(-\w+)*\.service", service)
|
||||||
).union(["kvmd.service"])
|
).union(["kvmd.service"])
|
||||||
|
|
||||||
@ -69,10 +67,15 @@ class LogReader:
|
|||||||
else:
|
else:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
else:
|
else:
|
||||||
server = ServerProxy('http://127.0.0.1',transport=supervisor.xmlrpc.SupervisorTransport(None, None, serverurl='unix:///tmp/supervisor.sock'))
|
import supervisor.xmlrpc # pylint: disable=import-outside-toplevel
|
||||||
log_entries = server.supervisor.readLog(0,0)
|
server_transport = supervisor.xmlrpc.SupervisorTransport(None, None, serverurl="unix:///tmp/supervisor.sock")
|
||||||
yield log_entries
|
server = ServerProxy("http://127.0.0.1", transport=server_transport)
|
||||||
|
log_entries = server.supervisor.readLog(0, 0)
|
||||||
|
yield {
|
||||||
|
"dt": int(time.time()),
|
||||||
|
"service": "kvmd.service",
|
||||||
|
"msg": str(log_entries).rstrip()
|
||||||
|
}
|
||||||
|
|
||||||
def __entry_to_record(self, entry: dict) -> dict[str, dict]:
|
def __entry_to_record(self, entry: dict) -> dict[str, dict]:
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -254,6 +254,10 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
|
|||||||
async def __ws_ping_handler(self, ws: WsSession, _: dict) -> None:
|
async def __ws_ping_handler(self, ws: WsSession, _: dict) -> None:
|
||||||
await ws.send_event("pong", {})
|
await ws.send_event("pong", {})
|
||||||
|
|
||||||
|
@exposed_ws(0)
|
||||||
|
async def __ws_bin_ping_handler(self, ws: WsSession, _: bytes) -> None:
|
||||||
|
await ws.send_bin(255, b"") # Ping-pong
|
||||||
|
|
||||||
# ===== SYSTEM STUFF
|
# ===== SYSTEM STUFF
|
||||||
|
|
||||||
def run(self, **kwargs: Any) -> None: # type: ignore # pylint: disable=arguments-differ
|
def run(self, **kwargs: Any) -> None: # type: ignore # pylint: disable=arguments-differ
|
||||||
@ -318,18 +322,17 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
|
|||||||
while True:
|
while True:
|
||||||
cur = (self.__has_stream_clients() or self.__snapshoter.snapshoting() or self.__stream_forever)
|
cur = (self.__has_stream_clients() or self.__snapshoter.snapshoting() or self.__stream_forever)
|
||||||
if not prev and cur:
|
if not prev and cur:
|
||||||
await self.__streamer.ensure_start(reset=False)
|
await self.__streamer.ensure_start()
|
||||||
elif prev and not cur:
|
elif prev and not cur:
|
||||||
await self.__streamer.ensure_stop(immediately=False)
|
await self.__streamer.ensure_stop()
|
||||||
|
|
||||||
if self.__reset_streamer or self.__new_streamer_params:
|
if self.__new_streamer_params:
|
||||||
start = self.__streamer.is_working()
|
self.__streamer.set_params(self.__new_streamer_params)
|
||||||
await self.__streamer.ensure_stop(immediately=True)
|
self.__new_streamer_params = {}
|
||||||
if self.__new_streamer_params:
|
self.__reset_streamer = True
|
||||||
self.__streamer.set_params(self.__new_streamer_params)
|
|
||||||
self.__new_streamer_params = {}
|
if self.__reset_streamer:
|
||||||
if start:
|
await self.__streamer.ensure_restart()
|
||||||
await self.__streamer.ensure_start(reset=self.__reset_streamer)
|
|
||||||
self.__reset_streamer = False
|
self.__reset_streamer = False
|
||||||
|
|
||||||
prev = cur
|
prev = cur
|
||||||
|
|||||||
@ -31,6 +31,8 @@ from ... import aiotools
|
|||||||
|
|
||||||
from ...plugins.hid import BaseHid
|
from ...plugins.hid import BaseHid
|
||||||
|
|
||||||
|
from ...keyboard.mappings import WEB_TO_EVDEV
|
||||||
|
|
||||||
from .streamer import Streamer
|
from .streamer import Streamer
|
||||||
|
|
||||||
|
|
||||||
@ -63,7 +65,7 @@ class Snapshoter: # pylint: disable=too-many-instance-attributes
|
|||||||
else:
|
else:
|
||||||
self.__idle_interval = self.__live_interval = 0.0
|
self.__idle_interval = self.__live_interval = 0.0
|
||||||
|
|
||||||
self.__wakeup_key = wakeup_key
|
self.__wakeup_key = WEB_TO_EVDEV.get(wakeup_key, 0)
|
||||||
self.__wakeup_move = wakeup_move
|
self.__wakeup_move = wakeup_move
|
||||||
|
|
||||||
self.__online_delay = online_delay
|
self.__online_delay = online_delay
|
||||||
@ -121,8 +123,8 @@ class Snapshoter: # pylint: disable=too-many-instance-attributes
|
|||||||
async def __wakeup(self) -> None:
|
async def __wakeup(self) -> None:
|
||||||
logger = get_logger(0)
|
logger = get_logger(0)
|
||||||
|
|
||||||
if self.__wakeup_key:
|
if self.__wakeup_key > 0:
|
||||||
logger.info("Waking up using key %r ...", self.__wakeup_key)
|
logger.info("Waking up using keyboard ...")
|
||||||
await self.__hid.send_key_events(
|
await self.__hid.send_key_events(
|
||||||
keys=[(self.__wakeup_key, True), (self.__wakeup_key, False)],
|
keys=[(self.__wakeup_key, True), (self.__wakeup_key, False)],
|
||||||
no_ignore_keys=True,
|
no_ignore_keys=True,
|
||||||
|
|||||||
@ -1,456 +0,0 @@
|
|||||||
# ========================================================================== #
|
|
||||||
# #
|
|
||||||
# KVMD - The main PiKVM daemon. #
|
|
||||||
# #
|
|
||||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
|
||||||
# #
|
|
||||||
# This program is free software: you can redistribute it and/or modify #
|
|
||||||
# it under the terms of the GNU General Public License as published by #
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or #
|
|
||||||
# (at your option) any later version. #
|
|
||||||
# #
|
|
||||||
# This program is distributed in the hope that it will be useful, #
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
|
||||||
# GNU General Public License for more details. #
|
|
||||||
# #
|
|
||||||
# You should have received a copy of the GNU General Public License #
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
|
||||||
# #
|
|
||||||
# ========================================================================== #
|
|
||||||
|
|
||||||
|
|
||||||
import signal
|
|
||||||
import asyncio
|
|
||||||
import asyncio.subprocess
|
|
||||||
import dataclasses
|
|
||||||
import copy
|
|
||||||
|
|
||||||
from typing import AsyncGenerator
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
from ...logging import get_logger
|
|
||||||
|
|
||||||
from ...clients.streamer import StreamerSnapshot
|
|
||||||
from ...clients.streamer import HttpStreamerClient
|
|
||||||
from ...clients.streamer import HttpStreamerClientSession
|
|
||||||
|
|
||||||
from ... import tools
|
|
||||||
from ... import aiotools
|
|
||||||
from ... import aioproc
|
|
||||||
from ... import htclient
|
|
||||||
|
|
||||||
|
|
||||||
# =====
|
|
||||||
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 = copy.deepcopy(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
|
|
||||||
__ST_FULL = 0xFF
|
|
||||||
__ST_PARAMS = 0x01
|
|
||||||
__ST_STREAMER = 0x02
|
|
||||||
__ST_SNAPSHOT = 0x04
|
|
||||||
|
|
||||||
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,
|
|
||||||
snapshot_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.__snapshot_timeout = snapshot_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.__client = HttpStreamerClient(
|
|
||||||
name="jpeg",
|
|
||||||
unix_path=self.__unix_path,
|
|
||||||
timeout=timeout,
|
|
||||||
user_agent=htclient.make_user_agent("KVMD"),
|
|
||||||
)
|
|
||||||
self.__client_session: (HttpStreamerClientSession | 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
|
|
||||||
self.__notifier.notify(self.__ST_PARAMS)
|
|
||||||
return self.__params.set_params(params)
|
|
||||||
|
|
||||||
def get_params(self) -> dict:
|
|
||||||
return self.__params.get_params()
|
|
||||||
|
|
||||||
# =====
|
|
||||||
|
|
||||||
async def get_state(self) -> dict:
|
|
||||||
return {
|
|
||||||
"features": self.__params.get_features(),
|
|
||||||
"limits": self.__params.get_limits(),
|
|
||||||
"params": self.__params.get_params(),
|
|
||||||
"streamer": (await self.__get_streamer_state()),
|
|
||||||
"snapshot": self.__get_snapshot_state(),
|
|
||||||
}
|
|
||||||
|
|
||||||
async def trigger_state(self) -> None:
|
|
||||||
self.__notifier.notify(self.__ST_FULL)
|
|
||||||
|
|
||||||
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
|
||||||
# ==== Granularity table ====
|
|
||||||
# - features -- Full
|
|
||||||
# - limits -- Partial, paired with params
|
|
||||||
# - params -- Partial, paired with limits
|
|
||||||
# - streamer -- Partial, nullable
|
|
||||||
# - snapshot -- Partial
|
|
||||||
# ===========================
|
|
||||||
|
|
||||||
def signal_handler(*_: Any) -> None:
|
|
||||||
get_logger(0).info("Got SIGUSR2, checking the stream state ...")
|
|
||||||
self.__notifier.notify(self.__ST_STREAMER)
|
|
||||||
|
|
||||||
get_logger(0).info("Installing SIGUSR2 streamer handler ...")
|
|
||||||
asyncio.get_event_loop().add_signal_handler(signal.SIGUSR2, signal_handler)
|
|
||||||
|
|
||||||
prev: dict = {}
|
|
||||||
while True:
|
|
||||||
new: dict = {}
|
|
||||||
|
|
||||||
mask = await self.__notifier.wait(timeout=self.__state_poll)
|
|
||||||
if mask == self.__ST_FULL:
|
|
||||||
new = await self.get_state()
|
|
||||||
prev = copy.deepcopy(new)
|
|
||||||
yield new
|
|
||||||
continue
|
|
||||||
|
|
||||||
if mask < 0:
|
|
||||||
mask = self.__ST_STREAMER
|
|
||||||
|
|
||||||
def check_update(key: str, value: (dict | None)) -> None:
|
|
||||||
if prev.get(key) != value:
|
|
||||||
new[key] = value
|
|
||||||
|
|
||||||
if mask & self.__ST_PARAMS:
|
|
||||||
check_update("params", self.__params.get_params())
|
|
||||||
if mask & self.__ST_STREAMER:
|
|
||||||
check_update("streamer", await self.__get_streamer_state())
|
|
||||||
if mask & self.__ST_SNAPSHOT:
|
|
||||||
check_update("snapshot", self.__get_snapshot_state())
|
|
||||||
|
|
||||||
if new and prev != new:
|
|
||||||
prev.update(copy.deepcopy(new))
|
|
||||||
yield new
|
|
||||||
|
|
||||||
async def __get_streamer_state(self) -> (dict | None):
|
|
||||||
if self.__streamer_task:
|
|
||||||
session = self.__ensure_client_session()
|
|
||||||
try:
|
|
||||||
return (await session.get_state())
|
|
||||||
except (aiohttp.ClientConnectionError, aiohttp.ServerConnectionError):
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
get_logger().exception("Invalid streamer response from /state")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def __get_snapshot_state(self) -> dict:
|
|
||||||
if self.__snapshot:
|
|
||||||
snapshot = dataclasses.asdict(self.__snapshot)
|
|
||||||
del snapshot["headers"]
|
|
||||||
del snapshot["data"]
|
|
||||||
return {"saved": snapshot}
|
|
||||||
return {"saved": None}
|
|
||||||
|
|
||||||
# =====
|
|
||||||
|
|
||||||
async def take_snapshot(self, save: bool, load: bool, allow_offline: bool) -> (StreamerSnapshot | None):
|
|
||||||
if load:
|
|
||||||
return self.__snapshot
|
|
||||||
logger = get_logger()
|
|
||||||
session = self.__ensure_client_session()
|
|
||||||
try:
|
|
||||||
snapshot = await session.take_snapshot(self.__snapshot_timeout)
|
|
||||||
if snapshot.online or allow_offline:
|
|
||||||
if save:
|
|
||||||
self.__snapshot = snapshot
|
|
||||||
self.__notifier.notify(self.__ST_SNAPSHOT)
|
|
||||||
return snapshot
|
|
||||||
logger.error("Stream is offline, no signal or so")
|
|
||||||
except (aiohttp.ClientConnectionError, aiohttp.ServerConnectionError) as ex:
|
|
||||||
logger.error("Can't connect to streamer: %s", tools.efmt(ex))
|
|
||||||
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.__client_session:
|
|
||||||
await self.__client_session.close()
|
|
||||||
self.__client_session = None
|
|
||||||
|
|
||||||
def __ensure_client_session(self) -> HttpStreamerClientSession:
|
|
||||||
if not self.__client_session:
|
|
||||||
self.__client_session = self.__client.make_session()
|
|
||||||
return self.__client_session
|
|
||||||
|
|
||||||
# =====
|
|
||||||
|
|
||||||
@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 ex:
|
|
||||||
logger.exception("Can't execute command: %s", ex)
|
|
||||||
|
|
||||||
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
|
|
||||||
254
kvmd/apps/kvmd/streamer/__init__.py
Normal file
254
kvmd/apps/kvmd/streamer/__init__.py
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
# ========================================================================== #
|
||||||
|
# #
|
||||||
|
# KVMD - The main PiKVM daemon. #
|
||||||
|
# #
|
||||||
|
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This program is free software: you can redistribute it and/or modify #
|
||||||
|
# it under the terms of the GNU General Public License as published by #
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or #
|
||||||
|
# (at your option) any later version. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, #
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||||
|
# GNU General Public License for more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License #
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
|
# #
|
||||||
|
# ========================================================================== #
|
||||||
|
|
||||||
|
|
||||||
|
import signal
|
||||||
|
import asyncio
|
||||||
|
import dataclasses
|
||||||
|
import copy
|
||||||
|
|
||||||
|
from typing import AsyncGenerator
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from ....logging import get_logger
|
||||||
|
|
||||||
|
from ....clients.streamer import StreamerSnapshot
|
||||||
|
from ....clients.streamer import HttpStreamerClient
|
||||||
|
from ....clients.streamer import HttpStreamerClientSession
|
||||||
|
|
||||||
|
from .... import tools
|
||||||
|
from .... import aiotools
|
||||||
|
from .... import htclient
|
||||||
|
|
||||||
|
from .params import Params
|
||||||
|
from .runner import Runner
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
class Streamer: # pylint: disable=too-many-instance-attributes
|
||||||
|
__ST_FULL = 0xFF
|
||||||
|
__ST_PARAMS = 0x01
|
||||||
|
__ST_STREAMER = 0x02
|
||||||
|
__ST_SNAPSHOT = 0x04
|
||||||
|
|
||||||
|
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,
|
||||||
|
snapshot_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.__state_poll = state_poll
|
||||||
|
|
||||||
|
self.__unix_path = unix_path
|
||||||
|
self.__snapshot_timeout = snapshot_timeout
|
||||||
|
self.__process_name_prefix = process_name_prefix
|
||||||
|
|
||||||
|
self.__params = Params(**params_kwargs)
|
||||||
|
|
||||||
|
self.__runner = Runner(
|
||||||
|
reset_delay=reset_delay,
|
||||||
|
shutdown_delay=shutdown_delay,
|
||||||
|
pre_start_cmd=tools.build_cmd(pre_start_cmd, pre_start_cmd_remove, pre_start_cmd_append),
|
||||||
|
cmd=tools.build_cmd(cmd, cmd_remove, cmd_append),
|
||||||
|
post_stop_cmd=tools.build_cmd(post_stop_cmd, post_stop_cmd_remove, post_stop_cmd_append),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.__client = HttpStreamerClient(
|
||||||
|
name="jpeg",
|
||||||
|
unix_path=self.__unix_path,
|
||||||
|
timeout=timeout,
|
||||||
|
user_agent=htclient.make_user_agent("KVMD"),
|
||||||
|
)
|
||||||
|
self.__client_session: (HttpStreamerClientSession | None) = None
|
||||||
|
|
||||||
|
self.__snapshot: (StreamerSnapshot | None) = None
|
||||||
|
|
||||||
|
self.__notifier = aiotools.AioNotifier()
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
|
@aiotools.atomic_fg
|
||||||
|
async def ensure_start(self) -> None:
|
||||||
|
await self.__runner.ensure_start(self.__make_params())
|
||||||
|
|
||||||
|
@aiotools.atomic_fg
|
||||||
|
async def ensure_restart(self) -> None:
|
||||||
|
await self.__runner.ensure_restart(self.__make_params())
|
||||||
|
|
||||||
|
def __make_params(self) -> dict:
|
||||||
|
return {
|
||||||
|
"unix": self.__unix_path,
|
||||||
|
"process_name_prefix": self.__process_name_prefix,
|
||||||
|
**self.__params.get_params(),
|
||||||
|
}
|
||||||
|
|
||||||
|
@aiotools.atomic_fg
|
||||||
|
async def ensure_stop(self) -> None:
|
||||||
|
await self.__runner.ensure_stop(immediately=False)
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
|
def set_params(self, params: dict) -> None:
|
||||||
|
self.__notifier.notify(self.__ST_PARAMS)
|
||||||
|
return self.__params.set_params(params)
|
||||||
|
|
||||||
|
def get_params(self) -> dict:
|
||||||
|
return self.__params.get_params()
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
|
async def get_state(self) -> dict:
|
||||||
|
return {
|
||||||
|
"features": self.__params.get_features(),
|
||||||
|
"limits": self.__params.get_limits(),
|
||||||
|
"params": self.__params.get_params(),
|
||||||
|
"streamer": (await self.__get_streamer_state()),
|
||||||
|
"snapshot": self.__get_snapshot_state(),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def trigger_state(self) -> None:
|
||||||
|
self.__notifier.notify(self.__ST_FULL)
|
||||||
|
|
||||||
|
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
||||||
|
# ==== Granularity table ====
|
||||||
|
# - features -- Full
|
||||||
|
# - limits -- Partial, paired with params
|
||||||
|
# - params -- Partial, paired with limits
|
||||||
|
# - streamer -- Partial, nullable
|
||||||
|
# - snapshot -- Partial
|
||||||
|
# ===========================
|
||||||
|
|
||||||
|
def signal_handler(*_: Any) -> None:
|
||||||
|
get_logger(0).info("Got SIGUSR2, checking the stream state ...")
|
||||||
|
self.__notifier.notify(self.__ST_STREAMER)
|
||||||
|
|
||||||
|
get_logger(0).info("Installing SIGUSR2 streamer handler ...")
|
||||||
|
asyncio.get_event_loop().add_signal_handler(signal.SIGUSR2, signal_handler)
|
||||||
|
|
||||||
|
prev: dict = {}
|
||||||
|
while True:
|
||||||
|
new: dict = {}
|
||||||
|
|
||||||
|
mask = await self.__notifier.wait(timeout=self.__state_poll)
|
||||||
|
if mask == self.__ST_FULL:
|
||||||
|
new = await self.get_state()
|
||||||
|
prev = copy.deepcopy(new)
|
||||||
|
yield new
|
||||||
|
continue
|
||||||
|
|
||||||
|
if mask < 0:
|
||||||
|
mask = self.__ST_STREAMER
|
||||||
|
|
||||||
|
def check_update(key: str, value: (dict | None)) -> None:
|
||||||
|
if prev.get(key) != value:
|
||||||
|
new[key] = value
|
||||||
|
|
||||||
|
if mask & self.__ST_PARAMS:
|
||||||
|
check_update("params", self.__params.get_params())
|
||||||
|
if mask & self.__ST_STREAMER:
|
||||||
|
check_update("streamer", await self.__get_streamer_state())
|
||||||
|
if mask & self.__ST_SNAPSHOT:
|
||||||
|
check_update("snapshot", self.__get_snapshot_state())
|
||||||
|
|
||||||
|
if new and prev != new:
|
||||||
|
prev.update(copy.deepcopy(new))
|
||||||
|
yield new
|
||||||
|
|
||||||
|
async def __get_streamer_state(self) -> (dict | None):
|
||||||
|
if self.__runner.is_running():
|
||||||
|
session = self.__ensure_client_session()
|
||||||
|
try:
|
||||||
|
return (await session.get_state())
|
||||||
|
except (aiohttp.ClientConnectionError, aiohttp.ServerConnectionError):
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
get_logger().exception("Invalid streamer response from /state")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __get_snapshot_state(self) -> dict:
|
||||||
|
if self.__snapshot:
|
||||||
|
snapshot = dataclasses.asdict(self.__snapshot)
|
||||||
|
del snapshot["headers"]
|
||||||
|
del snapshot["data"]
|
||||||
|
return {"saved": snapshot}
|
||||||
|
return {"saved": None}
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
|
async def take_snapshot(self, save: bool, load: bool, allow_offline: bool) -> (StreamerSnapshot | None):
|
||||||
|
if load:
|
||||||
|
return self.__snapshot
|
||||||
|
logger = get_logger()
|
||||||
|
session = self.__ensure_client_session()
|
||||||
|
try:
|
||||||
|
snapshot = await session.take_snapshot(self.__snapshot_timeout)
|
||||||
|
if snapshot.online or allow_offline:
|
||||||
|
if save:
|
||||||
|
self.__snapshot = snapshot
|
||||||
|
self.__notifier.notify(self.__ST_SNAPSHOT)
|
||||||
|
return snapshot
|
||||||
|
logger.error("Stream is offline, no signal or so")
|
||||||
|
except (aiohttp.ClientConnectionError, aiohttp.ServerConnectionError) as ex:
|
||||||
|
logger.error("Can't connect to streamer: %s", tools.efmt(ex))
|
||||||
|
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.__runner.ensure_stop(immediately=True)
|
||||||
|
if self.__client_session:
|
||||||
|
await self.__client_session.close()
|
||||||
|
self.__client_session = None
|
||||||
|
|
||||||
|
def __ensure_client_session(self) -> HttpStreamerClientSession:
|
||||||
|
if not self.__client_session:
|
||||||
|
self.__client_session = self.__client.make_session()
|
||||||
|
return self.__client_session
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user