Compare commits

...

16 Commits

Author SHA1 Message Date
755944fc58 合并:汇总全部并行工作(会话就绪检测/GPU 看板与直通/README 与 .gitignore 统一) 2026-10-30 15:21:00 +08:00
6d483bbf3c 补充:从公司 main 保留 README 与各模块占位目录(RDP 遗留代码不恢复) 2026-10-30 15:14:33 +08:00
781974d588 合并:保留公司 Gitea main 完整历史,内容以本地开发版为准(删 RDP/直进桌面列表/GPU 看板与直通/防闪重构等全部工作) 2026-10-30 15:14:32 +08:00
8d24a5b2f2 功能:kiosk 会话就绪检测重构(Moonlight 输出解析器/事件化回调/单元测试);GPU 直通实操记录(vfio Subsystem 注入、sysfs vBIOS、7zr+pnputil 安装、566.36 驱动、Sunshine 配对断链修复) 2026-10-30 14:42:11 +08:00
2cc80989c6 文档:GPU 直通实施记录——vfio/驱动安装成功,阻塞于 GM204M vBIOS 缺失(Code 43),已回退保环境并列出三条后续路线 2026-10-30 13:56:25 +08:00
ade16efe49 功能:GPU 数据展示看板上线(http://192.168.1.51:8090,systemd 自启);修正 GPU/IOMMU 实测事实
- tools/gpu_dashboard.py:宿主机端采集(lspci/sysfs/qm),直通前显示驱动/解绑/IOMMU 状态,直通后经 guest agent 采 nvidia-smi 实时利用率/显存/温度/NVENC 会话
- 实测修正:GPU 为 CLEVO 准系统 GM204M/965M(非笔记本误判);IOMMU 已启用(组 2 = 00:01.0 + 01:00.0 整组直通);Maxwell 需 R580 legacy 驱动
- docs/gpu-passthrough-20261030.md:明日直通操作单
2026-10-30 09:52:18 +08:00
27edbe2d82 功能:开机直进桌面列表——设备身份自动登录,移除登录页与退出登录入口
- 构造时以设备 SN 走演示认证,kConfig->kAuthenticating->kDesktopList 同步完成,开机不再显示登录页
- 列表页移除"退出登录"按钮(无登录概念无去向);管理菜单"退出终端"保留
- 登录页与认证代码保留,接平台正式认证后由 DesktopService 驱动恢复登录流程
- 已部署盒子并抓屏验证;文档与 MEMORY 同步
2026-10-30 09:19:17 +08:00
8ec7b1ea49 清理:删除 RDP 时代文档与临时截图/缓存/.zcode;纳管正式产品 Word、VDD 安装资产、对比记录与 PVE 运维工具脚本 2026-10-29 17:44:25 +08:00
9f1824ff21 功能:删除 RDP 渲染管线,协议统一 Moonlight;退出三层设计与串流过场层落地
- 移除 rdp_session.*、FreeRDP 构建依赖与代码内协议概念,Moonlight 为唯一会话通道
- Ctrl+Alt+Shift+Q 退串流回列表(kiosk 兜底 + moonlight-qt 主路径);登录页去退出按钮
- 新增 TransitionOverlay:全屏过场层盖住串流启动闪烁,进程 CPU 首帧检测后淡出
- Sunshine 写入 sw_preset=ultrafast;实测推翻 x264 瓶颈结论(21.6FPS 帽子在虚拟显示/捕获节奏,CPU≈0%)
- 文档:麒麟(101/.71)/Ubuntu(102/.72) 接入信息与四件事落地记录;MEMORY 根因修正
2026-10-29 17:37:38 +08:00
653c4f627e 调整:协议统一 Moonlight,移除 RDP 选项
- 三张桌面默认协议全部 Moonlight(Ubuntu/麒麟装 Sunshine 后即可直连)
- 设置对话框移除协议选择行,保存时固定 Moonlight
- RDP 内嵌代码保留作网关备用,不再从本地配置暴露
2026-10-29 14:56:55 +08:00
8ee0cd2038 功能:桌面连接可配置化——每卡设置、地址支持 IP/域名/VPN 隧道
- 新增 DesktopStore:~/.config/vdi-terminal/desktops.json 持久化连接参数,
  首次启动生成三张默认卡(待分配),老盒子自动从 client.conf 迁移 server 地址
- 新增 DesktopSettingsDialog:按桌面编辑协议/地址/应用名/分辨率/帧率,
  置顶显示避免被全屏 kiosk 遮挡
- 桌面列表每张卡增加“设置”按钮;管理菜单 Ctrl+Alt+Shift+M 增加入口
- DesktopService 改为从 store 读取;地址为空的桌面显示待分配且不可连接
- 修复:默认卡未按 endpoint 重算状态导致迁移后仍显示待分配
- 实测真机通过:Windows 自动迁移可连接、对话框显示与保存正常
2026-10-29 14:05:35 +08:00
66f0cacb32 修复:QProcess::finished 重载消歧;文档:串流失败根因(双 IP ARP 混乱)与延迟 A/B 结论
- moonlight_session.cpp:QOverload 显式指定 finished 重载,修复 ARM64 编译
- moonlight.conf:vsync/frame-pacing 默认改回开启(A/B 实测关闭无改善)
- docs:补充 10-29 登录流程验收、重启验收、Monitor 恢复、双 IP 根因与 A/B 数据
- tools:新增 deploy_kiosk.sh 一键部署脚本与语法自检
2026-10-29 11:58:32 +08:00
a9e42a4084 feat(kiosk-shell): 登录/桌面列表产品流程 + Moonlight 会话 + 看门狗进程组修复
- 登录页改为用户认证(本地演示认证,账号密码非空通过)
- 新增桌面列表页:Windows/Moonlight 可连接,Ubuntu/麒麟待分配
- 新增 DesktopService 适配层(登录/桌面列表/连接票据,后续接 API)
- 新增 MoonlightSession:QProcess 参数数组直启 moonlight-qt,退出回列表
- 状态机扩展 AUTHENTICATING / DESKTOP_LIST
- 移除开机自动 RDP,收敛为 monitor -> kiosk-shell 单一自启链
- Monitor 按进程组结束整棵进程树,修复孤儿串流进程
- vdi-moonlight 启动器新增 vsync/frame-pacing/overlay 低延迟开关
2026-10-28 20:48:12 +08:00
dev
ca15672256 docs: add complete project context for VDI terminal 2026-10-26 10:32:42 +08:00
dev
589cde457c feat(kiosk-shell): FreeRDP 3 内嵌客户端跑通,登录页可进入远程桌面
- 按方案 3.2/3.3 节以 libfreerdp3 API 内嵌,不拉起外部进程
- gdi_init 移到 PostConnect(提前调用会段错误)
- 去掉 ContextSize 扩展 rdpContext,改用 freerdp* 注册表 + gfx->custom
- NLA 认证传空字符串而非 nullptr,修复 SEC_E_NO_CREDENTIALS
- 补充 FreeRDP3 内嵌实现笔记文档
2026-10-23 18:57:05 +08:00
dev
419630fcd7 feat(kiosk-shell): 首版登录页与状态机骨架
按《VDI 云桌面软件开发方案》3.2 节实现:
- 状态机 BOOT/CONFIG/CONNECTING/SESSION/RECONNECT/ERROR
- 首配页(SN 绑定、服务器地址、账号、密码、显示档位)
- 全局热键 Ctrl+Alt+Shift+M 呼出管理菜单
- FreeRDP 3 以 libfreerdp3 API 内嵌,非拉起外部进程
2026-10-23 17:22:55 +08:00
18 changed files with 1917 additions and 0 deletions

79
MEMORY.md Normal file
View File

@ -0,0 +1,79 @@
# 继续开发前必读
完整对话总结在:`D:\DAIMA\vdi-cloud\PROJECT_CONTEXT.md`
## 当前状态
- 项目目录:`D:\DAIMA\vdi-cloud`
- 终端盒子:RK3528 PRO DDR4,Armbian Ubuntu Noble,aarch64,IP `192.168.1.188`
- 当前图形:LightDM + Xorg;正式文档要求 Weston + Wayland + Qt,当前只是 POC
- Proxmox:9.2.2 节点 `starhe`。VM 100 win10-test `192.168.1.70`(Sunshine/Moonlight 云桌面);
VM 101 kylin-test `192.168.1.71`(test1)、VM 102 ubuntu-test `192.168.1.72`(test2),
密码均 `yx66668888`,SSH 可登、图形会话在跑,**尚未装 Sunshine**,暂不能串流
- `kiosk-shell`(Qt/C++):**开机直进桌面列表**(2026-10-30 起,设备 SN 即用户自动登录,
登录页代码保留待平台正式认证接入)→ 选择桌面 → Moonlight 会话;**RDP 代码已于 2026-10-29
全部删除**(git 历史可查),Moonlight 是唯一会话通道,无 freerdp 构建依赖
- 新增退出三层与会话过场层:`Ctrl+Alt+Shift+Q` 退串流回列表;列表页"退出登录";
登录页无退出按钮(退出终端只在管理菜单);点连接有全屏过场层,首帧后淡出。
详见 `docs/moonlight-kiosk-20261028.md` 2026-10-29 下午节
- 代码位置:`terminal/kiosk-shell`
- 远程安装路径:`~/vdi-terminal/bin/`(`kiosk-shell`、`vdi-kiosk-monitor`、`vdi-moonlight`)
## GPU 直通进行中(2026-10-30 下午,见 moonlight-kiosk 文档当日追加节)
- 宿主侧已就绪:nouveau 已黑名单、GPU 在 vfio-pci、IOMMU 组 2(root port+GPU)
- 关键坑:vfio 清零 Subsystem ID(必须 `x-pci-sub-vendor-id/device-id` 注入 1558:6557);
移动卡 vBIOS 需 sysfs 抽取(/usr/share/kvm/gtx965m.rom);NVIDIA 安装器在 SYSTEM 会话
挂起(用 7zr 解包 + pnputil);582.78 绑定后 D3D 全灭已换 566.36(D3D 待验证)
- **当前 VM 100 未挂 GPU**:另一并行会话在改配置(hv_vendor_id 方案),本会话已停手避撞,
两个会话不要同时改 VM 100 配置!
- Sunshine 配对 2026.914 改为"Web UI 批准式":`moonlight pair --pin NNNN` +
`GET /api/csrf-token` + `POST /api/pin {pairing_id,pin}`;Web 凭据 vdiadmin/VdiPass2026;
删 sunshine_state.json = 全部失配;盒子 Moonlight 配置目录缓存旧 uniqueid 会卡死配对
## 必须遵守
- 所有实现严格参考用户提供的两个正式 Word 和会议纪要;文档未确定的技术只能标记待调研,不得擅自定版。
- 终端先做,后接自研管理平台 API;终端不直接调用 Proxmox API。
- 正式状态机:`BOOT -> CONFIG -> CONNECTING -> SESSION -> RECONNECT -> ERROR`
- 正式 Kiosk 选型:Weston + Wayland + Qt;会话通道统一 Moonlight(RDP 已删,勿在代码里复活协议分支)。
- `/cert:ignore`、空密码仅限局域网 POC。
- 项目经理新增待调研:RDP 是否适合 GPU/CAD/3D;研究 Moonlight/Sunshine,但不要在调研前删除 RDP、确定 Moonlight 参数或定版双协议。
## 已知关键结论
- WiFi:约 -47 dBm,协商约 200 Mbps,零丢包/重传;卷帘问题不能简单归因于 WiFi。
- HDMI 当前原生:`1920x1440@75Hz`(10-23 记录);2026-10-28 实测盒子系统已是 `1920x1080@60Hz`,
与 Sunshine 侧虚拟显示器一致,串流 1:1 无缩放。以现场 `xrandr` 为准。
- `/dev/mpp_service`、`/dev/rga` 存在,但 `mppvideodec` GStreamer 插件未确认;Weston/DRM 零拷贝待验证。
- FreeRDP 3:`gdi_init` 必须在 PostConnect;NLA 空密码传 `strdup("")`;BGRX32 需转换为 Qt RGB;`/gfx` 是能力组合语义。
- Moonlight/Sunshine(2026-10-28 实测):Windows 防火墙挡 ICMP,ping 不通不代表不通,
可达性判断必须用 TCP 探测;`moonlight-qt` 的 app id 会随服务端重装失效,
命令行要用应用名 `Desktop`;RK3528 上 `--video-decoder auto` 会选到不可用的
`h264_v4l2m2m`,必须显式 `software`。
- **21.6 FPS 上限根因(2026-10-29 修正)**:不是 x264 编码算力——ultrafast 前后帧率分毫不差,
串流中 VM CPU 仅 0.5%、sunshine 累计 14 秒 CPU/3 分钟会话,鼠标运动也不变;
真正瓶颈在虚拟显示/捕获节奏(MttVDD 呈现 + Desktop Duplication 脏帧 + QEMU 显示栈,
VM CPU 型号 `QEMU Virtual CPU version 2.5+` 未透传宿主 AVX2)。
根治 = GPU 直通(NVENC + 真实显示栈);备选 = VirtIO-GPU / cpu=host 排除 QEMU 显示栈因素。
客户端解码 10~15 ms、网络丢包 0%、延迟 3 ms,链路健康。
## Web 控制台(2026-10-28 方案确认)
- `console/` 是管理员 Web 管理控制台,不是 RK3528 盒子的用户登录页。
- 正式技术栈:Vue3 + Vite + TypeScript 严格模式 + Element Plus;后端通过 api-gateway,JWT + RBAC。
- 页面:仪表盘、终端管理、桌面管理、用户与绑定、GPU 池、监控、OTA、审计日志。
- 角色:超级管理员、运维、只读。
- 终端产品流程仍应是 `kiosk-shell 登录/首配 -> 桌面列表 -> 获取短期连接票据 -> RDP/Moonlight 会话`;
当前开机直连 Moonlight 仅是串流技术验证,不是最终产品登录流程。
- 开发策略:先做功能骨架、Mock API、OpenAPI 契约、RBAC 与 E2E;项目经理 UI 稿到后替换视觉层。
- 完整方案:`docs/web-console-mvp-plan-20261028.md`。
## 尚未完成
- 服务端 GPU 直通:GPU 为 CLEVO 准系统上的 GM204M/965M(10de:13d9,宿主被 nouveau 占用需解绑);
**IOMMU 已启用**(组 2 = root port 00:01.0 + GPU 01:00.0,整组直通);VM 内需装 R580 legacy 驱动。
操作单与 GPU 看板见 `docs/gpu-passthrough-20261030.md`,看板 `http://192.168.1.51:8090/`(systemd 自启)
- RK3528 VPU 硬解(h264_v4l2m2m 等缺可用设备,需 MPP FFmpeg 补丁或 V4L2 request 驱动)
- 2K 分辨率链路验证(虚拟显示器 + 盒子显示模式需同步调整)
- Weston/DRM/mppvideodec
- 后端 API 注册、配置、MQTT 心跳(Moonlight 路径目前是本地静态配置)
- 终端音频出音现场确认
- 协议最终选型定版(RDP / Moonlight / 双协议,需实测数据 + 产品方案共同确认)
- 监控与运维脚本、OTA、USB 重定向等终端其余能力
先读 `PROJECT_CONTEXT.md`,再查看 `git status`,不要丢弃未提交改动。

409
PROJECT_CONTEXT.md Normal file
View File

@ -0,0 +1,409 @@
# 数字员工 VDI 云终端项目上下文
> 这是本项目后续继续开发时的必读上下文。所有实现必须优先以项目经理提供的正式文档和会议纪要为准;文档未明确的地方只能标记为待确认/待调研,不能擅自定版。
>
> 本文件整理自截至 2026-10-23 的完整开发对话。
## 1. 项目目标
项目是“云-管-端” VDI 云桌面方案:
```text
RK3528 云终端
-> 终端接入程序
-> 自研管理平台 API
-> Proxmox VE 管理的虚拟机
-> Windows / Ubuntu / 麒麟 / 后续安卓桌面
```
终端侧最终职责:
- 开机进入 Kiosk/首配登录页;
- WiFi/有线联网;
- 终端注册、心跳、配置下发、日志上报;
- 连接远程桌面;
- 显示远程画面并传递键盘、鼠标、U 盘等外设;
- 断线重连;
- 独立 Monitor 看门狗;
- A/B OTA;
- 后续使用 VPU/DRM 硬件解码与零拷贝显示。
项目经理新增方向:RDP 可能不适合 GPU/CAD/3D 高质量低延迟串流,周一需要调研 RDP 与 Moonlight。Moonlight 只作为待调研/待验证的协议方向,不能在未验证前替换 RDP 或擅自确定端口、认证字段、嵌入方式。
## 2. 权威需求来源
1. `E:\Desktop\数字员工\VDI云终端产品方案-定版摘要(1).docx`
- 产品定版 v2.1。
- 规定 RK3528 终端、2K 默认/4K 上限、FreeRDP 3、GStreamer + mppvideodec、Proxmox/KVM/VFIO、GPU 算力池、管理平台等。
2. `E:\Desktop\数字员工\VDI云桌面软件开发方案(1).docx`
- 软件开发 v1.0。
- 规定 monorepo、终端/云端/控制台技术栈、20 项需求追踪矩阵、测试层次、M1-M4 里程碑。
3. `E:\Documents\xwechat_files\wxid_q6y4us6kip0g22_aa63\msg\file\2026-10\export_1792654201375.docx`
- 会议纪要。
- 规定当前优先:先拿盒子、终端基础连接、Proxmox 逻辑/API、2K/4K/发热/网络验证、Monitor、OTA 后置;张帅协助 P100 GPU 验证。
4. 会议纪要图片:项目技术要点思维导图。
核心纪律:如果代码实现与上述文档不一致,先指出偏差并等待确认;不能以“更快”或“更方便”为理由改用未定版技术栈。
## 3. 正式技术选型与边界
正式开发方案 2.1/2.2 节:
```text
vdi-cloud/
├─ terminal/
│ ├─ meta-vdi/ # Yocto:BSP/内核/设备树/U-Boot/Kiosk/Agent 配方
│ ├─ kiosk-shell/ # Weston + 全屏 Qt 壳
│ ├─ freerdp-rk/ # FreeRDP 3 fork + MPP/VPU 硬解补丁
│ ├─ vdt-agent/ # Rust:注册/心跳/命令/日志
│ ├─ ota-updater/ # Rust:manifest/签名/A-B/回滚
│ └─ usb-monitor/ # Rust:udev/挂载/重定向策略
├─ platform/ # Go 云端服务
├─ console/ # Vue3 + TS + Element Plus
├─ deploy/ # compose/Helm/离线包
├─ docs/ # OpenAPI/MQTT/DB schema
└─ tools/ # 烧录/压测/CI
```
终端技术栈定版:
- Kiosk 壳:C/C++ + Qt,目标显示环境 Weston/Wayland;
- FreeRDP 插件:C/C++;
- Agent/OTA/USB:Rust;
- 协议主线:FreeRDP 3;SPICE 备用;自研 H.265 over QUIC 只作为后续预留;
- 云端:Go;
- 前端:Vue3 + TypeScript + Element Plus;
- 数据库:PostgreSQL + VictoriaMetrics;
- 通信:HTTPS + MQTT;
- 云端底座:Proxmox VE + KVM/QEMU + VFIO;
- GPU:NVIDIA CUDA、华为 CANN 等按卡型池化;终端不安装 GPU 计算组件。
开发方案 3.2 对 kiosk-shell 的明确要求:
- 技术:Weston(Wayland)+ 全屏 Qt 壳进程;
- 状态机:`BOOT -> CONFIG -> CONNECTING -> SESSION -> RECONNECT -> ERROR`;
- 首配页:SN 绑定、WiFi 扫码、服务器地址;
- 状态条:网络、会话、OTA 进度;
- FreeRDP:以 `libfreerdp3` API 内嵌,不拉起外部进程;
- 全局热键:`Ctrl+Alt+Shift+M` 呼出管理菜单。
当前实现是在现有 Armbian 的 LightDM + Xorg 环境中先完成 Qt/Kiosk POC;这不是对正式 Weston 选型的替代。Weston/DRM 必须继续验证。
## 4. 盒子与联调环境
### 盒子
- 型号:RK3528 PRO DDR4;
- OS:Armbian v25.05 rolling,Ubuntu Noble 用户空间;
- Kernel:`6.1.115-vendor-rk35xx`;
- 架构:`aarch64`;
- 内存:约 3.83 GB;
- 存储:约 29 GB;
- IP:`192.168.1.188`;
- SSH:`ssh armbian@192.168.1.188`;
- 图形:LightDM + Xorg,当前不是 Wayland;
- 显示器:`HDMI-1 1920x1440 @ 75Hz`,没有 2560x1440 EDID 模式;
- DRM:`/dev/dri/card0`、`card1`、`renderD128`、`renderD129`;
- 权限:用户已在 `video`、`render` 组;
- 温度:空载约 53°C;
- WiFi:`wlan0`,SSID `YXTech`,信号约 -47 dBm,协商约 200 Mbit/s,实测无丢包/重传;
- 设备节点:`/dev/mpp_service`、`/dev/rga` 存在;
- OpenGL:Mali450,Mesa 25.0.7;Xorg 日志显示 modesetting + glamor initialized。
### 已安装环境
- gcc/g++ 13;
- make、cmake 3.28、ninja、pkg-config、git;
- Qt5 5.15.13(Core/Gui/Widgets/Wayland 包);
- Weston 13.0.0;
- `freerdp3-dev` 3.32.1;
- `freerdp3-x11`,实际命令:`xfreerdp3`;
- `freerdp3-wayland`,实际命令:`wlfreerdp3`,但当前没有 Wayland 会话;
- `mesa-utils`;
- `xdotool`(测试用);
- 当前未确认 `mppvideodec`,Ubuntu 官方源没有 Rockchip MPP GStreamer 插件。
### apt 源
旧 `/etc/apt/sources.list` 中普通 Ubuntu 源会造成 ARM64 404,已备份为 `sources.list.bak` 并注释。有效配置在 `/etc/apt/sources.list.d/ubuntu.sources`,使用 TUNA 的 `ubuntu-ports`。
不要执行:
- `sudo apt upgrade`:曾显示约 298 个包待升级,当前不要整体升级;
- `do-release-upgrade`:不要升级系统版本;
- 不要在盒子上安装 Proxmox,盒子是 ARM64 终端,不是 Proxmox 主机。
本机已经给盒子安装了 SSH 公钥(注释 `zcode-dev`),可以免密远程执行:
```bash
ssh armbian@192.168.1.188
```
SSH 中执行图形命令要带:
```bash
export DISPLAY=:0
export XAUTHORITY=/home/armbian/.Xauthority
```
## 5. Proxmox/Windows 联调环境
- Proxmox VE:9.2.2;
- 节点:`starhe`;
- VM:101,名称 `win10-gongchang01`;
- Windows:Windows 10 22H2 专业版;
- VM IP:`192.168.1.70`;
- RDP:TCP 3389;
- 用户:`test1`;
- 当前 POC 允许空密码登录,正式环境不能使用空密码。
边界:终端最终不应直接调用 Proxmox API。最终关系是:
```text
终端 -> 自研管理平台 API -> Proxmox API -> 虚拟机
终端 -> 管理平台返回的桌面连接地址/网关 -> RDP 或其他协议
```
## 6. 已完成工作
### 6.1 网络与远程桌面基础验证
已完成:
- 盒子 WiFi 联通;
- Windows 10 VM 可达;
- 手工命令 `xfreerdp3` 成功显示 Windows 10 桌面;
- 键盘鼠标可操作;
- 退出全屏最可靠方式:从 SSH 执行 `pkill -f xfreerdp3`;
- 当前 X11 环境下不要直接在 SSH 无 DISPLAY 会话中启动 GUI。
### 6.2 monorepo
项目已在:
```text
D:\DAIMA\vdi-cloud
```
已初始化 git。最近已提交:
```text
589cde4 feat(kiosk-shell): FreeRDP 3 内嵌客户端跑通,登录页可进入远程桌面
419630f feat(kiosk-shell): 首版登录页与状态机骨架
```
当前 git 在日报暂停开发时有未提交改动,主要是 Monitor、心跳、网络显示修复等。不要丢弃这些改动。
### 6.3 kiosk-shell
位置:`D:\DAIMA\vdi-cloud\terminal\kiosk-shell`
已实现:
- Qt5 全屏登录页;
- 深色 UI;
- SN 显示;
- 服务器地址、账号、密码、显示分辨率;
- 记住连接参数;
- 本地配置文件:`~/.config/vdi-terminal/client.conf`;
- 状态机:`BOOT/CONFIG/CONNECTING/SESSION/RECONNECT/ERROR`;
- `Ctrl+Alt+Shift+M` 管理菜单;
- RDP 会话状态;
- 连接断开后重连状态;
- 主进程心跳文件设计:`/tmp/vdi-kiosk.heartbeat`;
- CMake + Ninja 原生 ARM64 编译通过过多次。
当前盒子上的实际运行路径:
```text
~/vdi-terminal/bin/kiosk-shell
```
配置示例:
```text
version=1
sn=...
deviceKey=
server=192.168.1.70
username=test1
password=
wifiSsid=
wifiPassword=
resolution=1920x1440
remember=1
```
### 6.4 libfreerdp3 内嵌
已实现 `rdp_session.cpp/h`,不拉起外部 `xfreerdp3` 进程:
```text
freerdp_new()
freerdp_context_new_ex()
freerdp_connect()
FreeRDP 3 AuthenticateEx
GDI primary buffer
Qt QImage/QLabel 显示
```
已实测修复的坑:
1. `gdi_init` 必须放在 `PostConnect` 中;连接前调用会在 `gdi_init_ex` 段错误;
2. 不能依赖扩展 `ContextSize` 保存自定义指针,`gdi_init_ex` 可能重新分配 context 导致段错误;当前使用按 `freerdp*` 的静态注册表和 `RdpgfxClientContext.custom`;
3. NLA 认证回调空密码必须传 `strdup("")`,不能传 `nullptr`,否则出现 `SEC_E_NO_CREDENTIALS`;
4. FreeRDP 3 设置键使用 `FreeRDP_ServerHostname`、`FreeRDP_Username`、`FreeRDP_DesktopWidth` 等,不要照抄 FreeRDP 2.x 键名;
5. `PIXEL_FORMAT_BGRX32` 是 B/G/R/X 四字节布局,Qt 显示前需要 BGR -> RGB 转换;
6. `/gfx` 命令行是能力组合语义,不能把 `AVC420`/`AVC444` 当作单个值使用。实测可解析形式包括 `progressive|RFX|AVC420|AVC444`。
重要:当前 POC 已成功显示 Windows 10 的真实远程桌面画面,颜色正确,进程保持运行。这是目前最重要的实测成果。
详细说明在:
```text
D:\DAIMA\vdi-cloud\docs\freerdp3-embedding-notes.md
```
### 6.5 显示问题
曾出现“从上到下卷帘式渲染”。目前确认:
- WiFi 信号/速率/丢包不是主要原因;
- 显示器原生是 1920x1440,而曾请求 2560x1440,存在缩放;
- X11/MATE/FreeRDP CPU 侧渲染路径性能有限;
- 正式文档要求 Weston + DRM plane + GStreamer mppvideodec 零拷贝,这正是后续方向;
- 该问题按用户要求暂时不阻断当前 GUI/登录页主线,后续处理。
注意:此前对“X11 无 glamor 硬件合成”的判断不准确,Xorg 日志实际显示 `glamor X acceleration enabled on Mali450`;后续不要继续引用这个错误结论。正确表述是:当前路径仍未达到目标流畅度,具体瓶颈需要后续基准测试确认。
### 6.6 Moonlight 云桌面开机直达(2026-10-28)
RDP/Moonlight 实测对比见 `docs/rdp-vs-moonlight-comparison-20261028.md`,
落地记录见 `docs/moonlight-kiosk-20261028.md`。
盒子当前开机默认走 Moonlight 直连云桌面,链路:
```text
~/.config/autostart/vdi-moonlight.desktop
-> vdi-kiosk-monitor --heartbeat-file /tmp/vdi-moonlight.heartbeat --max-restart 200
-- /home/armbian/vdi-terminal/bin/vdi-moonlight
-> moonlight-qt stream 192.168.1.70 Desktop (fullscreen, 1920x1080, H.264, 软解)
```
仓库新增:`terminal/kiosk-shell/scripts/vdi-moonlight`、`etc/moonlight.conf`、
`vdi-moonlight.desktop`;`src/monitor.cpp` 改为按进程组结束整棵进程树。
实测:满屏云桌面 1:1 无缩放;杀客户端 5 秒内重连;杀启动器 3 秒内被看门狗拉起;
性能叠图 1080p60 实际 21.59 FPS、网络丢包 0%、延迟 3 ms、解码 15.04 ms、抖动丢帧 13.64%。
关键坑位(后续维护必须知道):
1. Windows 防火墙屏蔽 ICMP,盒子 ping 不到云桌面主机,但 TCP/UDP 正常;
可达性判断必须用 TCP 探测,ping 会误判;
2. `moonlight-qt` 的应用 id 随服务端重装/升级失效,旧 id 会报“找不到应用程序 <id>”;
命令行要用应用名 `Desktop`;
3. RK3528 上 `--video-decoder auto` 会选到不可用的 `h264_v4l2m2m`,
必须显式 `--video-decoder software`;
4. RDP 路径的自启项已禁用但保留,可随时切回。
## 7. 当前未完成/未验证
1. 服务端 GPU 直通未做:宿主机 `starhe` 有 GTX 965M(`0000:01:00.0`,IOMMU Group 2 独占),
主机同时有 Intel 核显 `0000:00:02.0` 可继续驱动本机显示,具备直通条件;
直通后 Sunshine 才能用 NVENC 硬编码,把 1080p 帧率从 ~21.6 FPS 提到 60 FPS;
2. RK3528 VPU 硬解不可用:`h264_v4l2m2m` / `hevc_v4l2m2m` / `hevc_v4l2request` 均报
`Could not find a valid device`,当前只能用软件解码,4K 目标必须先解决;
3. 2K 分辨率链路未验证:盒子屏幕现为 1920x1080@60Hz,Sunshine 侧虚拟显示器也是 1920x1080@60Hz;
4. 当前实现仍使用 X11 POC,正式方案要求 Weston/Wayland;
5. 当前 RDP 会话使用 CPU GDI -> Qt 复制,尚未接 GStreamer/mppvideodec/VPU;
6. `mppvideodec` 来源和可用性未确认;
7. 终端注册、MQTT 心跳、配置下发、日志上报、OTA、USB 尚未接后端 API;
8. 前端/后端管理平台尚未接入;Moonlight 路径目前读本地静态配置文件;
9. 终端音频出音未现场确认(`--audio-on-host` 已开);
10. 协议最终选型未定版:实测已证明 Moonlight 画面质量与 CPU 代价优于 RDP,
但“办公用 RDP、GPU/CAD/3D 用 Moonlight”的双协议结论仍需产品方案确认。
## 8. 下一步建议(按优先级)
### A. 服务端 GPU 直通(当前最高价值项)
Sunshine 在 VM 内用 libx264 软件编码,4 核同时承担 1080p 捕获与编码,
是 21.6 FPS 与 13.64% 抖动丢帧的唯一原因;客户端侧链路完全健康。
```text
宿主机 starhe(Xeon E3-1200 v3 + Intel 核显 + GTX 965M)
-> 确认内核 IOMMU 已启用(PCI 设备已有独立 IOMMU group)
-> vfio-pci 绑定 0000:01:00.0,提取 vBIOS ROM
-> VM 100 增加 hostpci,Windows 内装 GeForce 驱动
-> nvidia-smi 正常后 Sunshine 切 NVENC 硬编码
-> 复测 1080p60 帧率、抖动丢帧、服务端 CPU
```
主机 root 密码已知(`yx66668888`),可用 `SSH_ASKPASS` 非交互登录。
### B. 收尾当前终端 POC
1. 保留当前 Moonlight 开机直达版本作为可回退基线(`docs/moonlight-kiosk-20261028.md`);
2. 确认开机自启在真实重启后生效(当前只验证了手动拉起与被守护重启);
3. 网络状态显示修复(此前显示错误的 `600`)尚未确认最新二进制效果;
4. RDP 路径代码保留但自启已禁用,按需切回。
### C. 协议策略与代码结构
实测结论已经支持双协议方向,但仍未定版:
```text
普通办公 -> RDP
GPU/CAD/3D -> Moonlight
```
如果确认需要双协议,代码结构应演进为:
```text
DesktopSession 接口
├─ RdpSession # 保留当前 libfreerdp3
└─ MoonlightSession # 当前是 shell 启动器 + moonlght-qt 子进程
SessionManager # 根据 API 下发 protocol 选择实现
```
不要在定版前擅自确定 Moonlight 的嵌入方式、认证字段或删除 RDP 代码。
### C. 后续 API 对接
与后端确认:
- `/api/v1/terminal/register`;
- 配置拉取接口;
- MQTT 心跳和命令格式;
- 桌面连接信息字段;
- 返回 VM 地址还是 RDP/Moonlight 网关地址;
- 是否使用一次性票据;
- GPU 桌面如何标记协议/资源池;
- 终端不直接调用 Proxmox API。
## 9. 安全与工程注意事项
- `/cert:ignore` 只能用于当前局域网 POC,正式版本必须证书校验;
- 空密码只能用于当前测试 VM,正式环境必须禁用;
- 不要把密码提交到 git;当前本地配置仅用于 POC;
- 终端正式版本不要长期保存 Windows 密码,应使用管理平台/网关/短期凭据方案;
- 不要执行 `git reset --hard`、删除用户已有改动或覆盖未审阅文件;
- 不要把推测写成“已实现”;日报和项目状态要区分“已验证/已完成/待验证/待调研”。
## 10. 今日日报关联
日报模板:`E:\Desktop\日报.txt`。
已按 10 月 23 日实际情况填入并保存:
- 终端硬件与系统基线;
- 编译环境;
- monorepo/kiosk-shell;
- libfreerdp3 内嵌 RDP;
- Windows 10 远程桌面实机验证;
- 2K 显示异常初步定位;
- 明日 Monitor/Weston/mppvideodec/API 待办;
- AVC444、mppvideodec、Weston、API 契约风险。
如果继续写日报,必须以实际工具执行结果为准,不能把未编译或未实测项目写成完成。

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,93 @@
# GPU 直通前置核查与数据看板(2026-10-30)
## 硬件事实(SSH 到宿主机实测,修正旧文档误判)
宿主机 `starhe`(192.168.1.51,pve-manager/9.2.21,内核 7.0.2-6-pve):
```text
00:02.0 VGA : Intel 4th Gen Core (Haswell) iGPU [8086:0416] 驱动 i915(宿主显示用,保留)
01:00.0 3D : NVIDIA GM204M [GeForce GTX 965M] [10de:13d9] 驱动 nouveau(直通前需解绑)
两者 Subsystem 均为 CLEVO/KAPOK [1558:6557]
```
- 机身是 CLEVO 准系统(外形像台式机箱),GPU 是移动版 GM204M/965M——
此前"笔记本"与"IOMMU Group 2 独占"的记载部分有误,以本页为准。
- HDMI 音频功能(01:00.1)不存在,无需处理。
- **IOMMU 实测已启用**(/sys/kernel/iommu_groups 有 15 组,DMAR 生效;
内核 6.8+ 默认开启,不需要显式 intel_iommu=on)。
- GPU 位于 **IOMMU 组 2**,组内设备:`0000:00:01.0`(root port)+ `0000:01:00.0`(GPU),
直通需整组传入。
- VM 100:q35 + SeaBIOS、4 核 8G、win10,未配置 hostpci。
## 明日直通操作单(按序执行)
1. 约窗口:宿主机重启会让全部 VM(100/101/102)停机。
2. 解绑 nouveau:`/etc/modprobe.d/blacklist.conf` 加 `blacklist nouveau`,
`update-initramfs -u`,重启宿主机(重启后 `lsmod | grep nouveau` 应为空)。
3. 绑 vfio:`/etc/modprobe.d/vfio.conf` 写 `options vfio-pci ids=10de:13d9`
(含 root port 的话用 QEMU 参数整组传),重启后确认
`lspci -nnk -s 01:00.0` 显示 `Kernel driver in use: vfio-pci`(看板会自动变绿)。
4. 挂载:`qm set 100 -hostpci0 00:01.0,pcie=1`(按组传;若启动报隔离不满足,
用 `-hostpci0 0000:00:01.0;0000:01:00.0` 形式补传 root port)。
vBIOS ROM 先不挂,Code 43 在 R465+ 驱动已移除检测。
5. VM 内装驱动:Maxwell 已进 legacy 分支,装 **R580 系**驱动(提前下载好放 VM)。
6. 验证:VM 内 `nvidia-smi` 出卡 → 看板"VM 内实时数据"卡片自动出现利用率/显存/温度。
7. Sunshine 切 NVENC(配置 encoder=nvenc 或按其自动探测),1080p60 复测帧率。
## GPU 数据看板(已上线)
- 地址:`http://192.168.1.51:8090/`(自动刷新 4 秒)
- 源码:`tools/gpu_dashboard.py`(宿主机 `/root/gpu_dashboard.py`,
systemd 单元 `gpu-dashboard.service` 已设开机自启)
- 直通前显示:型号/驱动/直通阶段/IOMMU 状态与组内设备/VM 配置;
直通后自动经 `qm guest exec 100` 采集 nvidia-smi:利用率、显存、温度、
驱动版本、功耗、NVENC 编码会话数。
- 手动重启:`systemctl restart gpu-dashboard`。
- 后续:接管理平台后,该数据源可换成平台 GPU 池接口(Web 控制台监控模块复用)。
---
# 2026-10-30 实施记录:直通落地成功,卡在 vBIOS,已回退保环境
## 已完成并持久化
1. 宿主机:blacklist nouveau + vfio.conf(ids=10de:13d9,8086:0c01) + initramfs 更新 + 重启;
重启后 `01:00.0` 由 vfio-pci 接管,nouveau 0。
2. VM 100:hostpci0=0000:01:00.0,pcie=1 挂载成功;客户机出现 `3D Video Controller`。
3. 驱动:582.66 WHQL 经"PC 下载 → 宿主机 8091 HTTP → VM 内网拉取"进入 VM
(VM 直连 NVIDIA CDN 被 403,需走内网中转);安装器兼容性检测拒绝(桌面包 INF 无移动 ID),
改用 `pnputil /add-driver Display.Driver/nvcvi.inf /install` 成功——
nvcvi.inf 含我们的精确 ID `PCI\VEN_10DE&DEV_13D9&SUBSYS_65571558`,
设备绑定成功、`C:\Windows\System32\nvidia-smi.exe` 出现。
4. 全部 VM 已设 onboot 自启;宿主机新增 systemd 服务:`gpu-dashboard`(8090)、`dl-http`(8091)。
## 阻塞点:GM204M 缺少可用 vBIOS,驱动初始化失败(Code 43)
- 宿主机 dmesg:`Invalid PCI ROM header signature: expecting 0xaa55, got 0xffff` ——
该 Optimus 移动卡的 ROM BAR 读不出 vBIOS;sysfs 转储 0 字节。
- 已尝试无效:`cpu host,hidden=1`;`args -cpu host,-hypervisor,hv_vendor_id=...,kvm=off`;
`rombar=0`(设备直接不枚举)。
- 供体 ROM 实验:TechPowerUp 上两个 "GTX 965M" 条目实为 GM206(1427) 改版(芯片不符);
同 GM204 的 Clevo GTX 970M(13D8) ROM 经 PCIR 补丁(vendor 10de + device 13d9 + 子系统 6557)
后 guest 仍不枚举设备。
- 结论:**需要这块卡(或同款 GM204M/13D9)的真 vBIOS 才能完成驱动初始化**。
## 已回退(保客户演示环境)
- VM 100 已摘除 hostpci 并重启,Sunshine 正常(x264 路线),47989 开放;
失败安装无残留(无 NVIDIA 目录)。
- 直通重新启用只需两条命令:`qm set 100 -hostpci0 0000:01:00.0,pcie=1[,romfile=...]` + 重启 VM。
## 后续选项(按代价排序)
1. **宿主机整机冷断电 30 秒以上**(现场 1 分钟操作;Maxwell 多次热重启后设备状态可能损坏,
冷启动是社区最后一招)→ 开机后重挂 `romfile=gtx965m-fixed.rom`(已留存
`/usr/share/kvm/gtx965m-fixed.rom`,即 970M ROM 补丁版)再试。
2. **获取真 GM204M(13D9) vBIOS**:任何可进系统的 965M 机器用 GPU-Z 导出
(同事/二手渠道),或从 CLEVO BIOS 刷写包提取;放到 `/usr/share/kvm/` 后 romfile 挂载。
3. 业务备选:若 965M 直通确认不可行,NVENC 需换卡(桌面 GM204 及以上)或改用其他硬件方案。
## GPU 数据看板
`http://192.168.1.51:8090/`(systemd `gpu-dashboard`,源码 `tools/gpu_dashboard.py`)。
驱动一旦可初始化,"VM 内实时数据"卡片自动显示利用率/显存/温度/NVENC 会话,无需改代码。

View File

@ -0,0 +1,39 @@
@echo off
REM ============================================================
REM MttVDD 虚拟显示驱动 - 一键安装(管理员运行)
REM 用法:右键本文件 -> 以管理员身份运行
REM ============================================================
setlocal
cd /d "%~dp0"
echo [1/4] 关闭可能占用驱动的程序...
sc stop SunshineService >nul 2>&1
taskkill /f /im sunshine.exe >nul 2>&1
echo [2/4] 安装驱动签名证书...
certutil -addstore -f "Root" "Virtual_Display_Driver.cer"
if errorlevel 1 (
echo [错误] 证书安装失败,请确认以管理员身份运行
pause
exit /b 1
)
echo [3/4] 安装 MttVDD 驱动...
pnputil /add-driver "MttVDD.inf" /install
if errorlevel 1 (
echo [警告] 驱动安装返回错误,尝试另一种方式...
rundll32.exe setupapi.dll,InstallHinfSection DefaultInstall 132 .\MttVDD.inf
)
echo [4/4] 显示当前虚拟显示器状态...
pnputil /enum-devices /class Display
echo.
echo ============================================================
echo 安装完成。请重启 Windows 后:
echo 1) 设置 - 显示 - 找到 MttVDD 虚拟显示器
echo 2) 分辨率选 1920x1080,刷新率选 60Hz
echo 3) 重启 SunshineService 服务
echo 重启方式:开始菜单搜索 services.msc -> SunshineService -> 重新启动
echo ============================================================
pause

View File

@ -0,0 +1,322 @@
# Moonlight 云桌面开机直达落地记录(2026-10-28)
## 起因
上一个对话在做完 RDP/Moonlight 实测对比(见 `rdp-vs-moonlight-comparison-20261028.md`)后,
提出把 Moonlight 产品化为开机直连云桌面,但只列了 5 条待办就中断了。
现场表现:登录盒子后看到的是 MATE 的绿色桌面,不是云桌面。
## 根因
盒子自启项 `~/.config/autostart/vdi-kiosk.desktop` 仍然拉起的是 RDP 版 `kiosk-shell`,
而 Moonlight 从来没有进入过开机自启链;同时命令行直连串流还存在两处硬伤:
1. **应用 id 失效**:Moonlight 图形界面里配对的记录里,`Desktop` 的 app id 是 `881448767`,
但 Sunshine 重装/升级后服务端的应用 id 已经变了,用旧 id 启动直接报
`Qt Critical: 找不到应用程序 881448767`,串流根本起不来。
改用应用**名称** `Desktop` 启动即可,服务端当前返回的应用列表是
`Desktop` / `Steam Big Picture`。
2. **硬解路径不可用**:`moonlight-qt` 的 `--video-decoder auto` 会优先尝试
`h264_v4l2m2m`,在 RK3528 上报 `Could not find a valid device`
(尽管存在 `/dev/mpp_service`、`/dev/rga`、`/dev/dri/renderD128`)。
显式指定 `--video-decoder software` 后串流稳定。
另外两个在联调中发现并修掉的问题:
3. **Monitor 只杀直接子进程**:`vdi-kiosk-monitor` 通过 `sh -c` 启动主进程,
杀掉它只会结束 `sh -c` 包装进程,真正的启动器变成孤儿并继续拉起第二条串流,
两条流同时抢显示设备。已改为子进程独立进程组、按组结束整棵进程树。
4. **主机可达性判断用错了协议**:Windows 防火墙屏蔽 ICMP,盒子 ping 不到 `192.168.1.70`,
但 TCP/UDP 正常。可达性判断改成 TCP 连接探测(`bash /dev/tcp`)。
## 已交付
仓库内新增(`terminal/kiosk-shell/`):
```text
scripts/vdi-moonlight # 云桌面串流启动器(POSIX sh)
etc/moonlight.conf # 串流参数模板
vdi-moonlight.desktop # 开机自启条目(Monitor + 启动器)
```
修改:`src/monitor.cpp`(进程组管理)、`src/monitor_main.cpp` 无改动。
盒子上的部署位置:
```text
~/vdi-terminal/bin/vdi-moonlight # 启动器
~/vdi-terminal/bin/vdi-kiosk-monitor # 看门狗(已按本次修改重新编译)
~/.config/vdi-terminal/moonlight.conf # 串流参数
~/.config/autostart/vdi-moonlight.desktop # 开机自启(已启用)
~/.config/autostart/vdi-kiosk.desktop # RDP 旧路径(已禁用,保留作回退)
```
启动链:
```text
systemd/user autostart
-> vdi-kiosk-monitor --heartbeat-file /tmp/vdi-moonlight.heartbeat --max-restart 200
-- /home/armbian/vdi-terminal/bin/vdi-moonlight
-> moonlight-qt stream 192.168.1.70 Desktop ... (fullscreen)
```
启动器职责:读本地配置 → 等云桌面主机就绪(TCP 探测)→ 全屏串流 →
每 5 秒写心跳 → 检测串流假死(CPU 与网络均无流量超过 `hang-timeout`)→ 失败重试。
## 实测结果
全部在 RK3528 盒子(192.168.1.188)上实测:
| 项目 | 结果 |
|---|---|
| 开机/手动拉起后屏幕内容 | 满屏 Windows 10 云桌面,1920x1080 与屏幕 1:1 无缩放 |
| 杀掉 moonlight-qt 客户端 | 启动器 5 秒内重连成功 |
| 杀掉启动器 | 看门狗 3 秒后重启启动器,重新串流 |
| 心跳文件 | `/tmp/vdi-moonlight.heartbeat` 每 5 秒刷新 |
| 串流进程数 | 始终 1 条,无孤儿进程 |
`moonlight-qt --performance-overlay` 实测(1080p60 / H.264 / 软解):
```text
Video stream: 1920x1080 21.59 FPS (Codec: H.264)
Incoming frame rate from network: 21.59 FPS
Decoding frame rate: 21.59 FPS
Rendering frame rate: 18.15 FPS
Frames dropped by your network connection: 0.00%
Frames dropped due to network jitter: 13.64%
Average network latency: 3 ms (variance: 1 ms)
Average decoding time: 15.04 ms
Average frame queue delay: 47.50 ms
Average rendering time (including monitor V-sync latency): 51.09 ms
```
结论:链路健康,网络丢包 0%,延迟 3 ms;21.6 FPS 的上限来自**服务端**,
VM 100 内 Sunshine 走 libx264 软件编码(无 GPU 直通),抖动丢帧 13.64% 同源。
## 仍未验证 / 下一步
1. **服务端 GPU 直通**:宿主机 `starhe` 有 GTX 965M(`0000:01:00.0`,IOMMU Group 2 独占),
主机同时有 Intel 核显 `0000:00:02.0` 可继续驱动本机显示,具备直通条件。
直通后 Sunshine 可用 NVENC 硬编码,是把帧率提到 60 FPS、改善鼠标拖尾/卡顿的前提。
2. **鼠标卡顿**:性能叠图实测渲染帧率仅 18.15 FPS、帧队列延迟 47.50 ms、
渲染/VSync 51.09 ms,再叠加服务端 libx264 软编码,视频链路本身已累计约 100 ms。
`--absolute-mouse` 已启用,输入模式没有配置错误;体感滞后来自低帧率和视频队列,
根治仍是 Sunshine NVENC。短期可降为 1080p30、使用 `--no-vsync --no-frame-pacing`
做 A/B 测试,但可能增加撕裂,不能替代服务端硬编码。
3. **RK3528 硬解**:`h264_v4l2m2m` / `hevc_v4l2m2m` 缺少可用设备,需要 Rockchip MPP 的
FFmpeg 补丁或 V4L2 request 驱动。当前 1080p 软解可用,4K 需要硬解。
4. **2K 分辨率**:盒子当前屏幕是 1920x1080@60Hz,Sunshine 侧虚拟显示器也是 1920x1080@60Hz。
若产品要求 2K,需要同时调整虚拟显示器分辨率和盒子端显示模式。
5. **音频**:`--audio-on-host` 已开启,但终端出音未经现场耳朵确认。
6. **产品登录流程**:当前开机直连 Moonlight 只是串流技术验证,不是最终产品形态。
最终应由 `kiosk-shell` 显示首配/登录/桌面列表,用户选择后再获取短期连接票据并启动会话;
管理员 Web 控制台是独立的 `console/`,方案见 `docs/web-console-mvp-plan-20261028.md`。
7. **与 RDP 的关系**:RDP 路径代码保留在 `src/rdp_session.*`,自启项已禁用,可随时切回。
正式协议选型仍需按实测数据与产品方案确认,不能仅凭本记录定版。
8. **管理平台 API**:Moonlight 路径目前是本地配置文件驱动,接入自研管理平台后
应由 API 下发主机/分辨率/帧率等参数,替换 `etc/moonlight.conf` 的静态配置。
9. **重启验收异常**:2026-10-28 15:54 执行真实重启后,盒子在 3 分钟内未恢复
`192.168.1.188` 的网络/SSH,局域网扫描也未发现其 SSH 地址。重启前已确认
NetworkManager 连接 `YXTech` 的 `autoconnect=yes`,但真实重启后的开机自启验收因此未完成;
需要现场查看盒子 HDMI 画面/电源状态,并通过串口或本地键盘确认启动与 WiFi 情况。
## 附:常用运维命令
```bash
# 查看状态
/home/armbian/vdi-terminal/bin/vdi-moonlight status
# 查看日志
tail -f ~/vdi-terminal/logs/moonlight-$(date +%Y%m%d).log
tail -f /tmp/vdi-monitor.log
# 手动重启串流
pkill -f '^moonlight-qt stream' # 启动器会自动重连
# 切回 RDP 路径
sed -i 's/^X-GNOME-Autostart-enabled=false/X-GNOME-Autostart-enabled=true/' \
~/.config/autostart/vdi-kiosk.desktop
sed -i 's/^X-GNOME-Autostart-enabled=true/X-GNOME-Autostart-enabled=false/' \
~/.config/autostart/vdi-moonlight.desktop
```
---
# 2026-10-29 追加:登录/桌面列表产品流程落地与网络根因修复
## 新增验证结论(全部实测)
1. **产品流程全面跑通**:开机 → 登录页(本地演示认证)→ 桌面列表
(Windows 可连接 / Ubuntu、麒麟待分配)→ 点击连接 → Moonlight 全屏串流
Windows 10 桌面 → 串流结束自动回桌面列表。
2. **重启验收通过**:真实重启后固定 IP `192.168.1.188` 自动恢复,
唯一自启链 `vdi-kiosk-monitor -> kiosk-shell` 生效,开机只进登录页,
不再出现"先 RDP 后 Moonlight"双链竞争。
3. **Monitor 崩溃恢复**:kill -9 kiosk-shell 后 4 秒内自动重启,心跳正常。
## 昨晚两次串流失败的根因(重要)
用户给盒子配静态 IP `192.168.1.188` 时,是在 DHCP 连接上"添加地址",
导致 wlan0 同时持有 `.185`(DHCP)和 `.188`(静态)两个地址。
双地址引发 ARP 混乱,实测:
```text
盒子 -> 网关 ping 丢包 66%~85%(间歇)
PC -> 盒子 ping 丢包 33%
UDP 双向近乎全丢(Moonlight 的视频/控制都是 UDP)
TCP/SSH 基本正常
```
Moonlight 两次失败(38 秒断流、第二次连不上)均为该丢包所致,
与 Moonlight 参数、Sunshine 无关。WiFi 信号本身满格(-38dBm)。
修复:`nmcli` 将 YXTech 连接改为 `ipv4.method=manual` 纯静态,
只保留 `.188` 一个地址。修复后网关 ping 0% 丢包(60/60),
串流稳定运行 7 分钟以上直至手动结束。
**教训:给无线终端配静态 IP 必须用 `ipv4.method=manual` 整体替换,
不能在 DHCP 连接上叠加第二个地址。**
## 鼠标延迟 A/B 实测(1080p60,服务端 libx264)
| 指标 | 基线 vsync+frame-pacing | no-vsync + no-frame-pacing |
|---|---|---|
| 串流帧率 | 21.64 FPS | 21.60 FPS |
| 渲染帧率 | 18.20 FPS | 18.16 FPS |
| 网络丢帧 | 0.00% | 0.00% |
| 抖动丢帧 | 15.91% | 13.64% |
| 帧队列延迟 | 44.13 ms | 47.21 ms |
| 渲染/VSync | 48.79 ms | 49.49 ms |
结论:关闭 vsync/frame-pacing **无改善**(队列延迟反而略升)。
~45ms 队列延迟 ≈ 一个帧间隔(21.6FPS → 46ms/帧),
瓶颈是服务端软编码帧率,不是客户端参数。
决定:保持 vsync + frame-pacing 开启(C++ 默认),
`etc/moonlight.conf` 默认值已同步改回 `1`。
鼠标卡顿的根治手段仍是服务端 GPU 直通 + NVENC。
---
# 2026-10-29 下午追加:四件事落地(删 RDP / 退出设计 / 过场层 / ultrafast 实测)
## 已交付(全部编译部署到盒子并实测)
1. **删 RDP**:`rdp_session.*`、FreeRDP 渲染管线、CMake 构建依赖全部移除,
协议概念(`Protocol` 枚举)从代码删除,Moonlight 是唯一会话通道;
RDP 代码只存在于 git 历史(网关备用)。部署后 `ldd kiosk-shell` 无任何 freerdp 依赖。
2. **退出设计三层**:
- 会话内 `Ctrl+Alt+Shift+Q` 退出串流回桌面列表——moonlight-qt 自带快捷键为主路径,
kiosk 侧保留同一快捷键兜底(串流未建立/焦点仍在 kiosk 时生效);
- 桌面列表右上角"退出登录"→ 登录页(已有);
- 登录页删除"退出"按钮(退出终端是维护功能,只留在管理菜单 Ctrl+Alt+Shift+M)。
3. **加载过场层**:新增 `src/transition_overlay.*`,点连接后全屏置顶黑层
("正在进入云桌面…"动态省略号 + 旋轮),盖住 moonlight-qt 启动期左上角小窗闪烁;
首帧检测用 moonlight 进程 CPU 时间近似(软解 CPU 远高于握手期,阈值 120 tick / 15s 兜底),
检测到后 1 秒淡出。活动期间吞键盘鼠标、周期 raise 保持置顶。
4. **Sunshine x264 预设 ultrafast**:VM 100 的 `C:\Program Files\Sunshine\config\sunshine.conf`
写入 `sw_preset = ultrafast`(该版本支持 `sw_preset`/`sw_tune` 键),服务已重启生效。
## 关键实测结论:21.6 FPS 帽子不在 x264,旧根因判定被推翻
ultrafast 前后对照(1080p60 / H.264 / 盒子软解,同参数同网络):
| 指标 | medium(基线 10-28) | ultrafast(本次) |
|---|---|---|
| Video stream FPS | 21.59~21.64 | 21.57~21.61 |
| 抖动丢帧 | 13.64%~15.91% | 13.64%(分毫不差) |
| 客户端解码 | 15.04 ms | 10.51~11.14 ms |
决定性证据:
- 串流进行中 VM 内 `Processor(_Total)` CPU 仅 0.2%~0.5%,
sunshine 进程自服务重启起累计 CPU 时间仅 14 秒(含三次共约 3 分钟会话);
- 用 xdotool 持续晃鼠标制造画面运动,帧率与 CPU 都不变;
- libx264 只有 `MMX2 SSE2Fast SSSE3 SSE4.2`——VM CPU 型号是
`QEMU Virtual CPU version 2.5+`(kvm64 类),宿主指令集(含 AVX2)没有透传。
结论:**21.6 FPS 的上限来自虚拟显示/捕获节奏(IddSampleDriver/MttVDD 呈现与
Desktop Duplication 的脏帧节奏,叠加 QEMU 显示栈),不是编码器算力**——
编码器近乎空转,预设怎么调都不会变。ultrafast 保留(无害,客户端解码也略降)。
根治路径按优先级:GPU 直通(NVENC + 真实显示栈)> 换 VM 显示方案
(VirtIO-GPU / cpu=host 先排除 QEMU 显示栈因素)> 都不是调 Sunshine 参数能解决的。
## 运维新装 Linux 云桌面 VM(2026-10-29 接入信息)
| VM | Proxmox | 地址 | 账号 | 实测 |
|---|---|---|---|---|
| kylin-test | 101 | 192.168.1.71 | test1 / yx66668888 | SSH 可登,银河麒麟 V10 SP1 桌面版,UKUI + Xorg,图形会话运行中 |
| ubuntu-test | 102 | 192.168.1.72 | test2 / yx66668888 | SSH 可登,Ubuntu 24.04.4 LTS,GNOME + Xorg,图形会话运行中(4 核 / 5.8GiB) |
两台均未装 Sunshine(47989 等端口关闭),还不能被 Moonlight 连接。
接入桌面列表的步骤:装 Sunshine(Linux 版)→ 配对 → 在盒子"桌面连接设置"里
把 Ubuntu/麒麟卡片的地址填上即可(kiosk 代码已支持,无需改代码)。
---
# 2026-10-30 下午追加:GPU 直通实操 + 配对断链修复(大量新坑位)
## 直通实操结论(VM 100 / GTX 965M CLEVO 1558:6557)
1. **vfio reset 会把 GPU 的 Subsystem ID 清零**(Windows 里实例变成
`SUBSYS_00000000`),而 NVIDIA 移动卡驱动全部按 SUBSYS 匹配
(本卡在 `nvcvi.inf` 的 `SUBSYS_65571558`),不注入就永远"无法安装"。
解法 = QEMU 设备参数 `x-pci-sub-vendor-id=0x1558,x-pci-sub-device-id=0x6557`。
2. **移动卡 vBIOS 不在卡上**(ROM BAR 为空,宿主 dmesg 反复
"No more images in PCI ROM")。宿主开机时系统 BIOS 已 shadow 过 vBIOS,
可从 sysfs 抽取:停 VM → `echo 0000:01:00.0 > /sys/bus/pci/drivers/vfio-pci/unbind`
→ `echo 1 > rom; cat rom > gtx965m.rom`(108KB,55AA 开头有效)→ 重绑 vfio。
3. **NVIDIA 安装器(-s/-x)在 qemu-ga SYSTEM 会话下静默挂起**(CPU 时间 ~0s,
无解包无日志)。VM 内装驱动的可行路径:PC 端 7zr 解包 → 取 Display.Driver →
zip 传入 VM → `pnputil /add-driver nvcvi.inf /install`。
国内线路被 NVIDIA CDN 403,直链需 PC 中转(PC→scp 宿主机→宿主机 HTTP→VM 拉)。
4. **582.78 绑定成功但 D3D 全灭**:PnP Status OK(需手动
`pnputil /restart-device` 踢一次,开机自启是 Code 43),但 dxdiag 看不到适配器、
NVML 初始化失败、所有硬编编码器 D3D11 建设备报 0x887A0004 (DEVICE_REMOVED)。
已换 **566.36**(2024-12,Maxwell+VM 社区验证充分),包已装、设备已绑定;
D3D 是否健康**尚未验证**(GPU 在替换期间被移出 VM,见下)。
5. **可用挂载参数模板**(未来重启 VM 用):
`-device vfio-pci,host=0000:01:00.0,bus=pcie.0,addr=0x10,x-pci-sub-vendor-id=0x1558,x-pci-sub-device-id=0x6557,romfile=gtx965m.rom`
(rom 放 /usr/share/kvm/;另一会话在测 `hv_vendor_id=NVidiaFIX,kvm=off` 方案)。
6. **local-lvm thin 池曾被驱动文件写满**(100% → error IO → VM 被 QEMU 暂停),
已 `lvextend -L +13G pve/data`(49→62GB)。VM 内 C:\nvsetup 有 ~12GB 驱动
中间产物可清理;池仍超配(160GB 卷/62GB 池),建议后续扩存储。
## 配对断链与修复(重要运维知识)
- `sunshine_state.json` 存服务器 uniqueid 与已配对客户端。**删它 = 所有客户端失配**。
修复后的新流程(2026.914 的"Web UI 批准式"配对):
1. 盒子:`moonlight-qt pair 192.168.1.70 --pin 4321`(**PIN 由客户端生成**,可 `--pin` 指定);
2. 宿主/PC 侧调 Sunshine Web API 批准:
`GET /api/csrf-token`(Basic Auth)→
`POST /api/pin` body `{"pairing_id":"<GET /api/pin 里拿>","pin":"4321"}`,
头带 `X-CSRF-Token` + Basic Auth(无 /api/login;POST 必须 CSRF 头)。
- Web 凭据已重置为 `vdiadmin / VdiPass2026`(写在本页,勿提交正式环境)。
- **旧版缓存 uniqueid 会让 pair 卡 "Found unexpected PC" 死循环**:
盒子 `~/.config/Moonlight Game Streaming Project/` 整目录删除后重配即可。
- Sunshine 重启不再换 uniqueid(持久化已恢复);**若再出现"每次重启换 uniqueid",
先查 sunshine_state.json 是否能写**。
- NVIDIA 设备半坏时 Sunshine 捕获初始化会崩循环(服务恢复 5-6 分钟拉起一次),
表象是"每几分钟换一次 UUID + 配对全部失败"。根因仍是驱动 D3D 问题。
## 当前现场(2026-10-30 14:50)
- VM 100:**GPU 未挂载**(另一并行会话用 `hv_vendor_id=NVidiaFIX` 方案接手了配置,
为避免两个会话互相覆盖,本会话已停止改 VM 配置);VM 内 566.36 驱动已装好待用。
- 盒子:kiosk 单链已恢复、配对已修复、串流实测 ~23 FPS(软编基线)可正常进出。
- 宿主机 /root 下留有 nv582.78.exe、DisplayDriver.zip、DD566.zip、gtx965m.rom;
8018 端口有临时 HTTP 服务(http8018svc)供 VM 拉文件,用完可
`systemctl stop http8018svc`。
---
# 2026-10-30 追加:开机直进桌面列表
按产品决定,盒子开机不再显示 kiosk 登录页,直接进入"选择云桌面"列表页:
- 构造时以设备身份(SN)自动走 DesktopService 演示认证(SN 即当前用户展示),
状态链 `kConfig -> kAuthenticating -> kDesktopList` 同步完成,无登录页闪烁;
- 列表页右上角"退出登录"按钮移除(无登录概念后无去向),管理菜单"退出终端"保留;
- **登录页与 DesktopService 认证代码全部保留**:接入管理平台正式认证
(/api/v1/terminal/session/login)后,由 DesktopService 决定是否回到登录流程,
页面代码无需重写;git 历史含完整"登录页 -> 桌面列表"产品流程版本。
- 已编译部署盒子并抓屏验证:开机落在桌面列表,Windows 可连接、Ubuntu/麒麟待分配。

View File

@ -0,0 +1,82 @@
# RDP 与 Moonlight 实测对比(2026-10-28)
## 测试环境
```text
终端:RK3528 PRO DDR4,Armbian 25.05,aarch64,LightDM + Xorg
显示器:1920×1440 @ 75Hz
云桌面:VM 100(win10-test),Proxmox VE 9.2.21,节点 starhe
终端到云桌面:192.168.1.188 -> 192.168.1.70,同一局域网 WiFi
```
服务端显示环境改造(本次关键前提):
```text
改造前:Microsoft Basic Display Adapter,1920×1080 @ 1Hz
改造后:Virtual Display Driver 8.35.11.365,1920×1080 @ 60Hz
```
改造前 Sunshine 抓不到稳定帧(1Hz),改造后可正常编码。
## 对比数据
| 指标 | RDP(当前 kiosk-shell) | Moonlight 1080p30 | Moonlight 1080p60 |
|---|---|---|---|
| 协议/编码 | RDP GFX,CPU 软解 | H.264 / libx264 软编 | H.264 / libx264 软编 |
| 分辨率 | 1920×1440 | 1920×1080 | 1920×1080 |
| 终端 CPU | 约 111%(单核近满载) | 约 26% | 约 26% |
| 终端内存 | 约 232 MB | 约 192 MB | 约 165 MB |
| 终端温度 | 约 58~65℃ | 约 56℃ | 约 53℃ |
| 实际帧率 | 无可靠统计(观感为低帧+卷帘) | 12.83 FPS | 22.79 FPS |
| 网络丢包 | 未测 | 0.00% | 0.00% |
| 网络抖动丢帧 | 未测 | 0.08% | 15.28% |
| 网络延迟 | 未测 | 2 ms | 6 ms |
| 主机处理延迟 | 未测 | 14.4 / 27.6 / 平均 18.1 ms | 18.8 / 78.3 / 平均 38.2 ms |
| 平均渲染耗时 | 未测 | 3.34 ms | 46.26 ms |
| 画面观感 | 卷帘、翻页 | 完整清晰,无撕裂 | 完整清晰,无撕裂 |
## 结论
1. **卷帘问题不是 RDP 协议的限制,是当前 RDP 客户端实现路径的问题。**
Moonlight 在同一台盒子、同一台云桌面、同一网络下画面完整清晰,无卷帘无撕裂,
抓屏确认 1080p 画面文字锐利、任务栏时间正确。
2. **Moonlight 的瓶颈在服务端,不在终端,也不<E4B99F><E4B88D><EFBFBD>网络。**
1080p30 时网络丢包 0.00%、延迟 2 ms、渲染 3.34 ms、终端 CPU 26%,
但实际帧率仅 12.83 FPS;1080p60 时主机处理延迟升到平均 38.2 ms、渲染 46.26 ms。
服务端当前使用 libx264 软件编码(VM 内无 NVIDIA 直通,`nvidia-smi` 不存在),
4 核 CPU 同时承担 1080p 编码与桌面捕获,是帧率上限的真正原因。
3. **RDP 路径的 CPU 代价约为 Moonlight 的 4 倍**(111% vs 26%),
因为 RDP 走 `FreeRDP GDI -> CPU 内存复制 -> QImage/QPixmap -> QLabel -> Xorg`,
而 Moonlight 只做一次解码和一次窗口呈现。
4. **RK3528 VPU 硬解在当前 moonlight-qt 6.2.0 下不可用。**
`h264_v4l2m2m` / `hevc_v4l2m2m` / `hevc_v4l2request` 均报
`Could not find a valid device`,即使盒子存在 /dev/mpp_service、/dev/rga 和
/dev/dri/renderD128。需 Rockchip MPP FFmpeg 补丁或 V4L2 request 驱动。
## 后续建议
优先级从高到低:
1. **服务端 GPU 直通**:将宿主机 GTX 965M(PCI 01:00.0,IOMMU Group 2 独占)
直通给 VM 100,使 Sunshine 可用 NVENC 硬编码。预期把 1080p 帧率从 13 FPS
提升到 60 FPS,同时释放服务端 CPU。需先确认内核 `intel_iommu=on`、
`vfio-pci` 已绑定,以及该笔记本 GPU 的 vBIOS ROM 提取。
2. **消除帧率随请求升高而抖动**(60 FPS 时抖动丢帧 15.28%):服务端稳定后复测。
3. **RK3528 硬解适配**:解决 V4L2 request/m2m 设备缺失,这是达成 4K 的前提。
4. **协议选型建议**:
- 若 GPU 直通可行,Moonlight 在 CAD/3D/视频场景优于 RDP,建议按产品方案采用双协议:
普通办公用 RDP,GPU/CAD/3D 用 Moonlight;
- 同时保留并优化 RDP 路径的 Weston + MPP/VPU + DMABUF/DRM 零拷贝改造,
使 RDP 在办公场景达到可接受体验。
5. 当前 Moonlight 未接 VPU,1080p 软件解码仅 26% CPU 但 4K 会超出能力,
需在 4K 目标确定后优先解决硬解。
## 附:本次变更
- VM 100:安装 Virtual Display Driver(MttVDD),提供 1920×1080@60Hz 虚拟显示器。
- VM 100:安装 Sunshine v2026.914.233613,Web UI 账号 admin / vdi12345。
- 终端:安装 moonlight-qt 6.2.0,已与 Sunshine 完成配对。
- 终端:kiosk-shell 自启动已恢复(~/.config/autostart/vdi-kiosk.desktop)。

View File

@ -0,0 +1,74 @@
# Sunshine 显卡环境配置指引(转给操作 VM 的同事)
## 背景
我们在 VM 100(win10-test,192.168.1.70)里装了 Sunshine 用于 Moonlight 串流验证。当前卡在:VM 没有可用 GPU,Windows 只有「Microsoft 基本显示适配器」,分辨率 1024×768、刷新率 1Hz,Sunshine 无法按 30/60fps 捕获编码。
不需要动 Proxmox,只需要在 Windows 里完成两件事:装虚拟显示器驱动 + 确认 Sunshine 配置。
## 第 1 步:安装虚拟显示器驱动(IddSampleDriver)
让 Windows 出现一个虚拟显卡,可自定义分辨率和刷新率:
1. 下载虚拟显示驱动包(ParrotGeek 的 IddSampleDriver,或等效的「Virtual Display Driver」)。
常用地址:https://github.com/itsmikethetech/Virtual-Display-Driver/releases
(如果 GitHub 打不开,用镜像:把链接前面加 https://gh-proxy.com/ )
2. 解压后右键 `IddSampleDriver.inf` →「安装」。
3. 重启 Windows。
4. 装好后设备管理器会出现「IddSampleDriver Device」,显示设置里会多出一个虚拟显示器。
5. 把虚拟显示器设置为:
- 分辨率:1920×1080(先用 1080p,稳定后再试 2K)
- 刷新率:60Hz(关键!当前 1Hz 是主要问题)
- 如果看到多显示器,把虚拟屏设为「扩展这些显示器」, Sunshine 默认抓主屏;也可以只保留虚拟屏。
## 第 2 步:确认 NVIDIA 独显是否可用(如果宿主已直通/有驱动)
打开 PowerShell 执行:
```powershell
nvidia-smi
```
- 如果能输出显卡信息(GTX 965M):很好,说明 VM 内能用 NVIDIA。
- 请安装 GeForce 驱动(如未装),然后告知我们,我们把 Sunshine 切到 NVENC 硬编码。
- 如果提示找不到命令或没有 NVIDIA 设备:跳过此步,走纯软件编码 + 虚拟显示器路线(也够验证流畅度)。
## 第 3 步:检查 Sunshine 服务状态
Sunshine 已装在 `C:\Program Files\Sunshine\`,服务名 `SunshineService`。
PowerShell 执行:
```powershell
Get-Service SunshineService
Restart-Service SunshineService
```
重启服务后再看日志确认识别到新显示器:
```powershell
Get-Content "C:\Program Files\Sunshine\config\sunshine.log" -Tail 60
```
需要看到的关键行(类似):
```
Desktop resolution [1920x1080]
Display refresh rate [60 Hz]
```
## 第 4 步:告知我们结果
只需回三条信息:
1. 虚拟显示器是否装好,分辨率/刷新率是多少;
2. `nvidia-smi` 是否有输出;
3. sunshine.log 最后几十行里的 Desktop resolution / Display refresh rate 数值。
我们这边会立刻重新发起 Moonlight 串流测试并采集性能数据。
## 备注
- Web UI(可选):https://192.168.1.70:47990 账号 admin / 密码 vdi12345
- 装驱动需要重启 VM,重启后 SunshineService 会自启,无需其他操作。
- Sunshine 的串流端口 47984/47989/47990/48010 已确认开放,防火墙无需改动。

20
docs/vdd_settings.xml Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<display_config>
<target_display>
<target_x>0</target_x>
<target_y>0</target_y>
<target_width>1920</target_width>
<target_height>1080</target_height>
<target_refresh_rate>60</target_refresh_rate>
</target_display>
<adapter_index>0</adapter_index>
<modes>
<mode>
<width>1920</width>
<height>1080</height>
<refresh_rate>60</refresh_rate>
</mode>
</modes>
</display_config>
</root>

View File

@ -0,0 +1,287 @@
# VDI Web 管理控制台 MVP 实施方案
> 版本:v0.1(功能先行,UI 稿待项目经理提供)
> 日期:2026-10-28
> 依据:`VDI云桌面软件开发方案(1).docx` 第 2、4、5 节及 `VDI云桌面完整方案-云管端.docx` 管理平台模块表。
## 1. 先明确两个“登录”
本项目有两个不同的界面,必须分开:
| 界面 | 使用者 | 运行位置 | 职责 |
|---|---|---|---|
| 终端 kiosk-shell | 云桌面最终用户 | RK3528 盒子本地全屏 | 首配、设备绑定、用户登录、选择/进入被授权桌面、会话状态 |
| Web 管理控制台 console | 超级管理员、运维、只读人员 | PC 浏览器 | 终端、桌面池、用户绑定、GPU、监控、OTA、审计管理 |
正式文档中的 `console/` 指第二项。它不应运行在盒子上,也不应直接发起 RDP/Moonlight;它通过平台 API 管理资源和下发策略。
“盒子开机直连 Moonlight”只能作为当前串流技术验证,不是最终产品登录流程。最终终端流程应恢复为:
```text
盒子开机
-> kiosk-shell 全屏终端登录/首配页
-> vdt-agent 注册并获取终端身份
-> 用户登录或终端绑定校验
-> 平台返回该用户可用的桌面列表及短期连接信息
-> 用户选择桌面
-> kiosk-shell 按 protocol=rdp|moonlight 启动会话
-> 断开后回桌面列表/登录页
```
## 2. Web 控制台 MVP 目标
第一版目标不是做漂亮的大屏,而是跑通一个可演示、可替换 UI、可接真实后端的后台系统:
1. 用户能登录和退出;
2. 根据角色控制菜单和按钮;
3. 管理员能查看终端、桌面、用户及绑定关系;
4. 能对终端下发命令、对桌面执行开关机/重置;
5. 能查看基础状态、操作结果和审计记录;
6. 后端未完成时使用 Mock API,后端接口上线后不改页面业务逻辑即可切换。
## 3. 技术栈与工程结构
按定版文档执行:Vue 3 + Vite + TypeScript 严格模式 + Element Plus。
建议补充:Pinia(状态)、Vue Router、Axios、Vitest、Playwright、MSW(Mock API)。
```text
console/
├─ src/
│ ├─ api/ # OpenAPI 生成 SDK 的薄封装
│ ├─ assets/
│ ├─ components/ # 领域无关组件
│ ├─ layouts/ # 登录布局、控制台布局
│ ├─ mocks/ # MSW handlers 与演示数据
│ ├─ router/ # 路由、鉴权守卫、RBAC meta
│ ├─ stores/ # auth、app、notification
│ ├─ types/ # 页面 ViewModel;API 类型由生成器管理
│ ├─ views/
│ │ ├─ login/
│ │ ├─ dashboard/
│ │ ├─ terminals/
│ │ ├─ desktops/
│ │ ├─ users/
│ │ ├─ gpu/
│ │ ├─ monitoring/
│ │ ├─ ota/
│ │ └─ audit/
│ ├─ App.vue
│ └─ main.ts
├─ e2e/
├─ public/
├─ .env.development
├─ .env.mock
├─ package.json
└─ vite.config.ts
```
约束:页面不能直接拼 URL 或写 Axios;所有网络调用只经过 `api/`。这样 Mock 与真实后端可无感切换。
## 4. 页面与优先级
### M0:登录与应用框架
- 登录页:用户名、密码、登录按钮、错误状态、加载状态;
- 控制台壳:侧边导航、顶部栏、面包屑、当前账号、退出;
- 401 自动清理会话并回登录;
- 403 权限提示;
- 路由级 RBAC:`super_admin`、`operator`、`viewer`;
- 开发环境使用 Mock 登录,生产环境只调用真实 API。
### M1:核心演示闭环
1. 仪表盘:在线终端、活动会话、运行桌面、告警数量;
2. 终端管理:列表、搜索、状态筛选、详情、重启、重置会话;
3. 桌面管理:列表、状态筛选、开机、关机、重启、重置;
4. 用户与绑定:用户列表、用户详情、终端/桌面绑定与解绑;
5. 审计日志:操作者、动作、目标、结果、时间、请求 ID。
M1 完成后已经可以给项目经理演示完整业务流程。
### M2:完整文档范围
- 桌面池:模板、容量、回收策略;
- GPU 池:节点/PCI 地址/型号/状态/绑定桌面;
- 监控:会话在线数、FPS、RTT、节点水位、告警;
- OTA 中心:版本、批次、灰度比例、进度、失败终端;
- CSV 用户/绑定批量导入向导;
- 配置模板与配置版本下发。
## 5. 登录与权限方案
### 登录接口
```http
POST /api/v1/auth/login
Content-Type: application/json
{
"username": "admin",
"password": "******"
}
```
建议响应:
```json
{
"accessToken": "jwt",
"expiresIn": 3600,
"user": {
"id": "usr_001",
"username": "admin",
"displayName": "系统管理员",
"role": "super_admin"
}
}
```
建议第一版 access token 仅存内存;刷新 token 使用 `HttpOnly + Secure + SameSite` Cookie。若后端尚未支持 refresh,MVP 可暂存 `sessionStorage`,但不要长期使用 `localStorage` 保存高权限令牌。
### 权限矩阵
| 能力 | super_admin | operator | viewer |
|---|---:|---:|---:|
| 查看所有页面 | 是 | 是 | 是 |
| 终端远程命令 | 是 | 是 | 否 |
| 桌面生命周期操作 | 是 | 是 | 否 |
| 用户/绑定修改 | 是 | 否 | 否 |
| GPU 绑定 | 是 | 否 | 否 |
| OTA 发布/灰度 | 是 | 是 | 否 |
| 只读监控/审计 | 是 | 是 | 是 |
前端 RBAC 只是交互约束,真正授权必须由 `api-gateway` 再验证,不能只隐藏按钮。
## 6. 第一版 API 契约
文档已明确的接口优先沿用:
```text
POST /api/v1/auth/login
GET /api/v1/users
POST /api/v1/desktop/assign
POST /api/v1/pool/{id}/action
GET /api/v1/gpu/cards
POST /api/v1/gpu/bind
GET /api/v1/monitor/query
```
为控制台 MVP 补齐建议接口(需后端确认并写入 OpenAPI):
```text
GET /api/v1/auth/me
POST /api/v1/auth/logout
GET /api/v1/terminals
GET /api/v1/terminals/{id}
POST /api/v1/terminals/{id}/commands
GET /api/v1/desktops
GET /api/v1/desktops/{id}
POST /api/v1/desktops/{id}/actions
GET /api/v1/bindings
DELETE /api/v1/bindings/{id}
GET /api/v1/audit-logs
```
所有列表统一查询参数:`page`、`pageSize`、`keyword`、`status`、`sort`;统一响应:
```json
{
"items": [],
"page": 1,
"pageSize": 20,
"total": 0,
"requestId": "req_xxx"
}
```
所有命令型接口必须返回 `commandId`,页面展示“已提交”,再轮询或通过 WebSocket/MQTT 网关显示最终结果,不能把 HTTP 200 当作设备已经执行成功。
## 7. Mock 先行策略
后端还没完成时,前端不应硬编码页面数据。使用 MSW 模拟真实 HTTP:
- 固定 3 个测试账号,对应三角色;
- 终端包含在线/离线/会话中/升级中;
- 桌面包含运行/停止/错误/任务中;
- 所有命令先进入 `pending`,1–3 秒后变成 `success` 或 `failed`;
- 登录失败、401、403、网络超时、空列表都要有测试场景;
- `VITE_API_MODE=mock|real` 切换数据源。
项目经理 UI 稿到后,Mock 数据仍然可用于逐页视觉验收。
## 8. 与终端登录流程的接口边界
管理员 Web 控制台不直接把 VM 密码写进终端,也不让终端调用 Proxmox API。
建议终端用户登录流程由 `auth-svc` 单独提供:
```text
POST /api/v1/terminal/session/login
GET /api/v1/terminal/session/desktops
POST /api/v1/terminal/session/connect-ticket
```
`connect-ticket` 返回短期、一次性连接信息:
```json
{
"desktopId": "desk_001",
"protocol": "moonlight",
"endpoint": "192.168.1.70",
"app": "Desktop",
"display": { "width": 1920, "height": 1080, "fps": 60 },
"ticket": "short-lived-token",
"expiresAt": "2026-10-28T16:30:00+08:00"
}
```
这部分不是 `console/` 页面,但必须和 Web 控制台的“用户/终端/桌面绑定”共享同一套后端数据。
## 9. UI 稿到来前的页面设计原则
第一版只做稳定工作台,不做营销式页面:
- 左侧导航 + 顶部工具栏 + 主内容区;
- 列表优先,详情使用独立页或抽屉;
- 状态使用图标/标签,不依赖单一颜色;
- 危险操作二次确认;
- 所有异步操作显示提交、执行中、成功、失败四态;
- 页面级空态、错误态、无权限态必须完整;
- 颜色、字号、间距集中为设计 token,避免 UI 稿到后逐页改 CSS;
- 表格列、筛选区、表单字段以配置驱动,减少视觉改版成本。
## 10. 验收标准
M1 应达到:
1. 三种角色都能登录,菜单/按钮权限正确;
2. 刷新页面仍能恢复会话,令牌过期自动回登录;
3. 终端、桌面、用户/绑定、审计五条流程可演示;
4. 命令有执行中与最终结果,不出现“点击无反馈”;
5. API 全走统一 SDK/适配层,无页面直连 Axios;
6. Vitest 覆盖 auth store、路由守卫、权限判断;
7. Playwright 覆盖登录、终端命令、桌面操作、用户绑定四条主路径;
8. `npm run lint`、`npm run typecheck`、`npm run test`、`npm run build` 全通过。
## 11. 推荐实施顺序
```text
第 1 步:初始化 console 工程、路由、布局、设计 token
第 2 步:OpenAPI 初稿 + API SDK 生成 + MSW Mock
第 3 步:登录、会话恢复、RBAC、错误处理
第 4 步:终端管理 + 桌面管理
第 5 步:用户与绑定 + 审计日志
第 6 步:仪表盘聚合数据
第 7 步:Playwright E2E 与生产构建
第 8 步:项目经理 UI 稿到后替换视觉层
第 9 步:接真实 Go API,关闭 Mock
```
## 12. 当前决策建议
- 现在先开发 `console/` 的 M0 + M1,不等 UI 稿;
- 同时为 `terminal/kiosk-shell` 恢复“登录/桌面列表后连接”的产品流程,不再把开机直连 Moonlight 当最终形态;
- Web 控制台第一版只接 Mock,API 契约先和后端负责人确认;
- Moonlight/RDP 统一抽象成终端连接票据的 `protocol` 字段,Web 控制台只管理策略,不直接操控串流客户端;
- UI 稿到后只调整布局组件与 design tokens,业务路由、权限、API、测试保留。

249
tools/gpu_dashboard.py Normal file
View File

@ -0,0 +1,249 @@
#!/usr/bin/env python3
"""服务器 GPU 状态看板(部署在 PVE 宿主机上,仅依赖 python3 标准库)。
数据源:
- lspci / /sys / lsmod:GPU 型号、当前驱动(nouveau/vfio-pci/nvidia)、IOMMU 状态
- qm config/status:VM 100 的直通配置(hostpci0)与运行状态
- 直通生效后:qm guest exec 在 VM 100 内执行 nvidia-smi 采集实时数据
(利用率 / 显存 / 温度 / 驱动版本 / NVENC 编码会话)
用法:python3 gpu_dashboard.py [端口] # 默认 8090,浏览器访问 http://<宿主机IP>:8090/
"""
import json
import subprocess
import sys
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
VMID = 100
CACHE_TTL = 2.0
GUEST_TIMEOUT = 25
_lock = threading.Lock()
_cache = {"t": 0.0, "data": None}
def run(cmd, timeout=15):
try:
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
return (r.stdout or "").strip()
except Exception as e:
return "ERROR: %s" % e
def read(path):
try:
with open(path) as f:
return f.read().strip()
except Exception:
return ""
def gpu_identity():
out = run("lspci -nnk -s 01:00.0")
name = ""
driver = ""
for line in out.splitlines():
if "NVIDIA" in line and not name:
idx = line.find("NVIDIA")
name = line[idx:].strip()
if "Kernel driver in use" in line:
driver = line.split(":", 1)[-1].strip()
audio = ""
for line in run("lspci -nnk -s 01:00.1").splitlines():
if "Kernel driver in use" in line:
audio = line.split(":", 1)[-1].strip()
return {
"name": name or "NVIDIA GPU (0000:01:00.0)",
"driver": driver or "(无,未绑定)",
"audio_driver": audio or "(无)",
}
def iommu_state():
# 新内核(6.8+)默认启用 Intel DMAR,无需显式 intel_iommu=on;
# 以 iommu_groups 是否存在且 GPU 已入组为准,内核参数仅作展示。
cmdline = read("/proc/cmdline")
flag = "intel_iommu=on" in cmdline or "iommu=on" in cmdline
groups = run("find /sys/kernel/iommu_groups -maxdepth 1 -mindepth 1 -type d 2>/dev/null | wc -l")
n = int(groups) if groups.isdigit() else 0
gpu_group = run("for d in /sys/kernel/iommu_groups/*/devices/0000:01:00.0; do "
"[ -e \"$d\" ] && dirname \"$d\" | sed 's|.*iommu_groups/||;s|/devices||'; done")
members = run("ls /sys/kernel/iommu_groups/%s/devices/ 2>/dev/null" % gpu_group) if gpu_group else ""
if n == 0:
state, ok = "未启用(iommu 组数为 0,需 BIOS 开 VT-d + 内核参数 intel_iommu=on)", False
elif not gpu_group:
state, ok = "已启用,但 GPU 不在任何组内(异常)", False
else:
state, ok = "已启用(DMAR 生效)", True
return {"state": state, "ok": ok, "kernel_flag": flag, "groups": n,
"gpu_group": gpu_group, "group_members": members}
def passthrough_state():
cfg = run("qm config %d 2>/dev/null" % VMID)
hostpci = ""
for line in cfg.splitlines():
if line.startswith("hostpci"):
hostpci = line
break
status = run("qm status %d 2>/dev/null" % VMID).replace("status: ", "").replace("status:", "").strip()
return {"hostpci": hostpci or "(未配置)", "vm_status": status or "unknown"}
def guest_gpu():
"""直通后在 VM 内经 guest agent 执行 nvidia-smi;未直通/无驱动时返回 None。"""
q = ("nvidia-smi --query-gpu=name,utilization.gpu,memory.used,memory.total,"
"temperature.gpu,driver_version,power.draw --format=csv,noheader")
out = run("qm guest exec %d -- powershell -NoProfile -Command \"%s\"" % (VMID, q),
timeout=GUEST_TIMEOUT)
try:
d = json.loads(out)
except Exception:
return None
if d.get("returncode") != 0:
return None
text = (d.get("out-data") or "").strip()
if not text or "nvidia-smi" in (d.get("err-data") or ""):
return None
parts = [p.strip() for p in text.split(",")]
if len(parts) < 6:
return None
info = {
"name": parts[0],
"utilization": parts[1],
"vram_used": parts[2],
"vram_total": parts[3],
"temperature": parts[4],
"driver": parts[5],
"power": parts[6] if len(parts) > 6 else "",
}
enc = run("qm guest exec %d -- powershell -NoProfile -Command \""
"nvidia-smi --query-gpu=encoder.stats.sessionCount --format=csv,noheader\"" % VMID,
timeout=GUEST_TIMEOUT)
try:
d2 = json.loads(enc)
v = (d2.get("out-data") or "").strip()
if d2.get("returncode") == 0 and v:
info["enc_sessions"] = v
except Exception:
pass
return info
def collect():
ident = gpu_identity()
driver = ident["driver"]
if "vfio-pci" in driver:
stage = "vfio 已接管(可直通/已解绑 nouveau)"
elif "nouveau" in driver:
stage = "未直通(GPU 由宿主 nouveau 占用,直通前需解绑)"
elif "nvidia" in driver:
stage = "未直通(GPU 由宿主 nvidia 驱动占用)"
else:
stage = "GPU 未绑定驱动"
data = {
"time": time.strftime("%Y-%m-%d %H:%M:%S"),
"gpu": ident,
"stage": stage,
"iommu": iommu_state(),
"pt": passthrough_state(),
"guest": guest_gpu() if "hostpci" in passthrough_state()["hostpci"] else None,
}
return data
def get_data():
with _lock:
now = time.time()
if _cache["data"] is not None and now - _cache["t"] < CACHE_TTL:
return _cache["data"]
data = collect()
_cache["data"] = data
_cache["t"] = now
return data
HTML = """<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8">
<title>服务器 GPU 状态</title>
<style>
body { background:#0f1420; color:#e6e9ef; font-family:"Microsoft YaHei",sans-serif;
margin:0; padding:32px; }
h1 { font-size:22px; margin:0 0 4px; }
.sub { color:#8a94a6; font-size:12px; margin-bottom:24px; }
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:14px; max-width:1100px; }
.card { background:#1a2233; border:1px solid #2b3654; border-radius:10px; padding:16px 18px; }
.card h3 { margin:0 0 10px; font-size:13px; color:#8a94a6; font-weight:normal; }
.card .v { font-size:15px; line-height:1.7; word-break:break-all; }
.ok { color:#3fb950; } .bad { color:#e05561; } .warn { color:#d29922; }
.big { font-size:30px; font-weight:bold; }
</style></head><body>
<h1>服务器 GPU 状态</h1>
<div class="sub" id="time">加载中…(每 4 秒自动刷新)</div>
<div class="grid">
<div class="card"><h3>显卡</h3><div class="v" id="gpu">-</div></div>
<div class="card"><h3>直通阶段</h3><div class="v" id="stage">-</div></div>
<div class="card"><h3>IOMMU / VT-d</h3><div class="v" id="iommu">-</div></div>
<div class="card"><h3>VM %d 直通配置</h3><div class="v" id="pt">-</div></div>
<div class="card"><h3>VM 内实时数据(nvidia-smi)</h3><div class="v" id="guest">直通生效后显示</div></div>
</div>
<script>
const $ = id => document.getElementById(id);
function esc(s){ return String(s).replace(/</g,"&lt;"); }
function render(d){
$("time").textContent = "更新于 " + d.time + "(每 4 秒自动刷新)";
$("gpu").innerHTML = "<b>" + esc(d.gpu.name) + "</b><br>当前驱动:" + esc(d.gpu.driver) +
"<br>HDMI 音频驱动:" + esc(d.gpu.audio_driver);
const stColor = d.stage.includes("未直通") ? "warn" : "ok";
$("stage").innerHTML = "<span class='" + stColor + "'>" + esc(d.stage) + "</span>";
$("iommu").innerHTML = (d.iommu.ok ? "<span class='ok'>" : "<span class='bad'>") +
esc(d.iommu.state) + "</span><br>内核参数 intel_iommu=on:" +
(d.iommu.kernel_flag ? "已设置" : "未设置(新内核默认开启,非必须)") +
(d.iommu.gpu_group ? "<br>GPU 所在组:" + esc(d.iommu.gpu_group) +
(d.iommu.group_members ? "<br>组内设备:" + esc(d.iommu.group_members) : "") : "");
$("pt").innerHTML = "运行状态:" + esc(d.pt.vm_status) + "<br>hostpci:" + esc(d.pt.hostpci);
if (d.guest) {
$("guest").innerHTML =
"<span class='big'>" + esc(d.guest.utilization) + "</span> 利用率<br>" +
"显存:" + esc(d.guest.vram_used) + " / " + esc(d.guest.vram_total) + "<br>" +
"温度:" + esc(d.guest.temperature) +
(d.guest.power ? " 功耗:" + esc(d.guest.power) : "") + "<br>" +
"驱动:" + esc(d.guest.driver) +
(d.guest.enc_sessions !== undefined ? "<br>NVENC 编码会话:" + esc(d.guest.enc_sessions) : "");
} else {
$("guest").innerHTML = "<span class='warn'>直通生效并在 VM 内装好驱动后,这里显示实时利用率/显存/温度/NVENC 会话</span>";
}
}
function tick(){ fetch("/api").then(r=>r.json()).then(render).catch(()=>{}); }
tick(); setInterval(tick, 4000);
</script></body></html>""" % VMID
class Handler(BaseHTTPRequestHandler):
def _send(self, code, body, ctype):
raw = body.encode("utf-8")
self.send_response(code)
self.send_header("Content-Type", ctype + "; charset=utf-8")
self.send_header("Content-Length", str(len(raw)))
self.end_headers()
self.wfile.write(raw)
def do_GET(self):
if self.path.startswith("/api"):
self._send(200, json.dumps(get_data(), ensure_ascii=False), "application/json")
elif self.path.startswith("/healthz"):
self._send(200, "ok", "text/plain")
else:
self._send(200, HTML, "text/html")
def log_message(self, *args):
pass
if __name__ == "__main__":
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8090
srv = ThreadingHTTPServer(("0.0.0.0", port), Handler)
print("GPU dashboard on http://0.0.0.0:%d/ (VM %d)" % (port, VMID), flush=True)
srv.serve_forever()

View File

@ -0,0 +1,29 @@
#!/bin/sh
set -u
LOG_DIR="$HOME/vdi-terminal/logs"
LOG_FILE="$LOG_DIR/overnight-$(date +%Y%m%d).log"
mkdir -p "$LOG_DIR"
log() { printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S %z')" "$*" >> "$LOG_FILE"; }
log "monitor_start pid=$$ interval=60s"
while :; do
now=$(date '+%Y-%m-%d %H:%M:%S %z')
shell_pid=$(pgrep -f '^/home/armbian/vdi-terminal/bin/kiosk-shell$' | head -n 1 || true)
monitor_pid=$(pgrep -f '^/home/armbian/vdi-terminal/bin/vdi-kiosk-monitor' | head -n 1 || true)
if [ -n "$shell_pid" ]; then
ps_line=$(ps -o pcpu=,pmem=,rss=,etime= -p "$shell_pid" | tr '\n' ' ')
else
ps_line="missing"
fi
temp=$(awk '{printf "%.1f", $1/1000}' /sys/class/thermal/thermal_zone0/temp 2>/dev/null || printf "unknown")
mem=$(awk '/MemTotal:|MemAvailable:|SwapFree:/{printf "%s=%sKB ",$1,$2}' /proc/meminfo 2>/dev/null || true)
heartbeat="missing"
if [ -f /tmp/vdi-kiosk.heartbeat ]; then
heartbeat=$(stat -c '%Y' /tmp/vdi-kiosk.heartbeat 2>/dev/null || printf 'unknown')
fi
rdp=$(ss -tnp 2>/dev/null | grep '192.168.1.70:3389' | tr '\n' ';' || true)
starts=$(grep -c '主进程已启动' /tmp/vdi-kiosk-monitor.log 2>/dev/null || printf 0)
exits=$(grep -c '主进程已退出' /tmp/vdi-kiosk-monitor.log 2>/dev/null || printf 0)
crashes=$(grep -ciE 'segmentation fault|bus error|double free|invalid pointer|aborted|terminate called' /tmp/vdi-kiosk-monitor.log 2>/dev/null || printf 0)
log "sample now=$now shell_pid=${shell_pid:-missing} monitor_pid=${monitor_pid:-missing} ps=[$ps_line] temp_c=$temp heartbeat_epoch=$heartbeat rdp=[$rdp] starts=$starts exits=$exits crashes=$crashes $mem"
sleep 60
done

89
tools/pve_term_exec.py Normal file
View File

@ -0,0 +1,89 @@
"""Run a command on the PVE node via the same VNC term channel the web console uses.
Usage: python3 pve_term_exec.py "<command>"
Reads ticket from /tmp/pve_t.txt (refreshed automatically if expired).
Prints the command output captured from the terminal stream.
"""
import base64
import json
import ssl
import struct
import sys
import time
import urllib.parse
import urllib.request
HOST = "https://192.168.1.51:8006"
NODE = "starhe"
CTX = ssl.create_default_context()
CTX.check_hostname = False
CTX.verify_mode = ssl.CERT_NONE
def http(path, data=None, ticket=None, csrf=None):
body = urllib.parse.urlencode(data).encode() if data is not None else None
req = urllib.request.Request(HOST + path, body)
if ticket:
req.add_header("Cookie", "PVEAuthCookie=" + ticket)
if csrf:
req.add_header("CSRFPreventionToken", csrf)
with urllib.request.urlopen(req, context=CTX, timeout=30) as r:
raw = r.read()
return json.loads(raw)["data"] if raw else None
def login():
d = http("/api2/json/access/ticket",
{"username": "root@pam", "password": "yx66668888"})
return d["ticket"], d["CSRFPreventionToken"]
def run_cmd(command, timeout=60):
ticket, csrf = login()
# 1. open a term proxy (VNC channel to node shell)
r = http("/api2/json/nodes/%s/termproxy" % NODE, {}, ticket, csrf)
port, ticket_vnc = r["port"], r["ticket"]
user, realm_sep, _ = "root", "@", "pam"
import socket
sock = socket.create_connection(("192.168.1.51", port), timeout=10)
# PVE handshake: username@realm\n ticket\n binary handshake
sock.sendall(b"root@pam\n" + ticket_vnc.encode() + b"\n")
gh = sock.recv(16)
maj, mino = struct.unpack("BB", gh[0:2])
said = sock.recv(16 * 4)
# client sends its own handshake
ch = struct.pack("BB", maj, mino) + b"PVEVNC Client 00" + b"\x00" * 0
sock.sendall(ch)
# 2. run the command in the shell by typing it and reading output
marker = "PVEDONE%s" % int(time.time())
payload = command + " ; echo " + marker + "\n"
time.sleep(0.5)
sock.sendall(payload.encode())
buf = b""
deadline = time.time() + timeout
while time.time() < deadline:
try:
sock.settimeout(2)
chunk = sock.recv(65536)
if not chunk:
break
buf += chunk
if marker.encode() in buf.replace(b"\x1b", b"").replace(b"[?2004l", b""):
break
except socket.timeout:
if marker.encode() in buf:
break
continue
sock.close()
# strip terminal control chars roughly and the echoed command
text = buf.decode("utf-8", "replace")
text = text.replace(marker, "")
lines = [l for l in text.split("\r\n")]
return "\n".join(lines)
if __name__ == "__main__":
print(run_cmd(sys.argv[1]))

55
tools/pve_vm.py Normal file
View File

@ -0,0 +1,55 @@
import json
import ssl
import sys
import urllib.parse
import urllib.request
HOST = "https://192.168.1.51:8006"
CTX = ssl.create_default_context()
CTX.check_hostname = False
CTX.verify_mode = ssl.CERT_NONE
def api(path, data=None, ticket=None, csrf=None, json_body=None):
body = None
headers = {}
if json_body is not None:
body = json.dumps(json_body).encode()
headers["Content-Type"] = "application/json"
elif data is not None:
body = urllib.parse.urlencode(data).encode()
req = urllib.request.Request(HOST + path, body, headers)
if ticket:
req.add_header("Cookie", "PVEAuthCookie=" + ticket)
if csrf:
req.add_header("CSRFPreventionToken", csrf)
with urllib.request.urlopen(req, context=CTX, timeout=40) as r:
raw = r.read()
return json.loads(raw)["data"] if raw else None
def agent(ticket, csrf, command, timeout=90):
r = api("/api2/json/nodes/starhe/qemu/100/agent/exec",
json_body={"command": command}, ticket=ticket, csrf=csrf)
pid = r["pid"]
for _ in range(timeout // 2):
import time
time.sleep(2)
st = api("/api2/json/nodes/starhe/qemu/100/agent/exec-status?pid=%d" % pid,
ticket=ticket)
if st.get("exited"):
return st.get("out-data") or "", st.get("err-data") or ""
return "TIMEOUT", ""
if __name__ == "__main__":
d = api("/api2/json/access/ticket",
{"username": "root@pam", "password": "yx66668888"})
ticket, csrf = d["ticket"], d["CSRFPreventionToken"]
ps = sys.argv[1]
out, err = agent(ticket, csrf, [
"C:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe",
"-NoProfile", "-Command", ps])
print(out)
if err:
print("ERR:", err[:300])

View File

@ -8,6 +8,10 @@ os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..", "terminal", "kiosk-shell"))
FILES = [
"src/desktop_settings_dialog.h",
"src/desktop_settings_dialog.cpp",
"src/desktop_store.h",
"src/desktop_store.cpp",
"src/main_window.cpp",
"src/main_window.h",
"src/desktop_list_page.cpp",
@ -17,6 +21,8 @@ FILES = [
"src/desktop_session.h",
"src/moonlight_session.cpp",
"src/moonlight_session.h",
"src/transition_overlay.h",
"src/transition_overlay.cpp",
"src/login_page.cpp",
"src/login_page.h",
"src/state_machine.cpp",

84
tools/xwd2png.py Normal file
View File

@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Convert an XWD dump (from xwd) into a PNG, without external image libraries.
Usage: xwd -root -silent | python3 xwd2png.py out.png
"""
import struct
import sys
import zlib
def read_header(data):
fields = struct.unpack(">25I", data[:100])
names = [
"header_size", "file_version", "pixmap_format", "pixmap_depth",
"pixmap_width", "pixmap_height", "xoffset", "byte_order",
"bitmap_unit", "bitmap_bit_order", "bitmap_pad", "bits_per_pixel",
"bytes_per_line", "visual_class", "red_mask", "green_mask",
"blue_mask", "bits_per_rgb", "colormap_entries", "ncolors",
"window_width", "window_height", "window_x", "window_y",
"window_bdrwidth",
]
return dict(zip(names, fields))
def write_png(path, width, height, rgb_rows):
raw = b"".join(b"\x00" + row for row in rgb_rows)
def chunk(tag, payload):
return (struct.pack(">I", len(payload)) + tag + payload +
struct.pack(">I", zlib.crc32(tag + payload) & 0xFFFFFFFF))
header = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)
png = (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", header) +
chunk(b"IDAT", zlib.compress(raw, 6)) + chunk(b"IEND", b""))
with open(path, "wb") as handle:
handle.write(png)
def main():
out_path = sys.argv[1] if len(sys.argv) > 1 else "out.png"
data = sys.stdin.buffer.read()
head = read_header(data)
width = head["pixmap_width"]
height = head["pixmap_height"]
bpp = head["bits_per_pixel"]
stride = head["bytes_per_line"]
offset = head["header_size"] + head["ncolors"] * 12
pixels = data[offset:]
if bpp == 32:
rows = []
for y in range(height):
line = pixels[y * stride:y * stride + width * 4]
out = bytearray(width * 3)
for x in range(width):
src = x * 4
dst = x * 3
out[dst] = line[src + 2]
out[dst + 1] = line[src + 1]
out[dst + 2] = line[src]
rows.append(bytes(out))
elif bpp == 24:
rows = []
for y in range(height):
line = pixels[y * stride:y * stride + width * 3]
out = bytearray(width * 3)
for x in range(width):
src = x * 3
dst = x * 3
out[dst] = line[src + 2]
out[dst + 1] = line[src + 1]
out[dst + 2] = line[src]
rows.append(bytes(out))
else:
sys.stderr.write("unsupported bits_per_pixel=%s\n" % bpp)
return 2
write_png(out_path, width, height, rows)
print("%s %dx%d" % (out_path, width, height))
return 0
if __name__ == "__main__":
sys.exit(main())