vdi-cloud/terminal/native-cursor/README.md

65 lines
9.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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开发包:
```sh
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`:
```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 路径及测量边界见 [本轮实施证据](../../docs/evidence/linux-gpu-20261111/README.md)。
重启显示管理器可能使临时 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,避免旧客户端连入无光标捕获。
测试结论与边界见[云桌面说明](../../docs/desktop-guide.md#cursor),完整测试与失败实验原文见[历史归档](../../docs/archive/README.md)中的 `linux-native-cursor-20261105.md`;最新部署 SHA 见[终端说明](../../docs/terminal-guide.md#build)。本地边界单测:`python -m unittest discover -s tools/tests -p test_native_cursor.py -v`。