升级完成重启应用程序

This commit is contained in:
zqm
2026-04-13 10:40:55 +08:00
parent 0741a56c99
commit e8e45ba012

View File

@@ -105,8 +105,8 @@ struct UpdateContext {
server_updater_version: Mutex<Option<String>>,
/// 当前更新阶段
current_phase: Mutex<UpdatePhase>,
/// 候选应用列表 (app_name, local_version)阶段3使用
candidates: Mutex<Vec<(String, String)>>,
/// 候选应用列表 (app_name, local_version, exe_path)阶段3使用
candidates: Mutex<Vec<(String, String, String)>>,
/// 等待 GetAllFile 响应的应用列表阶段3.5使用)
pending_allfile_apps: Mutex<Vec<String>>,
/// 待下载队列 (app_name, filename, offset, expected_md5),顺序下载
@@ -115,8 +115,8 @@ struct UpdateContext {
is_downloading: Mutex<bool>,
/// 当前正在下载的文件的期望 MD5DownloadComplete 校验用)
current_download_md5: Mutex<Option<String>>,
/// 已升级的应用列表 (app_name, current_ver, latest_ver),下载完成后通知用
upgraded_apps: Mutex<Vec<(String, String, String)>>,
/// 已升级的应用列表 (app_name, current_ver, latest_ver, exe_path),下载完成后通知用
upgraded_apps: Mutex<Vec<(String, String, String, String)>>,
}
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<u32>) -> 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<String> = candidates.iter().map(|(n, v)| format!("{}(v{})", n, v)).collect();
let app_names: Vec<String> = 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<String> = candidates.iter().map(|(n, v)| format!("{}(v{})", n, v)).collect();
let app_names: Vec<String> = 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()