性能优化
This commit is contained in:
@@ -0,0 +1,459 @@
|
||||
//! 内核(mihomo.exe)安装 / 更新 / 版本查询。
|
||||
//! 子模块通过 `impl super::MihomoManager` 为管理器追加方法,可访问父模块私有字段。
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::PathBuf;
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
|
||||
use crate::constants::events::KERNEL_INSTALL_PROGRESS;
|
||||
use super::{InstallProgress, KernelInfo, KernelUpdateInfo, MihomoManager};
|
||||
|
||||
impl MihomoManager {
|
||||
// ---------- 内核 ----------
|
||||
pub fn kernel_info(&self) -> KernelInfo {
|
||||
let path = self.kernel_path();
|
||||
let exists = path.exists();
|
||||
let version = if exists {
|
||||
let mut cmd = std::process::Command::new(&path);
|
||||
cmd.arg("-v");
|
||||
// 隐藏控制台窗口(mihomo.exe -v 也会弹窗)
|
||||
crate::process_manager::setup_creation_flags(&mut cmd);
|
||||
// 重定向 stdio,避免继承主进程控制台
|
||||
cmd.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.stdin(std::process::Stdio::null());
|
||||
cmd.output()
|
||||
.ok()
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
.and_then(|s| {
|
||||
s.lines()
|
||||
.find(|l| l.contains("Mihomo Meta") || l.contains("mihomo"))
|
||||
.map(|l| l.trim().to_string())
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
KernelInfo {
|
||||
path: path.to_string_lossy().to_string(),
|
||||
exists,
|
||||
version,
|
||||
}
|
||||
}
|
||||
|
||||
/// 确保内核就位:若 cores/ 无内核,尝试从 resource 目录复制
|
||||
pub fn prepare_kernel(&self, app: &AppHandle) -> Result<KernelInfo, String> {
|
||||
let kernel = self.kernel_path();
|
||||
if !kernel.exists() {
|
||||
if let Ok(res) = app.path().resolve("binaries/mihomo.exe", tauri::path::BaseDirectory::Resource) {
|
||||
if res.exists() {
|
||||
fs::copy(&res, &kernel).map_err(|e| format!("复制内核失败: {}", e))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(self.kernel_info())
|
||||
}
|
||||
|
||||
/// 检查 GitHub 上的最新 mihomo 版本
|
||||
/// 策略:优先用 API(能拿到完整资产列表,命名变化时更健壮),
|
||||
/// 失败时回退到重定向解析(不受 API rate limit 限制)
|
||||
pub async fn check_kernel_update(&self) -> Result<KernelUpdateInfo, String> {
|
||||
match self.fetch_latest_via_api().await {
|
||||
Ok(info) => Ok(info),
|
||||
Err(api_err) => {
|
||||
crate::logger::log_warn("mihomo", &format!("API 查询失败,回退到重定向解析: {}", api_err));
|
||||
self.fetch_latest_via_redirect().await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 通过 GitHub API 查询最新版本(受 rate limit 限制:未认证 60次/小时/IP)
|
||||
async fn fetch_latest_via_api(&self) -> Result<KernelUpdateInfo, String> {
|
||||
let resp = self
|
||||
.client
|
||||
.get("https://api.github.com/repos/MetaCubeX/mihomo/releases/latest")
|
||||
.header("User-Agent", "thing-app")
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("请求 GitHub API 失败: {}", e))?;
|
||||
let status = resp.status();
|
||||
if !status.is_success() {
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
// 按字符边界截断,避免切在多字节字符中间导致 panic
|
||||
let preview: String = body.chars().take(300).collect();
|
||||
return Err(format!(
|
||||
"GitHub API 返回 HTTP {}:{}{}",
|
||||
status.as_u16(),
|
||||
preview,
|
||||
if body.chars().count() > 300 { "..." } else { "" }
|
||||
));
|
||||
}
|
||||
let resp: serde_json::Value = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("解析 GitHub 响应失败: {}", e))?;
|
||||
|
||||
let latest_version = resp
|
||||
.get("tag_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
let assets_arr = resp.get("assets").and_then(|a| a.as_array());
|
||||
|
||||
// 收集所有 windows amd64 zip 候选资产(排除 compatible/arm64/386)
|
||||
let candidates: Vec<(String, String)> = assets_arr
|
||||
.map(|assets| {
|
||||
assets.iter().filter_map(|asset| {
|
||||
let name = asset.get("name")?.as_str()?;
|
||||
let url = asset.get("browser_download_url")?.as_str()?;
|
||||
if name.starts_with("mihomo-windows-amd64-")
|
||||
&& name.ends_with(".zip")
|
||||
&& !name.contains("compatible")
|
||||
&& !name.contains("arm64")
|
||||
&& !name.contains("386")
|
||||
{
|
||||
Some((name.to_string(), url.to_string()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}).collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// 按优先级匹配:v3 标准 > v3-go124 > v3-go123 > v3 其他 > v2 > v1 > 旧命名
|
||||
let download_url = candidates
|
||||
.iter().find(|(n, _)| n.contains("-v3-v") && !n.contains("-go"))
|
||||
.or_else(|| candidates.iter().find(|(n, _)| n.contains("-v3-go124-")))
|
||||
.or_else(|| candidates.iter().find(|(n, _)| n.contains("-v3-go123-")))
|
||||
.or_else(|| candidates.iter().find(|(n, _)| n.contains("-v3-go")))
|
||||
.or_else(|| candidates.iter().find(|(n, _)| n.contains("-v2-v")))
|
||||
.or_else(|| candidates.iter().find(|(n, _)| n.contains("-v1-v")))
|
||||
.or_else(|| candidates.iter().find(|(n, _)| {
|
||||
!n.contains("-v1-") && !n.contains("-v2-") && !n.contains("-v3-")
|
||||
}))
|
||||
.map(|(_, u)| u.clone())
|
||||
.ok_or_else(|| {
|
||||
let candidates_str = candidates.iter()
|
||||
.map(|(n, _)| n.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
format!("API 未找到适用的 Windows amd64 内核资产。候选:[{}]", candidates_str)
|
||||
})?;
|
||||
|
||||
Ok(self.build_update_info(latest_version, download_url))
|
||||
}
|
||||
|
||||
/// 通过 releases/latest 重定向解析版本号(不受 API rate limit 限制)
|
||||
/// 访问 https://github.com/MetaCubeX/mihomo/releases/latest 会 302 到
|
||||
/// https://github.com/MetaCubeX/mihomo/releases/tag/v1.19.13
|
||||
/// 从最终 URL 提取版本号后,按 v1.19+ 稳定命名规则构造下载 URL
|
||||
async fn fetch_latest_via_redirect(&self) -> Result<KernelUpdateInfo, String> {
|
||||
let resp = self
|
||||
.client
|
||||
.get("https://github.com/MetaCubeX/mihomo/releases/latest")
|
||||
.header("User-Agent", "thing-app")
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("请求 GitHub releases 页面失败: {}", e))?;
|
||||
|
||||
// 从重定向后的最终 URL 提取版本号
|
||||
let final_url = resp.url().to_string();
|
||||
let latest_version = final_url
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.filter(|s| s.starts_with('v') && s.chars().any(|c| c == '.'))
|
||||
.ok_or_else(|| format!("无法从重定向 URL 提取版本号: {}", final_url))?
|
||||
.to_string();
|
||||
|
||||
// 构造下载 URL:mihomo v1.19+ 稳定使用 -v3-vX.X.X.zip 命名(CPU level v3)
|
||||
let download_url = format!(
|
||||
"https://github.com/MetaCubeX/mihomo/releases/download/{}/mihomo-windows-amd64-v3-{}.zip",
|
||||
latest_version, latest_version
|
||||
);
|
||||
|
||||
crate::logger::log_info(
|
||||
"mihomo",
|
||||
&format!("重定向解析成功: version={}, url={}", latest_version, download_url),
|
||||
);
|
||||
Ok(self.build_update_info(latest_version, download_url))
|
||||
}
|
||||
|
||||
/// 根据最新版本和下载 URL 构造更新信息(含当前版本比较)
|
||||
fn build_update_info(&self, latest_version: String, download_url: String) -> KernelUpdateInfo {
|
||||
let current = self.kernel_info().version;
|
||||
let has_update = match ¤t {
|
||||
Some(c) => {
|
||||
let cur_ver = c
|
||||
.split_whitespace()
|
||||
.find(|s| s.starts_with('v') && s.chars().filter(|c| *c == '.').count() >= 2)
|
||||
.unwrap_or("");
|
||||
cur_ver != latest_version && !latest_version.is_empty()
|
||||
}
|
||||
None => true,
|
||||
};
|
||||
KernelUpdateInfo {
|
||||
current_version: current,
|
||||
latest_version,
|
||||
download_url,
|
||||
has_update,
|
||||
}
|
||||
}
|
||||
|
||||
/// 下载并安装内核(首次安装与更新共用此方法)
|
||||
/// - mirror_prefix: 用户选择的镜像源前缀(空串=直连 GitHub)
|
||||
/// - 流式下载:实时推送下载进度到前端
|
||||
/// - zip crate 解压:替代 PowerShell,避免执行策略问题
|
||||
/// - 备份旧内核:替换前备份为 .bak
|
||||
/// 任何阶段失败都会 emit error 事件,避免前端进度卡在初始状态
|
||||
pub async fn install_kernel(&self, app: &AppHandle, mirror_prefix: String) -> Result<KernelInfo, String> {
|
||||
let result = self.install_kernel_inner(app, mirror_prefix).await;
|
||||
if let Err(ref e) = result {
|
||||
let _ = app.emit(
|
||||
KERNEL_INSTALL_PROGRESS,
|
||||
InstallProgress {
|
||||
stage: "error".into(),
|
||||
percent: 0,
|
||||
downloaded_bytes: 0,
|
||||
total_bytes: None,
|
||||
message: e.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
async fn install_kernel_inner(&self, app: &AppHandle, mirror_prefix: String) -> Result<KernelInfo, String> {
|
||||
let info = self.check_kernel_update().await?;
|
||||
let zip_path = self.cores_dir().join("mihomo-update.zip");
|
||||
let extract_dir = self.cores_dir().join("mihomo-update-tmp");
|
||||
|
||||
// 拼接用户选择的镜像源 URL
|
||||
let url = if mirror_prefix.is_empty() {
|
||||
info.download_url.clone()
|
||||
} else {
|
||||
format!("{}{}", mirror_prefix, info.download_url)
|
||||
};
|
||||
let label = if mirror_prefix.is_empty() { "GitHub 直连".to_string() } else { mirror_prefix.clone() };
|
||||
let _ = app.emit(
|
||||
KERNEL_INSTALL_PROGRESS,
|
||||
InstallProgress {
|
||||
stage: "downloading".into(),
|
||||
percent: 0,
|
||||
downloaded_bytes: 0,
|
||||
total_bytes: None,
|
||||
message: format!("正在下载:{}", label),
|
||||
},
|
||||
);
|
||||
|
||||
// 单源下载(用户已选择)
|
||||
match self.download_with_progress(app, &url, &zip_path).await {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
let msg = format!("下载失败({}):{}", label, e);
|
||||
let _ = app.emit(
|
||||
KERNEL_INSTALL_PROGRESS,
|
||||
InstallProgress {
|
||||
stage: "error".into(),
|
||||
percent: 0,
|
||||
downloaded_bytes: 0,
|
||||
total_bytes: None,
|
||||
message: msg.clone(),
|
||||
},
|
||||
);
|
||||
let _ = fs::remove_file(&zip_path);
|
||||
return Err(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// 解压阶段
|
||||
let _ = app.emit(
|
||||
KERNEL_INSTALL_PROGRESS,
|
||||
InstallProgress {
|
||||
stage: "extracting".into(),
|
||||
percent: 92,
|
||||
downloaded_bytes: 0,
|
||||
total_bytes: None,
|
||||
message: "正在解压...".into(),
|
||||
},
|
||||
);
|
||||
if extract_dir.exists() {
|
||||
fs::remove_dir_all(&extract_dir).ok();
|
||||
}
|
||||
fs::create_dir_all(&extract_dir).map_err(|e| e.to_string())?;
|
||||
if let Err(e) = self.extract_zip(&zip_path, &extract_dir) {
|
||||
let _ = app.emit(
|
||||
KERNEL_INSTALL_PROGRESS,
|
||||
InstallProgress {
|
||||
stage: "error".into(),
|
||||
percent: 0,
|
||||
downloaded_bytes: 0,
|
||||
total_bytes: None,
|
||||
message: format!("解压失败:{}", e),
|
||||
},
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// 在解压目录中递归查找 exe 文件
|
||||
// mihomo zip 内的 exe 名字通常与 zip 同名(如 mihomo-windows-amd64-v3-v1.19.13.exe),
|
||||
// 不是固定的 mihomo.exe,所以查找唯一的 .exe 文件即可
|
||||
let new_exe = self
|
||||
.find_exe_in_dir(&extract_dir)
|
||||
.ok_or_else(|| "解压后未找到任何 .exe 文件".to_string())?;
|
||||
|
||||
// 替换阶段
|
||||
let _ = app.emit(
|
||||
KERNEL_INSTALL_PROGRESS,
|
||||
InstallProgress {
|
||||
stage: "replacing".into(),
|
||||
percent: 96,
|
||||
downloaded_bytes: 0,
|
||||
total_bytes: None,
|
||||
message: "正在安装...".into(),
|
||||
},
|
||||
);
|
||||
let kernel = self.kernel_path();
|
||||
if kernel.exists() {
|
||||
let bak = self.cores_dir().join("mihomo.exe.bak");
|
||||
fs::remove_file(&bak).ok();
|
||||
fs::rename(&kernel, &bak).map_err(|e| format!("备份旧内核失败: {}", e))?;
|
||||
}
|
||||
fs::rename(&new_exe, &kernel).map_err(|e| format!("替换内核失败: {}", e))?;
|
||||
|
||||
// 清理临时文件
|
||||
fs::remove_file(&zip_path).ok();
|
||||
fs::remove_dir_all(&extract_dir).ok();
|
||||
|
||||
let final_info = self.kernel_info();
|
||||
let _ = app.emit(
|
||||
KERNEL_INSTALL_PROGRESS,
|
||||
InstallProgress {
|
||||
stage: "done".into(),
|
||||
percent: 100,
|
||||
downloaded_bytes: 0,
|
||||
total_bytes: None,
|
||||
message: format!(
|
||||
"安装完成 ({})",
|
||||
final_info.version.as_deref().unwrap_or("unknown")
|
||||
),
|
||||
},
|
||||
);
|
||||
Ok(final_info)
|
||||
}
|
||||
|
||||
/// 流式下载并实时推送进度事件
|
||||
async fn download_with_progress(
|
||||
&self,
|
||||
app: &AppHandle,
|
||||
url: &str,
|
||||
dest: &PathBuf,
|
||||
) -> Result<(), String> {
|
||||
let resp = self
|
||||
.client
|
||||
.get(url)
|
||||
.header("User-Agent", "thing-app")
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("请求失败: {}", e))?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("HTTP {}", resp.status()));
|
||||
}
|
||||
let total = resp.content_length();
|
||||
let mut stream = resp.bytes_stream();
|
||||
let mut file = fs::File::create(dest).map_err(|e| format!("创建文件失败: {}", e))?;
|
||||
let mut downloaded: u64 = 0;
|
||||
let mut last_percent: u8 = 0;
|
||||
while let Some(chunk) = stream.next().await {
|
||||
let chunk = chunk.map_err(|e| format!("读取流失败: {}", e))?;
|
||||
file.write_all(&chunk).map_err(|e| format!("写入文件失败: {}", e))?;
|
||||
downloaded += chunk.len() as u64;
|
||||
// 下载占总进度的 0-90%
|
||||
let percent = match total {
|
||||
Some(t) if t > 0 => ((downloaded as f64 / t as f64) * 90.0) as u8,
|
||||
_ => 0,
|
||||
};
|
||||
// 仅在变化超过 1% 时 emit,避免事件轰炸
|
||||
if percent >= last_percent + 1 {
|
||||
last_percent = percent;
|
||||
let _ = app.emit(
|
||||
KERNEL_INSTALL_PROGRESS,
|
||||
InstallProgress {
|
||||
stage: "downloading".into(),
|
||||
percent,
|
||||
downloaded_bytes: downloaded,
|
||||
total_bytes: total,
|
||||
message: format!("已下载 {:.2} MB", downloaded as f64 / 1024.0 / 1024.0),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
file.flush().map_err(|e| format!("flush 失败: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 用 zip crate 解压(纯 Rust,避免 PowerShell 执行策略问题)
|
||||
fn extract_zip(&self, zip_path: &PathBuf, dest: &PathBuf) -> Result<(), String> {
|
||||
let file = fs::File::open(zip_path).map_err(|e| format!("打开 zip 失败: {}", e))?;
|
||||
let mut archive = zip::ZipArchive::new(file).map_err(|e| format!("读取 zip 失败: {}", e))?;
|
||||
for i in 0..archive.len() {
|
||||
let mut entry = archive
|
||||
.by_index(i)
|
||||
.map_err(|e| format!("读取条目失败: {}", e))?;
|
||||
let outpath = match entry.enclosed_name() {
|
||||
Some(p) => dest.join(p),
|
||||
None => continue,
|
||||
};
|
||||
if entry.is_dir() {
|
||||
fs::create_dir_all(&outpath).map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
if let Some(parent) = outpath.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
||||
}
|
||||
let mut outfile = fs::File::create(&outpath).map_err(|e| e.to_string())?;
|
||||
let mut buf = [0u8; 8192];
|
||||
loop {
|
||||
let n = entry.read(&mut buf).map_err(|e| e.to_string())?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
outfile.write_all(&buf[..n]).map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 递归查找目录中的 .exe 文件
|
||||
/// mihomo zip 内的 exe 名字不固定(可能含版本号、CPU level 等),
|
||||
/// 策略:收集所有 .exe,优先返回名字含 "mihomo" 的,否则返回第一个
|
||||
fn find_exe_in_dir(&self, dir: &PathBuf) -> Option<PathBuf> {
|
||||
let mut exes: Vec<PathBuf> = Vec::new();
|
||||
self.collect_exes(dir, &mut exes);
|
||||
if exes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// 优先选名字含 mihomo 的
|
||||
exes.iter()
|
||||
.find(|p| p.file_name().and_then(|n| n.to_str()).map(|s| s.to_lowercase().contains("mihomo")).unwrap_or(false))
|
||||
.or_else(|| exes.first())
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn collect_exes(&self, dir: &PathBuf, out: &mut Vec<PathBuf>) {
|
||||
if let Ok(entries) = fs::read_dir(dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
self.collect_exes(&path, out);
|
||||
} else if path.extension().and_then(|e| e.to_str()).map(|s| s.eq_ignore_ascii_case("exe")).unwrap_or(false) {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user