Rust 桌面工具开发踩坑:从 TLS 指纹到系统托盘
本文汇总多个 Rust 桌面工具(余额监控、壁纸应用、命令行 AI 助手)开发中积累的踩坑记录,覆盖 TLS 与网络、MSRV 兼容性、Win32/系统集成、SQLite 与进程管理几个方向。这些项目普遍有”支持老旧系统”与”跨 Windows/Linux”的硬约束,坑尤其密集。
一、TLS 指纹:不是所有 TLS 都一样
现象:某个 HTTPS 状态接口永远连接失败(Connection reset by peer (os error 104)),但 curl/Go 请求同一接口完全正常。
根因:该接口要求 TLS 握手的第一个密钥交换组为后量子混合组 X25519MLKEM768,而旧版 rustls(0.21,随 reqwest 0.11)不支持——curl/Go 能通只是 TLS 指纹不同。
修复:升级 rustls 0.23 + prefer-post-quantum 特性 + reqwest 0.12(aws-lc-rs 后端不需要 cmake,Windows 自带 NASM)。
Windows 兼容的反向坑:为支持 Win7 改用 rustls + webpki-roots 后,企业 MITM 代理/HTTPS 扫描杀软会校验失败——老系统兼容与网络环境兼容有时是互斥的,需要按用户群体取舍。
二、MSRV 钉子:依赖树的暗雷
固定旧版 Rust(如 1.77.2)的项目,依赖升级风暴随时可能引爆:
- edition2024 传染:
icu_normalizer新版本要求 edition2024(Cargo 1.85+),它位于reqwest → url → idna → idna_adapter链上。cargo generate-lockfile不校验 manifest,本机生成”成功”不代表能编译; - 一次连环钉死了 9 个依赖版本(idna_adapter、home、indexmap、time、litemap、zerofrom、jobserver、unicode-segmentation、winresource);
generate-lockfile会重置手工钉子——版本钉子必须写进Cargo.toml的依赖声明(=版本),不能只靠 lock;- 缺少目标平台工具链时(如本机是 Linux、只出 Windows 包),用
cargo check+cargo fetch --locked覆盖跨平台依赖超集,把大部分 MSRV/edition 问题在本地消掉。
三、Win32 原生控件(native-windows-gui)的坑
LabelFlags::NONE等于隐形空白:编译能过、界面全白——所有 Label 必须显式VISIBLE;- “能显示窗口但一秒自动关闭”:
RefCell借用嵌套(借用守卫未释放又去加载预览)导致崩溃——先取状态快照释放借用再分步操作;同时加 panic 钩子写日志,崩溃现场必入日志; - 语言切换崩溃实为死锁:持有配置锁时调用会再次加同一把不可重入
std::sync::Mutex的函数,UI 线程永久挂起——先在独立作用域释放锁再执行; - 高 DPI 缩放:manifest 声明 DPI 感知后按物理像素渲染,固定布局在 125%/150% 屏上过小——用
GetDeviceCaps(LOGPIXELSX)查询 DPI,以 96 为基准线性缩放全部坐标; - “无法定位程序输入点 GetWindowSubclass”:exe 缺 Common-Controls v6 manifest,老系统加载的是 system32 的旧版 comctl32。修复:
build.rs嵌入 manifest,声明 Common Controls 6.0 + supportedOS GUID + DPI 感知。
四、系统托盘与窗口生命周期(跨平台)
- 托盘图标创建失败被静默吞掉:
Shell_NotifyIconW失败只等 TaskbarCreated 广播——“进程在跑、没有图标”。需要重试与兜底; - 气泡通知的编号偏移:builder 会占用一个编号,通知必须按 2 指认,写死 1 则永远不出现;
- 开机自启早于外壳:窗口自藏会变成隐形——应等托盘确实持有(最多 15 秒)再隐藏;
- Wayland 限制:
set_visible空实现、禁止取消最小化、无输入序列号的后台激活被 KWin 拒绝——默认策略选prefer_x11(); - X11 图标尺寸上限:
_NET_WM_ICON单次属性请求上限 65535 个 32 位字,256×256 需要 65538 → 静默失败窗口无图标,改用 128px; - 托盘库的默认 feature 会拉入完整 Tokio:无 Tokio runtime 时 panic——用
default-features = false+async-io。
五、SQLite 并发与迁移
- 无
busy_timeout/WAL 时多线程报database is locked; - 迁移竞态:
ensure_window_column先查后 ALTER,升级后首启时监控线程与界面线程同时 ALTER → 一边报duplicate column name、open_db返回 Err → 表现像”密钥丢失”,重启即好。迁移要用幂等 ALTER(先探测列)并串行化; - 迁移顺序:ALTER 迁移排在 CREATE TABLE 之前 → 全新安装报
no such table; - 测试写真实目录是灾难:crypto 测试直接改写用户状态目录里的密钥文件,而搬迁规则”目标已存在就跳过”→ 密钥解不开像数据全丢。修复:测试状态/配置指向
/tmp/<app>-tests-<pid>/,新测试必须先调”隔离目录”辅助函数。
六、进程清理的两种误伤
1 | # 误杀同名旧版本进程 |
七、文件路径安全
从远程返回的 ID 直接拼路径可写/删任意文件(路径遍历,高危)。三层拦截:
- ID 白名单校验(ASCII、拒绝
..、长度 ≤128); - 路径边界断言(拼接结果必须在目标目录内);
- 下载限额(大小上限 + Content-Length 预检 + 强制 HTTPS + 图片 magic bytes 校验)。
八、CI 与构建的补充教训
- CI 只有 fmt+build、没有
cargo test:安全回归单测从未在任何环境执行过,补上门禁后首跑即失败(期望值漏目录段、硬编码/分隔符在 Windows 不成立); - 双架构产物同名:x64/x86 产物同名会在 Release 上互相覆盖,构建后先重命名;
- 本地能过、CI 失败的常见原因:陈旧
*.tsbuildinfo(大量 TS6305 报错,清理即过)、本地残留的构建产物造成假象。
总结
Rust 桌面工具开发的坑集中在”边界条件“:老旧系统、企业网络环境、高 DPI、跨平台窗口系统、多线程数据库访问——每一项都有隐藏的窄路径。策略是:把兼容性约束写进 CI 门禁、把平台差异封装成显式的探测与降级,而不是”出问题再补”。