From d6a143d34af7b9fb4eedbea6cb7d86e488317b9a Mon Sep 17 00:00:00 2001 From: zqm Date: Thu, 9 Apr 2026 16:34:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E5=85=B6=E5=AE=83=E5=BA=94?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Windows/CS/Framework4.0/Updater/src/main.rs | 370 +++++++++++++++----- 1 file changed, 287 insertions(+), 83 deletions(-) diff --git a/Windows/CS/Framework4.0/Updater/src/main.rs b/Windows/CS/Framework4.0/Updater/src/main.rs index 7bafaf5..ca296db 100644 --- a/Windows/CS/Framework4.0/Updater/src/main.rs +++ b/Windows/CS/Framework4.0/Updater/src/main.rs @@ -77,10 +77,11 @@ macro_rules! log_eprintln { /// 更新流程的三个阶段 #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum UpdatePhase { - BootLoader, // 阶段1:检查 BootLoader.exe - Updater, // 阶段2:检查 Updater.exe - Apps, // 阶段3:检查其他应用 - Complete, // 所有阶段完成 + BootLoader, // 阶段1:检查 BootLoader.exe + Updater, // 阶段2:检查 Updater.exe + Apps, // 阶段3:检查其他应用版本 + AppsWaitAllFile, // 阶段3.5:等待 GetAllFile 响应 + Complete, // 所有阶段完成 } // ===================== 更新上下文(局部状态 + Arc 共享)===================== @@ -99,6 +100,8 @@ struct UpdateContext { current_phase: Mutex, /// 候选应用列表(阶段3使用) candidates: Mutex>, + /// 等待 GetAllFile 响应的应用列表(阶段3.5使用) + pending_allfile_apps: Mutex>, } impl Default for UpdateContext { @@ -110,6 +113,7 @@ impl Default for UpdateContext { server_updater_version: Mutex::new(None), current_phase: Mutex::new(UpdatePhase::BootLoader), candidates: Mutex::new(Vec::new()), + pending_allfile_apps: Mutex::new(Vec::new()), } } } @@ -369,6 +373,7 @@ fn version_less_than(a: &str, b: &str) -> bool { } /// 计算本地文件前 N 字节的 MD5 hash(字节数为 0 表示全部) +#[allow(dead_code)] fn compute_file_hash_in_dir(app_name: &str, filename: &str, bytes: u64, _debug: bool) -> Option { let file_path = get_updater_data_dir() .join("Updater") @@ -422,6 +427,17 @@ fn compute_file_hash(filename: &str, bytes: u64, _debug: bool) -> Option Some(format!("{:x}", hash)) } +/// 计算文件的完整 MD5 hash +fn compute_file_md5(file_path: &PathBuf) -> Option { + if !file_path.exists() { + return None; + } + + let file_data = fs::read(file_path).ok()?; + let hash = md5::compute(&file_data); + Some(format!("{:x}", hash)) +} + /// 获取临时文件的当前大小(字节数) /// 注意:Updater.exe 的临时文件是 Updater.new.exe.tmp fn get_tmp_file_size(filename: &str) -> u64 { @@ -723,6 +739,7 @@ fn handle_download_complete( // ===================== 其他应用程序更新 ===================== /// 获取其他应用文件的临时文件大小 +#[allow(dead_code)] fn get_app_tmp_file_size(app_name: &str, relative_path: &str) -> u64 { let tmp_path = get_updater_data_dir() .join("Updater") @@ -782,6 +799,7 @@ fn get_local_app_version(app_name: &str, relative_path: &str) -> String { } /// 向服务器查询指定文件的 md5(用于断点续传校验) +#[allow(dead_code)] fn request_file_md5_for_app( sender: &cube_lib::websocket::MessageSender, app_name: &str, @@ -1135,6 +1153,7 @@ fn get_app_candidates(debug: bool) -> Vec { /// 递归扫描升级目录,返回 (relative_path, full_path) 列表 /// 跳过 .tmp 文件(如果存在同名非 tmp 文件的话) +#[allow(dead_code)] fn scan_upgrade_dir(app_name: &str) -> Vec<(String, PathBuf)> { let upgrade_base = get_updater_data_dir() .join("Updater") @@ -1188,6 +1207,62 @@ fn scan_upgrade_dir(app_name: &str) -> Vec<(String, PathBuf)> { files } +/// 扫描本地升级目录,返回 .tmp 文件列表(相对路径) +fn scan_local_tmp_files(app_name: &str) -> Vec { + let upgrade_base = get_updater_data_dir() + .join("Updater") + .join("UpGrade") + .join(app_name); + + if !upgrade_base.exists() { + return Vec::new(); + } + + let mut tmp_files = Vec::new(); + let mut visited_dirs = std::collections::VecDeque::new(); + visited_dirs.push_back(upgrade_base.clone()); + + while let Some(dir) = visited_dirs.pop_front() { + if let Ok(entries) = fs::read_dir(&dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + visited_dirs.push_back(path); + } else if path.is_file() { + if let Some(filename) = path.file_name().and_then(|n| n.to_str()) { + if filename.ends_with(".tmp") { + // 计算相对路径 + if let Ok(rel) = path.strip_prefix(&upgrade_base) { + let rel_str = rel.to_string_lossy().replace('\\', "/"); + let rel_str = rel_str.trim_start_matches('/').to_string(); + tmp_files.push(rel_str); + } + } + } + } + } + } + } + + tmp_files +} + +/// 发送 GetAllFile 请求 +fn send_get_all_file( + sender: &cube_lib::websocket::MessageSender, + app_name: &str, + tmp_files: &[String], +) { + let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + let tmp_files_json = serde_json::to_string(tmp_files).unwrap_or_else(|_| "[]".to_string()); + let msg_str = format!( + r#"{{"Type":"GetAllFile","Data":{{"app_name":"{}","tmp_files":{}}}}}"#, + app_name, tmp_files_json + ); + log_print!("{} 发送消息:{}", ts, msg_str); + sender.send(msg_str); +} + /// 主动断连信号(用于在消息回调中请求优雅退出) /// - shutdown_tx: 从同步回调中发送,通知主循环主动断开 /// - disconnect_rx: 在主循环中等待 @@ -1321,6 +1396,9 @@ async fn run_updater(debug_mode: bool) -> bool { let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); match current_phase { + UpdatePhase::AppsWaitAllFile => { + // 等待 AllFile 响应,忽略 FileVer + } UpdatePhase::BootLoader => { // ========== 阶段1:处理 BootLoader ========== if let Some(server_ver) = file_versions.get("BootLoader.exe") { @@ -1458,7 +1536,7 @@ async fn run_updater(debug_mode: bool) -> bool { } UpdatePhase::Apps => { - // ========== 阶段3:处理应用 ========== + // ========== 阶段3:处理应用版本,决定是否需要升级 ========== let candidates = ctx_clone.candidates.lock().unwrap().clone(); let mut apps_to_update = Vec::new(); @@ -1481,39 +1559,24 @@ async fn run_updater(debug_mode: bool) -> bool { } } - // 处理需要更新的应用 - let mut has_downloads = false; - for app_name in &apps_to_update { - let upgrade_files = scan_upgrade_dir(app_name); - if upgrade_files.is_empty() { - log_print!("{} [应用] {} 没有升级文件,跳过", ts, app_name); - continue; + if apps_to_update.is_empty() { + // 没有需要更新的应用,所有阶段完成 + log_print!("{} [阶段完成] 所有应用已是最新版本,等待下次检查...", ts); + *ctx_clone.current_phase.lock().unwrap() = UpdatePhase::Complete; + update_check_done_clone2.store(true, std::sync::atomic::Ordering::SeqCst); + if let Some(tx) = shutdown_tx_arc_clone.lock().unwrap().take() { + let _ = tx.send(()); } - - for (rel_path, _) in upgrade_files { - let tmp_size = get_app_tmp_file_size(app_name, &rel_path); - if tmp_size > 0 { - request_file_md5_for_app(&sender, app_name, &rel_path, tmp_size); - } else { - request_download_for_app(&sender, &rel_path, 0); - } - update_performed_clone2.store(true, std::sync::atomic::Ordering::SeqCst); - has_downloads = true; - } - } - - // 所有阶段完成 - if !has_downloads { - println!("{} [阶段完成] 所有阶段检查完成,等待下次检查...", ts); } else { - println!("{} [阶段完成] 应用下载进行中...", ts); - } - *ctx_clone.current_phase.lock().unwrap() = UpdatePhase::Complete; - update_check_done_clone2.store(true, std::sync::atomic::Ordering::SeqCst); + // 存储待处理的应用列表,发送 GetAllFile 请求 + *ctx_clone.pending_allfile_apps.lock().unwrap() = apps_to_update.clone(); + *ctx_clone.current_phase.lock().unwrap() = UpdatePhase::AppsWaitAllFile; + log_print!("{} [阶段3.5] 等待 GetAllFile 响应 ({} 个应用)", ts, apps_to_update.len()); - // 发送断连信号 - if let Some(tx) = shutdown_tx_arc_clone.lock().unwrap().take() { - let _ = tx.send(()); + for app_name in &apps_to_update { + let tmp_files = scan_local_tmp_files(app_name); + send_get_all_file(&sender, app_name, &tmp_files); + } } } @@ -1532,50 +1595,6 @@ async fn run_updater(debug_mode: bool) -> bool { return; } - let current_phase = *ctx_clone.current_phase.lock().unwrap(); - - // ========== 阶段3:处理应用 Md5 ========== - if current_phase == UpdatePhase::Apps { - // 遍历候选应用,找匹配的相对路径 - let candidates = ctx_clone.candidates.lock().unwrap().clone(); - for app_name in &candidates { - let upgrade_path = get_updater_data_dir() - .join("Updater") - .join("UpGrade") - .join(app_name) - .join(filename); - if !upgrade_path.exists() { - continue; - } - - let server_md5 = md5_data.get("md5").and_then(|v| v.as_str()).unwrap_or(""); - let bytes = md5_data.get("bytes").and_then(|v| v.as_u64()).unwrap_or(0); - - let tmp_filename = format!("{}.tmp", filename); - let local_md5 = compute_file_hash_in_dir(app_name, &tmp_filename, bytes, debug_msg); - - - let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); - if let Some(ref lm) = local_md5 { - log_print!("{} [应用] {} md5: 本地={}, 服务端={}", ts, filename, lm, server_md5); - } - - if local_md5.as_deref() == Some(server_md5) { - request_download_for_app(&sender, filename, bytes); - } else { - let tmp_path = get_updater_data_dir() - .join("Updater") - .join("UpGrade") - .join(app_name) - .join(&tmp_filename); - let _ = fs::remove_file(&tmp_path); - request_download_for_app(&sender, filename, 0); - } - break; - } - return; - } - // ========== BootLoader/Updater 的 Md5 处理 ========== let server_md5 = md5_data.get("md5").and_then(|v| v.as_str()).unwrap_or(""); let bytes = md5_data.get("bytes").and_then(|v| v.as_u64()).unwrap_or(0); @@ -1615,6 +1634,191 @@ async fn run_updater(debug_mode: bool) -> bool { } } + // 处理 AllFile 响应 + if msg_type == "AllFile" { + if let Some(allfile_data) = data.get("Data").and_then(|v| v.as_object()) { + let app_name = allfile_data.get("app_name").and_then(|v| v.as_str()).unwrap_or(""); + if app_name.is_empty() { + return; + } + + // 检查是否在等待此应用的响应 + let mut pending_apps = ctx_clone.pending_allfile_apps.lock().unwrap(); + if !pending_apps.contains(&app_name.to_string()) { + return; + } + pending_apps.retain(|a| a != app_name); + + let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + log_print!("{} [AllFile] 处理 {} 的文件列表", ts, app_name); + + // 获取服务端文件列表 + let server_files: Vec<(String, u64, String)> = allfile_data + .get("files") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter().filter_map(|f| { + let filename = f.get("filename").and_then(|v| v.as_str())?; + let size = f.get("size").and_then(|v| v.as_u64()).unwrap_or(0); + let md5 = f.get("md5").and_then(|v| v.as_str()).unwrap_or(""); + Some((filename.to_string(), size, md5.to_string())) + }).collect() + }) + .unwrap_or_default(); + + // 获取服务端临时文件列表 + let server_tmp_files: Vec<(String, Option, Option)> = allfile_data + .get("tmp_files") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter().filter_map(|f| { + let filename = f.get("filename").and_then(|v| v.as_str())?; + let size = f.get("size").and_then(|v| v.as_u64()); + let md5 = f.get("md5").and_then(|v| v.as_str()); + Some(( + filename.to_string(), + size, + md5.map(|s| s.to_string()), + )) + }).collect() + }) + .unwrap_or_default(); + + // 升级目录 + let upgrade_base = get_updater_data_dir() + .join("Updater") + .join("UpGrade") + .join(app_name); + + // 1. 扫描本地文件 + let mut local_files: std::collections::HashSet = std::collections::HashSet::new(); + if upgrade_base.exists() { + if let Ok(entries) = fs::read_dir(&upgrade_base) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() { + if let Some(name) = path.file_name().and_then(|n| n.to_str()) { + local_files.insert(name.to_string()); + } + } + } + } + } + + // 2. 以服务端为准,删除本地多余文件 + let server_filenames: std::collections::HashSet = server_files + .iter() + .map(|(name, _, _)| name.clone()) + .chain(server_tmp_files.iter().map(|(name, _, _)| name.clone())) + .collect(); + + for local_file in &local_files { + if !server_filenames.contains(local_file) { + let file_path = upgrade_base.join(local_file); + if file_path.exists() { + log_print!("{} [AllFile] 删除多余文件: {}", ts, local_file); + let _ = fs::remove_file(&file_path); + } + } + } + + // 3. 处理临时文件(MD5 不一致则删除) + for (tmp_filename, _, server_md5) in &server_tmp_files { + let tmp_path = upgrade_base.join(tmp_filename); + if tmp_path.exists() { + if let Some(sm) = server_md5 { + // 计算本地 tmp 文件的 md5(全文件) + let local_md5 = compute_file_md5(&tmp_path); + if let Some(lm) = local_md5 { + if &lm != sm { + log_print!("{} [AllFile] tmp {} MD5不一致 (本地={}, 服务端={}),删除", ts, tmp_filename, lm, sm); + let _ = fs::remove_file(&tmp_path); + } else { + log_print!("{} [AllFile] tmp {} MD5一致,跳过", ts, tmp_filename); + } + } + } + } + } + + // 4. 处理正式文件(MD5 不一致则创建空 tmp 文件) + for (filename, _, server_md5) in &server_files { + let file_path = upgrade_base.join(filename); + let tmp_filename = format!("{}.tmp", filename); + let tmp_path = upgrade_base.join(&tmp_filename); + + // 检查是否已有有效的 tmp 文件 + if tmp_path.exists() { + // tmp 已存在,由后续 Md5 响应处理 + continue; + } + + // 检查正式文件是否存在 + if file_path.exists() { + // 计算本地文件的 md5 + let local_md5 = compute_file_md5(&file_path); + if let Some(ref lm) = local_md5 { + if lm != server_md5 { + log_print!("{} [AllFile] {} MD5不一致 (本地={}, 服务端={}),创建 tmp", ts, filename, lm, server_md5); + // 先创建目录 + let _ = fs::create_dir_all(&upgrade_base); + // 创建空 tmp 文件标记需要下载 + if let Ok(_) = File::create(&tmp_path) { + log_print!("{} [AllFile] 创建空 tmp 文件: {}", ts, tmp_filename); + // 为这个正式文件发送下载请求 + log_print!("{} [AllFile] 请求下载 {}", ts, filename); + request_download_for_app(&sender, filename, 0); + update_performed_clone2.store(true, std::sync::atomic::Ordering::SeqCst); + } + } else { + log_print!("{} [AllFile] {} MD5一致,无需更新", ts, filename); + } + } + } else { + // 文件不存在,创建空 tmp + log_print!("{} [AllFile] {} 不存在,创建 tmp", ts, filename); + // 先创建目录 + let _ = fs::create_dir_all(&upgrade_base); + if let Ok(_) = File::create(&tmp_path) { + log_print!("{} [AllFile] 创建空 tmp 文件: {}", ts, tmp_filename); + // 为这个正式文件发送下载请求 + log_print!("{} [AllFile] 请求下载 {}", ts, filename); + request_download_for_app(&sender, filename, 0); + update_performed_clone2.store(true, std::sync::atomic::Ordering::SeqCst); + } + } + } + + // 5. 发送下载请求(处理服务端返回的 tmp 文件) + for (tmp_filename, _, _server_md5) in &server_tmp_files { + let tmp_path = upgrade_base.join(tmp_filename); + let original_filename = tmp_filename.trim_end_matches(".tmp"); + + if tmp_path.exists() { + // tmp 存在,发送请求续传 + let file_size = fs::metadata(&tmp_path).map(|m| m.len()).unwrap_or(0); + // log_print!("{} [AllFile] 请求续传 {} (已下载 {} 字节)", ts, original_filename, file_size); + request_download_for_app(&sender, original_filename, file_size); + } else { + // tmp 不存在,重新下载 + log_print!("{} [AllFile] 请求下载 {}", ts, original_filename); + request_download_for_app(&sender, original_filename, 0); + } + update_performed_clone2.store(true, std::sync::atomic::Ordering::SeqCst); + } + + // 6. 检查是否所有应用都处理完成 + if pending_apps.is_empty() { + log_print!("{} [AllFile] 所有应用文件同步完成,进入 Complete 阶段", ts); + *ctx_clone.current_phase.lock().unwrap() = UpdatePhase::Complete; + update_check_done_clone2.store(true, std::sync::atomic::Ordering::SeqCst); + if let Some(tx) = shutdown_tx_arc_clone.lock().unwrap().take() { + let _ = tx.send(()); + } + } + } + } + // 处理文件块 if msg_type == "FileChunk" { if let Some(data_obj) = data.get("Data").and_then(|v| v.as_object()) { @@ -1622,8 +1826,8 @@ async fn run_updater(debug_mode: bool) -> bool { let current_phase = *ctx_clone.current_phase.lock().unwrap(); - // ========== 阶段3:处理应用 FileChunk ========== - if current_phase == UpdatePhase::Apps && !filename.is_empty() { + // ========== 阶段3.5:处理应用 FileChunk ========== + if current_phase == UpdatePhase::AppsWaitAllFile && !filename.is_empty() { let candidates = ctx_clone.candidates.lock().unwrap().clone(); for app_name in &candidates { let upgrade_path = get_updater_data_dir() @@ -1675,8 +1879,8 @@ async fn run_updater(debug_mode: bool) -> bool { let current_phase = *ctx_clone.current_phase.lock().unwrap(); - // ========== 阶段3:处理应用 DownloadComplete ========== - if current_phase == UpdatePhase::Apps { + // ========== 阶段3.5:处理应用 DownloadComplete ========== + if current_phase == UpdatePhase::AppsWaitAllFile { let candidates = ctx_clone.candidates.lock().unwrap().clone(); for app_name in &candidates { let upgrade_path = get_updater_data_dir()