主界面修改及代理模块初始化
This commit is contained in:
+86
-4
@@ -1,21 +1,92 @@
|
||||
use tauri::Manager;
|
||||
|
||||
mod logger;
|
||||
mod mihomo_manager;
|
||||
mod process_manager;
|
||||
|
||||
use logger::{
|
||||
clear_logs, get_log_info, get_logs, log_message, LogManager,
|
||||
};
|
||||
use mihomo_manager::{
|
||||
proxy_activate_profile, 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_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_profile, proxy_version, MihomoManager,
|
||||
};
|
||||
use process_manager::{
|
||||
get_all_process_status, get_process_status, start_monitoring_thread, start_process,
|
||||
stop_all_processes, stop_process, ProcessManager,
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
fn greet(name: &str) -> String {
|
||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn quit_app() {
|
||||
fn quit_app(state: tauri::State<'_, ProcessManager>) {
|
||||
// 退出前停止所有子进程
|
||||
state.stop_all();
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_autostart::Builder::new().build())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![greet, quit_app])
|
||||
.manage(ProcessManager::new())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
greet,
|
||||
quit_app,
|
||||
start_process,
|
||||
stop_process,
|
||||
get_process_status,
|
||||
get_all_process_status,
|
||||
stop_all_processes,
|
||||
log_message,
|
||||
get_logs,
|
||||
clear_logs,
|
||||
get_log_info,
|
||||
proxy_get_settings,
|
||||
proxy_save_settings,
|
||||
proxy_kernel_info,
|
||||
proxy_status,
|
||||
proxy_start,
|
||||
proxy_stop,
|
||||
proxy_restart,
|
||||
proxy_version,
|
||||
proxy_get_proxies,
|
||||
proxy_select_proxy,
|
||||
proxy_test_delay,
|
||||
proxy_get_connections,
|
||||
proxy_close_connection,
|
||||
proxy_patch_configs,
|
||||
proxy_import_profile,
|
||||
proxy_update_profile,
|
||||
proxy_delete_profile,
|
||||
proxy_activate_profile,
|
||||
proxy_set_system_proxy,
|
||||
proxy_clear_system_proxy,
|
||||
proxy_get_system_proxy
|
||||
])
|
||||
.setup(|app| {
|
||||
// 初始化日志系统,日志目录: {app_data_dir}/logs/
|
||||
let log_dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from("."))
|
||||
.join("logs");
|
||||
app.manage(LogManager::new(log_dir));
|
||||
|
||||
// 初始化 MihomoManager,数据目录: {app_data_dir}/proxy/
|
||||
let app_data_dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from("."));
|
||||
app.manage(MihomoManager::new(app_data_dir));
|
||||
|
||||
let open = tauri::menu::MenuItem::with_id(app, "open", "设置", true, None::<&str>)?;
|
||||
let quit = tauri::menu::MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
|
||||
let menu = tauri::menu::Menu::with_items(app, &[&open, &quit])?;
|
||||
@@ -32,12 +103,20 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
// 退出前停止所有子进程
|
||||
if let Some(pm) = app.try_state::<ProcessManager>() {
|
||||
pm.stop_all();
|
||||
}
|
||||
app.exit(0);
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.on_tray_icon_event(|tray, event| {
|
||||
if let tauri::tray::TrayIconEvent::Click { button: tauri::tray::MouseButton::Left, .. } = event {
|
||||
if let tauri::tray::TrayIconEvent::Click {
|
||||
button: tauri::tray::MouseButton::Left,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
let app = tray.app_handle();
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
window.show().ok();
|
||||
@@ -47,6 +126,9 @@ pub fn run() {
|
||||
})
|
||||
.build(app)?;
|
||||
|
||||
// 启动进程监控线程
|
||||
start_monitoring_thread(app.handle().clone());
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|window, event| {
|
||||
@@ -57,4 +139,4 @@ pub fn run() {
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
use chrono::Local;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// 日志级别
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum LogLevel {
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
/// 单条日志记录
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LogEntry {
|
||||
pub timestamp: String,
|
||||
pub level: LogLevel,
|
||||
pub module: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// 日志文件信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LogInfo {
|
||||
pub log_dir: String,
|
||||
pub log_files: Vec<String>,
|
||||
pub total_size_bytes: u64,
|
||||
pub max_file_size_bytes: u64,
|
||||
pub max_files: u32,
|
||||
}
|
||||
|
||||
/// 日志管理器 —— 负责文件轮转、写入、查询
|
||||
pub struct LogManager {
|
||||
log_dir: PathBuf,
|
||||
max_file_size: u64,
|
||||
max_files: u32,
|
||||
base_name: String,
|
||||
}
|
||||
|
||||
impl LogManager {
|
||||
pub fn new(log_dir: PathBuf) -> Self {
|
||||
fs::create_dir_all(&log_dir).ok();
|
||||
Self {
|
||||
log_dir,
|
||||
max_file_size: 5 * 1024 * 1024,
|
||||
max_files: 5,
|
||||
base_name: "thing".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 写入一条日志
|
||||
pub fn log(&self, level: LogLevel, module: &str, message: &str) {
|
||||
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
let level_str = match level {
|
||||
LogLevel::Debug => "DEBUG",
|
||||
LogLevel::Info => "INFO",
|
||||
LogLevel::Warn => "WARN",
|
||||
LogLevel::Error => "ERROR",
|
||||
};
|
||||
let line = format!(
|
||||
"[{}] [{}] [{}] {}\n",
|
||||
timestamp, level_str, module, message
|
||||
);
|
||||
|
||||
let current_log = self.log_dir.join(format!("{}.log", self.base_name));
|
||||
|
||||
// 检查是否需要轮转
|
||||
if let Ok(meta) = fs::metadata(¤t_log) {
|
||||
if meta.len() >= self.max_file_size {
|
||||
self.rotate();
|
||||
}
|
||||
}
|
||||
|
||||
// 追加写入
|
||||
if let Ok(mut file) = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(¤t_log)
|
||||
{
|
||||
let _ = file.write_all(line.as_bytes());
|
||||
let _ = file.flush();
|
||||
}
|
||||
}
|
||||
|
||||
/// 日志文件轮转: thing.log → thing.1.log, thing.1.log → thing.2.log, …
|
||||
fn rotate(&self) {
|
||||
// 删除最旧的文件
|
||||
let oldest = self
|
||||
.log_dir
|
||||
.join(format!("{}.{}.log", self.base_name, self.max_files));
|
||||
fs::remove_file(&oldest).ok();
|
||||
|
||||
// 依次重命名
|
||||
for i in (1..self.max_files).rev() {
|
||||
let src = self
|
||||
.log_dir
|
||||
.join(format!("{}.{}.log", self.base_name, i));
|
||||
let dst = self
|
||||
.log_dir
|
||||
.join(format!("{}.{}.log", self.base_name, i + 1));
|
||||
fs::rename(&src, &dst).ok();
|
||||
}
|
||||
|
||||
// thing.log → thing.1.log
|
||||
let current = self.log_dir.join(format!("{}.log", self.base_name));
|
||||
let first_rotated = self
|
||||
.log_dir
|
||||
.join(format!("{}.1.log", self.base_name));
|
||||
fs::rename(¤t, &first_rotated).ok();
|
||||
}
|
||||
|
||||
/// 读取日志(支持按模块/级别过滤、条数限制)
|
||||
pub fn get_logs(
|
||||
&self,
|
||||
module: Option<&str>,
|
||||
level: Option<LogLevel>,
|
||||
limit: Option<usize>,
|
||||
) -> Vec<LogEntry> {
|
||||
let mut entries: Vec<LogEntry> = Vec::new();
|
||||
|
||||
// 收集所有日志文件(包括轮转文件)
|
||||
let mut log_files: Vec<PathBuf> = Vec::new();
|
||||
let current = self.log_dir.join(format!("{}.log", self.base_name));
|
||||
if current.exists() {
|
||||
log_files.push(current);
|
||||
}
|
||||
for i in 1..=self.max_files {
|
||||
let rotated = self
|
||||
.log_dir
|
||||
.join(format!("{}.{}.log", self.base_name, i));
|
||||
if rotated.exists() {
|
||||
log_files.push(rotated);
|
||||
}
|
||||
}
|
||||
|
||||
for path in &log_files {
|
||||
if let Ok(file) = File::open(path) {
|
||||
for line in BufReader::new(file).lines().flatten() {
|
||||
if let Some(entry) = Self::parse_line(&line) {
|
||||
if let Some(ref m) = module {
|
||||
if entry.module != *m {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(ref l) = level {
|
||||
if entry.level != *l {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按时间戳降序(最新在前)
|
||||
entries.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
|
||||
|
||||
if let Some(n) = limit {
|
||||
entries.truncate(n);
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
|
||||
/// 解析一行日志: `[YYYY-MM-DD HH:MM:SS] [LEVEL] [MODULE] message`
|
||||
fn parse_line(line: &str) -> Option<LogEntry> {
|
||||
let line = line.trim();
|
||||
if line.len() < 22 || !line.starts_with('[') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let ts_end = line[1..].find(']')? + 1;
|
||||
let timestamp = line[1..ts_end].to_string();
|
||||
|
||||
let rest = line[ts_end + 1..].trim();
|
||||
if !rest.starts_with('[') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let lv_end = rest[1..].find(']')? + 1;
|
||||
let level_str = &rest[1..lv_end];
|
||||
let level = match level_str {
|
||||
"DEBUG" => LogLevel::Debug,
|
||||
"INFO" => LogLevel::Info,
|
||||
"WARN" => LogLevel::Warn,
|
||||
"ERROR" => LogLevel::Error,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let rest = rest[lv_end + 1..].trim();
|
||||
if !rest.starts_with('[') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mod_end = rest[1..].find(']')? + 1;
|
||||
let module = rest[1..mod_end].to_string();
|
||||
|
||||
let message = rest[mod_end + 1..].trim().to_string();
|
||||
|
||||
Some(LogEntry {
|
||||
timestamp,
|
||||
level,
|
||||
module,
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
/// 清空所有日志文件
|
||||
pub fn clear_logs(&self) -> Result<(), String> {
|
||||
let current = self.log_dir.join(format!("{}.log", self.base_name));
|
||||
fs::remove_file(¤t).map_err(|e| e.to_string())?;
|
||||
for i in 1..=self.max_files {
|
||||
let rotated = self
|
||||
.log_dir
|
||||
.join(format!("{}.{}.log", self.base_name, i));
|
||||
fs::remove_file(&rotated).ok();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取日志系统信息
|
||||
pub fn get_info(&self) -> LogInfo {
|
||||
let mut log_files: Vec<String> = Vec::new();
|
||||
let mut total_size: u64 = 0;
|
||||
|
||||
let current = self.log_dir.join(format!("{}.log", self.base_name));
|
||||
if current.exists() {
|
||||
if let Ok(meta) = fs::metadata(¤t) {
|
||||
total_size += meta.len();
|
||||
}
|
||||
log_files.push(format!("{}.log", self.base_name));
|
||||
}
|
||||
for i in 1..=self.max_files {
|
||||
let rotated = self
|
||||
.log_dir
|
||||
.join(format!("{}.{}.log", self.base_name, i));
|
||||
if rotated.exists() {
|
||||
if let Ok(meta) = fs::metadata(&rotated) {
|
||||
total_size += meta.len();
|
||||
}
|
||||
log_files.push(format!("{}.{}.log", self.base_name, i));
|
||||
}
|
||||
}
|
||||
|
||||
LogInfo {
|
||||
log_dir: self.log_dir.to_string_lossy().to_string(),
|
||||
log_files,
|
||||
total_size_bytes: total_size,
|
||||
max_file_size_bytes: self.max_file_size,
|
||||
max_files: self.max_files,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Tauri 命令 =====
|
||||
|
||||
#[tauri::command]
|
||||
pub fn log_message(
|
||||
state: tauri::State<'_, LogManager>,
|
||||
level: String,
|
||||
module: String,
|
||||
message: String,
|
||||
) -> Result<(), String> {
|
||||
let level = match level.as_str() {
|
||||
"debug" => LogLevel::Debug,
|
||||
"info" => LogLevel::Info,
|
||||
"warn" => LogLevel::Warn,
|
||||
"error" => LogLevel::Error,
|
||||
_ => return Err(format!("无效的日志级别: {}", level)),
|
||||
};
|
||||
state.log(level, &module, &message);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_logs(
|
||||
state: tauri::State<'_, LogManager>,
|
||||
module: Option<String>,
|
||||
level: Option<String>,
|
||||
limit: Option<usize>,
|
||||
) -> Vec<LogEntry> {
|
||||
let level = level.and_then(|l| match l.as_str() {
|
||||
"debug" => Some(LogLevel::Debug),
|
||||
"info" => Some(LogLevel::Info),
|
||||
"warn" => Some(LogLevel::Warn),
|
||||
"error" => Some(LogLevel::Error),
|
||||
_ => None,
|
||||
});
|
||||
state.get_logs(module.as_deref(), level, limit)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn clear_logs(state: tauri::State<'_, LogManager>) -> Result<(), String> {
|
||||
state.clear_logs()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_log_info(state: tauri::State<'_, LogManager>) -> LogInfo {
|
||||
state.get_info()
|
||||
}
|
||||
@@ -0,0 +1,705 @@
|
||||
use chrono::Local;
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Value as YamlValue;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri::path::BaseDirectory;
|
||||
|
||||
use crate::process_manager::{ProcessInfo, ProcessManager, StartProcessParams};
|
||||
|
||||
// ===================== 数据结构 =====================
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProxySettings {
|
||||
pub mixed_port: u16,
|
||||
pub external_controller: String,
|
||||
pub secret: String,
|
||||
pub mode: String,
|
||||
pub log_level: String,
|
||||
pub allow_lan: bool,
|
||||
pub system_proxy: bool,
|
||||
pub auto_start: bool,
|
||||
pub current_profile: Option<String>,
|
||||
pub profiles: Vec<ProfileMeta>,
|
||||
}
|
||||
|
||||
impl Default for ProxySettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mixed_port: 7890,
|
||||
external_controller: "127.0.0.1:9090".into(),
|
||||
secret: String::new(),
|
||||
mode: "rule".into(),
|
||||
log_level: "info".into(),
|
||||
allow_lan: false,
|
||||
system_proxy: false,
|
||||
auto_start: false,
|
||||
current_profile: None,
|
||||
profiles: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileMeta {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub added_at: String,
|
||||
pub updated_at: String,
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct KernelInfo {
|
||||
pub path: String,
|
||||
pub exists: bool,
|
||||
pub version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProxyStatus {
|
||||
pub running: bool,
|
||||
pub pid: Option<u32>,
|
||||
pub restart_count: u32,
|
||||
}
|
||||
|
||||
// ===================== MihomoManager =====================
|
||||
|
||||
pub struct MihomoManager {
|
||||
root: PathBuf,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
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()
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new()),
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
fs::read_to_string(self.settings_path())
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str::<ProxySettings>(&s).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
// ---------- 内核 ----------
|
||||
pub fn kernel_info(&self) -> KernelInfo {
|
||||
let path = self.kernel_path();
|
||||
let exists = path.exists();
|
||||
let version = if exists {
|
||||
std::process::Command::new(&path)
|
||||
.arg("-v")
|
||||
.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", BaseDirectory::Resource) {
|
||||
if res.exists() {
|
||||
fs::copy(&res, &kernel).map_err(|e| format!("复制内核失败: {}", e))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(self.kernel_info())
|
||||
}
|
||||
|
||||
// ---------- 配置生成 ----------
|
||||
/// 合并 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 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,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------- 订阅管理 ----------
|
||||
pub async fn import_profile(&self, url: &str, name: &str) -> Result<ProfileMeta, String> {
|
||||
let resp = self
|
||||
.client
|
||||
.get(url)
|
||||
.header("User-Agent", "clash.meta/thing")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("下载订阅失败: {}", e))?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("订阅下载失败: HTTP {}", resp.status()));
|
||||
}
|
||||
let content = resp.text().await.map_err(|e| e.to_string())?;
|
||||
if !content.contains("proxies") && !content.contains("Proxy") {
|
||||
return Err("订阅内容不像有效的 Clash/mihomo 配置".into());
|
||||
}
|
||||
let id = format!("profile-{}", Local::now().format("%Y%m%d%H%M%S"));
|
||||
let path = self.profiles_dir().join(format!("{}.yaml", id));
|
||||
fs::write(&path, &content).map_err(|e| e.to_string())?;
|
||||
let now = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
let meta = ProfileMeta {
|
||||
id: id.clone(),
|
||||
name: name.to_string(),
|
||||
url: url.to_string(),
|
||||
added_at: now.clone(),
|
||||
updated_at: now,
|
||||
size: content.len() as u64,
|
||||
};
|
||||
let mut settings = self.load_settings();
|
||||
settings.profiles.push(meta.clone());
|
||||
if settings.current_profile.is_none() {
|
||||
settings.current_profile = Some(id);
|
||||
}
|
||||
self.save_settings(&settings)?;
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
pub async fn update_profile(&self, id: &str) -> Result<ProfileMeta, String> {
|
||||
let mut settings = self.load_settings();
|
||||
let meta = settings
|
||||
.profiles
|
||||
.iter()
|
||||
.find(|p| p.id == id)
|
||||
.cloned()
|
||||
.ok_or_else(|| "订阅不存在".to_string())?;
|
||||
let resp = self
|
||||
.client
|
||||
.get(&meta.url)
|
||||
.header("User-Agent", "clash.meta/thing")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("更新订阅失败: {}", e))?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("更新订阅失败: HTTP {}", resp.status()));
|
||||
}
|
||||
let content = resp.text().await.map_err(|e| e.to_string())?;
|
||||
let path = self.profiles_dir().join(format!("{}.yaml", id));
|
||||
fs::write(&path, &content).map_err(|e| e.to_string())?;
|
||||
let now = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
let size = content.len() as u64;
|
||||
if let Some(p) = settings.profiles.iter_mut().find(|p| p.id == id) {
|
||||
p.updated_at = now.clone();
|
||||
p.size = size;
|
||||
}
|
||||
self.save_settings(&settings)?;
|
||||
Ok(ProfileMeta {
|
||||
id: id.to_string(),
|
||||
name: meta.name,
|
||||
url: meta.url,
|
||||
added_at: meta.added_at,
|
||||
updated_at: now,
|
||||
size,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_profile(&self, id: &str) -> Result<(), String> {
|
||||
let path = self.profiles_dir().join(format!("{}.yaml", id));
|
||||
fs::remove_file(&path).ok();
|
||||
let mut settings = self.load_settings();
|
||||
settings.profiles.retain(|p| p.id != id);
|
||||
if settings.current_profile.as_deref() == Some(id) {
|
||||
settings.current_profile = settings.profiles.first().map(|p| p.id.clone());
|
||||
}
|
||||
self.save_settings(&settings)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn activate_profile(&self, id: &str) -> Result<(), String> {
|
||||
let mut settings = self.load_settings();
|
||||
if !settings.profiles.iter().any(|p| p.id == id) {
|
||||
return Err("订阅不存在".into());
|
||||
}
|
||||
settings.current_profile = Some(id.to_string());
|
||||
self.save_settings(&settings)?;
|
||||
self.generate_config()
|
||||
}
|
||||
|
||||
// ---------- 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);
|
||||
}
|
||||
let resp = req.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.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
|
||||
}
|
||||
|
||||
// ===================== 系统代理(Windows) =====================
|
||||
|
||||
#[cfg(windows)]
|
||||
fn set_system_proxy_windows(addr: &str) -> Result<(), String> {
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
let (settings, _) = hkcu
|
||||
.create_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")
|
||||
.map_err(|e| e.to_string())?;
|
||||
settings
|
||||
.set_value("ProxyEnable", &1u32)
|
||||
.map_err(|e| e.to_string())?;
|
||||
settings
|
||||
.set_value("ProxyServer", &addr)
|
||||
.map_err(|e| e.to_string())?;
|
||||
notify_wininet();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn clear_system_proxy_windows() -> Result<(), String> {
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
let (settings, _) = hkcu
|
||||
.create_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")
|
||||
.map_err(|e| e.to_string())?;
|
||||
settings
|
||||
.set_value("ProxyEnable", &0u32)
|
||||
.map_err(|e| e.to_string())?;
|
||||
notify_wininet();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn get_system_proxy_windows() -> bool {
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")
|
||||
.ok()
|
||||
.and_then(|s| s.get_value::<u32, _>("ProxyEnable").ok())
|
||||
.map(|v| v != 0)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn notify_wininet() {
|
||||
unsafe {
|
||||
use windows_sys::Win32::Networking::WinInet::*;
|
||||
InternetSetOptionW(std::ptr::null(), INTERNET_OPTION_SETTINGS_CHANGED, std::ptr::null(), 0);
|
||||
InternetSetOptionW(std::ptr::null(), INTERNET_OPTION_REFRESH, std::ptr::null(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn set_system_proxy_windows(_addr: &str) -> Result<(), String> {
|
||||
Err("系统代理仅支持 Windows".into())
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
fn clear_system_proxy_windows() -> Result<(), String> {
|
||||
Err("系统代理仅支持 Windows".into())
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
fn get_system_proxy_windows() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
// ===================== Tauri 命令 =====================
|
||||
|
||||
#[tauri::command]
|
||||
pub fn proxy_get_settings(state: tauri::State<'_, MihomoManager>) -> ProxySettings {
|
||||
state.load_settings()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn proxy_save_settings(
|
||||
state: tauri::State<'_, MihomoManager>,
|
||||
settings: ProxySettings,
|
||||
) -> Result<(), String> {
|
||||
state.save_settings(&settings)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn proxy_kernel_info(
|
||||
state: tauri::State<'_, MihomoManager>,
|
||||
app: AppHandle,
|
||||
) -> Result<KernelInfo, String> {
|
||||
state.prepare_kernel(&app)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn proxy_status(pm: tauri::State<'_, ProcessManager>) -> ProxyStatus {
|
||||
match pm.get_status("proxy") {
|
||||
Some(p) => ProxyStatus {
|
||||
running: matches!(p.status, crate::process_manager::ProcessStatus::Running),
|
||||
pid: p.pid,
|
||||
restart_count: p.restart_count,
|
||||
},
|
||||
None => ProxyStatus {
|
||||
running: false,
|
||||
pid: None,
|
||||
restart_count: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn proxy_start(
|
||||
state: tauri::State<'_, MihomoManager>,
|
||||
pm: tauri::State<'_, ProcessManager>,
|
||||
app: AppHandle,
|
||||
) -> Result<ProcessInfo, String> {
|
||||
let params = state.prepare_for_start(&app)?;
|
||||
pm.start(params)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn proxy_stop(pm: tauri::State<'_, ProcessManager>) -> Result<(), String> {
|
||||
pm.stop("proxy")
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn proxy_restart(
|
||||
state: tauri::State<'_, MihomoManager>,
|
||||
pm: tauri::State<'_, ProcessManager>,
|
||||
app: AppHandle,
|
||||
) -> Result<ProcessInfo, String> {
|
||||
let _ = pm.stop("proxy");
|
||||
let params = state.prepare_for_start(&app)?;
|
||||
pm.start(params)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn proxy_version(state: tauri::State<'_, MihomoManager>) -> Result<serde_json::Value, String> {
|
||||
state.get_version().await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn proxy_get_proxies(state: tauri::State<'_, MihomoManager>) -> Result<serde_json::Value, String> {
|
||||
state.get_proxies().await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn proxy_select_proxy(
|
||||
state: tauri::State<'_, MihomoManager>,
|
||||
group: String,
|
||||
name: String,
|
||||
) -> Result<(), String> {
|
||||
state.select_proxy(&group, &name).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn proxy_test_delay(
|
||||
state: tauri::State<'_, MihomoManager>,
|
||||
name: String,
|
||||
url: Option<String>,
|
||||
timeout: Option<u32>,
|
||||
) -> Result<u32, String> {
|
||||
state
|
||||
.test_delay(
|
||||
&name,
|
||||
url.as_deref().unwrap_or("https://www.gstatic.com/generate_204"),
|
||||
timeout.unwrap_or(5000),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn proxy_get_connections(
|
||||
state: tauri::State<'_, MihomoManager>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
state.get_connections().await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn proxy_close_connection(
|
||||
state: tauri::State<'_, MihomoManager>,
|
||||
id: String,
|
||||
) -> Result<(), String> {
|
||||
state.close_connection(&id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn proxy_patch_configs(
|
||||
state: tauri::State<'_, MihomoManager>,
|
||||
body: serde_json::Value,
|
||||
) -> Result<(), String> {
|
||||
state.patch_configs(body).await
|
||||
}
|
||||
|
||||
// ---------- 订阅 ----------
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn proxy_import_profile(
|
||||
state: tauri::State<'_, MihomoManager>,
|
||||
url: String,
|
||||
name: String,
|
||||
) -> Result<ProfileMeta, String> {
|
||||
state.import_profile(&url, &name).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn proxy_update_profile(
|
||||
state: tauri::State<'_, MihomoManager>,
|
||||
id: String,
|
||||
) -> Result<ProfileMeta, String> {
|
||||
state.update_profile(&id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn proxy_delete_profile(
|
||||
state: tauri::State<'_, MihomoManager>,
|
||||
id: String,
|
||||
) -> Result<(), String> {
|
||||
state.delete_profile(&id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn proxy_activate_profile(
|
||||
state: tauri::State<'_, MihomoManager>,
|
||||
id: String,
|
||||
) -> Result<(), String> {
|
||||
state.activate_profile(&id)
|
||||
}
|
||||
|
||||
// ---------- 系统代理 ----------
|
||||
|
||||
#[tauri::command]
|
||||
pub fn proxy_set_system_proxy(
|
||||
state: tauri::State<'_, MihomoManager>,
|
||||
) -> Result<(), String> {
|
||||
let settings = state.load_settings();
|
||||
let addr = format!("127.0.0.1:{}", settings.mixed_port);
|
||||
set_system_proxy_windows(&addr)?;
|
||||
let mut settings = settings;
|
||||
settings.system_proxy = true;
|
||||
state.save_settings(&settings)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn proxy_clear_system_proxy(
|
||||
state: tauri::State<'_, MihomoManager>,
|
||||
) -> Result<(), String> {
|
||||
clear_system_proxy_windows()?;
|
||||
let mut settings = state.load_settings();
|
||||
settings.system_proxy = false;
|
||||
state.save_settings(&settings)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn proxy_get_system_proxy() -> bool {
|
||||
get_system_proxy_windows()
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
|
||||
/// 进程状态枚举
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ProcessStatus {
|
||||
Running,
|
||||
Stopped,
|
||||
Crashed,
|
||||
#[allow(dead_code)]
|
||||
Starting,
|
||||
}
|
||||
|
||||
/// 进程信息(返回给前端)
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct ProcessInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub status: ProcessStatus,
|
||||
pub pid: Option<u32>,
|
||||
pub restart_count: u32,
|
||||
}
|
||||
|
||||
/// 进程启动参数(从前端传入)
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StartProcessParams {
|
||||
pub id: String,
|
||||
pub executable: String,
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub cwd: Option<String>,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub restart_on_crash: bool,
|
||||
#[serde(default = "default_max_restarts")]
|
||||
pub max_restarts: u32,
|
||||
}
|
||||
|
||||
fn default_max_restarts() -> u32 {
|
||||
3
|
||||
}
|
||||
|
||||
/// 内部进程条目
|
||||
struct ProcessEntry {
|
||||
child: Child,
|
||||
name: String,
|
||||
restart_count: u32,
|
||||
max_restarts: u32,
|
||||
restart_on_crash: bool,
|
||||
executable: String,
|
||||
args: Vec<String>,
|
||||
cwd: Option<String>,
|
||||
}
|
||||
|
||||
/// 进程管理器 —— 管理子进程的完整生命周期
|
||||
pub struct ProcessManager {
|
||||
processes: Mutex<HashMap<String, ProcessEntry>>,
|
||||
}
|
||||
|
||||
impl ProcessManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
processes: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动一个子进程
|
||||
pub fn start(&self, params: StartProcessParams) -> Result<ProcessInfo, String> {
|
||||
let mut processes = self.processes.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
// 如果已有同名进程且仍在运行,返回错误
|
||||
if let Some(entry) = processes.get_mut(¶ms.id) {
|
||||
if entry.child.try_wait().map_err(|e| e.to_string())?.is_none() {
|
||||
return Err(format!("进程 '{}' 已在运行中", params.id));
|
||||
}
|
||||
// 进程已退出,移除旧记录
|
||||
processes.remove(¶ms.id);
|
||||
}
|
||||
|
||||
let mut cmd = Command::new(¶ms.executable);
|
||||
cmd.args(¶ms.args);
|
||||
if let Some(ref dir) = params.cwd {
|
||||
cmd.current_dir(dir);
|
||||
}
|
||||
// 子进程的 stdout/stderr/stdin 不继承主进程
|
||||
cmd.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.stdin(Stdio::null());
|
||||
|
||||
let child = cmd
|
||||
.spawn()
|
||||
.map_err(|e| format!("启动进程 '{}' 失败: {}", params.id, e))?;
|
||||
let pid = child.id();
|
||||
|
||||
let entry = ProcessEntry {
|
||||
child,
|
||||
name: params.name.clone(),
|
||||
restart_count: 0,
|
||||
max_restarts: params.max_restarts,
|
||||
restart_on_crash: params.restart_on_crash,
|
||||
executable: params.executable,
|
||||
args: params.args,
|
||||
cwd: params.cwd,
|
||||
};
|
||||
|
||||
processes.insert(params.id.clone(), entry);
|
||||
|
||||
Ok(ProcessInfo {
|
||||
id: params.id,
|
||||
name: params.name,
|
||||
status: ProcessStatus::Running,
|
||||
pid: Some(pid),
|
||||
restart_count: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// 停止指定进程
|
||||
pub fn stop(&self, id: &str) -> Result<(), String> {
|
||||
let mut processes = self.processes.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
if let Some(mut entry) = processes.remove(id) {
|
||||
entry
|
||||
.child
|
||||
.kill()
|
||||
.map_err(|e| format!("终止进程 '{}' 失败: {}", id, e))?;
|
||||
let _ = entry.child.wait();
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("进程 '{}' 不存在", id))
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止所有进程(应用退出时调用)
|
||||
pub fn stop_all(&self) {
|
||||
if let Ok(mut processes) = self.processes.lock() {
|
||||
for (id, mut entry) in processes.drain() {
|
||||
let _ = entry.child.kill();
|
||||
let _ = entry.child.wait();
|
||||
println!("[ProcessManager] 已停止进程: {}", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取单个进程状态
|
||||
pub fn get_status(&self, id: &str) -> Option<ProcessInfo> {
|
||||
let mut processes = self.processes.lock().ok()?;
|
||||
let entry = processes.get_mut(id)?;
|
||||
|
||||
let (status, pid) = match entry.child.try_wait() {
|
||||
Ok(None) => (ProcessStatus::Running, Some(entry.child.id())),
|
||||
Ok(Some(_)) => (ProcessStatus::Crashed, None),
|
||||
Err(_) => (ProcessStatus::Stopped, None),
|
||||
};
|
||||
|
||||
Some(ProcessInfo {
|
||||
id: id.to_string(),
|
||||
name: entry.name.clone(),
|
||||
status,
|
||||
pid,
|
||||
restart_count: entry.restart_count,
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取所有进程状态
|
||||
pub fn get_all_status(&self) -> Vec<ProcessInfo> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
if let Ok(mut processes) = self.processes.lock() {
|
||||
for (id, entry) in processes.iter_mut() {
|
||||
let (status, pid) = match entry.child.try_wait() {
|
||||
Ok(None) => (ProcessStatus::Running, Some(entry.child.id())),
|
||||
Ok(Some(_)) => (ProcessStatus::Crashed, None),
|
||||
Err(_) => (ProcessStatus::Stopped, None),
|
||||
};
|
||||
|
||||
result.push(ProcessInfo {
|
||||
id: id.clone(),
|
||||
name: entry.name.clone(),
|
||||
status,
|
||||
pid,
|
||||
restart_count: entry.restart_count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// 检查所有进程,处理崩溃的进程(自动重启或移除)
|
||||
/// 返回状态发生变化的进程列表
|
||||
pub fn check_and_cleanup(&self) -> Vec<ProcessInfo> {
|
||||
let mut changes = Vec::new();
|
||||
|
||||
let mut processes = match self.processes.lock() {
|
||||
Ok(p) => p,
|
||||
Err(_) => return changes,
|
||||
};
|
||||
|
||||
let ids: Vec<String> = processes.keys().cloned().collect();
|
||||
|
||||
for id in ids {
|
||||
if let Some(entry) = processes.get_mut(&id) {
|
||||
match entry.child.try_wait() {
|
||||
Ok(None) => {
|
||||
// 仍在运行,无需处理
|
||||
}
|
||||
Ok(Some(_)) => {
|
||||
// 进程已退出
|
||||
if entry.restart_on_crash
|
||||
&& (entry.max_restarts == 0
|
||||
|| entry.restart_count < entry.max_restarts)
|
||||
{
|
||||
// 自动重启
|
||||
let restart_count = entry.restart_count + 1;
|
||||
let executable = entry.executable.clone();
|
||||
let args = entry.args.clone();
|
||||
let cwd = entry.cwd.clone();
|
||||
let name = entry.name.clone();
|
||||
|
||||
// 先终止旧进程
|
||||
let _ = entry.child.kill();
|
||||
let _ = entry.child.wait();
|
||||
|
||||
// 重新启动
|
||||
let mut cmd = Command::new(&executable);
|
||||
cmd.args(&args);
|
||||
if let Some(ref dir) = cwd {
|
||||
cmd.current_dir(dir);
|
||||
}
|
||||
cmd.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.stdin(Stdio::null());
|
||||
|
||||
match cmd.spawn() {
|
||||
Ok(new_child) => {
|
||||
let pid = new_child.id();
|
||||
entry.child = new_child;
|
||||
entry.restart_count = restart_count;
|
||||
|
||||
changes.push(ProcessInfo {
|
||||
id: id.clone(),
|
||||
name: name.clone(),
|
||||
status: ProcessStatus::Running,
|
||||
pid: Some(pid),
|
||||
restart_count,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"[ProcessManager] 重启进程 '{}' 失败: {}",
|
||||
id, e
|
||||
);
|
||||
processes.remove(&id);
|
||||
changes.push(ProcessInfo {
|
||||
id: id.clone(),
|
||||
name,
|
||||
status: ProcessStatus::Crashed,
|
||||
pid: None,
|
||||
restart_count,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 不自动重启,移除记录
|
||||
let name = entry.name.clone();
|
||||
let restart_count = entry.restart_count;
|
||||
processes.remove(&id);
|
||||
changes.push(ProcessInfo {
|
||||
id: id.clone(),
|
||||
name,
|
||||
status: ProcessStatus::Stopped,
|
||||
pid: None,
|
||||
restart_count,
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// try_wait 出错
|
||||
let name = entry.name.clone();
|
||||
let restart_count = entry.restart_count;
|
||||
processes.remove(&id);
|
||||
changes.push(ProcessInfo {
|
||||
id: id.clone(),
|
||||
name,
|
||||
status: ProcessStatus::Stopped,
|
||||
pid: None,
|
||||
restart_count,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changes
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Tauri 命令 =====
|
||||
|
||||
#[tauri::command]
|
||||
pub fn start_process(
|
||||
state: tauri::State<'_, ProcessManager>,
|
||||
params: StartProcessParams,
|
||||
) -> Result<ProcessInfo, String> {
|
||||
state.start(params)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn stop_process(
|
||||
state: tauri::State<'_, ProcessManager>,
|
||||
id: String,
|
||||
) -> Result<(), String> {
|
||||
state.stop(&id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_process_status(
|
||||
state: tauri::State<'_, ProcessManager>,
|
||||
id: String,
|
||||
) -> Option<ProcessInfo> {
|
||||
state.get_status(&id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_all_process_status(
|
||||
state: tauri::State<'_, ProcessManager>,
|
||||
) -> Vec<ProcessInfo> {
|
||||
state.get_all_status()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn stop_all_processes(state: tauri::State<'_, ProcessManager>) {
|
||||
state.stop_all()
|
||||
}
|
||||
|
||||
/// 启动后台监控线程,定期检查进程状态并向前端发送事件
|
||||
pub fn start_monitoring_thread(app: AppHandle) {
|
||||
thread::spawn(move || {
|
||||
loop {
|
||||
thread::sleep(Duration::from_secs(3));
|
||||
|
||||
let state = app.state::<ProcessManager>();
|
||||
let changes = state.check_and_cleanup();
|
||||
|
||||
for change in changes {
|
||||
let _ = app.emit("process-status-changed", &change);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user