fix(app): OTA 信任链加固、引擎背压与协议正确性

OTA(update/):
- 解压前校验 7z 条目清单:sevenz-rust 0.6.1 的 decompress_file 直接 dest.join(entry.name()),不校验 ".."、绝对路径与盘符,恶意归档可越界写文件;现在要求恰好一个条目且名字与约定二进制一致(fail-closed)
- 无 .sha256 侧车时拒绝安装,不再静默跳过校验而 UI 仍宣称已校验
- 安装前重算暂存文件哈希;同意标记从仅版本号改为绑定「版本 + 哈希」
- 跨卷 rename 失败回退「复制 + fsync + 删源」(暂存在 APPDATA、exe 在安装目录,不同卷时必失败且报错误导)
- .old 回滚备份保留 7 天:原实现新版本首启即删,新版本一旦崩溃用户无任何回退路径
- 新增安装互斥锁:两实例同时安装会互删暂存包、争抢同一个 .old
- platform_triple 用 std::env::consts::ARCH:写死 x86_64 让 ARM 客户端永远挑不到产物
- pick_assets 按命名约定精确后缀匹配:非 Windows 下 ends_with("") 恒真,.tar.gz/.deb 会被当成可执行文件下载并替换自身

引擎(mqtt/):
- 事件通道改有界并分级:高频事件(消息/日志)满即丢并计数、每 1000 条提示一次;状态/订阅结果/发布回执异步补投。原无界通道在到达速率超过消费速率时内存无上界(单条最长 128KB)
- 重连改指数退避(1s→30s 封顶)+ 0~500ms 抖动,替代固定 2 秒的惊群式重试
- 测试连接的建连(读证书,阻塞 IO)移入 spawn_blocking,与 connect 一致

正确性与性能:
- session_params_changed 补齐 4 个 v5 CONNECT 属性(会话过期/接收上限/最大报文/主题别名),漏掉会让"重连后生效"提示静默失效;测试改逐字段防漏式断言
- ConnectionConfig / AliyunPreset 手写脱敏 Debug:password 与 AccessKeySecret/DeviceSecret 输出 <redacted>,杜绝一次 {:?} 把凭据送进 stderr 与 Sentry
- MqttRecord.user_properties 改 Arc<[(String,String)]>、应用日志改 Arc<LogEntry>:消息面板每帧 300 条、日志面板每帧 3000 条的深拷贝降为引用计数
- 阿里云 DeviceName 空值校验,避免拼出非法 clientId 后服务端报错难懂
This commit is contained in:
2026-09-30 10:15:40 +08:00
parent 2661a734ee
commit 9b6df8b9eb
15 changed files with 451 additions and 90 deletions
+17 -8
View File
@@ -2,7 +2,6 @@
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use rumqttc::mqttbytes::v5::{
ConnectReturnCode as V5ReturnCode, Packet as V5Packet,
@@ -103,7 +102,12 @@ pub(super) async fn test_handshake(cfg_in: &ConnectionConfig) -> Result<(), Stri
cfg.client_id = format!("mqttx_test_{}", &uuid::Uuid::new_v4().simple().to_string()[..8]);
let cfg = &cfg;
if cfg.protocol.is_v5() {
let (client, mut eventloop) = build_v5(cfg)?;
// 建 client 会读 CA/证书文件(阻塞 IO):与 connect 保持一致放 spawn_blocking,
// 避免占住 2 线程引擎 runtime 的 worker
let owned = cfg.clone();
let (client, mut eventloop) = tokio::task::spawn_blocking(move || build_v5(&owned))
.await
.map_err(|e| format!("内部任务失败: {e}"))??;
loop {
match eventloop.poll().await {
Ok(V5Event::Incoming(V5Packet::ConnAck(ack))) => {
@@ -129,7 +133,11 @@ pub(super) async fn test_handshake(cfg_in: &ConnectionConfig) -> Result<(), Stri
}
}
} else {
let (client, mut eventloop) = build_v4(cfg)?;
// 同上:v4 分支同样把阻塞的建连放到阻塞线程池
let owned = cfg.clone();
let (client, mut eventloop) = tokio::task::spawn_blocking(move || build_v4(&owned))
.await
.map_err(|e| format!("内部任务失败: {e}"))??;
loop {
match eventloop.poll().await {
Ok(rv4::Event::Incoming(rv4::Packet::ConnAck(ack))) => {
@@ -193,7 +201,7 @@ pub(super) fn spawn_v4_loop(
},
retain: p.retain,
timestamp: now_ms(),
user_properties: Vec::new(),
user_properties: Default::default(),
content_type: None,
response_topic: None,
correlation_data: None,
@@ -294,13 +302,14 @@ pub(super) fn spawn_v4_loop(
break;
}
reconnect_attempts += 1;
let delay = reconnect_delay(reconnect_attempts);
engine.log(&id, LogLevel::Warn, "connection_error",
format!("连接错误,2 秒后重连(第 {reconnect_attempts} 次): {e}"), None);
// 退避等待也必须可取消:否则 close() 的取消信号最长
// 2 秒不可见,期间会对用户已关闭的连接继续重连。
format!("连接错误,{:.1} 秒后重连(第 {reconnect_attempts} 次): {e}", delay.as_secs_f32()), None);
// 退避等待也必须可取消:否则 close() 的取消信号要等一整个
// 退避周期才可见,期间会对用户已关闭的连接继续重连。
tokio::select! {
_ = cancel_rx.changed() => break,
_ = tokio::time::sleep(Duration::from_secs(2)) => {}
_ = tokio::time::sleep(delay) => {}
}
}
}