只读分析:架构与并发模型、34 项问题清单(P0/P1/P2 分级 + 可信度标注)、发布链路/OTA 信任模型/性能专项、测试覆盖评估、README 差异、修复路线图;0.4/0.5 节记录两批修复与验证证据。
50 KiB
MQTTX(GPUI 重写版)项目深度分析报告
- 分析对象:
mqttx工作区(唯一成员gpui-app),HEAD =bc9f55c("refactor(ui): 拆分 app/connection_view/widgets 为子组件"),工作区干净- 分析日期:2026-09-30
- 分析性质:分析阶段全程只读(未修改/创建/删除任何源文件)。分析完成后按第 0.4 节实施了批次 0 修复,改动清单与验证证据见该节
- 验证环境:Windows / rustc 1.101.0-nightly (d080e7dff 2026-09-27) / cargo 1.101.0-nightly
- 方法:全量通读
gpui-app/src48 个源文件 + CI/发布脚本/依赖清单;三路并行模块复审(OTA 与阿里云 / UI 表单与对话框 / 支撑模块与工程化);对复审结论逐条回归源码复核- 可信度标注约定:
- ✅ 实测量:本机实际运行命令得到的结论
- ✅ 代码读取:直接读到源码或依赖源码得到的结论
- ⚠️ 依赖语义:结论依赖第三方库的文档/源码语义,未做实验
- 🟡 需实测:无法在离线环境判定,必须用真实环境或官方测试向量核对
- ❌ 未采信:证据不足或存在相反证据,本报告不作为缺陷传递
目录
- 0. 摘要
- 1. 项目画像
- 2. 架构分析
- 3. 模块点评
- 4. 值得肯定的设计
- 5. 问题清单
- 6. 专项分析
- 7. 测试与质量保障
- 8. README 与实现不一致清单
- 9. 修复路线图
- 10. 附录
0. 摘要
0.1 一句话结论
代码底子明显高于平均线,但工程化外圈是全项目最脆弱的部分。 分层清晰、竞态处理有针对性设计、持久化语义正确;然而 CI 从未在 Linux 上跑通过、发布默认路径会造出非 semver 版本、Windows 产物靠本地脚本手工补、OTA 的信任边界只有 TLS 一道。当前最大的问题不是"写得对不对",而是"发不出去、也升不上来"。
0.2 最紧急的 5 件事(批次 0,约半天)
| # | 事项 | 证据 | 后果 |
|---|---|---|---|
| 1 | Linux 单测确定性失败 + test job 缺图形库依赖 | sysmon.rs:194 + release.yml:39-46 | CI 永远红,无任何产物 —— ✅ 已修复(0.4) |
| 2 | 发布脚本用 git describe --tags --always 当版本 |
publish-gitea.sh:71-78 | 造出 v1.0.0-4-gbc9f55c 这类 Release,客户端判定"不新",且遮蔽真更新 —— ✅ 已修复(0.4) |
| 3 | 版本双真源(tag vs CARGO_PKG_VERSION)无校验 |
Cargo.toml:3 + release.yml:33 | 忘 bump → 装完仍自报旧版 → 无限提示更新 —— ✅ 已修复(0.4) |
| 4 | Release 创建/上传失败被静默吞掉(exit 0) |
publish-gitea.sh:174-181 | job 变绿而 OTA 通道为空 —— ✅ 已修复(0.4) |
| 5 | 非 Windows 自更新缺可执行位,必失败 | install.rs:92、220 | Linux 用户永远装不上 —— ⏳ 待批次 1 |
0.3 问题分布
| 级别 | 数量 | 代表 |
|---|---|---|
| P0 阻断发布 / 安全边界 | 6 | 7z 路径穿越、事件总线无背压、发布链路三不通 |
| P1 正确性 | 11 | MQTT5 收到 0 值直接断连、主题键名不生效、TOCTOU |
| P2 一致性 / 工程化 | 22 | 对话框栈语义、README 漂移、死代码 |
| 未决 需实测 | 2 | 阿里云签名 content、securemode 与传输层矛盾 |
0.4 本次已修复项(2026-09-30,批次 0)
分析完成后实施了批次 0 的修复,改动集中在 CI/脚本/测试与文档,未触碰任何业务逻辑:
| 对应问题 | 改动 | 文件 | 验证证据 |
|---|---|---|---|
| P0-1 | 采样测试改为分平台断言(if cfg! 而非无条件 > 0,两个分支都参与编译) |
gpui-app/src/sysmon.rs |
Windows cargo test --lib 33 passed;clippy --all-targets 零告警 |
| P0-1 | test job 补齐 GPUI 的 Linux 系统依赖;命令改为 --lib --tests(集成测试只编译不执行) |
.gitea/workflows/release.yml |
YAML 解析无错误;test job 4 个 step 结构正确 |
| P0-2 | 版本解析改用 git describe --tags --exact-match + Cargo.toml 回退;新增 semver 强制校验 |
scripts/publish-gitea.sh、.ps1 |
实测:无参数时回退 1.0.0(不再产出 v1.0.0-4-gbc9f55c);显式传该串被拒绝且不发生网络请求;v1.0.1 通过校验 |
| P0-3 | meta job 新增「tag == v + Cargo.toml 版本」断言,不一致即整条流水线失败 | .gitea/workflows/release.yml |
实测断言逻辑:v1.0.0 通过,v1.0.1 / V1.0.0 拒绝 |
| 摘要-4 | Release 创建/查询/解析失败一律 exit 1(原为 exit 0) |
scripts/publish-gitea.sh、.ps1 |
bash -n 语法检查通过;PowerShell 解析器无错误 |
| README 漂移 | 订正版本推断说明,并重写「方式二」CI 流程(双平台 → 实际仅 Linux;补 Release/7z OTA 通道与命名约定、repo 写权限要求) | README.md |
人工核对 |
批次 0 只解除了"CI 必红"与"版本不可用"两个确定性故障。 让 Linux 客户端真正装上 更新的 P0-6(产物缺可执行位)属于批次 1,尚未实施;OTA 信任模型(P0-4)、 事件总线背压(P0-5)同样未动。
0.5 第二批修复(2026-09-30,批次 1 + 批次 2)
在 0.4 之后继续实施,共 30 个文件、+747/−170。验证:cargo test --lib 34 passed
(新增 1 个资产挑选回归测试)、clippy --all-targets 零告警、主题 JSON 解析确认
font.size=14 / radius.lg=10 已生效。
安全与数据完整性
| 问题 | 修复 |
|---|---|
| P0-4 7z 路径穿越 | 新增 verify_archive_entries:解压前只读归档头校验条目清单,必须恰好一个且名字与约定二进制名完全一致(fail-closed);sevenz-rust 0.6.1 的 decompress_file 不做任何路径校验 |
| P0-5 事件总线无背压 | 通道改有界(容量 1024);emit 分级:高频事件(消息/日志)满即丢并计数、每 1000 条提示一次,低频关键事件(状态/订阅结果/发布回执)异步补投 |
| P0-6 非 Windows 缺可执行位 | 新增 make_executable(Unix chmod +x,Windows 空实现),暂存产物落盘即补权限 |
| P1-7 校验可静默关闭 | ① 无 .sha256 侧车 → 拒绝安装(fail-closed)② install_staged 安装前重算哈希 ③ 同意标记绑定「版本 + 暂存文件哈希」 |
| P1-8 跨卷 rename 失败 | 新增 place_file:rename 失败回退「复制 + fsync + 删源」 |
| P1-9 备份即删 | cleanup_old 保留最近一份 .old 至 OLD_BACKUP_KEEP(7 天),更早的清理 |
| P2-19 无安装互斥 | 新增 InstallLock(create_new 原子抢占 + 10 分钟陈旧锁接管) |
| P2-21 死分支 | 删除永不命中的 .update.tmp 清理条件 |
正确性
| 问题 | 修复 |
|---|---|
| P1-1 协议非法值 | 端口改 parse_port(1~65535);接收上限/最大报文/主题别名拒 0(parse_opt_positive_u32,0 是 MQTT 5 协议错误);遗嘱主题 trim + 拒绝通配符 |
| P1-2 校验反馈滞后 | 16 个被校验字段订阅 InputEvent::Change,编辑即清空字段错误 |
| P1-3 重连提示漏字段 | session_params_changed 补齐 4 个 v5 CONNECT 属性;测试改为逐字段防漏式断言 |
| P1-4 三元组写死 | platform_triple() 改用 std::env::consts::ARCH;连带修复两个写死三元组导致 Linux CI 必红的资产测试,并新增「非约定附件不得选中」测试 |
| P1-5 固定 2s 退避 | 新增 reconnect_delay:指数退避(1s→30s 封顶)+ 0~500ms 抖动 |
| P1-6 测试连接占住 worker | test_handshake 的建连(读证书)放 spawn_blocking,与 connect 一致 |
| P1-10 主题键名不生效 | font_size→font.size、radius_lg→radius.lg(实测解析出 14px/10px) |
| 复审 D8 资产挑选退化 | pick_assets 按命名约定精确后缀匹配(mqttx[.exe] / .7z),任何含三元组的其它附件不再被当成可执行文件 |
性能(P1-11 的低成本部分)
| 问题 | 修复 |
|---|---|
| 每帧深拷贝消息记录 | MqttRecord.user_properties 改 Arc<[(String, String)]>(每帧 300 条记录的属性 Vec 深拷贝 → 引用计数) |
| 每帧深拷贝日志 | app.logs 改 VecDeque<Arc<LogEntry>>(每帧最多 3000 条 × 4 个 String 的堆分配 → 引用计数) |
| 每行重复读实体 | show_millis 提升到 render_messages 帧级,一次读取传入每行 |
其他(低成本 P2)
P2-5 五个对话框 open() 统一加 has_active_dialog 守卫;P2-6 OptionDelegate::position
按框架契约如实实现;P2-9 阿里云对话框预填即选中(不再复制预设);P2-12
ConnectionConfig / AliyunPreset 手写脱敏 Debug(password/密钥输出 <redacted>,
杜绝一次 {:?} 把凭据送进 Sentry);P2-14 CPU 采样间隔过近时沿用上次结果且不前移
差分基准、修正注释口径;P2-17 非 Windows 不渲染必然报错的置顶按钮;
aliyun.rs 补 DeviceName 空值校验;Cargo.toml 补 license = "Apache-2.0"。
仍未修复(明确留待后续)
P1-11 完整版(消息/日志虚拟列表);P2-8 KvEditor 行 uid;P2-11 保存失败不可见;
P2-13 硬编码更新源与 Sentry DSN;P2-16 采样在 UI 线程;P2-20 curl 令牌暴露面;
复审 D11(tag 大写 V 的归一化不一致);D12(预发布版本永远收不到正式版);
LICENSE 文件本体(元数据已声明 Apache-2.0,正文需从官方源获取);
aliyun.rs 一机一密 securemode 与传输层的矛盾(0.5 之外,见 6.4 未决项)。
1. 项目画像
1.1 定位
用 Rust + gpui-kit(GPUI 0.3 / gpui-component 0.6)重写的 MQTT 调试客户端,对标官方 Electron 版 MQTTX。纯 Rust 单二进制,无 Electron/WebView 运行时。早期 Tauri + Vue 实现已归档到 tauri-legacy 分支。
1.2 代码量化
| 层 | 行数 | 文件数 | 说明 |
|---|---|---|---|
ui/ |
7,607 | 27 | 占 66%,是所有复杂度的集中地 |
mqtt/ |
1,388 | 4 | 引擎:连接生命周期 + v5/v4 事件循环 + TLS |
model/ |
1,055 | 8 | 纯数据层,零 GPUI 依赖 |
| 根模块(store/aliyun/sysmon/platform/main/lib) | 822 | 6 | 持久化、签名、采样、入口 |
update/ |
621 | 3 | OTA |
| 合计 | 11,493 | 48 | 另有 tests 97 行、examples 61 行 |
1.3 依赖画像
- 直接依赖 30 项;
Cargo.lock905 个包,其中 89 个包存在多版本(集中在windows*/hashbrown/getrandom/rand等传递依赖)。 rustls单版本 0.23.45 —— 这是自定义 TLS(TlsConfiguration::Rustls)能与 rumqttc 内部类型互通的前提,属关键约束。ring0.17.14 与aws-lc-rs1.18.1 同时存在,因此必须在运行时显式安装 CryptoProvider(tls.rs:104-114)——作者已正确处理。- 重量级自有选型:
sentry(错误上报)、ureq(OTA 下载)、sevenz-rust(纯 Rust 解压)、rfd(原生文件对话框)、embed-resource(图标)。
1.4 产物与构建
| 项 | 实测值 |
|---|---|
target/release/mqttx.exe |
20.4 MB(README 写 ~17MB,已过时) |
target/debug/mqttx.exe |
68.0 MB |
| release profile | lto=true, codegen-units=1, opt-level=3, panic="abort", strip=true |
| 工具链 | 本机同时装有 stable / nightly;README 声称"必须 nightly",但仓库无 rust-toolchain.toml,CI 用默认 stable 🟡 |
1.5 提交历史
- 66 次提交,2026-08-18 → 2026-09-29(41 天),单人双身份(JoeAllen / Joe Allen)。
- 提交信息质量高,且修复类提交带根因描述(如"连接生命周期竞态与事件处理""数据文件损坏防护、写盘 fsync 与临时文件清理")。
- 最近两次提交是纯结构重构(拆
model/mqtt/update、拆ui/app),说明项目正处在**"功能收口、开始整理结构"**的阶段——此刻做架构级修复成本最低。
2. 架构分析
2.1 分层与依赖方向
main.rs ──► ui::app::MqttXApp(唯一状态所有者)
├─ model/* 纯数据 + 纯函数(不依赖 GPUI)
├─ store.rs JSON 原子写(tmp + fsync + rename)
├─ mqtt/* 引擎(独立 tokio runtime)
└─ update/* OTA(ureq + 7z + 自替换)
lib.rs 导出 model/mqtt/store/update,使 tests/ 能脱离 GUI 测引擎
依赖方向严格单向、无环:ui → {model, store, mqtt, update},mqtt → model,model 不依赖任何上层。这是 11k 行规模下最难得的一点:数据层零 GPUI 依赖,33 个单测里有 25 个因此可以直接测业务逻辑。
2.2 并发模型
GPUI 主线程 engine 线程(tokio, 2 worker)
───────────── ──────────────────────────────
MqttXApp 状态 MqttEngine { conns: Mutex<HashMap>, runtime }
▲ │
│ cx.notify() ├─ connect(): 占位句柄(client=None) + generation
│ ├─ spawn_blocking: 建 client(读证书/CA 文件)
│ ├─ 事件循环: tokio::select!{ poll() , cancel_rx }
└── smol::channel::unbounded ◄─────────┘ emit(): tx.try_send(event) ← 无界、不阻塞
三个关键设计:
- 连接代数(generation):同一
id的每次connect递增代数;事件循环退出时只删自己那一代(remove_handle_if)。这修掉了"重连后句柄被旧循环误删、从此无法从 UI 断开"的经典竞态。 - 建连期占位句柄
client: None:让close()在建连中途也能取消,且is_connected()在此之前恒为 false,天然做了防抖。 - 阻塞 IO 分离:
connect把证书读取放进spawn_blocking(mqtt/mod.rs:198-200),OTA 下载走run_blocking。但test_connection是例外(见 P1-6)。
缺口:emit 用 try_send(mqtt/mod.rs:127-128),通道容量由 UI 侧决定(ui/app/mod.rs:129 用的是 unbounded)。当前等价于"永不丢弃、永不阻塞"——见 P0-5。
2.3 一次消息的完整旅程
broker ──► eventloop.poll() ──► v5_record() ──► engine.intern(topic) ← Arc<str> 驻留
│ engine.emit(Message)
▼
[无界 smol 通道] ← 背压缺口在这里
▼
UI pump ──► on_engine_event():
├─ 丢弃已删除连接的事件(防泄漏)
├─ push_message() → MessageRing(条数 + 32MB 字节双预算驱逐)
├─ push_log() → 内存环形(3000) + 同步写 mqttx-YYYY-MM-DD.log ← 磁盘 IO 在 UI 线程
└─ view.notify() → 下一次渲染重建 ≤300 行消息
这条链路有三处刻意优化(主题驻留、双预算驱逐、日志句柄按日缓存),也有三处代价(无背压、UI 线程落盘、每帧全量重建),构成性能与可靠性问题的主干。
2.4 连接生命周期
ConnectionStatus 四态由引擎单向推送,UI 不猜测状态,只在 Connected 事件上触发 auto resubscribe。订阅采用乐观更新:先入库落盘,SUBACK 失败再回滚,并特意避免"SUBACK 前被删除的订阅被事件用默认字段复活"。
2.5 持久化
- 数据目录:
%APPDATA%\MQTTX-GPUI(刻意与官方 Electron 版MQTTX/区分)。 - 6 个 JSON 文件:connections / subscriptions / presets / variables / aliyun / settings。
- 写入语义:
tmp(pid 命名) → write_all → fsync → rename,失败清理 tmp。 - 损坏文件防护:加载失败的文件名进 session 集合,下次保存前先重命名为
.corrupt-<时间戳>备份,把"连接全没了且被空数据覆盖"从不可恢复降为可抢救。 - 读取容忍 UTF-8 BOM。
2.6 OTA 链路
启动 ──► cleanup_old() ──► load_staged() ──► consent 匹配? ──► install_staged()(改名+spawn 新进程)──► 退出
│ 否
└─► 进入 UI,auto_check_update 时联网检查
check_latest() ─► pick_assets() ─► download_and_stage() ─► 大小对账 + sha256 ─► 弹窗征得同意
设计上明确"下载即暂存、用户同意才安装",拒绝静默强制升级——产品方向正确;代价是暂存区成为长期驻留的攻击面(见 P0-4、P1-7)。
2.7 UI 组织与渲染
ui/app/(外壳与状态)+ ui/connection_view/(单连接工作区)+ ui/dialogs/(模态)+ ui/widgets/(复用件)。ConnectionView 是最大的实体(约 30 个字段,含 12 个 Entity<InputState>)。
渲染上限:MAX_RENDERED_MESSAGES = 300、MAX_RENDERED_LOGS = 3000;消息最新在最上方。每帧全量重建,无虚拟列表——见 P1-11。
3. 模块点评
| 模块 | 职责 | 亮点 | 主要问题 |
|---|---|---|---|
model/ |
纯数据 + 纯函数 | 字节预算环形缓冲;非文本负载保留原始字节;模板渲染单时间基准 | session_params_changed 漏字段(P1-3) |
mqtt/ |
引擎与事件循环 | 连接代数、占位句柄、可取消退避、显式 CryptoProvider | 无背压(P0-5)、test_connection 阻塞 IO(P1-6)、固定 2s 退避(P1-5) |
store.rs |
JSON 原子持久化 | fsync+rename、损坏备份、BOM 容忍 | 明文密码落盘(P2-12) |
update/ |
OTA | 大小对账、双阶段回滚、同意绑定版本 | 路径穿越(P0-4)、缺可执行位(P0-6)、TOCTOU(P1-7)、跨卷 rename(P1-8)、备份即删(P1-9) |
aliyun.rs |
阿里云签名 | Token 模式与官方一致 | securemode/传输矛盾(未决-2)、无测试 |
sysmon.rs |
进程采样 | Win32 结构口径正确、失败不 panic | Linux 测试必失败(P0-1)、CPU 基准前移(P2-14) |
platform.rs |
窗口置顶 | 失败路径不 panic | 非 Windows 必报错(P2-17)、双分派死代码 |
ui/ |
全部界面 | 色值零硬编码、异步回调统一弱引用 | 渲染热路径(P1-11)、对话框栈语义(P2-5)、键名不生效(P1-10) |
4. 值得肯定的设计
- 连接代数 + 占位句柄(mqtt/mod.rs:153-250)——正确解决"重连/关闭与建连并发"这一最难调的竞态。
- 消息环形缓冲的条数 + 字节双预算(message.rs:136-146,32MB/连接)——大报文流下条数上限形同虚设(2000×128KB≈256MB),作者显式解决了。
- 非文本负载保留原始字节(message.rs:180-197)——lossy 文本会失真,Hex/Base64 详情仍能对上线上报文。
- 损坏文件"先备份再覆盖"(store.rs:88-103)——把不可恢复的丢失降为可抢救。
- 落盘 tmp+fsync+rename + pid 命名(store.rs:105-122)——掉电语义正确,并发不互踩。
- 显式安装 rustls CryptoProvider(tls.rs:104-114)——ring 与 aws-lc-rs 并存时
ClientConfig::builder()会 panic,属隐藏地雷,被提前拆掉。 - 下载大小对账(install.rs:110-116)——弥补
read()==0无法区分"正常结束/断流",判断精准。 - 自替换双阶段回滚(install.rs:212-242)——rename 失败与 spawn 失败都尝试回滚,并区分"已恢复/回滚失败+备份路径"。
- 同意标记绑定版本(install.rs:265-271)——避免"下完就换"。
- 主题色值零硬编码——范围内 11 个文件仅 3 处出现字面色,且都是主题色的 alpha 派生,浅/深色都成立。
- 异步回调统一弱引用 +
.ok()——无 Entity 泄漏,不会在实体/窗口销毁后 panic;资源监控用弱引用退出循环并有注释说明。 - 消息内存按字节预算与
flood压测工具并存,说明作者主动验证过内存行为。 - sysmon 的 Win32 用法正确——
PROCESS_MEMORY_COUNTERS_EX扁平结构 + 按 EX 大小传cb,PrivateUsage取"提交内存"口径;CPU 用GetProcessTimes差分而非近似采样。 - build.rs 用
CARGO_CFG_TARGET_OS而非宿主 OS——交叉编译时行为正确;"资源 ID 1"的注释与 gpui-pre-windows 的LoadImageW实测一致。
5. 问题清单
每条格式:现象 → 证据(文件:行)→ 影响 → 修复方向。标题后的标记表示验证状态。文件路径相对仓库根,格式为
路径:行。
P0-1 ✅实测 【阻断】Linux 单测确定性失败,CI 永远红 —— 🛠 已修复(见 0.4)
- 证据:sysmon.rs:9(
Sample派生Default)、41(非 Windows 直接返回Sample::default(),working_set = 0)、194(无条件assert!(second.working_set > 0));release.yml:39-46 在ubuntu-latest跑cargo test -p mqttx-desktop --lib;release.yml:52-53 build/publishneeds: [meta, test]。 - 加重因素:test job 未安装任何图形库
-dev包,而同一 workflow 的 build job 装了 10 个("Install Linux system deps (GPUI)")。同一份依赖图,test job 在干净 runner 上大概率连编译都过不去。 - 影响:打
v*tag 后 Test 阶段即断,产物与 Release 一个都发不出去。该红灯在sysmon引入之后才存在,而最后一次 tag(v1.0.0)早于它,因此从未暴露。 - 修复:该测试加
#[cfg(target_os = "windows")]或分平台断言;test job 复用 build job 的依赖安装步骤。
P0-2 ✅实测 【OTA】发布脚本产出非 semver 版本,静默废掉更新通道 —— 🛠 已修复(见 0.4)
- 证据:publish-gitea.sh:71-78、publish-gitea.ps1:115-123 用
git describe --tags --always兜底版本。实测本仓库 HEAD:git describe --tags --always→v1.0.0-4-gbc9f55c,--exact-match失败(HEAD 无 tag)。 - 影响链:
VER_NUM = 1.0.0-4-gbc9f55c→ 创建/复用该 tag 的公开 Release → 客户端release_part(update/mod.rs:78-84)截成1.0.0→is_newer判否 → 老用户永远收不到更新;且该 Release 会被/releases/latest命中,把真正的更新挡在后面。 - 修复:脚本入口强制 semver 正则校验,不匹配即失败;把
--always换成--exact-match。
P0-3 ✅代码读取 【发布一致性】版本双真源,无一致性校验 —— 🛠 已修复(见 0.4)
- 证据:Cargo.toml:3(
CARGO_PKG_VERSION,当前 1.0.0)vs release.yml:33(GITHUB_REF_NAME),workflow 内无比对步骤。 - 影响:打
v1.0.4但忘 bump → 新包装完仍自报 1.0.0 → 每次启动重复提示同一更新(无限循环);反向则永远无更新提示。 - 修复:CI 增加
v$(Cargo.toml version) == tag断言;或让 build.rs 从 tag 生成版本,收敛为单一真源。
P0-4 ✅代码读取 【安全】7z 解压路径穿越 + 同源 sha256
- 证据:install.rs:59 调用
sevenz_rust::decompress_file;依赖sevenz-rust-0.6.1/src/de_funcs.rs:102直接dest.join(entry.name()),不校验绝对路径与..,且default_entry_extract_fn会自动create_dir_all父目录。校验值来自同一 Release 的侧车(check.rs:52-59 → install.rs:119)。 - 影响:能创建 Release 的人(或能改写 Gitea 响应的一方)可对所有客户端完成任意路径写入(含启动目录),随后
install_staged还会执行解压出的同名二进制。sha256 只防传输损坏,不防伪造。 - 修复:改用
decompress_with_extract_fn做白名单 + 拒绝../盘符/UNC + 限制条目数与解压总量;同时引入 ed25519/minisign 签名(内嵌公钥)作为真正的信任锚。
P0-5 ✅代码读取 【可靠性】事件总线无背压,极端流量下内存无上界
- 证据:ui/app/mod.rs:129 建无界通道;mqtt/mod.rs:127-128
try_send永不失败;消费端每条事件做线性查找 + 入环形缓冲 + 同步写日志文件(events.rs:184 → 195-259)+cx.notify()。 - 影响:环形缓冲只约束"已入库"的记录,不约束"在途"事件。到达速率持续超过 UI 消费速率时队列无界增长,每个在途
Message事件最长可携带 128KB payload。仓库自带的examples/flood.rs正是按速率压测的发布器,说明作者关心该场景,但管道本身无兜底。 - 修复:改有界通道 + 显式丢弃策略(消息可丢、状态/日志不可丢);或把
Message事件批量化(一次携带Vec<MqttRecord>),把"每条一次 notify"变成"每帧一次";日志落盘移出热路径。
P0-6 ✅代码读取 【OTA】非 Windows 自更新因缺可执行位必定失败
- 证据:install.rs:92 用
File::create落盘(Unix 权限 0644),7z 路径的解压同样走File::create;install.rs:212rename就位(保留权限);install.rs:220Command::new(&cur).spawn()→ EACCES,走回滚 222-227。 - 影响:Linux/macOS 用户永远装不上,且提示只有一句"启动新版本失败"。
- 修复:Unix 下在 rename 前
set_permissions(0o755)。
P1-1 ✅代码读取 【协议】MQTT 5 的 0 值参数直接进 CONNECT 并落盘
- 证据:connection_form.rs:431-444 的
parse_opt_u32对"0"返回Some(0)(只做 u16 上限转换)→ v5.rs:79-85 原样set_receive_maximum(Some(0))/set_max_packet_size(Some(0))→ 保存即写connections.json。 - 影响:MQTT 5 规范把 Receive Maximum = 0 与 Maximum Packet Size = 0 定义为 Protocol Error,broker 直接断连;值被持久化,重启后持续失败,用户只看到"连接测试失败"。
- 修复:解析后
filter(|v| *v > 0),文案写明"必须 ≥1(0 为协议错误)"。
P1-2 ✅代码读取 【校验】表单校验"一次性 + 首错即停 + 无变更订阅"
- 证据:connection_form.rs:387-389(每次 build 先
clear)、346-350(fail后立即?返回)、731-734(仅从field_errors渲染,无输入变更订阅)。 - 影响:改好端口后红字仍在;3 个字段非法要点 3 次保存才能看全。
- 修复:订阅
InputState的Change清除对应 key;或一次性收集全部错误。
P1-3 ✅代码读取 【正确性】session_params_changed 漏掉 4 个 v5 CONNECT 属性
- 证据:connection.rs:255-268 比较了 host/port/path/transport/protocol/client_id/账号/clean_start/keep_alive/遗嘱/SSL,唯独漏了
session_expiry_interval、receive_maximum、maximum_packet_size、topic_alias_maximum——而它们在 v5.rs:73-87 确实是随 CONNECT 一次性生效的。 - 影响:连接已建立时改这四项,用户不会收到"重连后生效"提示,改动静默不生效。对应的单测自称覆盖"所有 CONNECT 时报文生效字段",实际未覆盖,给了虚假安全感。
- 修复:补齐比较;测试改为遍历式断言(列出全部 CONNECT 字段逐一改动验证)。
P1-4 ✅代码读取 【平台】platform_triple() 硬编码 x86_64
- 证据:update/mod.rs:56-64 只按
cfg!(target_os)判断,架构写死x86_64-*;而发布脚本 publish-gitea.sh:107-108 明确支持aarch64-apple-darwin/aarch64-unknown-linux-gnu。 - 影响:ARM 客户端永远发现不了更新;发布端若产出 aarch64 资产,则成为永远无人下载的僵尸资产。当前 CI 只出 Linux x86_64,属潜在坑。
- 修复:
std::env::consts::ARCH参与拼接;补"客户端三元组 == 脚本三元组"的对照测试。
P1-5 ✅代码读取 【可靠性】重连固定 2 秒,无指数退避与抖动
- 证据:v5.rs:338-341、v4.rs:301-304;
max_reconnect_times默认 0 即无限重连。 - 影响:broker 长时间不可用时每 2 秒一次尝试 + 每 2 秒一次
Status::Error事件 → UI 抖动与日志刷屏;多连接会在同一时刻齐步重连(惊群)。 - 修复:指数退避(1s→2s→…→60s)+ 抖动;连续失败期间不重复推送
Error状态。
P1-6 ✅代码读取 【一致性】test_connection 在 runtime 线程做阻塞 IO
- 证据:mqtt/mod.rs:568-585 直接把
test_handshake(&cfg)放进timeout,而test_handshake内部同步调用build_v5/build_v4(读 CA/证书文件)。对照 mqtt/mod.rs:198-200,作者在connect里明确写了"build 含阻塞 IO,放 spawn_blocking"。 - 影响:慢盘上测试连接会占住 2 个 worker 之一,影响同一 runtime 上其他连接的收发。
- 修复:同样走
spawn_blocking。
P1-7 ✅代码读取 【安全】安装前不复核 + 无侧车静默跳过校验
- 证据:install.rs:204-231 只 rename+spawn,不重算哈希;check.rs:115-119 同版本已暂存即跳过下载;install.rs:177-200 信任目录内任何命名合规文件;install.rs:265-271 consent 只比对版本字符串。侧车缺失时 check.rs:52-59 返回
None→ install.rs:153-156 → 跳过校验,而 UI 仍写"已通过 sha256 校验"(ui/app/mod.rs:173)。 - 影响:一次 Release 漏传侧车,所有客户端就在零完整性校验下安装;下载校验完成到"下次启动安装"之间,暂存文件被替换/损坏仍会被原样安装。
- 修复:无侧车拒绝安装或显式降级提示;安装前重算哈希;consent 绑定哈希。
P1-8 ✅代码读取 【可靠性】跨卷 rename 必失败,且报错误导
- 证据:install.rs:11-18 暂存在
%APPDATA%\MQTTX-GPUI\updates;install.rs:205-212 把 exe 同目录的文件改名就位。Windows 上跨卷MoveFile直接失败(ERROR_NOT_SAME_DEVICE),而错误文案是"替换失败(可能无写入权限)"。 - 影响:应用装在 D 盘、
%APPDATA%在 C 盘时更新必失败;域环境 Roaming 重定向到网络盘更常见。 - 修复:rename 失败回退
copy + fsync + replace;或把暂存放 exe 同目录。
P1-9 ✅代码读取 【可靠性】新版本首启即删除唯一回滚备份
- 证据:main.rs:26(安装后新进程启动第一步就
cleanup_old())→ install.rs:289-293 无条件删*.old;ui/app/mod.rs:144 再删一次。 - 影响:自替换已覆盖原 exe,备份又被新版本自己删掉;新版本一旦启动即崩溃,用户没有任何自助回退路径。
- 修复:新版本健康运行一段时间后再删;或保留最近 1-2 份 + 启动失败自动回退。
P1-10 ✅代码读取 【UI】主题 font_size / radius_lg 键名不符规范,被静默忽略
- 证据:linear-theme.json:6-8 写
"font_size": 14/"radius": 6/"radius_lg": 10;依赖 schemagpui-component-0.6.6/src/theme/schema.rs:36-71为#[serde(default)]且rename = "font.size"(默认 16)、rename = "radius.lg"(默认 8)、rename = "radius"(默认 6),未知键不报错。 - 影响:主题里写的 14px 字号与 10px 大圆角从未生效(实际是框架默认 16px / 8px),只有
radius: 6因为同名而生效。README 的"14px 紧凑排版"因此不成立。 - 修复:改成
"font.size": 14、"radius.lg": 10;建议补一条"主题键名与 schema 对齐"的测试。
P1-11 ✅代码读取 【性能】渲染热路径全量深拷贝
- 证据:消息每帧
r.clone()最多 300 条(messages.rs:438-478),MqttRecord::clone虽共享Arc<str>,但user_properties: Vec<(String,String)>与 4 个Option<String>是深拷贝;日志每帧filter(..).take(3000).cloned()(messages.rs:687-698),每条LogEntry含 4 个String;发布面板每帧对 topic+payload 做extract_placeholder_keys(publish.rs:180-184),展开时还每帧render_template(vars.rs:84-88);每行额外一次with_app读show_millis(messages.rs:153-155)。 - 影响:日志面板打开且有 3000 条日志时,60fps 下约 18 万次 LogEntry 克隆/秒(≈72 万次 String 分配/秒);发布大负载时每帧一次 MB 级拷贝。
- 修复:虚拟列表(只渲染可见窗口);
show_millis/subs提到帧级只算一次;用Arc<MqttRecord>让列表渲染零拷贝;占位符提取改为Change事件增量计算并缓存。
P2 清单(22 项,摘要)
| # | 问题 | 证据 | 影响 |
|---|---|---|---|
| P2-1 | 端口允许 0 | connection_form.rs:361-365、405 | 非法值落盘,报错与真实原因无关 |
| P2-2 | 遗嘱主题不 trim、不校验通配符 | connection_form.rs:530-546 | 保存成功、连接必失败 |
| P2-3 | 传输切换的"默认端口"判定跨所有传输 | connection_form.rs:257-267 | 手输 8084 后切 TLS 被静默改 8883 |
| P2-4 | 非 WS 传输下 path 仍入库但输入框隐藏 | connection_form.rs:500-503 | 隐形脏数据 |
| P2-5 | close_dialog 关栈顶而非自己;叠加守卫只挂 5 个快捷键 |
settings_dialog.rs:568、ui/app/mod.rs:284-330 | 嵌套确认时可能关错层 |
| P2-6 | OptionDelegate::position() 恒 None,违背框架契约 |
widgets/select.rs:49-59 | 一旦启用搜索或 set_selected_value 即"下拉显示空、保存写默认值" |
| P2-7 | KvEditor 空 key 静默丢弃、重复 key 静默覆盖 | widgets/kv_editor.rs:54-73 | 用户填的行保存后无声消失 |
| P2-8 | KvEditor 用下标做元素 id | widgets/kv_editor.rs:85-100 | 删中间行后 hover/焦点串台一帧 |
| P2-9 | 阿里云对话框预填 first 但 selected=None |
dialogs/aliyun_dialog.rs:54-58、188-190 | 打开即保存 → 凭空复制一条预设 |
| P2-10 | 阿里云"生成并连接"每次新建连接并落盘 | dialogs/aliyun_dialog.rs:206-213、ui/app/actions.rs:16-23 | 连点两次多两条同名连接 |
| P2-11 | 保存失败不可见(写盘失败仅 elog) |
ui/app/actions.rs:16-23、store.rs:140-144 | 磁盘满/无权限时用户以为已保存 |
| P2-12 | 明文落盘与导出:连接密码、AccessKeySecret/DeviceSecret | store.rs:221-225、aliyun.rs:66-77 | 导出文件即凭据;两结构体 derive(Debug),一次 {:?} 就进 stderr+Sentry(当前未见此类调用) |
| P2-13 | 硬编码基础设施地址与 Sentry DSN | main.rs:8-9、update/mod.rs:22-24 | 换源/开源需改代码 |
| P2-14 | CPU 采样 dt≤50ms 时既报 0 又前移基准;注释与公式矛盾 | sysmon.rs:118-125、17 | 手点"立即刷新"读到 0.0%;按注释理解会误判 8 倍 |
| P2-15 | 非 Windows 返回"全 0"而非"能力缺失" | sysmon.rs:41-43 | UI 显示 0 B / 0%,无法区分"不支持" |
| P2-16 | 线程采样抓全系统快照并同步跑在 UI 线程 | sysmon.rs:131-148 | 线程数上万时毫秒级开销落在渲染路径 |
| P2-17 | 非 Windows 置顶按钮必弹错误;双分派死代码 | platform.rs:5-8、37-40 | 跨平台产品上的坏按钮 |
| P2-18 | build.rs 用 manifest_required() 把"缺 rc.exe"升级为硬失败 ⚠️ |
build.rs:11-13 | 无 Windows SDK 时连 UI 都起不来(代价只是图标回退默认) |
| P2-19 | 无单实例/安装互斥 | 全仓库无单实例锁 | 两实例互删暂存、争抢同一个 .old |
| P2-20 | 发布脚本令牌走 curl 命令行;Release 失败 exit 0 |
publish-gitea.sh:119-139、174-181 | 同机 ps 可见令牌;job 变绿但 OTA 通道为空 |
| P2-21 | .update.tmp 清理分支永不命中 |
install.rs:290 vs 39-47 | 历史遗留 tmp 永远清不掉 |
| P2-22 | 无 LICENSE 却宣称 Apache-2.0;Cargo 无 license 字段 | settings_dialog.rs:506;glob LICENSE* 为空 |
对外分发前的合规缺口 |
其他低危项(供参考):engine_live.rs 从不读取 MQTTX_LIVE 环境变量;pump() 把通道关闭当空轮询;flood.rs 的 eventloop 静默吞错且统计不区分"已请求/已发出";widgets/select.rs:84 的 select_element 零调用;logo.rs:122 死导入;resource_dialog 连接卡缺溢出保护。
6. 专项分析
6.1 发布链路:三种走法都不通(推演)
| 走法 | 结果 | 依据 |
|---|---|---|
| CI 自动发布(tag → build → publish) | 不通:Test 阶段必断 | P0-1 |
本地脚本默认路径(不带 -Version 直接跑) |
通路但产出垃圾版本:Release tag 变成 v1.0.0-4-gbc9f55c |
P0-2(实测) |
本地脚本显式版本(-Version v1.0.1) |
相对可用,但仍需人工保证 tag 与 Cargo 版本一致,且 Windows triple 硬编码 | P0-3、P2-20 |
结论:在批次 0 完成之前,"发布"这件事在三种走法里没有一条是可靠的。这也解释了为什么 OTA 相关缺陷至今未被触发——通道本身从未真正跑通过。
6.2 OTA 信任模型
当前信任锚只有两个:HTTPS/TLS 证书 与 Gitea 的发布权限。完整性校验(sha256 侧车)与产物同源,只能证明"传输没坏"。叠加以下事实,构成一条完整的自动执行链:
- 暂存目录与同意标记都在用户可写的
%APPDATA%(P1-7 / P2-19); load_staged信任目录内任何命名合规的文件(install.rs:177-200);- 同意标记是纯文本版本号,任何同用户进程可伪造(install.rs:246-271);
- 安装前不复核哈希(P1-7);
- 7z 解压可越界写(P0-4)。
当应用安装在 Program Files(替换 exe 需管理员)时,这等于把用户可写目录接进了管理员执行路径。
加固优先级:签名验证 > 解压白名单 > consent 绑定哈希 > 安装前复核 > 安装互斥锁。
6.3 性能与内存
- 已做对的:单条 payload 128KB 截断、每连接 32MB 字节预算、主题字符串驻留、消息列表渲染上限 300。
- 缺口:无背压(P0-5)、每帧全量深拷贝(P1-11)、日志同步落盘。
- 失效模式链:高频消息 → UI 线程写日志/重建元素变慢 → 无界队列积压 → 内存增长。三处缺一不可,先修背压收益最大。
- 建议观测指标:单连接 payload+raw 驻留字节(已有
retained_bytes()可直接复用)、通道在途事件数、单帧渲染耗时。
6.4 阿里云签名争议 ❌ 未采信
争议点:一机一密的 signing content 中 clientId 段应取什么值。
复审意见(被列为中高缺陷):认为官方 content 应使用"客户端实际发送的完整 clientId",且 clientId 前缀应为 deviceName 而非 productKey.deviceName。
本报告不采信,理由:
- 实现、模块文档、提交信息三者自洽——aliyun.rs:11-13 明写 content 为
clientId{ProductKey}.{DeviceName}deviceName{...}productKey{...}timestamp{...},代码 176-179 完全照此实现;提交2f35bca("按物联网平台官方直连规范重写")的 diff 显示这是对照规范后的重写结果,不是随手写的。 - 该拼装形式(content 的 clientId 段取
pk.dn、不带|securemode…|后缀)与常见 Aliyun IoT SDK 示例一致。 - 复审自身声明"无法联网核对官方原文",其论据"两处拼装必须自洽"并不成立——规范允许 content 中的占位值与报文中的字面值不同。
- 本报告作者尝试核验亦失败:本环境
help.aliyun.com解析到非公网 IP(web_fetch直接拒绝),web_search超时。
处理建议(发布前必做,约 10 分钟):用官方文档自带的示例四元组(ProductKey / DeviceName / DeviceSecret / timestamp → 示例 password)建立离线测试向量,两种拼装各算一次,与示例比对。这同时补上了 aliyun.rs 目前零测试的缺口。
但"securemode 与传输层矛盾"保留为未决项(见附录 C):代码在 clientId 里声明 securemode=2(官方语义为 TLS 直连),而 aliyun.rs:142-143 固定 TransportKind::Tcp(1883)。这是代码内部可判定的不一致,与 content 规则如何无关——声明与实际传输必须一致(要么 securemode=3 + TCP,要么 TLS + 8883)。需设备实测确认 broker 是否拒绝。
6.5 主题与视觉
- 做得好的:色值零硬编码,全部经
cx.theme()或主题色 alpha 派生;hue_color()按色相段微调明度以兼顾浅/深色;订阅色板有语义。 - 缺陷:P1-10 的两个键名被静默忽略,导致"14px 紧凑排版"名不副实。
- 建议:把主题 JSON 的键名与
schema.rs做一次对齐检查并纳入 CI(该 JSON 是include_str!内嵌的,编译期无法发现键名错误)。
7. 测试与质量保障
7.1 现状
| 项 | 值 |
|---|---|
| 单测数量 | 33 个,全部通过(本机 Windows,0.19s) |
| 分布 | model/mod.rs 18、update/mod.rs 6、store.rs 5、sysmon.rs 2、connection_view/util.rs 2 |
| 集成测试 | tests/engine_live.rs 2 个,默认 #[ignore],依赖外网 |
| 无测试的源文件 | 43 / 48 |
| 静态检查 | cargo clippy --all-targets 零告警 |
| CI 覆盖 | cargo test --lib(Linux,当前必失败)+ 默认 cargo build;不编译 tests/examples |
7.2 覆盖矩阵
| 模块 | 单测 | 说明 |
|---|---|---|
| model(纯函数) | ✅ 充分 | 覆盖了 Hex 奇数长度 panic、UTF-8 截断边界、字节预算、向后兼容反序列化等真实缺陷 |
| store | ✅ 基本够 | 导入导出往返、BOM、损坏备份 |
| update(纯函数) | ⚠️ 偏弱 | 版本比较/资产挑选有覆盖;签名、解压、安装路径三条最危险的分支零覆盖;"命名约定"测试是同义反复(把同一个 format! 再写一遍),无法发现与 CI/脚本的偏离 |
| mqtt(引擎) | ❌ 零 | 并发、生命周期、代数、取消语义全无自动化验证 |
| aliyun | ❌ 零 | 签名是纯函数,最适合表驱动测试 |
| ui | ❌ 仅 util | 表单校验、对话框生命周期无覆盖 |
根本问题不是测试写得差,而是模块选错了:测试全押在纯数据层,把风险最高的引擎留给了手工验证。
7.3 建议补的最小测试集(均无需外网)
- 连接代数与
remove_handle_if语义、close期间建连完成的丢弃路径; max_reconnect_times计数与成功后清零;topic_matches的 MQTT 规范边界(a/#vsa、+vs 空层、sport/+vssport/);session_params_changed的字段完备性(遍历式防漏);- 阿里云两种签名对官方测试向量;
connection_form::build的边界(端口 0 / 65536、receive_maximum 0、遗嘱主题带#);- 主题 JSON 键名与
schema.rs对齐。
8. README 与实现不一致清单
| # | README | 实际 |
|---|---|---|
| 1 | L13 "用户属性…连接/遗嘱/发布三处均支持" | 只有发布支持。ConnectionConfig 无该字段;LastWill 只有 content_type/response_topic |
| 2 | L160 "Windows / Linux 双平台 release 构建" | release.yml:62-65 Windows 矩阵整段被注释,CI 只出 Linux |
| 3 | L160 命名 mqttx-<ver>-<target>-<ext> |
实际是 mqttx-<ver>-<target>-mqttx[.exe](<ext> 易被误读) |
| 4 | L161 只写"上传到包注册表" | 完全漏掉 Release 附件(7z + 裸二进制 + .sha256)这一 OTA 唯一通道与命名约定 |
| 5 | L99 "前置:Rust nightly" | 无 rust-toolchain.toml;CI 用默认 stable;代码无 #![feature] 🟡 |
| 6 | L118 MQTTX_LIVE=1 |
代码从不读该变量,只有 #[ignore] 生效 |
| 7 | L132 只需 package 写权限 | 脚本还要创建 Release/上传附件,需 repo 写权限 |
| 8 | L6 归档链接 ../../archive/tauri-legacy.tar.gz |
不存在;真实归档是分支 tauri-legacy |
| 9 | L24 快捷键列表 | 缺 Ctrl+Shift+R(资源监控);Ctrl+Enter 只在主题/负载有焦点时生效 |
| 10 | L26 设置项列表 | 漏了手动检查更新/下载进度 |
| 11 | L29-37 技术栈表 | 漏 sentry(全文未提会把 panic 上报到自建 Sentry,且该功能无开关)、ureq/sha2/sevenz-rust/rfd/rustls/smol 等 |
| 12 | L43-95 目录树 | 漏 build.rs、resources/、examples/flood.rs、linear-theme.json、scripts/、.cargo/config.toml、.gitea/ |
| 13 | L178 "release ~17MB" | 实测 20.4 MB |
| 14 | 功能特性 | 完全未提 OTA 自更新与资源监控两个子系统;关于页宣称 Apache-2.0 但仓库无 LICENSE |
| 15 | — | 已核对一致的部分:数据目录表、BOM 容忍、产物路径、Ctrl+N / Ctrl+, / Ctrl+Shift+E / Ctrl+Shift+I、重连与超时字段、订阅 v5 选项、分组 chips、.cargo/config.toml 镜像说明 |
9. 修复路线图
批次 0 —— 解阻发布(约半天,最高 ROI)
sysmon测试加#[cfg(target_os = "windows")]或分平台断言;test job 复用 build job 的系统依赖安装,并核实libasound2-dev是否多余(Cargo.lock中无 alsa/cpal 相关包)。- 发布脚本入口强制 semver 正则校验 +
--exact-match,不匹配即失败。 - CI 增加
tag == Cargo.toml version断言。 - Release 创建/上传失败一律
exit 1。 - 验收:本地
cargo test --lib在 Linux 容器内通过;打一个测试 tag,CI 全绿并产出 Release 资产;客户端能发现该版本。
批次 1 —— 安全与数据完整性(1~2 天)
- 7z 解压改
decompress_with_extract_fn:白名单文件名、拒绝../盘符/UNC、限制条目数与总量;引入 ed25519 签名验证。 - Unix 下
set_permissions(0o755)。 - 无侧车不静默跳过;安装前复核哈希;consent 绑定哈希;安装互斥锁。
- 事件总线改有界 + 显式丢弃策略;日志落盘移出事件热路径(批量/异步)。
- 跨卷 rename 回退
copy+fsync+replace;.old保留至新版本健康启动。 - 验收:在 Linux 上完整走通一次自更新;构造一个带
..条目的 7z 验证被拒绝;构造洪水流量观察内存曲线有上界。
批次 2 —— 正确性与边界(1~2 天)
Receive Maximum/Maximum Packet Size拒 0;端口1..=65535;遗嘱主题 trim + 通配符校验。session_params_changed补 4 字段并改成防漏式测试。platform_triple用std::env::consts::ARCH。test_connection补spawn_blocking;重连指数退避 + 抖动。- 主题 JSON 键改
"font.size"/"radius.lg"。 - 验收:新增单测全绿;CI 加
cargo clippy -D warnings与cargo test --lib --tests(#[ignore]只跳执行、仍会编译)。
批次 3 —— 体验与工程化(2~3 天)
- 消息/日志列表虚拟化 + 帧级缓存。
- 对话框栈守卫下沉到各
dialogs::*::open;close_dialog语义明确化。 - 实现
OptionDelegate::position;KvEditor 空/重复 key 显式提示。 - 阿里云对话框语义对齐(预填即选中、按预设复用连接)。
- 补 LICENSE 与 Cargo 元数据;README 按第 8 节逐条订正。
10. 附录
附录 A:方法与验证记录
| 动作 | 结果 |
|---|---|
cargo test -p mqttx-desktop --lib |
33 passed / 0 failed(0.19s,Windows) |
cargo clippy -p mqttx-desktop --all-targets |
零告警 |
git status |
干净(仅 .zcodeignore 未跟踪);本报告为唯一新增文件 |
git describe --tags --always |
v1.0.0-4-gbc9f55c(仓库唯一 tag 为 v1.0.0) |
| 依赖核对 | 直接读取 Cargo.lock(905 包 / 89 个多版本)与 cargo registry 源码(sevenz-rust-0.6.1、gpui-component-0.6.6、embed-resource、ureq-2.12.1) |
| 未完成的核验 | 阿里云官方文档(本环境 help.aliyun.com 解析到非公网 IP,web_fetch 被拒;web_search 超时) |
附录 B:可信度台账
| 结论 | 状态 |
|---|---|
Linux 单测必失败、git describe 产出垃圾版本、非 Windows 缺 +x、主题键名不生效、阿里云对话框空保存复制预设、7z 路径穿越 |
✅ 实测 / 代码读取 |
ureq 默认 https_only=false 且跟随重定向;manifest_required() 的失败语义 |
⚠️ 依赖源码读取,未做实验 |
| 阿里云 content 拼装"错误" | ❌ 未采信(见 6.4) |
| rc.exe 缺失导致构建失败、Windows 跨卷 rename 失败 | 🟡 语义明确但未在目标环境复现 |
| MQTT 5 参数语义(Receive Maximum / Maximum Packet Size 为 0 即 Protocol Error) | ✅ 规范明确 + 代码路径已核对 |
附录 C:未决 / 需实测问题
- 阿里云一机一密 signing content:需官方测试向量离线比对(见 6.4)。
securemode=2+ 明文 TCP:1883 是否被 broker 拒绝:需真实设备实测;无论结果如何,"声明与传输必须一致"这一条成立。- stable 工具链能否构建:本机只用 nightly 验证过;CI 用 stable,需一次干净环境实测。
libasound2-dev是否必要:Cargo.lock中无 alsa/cpal/libpulse/jack,倾向于多余,但需在干净 runner 上验证。- 发布脚本
git credential fill回退路径的实际作用域:取决于本机 GCM 配置,需按最小权限原则收窄。
附录 D:关键文件索引
| 关注点 | 文件 |
|---|---|
| 引擎与生命周期 | gpui-app/src/mqtt/mod.rs、v5.rs、v4.rs、tls.rs |
| 数据模型 | gpui-app/src/model/*.rs |
| 持久化 | gpui-app/src/store.rs |
| OTA | gpui-app/src/update/{mod,check,install}.rs |
| 阿里云 | gpui-app/src/aliyun.rs |
| UI 外壳与状态 | gpui-app/src/ui/app/*.rs |
| 连接工作区 | gpui-app/src/ui/connection_view/*.rs |
| 表单与对话框 | gpui-app/src/ui/connection_form.rs、gpui-app/src/ui/dialogs/*.rs |
| 发布与 CI | .gitea/workflows/release.yml、scripts/publish-gitea.{sh,ps1} |
| 测试与压测 | gpui-app/tests/engine_live.rs、gpui-app/examples/flood.rs |
本报告基于 HEAD bc9f55c 的只读分析,未对仓库源文件做任何修改。