From e8e45ba012a6f46e78bc0177a05b35f2f70c81ee Mon Sep 17 00:00:00 2001 From: zqm Date: Mon, 13 Apr 2026 10:40:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E5=AE=8C=E6=88=90=E9=87=8D?= =?UTF-8?q?=E5=90=AF=E5=BA=94=E7=94=A8=E7=A8=8B=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Windows/CS/Framework4.0/Updater/src/main.rs | 298 ++++++++++++++------ 1 file changed, 217 insertions(+), 81 deletions(-) diff --git a/Windows/CS/Framework4.0/Updater/src/main.rs b/Windows/CS/Framework4.0/Updater/src/main.rs index 1cf5647..b764a21 100644 --- a/Windows/CS/Framework4.0/Updater/src/main.rs +++ b/Windows/CS/Framework4.0/Updater/src/main.rs @@ -105,8 +105,8 @@ struct UpdateContext { server_updater_version: Mutex>, /// 当前更新阶段 current_phase: Mutex, - /// 候选应用列表 (app_name, local_version),阶段3使用 - candidates: Mutex>, + /// 候选应用列表 (app_name, local_version, exe_path),阶段3使用 + candidates: Mutex>, /// 等待 GetAllFile 响应的应用列表(阶段3.5使用) pending_allfile_apps: Mutex>, /// 待下载队列 (app_name, filename, offset, expected_md5),顺序下载 @@ -115,8 +115,8 @@ struct UpdateContext { is_downloading: Mutex, /// 当前正在下载的文件的期望 MD5(DownloadComplete 校验用) current_download_md5: Mutex>, - /// 已升级的应用列表 (app_name, current_ver, latest_ver),下载完成后通知用 - upgraded_apps: Mutex>, + /// 已升级的应用列表 (app_name, current_ver, latest_ver, exe_path),下载完成后通知用 + upgraded_apps: Mutex>, } impl Default for UpdateContext { @@ -127,7 +127,7 @@ impl Default for UpdateContext { app_completed_set: Mutex::new(HashSet::new()), server_updater_version: Mutex::new(None), current_phase: Mutex::new(UpdatePhase::BootLoader), - candidates: Mutex::new(Vec::new()), + candidates: Mutex::new(Vec::<(String, String, String)>::new()), pending_allfile_apps: Mutex::new(Vec::new()), download_queue: Mutex::new(std::collections::VecDeque::new()), is_downloading: Mutex::new(false), @@ -1185,41 +1185,10 @@ fn is_process_running_ex(process_name: &str, exclude_pid: Option) -> bool { count > 0 } -/// 从运行中进程获取版本号(通过进程的可执行文件路径) -#[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)> { +/// 获取 AppData 目录下所有候选应用(排除 Updater) +/// 返回 (app_name, local_version, exe_path) 列表 +/// 版本号和路径在进程运行期间直接从进程路径读取,保证与进程实际加载的一致 +fn get_app_candidates(debug: bool) -> Vec<(String, String, String)> { let appdata = get_updater_data_dir(); // X:\AppData\ if !appdata.exists() { return Vec::new(); @@ -1233,14 +1202,13 @@ fn get_app_candidates(debug: bool) -> Vec<(String, String)> { if path.is_dir() { if let Some(name) = path.file_name().and_then(|n| n.to_str()) { if name != "Updater" && !name.starts_with('.') { - // 检查该目录是否对应一个运行中的进程 let exe_name = format!("{}.exe", name); if is_process_running(&exe_name) { - let local_version = get_version_from_process(name); + let (local_version, exe_path) = get_version_and_path_from_process(name); if debug { - println!("[应用] 候选应用: {} v{} (进程运行中)", name, local_version); + println!("[应用] 候选应用: {} v{} ({})", name, local_version, exe_path); } - candidates.push((name.to_string(), local_version)); + candidates.push((name.to_string(), local_version, exe_path)); } else if debug { println!("[应用] 跳过 {} (进程未运行)", name); } @@ -1253,6 +1221,151 @@ fn get_app_candidates(debug: bool) -> Vec<(String, String)> { candidates } +/// 从运行中进程获取版本号和可执行文件路径 +#[cfg(windows)] +fn get_version_and_path_from_process(app_name: &str) -> (String, String) { + let ps_script = format!( + "$p = Get-Process -Name '{}' -ErrorAction SilentlyContinue | Select-Object -First 1; \ + if ($p -and $p.Path) {{ \ + $v = (Get-Item $p.Path -ErrorAction SilentlyContinue).VersionInfo.FileVersion; \ + \"$v|$($p.Path)\" \ + }} elseif ($p -and $p.Id) {{ \ + $wmi = Get-CimInstance Win32_Process -Filter \"ProcessId=$($p.Id)\" -ErrorAction SilentlyContinue; \ + if ($wmi.ExecutablePath) {{ \ + $v = (Get-Item $wmi.ExecutablePath -ErrorAction SilentlyContinue).VersionInfo.FileVersion; \ + \"$v|$($wmi.ExecutablePath)\" \ + }} \ + }}", + 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 line = stdout.trim(); + if !line.is_empty() && !line.contains("没有文件") { + if let Some(sep) = line.find('|') { + let version = line[..sep].trim().to_string(); + let path = line[sep + 1..].trim().to_string(); + if !version.is_empty() && version != "0" { + return (version, path); + } + } + } + } + ("0.0.0".to_string(), String::new()) +} + +/// 执行应用文件升级:把升级目录中的文件复制到目标目录 +/// 升级目录:X:\AppData\Updater\UpGrade\{app_name}\* +/// 目标目录:exe_path 的父目录(如 C:\AppData\EasyTest\) +fn upgrade_app_files(app_name: &str, exe_path: &str) { + let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f").to_string(); + + let upgrade_base = get_updater_data_dir() + .join("Updater") + .join("UpGrade") + .join(app_name); + + if !upgrade_base.exists() { + log_print!("{} [升级替换] {} 升级目录不存在: {:?}", ts, app_name, upgrade_base); + return; + } + + // 目标目录 = exe_path 的父目录 + let target_dir = std::path::Path::new(exe_path) + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| std::path::PathBuf::from(".")); + + log_print!("{} [升级替换] {} -> {:?}", ts, app_name, target_dir); + + let mut success_count = 0; + let mut fail_count = 0; + + // 递归扫描升级目录 + fn copy_dir_recursive(src: &std::path::Path, dst: &std::path::Path, ts: &str) -> (i32, i32) { + let mut ok = 0; + let mut fail = 0; + + let _ = fs::create_dir_all(dst); + + if let Ok(entries) = fs::read_dir(src) { + for entry in entries.flatten() { + let src_path = entry.path(); + let file_name = entry.file_name(); + let dst_path = dst.join(&file_name); + + if src_path.is_dir() { + let (o, f) = copy_dir_recursive(&src_path, &dst_path, ts); + ok += o; + fail += f; + } else { + // 跳过 .tmp 文件 + if let Some(name) = file_name.to_str() { + if name.ends_with(".tmp") { + continue; + } + } + + // 先删除目标文件(可能只读) + let _ = fs::remove_file(&dst_path); + + match fs::copy(&src_path, &dst_path) { + Ok(_) => { + log_print!("{} [升级替换] {} -> {}", + ts, file_name.to_string_lossy(), dst_path.display()); + ok += 1; + } + Err(e) => { + log_print!("{} [升级替换] 复制失败 {}: {}", ts, file_name.to_string_lossy(), e); + fail += 1; + } + } + } + } + } + + (ok, fail) + } + + let (ok, fail) = copy_dir_recursive(&upgrade_base, &target_dir, ts.as_str()); + success_count += ok; + fail_count += fail; + + log_print!("{} [升级替换] {} 升级完成:成功 {} 个,失败 {} 个", + ts, app_name, success_count, fail_count); +} + +/// 重启指定应用(工作目录 = exe 所在目录) +fn restart_app(app_name: &str, exe_path: &str) { + let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + + // 工作目录 = exe 所在目录 + let work_dir = std::path::Path::new(exe_path) + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| std::path::PathBuf::from(".")); + + use std::process::Command; + + match Command::new(exe_path) + .current_dir(&work_dir) + .spawn() + { + Ok(child) => { + log_print!("{} [重启] 已启动 {} (PID={}),工作目录: {}", + ts, app_name, child.id(), work_dir.display()); + } + Err(e) => { + log_print!("{} [重启] 启动失败 {}: {}", ts, exe_path, e); + } + } +} + /// 递归扫描升级目录,返回 (relative_path, full_path) 列表 /// 跳过 .tmp 文件(如果存在同名非 tmp 文件的话) #[allow(dead_code)] @@ -1385,7 +1498,8 @@ fn send_get_all_file( /// 向指定应用发送升级确认消息(修复版:直接用 Win32 API 连接管道,不再用 PowerShell) /// 消息格式:{"Type":"UpgradeConfirm","Data":{"AppName":"xxx","CurrentVer":"1.0.0","LatestVer":"1.1.0"}} -fn notify_app_upgrade(app_name: &str, current_ver: &str, latest_ver: &str, _debug: bool) { +/// 返回 true = 用户批准,false = 用户拒绝/通信失败 +fn notify_app_upgrade(app_name: &str, current_ver: &str, latest_ver: &str, _debug: bool) -> bool { #[cfg(windows)] { use std::process::Command; @@ -1404,13 +1518,13 @@ fn notify_app_upgrade(app_name: &str, current_ver: &str, latest_ver: &str, _debu let s = String::from_utf8_lossy(&o.stdout); s.lines().filter_map(|l| l.trim().parse().ok()).collect() } - Err(_) => return, + Err(_) => return false, }; if pids.is_empty() { let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); log_print!("{} [升级确认] {} 未运行,跳过通知", ts, app_name); - return; + return false; } // 2. 构造消息 @@ -1432,43 +1546,29 @@ fn notify_app_upgrade(app_name: &str, current_ver: &str, latest_ver: &str, _debu Ok(true) => { log_print!("{} [升级确认] 用户批准升级 {} (PID={}),应用将退出", ts, app_name, pid); - // 等待应用退出(最多 5 秒) - log_print!("{} [升级确认] 等待 {} 退出...", ts, app_name); - let start = std::time::Instant::now(); - loop { - let check = Command::new("powershell") - .args(["-NoProfile", "-NonInteractive", "-Command", - &format!("if ((Get-Process -Id {pid} -ErrorAction SilentlyContinue) -eq $null) {{ 'exit' }} else {{ 'running' }}")]) - .output(); - if let Ok(out) = check { - if String::from_utf8_lossy(&out.stdout).contains("exit") { - log_print!("{} [升级确认] {} 已退出,升级完成", ts, app_name); - break; - } - } - if start.elapsed().as_secs() > 5 { - log_print!("{} [升级确认] 等待退出超时,跳过", ts); - break; - } - std::thread::sleep(std::time::Duration::from_millis(500)); - } + return true; } Ok(false) => { log_print!("{} [升级确认] 用户拒绝升级 {} (PID={})", ts, app_name, pid); + return false; } Err(e) => { log_print!("{} [升级确认] 通信失败 {} (PID={}): {}", ts, app_name, pid, e); + return false; } } } + + return false; } #[cfg(not(windows))] { let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); log_print!("{} [升级确认] 非 Windows 平台,跳过通知", ts); + return false; } } @@ -1609,8 +1709,8 @@ fn connect_and_wait_response( } /// 通知所有升级了的应用(EasyTest 等) -/// app_upgrades: Vec<(app_name, current_ver, latest_ver)> -fn notify_all_app_upgrades(app_upgrades: &[(String, String, String)], _debug: bool) { +/// app_upgrades: Vec<(app_name, current_ver, latest_ver, exe_path)> +fn notify_all_app_upgrades(app_upgrades: &[(String, String, String, String)], _debug: bool) { let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); if app_upgrades.is_empty() { @@ -1620,8 +1720,44 @@ fn notify_all_app_upgrades(app_upgrades: &[(String, String, String)], _debug: bo log_print!("{} [升级确认] 准备通知 {} 个应用: {:?}", ts, app_upgrades.len(), app_upgrades); - for (app_name, current_ver, latest_ver) in app_upgrades { - notify_app_upgrade(app_name, current_ver, latest_ver, true); // 始终打印通知详情 + for (app_name, current_ver, latest_ver, exe_path) in app_upgrades { + // 发送升级确认消息,等待用户响应 + let approved = notify_app_upgrade(app_name, current_ver, latest_ver, true); + + // 用户批准升级后,等待应用退出,然后复制文件并重启 + if approved { + // 等待应用退出(最多 15 秒) + log_print!("{} [升级确认] 等待 {} 退出...", ts, app_name); + let start = std::time::Instant::now(); + let mut exited = false; + + loop { + let check = Command::new("powershell") + .args(["-NoProfile", "-NonInteractive", "-Command", + &format!("if ((Get-Process -Name '{}' -ErrorAction SilentlyContinue) -eq $null) {{ 'exit' }} else {{ 'running' }}", app_name)]) + .output(); + + if let Ok(out) = check { + if String::from_utf8_lossy(&out.stdout).contains("exit") { + exited = true; + break; + } + } + + if start.elapsed().as_secs() > 15 { + log_print!("{} [升级确认] 等待退出超时,跳过", ts); + break; + } + + std::thread::sleep(std::time::Duration::from_millis(500)); + } + + // 应用退出后,复制文件并重启 + if exited { + upgrade_app_files(app_name, exe_path); + restart_app(app_name, exe_path); + } + } } } @@ -1849,12 +1985,12 @@ async fn run_updater(debug_mode: bool) -> bool { } else { *ctx_clone.current_phase.lock().unwrap() = UpdatePhase::Apps; // 打印候选应用及其预存版本 - let app_names: Vec = candidates.iter().map(|(n, v)| format!("{}(v{})", n, v)).collect(); + 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_name, _) in &candidates { + 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()); @@ -1879,11 +2015,11 @@ async fn run_updater(debug_mode: bool) -> bool { } else { *ctx_clone.current_phase.lock().unwrap() = UpdatePhase::Apps; // 打印候选应用及其预存版本 - let app_names: Vec = candidates.iter().map(|(n, v)| format!("{}(v{})", n, v)).collect(); + 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_name, _) in &candidates { + 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()); @@ -1902,9 +2038,9 @@ async fn run_updater(debug_mode: bool) -> bool { // 使用预存的版本(在候选发现阶段已从运行中进程读取) let candidates = ctx_clone.candidates.lock().unwrap().clone(); let mut apps_to_update = Vec::new(); - let mut upgraded_apps = Vec::new(); // (app_name, current_ver, latest_ver) + let mut upgraded_apps = Vec::new(); // (app_name, current_ver, latest_ver, exe_path) - for (app_name, local_version) in candidates { + for (app_name, local_version, exe_path) 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(""); @@ -1919,7 +2055,7 @@ async fn run_updater(debug_mode: bool) -> bool { if local_version == "0.0.0" || version_less_than(&local_version, server_version) { apps_to_update.push(app_name.clone()); // 记录需要升级的应用信息(用于后续发送通知) - upgraded_apps.push((app_name.clone(), local_version.clone(), server_version.to_string())); + upgraded_apps.push((app_name.clone(), local_version.clone(), server_version.to_string(), exe_path.clone())); } } } @@ -2221,7 +2357,7 @@ async fn run_updater(debug_mode: bool) -> bool { log_print!("{} [FileChunk] is_last={}, queue_len={}", ts, is_last, queue_len); let candidates = ctx_clone.candidates.lock().unwrap().clone(); - for (app_name, _) in &candidates { + for (app_name, _, _) in &candidates { let result = handle_app_file_chunk(&ctx_clone, app_name, data_obj, debug_msg); if let Some((_, relative_path)) = result { if is_last { @@ -2287,9 +2423,9 @@ async fn run_updater(debug_mode: bool) -> bool { // ========== 阶段3.5:处理应用 DownloadComplete ========== if current_phase == UpdatePhase::AppsWaitAllFile { let candidates = ctx_clone.candidates.lock().unwrap().clone(); - let candidate_names: Vec<&str> = candidates.iter().map(|(n, _)| n.as_str()).collect(); + 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 { + for (app_name, _, _) in &candidates { // filename 可能含路径分隔符(如 "EasyTest/Audio.wav"),只取纯文件名部分 let tmp_filename = std::path::Path::new(filename) .file_name()