2026-04-08 16:15:28 +08:00
|
|
|
|
use rand::Rng;
|
2026-04-07 10:20:00 +08:00
|
|
|
|
use serde::{Deserialize, Serialize};
|
2026-04-08 14:39:36 +08:00
|
|
|
|
use std::io::Read;
|
2026-04-08 11:29:57 +08:00
|
|
|
|
use std::fs::{self, File};
|
|
|
|
|
|
use std::io::Write as IoWrite;
|
2026-04-07 10:20:00 +08:00
|
|
|
|
use std::path::PathBuf;
|
2026-04-07 15:47:23 +08:00
|
|
|
|
use std::pin::Pin;
|
2026-04-07 13:21:31 +08:00
|
|
|
|
use std::process::Command;
|
2026-04-07 15:09:44 +08:00
|
|
|
|
|
2026-04-08 11:29:57 +08:00
|
|
|
|
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
2026-04-07 15:09:44 +08:00
|
|
|
|
use cube_lib::websocket::{WebSocketClient, WebSocketConfig};
|
2026-04-07 10:20:00 +08:00
|
|
|
|
|
2026-04-08 11:29:57 +08:00
|
|
|
|
// ===================== 文件下载状态 =====================
|
|
|
|
|
|
struct DownloadState {
|
|
|
|
|
|
filename: String,
|
|
|
|
|
|
offset: u64,
|
|
|
|
|
|
temp_path: Option<PathBuf>,
|
|
|
|
|
|
file: Option<File>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for DownloadState {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
filename: String::new(),
|
|
|
|
|
|
offset: 0,
|
|
|
|
|
|
temp_path: None,
|
|
|
|
|
|
file: None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static DOWNLOAD_STATE: std::sync::Mutex<DownloadState> = std::sync::Mutex::new(DownloadState {
|
|
|
|
|
|
filename: String::new(),
|
|
|
|
|
|
offset: 0,
|
|
|
|
|
|
temp_path: None,
|
|
|
|
|
|
file: None,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-08 15:18:08 +08:00
|
|
|
|
// ===================== 下载流程状态 =====================
|
|
|
|
|
|
static BOOTLOADER_DOWNLOADED: std::sync::Mutex<bool> = std::sync::Mutex::new(false);
|
|
|
|
|
|
static UPDATER_DOWNLOADED: std::sync::Mutex<bool> = std::sync::Mutex::new(false);
|
|
|
|
|
|
static SERVER_UPDATER_VERSION: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
|
2026-04-08 16:15:28 +08:00
|
|
|
|
/// 标记一次更新检查是否已完成(用于主循环控制)
|
|
|
|
|
|
static UPDATE_CHECK_DONE: std::sync::Mutex<bool> = std::sync::Mutex::new(false);
|
|
|
|
|
|
/// 标记本次运行是否执行了更新(下载了文件)
|
|
|
|
|
|
static UPDATE_PERFORMED: std::sync::Mutex<bool> = std::sync::Mutex::new(false);
|
2026-04-08 15:18:08 +08:00
|
|
|
|
|
2026-04-08 11:29:57 +08:00
|
|
|
|
// ===================== 版本比较与下载 =====================
|
2026-04-08 13:36:50 +08:00
|
|
|
|
/// 获取 Updater 数据目录(X:\AppData\,存放 BootLoader.exe 等)
|
|
|
|
|
|
fn get_updater_data_dir() -> PathBuf {
|
|
|
|
|
|
let exe_path = std::env::current_exe().expect("Failed to get executable path");
|
|
|
|
|
|
let drive = exe_path
|
|
|
|
|
|
.parent()
|
|
|
|
|
|
.and_then(|p| p.as_os_str().to_str())
|
|
|
|
|
|
.and_then(|s| s.split('\\').next())
|
|
|
|
|
|
.unwrap_or("C:");
|
|
|
|
|
|
let appdata = PathBuf::from(format!("{}/AppData", drive));
|
|
|
|
|
|
let _ = fs::create_dir_all(&appdata);
|
|
|
|
|
|
appdata
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 11:29:57 +08:00
|
|
|
|
/// 获取本地文件版本号(使用 PowerShell 获取 PE 文件版本信息)
|
|
|
|
|
|
fn get_local_file_version(filename: &str) -> String {
|
2026-04-08 13:36:50 +08:00
|
|
|
|
let file_path = get_updater_data_dir().join(filename);
|
2026-04-08 11:29:57 +08:00
|
|
|
|
|
|
|
|
|
|
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)
|
2026-04-08 16:15:28 +08:00
|
|
|
|
/// 使用 CubeLib 库的版本比较函数
|
2026-04-08 11:29:57 +08:00
|
|
|
|
fn version_less_than(a: &str, b: &str) -> bool {
|
2026-04-08 16:15:28 +08:00
|
|
|
|
cube_lib::version_less_than(a, b)
|
2026-04-08 11:29:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 14:39:36 +08:00
|
|
|
|
/// 计算本地文件前 N 字节的 MD5 hash(字节数为 0 表示全部)
|
|
|
|
|
|
fn compute_file_hash(filename: &str, bytes: u64, _debug: bool) -> Option<String> {
|
|
|
|
|
|
|
|
|
|
|
|
let file_path = get_updater_data_dir().join(filename);
|
|
|
|
|
|
if !file_path.exists() {
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mut file = File::open(&file_path).ok()?;
|
|
|
|
|
|
let file_size = file.metadata().ok()?.len();
|
|
|
|
|
|
|
|
|
|
|
|
// 如果 bytes=0 或超过文件大小,则计算整个文件
|
|
|
|
|
|
let read_bytes = if bytes == 0 || bytes > file_size {
|
|
|
|
|
|
file_size
|
|
|
|
|
|
} else {
|
|
|
|
|
|
bytes
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 读取前 read_bytes 字节
|
|
|
|
|
|
let mut buffer = vec![0u8; read_bytes as usize];
|
|
|
|
|
|
file.read_exact(&mut buffer).ok()?;
|
|
|
|
|
|
|
|
|
|
|
|
let hash = md5::compute(&buffer);
|
|
|
|
|
|
Some(format!("{:x}", hash))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 获取临时文件的当前大小(字节数)
|
2026-04-08 15:18:08 +08:00
|
|
|
|
/// 注意:Updater.exe 的临时文件是 Updater.new.exe.tmp
|
2026-04-08 14:39:36 +08:00
|
|
|
|
fn get_tmp_file_size(filename: &str) -> u64 {
|
2026-04-08 15:18:08 +08:00
|
|
|
|
let tmp_filename = if filename == "Updater.exe" { "Updater.new.exe.tmp" } else { filename };
|
|
|
|
|
|
let tmp_path = get_updater_data_dir().join(format!("{}.tmp", tmp_filename));
|
2026-04-08 14:39:36 +08:00
|
|
|
|
tmp_path.metadata().map(|m| m.len()).unwrap_or(0)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 15:18:08 +08:00
|
|
|
|
/// 检查是否存在 Updater.new.exe 文件
|
|
|
|
|
|
fn has_updater_new_exe() -> bool {
|
|
|
|
|
|
let updater_new_path = get_updater_data_dir().join("Updater.new.exe");
|
|
|
|
|
|
updater_new_path.exists()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 发送获取 Updater.exe 版本的请求
|
|
|
|
|
|
fn request_updater_version(sender: &cube_lib::websocket::MessageSender, device_number: &str, debug: bool) {
|
|
|
|
|
|
let msg_str = format!(
|
|
|
|
|
|
r#"{{"Type":"GetFileVer","Data":{{"DeviceNumber":"{}","file_list":["Updater.exe"]}}}}"#,
|
|
|
|
|
|
device_number
|
|
|
|
|
|
);
|
|
|
|
|
|
if debug {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
println!("{} [版本] 请求 Updater.exe 版本: {}", ts, msg_str);
|
|
|
|
|
|
}
|
|
|
|
|
|
sender.send(msg_str);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 检查并下载 Updater.exe
|
|
|
|
|
|
/// 当 BootLoader 下载完成后调用此函数
|
|
|
|
|
|
/// 返回 true 表示需要继续等待下载,false 表示流程结束
|
|
|
|
|
|
fn check_and_download_updater(sender: &cube_lib::websocket::MessageSender, device_number: &str, debug: bool) -> bool {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
|
|
|
|
|
|
// 获取服务端 Updater.exe 版本号
|
|
|
|
|
|
let server_version = SERVER_UPDATER_VERSION.lock().unwrap().clone();
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(sv) = server_version {
|
|
|
|
|
|
let local_version = get_local_file_version("Updater.exe");
|
|
|
|
|
|
|
|
|
|
|
|
if debug {
|
|
|
|
|
|
println!("{} [版本] Updater.exe: 服务端={}, 本地={}", ts, sv, local_version);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 比较版本:需要下载的条件
|
|
|
|
|
|
let need_update = local_version == "0.0.0" || version_less_than(&local_version, &sv);
|
|
|
|
|
|
|
|
|
|
|
|
if need_update {
|
|
|
|
|
|
// 需要下载 Updater.exe
|
|
|
|
|
|
let tmp_size = get_tmp_file_size("Updater.exe");
|
|
|
|
|
|
if tmp_size > 0 {
|
|
|
|
|
|
if debug {
|
|
|
|
|
|
println!("{} [续传] Updater.exe 发现未完成下载,请求 hash 校验...", ts);
|
|
|
|
|
|
}
|
|
|
|
|
|
request_file_md5(sender, "Updater.exe", tmp_size, debug);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if debug {
|
|
|
|
|
|
println!("{} [升级] Updater.exe 需要更新,开始下载...", ts);
|
|
|
|
|
|
}
|
|
|
|
|
|
request_download(sender, "Updater.exe", 0, debug);
|
|
|
|
|
|
}
|
|
|
|
|
|
true // 需要继续等待下载
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 版本已是最新,检查是否有 Updater.new.exe
|
|
|
|
|
|
if debug {
|
|
|
|
|
|
println!("{} [版本] Updater.exe 版本已是最新,无需下载", ts);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 删除可能存在的旧 Updater.new.exe
|
|
|
|
|
|
if has_updater_new_exe() {
|
|
|
|
|
|
let updater_new_path = get_updater_data_dir().join("Updater.new.exe");
|
|
|
|
|
|
let _ = fs::remove_file(&updater_new_path);
|
|
|
|
|
|
if debug {
|
|
|
|
|
|
println!("{} [清理] 删除旧的 Updater.new.exe", ts);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 返回 false 表示 Updater 不需要更新,调用者负责输出日志并退出
|
|
|
|
|
|
false
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 没有收到服务端 Updater 版本 → 重新请求 Updater 版本
|
|
|
|
|
|
if debug {
|
|
|
|
|
|
println!("{} [警告] 未收到 Updater.exe 版本信息,重新请求...", ts);
|
|
|
|
|
|
}
|
|
|
|
|
|
request_updater_version(sender, device_number, debug);
|
|
|
|
|
|
true // 需要继续等待版本响应
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 安排启动 BootLoader.exe(延迟执行,等待所有下载完成)
|
2026-04-08 16:15:28 +08:00
|
|
|
|
/// 通过 shutdown_tx 发送断连信号,通知主循环优雅退出
|
|
|
|
|
|
fn schedule_bootloader_launch(
|
|
|
|
|
|
debug: bool,
|
|
|
|
|
|
shutdown_tx_arc: std::sync::Arc<std::sync::Mutex<Option<tokio::sync::oneshot::Sender<()>>>>,
|
|
|
|
|
|
) {
|
2026-04-08 15:18:08 +08:00
|
|
|
|
// 延迟 1 秒后启动,确保文件句柄已关闭
|
|
|
|
|
|
std::thread::spawn(move || {
|
|
|
|
|
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
|
|
|
|
|
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
|
|
|
|
|
|
let bootloader_path = get_updater_data_dir().join("BootLoader.exe");
|
|
|
|
|
|
|
|
|
|
|
|
if !bootloader_path.exists() {
|
|
|
|
|
|
if debug {
|
|
|
|
|
|
println!("{} [错误] BootLoader.exe 不存在,无法启动", ts);
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 启动 BootLoader.exe
|
|
|
|
|
|
if debug {
|
|
|
|
|
|
println!("{} [启动] 正在启动 BootLoader.exe...", ts);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(windows)]
|
|
|
|
|
|
{
|
|
|
|
|
|
use std::os::windows::process::CommandExt;
|
|
|
|
|
|
const DETACHED_PROCESS: u32 = 0x00000008;
|
|
|
|
|
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
|
|
|
|
|
|
|
|
|
|
|
match Command::new(&bootloader_path)
|
|
|
|
|
|
.args(["--from-updater"])
|
|
|
|
|
|
.creation_flags(DETACHED_PROCESS | CREATE_NO_WINDOW)
|
|
|
|
|
|
.spawn()
|
|
|
|
|
|
{
|
|
|
|
|
|
Ok(_) => {
|
|
|
|
|
|
if debug {
|
|
|
|
|
|
println!("{} [启动] BootLoader.exe 已启动,Updater 即将退出", ts);
|
|
|
|
|
|
}
|
2026-04-08 16:15:28 +08:00
|
|
|
|
// 发送断连信号,通知主循环优雅退出
|
|
|
|
|
|
if let Some(tx) = shutdown_tx_arc.lock().unwrap().take() {
|
|
|
|
|
|
let _ = tx.send(());
|
|
|
|
|
|
}
|
2026-04-08 15:18:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
eprintln!("{} [错误] 启动 BootLoader.exe 失败: {}", ts, e);
|
2026-04-08 16:15:28 +08:00
|
|
|
|
// 启动失败也发送断连信号
|
|
|
|
|
|
if let Some(tx) = shutdown_tx_arc.lock().unwrap().take() {
|
|
|
|
|
|
let _ = tx.send(());
|
|
|
|
|
|
}
|
2026-04-08 15:18:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(windows))]
|
|
|
|
|
|
{
|
|
|
|
|
|
match Command::new(&bootloader_path)
|
|
|
|
|
|
.arg("--from-updater")
|
|
|
|
|
|
.spawn()
|
|
|
|
|
|
{
|
|
|
|
|
|
Ok(_) => {
|
|
|
|
|
|
if debug {
|
|
|
|
|
|
println!("{} [启动] BootLoader.exe 已启动,Updater 即将退出", ts);
|
|
|
|
|
|
}
|
2026-04-08 16:15:28 +08:00
|
|
|
|
if let Some(tx) = shutdown_tx_arc.lock().unwrap().take() {
|
|
|
|
|
|
let _ = tx.send(());
|
|
|
|
|
|
}
|
2026-04-08 15:18:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
eprintln!("{} [错误] 启动 BootLoader.exe 失败: {}", ts, e);
|
2026-04-08 16:15:28 +08:00
|
|
|
|
if let Some(tx) = shutdown_tx_arc.lock().unwrap().take() {
|
|
|
|
|
|
let _ = tx.send(());
|
|
|
|
|
|
}
|
2026-04-08 15:18:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 14:39:36 +08:00
|
|
|
|
/// 发送 GetFileMd5 请求
|
|
|
|
|
|
fn request_file_md5(sender: &cube_lib::websocket::MessageSender, filename: &str, bytes: u64, debug: bool) {
|
|
|
|
|
|
let msg_str = format!(
|
|
|
|
|
|
r#"{{"Type":"GetFileMd5","Data":{{"filename":"{}","bytes":{}}}}}"#,
|
|
|
|
|
|
filename, bytes
|
|
|
|
|
|
);
|
|
|
|
|
|
if debug {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
println!("{} 发送消息:{}", ts, msg_str);
|
|
|
|
|
|
}
|
|
|
|
|
|
sender.send(msg_str);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 11:29:57 +08:00
|
|
|
|
/// 发送文件下载请求(断点续传)
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 处理收到的文件块
|
2026-04-08 15:18:08 +08:00
|
|
|
|
fn handle_file_chunk(data: &serde_json::Map<std::string::String, serde_json::Value>, debug: bool) -> Option<String> {
|
2026-04-08 11:29:57 +08:00
|
|
|
|
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);
|
|
|
|
|
|
|
2026-04-08 15:18:08 +08:00
|
|
|
|
// Updater.exe 保存为 Updater.new.exe
|
|
|
|
|
|
let final_filename = if filename == "Updater.exe" { "Updater.new.exe".to_string() } else { filename.to_string() };
|
|
|
|
|
|
|
2026-04-08 11:29:57 +08:00
|
|
|
|
let mut state = DOWNLOAD_STATE.lock().unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是新文件或 offset=0,重置状态
|
2026-04-08 15:18:08 +08:00
|
|
|
|
if state.filename != final_filename || offset == 0 {
|
2026-04-08 11:29:57 +08:00
|
|
|
|
// 关闭旧文件
|
|
|
|
|
|
if let Some(f) = state.file.take() {
|
|
|
|
|
|
drop(f);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 删除旧临时文件
|
|
|
|
|
|
if let Some(ref tp) = state.temp_path {
|
|
|
|
|
|
let _ = fs::remove_file(tp);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 13:36:50 +08:00
|
|
|
|
// 使用 AppData/Updater/ 作为数据目录
|
|
|
|
|
|
let data_dir = get_updater_data_dir();
|
2026-04-08 15:18:08 +08:00
|
|
|
|
let temp_path = data_dir.join(format!("{}.tmp", final_filename));
|
2026-04-08 11:29:57 +08:00
|
|
|
|
|
|
|
|
|
|
// 创建临时文件(截断)
|
|
|
|
|
|
match File::create(&temp_path) {
|
|
|
|
|
|
Ok(file) => {
|
2026-04-08 15:18:08 +08:00
|
|
|
|
state.filename = final_filename.clone();
|
2026-04-08 11:29:57 +08:00
|
|
|
|
state.offset = 0;
|
|
|
|
|
|
state.temp_path = Some(temp_path);
|
|
|
|
|
|
state.file = Some(file);
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(_) => {
|
|
|
|
|
|
if debug {
|
|
|
|
|
|
eprintln!("[下载] 无法创建临时文件: {}", filename);
|
|
|
|
|
|
}
|
2026-04-08 15:18:08 +08:00
|
|
|
|
return None;
|
2026-04-08 11:29:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解码并追加数据
|
|
|
|
|
|
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 {
|
2026-04-08 13:36:50 +08:00
|
|
|
|
let data_dir = get_updater_data_dir();
|
2026-04-08 15:18:08 +08:00
|
|
|
|
let final_path = data_dir.join(&final_filename);
|
2026-04-08 11:29:57 +08:00
|
|
|
|
|
|
|
|
|
|
// 原子重命名
|
|
|
|
|
|
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");
|
2026-04-08 15:18:08 +08:00
|
|
|
|
println!("{} [下载] {} 下载完成,共 {} 字节", ts, final_filename, final_offset);
|
2026-04-08 11:29:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 重置状态
|
|
|
|
|
|
state.filename = String::new();
|
|
|
|
|
|
state.offset = 0;
|
|
|
|
|
|
state.temp_path = None;
|
|
|
|
|
|
state.file = None;
|
2026-04-08 15:18:08 +08:00
|
|
|
|
|
|
|
|
|
|
// 返回下载完成的文件名,用于后续流程判断
|
|
|
|
|
|
Some(final_filename)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
2026-04-08 11:29:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 处理下载完成消息
|
2026-04-08 15:18:08 +08:00
|
|
|
|
fn handle_download_complete(data: &serde_json::Map<std::string::String, serde_json::Value>, debug: bool) -> Option<String> {
|
2026-04-08 11:29:57 +08:00
|
|
|
|
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);
|
|
|
|
|
|
|
2026-04-08 15:18:08 +08:00
|
|
|
|
// Updater.exe 保存为 Updater.new.exe
|
|
|
|
|
|
let final_filename = if filename == "Updater.exe" { "Updater.new.exe".to_string() } else { filename.to_string() };
|
|
|
|
|
|
|
2026-04-08 11:29:57 +08:00
|
|
|
|
if debug {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
2026-04-08 15:18:08 +08:00
|
|
|
|
println!("{} [下载] {} 下载完成,最终大小: {} 字节", ts, final_filename, size);
|
2026-04-08 11:29:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mut state = DOWNLOAD_STATE.lock().unwrap();
|
2026-04-08 15:18:08 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果有临时文件,执行重命名(防止 DownloadComplete 先于最后一个 FileChunk 到达)
|
|
|
|
|
|
// 先克隆需要的数据
|
|
|
|
|
|
let temp_to_rename = state.temp_path.clone();
|
|
|
|
|
|
let has_temp = temp_to_rename.is_some();
|
|
|
|
|
|
|
|
|
|
|
|
if has_temp {
|
|
|
|
|
|
let data_dir = get_updater_data_dir();
|
|
|
|
|
|
let final_path = data_dir.join(&final_filename);
|
|
|
|
|
|
|
|
|
|
|
|
// 先关闭文件句柄并获取 temp_path
|
|
|
|
|
|
let temp_path_owned = state.temp_path.take().unwrap();
|
|
|
|
|
|
state.filename = String::new();
|
|
|
|
|
|
state.offset = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 原子重命名
|
|
|
|
|
|
if let Err(e) = fs::rename(&temp_path_owned, &final_path) {
|
|
|
|
|
|
if debug {
|
|
|
|
|
|
eprintln!("[下载] 重命名失败: {}", e);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 尝试直接覆盖写入
|
|
|
|
|
|
let _ = fs::copy(&temp_path_owned, &final_path);
|
|
|
|
|
|
let _ = fs::remove_file(&temp_path_owned);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if debug {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
println!("{} [下载] {} 临时文件已重命名", ts, final_filename);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 没有临时文件,只重置状态
|
|
|
|
|
|
state.filename = String::new();
|
|
|
|
|
|
state.offset = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Some(final_filename)
|
2026-04-08 11:29:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 13:21:31 +08:00
|
|
|
|
/// Updater 自身配置(AppData/Updater/config.json)
|
|
|
|
|
|
/// 只负责 Updater 自己的行为参数,连接地址从公共 config.json 加载
|
2026-04-07 10:20:00 +08:00
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
|
struct Config {
|
2026-04-07 13:21:31 +08:00
|
|
|
|
/// 调试模式:true 时保留控制台窗口并输出日志
|
2026-04-07 10:20:00 +08:00
|
|
|
|
debug_mode: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for Config {
|
|
|
|
|
|
fn default() -> Self {
|
2026-04-07 13:21:31 +08:00
|
|
|
|
Self { debug_mode: false }
|
2026-04-07 10:20:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 13:21:31 +08:00
|
|
|
|
/// 获取 Updater 自身配置路径 AppData/Updater/config.json
|
|
|
|
|
|
fn get_updater_config_path() -> PathBuf {
|
2026-04-08 13:36:50 +08:00
|
|
|
|
get_updater_data_dir().join("Updater").join("config.json")
|
2026-04-07 10:20:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 13:21:31 +08:00
|
|
|
|
/// 获取公共配置路径 AppData/config.json(与 BootLoader 同级)
|
|
|
|
|
|
fn get_public_config_path() -> PathBuf {
|
|
|
|
|
|
let exe_path = std::env::current_exe().expect("Failed to get executable path");
|
|
|
|
|
|
let drive = exe_path
|
|
|
|
|
|
.parent()
|
|
|
|
|
|
.and_then(|p| p.as_os_str().to_str())
|
|
|
|
|
|
.and_then(|s| s.split('\\').next())
|
|
|
|
|
|
.unwrap_or("C:");
|
|
|
|
|
|
PathBuf::from(format!("{}/AppData/config.json", drive))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 加载 Updater 自身配置;若文件不存在则写入默认值
|
|
|
|
|
|
fn load_updater_config() -> Config {
|
|
|
|
|
|
let config_path = get_updater_config_path();
|
2026-04-07 10:20:00 +08:00
|
|
|
|
if config_path.exists() {
|
2026-04-07 13:21:31 +08:00
|
|
|
|
if let Ok(content) = fs::read_to_string(&config_path) {
|
|
|
|
|
|
if let Ok(config) = serde_json::from_str::<Config>(&content) {
|
|
|
|
|
|
return config;
|
2026-04-07 10:42:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-07 10:20:00 +08:00
|
|
|
|
}
|
2026-04-07 13:21:31 +08:00
|
|
|
|
// 文件不存在或解析失败 → 写入默认值
|
|
|
|
|
|
let default_config = Config::default();
|
|
|
|
|
|
if let Ok(content) = serde_json::to_string_pretty(&default_config) {
|
|
|
|
|
|
let _ = fs::write(&config_path, content);
|
|
|
|
|
|
}
|
|
|
|
|
|
default_config
|
2026-04-07 10:20:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 13:21:31 +08:00
|
|
|
|
/// 从公共 config.json 读取 ServerUrl 字段
|
|
|
|
|
|
fn resolve_ws_url() -> String {
|
|
|
|
|
|
let config_path = get_public_config_path();
|
|
|
|
|
|
if let Ok(content) = fs::read_to_string(&config_path) {
|
|
|
|
|
|
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
|
|
|
|
|
|
if let Some(url) = json.get("ServerUrl").and_then(|v| v.as_str()) {
|
|
|
|
|
|
return url.to_string();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 读取失败 → 降级到默认值
|
|
|
|
|
|
"ws://127.0.0.1:8087/ws".to_string()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 16:41:10 +08:00
|
|
|
|
/// 从公共 config.json 读取 DeviceNumber 字段
|
|
|
|
|
|
fn resolve_device_number() -> String {
|
|
|
|
|
|
let config_path = get_public_config_path();
|
|
|
|
|
|
if let Ok(content) = fs::read_to_string(&config_path) {
|
|
|
|
|
|
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
|
|
|
|
|
|
// 尝试多个可能的字段名
|
|
|
|
|
|
if let Some(id) = json.get("DeviceNumber").and_then(|v| v.as_str()) {
|
|
|
|
|
|
if !id.is_empty() {
|
|
|
|
|
|
return id.to_string();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(id) = json.get("StationId").and_then(|v| v.as_str()) {
|
|
|
|
|
|
if !id.is_empty() {
|
|
|
|
|
|
return id.to_string();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(id) = json.get("Station").and_then(|v| v.as_str()) {
|
|
|
|
|
|
if !id.is_empty() {
|
|
|
|
|
|
return id.to_string();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 读取失败 → 降级到默认值
|
|
|
|
|
|
"UNKNOWN".to_string()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 13:21:31 +08:00
|
|
|
|
fn is_process_running(process_name: &str) -> bool {
|
|
|
|
|
|
use std::process::id;
|
|
|
|
|
|
|
|
|
|
|
|
let current_pid = id().to_string();
|
|
|
|
|
|
let output = Command::new("tasklist")
|
|
|
|
|
|
.args(["/FI", &format!("IMAGENAME eq {}", process_name), "/FO", "CSV"])
|
|
|
|
|
|
.output()
|
|
|
|
|
|
.expect("Failed to execute tasklist");
|
|
|
|
|
|
|
|
|
|
|
|
let output_str = String::from_utf8_lossy(&output.stdout);
|
|
|
|
|
|
let lines: Vec<&str> = output_str.lines().collect();
|
|
|
|
|
|
let mut count = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for line in lines {
|
|
|
|
|
|
if line.contains(&format!("\"{}\"", process_name)) && !line.contains(¤t_pid) {
|
|
|
|
|
|
count += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
count > 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 16:15:28 +08:00
|
|
|
|
/// 主动断连信号(用于在消息回调中请求优雅退出)
|
|
|
|
|
|
/// - shutdown_tx: 从同步回调中发送,通知主循环主动断开
|
|
|
|
|
|
/// - disconnect_rx: 在主循环中等待
|
|
|
|
|
|
fn make_shutdown_channel() -> (
|
|
|
|
|
|
std::sync::Arc<std::sync::Mutex<Option<tokio::sync::oneshot::Sender<()>>>>,
|
|
|
|
|
|
tokio::sync::oneshot::Receiver<()>,
|
|
|
|
|
|
) {
|
|
|
|
|
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
|
|
|
|
(std::sync::Arc::new(std::sync::Mutex::new(Some(tx))), rx)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 15:09:44 +08:00
|
|
|
|
/// 运行 Updater(使用 CubeLib 内置的自动重连)
|
2026-04-08 16:15:28 +08:00
|
|
|
|
/// 返回本次运行是否执行了更新(下载了文件)
|
|
|
|
|
|
async fn run_updater(debug_mode: bool) -> bool {
|
|
|
|
|
|
// 重置本次运行的更新状态
|
|
|
|
|
|
*UPDATE_PERFORMED.lock().unwrap() = false;
|
|
|
|
|
|
*BOOTLOADER_DOWNLOADED.lock().unwrap() = false;
|
|
|
|
|
|
*UPDATER_DOWNLOADED.lock().unwrap() = false;
|
|
|
|
|
|
*SERVER_UPDATER_VERSION.lock().unwrap() = None;
|
|
|
|
|
|
|
|
|
|
|
|
// 主动断连通道
|
|
|
|
|
|
let (shutdown_tx_arc, mut disconnect_rx) = make_shutdown_channel();
|
2026-04-07 15:09:44 +08:00
|
|
|
|
// 加载初始 URL
|
|
|
|
|
|
let server_url = resolve_ws_url();
|
2026-04-07 13:21:31 +08:00
|
|
|
|
|
|
|
|
|
|
if debug_mode {
|
2026-04-07 15:09:44 +08:00
|
|
|
|
println!("========================================");
|
|
|
|
|
|
println!("Updater 启动 (调试模式)");
|
|
|
|
|
|
println!("服务器地址: {}", server_url);
|
|
|
|
|
|
println!("自动重连: 启用 (指数退避: 1s - 30s)");
|
|
|
|
|
|
println!("========================================");
|
2026-04-07 13:21:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 15:09:44 +08:00
|
|
|
|
// 创建 WebSocket 配置(启用自动重连)
|
|
|
|
|
|
let config = WebSocketConfig::new(&server_url)
|
|
|
|
|
|
.with_client_type("Updater")
|
|
|
|
|
|
.with_debug(debug_mode)
|
|
|
|
|
|
.with_reconnect(true) // 启用自动重连
|
|
|
|
|
|
.with_reconnect_delay(1000) // 初始延迟 1s
|
|
|
|
|
|
.with_max_reconnect_delay(30000); // 最大延迟 30s
|
2026-04-07 13:21:31 +08:00
|
|
|
|
|
2026-04-07 15:09:44 +08:00
|
|
|
|
// 创建 WebSocket 客户端
|
|
|
|
|
|
let mut client = WebSocketClient::new(config);
|
2026-04-07 13:21:31 +08:00
|
|
|
|
|
2026-04-07 15:09:44 +08:00
|
|
|
|
// 设置连接成功回调
|
2026-04-08 10:52:47 +08:00
|
|
|
|
let debug_connected = debug_mode;
|
2026-04-07 15:09:44 +08:00
|
|
|
|
client.on_connected(move |url| {
|
2026-04-08 10:52:47 +08:00
|
|
|
|
if debug_connected {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
println!("{} 收到消息:{}", ts, url);
|
2026-04-07 15:09:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 设置消息接收回调
|
2026-04-08 10:52:47 +08:00
|
|
|
|
let debug_msg = debug_mode;
|
|
|
|
|
|
let device_number = resolve_device_number();
|
2026-04-08 16:15:28 +08:00
|
|
|
|
let shutdown_tx_arc_clone = shutdown_tx_arc.clone();
|
2026-04-08 10:52:47 +08:00
|
|
|
|
client.on_message(move |msg_type, data, sender| {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
if debug_msg {
|
2026-04-08 11:03:49 +08:00
|
|
|
|
// 收到消息日志:data 是完整 parsed 对象,取其中的 Data 字段打印
|
|
|
|
|
|
let actual_data = data.get("Data").unwrap_or(&serde_json::Value::Null);
|
|
|
|
|
|
let data_str = serde_json::to_string(actual_data).unwrap_or_else(|_| "{}".to_string());
|
2026-04-08 10:52:47 +08:00
|
|
|
|
println!("{} 收到消息:{{\"Type\":{},\"Data\":{}}}",
|
|
|
|
|
|
ts,
|
|
|
|
|
|
serde_json::to_string(&msg_type).unwrap_or_default(),
|
|
|
|
|
|
data_str
|
|
|
|
|
|
);
|
2026-04-07 15:09:44 +08:00
|
|
|
|
}
|
2026-04-08 10:52:47 +08:00
|
|
|
|
|
2026-04-08 15:18:08 +08:00
|
|
|
|
// 收到 welcome 后,发送 GetFileVer(同时请求 BootLoader 和 Updater)
|
2026-04-08 10:52:47 +08:00
|
|
|
|
if msg_type == "welcome" {
|
|
|
|
|
|
if !device_number.is_empty() && device_number != "UNKNOWN" {
|
|
|
|
|
|
let msg_str = format!(
|
2026-04-08 15:18:08 +08:00
|
|
|
|
r#"{{"Type":"GetFileVer","Data":{{"DeviceNumber":"{}","file_list":["BootLoader.exe","Updater.exe"]}}}}"#,
|
2026-04-08 10:52:47 +08:00
|
|
|
|
device_number
|
|
|
|
|
|
);
|
|
|
|
|
|
if debug_msg {
|
|
|
|
|
|
let ts2 = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
println!("{} 发送消息:{}", ts2, msg_str);
|
|
|
|
|
|
}
|
|
|
|
|
|
sender.send(msg_str);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 15:09:44 +08:00
|
|
|
|
// 处理 FileVer 响应
|
|
|
|
|
|
if msg_type == "FileVer" {
|
2026-04-08 09:07:08 +08:00
|
|
|
|
if let Some(file_versions) = data.get("Data").and_then(|d| d.get("file_versions")).and_then(|v| v.as_object()) {
|
2026-04-08 15:18:08 +08:00
|
|
|
|
// 第一步:先遍历保存所有版本号
|
|
|
|
|
|
for (filename, server_ver) in file_versions {
|
|
|
|
|
|
let server_version = server_ver.as_str().unwrap_or("0.0.0");
|
|
|
|
|
|
// 保存 Updater.exe 的服务端版本号
|
|
|
|
|
|
if filename == "Updater.exe" {
|
|
|
|
|
|
*SERVER_UPDATER_VERSION.lock().unwrap() = Some(server_version.to_string());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 第二步:再遍历处理每个文件
|
2026-04-08 11:29:57 +08:00
|
|
|
|
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);
|
2026-04-08 14:39:36 +08:00
|
|
|
|
let tmp_size = get_tmp_file_size(filename);
|
2026-04-08 11:29:57 +08:00
|
|
|
|
|
|
|
|
|
|
if debug_msg {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
2026-04-08 15:18:08 +08:00
|
|
|
|
println!("{} [版本] {}: 服务端={}, 本地={}, tmp大小={}",
|
2026-04-08 14:39:36 +08:00
|
|
|
|
ts, filename, server_version, local_version, tmp_size);
|
2026-04-08 11:29:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 比较版本:如果本地不存在或比服务端旧,则下载
|
|
|
|
|
|
let need_update = local_version == "0.0.0" || version_less_than(&local_version, server_version);
|
|
|
|
|
|
if need_update {
|
2026-04-08 14:39:36 +08:00
|
|
|
|
if tmp_size > 0 {
|
|
|
|
|
|
// 有临时文件,请求 hash 校验
|
|
|
|
|
|
if debug_msg {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
println!("{} [续传] {} 发现未完成下载,请求 hash 校验...", ts, filename);
|
|
|
|
|
|
}
|
|
|
|
|
|
request_file_md5(&sender, filename, tmp_size, debug_msg);
|
2026-04-08 16:15:28 +08:00
|
|
|
|
*UPDATE_PERFORMED.lock().unwrap() = true;
|
2026-04-08 14:39:36 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 无临时文件,从头下载
|
|
|
|
|
|
if debug_msg {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
println!("{} [升级] {} 需要更新,开始下载...", ts, filename);
|
|
|
|
|
|
}
|
|
|
|
|
|
request_download(&sender, filename, 0, debug_msg);
|
2026-04-08 16:15:28 +08:00
|
|
|
|
*UPDATE_PERFORMED.lock().unwrap() = true;
|
2026-04-08 11:29:57 +08:00
|
|
|
|
}
|
2026-04-08 15:18:08 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 不需要更新
|
|
|
|
|
|
if debug_msg {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
println!("{} [版本] {} 版本已是最新,无需更新", ts, filename);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 对于 Updater.exe,检查是否有 Updater.new.exe
|
|
|
|
|
|
if filename == "Updater.exe" {
|
|
|
|
|
|
if has_updater_new_exe() {
|
|
|
|
|
|
// 删除旧版 Updater.new.exe(因为会重新下载)
|
|
|
|
|
|
let updater_new_path = get_updater_data_dir().join("Updater.new.exe");
|
|
|
|
|
|
let _ = fs::remove_file(&updater_new_path);
|
|
|
|
|
|
if debug_msg {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
println!("{} [清理] 删除旧的 Updater.new.exe", ts);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 标记 BootLoader 已"下载完成"(无需更新)
|
|
|
|
|
|
if filename == "BootLoader.exe" {
|
|
|
|
|
|
*BOOTLOADER_DOWNLOADED.lock().unwrap() = true;
|
|
|
|
|
|
if debug_msg {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
println!("{} [升级] BootLoader.exe 版本已是最新", ts);
|
|
|
|
|
|
}
|
|
|
|
|
|
// BootLoader 无需下载,检查 Updater
|
|
|
|
|
|
if !check_and_download_updater(&sender, &device_number, debug_msg) {
|
|
|
|
|
|
// Updater 也不需要更新 → 检查是否有正在下载的临时文件
|
|
|
|
|
|
let tmp_size = get_tmp_file_size("Updater.exe");
|
|
|
|
|
|
if tmp_size > 0 {
|
|
|
|
|
|
// 有正在下载的 Updater.tmp,等待下载完成
|
|
|
|
|
|
if debug_msg {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
println!("{} [升级] 发现未完成下载,等待 Updater.exe 下载完成...", ts);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 请求 hash 校验,继续等待
|
|
|
|
|
|
request_file_md5(&sender, "Updater.exe", tmp_size, debug_msg);
|
|
|
|
|
|
} else {
|
2026-04-08 16:15:28 +08:00
|
|
|
|
// 真的不需要更新,也没有临时文件 → 标记检查完成
|
|
|
|
|
|
*UPDATE_CHECK_DONE.lock().unwrap() = true;
|
2026-04-08 15:18:08 +08:00
|
|
|
|
if debug_msg {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
2026-04-08 16:15:28 +08:00
|
|
|
|
println!("{} [升级] 所有文件已是最新版本,等待下次检查...", ts);
|
2026-04-08 15:18:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-08 11:29:57 +08:00
|
|
|
|
}
|
2026-04-08 09:07:08 +08:00
|
|
|
|
}
|
2026-04-07 13:21:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-08 11:29:57 +08:00
|
|
|
|
|
2026-04-08 14:39:36 +08:00
|
|
|
|
// 处理 Md5 响应
|
|
|
|
|
|
if msg_type == "Md5" {
|
|
|
|
|
|
if let Some(md5_data) = data.get("Data").and_then(|v| v.as_object()) {
|
|
|
|
|
|
let filename = md5_data.get("filename").and_then(|v| v.as_str()).unwrap_or("");
|
|
|
|
|
|
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);
|
2026-04-08 15:18:08 +08:00
|
|
|
|
// Updater.exe 的临时文件名是 Updater.new.exe.tmp
|
|
|
|
|
|
let tmp_filename = if filename == "Updater.exe" {
|
|
|
|
|
|
"Updater.new.exe.tmp".to_string()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
format!("{}.tmp", filename)
|
|
|
|
|
|
};
|
2026-04-08 14:39:36 +08:00
|
|
|
|
|
|
|
|
|
|
// 计算本地 tmp 文件前 bytes 字节的 md5
|
|
|
|
|
|
let local_md5 = compute_file_hash(&tmp_filename, bytes, debug_msg);
|
|
|
|
|
|
|
|
|
|
|
|
if debug_msg {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
if let Some(ref lm) = local_md5 {
|
|
|
|
|
|
println!("{} [续传] {} md5对比: 本地={}, 服务端={}", ts, filename, lm, server_md5);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
println!("{} [续传] {} 无法计算本地md5,重新下载", ts, filename);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 比较 md5
|
|
|
|
|
|
if local_md5.as_deref() == Some(server_md5) {
|
|
|
|
|
|
// md5 相同,从 offset bytes 处续传
|
|
|
|
|
|
if debug_msg {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
println!("{} [续传] {} md5匹配,从 {} 字节处续传", ts, filename, bytes);
|
|
|
|
|
|
}
|
|
|
|
|
|
request_download(&sender, filename, bytes, debug_msg);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// md5 不同,删除临时文件,重新下载
|
|
|
|
|
|
if debug_msg {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
println!("{} [续传] {} md5不匹配,重新下载", ts, filename);
|
|
|
|
|
|
}
|
|
|
|
|
|
let tmp_path = get_updater_data_dir().join(&tmp_filename);
|
|
|
|
|
|
let _ = fs::remove_file(&tmp_path);
|
|
|
|
|
|
request_download(&sender, filename, 0, debug_msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 11:29:57 +08:00
|
|
|
|
// 处理文件块
|
|
|
|
|
|
if msg_type == "FileChunk" {
|
|
|
|
|
|
if let Some(data_obj) = data.get("Data").and_then(|v| v.as_object()) {
|
2026-04-08 15:18:08 +08:00
|
|
|
|
let completed = handle_file_chunk(data_obj, debug_msg);
|
|
|
|
|
|
// 只处理 Updater 下载完成(BootLoader 由 DownloadComplete 统一处理,避免重复)
|
|
|
|
|
|
if let Some(ref completed_file) = completed {
|
|
|
|
|
|
if completed_file == "Updater.new.exe" {
|
|
|
|
|
|
let already_done = *UPDATER_DOWNLOADED.lock().unwrap();
|
|
|
|
|
|
if !already_done {
|
|
|
|
|
|
*UPDATER_DOWNLOADED.lock().unwrap() = true;
|
|
|
|
|
|
if debug_msg {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
println!("{} [升级] Updater.new.exe 下载完成(FileChunk),准备启动 BootLoader...", ts);
|
|
|
|
|
|
}
|
2026-04-08 16:15:28 +08:00
|
|
|
|
schedule_bootloader_launch(debug_msg, shutdown_tx_arc_clone.clone());
|
2026-04-08 15:18:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-08 11:29:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 15:18:08 +08:00
|
|
|
|
// 处理下载完成(统一处理,避免 FileChunk 和 DownloadComplete 重复处理)
|
2026-04-08 11:29:57 +08:00
|
|
|
|
if msg_type == "DownloadComplete" {
|
|
|
|
|
|
if let Some(data_obj) = data.get("Data").and_then(|v| v.as_object()) {
|
2026-04-08 15:18:08 +08:00
|
|
|
|
let completed = handle_download_complete(data_obj, debug_msg);
|
|
|
|
|
|
if let Some(ref completed_file) = completed {
|
|
|
|
|
|
// BootLoader.exe 下载完成
|
|
|
|
|
|
if completed_file == "BootLoader.exe" {
|
|
|
|
|
|
let already_started = *BOOTLOADER_DOWNLOADED.lock().unwrap();
|
|
|
|
|
|
if !already_started {
|
|
|
|
|
|
*BOOTLOADER_DOWNLOADED.lock().unwrap() = true;
|
|
|
|
|
|
if debug_msg {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
println!("{} [升级] BootLoader.exe 下载完成,开始下载 Updater.exe...", ts);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 开始下载 Updater.exe
|
|
|
|
|
|
request_download(&sender, "Updater.exe", 0, debug_msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Updater.new.exe 下载完成
|
|
|
|
|
|
else if completed_file == "Updater.new.exe" {
|
|
|
|
|
|
let already_done = *UPDATER_DOWNLOADED.lock().unwrap();
|
|
|
|
|
|
if !already_done {
|
|
|
|
|
|
*UPDATER_DOWNLOADED.lock().unwrap() = true;
|
|
|
|
|
|
if debug_msg {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
println!("{} [升级] Updater.new.exe 下载完成(DownloadComplete),准备启动 BootLoader...", ts);
|
|
|
|
|
|
}
|
2026-04-08 16:15:28 +08:00
|
|
|
|
schedule_bootloader_launch(debug_msg, shutdown_tx_arc.clone());
|
2026-04-08 15:18:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-08 11:29:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-07 15:09:44 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 设置断开连接回调
|
2026-04-08 10:52:47 +08:00
|
|
|
|
let debug_disconnect = debug_mode;
|
2026-04-07 15:09:44 +08:00
|
|
|
|
client.on_disconnected(move || {
|
2026-04-08 10:52:47 +08:00
|
|
|
|
if debug_disconnect {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
println!("{} [断开] 连接已断开", ts);
|
2026-04-07 15:09:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 设置错误回调
|
|
|
|
|
|
client.on_error(|error| {
|
|
|
|
|
|
eprintln!("[错误] WebSocket: {}", error);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-08 10:52:47 +08:00
|
|
|
|
// 设置首次连接回调(GetFileVer 改到收到 welcome 后发送)
|
|
|
|
|
|
let debug_first = debug_mode;
|
|
|
|
|
|
let device_number_first = resolve_device_number();
|
|
|
|
|
|
client.on_first_connect(move |_url, _sender| {
|
|
|
|
|
|
let device_number = device_number_first.clone();
|
2026-04-07 15:47:23 +08:00
|
|
|
|
Box::pin(async move {
|
2026-04-08 10:52:47 +08:00
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
2026-04-07 16:44:36 +08:00
|
|
|
|
if device_number.is_empty() || device_number == "UNKNOWN" {
|
2026-04-08 10:52:47 +08:00
|
|
|
|
if debug_first {
|
|
|
|
|
|
println!("{} [连接] 已连接,未配置设备号,仅维持心跳", ts);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if debug_first {
|
|
|
|
|
|
println!("{} [连接] 已连接,等待服务器欢迎消息...", ts);
|
2026-04-07 16:44:36 +08:00
|
|
|
|
}
|
2026-04-07 15:47:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
}) as Pin<Box<dyn std::future::Future<Output = ()> + Send + Sync>>
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-08 10:52:47 +08:00
|
|
|
|
// 设置重连回调
|
|
|
|
|
|
let debug_reconnect = debug_mode;
|
2026-04-07 15:09:44 +08:00
|
|
|
|
client.on_reconnecting(move |attempt, url_arc| {
|
2026-04-07 16:13:02 +08:00
|
|
|
|
Box::pin(async move {
|
2026-04-08 10:52:47 +08:00
|
|
|
|
if debug_reconnect {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
println!("{} [重连] 第 {} 次重连中...", ts, attempt);
|
2026-04-07 16:13:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
let new_url = resolve_ws_url();
|
|
|
|
|
|
*url_arc.lock().await = new_url.clone();
|
2026-04-08 10:52:47 +08:00
|
|
|
|
if debug_reconnect {
|
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
|
|
|
|
println!("{} [重连] 配置已更新,服务器: {}", ts, new_url);
|
2026-04-07 16:13:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
}) as Pin<Box<dyn std::future::Future<Output = ()> + Send + Sync>>
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-08 10:52:47 +08:00
|
|
|
|
// 设置重连成功回调
|
|
|
|
|
|
let debug_reconnected = debug_mode;
|
|
|
|
|
|
let device_number_reconn = resolve_device_number();
|
|
|
|
|
|
client.on_reconnected(move |_url, _sender| {
|
|
|
|
|
|
let device_number = device_number_reconn.clone();
|
2026-04-07 16:13:02 +08:00
|
|
|
|
Box::pin(async move {
|
2026-04-08 10:52:47 +08:00
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
2026-04-07 16:44:36 +08:00
|
|
|
|
if device_number.is_empty() || device_number == "UNKNOWN" {
|
2026-04-08 10:52:47 +08:00
|
|
|
|
if debug_reconnected {
|
|
|
|
|
|
println!("{} [重连] 已重连,未配置设备号", ts);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if debug_reconnected {
|
|
|
|
|
|
println!("{} [重连] 已重连,等待服务器欢迎消息...", ts);
|
2026-04-07 16:44:36 +08:00
|
|
|
|
}
|
2026-04-07 16:13:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
}) as Pin<Box<dyn std::future::Future<Output = ()> + Send + Sync>>
|
2026-04-07 15:09:44 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 连接(CubeLib 会自动处理重连)
|
|
|
|
|
|
if debug_mode {
|
|
|
|
|
|
println!("[启动] 开始连接...");
|
|
|
|
|
|
}
|
2026-04-08 16:15:28 +08:00
|
|
|
|
|
|
|
|
|
|
// 等待断连信号或连接正常结束
|
|
|
|
|
|
tokio::select! {
|
|
|
|
|
|
_ = &mut disconnect_rx => {
|
|
|
|
|
|
// 收到断连信号,主动断开
|
|
|
|
|
|
if debug_mode {
|
|
|
|
|
|
println!("[启动] 收到断连信号,主动断开...");
|
|
|
|
|
|
}
|
|
|
|
|
|
client.disconnect().await;
|
|
|
|
|
|
}
|
|
|
|
|
|
_ = client.connect() => {
|
|
|
|
|
|
// 连接正常结束(CubeLib 自动重连后的最终退出)
|
|
|
|
|
|
if debug_mode {
|
|
|
|
|
|
println!("[启动] 连接已结束");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-07 10:42:37 +08:00
|
|
|
|
}
|
2026-04-08 16:15:28 +08:00
|
|
|
|
|
|
|
|
|
|
if debug_mode {
|
|
|
|
|
|
println!("Updater 本次运行结束");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 返回是否执行了更新(下载了文件)
|
|
|
|
|
|
let performed = *UPDATE_PERFORMED.lock().unwrap();
|
|
|
|
|
|
performed
|
2026-04-07 10:20:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[tokio::main]
|
|
|
|
|
|
async fn main() {
|
2026-04-07 13:21:31 +08:00
|
|
|
|
// 检查是否已有 Updater 进程在运行
|
|
|
|
|
|
if is_process_running("Updater.exe") {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 加载 Updater 自身配置(debug_mode)
|
|
|
|
|
|
let config = load_updater_config();
|
|
|
|
|
|
|
|
|
|
|
|
// 非 debug 模式下释放控制台,后台静默运行
|
2026-04-07 11:04:06 +08:00
|
|
|
|
if !config.debug_mode {
|
|
|
|
|
|
#[cfg(windows)]
|
|
|
|
|
|
{
|
|
|
|
|
|
use windows::Win32::System::Console;
|
|
|
|
|
|
use windows::Win32::Foundation::HWND;
|
|
|
|
|
|
unsafe {
|
|
|
|
|
|
let console = Console::GetConsoleWindow();
|
|
|
|
|
|
if console != HWND::default() {
|
|
|
|
|
|
let _ = Console::FreeConsole();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-07 13:21:31 +08:00
|
|
|
|
|
2026-04-08 16:15:28 +08:00
|
|
|
|
// 主循环:常驻运行,定期检查更新
|
|
|
|
|
|
loop {
|
|
|
|
|
|
// 运行一次 Updater(连接、检查、下载)
|
|
|
|
|
|
let update_performed = run_updater(config.debug_mode).await;
|
|
|
|
|
|
|
|
|
|
|
|
if update_performed {
|
|
|
|
|
|
// 执行了更新(下载了文件)→ 退出循环,由 BootLoader 重启 Updater
|
|
|
|
|
|
if config.debug_mode {
|
|
|
|
|
|
println!("Updater 执行了更新,即将退出(等待 BootLoader 重启)");
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 无更新 → 随机等待 5-10 分钟后再次检查
|
|
|
|
|
|
let wait_seconds = {
|
|
|
|
|
|
let mut rng = rand::thread_rng();
|
|
|
|
|
|
let minutes: f64 = rng.gen_range(5.0..=10.0);
|
|
|
|
|
|
(minutes * 60.0) as u64
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if config.debug_mode {
|
|
|
|
|
|
let wait_mins = wait_seconds as f64 / 60.0;
|
|
|
|
|
|
println!("Updater 本次检查完成,无更新,等待 {:.1} 分钟后再次检查...", wait_mins);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tokio::time::sleep(std::time::Duration::from_secs(wait_seconds)).await;
|
|
|
|
|
|
}
|
2026-04-07 13:21:31 +08:00
|
|
|
|
|
2026-04-07 15:09:44 +08:00
|
|
|
|
if config.debug_mode {
|
|
|
|
|
|
println!("Updater 已退出");
|
2026-04-07 10:20:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|