From 0b931d57c9bd7fd70507cde4a2d659dc01f4399b Mon Sep 17 00:00:00 2001 From: zqm Date: Wed, 8 Apr 2026 11:29:57 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E4=BA=8C=E8=BF=9B=E5=88=B6?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Windows/CS/Framework4.0/Updater/Cargo.lock | 7 + Windows/CS/Framework4.0/Updater/Cargo.toml | 1 + Windows/CS/Framework4.0/Updater/src/main.rs | 281 +++++++++++++++++++- 3 files changed, 286 insertions(+), 3 deletions(-) diff --git a/Windows/CS/Framework4.0/Updater/Cargo.lock b/Windows/CS/Framework4.0/Updater/Cargo.lock index 8c0cd42..ae65e8a 100644 --- a/Windows/CS/Framework4.0/Updater/Cargo.lock +++ b/Windows/CS/Framework4.0/Updater/Cargo.lock @@ -6,6 +6,7 @@ version = 4 name = "Updater" version = "0.1.0" dependencies = [ + "base64", "chrono", "cube_lib", "dirs", @@ -57,6 +58,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.11.0" diff --git a/Windows/CS/Framework4.0/Updater/Cargo.toml b/Windows/CS/Framework4.0/Updater/Cargo.toml index d479d1b..9d2e759 100644 --- a/Windows/CS/Framework4.0/Updater/Cargo.toml +++ b/Windows/CS/Framework4.0/Updater/Cargo.toml @@ -10,6 +10,7 @@ dirs = "5.0" tokio = { version = "1.37", features = ["full"] } windows = { version = "0.56", features = ["Win32_System_Console"] } chrono = "0.4" +base64 = "0.22" # Local CubeLib for WebSocket cube_lib = { path = "../../../Rust/CubeLib" } diff --git a/Windows/CS/Framework4.0/Updater/src/main.rs b/Windows/CS/Framework4.0/Updater/src/main.rs index 1bb40a0..bdf54ff 100644 --- a/Windows/CS/Framework4.0/Updater/src/main.rs +++ b/Windows/CS/Framework4.0/Updater/src/main.rs @@ -1,11 +1,255 @@ use serde::{Deserialize, Serialize}; -use std::fs; +use std::fs::{self, File}; +use std::io::Write as IoWrite; use std::path::PathBuf; use std::pin::Pin; use std::process::Command; +use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; use cube_lib::websocket::{WebSocketClient, WebSocketConfig}; +// ===================== 文件下载状态 ===================== +struct DownloadState { + filename: String, + offset: u64, + temp_path: Option, + file: Option, +} + +impl Default for DownloadState { + fn default() -> Self { + Self { + filename: String::new(), + offset: 0, + temp_path: None, + file: None, + } + } +} + +static DOWNLOAD_STATE: std::sync::Mutex = std::sync::Mutex::new(DownloadState { + filename: String::new(), + offset: 0, + temp_path: None, + file: None, +}); + +// ===================== 版本比较与下载 ===================== +/// 获取本地文件版本号(使用 PowerShell 获取 PE 文件版本信息) +fn get_local_file_version(filename: &str) -> String { + let exe_path = std::env::current_exe().expect("Failed to get executable path"); + let base_dir = exe_path.parent().unwrap_or(std::path::Path::new("")); + let file_path = base_dir.join(filename); + + 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() + } +} + +/// 比较两个版本号(a < b 返回 true) +fn version_less_than(a: &str, b: &str) -> bool { + let parse_v = |v: &str| -> Vec { + v.split(|c: char| !c.is_ascii_digit() && c != '.') + .filter(|s| !s.is_empty()) + .filter_map(|s| s.parse().ok()) + .collect() + }; + let a_parts: Vec = parse_v(a); + let b_parts: Vec = parse_v(b); + let max_len = a_parts.len().max(b_parts.len()); + + for i in 0..max_len { + let av = if i < a_parts.len() { a_parts[i] } else { 0 }; + let bv = if i < b_parts.len() { b_parts[i] } else { 0 }; + if av < bv { + return true; + } + if av > bv { + return false; + } + } + false +} + +/// 发送文件下载请求(断点续传) +fn request_download(sender: &cube_lib::websocket::MessageSender, filename: &str, offset: u64, debug: bool) { + let msg_str = format!( + r#"{{"Type":"DownloadFile","Data":{{"filename":"{}","offset":{}}}}}"#, + filename, offset + ); + if debug { + let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + println!("{} 发送消息:{}", ts, msg_str); + } + sender.send(msg_str); +} + +/// 处理收到的文件块 +fn handle_file_chunk(data: &serde_json::Map, debug: bool) { + let filename = data.get("filename").and_then(|v| v.as_str()).unwrap_or(""); + let offset = data.get("offset").and_then(|v| v.as_u64()).unwrap_or(0); + let chunk_data = data.get("data").and_then(|v| v.as_str()).unwrap_or(""); + let is_last = data.get("is_last").and_then(|v| v.as_bool()).unwrap_or(false); + + let mut state = DOWNLOAD_STATE.lock().unwrap(); + + // 如果是新文件或 offset=0,重置状态 + if state.filename != filename || offset == 0 { + // 关闭旧文件 + if let Some(f) = state.file.take() { + drop(f); + } + // 删除旧临时文件 + if let Some(ref tp) = state.temp_path { + let _ = fs::remove_file(tp); + } + + // 获取可执行文件目录 + let exe_path = std::env::current_exe().expect("Failed to get executable path"); + let base_dir = exe_path.parent().unwrap_or(std::path::Path::new("")); + let temp_path = base_dir.join(format!("{}.tmp", filename)); + + // 创建临时文件(截断) + match File::create(&temp_path) { + Ok(file) => { + state.filename = filename.to_string(); + state.offset = 0; + state.temp_path = Some(temp_path); + state.file = Some(file); + } + Err(_) => { + if debug { + eprintln!("[下载] 无法创建临时文件: {}", filename); + } + return; + } + } + } + + // 解码并追加数据 + let current_offset = state.offset; + if offset == current_offset { + if let Some(ref mut file) = state.file { + match BASE64.decode(chunk_data) { + Ok(decoded) => { + match file.write_all(&decoded) { + Ok(_) => { + let _ = file.flush(); + state.offset += decoded.len() as u64; + } + Err(e) => { + if debug { + eprintln!("[下载] 写入失败: {}", e); + } + } + } + } + Err(e) => { + if debug { + eprintln!("[下载] Base64 解码失败: {}", e); + } + } + } + } + } else { + if debug { + eprintln!("[下载] 偏移不匹配: 期望 {}, 收到 {}", current_offset, offset); + } + } + + // 最后一块:重命名临时文件为正式文件 + if is_last { + let temp_path = state.temp_path.clone(); + let final_offset = state.offset; + + // 关闭文件句柄 + if let Some(f) = state.file.take() { + drop(f); + } + + if let Some(ref temp) = temp_path { + let exe_path = std::env::current_exe().expect("Failed to get executable path"); + let base_dir = exe_path.parent().unwrap_or(std::path::Path::new("")); + let final_path = base_dir.join(filename); + + // 原子重命名 + if let Err(e) = fs::rename(temp, &final_path) { + if debug { + eprintln!("[下载] 重命名失败: {}", e); + } + // 尝试直接覆盖写入 + let _ = fs::copy(temp, &final_path); + let _ = fs::remove_file(temp); + } + + if debug { + let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + println!("{} [下载] {} 下载完成,共 {} 字节", ts, filename, final_offset); + } + } + + // 重置状态 + state.filename = String::new(); + state.offset = 0; + state.temp_path = None; + state.file = None; + } +} + +/// 处理下载完成消息 +fn handle_download_complete(data: &serde_json::Map, debug: bool) { + let filename = data.get("filename").and_then(|v| v.as_str()).unwrap_or(""); + let size = data.get("size").and_then(|v| v.as_u64()).unwrap_or(0); + + if debug { + let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + println!("{} [下载] {} 下载完成,最终大小: {} 字节", ts, filename, size); + } + + let mut state = DOWNLOAD_STATE.lock().unwrap(); + state.filename = String::new(); + state.offset = 0; + state.temp_path = None; + state.file = None; +} + /// Updater 自身配置(AppData/Updater/config.json) /// 只负责 Updater 自己的行为参数,连接地址从公共 config.json 加载 #[derive(Debug, Serialize, Deserialize)] @@ -193,11 +437,42 @@ async fn run_updater(debug_mode: bool) { // 处理 FileVer 响应 if msg_type == "FileVer" { if let Some(file_versions) = data.get("Data").and_then(|d| d.get("file_versions")).and_then(|v| v.as_object()) { - for _ in file_versions { - // 静默记录版本,不输出日志 + for (filename, server_ver) in file_versions { + let server_version = server_ver.as_str().unwrap_or("0.0.0"); + let local_version = get_local_file_version(filename); + + if debug_msg { + let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + println!("{} [版本] {}: 服务端={}, 本地={}", ts, filename, server_version, local_version); + } + + // 比较版本:如果本地不存在或比服务端旧,则下载 + let need_update = local_version == "0.0.0" || version_less_than(&local_version, server_version); + if need_update { + if debug_msg { + let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + println!("{} [升级] {} 需要更新,开始下载...", ts, filename); + } + // 从 offset 0 开始下载(断点续传逻辑在 handle_file_chunk 中处理) + request_download(&sender, filename, 0, debug_msg); + } } } } + + // 处理文件块 + if msg_type == "FileChunk" { + if let Some(data_obj) = data.get("Data").and_then(|v| v.as_object()) { + handle_file_chunk(data_obj, debug_msg); + } + } + + // 处理下载完成 + if msg_type == "DownloadComplete" { + if let Some(data_obj) = data.get("Data").and_then(|v| v.as_object()) { + handle_download_complete(data_obj, debug_msg); + } + } }); // 设置断开连接回调