Files
Thing/src-tauri/src/mihomo_manager/mod.rs
T
2026-08-07 18:09:13 +08:00

438 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 代理模块(mihomo 管理器):按功能域拆分子模块。
//!
//! - [`MihomoManager`]:核心状态与目录/设置/配置/API 方法
//! - [`kernel`]:内核安装与更新
//! - [`profiles`]:订阅管理
//! - [`system_proxy`]Windows 系统代理开关
//! - [`commands`]Tauri 命令层
mod commands;
mod kernel;
mod profiles;
mod pseudo;
mod system_proxy;
mod types;
pub use pseudo::is_pseudo_node;
pub use system_proxy::get_system_proxy_windows;
pub use types::{InstallProgress, KernelInfo, KernelUpdateInfo, ProfileMeta, ProxySettings, ProxyStatus};
pub use commands::{
proxy_activate_profile, proxy_check_kernel_update, proxy_clear_system_proxy, proxy_close_connection,
proxy_delete_profile, proxy_get_connections, proxy_get_proxies, proxy_get_settings, proxy_get_system_proxy,
proxy_import_profile, proxy_install_kernel, proxy_kernel_info, proxy_patch_configs, proxy_restart,
proxy_save_settings, proxy_select_proxy, proxy_set_system_proxy, proxy_start, proxy_status, proxy_stop,
proxy_test_delay, proxy_update_kernel, proxy_update_profile, proxy_version,
};
use reqwest::Client;
use serde_yaml::Value as YamlValue;
use std::fs;
use std::path::PathBuf;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use tauri::AppHandle;
use crate::process_manager::{ProcessManager, StartProcessParams};
// ===================== MihomoManager =====================
/// settings 内存缓存条目(短时复用,避免高频状态轮询反复读盘)
struct SettingsCacheEntry {
read_at: Instant,
settings: ProxySettings,
}
pub struct MihomoManager {
root: PathBuf,
client: Client,
settings_cache: Mutex<Option<SettingsCacheEntry>>,
}
impl MihomoManager {
pub fn new(app_data_dir: PathBuf) -> Self {
let root = app_data_dir.join("proxy");
for d in ["cores", "mihomo", "profiles", "logs"] {
fs::create_dir_all(root.join(d)).ok();
}
Self {
root,
client: Client::builder()
// 默认 30s 兜底超时,防止遗漏显式 timeout 的请求永久悬挂
.timeout(std::time::Duration::from_secs(30))
.build()
.unwrap_or_else(|_| Client::new()),
settings_cache: Mutex::new(None),
}
}
fn cores_dir(&self) -> PathBuf {
self.root.join("cores")
}
pub fn kernel_path(&self) -> PathBuf {
self.cores_dir().join("mihomo.exe")
}
fn mihomo_dir(&self) -> PathBuf {
self.root.join("mihomo")
}
fn config_path(&self) -> PathBuf {
self.mihomo_dir().join("config.yaml")
}
fn profiles_dir(&self) -> PathBuf {
self.root.join("profiles")
}
#[allow(dead_code)]
fn logs_dir(&self) -> PathBuf {
self.root.join("logs")
}
fn settings_path(&self) -> PathBuf {
self.root.join("settings.json")
}
// ---------- 设置 ----------
pub fn load_settings(&self) -> ProxySettings {
// 内存缓存:500ms 内复用(高频调用如状态轮询/测速避免反复读盘)
if let Ok(cache) = self.settings_cache.lock() {
if let Some(entry) = cache.as_ref() {
if entry.read_at.elapsed() < Duration::from_millis(500) {
return entry.settings.clone();
}
}
}
let mut settings = fs::read_to_string(self.settings_path())
.ok()
.and_then(|s| serde_json::from_str::<ProxySettings>(&s).ok())
.unwrap_or_default();
// 恢复机制:扫描磁盘 profile 文件,补全 settings.profiles
// 防止 settings.json 损坏(如反序列化失败被 default 覆盖)导致订阅丢失
if self.reconcile_profiles(&mut settings) {
let _ = self.save_settings(&settings);
}
// 刷新缓存
if let Ok(mut cache) = self.settings_cache.lock() {
*cache = Some(SettingsCacheEntry {
read_at: Instant::now(),
settings: settings.clone(),
});
}
settings
}
/// 扫描磁盘 profile 文件,补全 settings.profiles 中缺失的条目。
/// 返回 true 表示有变化需要保存。
fn reconcile_profiles(&self, settings: &mut ProxySettings) -> bool {
let mut changed = false;
let existing_ids: std::collections::HashSet<String> =
settings.profiles.iter().map(|p| p.id.clone()).collect();
if let Ok(entries) = fs::read_dir(self.profiles_dir()) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("yaml") {
continue;
}
let Some(id) = path
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_string())
else {
continue;
};
if existing_ids.contains(&id) {
continue;
}
let size = fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
let updated_at = fs::metadata(&path)
.and_then(|m| m.modified())
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.and_then(|d| chrono::DateTime::from_timestamp(d.as_secs() as i64, 0))
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
.unwrap_or_default();
settings.profiles.push(ProfileMeta {
added_at: updated_at.clone(),
id: id.clone(),
name: id,
url: String::new(),
updated_at,
size,
});
changed = true;
}
}
// 如果 currentProfile 为 null 但有 profile,设置为第一个
if settings.current_profile.is_none() && !settings.profiles.is_empty() {
settings.current_profile = Some(settings.profiles[0].id.clone());
changed = true;
}
changed
}
pub fn save_settings(&self, settings: &ProxySettings) -> Result<(), String> {
let s = serde_json::to_string_pretty(settings).map_err(|e| e.to_string())?;
fs::write(self.settings_path(), s).map_err(|e| e.to_string())?;
// 写盘成功后同步刷新内存缓存(避免旧缓存被后续 load_settings 复用)
if let Ok(mut cache) = self.settings_cache.lock() {
*cache = Some(SettingsCacheEntry {
read_at: Instant::now(),
settings: settings.clone(),
});
}
Ok(())
}
// ---------- 配置生成 ----------
/// 合并 profile + 控制器设置,生成运行时 config.yaml
pub fn generate_config(&self) -> Result<(), String> {
let settings = self.load_settings();
let mut value: YamlValue = if let Some(id) = &settings.current_profile {
let path = self.profiles_dir().join(format!("{}.yaml", id));
if path.exists() {
let content = fs::read_to_string(&path).map_err(|e| e.to_string())?;
serde_yaml::from_str(&content).unwrap_or(YamlValue::Mapping(serde_yaml::Mapping::new()))
} else {
YamlValue::Mapping(serde_yaml::Mapping::new())
}
} else {
YamlValue::Mapping(serde_yaml::Mapping::new())
};
if !value.is_mapping() {
value = YamlValue::Mapping(serde_yaml::Mapping::new());
}
let m = value.as_mapping_mut().unwrap();
m.insert(YamlValue::String("mixed-port".into()), YamlValue::Number(settings.mixed_port.into()));
m.insert(
YamlValue::String("external-controller".into()),
YamlValue::String(settings.external_controller.clone()),
);
if !settings.secret.is_empty() {
m.insert(YamlValue::String("secret".into()), YamlValue::String(settings.secret.clone()));
}
m.insert(YamlValue::String("mode".into()), YamlValue::String(settings.mode.clone()));
m.insert(
YamlValue::String("log-level".into()),
YamlValue::String(settings.log_level.clone()),
);
m.insert(YamlValue::String("allow-lan".into()), YamlValue::Bool(settings.allow_lan));
// 日志写入文件,便于排查问题
let log_file = self.logs_dir().join("mihomo.log");
m.insert(
YamlValue::String("log-file".into()),
YamlValue::String(log_file.to_string_lossy().to_string()),
);
// Geo 数据库下载源(使用 jsdelivr 国内可访问镜像,避免无代理时 GitHub 超时)
let mut geox = serde_yaml::Mapping::new();
geox.insert(
YamlValue::String("mmdb".into()),
YamlValue::String("https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/country.mmdb".into()),
);
geox.insert(
YamlValue::String("geosite".into()),
YamlValue::String("https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat".into()),
);
geox.insert(
YamlValue::String("asn".into()),
YamlValue::String("https://cdn.jsdelivr.net/gh/xishang0128/bdg@master/GeoLite2-ASN.mmdb".into()),
);
m.insert(YamlValue::String("geox-url".into()), YamlValue::Mapping(geox));
let yaml = serde_yaml::to_string(&value).map_err(|e| e.to_string())?;
fs::write(self.config_path(), yaml).map_err(|e| e.to_string())?;
Ok(())
}
/// 构建启动 mihomo 所需的进程参数(含 prepare + config 生成)
pub fn prepare_for_start(&self, app: &AppHandle) -> Result<StartProcessParams, String> {
let info = self.prepare_kernel(app)?;
if !info.exists {
return Err(format!(
"mihomo 内核未安装。请将 mihomo.exe 放置到 src-tauri/binaries/ 后重新运行,或直接放到:\n{}",
self.cores_dir().to_string_lossy()
));
}
self.generate_config()?;
Ok(StartProcessParams {
id: "proxy".into(),
executable: self.kernel_path().to_string_lossy().to_string(),
args: vec![
"-d".into(),
self.mihomo_dir().to_string_lossy().to_string(),
"-f".into(),
self.config_path().to_string_lossy().to_string(),
],
cwd: Some(self.mihomo_dir().to_string_lossy().to_string()),
name: "mihomo".into(),
restart_on_crash: true,
max_restarts: 3,
})
}
/// 应用启动时检查是否需要自动启动 mihomo 和系统代理
pub fn auto_start_on_launch(&self, app: &AppHandle, pm: &ProcessManager) {
let settings = self.load_settings();
if !settings.auto_start {
return;
}
match self.prepare_for_start(app) {
Ok(params) => {
if let Err(e) = pm.start(params) {
crate::logger::log_error("mihomo", &format!("自动启动失败: {}", e));
} else if settings.auto_system_proxy {
// 启动成功后开启系统代理
let addr = format!("127.0.0.1:{}", settings.mixed_port);
let _ = system_proxy::set_system_proxy_windows(&addr);
let mut s = settings;
s.system_proxy = true;
let _ = self.save_settings(&s);
}
}
Err(e) => {
crate::logger::log_warn("mihomo", &format!("自动启动跳过: {}", e));
}
}
}
/// 应用退出时清理:关闭系统代理
pub fn cleanup_on_exit(&self) {
let settings = self.load_settings();
if settings.system_proxy || settings.auto_system_proxy {
let _ = system_proxy::clear_system_proxy_windows();
}
}
// ---------- mihomo API ----------
fn api_url(&self, path: &str) -> String {
let s = self.load_settings();
format!("http://{}{}", s.external_controller, path)
}
fn api_bearer(&self) -> Option<String> {
let s = self.load_settings();
if s.secret.is_empty() {
None
} else {
Some(format!("Bearer {}", s.secret))
}
}
async fn api_get(&self, path: &str) -> Result<serde_json::Value, String> {
let mut req = self.client.get(self.api_url(path));
if let Some(b) = self.api_bearer() {
req = req.header("Authorization", b);
}
// 显式超时:mihomo 卡死/未响应时命令立即返回,避免前端按钮永久转圈
let resp = req
.timeout(std::time::Duration::from_secs(10))
.send()
.await
.map_err(|e| format!("请求 mihomo 失败: {}", e))?;
if !resp.status().is_success() {
return Err(format!("mihomo API 错误: {}", resp.status()));
}
resp.json().await.map_err(|e| e.to_string())
}
async fn api_request(
&self,
method: reqwest::Method,
path: &str,
body: Option<serde_json::Value>,
) -> Result<(), String> {
let mut req = self.client.request(method, self.api_url(path));
if let Some(b) = self.api_bearer() {
req = req.header("Authorization", b);
}
if let Some(b) = body {
req = req.json(&b);
}
let resp = req
.timeout(std::time::Duration::from_secs(10))
.send()
.await
.map_err(|e| format!("请求 mihomo 失败: {}", e))?;
if !resp.status().is_success() {
return Err(format!("mihomo API 错误: {}", resp.status()));
}
Ok(())
}
pub async fn get_version(&self) -> Result<serde_json::Value, String> {
self.api_get("/version").await
}
pub async fn get_proxies(&self) -> Result<serde_json::Value, String> {
self.api_get("/proxies").await
}
pub async fn select_proxy(&self, group: &str, name: &str) -> Result<(), String> {
self.api_request(
reqwest::Method::PUT,
&format!("/proxies/{}", url_encode(group)),
Some(serde_json::json!({ "name": name })),
)
.await
}
pub async fn test_delay(&self, name: &str, url: &str, timeout: u32) -> Result<u32, String> {
let path = format!(
"/proxies/{}/delay?timeout={}&url={}",
url_encode(name),
timeout,
url_encode(url)
);
let v = self.api_get(&path).await?;
v.get("delay")
.and_then(|d| d.as_u64())
.map(|d| d as u32)
.ok_or_else(|| {
v.get("message")
.and_then(|m| m.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| "测速失败".into())
})
}
#[allow(dead_code)]
pub async fn get_rules(&self) -> Result<serde_json::Value, String> {
self.api_get("/rules").await
}
pub async fn get_connections(&self) -> Result<serde_json::Value, String> {
self.api_get("/connections").await
}
pub async fn close_connection(&self, id: &str) -> Result<(), String> {
self.api_request(
reqwest::Method::DELETE,
&format!("/connections/{}", url_encode(id)),
None,
)
.await
}
pub async fn patch_configs(&self, body: serde_json::Value) -> Result<(), String> {
self.api_request(reqwest::Method::PATCH, "/configs", Some(body)).await
}
}
fn url_encode(s: &str) -> String {
// 仅对路径段做最小编码,避免引入额外依赖
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(b as char);
}
_ => out.push_str(&format!("%{:02X}", b)),
}
}
out
}