diff --git a/Windows/CS/Framework4.0/Updater/src/main.rs b/Windows/CS/Framework4.0/Updater/src/main.rs index f524c3a..f067226 100644 --- a/Windows/CS/Framework4.0/Updater/src/main.rs +++ b/Windows/CS/Framework4.0/Updater/src/main.rs @@ -98,10 +98,16 @@ struct UpdateContext { server_updater_version: Mutex>, /// 当前更新阶段 current_phase: Mutex, - /// 候选应用列表(阶段3使用) - candidates: Mutex>, + /// 候选应用列表 (app_name, local_version),阶段3使用 + candidates: Mutex>, /// 等待 GetAllFile 响应的应用列表(阶段3.5使用) pending_allfile_apps: Mutex>, + /// 待下载队列 (app_name, filename, offset, expected_md5),顺序下载 + download_queue: Mutex>, + /// 是否正在下载中(队列中有待处理项) + is_downloading: Mutex, + /// 当前正在下载的文件的期望 MD5(DownloadComplete 校验用) + current_download_md5: Mutex>, } impl Default for UpdateContext { @@ -114,6 +120,9 @@ impl Default for UpdateContext { current_phase: Mutex::new(UpdatePhase::BootLoader), candidates: Mutex::new(Vec::new()), pending_allfile_apps: Mutex::new(Vec::new()), + download_queue: Mutex::new(std::collections::VecDeque::new()), + is_downloading: Mutex::new(false), + current_download_md5: Mutex::new(None), } } } @@ -430,10 +439,17 @@ fn compute_file_hash(filename: &str, bytes: u64, _debug: bool) -> Option /// 计算文件的完整 MD5 hash fn compute_file_md5(file_path: &PathBuf) -> Option { if !file_path.exists() { + log_print!("[MD5] 文件不存在: {:?}", file_path); return None; } - let file_data = fs::read(file_path).ok()?; + let file_data = match fs::read(file_path) { + Ok(data) => data, + Err(e) => { + log_print!("[MD5] 读取文件失败: {:?}, error: {}", file_path, e); + return None; + } + }; let hash = md5::compute(&file_data); Some(format!("{:x}", hash)) } @@ -749,54 +765,7 @@ fn get_app_tmp_file_size(app_name: &str, relative_path: &str) -> u64 { tmp_path.metadata().map(|m| m.len()).unwrap_or(0) } -/// 获取其他应用的本地版本号(通过 PowerShell 读取 PE 版本) -fn get_local_app_version(app_name: &str, relative_path: &str) -> String { - // 应用主 exe 一般在相对路径的顶级,如 app.exe 或 bin/app.exe - // 取第一段目录名或文件名作为 exe 查找起点 - let file_path = get_updater_data_dir() - .join(app_name) - .join(relative_path); - if !file_path.exists() { - return "0.0.0".to_string(); - } - - #[cfg(windows)] - { - let path_str = file_path.to_string_lossy().to_string(); - let ps_script = format!( - "(Get-Item -LiteralPath '{}' -ErrorAction SilentlyContinue).VersionInfo.FileVersion", - path_str.replace("'", "''") - ); - - let output = Command::new("powershell") - .args(["-NoProfile", "-NonInteractive", "-Command", &ps_script]) - .output(); - - if let Ok(output) = output { - let stdout = String::from_utf8_lossy(&output.stdout); - let version = stdout.trim(); - if !version.is_empty() && version != "0" && !version.contains("没有版本") { - return version.to_string(); - } - } - - if let Ok(metadata) = fs::metadata(&file_path) { - use std::time::UNIX_EPOCH; - if let Ok(modified) = metadata.modified() { - let duration = modified.duration_since(UNIX_EPOCH).unwrap_or_default(); - return format!("{}.{}", duration.as_secs(), duration.subsec_nanos() / 1_000_000); - } - } - - "0.0.0".to_string() - } - - #[cfg(not(windows))] - { - "0.0.0".to_string() - } -} /// 向服务器查询指定文件的 md5(用于断点续传校验) #[allow(dead_code)] @@ -835,6 +804,19 @@ fn request_download_for_app( sender.send(msg_str); } + +/// 从待下载队列取出一个文件发送(顺序下载) +fn send_next_download(ctx: &Arc, sender: &cube_lib::websocket::MessageSender) { + let mut queue = ctx.download_queue.lock().unwrap(); + if let Some((app_name, filename, offset, expected_md5)) = queue.pop_front() { + // 记录当前下载的期望 MD5,供 DownloadComplete 校验 + *ctx.current_download_md5.lock().unwrap() = Some(expected_md5); + request_download_for_app(sender, &app_name, &filename, offset); + } else { + *ctx.is_downloading.lock().unwrap() = false; + } +} + /// 处理其他应用的文件块(写入 AppData/{app_name}/{relative_path}) fn handle_app_file_chunk( ctx: &Arc, @@ -855,8 +837,12 @@ fn handle_app_file_chunk( let key = format!("{}/{}", app_name, filename); let mut app_map = ctx.app_download_map.lock().unwrap(); - // 新文件或 offset=0:重置状态 - if !app_map.contains_key(&key) || offset == 0 { + // 获取当前正在下载的文件名(用于判断是否是新文件) + let current_filename = app_map.values().next().map(|s| s.filename.clone()); + + // 新文件(非续传):清空旧 entry,等待 DownloadComplete 处理旧文件 + // 续传(同一文件 + 非零 offset):追加数据,不清 entry + if current_filename.as_ref() != Some(&filename.to_string()) && (!app_map.contains_key(&key) || offset == 0) { if let Some(f) = app_map.get_mut(&key) { if let Some(file) = f.file.take() { drop(file); @@ -867,16 +853,22 @@ fn handle_app_file_chunk( } app_map.remove(&key); + // filename 可能含路径分隔符,取纯文件名部分 + let tmp_filename = std::path::Path::new(filename) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or(filename); + let app_data_dir = get_updater_data_dir().join(app_name); let _ = fs::create_dir_all(&app_data_dir); - let final_path = app_data_dir.join(filename); + let final_path = app_data_dir.join(tmp_filename); let _ = fs::create_dir_all(final_path.parent().unwrap()); let temp_path = get_updater_data_dir() .join("Updater") .join("UpGrade") .join(app_name) - .join(format!("{}.tmp", filename)); + .join(format!("{}.tmp", tmp_filename)); match File::create(&temp_path) { Ok(file) => { @@ -934,8 +926,15 @@ fn handle_app_file_chunk( let final_offset = app_map.get(&key).map(|s| s.offset).unwrap_or(0); if let Some(ref temp) = temp_path { - let app_data_dir = get_updater_data_dir().join(app_name); - let final_path = app_data_dir.join(filename); + // filename 来自服务端消息,可能含路径前缀(如 "sticker/app_config.json") + // 只取纯文件名,与 DownloadComplete 处理器保持路径一致 + let final_filename = std::path::Path::new(filename) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or(filename); + // final_path 与 tmp 文件同一目录(升级目录),不是 app_data_dir + let upgrade_dir = temp.parent().unwrap(); + let final_path = upgrade_dir.join(final_filename); let _ = fs::create_dir_all(final_path.parent().unwrap()); if let Err(e) = fs::rename(temp, &final_path) { @@ -976,15 +975,22 @@ fn handle_app_download_complete( if let Some(ref temp) = temp_path { let app_data_dir = get_updater_data_dir().join(app_name); - let final_path = app_data_dir.join(filename); + // filename 可能含路径分隔符,取纯文件名部分以避免路径重复 + let tmp_filename = std::path::Path::new(filename) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or(filename); + let final_path = app_data_dir.join(tmp_filename); let _ = fs::create_dir_all(final_path.parent().unwrap()); if let Err(e) = fs::rename(temp, &final_path) { - if debug { - eprintln!("[应用] 重命名失败 {}: {}", filename, e); - } + let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + log_print!("{} [应用] 重命名失败 {} -> {:?}: {},改用 copy", ts, filename, final_path, e); let _ = fs::copy(temp, &final_path); let _ = fs::remove_file(temp); + } else { + let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + log_print!("{} [应用] 重命名成功 {} -> {:?}", ts, filename, final_path); } if debug { @@ -1120,8 +1126,41 @@ fn is_process_running_ex(process_name: &str, exclude_pid: Option) -> bool { count > 0 } -/// 获取 AppData 目录下所有候选应用(排除 Updater,返回应用名称列表) -fn get_app_candidates(debug: bool) -> Vec { +/// 从运行中进程获取版本号(通过进程的可执行文件路径) +#[cfg(windows)] +fn get_version_from_process(app_name: &str) -> String { + let ps_script = format!( + "$p = Get-Process -Name '{}' -ErrorAction SilentlyContinue | Select-Object -First 1; \ + if ($p -and $p.Path) {{ (Get-Item $p.Path -ErrorAction SilentlyContinue).VersionInfo.FileVersion }} \ + elseif ($p -and $p.Id) {{ \ + $wmi = Get-CimInstance Win32_Process -Filter \"ProcessId=$($p.Id)\" -ErrorAction SilentlyContinue; \ + if ($wmi.ExecutablePath) {{ (Get-Item $wmi.ExecutablePath -ErrorAction SilentlyContinue).VersionInfo.FileVersion }} \ + }}", + app_name.replace("'", "''") + ); + + let output = Command::new("powershell") + .args(["-NoProfile", "-NonInteractive", "-Command", &ps_script]) + .output(); + + if let Ok(output) = output { + let stdout = String::from_utf8_lossy(&output.stdout); + let version = stdout.trim(); + if !version.is_empty() && version != "0" && !version.contains("没有文件") { + return version.to_string(); + } + } + "0.0.0".to_string() +} + +#[cfg(not(windows))] +fn get_version_from_process(_app_name: &str) -> String { + "0.0.0".to_string() +} + +/// 获取 AppData 目录下所有候选应用(排除 Updater,返回 (app_name, local_version) 列表) +/// 版本号在进程运行期间直接从进程路径读取,保证版本与进程实际加载的一致 +fn get_app_candidates(debug: bool) -> Vec<(String, String)> { let appdata = get_updater_data_dir(); // X:\AppData\ if !appdata.exists() { return Vec::new(); @@ -1138,10 +1177,11 @@ fn get_app_candidates(debug: bool) -> Vec { // 检查该目录是否对应一个运行中的进程 let exe_name = format!("{}.exe", name); if is_process_running(&exe_name) { + let local_version = get_version_from_process(name); if debug { - println!("[应用] 候选应用: {} (进程运行中)", name); + println!("[应用] 候选应用: {} v{} (进程运行中)", name, local_version); } - candidates.push(name.to_string()); + candidates.push((name.to_string(), local_version)); } else if debug { println!("[应用] 跳过 {} (进程未运行)", name); } @@ -1509,12 +1549,14 @@ async fn run_updater(debug_mode: bool) -> bool { } } else { *ctx_clone.current_phase.lock().unwrap() = UpdatePhase::Apps; - log_print!("{} [阶段3] 检查应用: {:?}", ts, candidates); + // 打印候选应用及其预存版本 + let app_names: Vec = candidates.iter().map(|(n, v)| format!("{}(v{})", n, v)).collect(); + log_print!("{} [阶段3] 检查应用: {:?}", ts, app_names); // 构建应用版本查询 let mut file_list = Vec::new(); - for app in &candidates { - file_list.push(format!("{}\\{}.exe", app, app)); + for (app_name, _) in &candidates { + file_list.push(format!("{}\\{}.exe", app_name, app_name)); } let file_list_json = serde_json::to_string(&file_list).unwrap_or_else(|_| "[]".to_string()); let msg_str = format!( @@ -1537,11 +1579,13 @@ async fn run_updater(debug_mode: bool) -> bool { } } else { *ctx_clone.current_phase.lock().unwrap() = UpdatePhase::Apps; - println!("{} [阶段3] 检查应用: {:?}", ts, candidates); + // 打印候选应用及其预存版本 + let app_names: Vec = candidates.iter().map(|(n, v)| format!("{}(v{})", n, v)).collect(); + println!("{} [阶段3] 检查应用: {:?}", ts, app_names); let mut file_list = Vec::new(); - for app in &candidates { - file_list.push(format!("{}\\{}.exe", app, app)); + for (app_name, _) in &candidates { + file_list.push(format!("{}\\{}.exe", app_name, app_name)); } let file_list_json = serde_json::to_string(&file_list).unwrap_or_else(|_| "[]".to_string()); let msg_str = format!( @@ -1556,10 +1600,11 @@ async fn run_updater(debug_mode: bool) -> bool { UpdatePhase::Apps => { // ========== 阶段3:处理应用版本,决定是否需要升级 ========== + // 使用预存的版本(在候选发现阶段已从运行中进程读取) let candidates = ctx_clone.candidates.lock().unwrap().clone(); let mut apps_to_update = Vec::new(); - for app_name in &candidates { + for (app_name, local_version) in candidates { let exe_name = format!("{}\\{}.exe", app_name, app_name); if let Some(server_ver) = file_versions.get(&exe_name) { let server_version = server_ver.as_str().unwrap_or(""); @@ -1568,7 +1613,6 @@ async fn run_updater(debug_mode: bool) -> bool { log_print!("{} [应用] {} 服务端版本为空,跳过升级", ts, app_name); continue; } - let local_version = get_local_app_version(app_name, &exe_name); log_print!("{} [应用] {}: 服务端={}, 本地={}", ts, app_name, server_version, local_version); @@ -1756,7 +1800,8 @@ async fn run_updater(debug_mode: bool) -> bool { } } - // 3. 处理服务端文件列表:比较 MD5,决定如何处理 + // 3. 处理服务端文件列表:比较 MD5,将需要下载的文件加入队列 + let mut downloads_to_queue: Vec<(String, String, u64, String)> = Vec::new(); for (filename, _, server_md5) in &server_files { let file_path = upgrade_base.join(filename); let tmp_filename = format!("{}.tmp", filename); @@ -1767,9 +1812,8 @@ async fn run_updater(debug_mode: bool) -> bool { if has_tmp { if tmp_is_zero { - // 情况0:本地有 0 字节临时文件(0 字节未上报服务端,无法比对 MD5) - // 直接从 offset 0 断点续传 - request_download_for_app(&sender, &app_name, filename, 0); + // 情况0:本地有 0 字节临时文件,直接从头下载 + downloads_to_queue.push((app_name.to_string(), filename.to_string(), 0, server_md5.clone())); update_performed_clone2.store(true, std::sync::atomic::Ordering::SeqCst); } else { // 情况A:本地有临时文件(>0字节)→ 比较临时文件 @@ -1780,10 +1824,12 @@ async fn run_updater(debug_mode: bool) -> bool { if let Ok(file) = fs::File::create(&tmp_path) { let _ = file.set_len(0); } - request_download_for_app(&sender, &app_name, filename, 0); + downloads_to_queue.push((app_name.to_string(), filename.to_string(), 0, server_md5.clone())); update_performed_clone2.store(true, std::sync::atomic::Ordering::SeqCst); } else { log_print!("{} [AllFile] tmp {} MD5一致,续传", ts, tmp_filename); + let file_size = fs::metadata(&tmp_path).map(|m| m.len()).unwrap_or(0); + downloads_to_queue.push((app_name.to_string(), filename.to_string(), file_size, server_md5.clone())); } } } @@ -1798,7 +1844,7 @@ async fn run_updater(debug_mode: bool) -> bool { } if let Ok(_) = File::create(&tmp_path) { log_print!("{} [AllFile] 创建空 tmp 文件: {}", ts, tmp_filename); - request_download_for_app(&sender, &app_name, filename, 0); + downloads_to_queue.push((app_name.to_string(), filename.to_string(), 0, server_md5.clone())); update_performed_clone2.store(true, std::sync::atomic::Ordering::SeqCst); } } else { @@ -1813,28 +1859,33 @@ async fn run_updater(debug_mode: bool) -> bool { } if let Ok(_) = File::create(&tmp_path) { log_print!("{} [AllFile] 创建空 tmp 文件: {}", ts, tmp_filename); - request_download_for_app(&sender, &app_name, filename, 0); + downloads_to_queue.push((app_name.to_string(), filename.to_string(), 0, server_md5.clone())); update_performed_clone2.store(true, std::sync::atomic::Ordering::SeqCst); } } } - // 4. 为有效的临时文件(>0字节)发送续传请求 - for (filename, _, _) in &server_files { - let tmp_filename = format!("{}.tmp", filename); - let tmp_path = upgrade_base.join(&tmp_filename); - if tmp_path.exists() { - let file_size = fs::metadata(&tmp_path).map(|m| m.len()).unwrap_or(0); - // 0 字节 tmp 已在步骤3中处理过,跳过 - if file_size == 0 { - continue; - } - request_download_for_app(&sender, &app_name, filename, file_size); + // 4. 将所有待下载文件加入队列,按顺序发送第一个 + if !downloads_to_queue.is_empty() { + let mut queue = ctx_clone.download_queue.lock().unwrap(); + for item in downloads_to_queue { + queue.push_back(item); + } + drop(queue); + + // 如果当前没有在下载,发第一个 + let should_start = { + let downloading = ctx_clone.is_downloading.lock().unwrap(); + !*downloading + }; + if should_start { + *ctx_clone.is_downloading.lock().unwrap() = true; + send_next_download(&ctx_clone, &sender); } } - // 6. 检查是否所有应用都处理完成 - if pending_apps.is_empty() { + // 6. 检查是否所有应用都处理完成(AllFile 收完 且 队列清空) + if pending_apps.is_empty() && ctx_clone.download_queue.lock().unwrap().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); @@ -1855,12 +1906,17 @@ async fn run_updater(debug_mode: bool) -> bool { // ========== 阶段3.5:处理应用 FileChunk ========== if current_phase == UpdatePhase::AppsWaitAllFile && !filename.is_empty() { let candidates = ctx_clone.candidates.lock().unwrap().clone(); - for app_name in &candidates { + for (app_name, _) in &candidates { + // filename 可能含路径分隔符,取纯文件名部分来构造 tmp 路径 + let tmp_filename = std::path::Path::new(filename) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or(filename); let upgrade_path = get_updater_data_dir() .join("Updater") .join("UpGrade") .join(app_name) - .join(filename); + .join(format!("{}.tmp", tmp_filename)); if !upgrade_path.exists() { continue; } @@ -1872,6 +1928,8 @@ async fn run_updater(debug_mode: bool) -> bool { println!("{} [应用] {} / {} 下载完成", ts, app, file); } ctx_clone.app_completed_set.lock().unwrap().insert(format!("{}/{}", app, file)); + // 一个文件下完,发下一个 + send_next_download(&ctx_clone, &sender); } break; } @@ -1904,23 +1962,76 @@ async fn run_updater(debug_mode: bool) -> bool { } let current_phase = *ctx_clone.current_phase.lock().unwrap(); + let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + + // 调试:所有 DownloadComplete 都打印阶段信息 + let phase_name = match current_phase { + UpdatePhase::BootLoader => "BootLoader", + UpdatePhase::Updater => "Updater", + UpdatePhase::Apps => "Apps", + UpdatePhase::AppsWaitAllFile => "AppsWaitAllFile", + UpdatePhase::Complete => "Complete", + }; + log_print!("{} [DownloadComplete] 收到文件: {}, 当前阶段: {}", ts, filename, phase_name); // ========== 阶段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() + let candidate_names: Vec<&str> = candidates.iter().map(|(n, _)| n.as_str()).collect(); + log_print!("{} [DownloadComplete] 文件: {}, 候选应用: {:?}", ts, filename, candidate_names); + for (app_name, _) in &candidates { + // filename 可能含路径分隔符(如 "EasyTest/Audio.wav"),只取纯文件名部分 + let tmp_filename = std::path::Path::new(filename) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or(filename); + let tmp_path = get_updater_data_dir() .join("Updater") .join("UpGrade") .join(app_name) - .join(filename); - if !upgrade_path.exists() { + .join(format!("{}.tmp", tmp_filename)); + // rename 后的最终路径(与 handle_app_download_complete 内部保持一致) + let final_path = get_updater_data_dir().join(app_name).join(tmp_filename); + // tmp 或 final 文件存在才处理 + log_print!("{} [DownloadComplete] 检查路径: tmp={:?}, final={:?}, tmp_exists={}, final_exists={}", + ts, tmp_path, final_path, tmp_path.exists(), final_path.exists()); + if !tmp_path.exists() && !final_path.exists() { continue; } let size = data_obj.get("size").and_then(|v| v.as_u64()).unwrap_or(0); handle_app_download_complete(&ctx_clone, app_name, filename, size, debug_msg); ctx_clone.app_completed_set.lock().unwrap().insert(format!("{}/{}", app_name, filename)); + + // 校验下载文件的 MD5 + let expected_md5 = ctx_clone.current_download_md5.lock().unwrap().clone(); + let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + if let Some(expected) = expected_md5 { + let local_md5 = compute_file_md5(&final_path); + if let Some(lm) = local_md5 { + if &lm == &expected { + log_print!("{} [应用] {} MD5校验通过 ({}), 继续下一个文件", ts, filename, expected); + *ctx_clone.current_download_md5.lock().unwrap() = None; + send_next_download(&ctx_clone, &sender); + } else { + log_print!("{} [应用] {} MD5校验失败 (本地={}, 期望={}),重新下载", ts, filename, lm, expected); + // 删除损坏文件,加入队列末尾重新下载 + let _ = fs::remove_file(&final_path); + let mut queue = ctx_clone.download_queue.lock().unwrap(); + queue.push_back((app_name.to_string(), filename.to_string(), 0, expected)); + *ctx_clone.current_download_md5.lock().unwrap() = None; + drop(queue); + send_next_download(&ctx_clone, &sender); + } + } else { + log_print!("{} [应用] {} 无法计算MD5,继续下一个文件", ts, filename); + *ctx_clone.current_download_md5.lock().unwrap() = None; + send_next_download(&ctx_clone, &sender); + } + } else { + // 没有期望MD5(理论上不应发生),直接继续 + send_next_download(&ctx_clone, &sender); + } break; } } else {