vdi-cloud/terminal/native-cursor
2026-11-11 18:46:16 +08:00
..
vendor 功能:完善通用云管工作台、审计与诊断能力 2026-11-11 18:46:16 +08:00
capture-no-cursor.c 功能:完善通用云管工作台、审计与诊断能力 2026-11-11 18:46:16 +08:00
client.py 功能:接入Linux原生光标并完善设备接入工具 2026-11-05 19:18:54 +08:00
host.py 功能:完善通用云管工作台、审计与诊断能力 2026-11-11 18:46:16 +08:00
kms.py 功能:接入Linux原生光标并完善设备接入工具 2026-11-05 19:18:54 +08:00
README.md 功能:完善通用云管工作台、审计与诊断能力 2026-11-11 18:46:16 +08:00
ubuntu-kms.example.json 功能:接入Linux原生光标并完善设备接入工具 2026-11-05 19:18:54 +08:00
vdi_native_cursor.h 功能:接入Linux原生光标并完善设备接入工具 2026-11-05 19:18:54 +08:00

Linux 原生独立光标(X11 / 单座席 / 2K)

用于当前盒子 .188 的 Ubuntu .72 和麒麟 .71。主机光标图像、热点、动画帧和透明隐藏状态通过认证 SSH 同步;位置由盒子的 X11 原生指针立即显示。没有改系统光标主题,也没有用固定本地箭头替代原生形状。点击、拖窗内容和视频编码延迟仍存在。

本实现绕过两个不同 Sunshine 版本的大型重编译,使用按精确 SHA256 固定的私有副本和捕获适配器。这是本项目的受限接入方式,不是上游 Sunshine/Moonlight 已支持的协议能力。后续要支持其他捕获后端、相对鼠标或多个并发客户端,应扩展双方正式控制协议。

生命周期

  1. vdi-moonlight-rkmpp 调用 client.py。没有对应主机 JSON 配置,或 VDI_NATIVE_CURSOR=0,直接 exec 原客户端。Windows 没有该配置。启用时先核验客户端 SHA256,再获取远端租约;旧客户端或未复核的新构建会在修改主机服务前被拒绝。
  2. 客户端使用各主机独立密钥、固定 known_hosts、BatchMode 和严格主机校验打开 SSH。服务器 authorized_keys 使用 restrict,from="192.168.1.188",command="/usr/bin/python3 .../host.py stream",不提供任意 shell。
  3. host.py stream 获取独占 flock,检查版本、随机租约、单屏尺寸与光标边界;启动用户级 guard。guard 校验私有 Sunshine 和适配器 SHA256,停止原服务,在 /run/user/UID/systemd/user/ 创建临时 drop-in,再以适配器启动私有副本。
  4. capture-no-cursor.c 只在该 Sunshine 进程的 XFixesGetCursorImage 捕获结果中清零像素,不修改 X server 的实际光标、全局主题或系统库。adapter 的 dlsym receipt 和 /proc/PID/maps 都确认后才返回 READY,随后才允许新视频连接。因此所有新视频帧从一开始都不含光标,不在旧视频仍显示光标时直接切换。
  5. SSH 消息包含版本、128位随机租约、递增序号、尺寸、热点、RGBA 像素、源分辨率和输入确认。最多128×128像素;坏包、错误尺寸、重复序号或超时结束当前串流。客户端/bridge状态等待上限2秒,主机租约4秒;短暂停顿期间保留上次形状,本地指针位置继续移动。源几何变化要求重新连接。
  6. 盒子监督进程写入0700私有目录的原子状态文件。SDL主线程每约4ms检查状态,只在形状/缩放改变时创建和释放光标;X11原生指针移动不等该轮询或视频帧。SDL2 X11后端直接复制Xcursor像素,需要预乘alpha,已单独适配。
  7. 本地输入序号和目标坐标通过SSH回传;服务器实际指针到达该位置后确认。只在最新输入已确认、没有按键按下且本地静止500ms后接受服务器主动warp,避免迟到的位置回拉正在移动的光标。触摸/相对游戏模式不在本接入范围。
  8. 客户端保留原PID(fork监督进程后exec),正常kiosk窗口激活仍能识别。退出、父进程被kill或SSH异常时结束租约。guard先停止无光标生产进程,再删除临时drop-in并启动原服务;原视频连接不会被新普通生产进程接续。
  9. guard本身被kill时,systemd ExecStopPost仍执行恢复。重启后 /run 中的drop-in自然不存在;原已启用服务仍正常启动。没有启用全局LD_PRELOAD或永久无光标模式。

构建和安装

  • 客户端补丁:../moonlight/patches/0005-vdi-native-linux-cursor.patch;由 tools/build-rkmpp-client.sh 在0001~0004之后应用。头文件源码保留在本目录,修改时必须同步补丁中的新增头文件。
  • tools/deploy_kiosk.sh 部署 client.py 为 ~/vdi-terminal/bin/vdi-native-cursor。配置/密钥不会由普通kiosk部署脚本生成。
  • 两个客体的私有目录:~/vdi-terminal/native-cursor-v1/,包含 host.py、capture-no-cursor.c/.so、无额外file capability的原版Sunshine副本,以及 manifest.json。原系统二进制/能力保持原值。
  • .so在麒麟glibc2.31/GCC9上编译,两台复用。最小编译不依赖缺失的libc开发包:
gcc -shared -nostdlib -fPIC -O2 -Wall -Wextra -Werror -Wl,-z,defs \
  capture-no-cursor.c /lib/x86_64-linux-gnu/libdl.so.2 \
  /lib/x86_64-linux-gnu/libc.so.6 -o capture-no-cursor.so

麒麟仅对本次 host.py、私有 sunshine 和 .so 做 kysec_set -n exectl -v verified 单文件认证;源码变更后须重新认证。没有关闭KYSEC。Ubuntu原文件具有capabilities,安全加载器会忽略LD_PRELOAD,因此仅租约内使用相同SHA的无capability私有副本;X11捕获无需SYS_ADMIN,启动时其他捕获后端的能力探测告警不代表X11失败。

用户级 vdi-native-cursor-guard.service 的ExecStart为 /usr/bin/python3 <目录>/host.py guard,ExecStopPost为同脚本的 recover;Type=simple,TimeoutStopSec=50。服务按需启动,不enable。manifest.json仅包含两个键 sunshine、capture-no-cursor.so,值为部署文件的SHA256。

盒子 ~/.config/vdi-terminal/native-cursor/<IP>.json:

{"host":"192.168.1.72","user":"test2","key":"/绝对路径/该主机.key","known_hosts":"/绝对路径/该主机.known_hosts","size":[2560,1440],"client_sha256":"5d4330f703d7f8e053bc3795ca42bdaf231c9e9dd8ebb00a612013adbcccee03"}

每台主机单独的ed25519私钥和known_hosts留在盒子配置目录(目录700、私钥600),不进仓库。known_hosts是在工作站用用户提供的SSH凭据成功认证后取得的真实主机公钥,没有在运行时自动接受新主机密钥。JSON和密钥应在已验证0005的客户端部署后才启用。上例SHA为2026-11-05现场构建;重新构建后须先停用profile、完成对应验证,再更新SHA并启用,不能把旧值机械替换为任意新文件的SHA。

NVIDIA 硬件采集(2026-11-11)

当前两台 Linux 的现场部署已切换为 GTX 965M 渲染、NvFBC/CUDA 采集与 NVENC H.264 编码;不再沿用上文历史 X11 软件编码基线。使用兼容 Maxwell 的 Sunshine v2025.924.154138;不得仅更新主二进制而不复测 CUDA 架构支持和 pinned manifest。

Ubuntu 的 NVIDIA→modesetting PRIME 路径会把服务端软件光标提前画进画面,仅设 NvFBC bWithCursor=false 仍会出现本地光标拖着视频光标。该主机在 host.py 同目录启用空标记文件 hide-server-cursor:捕获 guard 用独立 X11 连接持有 XFixesHideCursor,原生光标形状仍由另一个连接通过 XFixesGetCursorImage 读取。结束或异常退出时先停 Sunshine,再显式恢复并关闭隐藏连接;X server 也会在连接断开时释放该客户端的隐藏状态。标记仅在 Ubuntu 现场启用,麒麟保持原路径。撤销须先退出串流、停 guard,再移除标记后重连,不在同一串流中途切换。

适配器编译增加 -DVDI_NVFBC -Ivendor。麒麟缺少 libc 开发启动对象时另加 -ffreestanding -nostdlib,并显式链接系统版本化 libc/libdl。vendor/NvFBC.h 保留上游 MIT 许可。拦截 NvFBC 函数表时检查 SDK 参数版本,仅在租约内关闭视频内光标,不修改调用方参数;nvfbc-ready-v1 是加载就绪凭据,nvfbc-v1 才表示实际捕获会话创建成功。

麒麟的 Optimus 卡没有可用物理输出,旧内核 PRIME 显存导入也失败,因此显式启用 VDI_NVFBC_HEADLESS=1。适配器只在驱动报告支持采集、没有 RandR 输出且真实 X screen 为 2560×1440 时枚举整屏,标记 RandR 不可用,使 Sunshine 使用 NVFBC_TRACKING_SCREEN。它不生成真实显示输出,也不把不可采集屏幕报告为可用。UKUI 已目视验证桌面正常;Ubuntu GNOME 在相同无输出条件下黑屏,已恢复 NVIDIA + modesetting PRIME 输出,不能直接照搬麒麟配置。

麒麟使用 /opt/vdi-sunshine-maxwell/runtime/ 的私有动态加载器和依赖库运行该 Sunshine,不替换系统 glibc;可执行文件、适配器和新增脚本按文件进行 KYSEC verified 认证。GPU 路径及测量边界见 本轮实施证据。

重启显示管理器可能使临时 guard 单元消失而遗留旧 ready/receipt;新租约在获得独占锁并确认旧 guard 停止后清理这两个标记,再等待新会话凭据。test_native_cursor_host.py 在实际 Ubuntu Linux 上覆盖该恢复情形及不完整 manifest 拒绝;run_nvfbc_adapter.py 配合 nvfbc_adapter_stub.c 在麒麟实际 glibc 上覆盖函数表 ABI、光标与整屏枚举边界。

停用与回退

退出当前Linux串流,等guard inactive且原vdi-sunshine active。将对应 <IP>.json 改名为 .disabled,下次从同一正常列表连接即使用原视频光标。临时诊断也可设置 VDI_NATIVE_CURSOR=0,但必须在当前租约结束、原服务恢复之后重连。

紧急恢复可在客体执行 systemctl --user stop vdi-native-cursor-guard.service,检查 /run/user/UID/systemd/user/vdi-sunshine.service.d/90-native-cursor.conf 已移除、MainPID路径为原二进制。盒子旧客户端和wrapper备份在 ~/vdi-terminal/backups/native-cursor-20261105/。仅恢复旧二进制前必须先停用两个主机JSON,避免旧客户端连入无光标捕获。

测试结论与边界见云桌面说明,完整测试与失败实验原文见历史归档中的 linux-native-cursor-20261105.md;最新部署 SHA 见终端说明。本地边界单测:python -m unittest discover -s tools/tests -p test_native_cursor.py -v。