下载其它应用

This commit is contained in:
zqm
2026-04-09 16:34:21 +08:00
parent 6681da7795
commit d6a143d34a

View File

@@ -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<UpdatePhase>,
/// 候选应用列表阶段3使用
candidates: Mutex<Vec<String>>,
/// 等待 GetAllFile 响应的应用列表阶段3.5使用)
pending_allfile_apps: Mutex<Vec<String>>,
}
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<String> {
let file_path = get_updater_data_dir()
.join("Updater")
@@ -422,6 +427,17 @@ fn compute_file_hash(filename: &str, bytes: u64, _debug: bool) -> Option<String>
Some(format!("{:x}", hash))
}
/// 计算文件的完整 MD5 hash
fn compute_file_md5(file_path: &PathBuf) -> Option<String> {
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<String> {
/// 递归扫描升级目录,返回 (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<String> {
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<u64>, Option<String>)> = 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<String> = 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<String> = 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()