Compare commits

...

37 Commits

56 changed files with 1257 additions and 1431 deletions

View File

@@ -87,6 +87,7 @@
- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122以解决 Intel 架构 Mac 无法运行内核的问题
- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单
- 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书`
- 允许设置 Mihomo 端口范围 1000(含) - 65536(含)
</details>

View File

@@ -40,10 +40,10 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^7.3.4",
"@mui/icons-material": "^7.3.5",
"@mui/lab": "7.0.0-beta.17",
"@mui/material": "^7.3.4",
"@mui/x-data-grid": "^8.16.0",
"@mui/material": "^7.3.5",
"@mui/x-data-grid": "^8.17.0",
"@tauri-apps/api": "2.9.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-dialog": "^2.4.2",
@@ -54,7 +54,7 @@
"@tauri-apps/plugin-updater": "2.9.0",
"@types/json-schema": "^7.0.15",
"ahooks": "^3.9.6",
"axios": "^1.13.1",
"axios": "^1.13.2",
"dayjs": "1.11.19",
"foxact": "^0.2.49",
"i18next": "^25.6.0",
@@ -68,7 +68,7 @@
"react-dom": "19.2.0",
"react-error-boundary": "6.0.0",
"react-hook-form": "^7.66.0",
"react-i18next": "16.2.3",
"react-i18next": "16.2.4",
"react-markdown": "10.1.0",
"react-monaco-editor": "0.59.0",
"react-router": "^7.9.5",
@@ -80,20 +80,20 @@
"devDependencies": {
"@actions/github": "^6.0.1",
"@eslint-react/eslint-plugin": "^2.3.1",
"@eslint/js": "^9.39.0",
"@tauri-apps/cli": "2.9.2",
"@eslint/js": "^9.39.1",
"@tauri-apps/cli": "2.9.3",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.10.0",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-react-swc": "^4.2.0",
"@vitejs/plugin-react-swc": "^4.2.1",
"adm-zip": "^0.5.16",
"cli-color": "^2.0.4",
"commander": "^14.0.2",
"cross-env": "^10.1.0",
"eslint": "^9.39.0",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import-x": "^4.16.1",
@@ -112,13 +112,13 @@
"prettier": "^3.6.2",
"sass": "^1.93.3",
"tar": "^7.5.2",
"terser": "^5.44.0",
"terser": "^5.44.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.2",
"vite": "^7.1.12",
"typescript-eslint": "^8.46.3",
"vite": "^7.2.1",
"vite-plugin-monaco-editor-esm": "^2.0.2",
"vite-plugin-svgr": "^4.5.0",
"vitest": "^4.0.6"
"vitest": "^4.0.7"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [

810
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -226,3 +226,23 @@ needless_raw_string_hashes = "deny" # Too many in existing code
or_fun_call = "deny"
cognitive_complexity = "deny"
useless_let_if_seq = "deny"
use_self = "deny"
tuple_array_conversions = "deny"
trait_duplication_in_bounds = "deny"
suspicious_operation_groupings = "deny"
string_lit_as_bytes = "deny"
significant_drop_tightening = "deny"
significant_drop_in_scrutinee = "deny"
redundant_clone = "deny"
# option_if_let_else = "deny" // 过于激进,暂时不开启
needless_pass_by_ref_mut = "deny"
needless_collect = "deny"
missing_const_for_fn = "deny"
iter_with_drain = "deny"
iter_on_single_items = "deny"
iter_on_empty_collections = "deny"
# fallible_impl_from = "deny" // 过于激进,暂时不开启
equatable_if_let = "deny"
collection_is_never_read = "deny"
branches_sharing_code = "deny"

View File

@@ -189,7 +189,6 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
})?;
logging!(info, Type::Config, "DNS config successfully applied");
handle::Handle::refresh_clash();
} else {
// 当关闭DNS设置时重新生成配置不加载DNS配置文件
logging!(
@@ -212,9 +211,9 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
})?;
logging!(info, Type::Config, "Config regenerated successfully");
handle::Handle::refresh_clash();
}
handle::Handle::refresh_clash();
Ok(())
}

View File

@@ -13,19 +13,23 @@ pub(super) async fn check_youtube_premium(client: &Client) -> UnlockItem {
Ok(response) => {
if let Ok(body) = response.text().await {
let body_lower = body.to_lowercase();
let mut status = "Failed";
let mut region = None;
if body_lower.contains("youtube premium is not available in your country") {
return UnlockItem {
name: "Youtube Premium".to_string(),
status: "No".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
if body_lower.contains("ad-free") {
let re = match Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#) {
Ok(re) => re,
status = "No";
} else if body_lower.contains("ad-free") {
match Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#) {
Ok(re) => {
if let Some(caps) = re.captures(&body)
&& let Some(m) = caps.get(1)
{
let country_code = m.as_str().trim();
let emoji = country_code_to_emoji(country_code);
region = Some(format!("{emoji}{country_code}"));
status = "Yes";
}
}
Err(e) => {
logging!(
error,
@@ -33,34 +37,14 @@ pub(super) async fn check_youtube_premium(client: &Client) -> UnlockItem {
"Failed to compile YouTube Premium regex: {}",
e
);
return UnlockItem {
name: "Youtube Premium".to_string(),
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
let region = re.captures(&body).and_then(|caps| {
caps.get(1).map(|m| {
let country_code = m.as_str().trim();
let emoji = country_code_to_emoji(country_code);
format!("{emoji}{country_code}")
})
});
return UnlockItem {
name: "Youtube Premium".to_string(),
status: "Yes".to_string(),
region,
check_time: Some(get_local_date_string()),
};
}
}
UnlockItem {
name: "Youtube Premium".to_string(),
status: "Failed".to_string(),
region: None,
status: status.to_string(),
region,
check_time: Some(get_local_date_string()),
}
} else {

View File

@@ -27,8 +27,8 @@ static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
pub async fn get_profiles() -> CmdResult<SharedBox<IProfiles>> {
logging!(debug, Type::Cmd, "获取配置文件列表");
let draft = Config::profiles().await;
let latest = draft.latest_arc();
Ok(latest)
let data = draft.data_arc();
Ok(data)
}
/// 增强配置文件
@@ -100,9 +100,11 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
match profiles_reorder_safe(&active_id, &over_id).await {
Ok(_) => {
logging!(info, Type::Cmd, "重新排序配置文件");
Config::profiles().await.apply();
Ok(())
}
Err(err) => {
Config::profiles().await.discard();
logging!(error, Type::Cmd, "重新排序配置文件失败: {}", err);
Err(format!("重新排序配置文件失败: {}", err).into())
}
@@ -120,12 +122,16 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
logging!(info, Type::Cmd, "[创建订阅] 发送配置变更通知: {}", uid);
handle::Handle::notify_profile_changed(uid.clone());
}
Config::profiles().await.apply();
Ok(())
}
Err(err) => match err.to_string().as_str() {
"the file already exists" => Err("the file already exists".into()),
_ => Err(format!("add profile error: {err}").into()),
},
Err(err) => {
Config::profiles().await.discard();
match err.to_string().as_str() {
"the file already exists" => Err("the file already exists".into()),
_ => Err(format!("add profile error: {err}").into()),
}
}
}
}
@@ -133,8 +139,12 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
#[tauri::command]
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
match feat::update_profile(&index, option.as_ref(), true, true).await {
Ok(_) => Ok(()),
Ok(_) => {
let _: () = Config::profiles().await.apply();
Ok(())
}
Err(e) => {
Config::profiles().await.discard();
logging!(error, Type::Cmd, "{}", e);
Err(e.to_string().into())
}
@@ -144,12 +154,11 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
/// 删除配置文件
#[tauri::command]
pub async fn delete_profile(index: String) -> CmdResult {
println!("delete_profile: {}", index);
// 使用Send-safe helper函数
let should_update = profiles_delete_item_safe(&index).await.stringify_err()?;
profiles_save_file_safe().await.stringify_err()?;
if should_update {
Config::profiles().await.apply();
match CoreManager::global().update_config().await {
Ok(_) => {
handle::Handle::refresh_clash();
@@ -235,7 +244,7 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
);
handle::Handle::notice_message(
"config_validate::yaml_syntax_error",
error_msg.clone(),
error_msg,
);
Err(())
}
@@ -244,7 +253,7 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
logging!(error, Type::Cmd, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::yaml_parse_error",
error_msg.clone(),
error_msg,
);
Err(())
}
@@ -253,19 +262,13 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
Ok(Err(err)) => {
let error_msg = format!("无法读取目标配置文件: {err}");
logging!(error, Type::Cmd, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_error",
error_msg.clone(),
);
handle::Handle::notice_message("config_validate::file_read_error", error_msg);
Err(())
}
Err(_) => {
let error_msg = "读取配置文件超时(5秒)".to_string();
logging!(error, Type::Cmd, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_timeout",
error_msg.clone(),
);
handle::Handle::notice_message("config_validate::file_read_timeout", error_msg);
Err(())
}
}
@@ -275,16 +278,15 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
}
/// 执行配置更新并处理结果
async fn restore_previous_profile(prev_profile: String) -> CmdResult<()> {
async fn restore_previous_profile(prev_profile: &String) -> CmdResult<()> {
logging!(info, Type::Cmd, "尝试恢复到之前的配置: {}", prev_profile);
let restore_profiles = IProfiles {
current: Some(prev_profile),
current: Some(prev_profile.to_owned()),
items: None,
};
Config::profiles()
.await
.edit_draft(|d| d.patch_config(&restore_profiles))
.stringify_err()?;
.edit_draft(|d| d.patch_config(&restore_profiles));
Config::profiles().await.apply();
crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = profiles_save_file_safe().await {
@@ -295,7 +297,7 @@ async fn restore_previous_profile(prev_profile: String) -> CmdResult<()> {
Ok(())
}
async fn handle_success(current_value: Option<String>) -> CmdResult<bool> {
async fn handle_success(current_value: Option<&String>) -> CmdResult<bool> {
Config::profiles().await.apply();
handle::Handle::refresh_clash();
@@ -311,9 +313,9 @@ async fn handle_success(current_value: Option<String>) -> CmdResult<bool> {
logging!(warn, Type::Cmd, "Warning: 异步保存配置文件失败: {e}");
}
if let Some(current) = &current_value {
logging!(info, Type::Cmd, "向前端发送配置变更事件: {}", current,);
handle::Handle::notify_profile_changed(current.clone());
if let Some(current) = current_value {
logging!(info, Type::Cmd, "向前端发送配置变更事件: {}", current);
handle::Handle::notify_profile_changed(current.to_owned());
}
Ok(true)
@@ -321,7 +323,7 @@ async fn handle_success(current_value: Option<String>) -> CmdResult<bool> {
async fn handle_validation_failure(
error_msg: String,
current_profile: Option<String>,
current_profile: Option<&String>,
) -> CmdResult<bool> {
logging!(warn, Type::Cmd, "配置验证失败: {}", error_msg);
Config::profiles().await.discard();
@@ -339,7 +341,7 @@ async fn handle_update_error<E: std::fmt::Display>(e: E) -> CmdResult<bool> {
Ok(false)
}
async fn handle_timeout(current_profile: Option<String>) -> CmdResult<bool> {
async fn handle_timeout(current_profile: Option<&String>) -> CmdResult<bool> {
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
logging!(error, Type::Cmd, "{}", timeout_msg);
Config::profiles().await.discard();
@@ -351,9 +353,12 @@ async fn handle_timeout(current_profile: Option<String>) -> CmdResult<bool> {
}
async fn perform_config_update(
current_value: Option<String>,
current_profile: Option<String>,
current_value: Option<&String>,
current_profile: Option<&String>,
) -> CmdResult<bool> {
defer! {
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
}
let update_result = tokio::time::timeout(
Duration::from_secs(30),
CoreManager::global().update_config(),
@@ -376,13 +381,10 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
.is_err()
{
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
return Err("switch_in_progress".into());
return Ok(false);
}
defer! {
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
}
let target_profile = profiles.current.clone();
let target_profile = profiles.current.as_ref();
logging!(
info,
@@ -392,24 +394,22 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
);
// 保存当前配置,以便在验证失败时恢复
let current_profile = Config::profiles().await.latest_arc().current.clone();
logging!(info, Type::Cmd, "当前配置: {:?}", current_profile);
let previous_profile = Config::profiles().await.data_arc().current.clone();
logging!(info, Type::Cmd, "当前配置: {:?}", previous_profile);
// 如果要切换配置,先检查目标配置文件是否有语法错误
if let Some(new_profile) = profiles.current.as_ref()
&& current_profile.as_ref() != Some(new_profile)
&& validate_new_profile(new_profile).await.is_err()
if let Some(switch_to_profile) = target_profile
&& previous_profile.as_ref() != Some(switch_to_profile)
&& validate_new_profile(switch_to_profile).await.is_err()
{
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
return Ok(false);
}
let _ = Config::profiles()
Config::profiles()
.await
.edit_draft(|d| d.patch_config(&profiles));
let current_value = profiles.current.clone();
perform_config_update(current_value, current_profile).await
perform_config_update(target_profile, previous_profile.as_ref()).await
}
/// 根据profile name修改profiles

View File

@@ -1,26 +1,28 @@
use std::sync::Arc;
use super::CmdResult;
use crate::{
core::{CoreManager, handle},
core::{CoreManager, handle, manager::RunningMode},
logging,
module::sysinfo::PlatformSpecification,
utils::logging::Type,
};
#[cfg(target_os = "windows")]
use deelevate::{PrivilegeLevel, Token};
use once_cell::sync::Lazy;
use std::{
sync::atomic::{AtomicI64, Ordering},
time::{SystemTime, UNIX_EPOCH},
};
use tauri_plugin_clipboard_manager::ClipboardExt;
use tokio::time::Instant;
// 存储应用启动时间的全局变量
static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
// 获取当前系统时间,转换为毫秒级时间戳
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
AtomicI64::new(now)
static APP_START_TIME: Lazy<Instant> = Lazy::new(Instant::now);
#[cfg(not(target_os = "windows"))]
static APPS_RUN_AS_ADMIN: Lazy<bool> = Lazy::new(|| unsafe { libc::geteuid() } == 0);
#[cfg(target_os = "windows")]
static APPS_RUN_AS_ADMIN: Lazy<bool> = Lazy::new(|| {
Token::with_current_process()
.and_then(|token| token.privilege_level())
.map(|level| level != PrivilegeLevel::NotPrivileged)
.unwrap_or(false)
});
#[tauri::command]
@@ -45,52 +47,18 @@ pub async fn get_system_info() -> CmdResult<String> {
/// 获取当前内核运行模式
#[tauri::command]
pub async fn get_running_mode() -> Result<String, String> {
Ok(CoreManager::global().get_running_mode().to_string())
pub async fn get_running_mode() -> Result<Arc<RunningMode>, String> {
Ok(CoreManager::global().get_running_mode())
}
/// 获取应用的运行时间(毫秒)
#[tauri::command]
pub fn get_app_uptime() -> CmdResult<i64> {
let start_time = APP_START_TIME.load(Ordering::Relaxed);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
Ok(now - start_time)
pub fn get_app_uptime() -> CmdResult<u128> {
Ok(APP_START_TIME.elapsed().as_millis())
}
/// 检查应用是否以管理员身份运行
#[tauri::command]
#[cfg(target_os = "windows")]
pub fn is_admin() -> CmdResult<bool> {
use deelevate::{PrivilegeLevel, Token};
let result = Token::with_current_process()
.and_then(|token| token.privilege_level())
.map(|level| level != PrivilegeLevel::NotPrivileged)
.unwrap_or(false);
Ok(result)
}
/// 非Windows平台检测是否以管理员身份运行
#[tauri::command]
#[cfg(not(target_os = "windows"))]
pub fn is_admin() -> CmdResult<bool> {
#[cfg(target_os = "macos")]
{
Ok(unsafe { libc::geteuid() } == 0)
}
#[cfg(target_os = "linux")]
{
Ok(unsafe { libc::geteuid() } == 0)
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
Ok(false)
}
Ok(*APPS_RUN_AS_ADMIN)
}

View File

@@ -17,7 +17,7 @@ mod platform {
mod platform {
use super::CmdResult;
pub fn invoke_uwp_tool() -> CmdResult {
pub const fn invoke_uwp_tool() -> CmdResult {
Ok(())
}
}

View File

@@ -1,16 +1,10 @@
use super::CmdResult;
use crate::{
cmd::StringifyErr,
config::{Config, IVerge},
feat,
utils::draft::SharedBox,
};
use crate::{cmd::StringifyErr, config::IVerge, feat, utils::draft::SharedBox};
/// 获取Verge配置
#[tauri::command]
pub async fn get_verge_config() -> CmdResult<SharedBox<IVerge>> {
let verge = Config::verge().await;
Ok(verge.latest_arc())
feat::fetch_verge_config().await.stringify_err()
}
/// 修改Verge配置

View File

@@ -22,11 +22,11 @@ pub struct Config {
}
impl Config {
pub async fn global() -> &'static Config {
pub async fn global() -> &'static Self {
static CONFIG: OnceCell<Config> = OnceCell::const_new();
CONFIG
.get_or_init(|| async {
Config {
Self {
clash_config: Draft::new(IClashTemp::new().await),
verge_config: Draft::new(IVerge::new().await),
profiles_config: Draft::new(IProfiles::new().await),
@@ -60,15 +60,15 @@ impl Config {
if !cmd::system::is_admin().unwrap_or_default()
&& service::is_service_available().await.is_err()
{
let verge = Config::verge().await;
let verge = Self::verge().await;
verge.edit_draft(|d| {
d.enable_tun_mode = Some(false);
});
verge.apply();
let _ = tray::Tray::global().update_tray_display().await;
let _ = tray::Tray::global().update_menu().await;
// 分离数据获取和异步调用避免Send问题
let verge_data = Config::verge().await.latest_arc();
let verge_data = Self::verge().await.latest_arc();
logging_error!(Type::Core, verge_data.save_file().await);
}
@@ -154,23 +154,21 @@ impl Config {
ConfigType::Check => dirs::app_home_dir()?.join(files::CHECK_CONFIG),
};
let runtime = Config::runtime().await;
let config = runtime
.latest_arc()
let runtime = Self::runtime().await;
let runtime_arc = runtime.latest_arc();
let config = runtime_arc
.config
.as_ref()
.ok_or_else(|| anyhow!("failed to get runtime config"))?
.clone();
drop(runtime); // 显式释放锁
.ok_or_else(|| anyhow!("failed to get runtime config"))?;
help::save_yaml(&path, &config, Some("# Generated by Clash Verge")).await?;
help::save_yaml(&path, config, Some("# Generated by Clash Verge")).await?;
Ok(path)
}
pub async fn generate() -> Result<()> {
let (config, exists_keys, logs) = enhance::enhance().await;
Config::runtime().await.edit_draft(|d| {
Self::runtime().await.edit_draft(|d| {
*d = IRuntime {
config: Some(config),
exists_keys,
@@ -191,11 +189,11 @@ impl Config {
};
let operation = || async {
if Config::runtime().await.latest_arc().config.is_some() {
if Self::runtime().await.latest_arc().config.is_some() {
return Ok::<(), BackoffError<anyhow::Error>>(());
}
Config::generate().await.map_err(BackoffError::transient)
Self::generate().await.map_err(BackoffError::transient)
};
if let Err(e) = backoff::future::retry(backoff_strategy, operation).await {

View File

@@ -5,9 +5,16 @@ use aes_gcm::{
};
use base64::{Engine, engine::general_purpose::STANDARD};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::cell::Cell;
use std::future::Future;
const NONCE_LENGTH: usize = 12;
// Use task-local context so the flag follows the async task across threads
tokio::task_local! {
static ENCRYPTION_ACTIVE: Cell<bool>;
}
/// Encrypt data
#[allow(deprecated)]
pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
@@ -59,39 +66,45 @@ where
T: Serialize,
S: Serializer,
{
// 如果序列化失败,返回 None
let json = match serde_json::to_string(value) {
Ok(j) => j,
Err(_) => return serializer.serialize_none(),
};
// 如果加密失败,返回 None
match encrypt_data(&json) {
Ok(encrypted) => serializer.serialize_str(&encrypted),
Err(_) => serializer.serialize_none(),
if is_encryption_active() {
let json = serde_json::to_string(value).map_err(serde::ser::Error::custom)?;
let encrypted = encrypt_data(&json).map_err(serde::ser::Error::custom)?;
serializer.serialize_str(&encrypted)
} else {
value.serialize(serializer)
}
}
/// Deserialize decrypted function
pub fn deserialize_encrypted<'a, T, D>(deserializer: D) -> Result<T, D::Error>
pub fn deserialize_encrypted<'a, D, T>(deserializer: D) -> Result<T, D::Error>
where
T: for<'de> Deserialize<'de> + Default,
D: Deserializer<'a>,
{
// 如果反序列化字符串失败,返回默认值
let encrypted = match String::deserialize(deserializer) {
Ok(s) => s,
Err(_) => return Ok(T::default()),
};
if is_encryption_active() {
let encrypted_opt: Option<String> = Option::deserialize(deserializer)?;
// 如果解密失败,返回默认值
let decrypted_string = match decrypt_data(&encrypted) {
Ok(data) => data,
Err(_) => return Ok(T::default()),
};
// 如果 JSON 解析失败,返回默认值
match serde_json::from_str(&decrypted_string) {
Ok(value) => Ok(value),
Err(_) => Ok(T::default()),
match encrypted_opt {
Some(encrypted) if !encrypted.is_empty() => {
let decrypted_string =
decrypt_data(&encrypted).map_err(serde::de::Error::custom)?;
serde_json::from_str(&decrypted_string).map_err(serde::de::Error::custom)
}
_ => Ok(T::default()),
}
} else {
T::deserialize(deserializer)
}
}
pub async fn with_encryption<F, Fut, R>(f: F) -> R
where
F: FnOnce() -> Fut,
Fut: Future<Output = R>,
{
ENCRYPTION_ACTIVE.scope(Cell::new(true), f()).await
}
fn is_encryption_active() -> bool {
ENCRYPTION_ACTIVE.try_with(|c| c.get()).unwrap_or(false)
}

View File

@@ -152,7 +152,7 @@ impl PrfOption {
impl PrfItem {
/// From partial item
/// must contain `itype`
pub async fn from(item: &PrfItem, file_data: Option<String>) -> Result<PrfItem> {
pub async fn from(item: &Self, file_data: Option<String>) -> Result<Self> {
if item.itype.is_none() {
bail!("type should not be null");
}
@@ -170,13 +170,13 @@ impl PrfItem {
let name = item.name.as_ref();
let desc = item.desc.as_ref();
let option = item.option.as_ref();
PrfItem::from_url(url, name, desc, option).await
Self::from_url(url, name, desc, option).await
}
"local" => {
let name = item.name.clone().unwrap_or_else(|| "Local File".into());
let desc = item.desc.clone().unwrap_or_else(|| "".into());
let option = item.option.as_ref();
PrfItem::from_local(name, desc, file_data, option).await
Self::from_local(name, desc, file_data, option).await
}
typ => bail!("invalid profile item type \"{typ}\""),
}
@@ -189,7 +189,7 @@ impl PrfItem {
desc: String,
file_data: Option<String>,
option: Option<&PrfOption>,
) -> Result<PrfItem> {
) -> Result<Self> {
let uid = help::get_uid("L").into();
let file = format!("{uid}.yaml").into();
let opt_ref = option.as_ref();
@@ -201,31 +201,31 @@ impl PrfItem {
let mut groups = opt_ref.and_then(|o| o.groups.clone());
if merge.is_none() {
let merge_item = &mut PrfItem::from_merge(None)?;
let merge_item = &mut Self::from_merge(None)?;
profiles::profiles_append_item_safe(merge_item).await?;
merge = merge_item.uid.clone();
}
if script.is_none() {
let script_item = &mut PrfItem::from_script(None)?;
let script_item = &mut Self::from_script(None)?;
profiles::profiles_append_item_safe(script_item).await?;
script = script_item.uid.clone();
}
if rules.is_none() {
let rules_item = &mut PrfItem::from_rules()?;
let rules_item = &mut Self::from_rules()?;
profiles::profiles_append_item_safe(rules_item).await?;
rules = rules_item.uid.clone();
}
if proxies.is_none() {
let proxies_item = &mut PrfItem::from_proxies()?;
let proxies_item = &mut Self::from_proxies()?;
profiles::profiles_append_item_safe(proxies_item).await?;
proxies = proxies_item.uid.clone();
}
if groups.is_none() {
let groups_item = &mut PrfItem::from_groups()?;
let groups_item = &mut Self::from_groups()?;
profiles::profiles_append_item_safe(groups_item).await?;
groups = groups_item.uid.clone();
}
Ok(PrfItem {
Ok(Self {
uid: Some(uid),
itype: Some("local".into()),
name: Some(name),
@@ -256,7 +256,7 @@ impl PrfItem {
name: Option<&String>,
desc: Option<&String>,
option: Option<&PrfOption>,
) -> Result<PrfItem> {
) -> Result<Self> {
let with_proxy = option.is_some_and(|o| o.with_proxy.unwrap_or(false));
let self_proxy = option.is_some_and(|o| o.self_proxy.unwrap_or(false));
let accept_invalid_certs =
@@ -393,32 +393,32 @@ impl PrfItem {
}
if merge.is_none() {
let merge_item = &mut PrfItem::from_merge(None)?;
let merge_item = &mut Self::from_merge(None)?;
profiles::profiles_append_item_safe(merge_item).await?;
merge = merge_item.uid.clone();
}
if script.is_none() {
let script_item = &mut PrfItem::from_script(None)?;
let script_item = &mut Self::from_script(None)?;
profiles::profiles_append_item_safe(script_item).await?;
script = script_item.uid.clone();
}
if rules.is_none() {
let rules_item = &mut PrfItem::from_rules()?;
let rules_item = &mut Self::from_rules()?;
profiles::profiles_append_item_safe(rules_item).await?;
rules = rules_item.uid.clone();
}
if proxies.is_none() {
let proxies_item = &mut PrfItem::from_proxies()?;
let proxies_item = &mut Self::from_proxies()?;
profiles::profiles_append_item_safe(proxies_item).await?;
proxies = proxies_item.uid.clone();
}
if groups.is_none() {
let groups_item = &mut PrfItem::from_groups()?;
let groups_item = &mut Self::from_groups()?;
profiles::profiles_append_item_safe(groups_item).await?;
groups = groups_item.uid.clone();
}
Ok(PrfItem {
Ok(Self {
uid: Some(uid),
itype: Some("remote".into()),
name: Some(name),
@@ -445,16 +445,15 @@ impl PrfItem {
/// ## Merge type (enhance)
/// create the enhanced item by using `merge` rule
pub fn from_merge(uid: Option<String>) -> Result<PrfItem> {
let mut id = help::get_uid("m").into();
let mut template = tmpl::ITEM_MERGE_EMPTY.into();
if let Some(uid) = uid {
id = uid;
template = tmpl::ITEM_MERGE.into();
}
pub fn from_merge(uid: Option<String>) -> Result<Self> {
let (id, template) = if let Some(uid) = uid {
(uid, tmpl::ITEM_MERGE.into())
} else {
(help::get_uid("m").into(), tmpl::ITEM_MERGE_EMPTY.into())
};
let file = format!("{id}.yaml").into();
Ok(PrfItem {
Ok(Self {
uid: Some(id),
itype: Some("merge".into()),
name: None,
@@ -472,14 +471,14 @@ impl PrfItem {
/// ## Script type (enhance)
/// create the enhanced item by using javascript quick.js
pub fn from_script(uid: Option<String>) -> Result<PrfItem> {
let mut id = help::get_uid("s").into();
if let Some(uid) = uid {
id = uid;
}
pub fn from_script(uid: Option<String>) -> Result<Self> {
let id = if let Some(uid) = uid {
uid
} else {
help::get_uid("s").into()
};
let file = format!("{id}.js").into(); // js ext
Ok(PrfItem {
Ok(Self {
uid: Some(id),
itype: Some("script".into()),
name: None,
@@ -496,11 +495,11 @@ impl PrfItem {
}
/// ## Rules type (enhance)
pub fn from_rules() -> Result<PrfItem> {
pub fn from_rules() -> Result<Self> {
let uid = help::get_uid("r").into();
let file = format!("{uid}.yaml").into(); // yaml ext
Ok(PrfItem {
Ok(Self {
uid: Some(uid),
itype: Some("rules".into()),
name: None,
@@ -517,11 +516,11 @@ impl PrfItem {
}
/// ## Proxies type (enhance)
pub fn from_proxies() -> Result<PrfItem> {
pub fn from_proxies() -> Result<Self> {
let uid = help::get_uid("p").into();
let file = format!("{uid}.yaml").into(); // yaml ext
Ok(PrfItem {
Ok(Self {
uid: Some(uid),
itype: Some("proxies".into()),
name: None,
@@ -538,11 +537,11 @@ impl PrfItem {
}
/// ## Groups type (enhance)
pub fn from_groups() -> Result<PrfItem> {
pub fn from_groups() -> Result<Self> {
let uid = help::get_uid("g").into();
let file = format!("{uid}.yaml").into(); // yaml ext
Ok(PrfItem {
Ok(Self {
uid: Some(uid),
itype: Some("groups".into()),
name: None,
@@ -584,7 +583,34 @@ impl PrfItem {
}
}
impl PrfItem {
/// 获取current指向的订阅的merge
pub fn current_merge(&self) -> Option<String> {
self.option.as_ref().and_then(|o| o.merge.clone())
}
/// 获取current指向的订阅的script
pub fn current_script(&self) -> Option<String> {
self.option.as_ref().and_then(|o| o.script.clone())
}
/// 获取current指向的订阅的rules
pub fn current_rules(&self) -> Option<String> {
self.option.as_ref().and_then(|o| o.rules.clone())
}
/// 获取current指向的订阅的proxies
pub fn current_proxies(&self) -> Option<String> {
self.option.as_ref().and_then(|o| o.proxies.clone())
}
/// 获取current指向的订阅的groups
pub fn current_groups(&self) -> Option<String> {
self.option.as_ref().and_then(|o| o.groups.clone())
}
}
// 向前兼容,默认为订阅启用自动更新
fn default_allow_auto_update() -> Option<bool> {
const fn default_allow_auto_update() -> Option<bool> {
Some(true)
}

View File

@@ -8,7 +8,7 @@ use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::collections::HashSet;
use std::{collections::HashSet, sync::Arc};
use tokio::fs;
/// Define the `profiles.yaml` schema
@@ -32,7 +32,7 @@ pub struct CleanupResult {
macro_rules! patch {
($lv: expr, $rv: expr, $key: tt) => {
if ($rv.$key).is_some() {
$lv.$key = $rv.$key.clone();
$lv.$key = $rv.$key.to_owned();
}
};
}
@@ -50,28 +50,26 @@ impl IProfiles {
}
None
}
pub async fn new() -> Self {
match dirs::profiles_path() {
Ok(path) => match help::read_yaml::<Self>(&path).await {
Ok(mut profiles) => {
if profiles.items.is_none() {
profiles.items = Some(vec![]);
let path = match dirs::profiles_path() {
Ok(p) => p,
Err(err) => {
logging!(error, Type::Config, "{err}");
return Self::default();
}
};
match help::read_yaml::<Self>(&path).await {
Ok(mut profiles) => {
let items = profiles.items.get_or_insert_with(Vec::new);
for item in items.iter_mut() {
if item.uid.is_none() {
item.uid = Some(help::get_uid("d").into());
}
// compatible with the old old old version
if let Some(items) = profiles.items.as_mut() {
for item in items.iter_mut() {
if item.uid.is_none() {
item.uid = Some(help::get_uid("d").into());
}
}
}
profiles
}
Err(err) => {
logging!(error, Type::Config, "{err}");
Self::default()
}
},
profiles
}
Err(err) => {
logging!(error, Type::Config, "{err}");
Self::default()
@@ -89,7 +87,7 @@ impl IProfiles {
}
/// 只修改currentvalid和chain
pub fn patch_config(&mut self, patch: &IProfiles) -> Result<()> {
pub fn patch_config(&mut self, patch: &Self) {
if self.items.is_none() {
self.items = Some(vec![]);
}
@@ -102,16 +100,14 @@ impl IProfiles {
self.current = some_uid.cloned();
}
}
Ok(())
}
pub fn get_current(&self) -> Option<String> {
self.current.clone()
pub const fn get_current(&self) -> Option<&String> {
self.current.as_ref()
}
/// get items ref
pub fn get_items(&self) -> Option<&Vec<PrfItem>> {
pub const fn get_items(&self) -> Option<&Vec<PrfItem>> {
self.items.as_ref()
}
@@ -132,6 +128,15 @@ impl IProfiles {
bail!("failed to get the profile item \"uid:{}\"", uid_str);
}
pub fn get_item_arc(&self, uid: &str) -> Option<Arc<PrfItem>> {
self.items.as_ref().and_then(|items| {
items
.iter()
.find(|it| it.uid.as_deref() == Some(uid))
.map(|it| Arc::new(it.clone()))
})
}
/// append new item
/// if the file_data is some
/// then should save the data to file
@@ -357,88 +362,18 @@ impl IProfiles {
}
}
/// 获取current指向的订阅的merge
pub fn current_merge(&self) -> Option<String> {
match (self.current.as_ref(), self.items.as_ref()) {
(Some(current), Some(items)) => {
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
let merge = item.option.as_ref().and_then(|e| e.merge.clone());
return merge;
}
None
}
_ => None,
}
}
/// 获取current指向的订阅的script
pub fn current_script(&self) -> Option<String> {
match (self.current.as_ref(), self.items.as_ref()) {
(Some(current), Some(items)) => {
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
let script = item.option.as_ref().and_then(|e| e.script.clone());
return script;
}
None
}
_ => None,
}
}
/// 获取current指向的订阅的rules
pub fn current_rules(&self) -> Option<String> {
match (self.current.as_ref(), self.items.as_ref()) {
(Some(current), Some(items)) => {
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
let rules = item.option.as_ref().and_then(|e| e.rules.clone());
return rules;
}
None
}
_ => None,
}
}
/// 获取current指向的订阅的proxies
pub fn current_proxies(&self) -> Option<String> {
match (self.current.as_ref(), self.items.as_ref()) {
(Some(current), Some(items)) => {
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
let proxies = item.option.as_ref().and_then(|e| e.proxies.clone());
return proxies;
}
None
}
_ => None,
}
}
/// 获取current指向的订阅的groups
pub fn current_groups(&self) -> Option<String> {
match (self.current.as_ref(), self.items.as_ref()) {
(Some(current), Some(items)) => {
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
let groups = item.option.as_ref().and_then(|e| e.groups.clone());
return groups;
}
None
}
_ => None,
}
}
/// 判断profile是否是current指向的
pub fn is_current_profile_index(&self, index: &String) -> bool {
self.current.as_ref() == Some(index)
}
/// 获取所有的profiles(uid名称)
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(String, String)>> {
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(&String, &String)>> {
self.items.as_ref().map(|items| {
items
.iter()
.filter_map(|e| {
if let (Some(uid), Some(name)) = (e.uid.clone(), e.name.clone()) {
if let (Some(uid), Some(name)) = (e.uid.as_ref(), e.name.as_ref()) {
Some((uid, name))
} else {
None
@@ -449,11 +384,11 @@ impl IProfiles {
}
/// 通过 uid 获取名称
pub fn get_name_by_uid(&self, uid: &String) -> Option<String> {
pub fn get_name_by_uid(&self, uid: &String) -> Option<&String> {
if let Some(items) = &self.items {
for item in items {
if item.uid.as_ref() == Some(uid) {
return item.name.clone();
return item.name.as_ref();
}
}
}
@@ -551,14 +486,14 @@ impl IProfiles {
}
/// 获取所有 active profile 关联的文件名
fn get_all_active_files(&self) -> HashSet<String> {
let mut active_files = HashSet::new();
fn get_all_active_files(&self) -> HashSet<&str> {
let mut active_files: HashSet<&str> = HashSet::new();
if let Some(items) = &self.items {
for item in items {
// 收集所有类型 profile 的文件
if let Some(file) = &item.file {
active_files.insert(file.clone());
active_files.insert(file);
}
// 对于主 profile 类型remote/local还需要收集其关联的扩展文件
@@ -571,35 +506,35 @@ impl IProfiles {
&& let Ok(merge_item) = self.get_item(merge_uid)
&& let Some(file) = &merge_item.file
{
active_files.insert(file.clone());
active_files.insert(file);
}
if let Some(script_uid) = &option.script
&& let Ok(script_item) = self.get_item(script_uid)
&& let Some(file) = &script_item.file
{
active_files.insert(file.clone());
active_files.insert(file);
}
if let Some(rules_uid) = &option.rules
&& let Ok(rules_item) = self.get_item(rules_uid)
&& let Some(file) = &rules_item.file
{
active_files.insert(file.clone());
active_files.insert(file);
}
if let Some(proxies_uid) = &option.proxies
&& let Ok(proxies_item) = self.get_item(proxies_uid)
&& let Some(file) = &proxies_item.file
{
active_files.insert(file.clone());
active_files.insert(file);
}
if let Some(groups_uid) = &option.groups
&& let Ok(groups_item) = self.get_item(groups_uid)
&& let Some(file) = &groups_item.file
{
active_files.insert(file.clone());
active_files.insert(file);
}
}
}

View File

@@ -46,19 +46,24 @@ pub struct IVerge {
pub enable_memory_usage: Option<bool>,
/// enable group icon
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_group_icon: Option<bool>,
/// common tray icon
#[serde(skip_serializing_if = "Option::is_none")]
pub common_tray_icon: Option<bool>,
/// tray icon
#[cfg(target_os = "macos")]
#[serde(skip_serializing_if = "Option::is_none")]
pub tray_icon: Option<String>,
/// menu icon
#[serde(skip_serializing_if = "Option::is_none")]
pub menu_icon: Option<String>,
/// menu order
#[serde(skip_serializing_if = "Option::is_none")]
pub menu_order: Option<Vec<String>>,
/// sysproxy tray icon
@@ -115,6 +120,7 @@ pub struct IVerge {
/// hotkey map
/// format: {func},{key}
#[serde(skip_serializing_if = "Option::is_none")]
pub hotkeys: Option<Vec<String>>,
/// enable global hotkey
@@ -134,7 +140,7 @@ pub struct IVerge {
pub default_latency_test: Option<String>,
/// 默认的延迟测试超时时间
pub default_latency_timeout: Option<i32>,
pub default_latency_timeout: Option<i16>,
/// 是否自动检测当前节点延迟
pub enable_auto_delay_detection: Option<bool>,
@@ -143,7 +149,7 @@ pub struct IVerge {
pub enable_builtin_enhanced: Option<bool>,
/// proxy 页面布局 列数
pub proxy_layout_column: Option<i32>,
pub proxy_layout_column: Option<u8>,
/// 测试站列表
pub test_list: Option<Vec<IVergeTestItem>>,
@@ -202,6 +208,7 @@ pub struct IVerge {
)]
pub webdav_password: Option<String>,
#[serde(skip)]
pub enable_tray_speed: Option<bool>,
// pub enable_tray_icon: Option<bool>,
@@ -255,7 +262,7 @@ impl IVerge {
/// 验证并修正配置文件中的clash_core值
pub async fn validate_and_fix_config() -> Result<()> {
let config_path = dirs::verge_path()?;
let mut config = match help::read_yaml::<IVerge>(&config_path).await {
let mut config = match help::read_yaml::<Self>(&config_path).await {
Ok(config) => config,
Err(_) => Self::template(),
};
@@ -304,7 +311,7 @@ impl IVerge {
}
/// 配置修正后重新加载配置
async fn reload_config_after_fix(updated_config: IVerge) -> Result<()> {
async fn reload_config_after_fix(updated_config: Self) -> Result<()> {
logging!(
info,
Type::Config,
@@ -344,7 +351,7 @@ impl IVerge {
pub async fn new() -> Self {
match dirs::verge_path() {
Ok(path) => match help::read_yaml::<IVerge>(&path).await {
Ok(path) => match help::read_yaml::<Self>(&path).await {
Ok(mut config) => {
// compatibility
if let Some(start_page) = config.start_page.clone()
@@ -439,7 +446,7 @@ impl IVerge {
/// patch verge config
/// only save to file
#[allow(clippy::cognitive_complexity)]
pub fn patch_config(&mut self, patch: &IVerge) {
pub fn patch_config(&mut self, patch: &Self) {
macro_rules! patch {
($key: tt) => {
if patch.$key.is_some() {
@@ -524,7 +531,7 @@ impl IVerge {
patch!(enable_external_controller);
}
pub fn get_singleton_port() -> u16 {
pub const fn get_singleton_port() -> u16 {
crate::constants::network::ports::SINGLETON_SERVER
}
@@ -545,156 +552,3 @@ impl IVerge {
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct IVergeResponse {
pub app_log_level: Option<String>,
pub app_log_max_size: Option<u64>,
pub app_log_max_count: Option<usize>,
pub language: Option<String>,
pub theme_mode: Option<String>,
pub tray_event: Option<String>,
pub env_type: Option<String>,
pub start_page: Option<String>,
pub startup_script: Option<String>,
pub traffic_graph: Option<bool>,
pub enable_memory_usage: Option<bool>,
pub enable_group_icon: Option<bool>,
pub common_tray_icon: Option<bool>,
#[cfg(target_os = "macos")]
pub tray_icon: Option<String>,
pub menu_icon: Option<String>,
pub menu_order: Option<Vec<String>>,
pub sysproxy_tray_icon: Option<bool>,
pub tun_tray_icon: Option<bool>,
pub enable_tun_mode: Option<bool>,
pub enable_auto_launch: Option<bool>,
pub enable_silent_start: Option<bool>,
pub enable_system_proxy: Option<bool>,
pub enable_proxy_guard: Option<bool>,
pub enable_global_hotkey: Option<bool>,
pub use_default_bypass: Option<bool>,
pub system_proxy_bypass: Option<String>,
pub proxy_guard_duration: Option<u64>,
pub proxy_auto_config: Option<bool>,
pub pac_file_content: Option<String>,
pub proxy_host: Option<String>,
pub theme_setting: Option<IVergeTheme>,
pub web_ui_list: Option<Vec<String>>,
pub clash_core: Option<String>,
pub hotkeys: Option<Vec<String>>,
pub auto_close_connection: Option<bool>,
pub auto_check_update: Option<bool>,
pub default_latency_test: Option<String>,
pub default_latency_timeout: Option<i32>,
pub enable_auto_delay_detection: Option<bool>,
pub enable_builtin_enhanced: Option<bool>,
pub proxy_layout_column: Option<i32>,
pub test_list: Option<Vec<IVergeTestItem>>,
pub auto_log_clean: Option<i32>,
#[cfg(not(target_os = "windows"))]
pub verge_redir_port: Option<u16>,
#[cfg(not(target_os = "windows"))]
pub verge_redir_enabled: Option<bool>,
#[cfg(target_os = "linux")]
pub verge_tproxy_port: Option<u16>,
#[cfg(target_os = "linux")]
pub verge_tproxy_enabled: Option<bool>,
pub verge_mixed_port: Option<u16>,
pub verge_socks_port: Option<u16>,
pub verge_socks_enabled: Option<bool>,
pub verge_port: Option<u16>,
pub verge_http_enabled: Option<bool>,
pub webdav_url: Option<String>,
pub webdav_username: Option<String>,
pub webdav_password: Option<String>,
pub enable_tray_speed: Option<bool>,
// pub enable_tray_icon: Option<bool>,
pub tray_inline_proxy_groups: Option<bool>,
pub enable_auto_light_weight_mode: Option<bool>,
pub auto_light_weight_minutes: Option<u64>,
pub enable_dns_settings: Option<bool>,
pub home_cards: Option<serde_json::Value>,
pub enable_hover_jump_navigator: Option<bool>,
pub hover_jump_navigator_delay: Option<u64>,
pub enable_external_controller: Option<bool>,
}
impl From<IVerge> for IVergeResponse {
fn from(verge: IVerge) -> Self {
// 先获取验证后的clash_core值避免后续借用冲突
let valid_clash_core = verge.get_valid_clash_core();
Self {
app_log_level: verge.app_log_level,
app_log_max_size: verge.app_log_max_size,
app_log_max_count: verge.app_log_max_count,
language: verge.language,
theme_mode: verge.theme_mode,
tray_event: verge.tray_event,
env_type: verge.env_type,
start_page: verge.start_page,
startup_script: verge.startup_script,
traffic_graph: verge.traffic_graph,
enable_memory_usage: verge.enable_memory_usage,
enable_group_icon: verge.enable_group_icon,
common_tray_icon: verge.common_tray_icon,
#[cfg(target_os = "macos")]
tray_icon: verge.tray_icon,
menu_icon: verge.menu_icon,
menu_order: verge.menu_order,
sysproxy_tray_icon: verge.sysproxy_tray_icon,
tun_tray_icon: verge.tun_tray_icon,
enable_tun_mode: verge.enable_tun_mode,
enable_auto_launch: verge.enable_auto_launch,
enable_silent_start: verge.enable_silent_start,
enable_system_proxy: verge.enable_system_proxy,
enable_proxy_guard: verge.enable_proxy_guard,
enable_global_hotkey: verge.enable_global_hotkey,
use_default_bypass: verge.use_default_bypass,
system_proxy_bypass: verge.system_proxy_bypass,
proxy_guard_duration: verge.proxy_guard_duration,
proxy_auto_config: verge.proxy_auto_config,
pac_file_content: verge.pac_file_content,
proxy_host: verge.proxy_host,
theme_setting: verge.theme_setting,
web_ui_list: verge.web_ui_list,
clash_core: Some(valid_clash_core),
hotkeys: verge.hotkeys,
auto_close_connection: verge.auto_close_connection,
auto_check_update: verge.auto_check_update,
default_latency_test: verge.default_latency_test,
default_latency_timeout: verge.default_latency_timeout,
enable_auto_delay_detection: verge.enable_auto_delay_detection,
enable_builtin_enhanced: verge.enable_builtin_enhanced,
proxy_layout_column: verge.proxy_layout_column,
test_list: verge.test_list,
auto_log_clean: verge.auto_log_clean,
#[cfg(not(target_os = "windows"))]
verge_redir_port: verge.verge_redir_port,
#[cfg(not(target_os = "windows"))]
verge_redir_enabled: verge.verge_redir_enabled,
#[cfg(target_os = "linux")]
verge_tproxy_port: verge.verge_tproxy_port,
#[cfg(target_os = "linux")]
verge_tproxy_enabled: verge.verge_tproxy_enabled,
verge_mixed_port: verge.verge_mixed_port,
verge_socks_port: verge.verge_socks_port,
verge_socks_enabled: verge.verge_socks_enabled,
verge_port: verge.verge_port,
verge_http_enabled: verge.verge_http_enabled,
webdav_url: verge.webdav_url,
webdav_username: verge.webdav_username,
webdav_password: verge.webdav_password,
enable_tray_speed: verge.enable_tray_speed,
// enable_tray_icon: verge.enable_tray_icon,
tray_inline_proxy_groups: verge.tray_inline_proxy_groups,
enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode,
auto_light_weight_minutes: verge.auto_light_weight_minutes,
enable_dns_settings: verge.enable_dns_settings,
home_cards: verge.home_cards,
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
hover_jump_navigator_delay: verge.hover_jump_navigator_delay,
enable_external_controller: verge.enable_external_controller,
}
}
}

View File

@@ -347,11 +347,12 @@ impl AsyncProxyQuery {
&mut buffer_size,
);
let mut proxy_server = String::new();
if server_result == 0 && value_type == REG_SZ && buffer_size > 0 {
let proxy_server = if server_result == 0 && value_type == REG_SZ && buffer_size > 0 {
let end_pos = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len());
proxy_server = String::from_utf16_lossy(&buffer[..end_pos]);
}
String::from_utf16_lossy(&buffer[..end_pos])
} else {
String::new()
};
// 读取代理绕过列表
let proxy_override_name = "ProxyOverride\0".encode_utf16().collect::<Vec<u16>>();
@@ -368,14 +369,16 @@ impl AsyncProxyQuery {
&mut bypass_buffer_size,
);
let mut bypass_list = String::new();
if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 {
let end_pos = bypass_buffer
.iter()
.position(|&x| x == 0)
.unwrap_or(bypass_buffer.len());
bypass_list = String::from_utf16_lossy(&bypass_buffer[..end_pos]);
}
let bypass_list =
if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 {
let end_pos = bypass_buffer
.iter()
.position(|&x| x == 0)
.unwrap_or(bypass_buffer.len());
String::from_utf16_lossy(&bypass_buffer[..end_pos])
} else {
String::new()
};
RegCloseKey(hkey);

View File

@@ -45,12 +45,12 @@ enum Operation {
}
impl Operation {
fn timeout(&self) -> u64 {
const fn timeout(&self) -> u64 {
match self {
Operation::Upload => TIMEOUT_UPLOAD,
Operation::Download => TIMEOUT_DOWNLOAD,
Operation::List => TIMEOUT_LIST,
Operation::Delete => TIMEOUT_DELETE,
Self::Upload => TIMEOUT_UPLOAD,
Self::Download => TIMEOUT_DOWNLOAD,
Self::List => TIMEOUT_LIST,
Self::Delete => TIMEOUT_DELETE,
}
}
}
@@ -61,9 +61,9 @@ pub struct WebDavClient {
}
impl WebDavClient {
pub fn global() -> &'static WebDavClient {
pub fn global() -> &'static Self {
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
WEBDAV_CLIENT.get_or_init(|| WebDavClient {
WEBDAV_CLIENT.get_or_init(|| Self {
config: Arc::new(ArcSwapOption::new(None)),
clients: Arc::new(ArcSwap::new(Arc::new(HashMap::new()))),
})
@@ -87,7 +87,7 @@ impl WebDavClient {
(*cfg_arc).clone()
} else {
// 释放锁后获取异步配置
let verge = Config::verge().await.latest_arc();
let verge = Config::verge().await.data_arc();
if verge.webdav_url.is_none()
|| verge.webdav_username.is_none()
|| verge.webdav_password.is_none()

View File

@@ -75,7 +75,7 @@ struct ProxyConfig {
static PROXY_MANAGER: Lazy<EventDrivenProxyManager> = Lazy::new(EventDrivenProxyManager::new);
impl EventDrivenProxyManager {
pub fn global() -> &'static EventDrivenProxyManager {
pub fn global() -> &'static Self {
&PROXY_MANAGER
}

View File

@@ -102,14 +102,14 @@ impl Handle {
Self::send_event(FrontendEvent::ProfileUpdateCompleted { uid });
}
// TODO 利用 &str 等缩短 Clone
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
let handle = Self::global();
let status_str = status.into();
let msg_str = msg.into();
if !*handle.startup_completed.read() {
let mut errors = handle.startup_errors.write();
errors.push(ErrorMessage {
handle.startup_errors.write().push(ErrorMessage {
status: status_str,
message: msg_str,
});
@@ -158,7 +158,7 @@ impl Handle {
.spawn(move || {
thread::sleep(timing::STARTUP_ERROR_DELAY);
let handle = Handle::global();
let handle = Self::global();
if handle.is_exiting() {
return;
}

View File

@@ -28,16 +28,16 @@ pub enum HotkeyFunction {
impl fmt::Display for HotkeyFunction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
HotkeyFunction::OpenOrCloseDashboard => "open_or_close_dashboard",
HotkeyFunction::ClashModeRule => "clash_mode_rule",
HotkeyFunction::ClashModeGlobal => "clash_mode_global",
HotkeyFunction::ClashModeDirect => "clash_mode_direct",
HotkeyFunction::ToggleSystemProxy => "toggle_system_proxy",
HotkeyFunction::ToggleTunMode => "toggle_tun_mode",
HotkeyFunction::EntryLightweightMode => "entry_lightweight_mode",
HotkeyFunction::Quit => "quit",
Self::OpenOrCloseDashboard => "open_or_close_dashboard",
Self::ClashModeRule => "clash_mode_rule",
Self::ClashModeGlobal => "clash_mode_global",
Self::ClashModeDirect => "clash_mode_direct",
Self::ToggleSystemProxy => "toggle_system_proxy",
Self::ToggleTunMode => "toggle_tun_mode",
Self::EntryLightweightMode => "entry_lightweight_mode",
Self::Quit => "quit",
#[cfg(target_os = "macos")]
HotkeyFunction::Hide => "hide",
Self::Hide => "hide",
};
write!(f, "{s}")
}
@@ -48,16 +48,16 @@ impl FromStr for HotkeyFunction {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim() {
"open_or_close_dashboard" => Ok(HotkeyFunction::OpenOrCloseDashboard),
"clash_mode_rule" => Ok(HotkeyFunction::ClashModeRule),
"clash_mode_global" => Ok(HotkeyFunction::ClashModeGlobal),
"clash_mode_direct" => Ok(HotkeyFunction::ClashModeDirect),
"toggle_system_proxy" => Ok(HotkeyFunction::ToggleSystemProxy),
"toggle_tun_mode" => Ok(HotkeyFunction::ToggleTunMode),
"entry_lightweight_mode" => Ok(HotkeyFunction::EntryLightweightMode),
"quit" => Ok(HotkeyFunction::Quit),
"open_or_close_dashboard" => Ok(Self::OpenOrCloseDashboard),
"clash_mode_rule" => Ok(Self::ClashModeRule),
"clash_mode_global" => Ok(Self::ClashModeGlobal),
"clash_mode_direct" => Ok(Self::ClashModeDirect),
"toggle_system_proxy" => Ok(Self::ToggleSystemProxy),
"toggle_tun_mode" => Ok(Self::ToggleTunMode),
"entry_lightweight_mode" => Ok(Self::EntryLightweightMode),
"quit" => Ok(Self::Quit),
#[cfg(target_os = "macos")]
"hide" => Ok(HotkeyFunction::Hide),
"hide" => Ok(Self::Hide),
_ => bail!("invalid hotkey function: {}", s),
}
}
@@ -75,8 +75,8 @@ pub enum SystemHotkey {
impl fmt::Display for SystemHotkey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
SystemHotkey::CmdQ => "CMD+Q",
SystemHotkey::CmdW => "CMD+W",
Self::CmdQ => "CMD+Q",
Self::CmdW => "CMD+W",
};
write!(f, "{s}")
}
@@ -84,10 +84,10 @@ impl fmt::Display for SystemHotkey {
#[cfg(target_os = "macos")]
impl SystemHotkey {
pub fn function(self) -> HotkeyFunction {
pub const fn function(self) -> HotkeyFunction {
match self {
SystemHotkey::CmdQ => HotkeyFunction::Quit,
SystemHotkey::CmdW => HotkeyFunction::Hide,
Self::CmdQ => HotkeyFunction::Quit,
Self::CmdW => HotkeyFunction::Hide,
}
}
}
@@ -211,28 +211,18 @@ impl Hotkey {
let is_quit = matches!(function, HotkeyFunction::Quit);
manager.on_shortcut(hotkey, move |_app_handle, hotkey_event, event| {
let hotkey_event_owned = *hotkey_event;
let event_owned = event;
let function_owned = function;
let is_quit_owned = is_quit;
AsyncHandler::spawn(move || async move {
if event_owned.state == ShortcutState::Pressed {
logging!(
debug,
Type::Hotkey,
"Hotkey pressed: {:?}",
hotkey_event_owned
);
if hotkey_event_owned.key == Code::KeyQ && is_quit_owned {
if let Some(window) = handle::Handle::get_window()
&& window.is_focused().unwrap_or(false)
{
logging!(debug, Type::Hotkey, "Executing quit function");
Self::execute_function(function_owned);
}
} else {
if event.state == ShortcutState::Pressed {
logging!(debug, Type::Hotkey, "Hotkey pressed: {:?}", hotkey_event);
let hotkey = hotkey_event.key;
if hotkey == Code::KeyQ && is_quit {
if let Some(window) = handle::Handle::get_window()
&& window.is_focused().unwrap_or(false)
{
logging!(debug, Type::Hotkey, "Executing quit function");
Self::execute_function(function);
}
} else {
AsyncHandler::spawn(move || async move {
logging!(debug, Type::Hotkey, "Executing function directly");
let is_enable_global_hotkey = Config::verge()
@@ -242,19 +232,19 @@ impl Hotkey {
.unwrap_or(true);
if is_enable_global_hotkey {
Self::execute_function(function_owned);
Self::execute_function(function);
} else {
use crate::utils::window_manager::WindowManager;
let is_visible = WindowManager::is_main_window_visible();
let is_focused = WindowManager::is_main_window_focused();
if is_focused && is_visible {
Self::execute_function(function_owned);
Self::execute_function(function);
}
}
}
});
}
});
}
})?;
logging!(

View File

@@ -137,7 +137,7 @@ impl CoreManager {
}
}
fn is_connection_io_error(kind: std::io::ErrorKind) -> bool {
const fn is_connection_io_error(kind: std::io::ErrorKind) -> bool {
matches!(
kind,
std::io::ErrorKind::ConnectionAborted

View File

@@ -68,7 +68,8 @@ impl CoreManager {
#[cfg(target_os = "windows")]
self.wait_for_service_if_needed().await;
let mode = match SERVICE_MANAGER.lock().await.current() {
let value = SERVICE_MANAGER.lock().await.current();
let mode = match value {
ServiceStatus::Ready => RunningMode::Service,
_ => RunningMode::Sidecar,
};

View File

@@ -62,8 +62,12 @@ impl CoreManager {
| tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
let mut now = DeferredNow::default();
let message = CompactString::from(String::from_utf8_lossy(&line).as_ref());
let w = shared_writer.lock().await;
write_sidecar_log(w, &mut now, Level::Error, &message);
write_sidecar_log(
shared_writer.lock().await,
&mut now,
Level::Error,
&message,
);
CLASH_LOGGER.append_log(message).await;
}
tauri_plugin_shell::process::CommandEvent::Terminated(term) => {
@@ -75,8 +79,12 @@ impl CoreManager {
} else {
CompactString::from("Process terminated")
};
let w = shared_writer.lock().await;
write_sidecar_log(w, &mut now, Level::Info, &message);
write_sidecar_log(
shared_writer.lock().await,
&mut now,
Level::Info,
&message,
);
CLASH_LOGGER.clear_logs().await;
break;
}

View File

@@ -1,3 +1,4 @@
use super::handle::Handle;
use crate::{
constants::{retry, timing},
logging,
@@ -11,7 +12,7 @@ use std::{
mpsc,
},
thread,
time::Instant,
time::{Duration, Instant},
};
use tauri::{Emitter, WebviewWindow};
@@ -91,30 +92,26 @@ impl NotificationSystem {
}
fn worker_loop(rx: mpsc::Receiver<FrontendEvent>) {
use super::handle::Handle;
let handle = Handle::global();
while !handle.is_exiting() {
match rx.recv() {
loop {
let handle = Handle::global();
if handle.is_exiting() {
break;
}
match rx.recv_timeout(Duration::from_millis(1_000)) {
Ok(event) => Self::process_event(handle, event),
Err(e) => {
logging!(
error,
Type::System,
"receive event error, stop notification worker: {}",
e
);
break;
}
Err(mpsc::RecvTimeoutError::Timeout) => (),
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
}
}
// Clippy 似乎对 parking lot 的 RwLock 有误报,这里禁用相关警告
#[allow(clippy::significant_drop_tightening)]
fn process_event(handle: &super::handle::Handle, event: FrontendEvent) {
let system_guard = handle.notification_system.read();
let Some(system) = system_guard.as_ref() else {
return;
let binding = handle.notification_system.read();
let system = match binding.as_ref() {
Some(s) => s,
None => return,
};
if system.should_skip_event(&event) {

View File

@@ -136,7 +136,7 @@ async fn uninstall_service() -> Result<()> {
let elevator = crate::utils::help::linux_elevator();
let status = match get_effective_uid() {
0 => StdCommand::new(uninstall_shell).status()?,
_ => StdCommand::new(elevator.clone())
_ => StdCommand::new(elevator)
.arg("sh")
.arg("-c")
.arg(uninstall_shell)
@@ -177,7 +177,7 @@ async fn install_service() -> Result<()> {
let elevator = crate::utils::help::linux_elevator();
let status = match get_effective_uid() {
0 => StdCommand::new(install_shell).status()?,
_ => StdCommand::new(elevator.clone())
_ => StdCommand::new(elevator)
.arg("sh")
.arg("-c")
.arg(install_shell)
@@ -449,7 +449,7 @@ impl ServiceManager {
Self(ServiceStatus::Unavailable("Need Checks".into()))
}
pub fn config() -> Option<clash_verge_service_ipc::IpcConfig> {
pub const fn config() -> Option<clash_verge_service_ipc::IpcConfig> {
Some(clash_verge_service_ipc::IpcConfig {
default_timeout: Duration::from_millis(30),
retry_delay: Duration::from_millis(250),
@@ -532,7 +532,7 @@ impl ServiceManager {
return Err(anyhow::anyhow!("服务不可用: {}", reason));
}
}
let _ = tray::Tray::global().update_tray_display().await;
let _ = tray::Tray::global().update_menu().await;
Ok(())
}
}

View File

@@ -84,7 +84,7 @@ async fn execute_sysproxy_command(args: Vec<std::string::String>) -> Result<()>
impl Default for Sysopt {
fn default() -> Self {
Sysopt {
Self {
initialed: AtomicBool::new(false),
update_sysproxy: AtomicBool::new(false),
reset_sysproxy: AtomicBool::new(false),

View File

@@ -46,7 +46,7 @@ singleton!(Timer, TIMER_INSTANCE);
impl Timer {
fn new() -> Self {
Timer {
Self {
delay_timer: Arc::new(RwLock::new(DelayTimerBuilder::default().build())),
timer_map: Arc::new(RwLock::new(HashMap::new())),
timer_count: AtomicU64::new(1),
@@ -154,19 +154,17 @@ impl Timer {
/// 每 3 秒更新系统托盘菜单,总共执行 3 次
pub fn add_update_tray_menu_task(&self) -> Result<()> {
let tid = self.timer_count.fetch_add(1, Ordering::SeqCst);
let delay_timer = self.delay_timer.write();
let task = TaskBuilder::default()
.set_task_id(tid)
.set_maximum_parallel_runnable_num(1)
.set_frequency_count_down_by_seconds(3, 3)
.spawn_async_routine(|| async move {
logging!(debug, Type::Timer, "Updating tray menu");
crate::core::tray::Tray::global()
.update_tray_display()
.await
crate::core::tray::Tray::global().update_menu().await
})
.context("failed to create update tray menu timer task")?;
delay_timer
self.delay_timer
.write()
.add_task(task)
.context("failed to add update tray menu timer task")?;
Ok(())
@@ -195,14 +193,12 @@ impl Timer {
// Perform sync operations while holding locks
{
let mut timer_map = self.timer_map.write();
let delay_timer = self.delay_timer.write();
for (uid, diff) in diff_map {
match diff {
DiffFlag::Del(tid) => {
timer_map.remove(&uid);
if let Err(e) = delay_timer.remove_task(tid) {
self.timer_map.write().remove(&uid);
let value = self.delay_timer.write().remove_task(tid);
if let Err(e) = value {
logging!(
warn,
Type::Timer,
@@ -222,12 +218,13 @@ impl Timer {
last_run: chrono::Local::now().timestamp(),
};
timer_map.insert(uid.clone(), task);
self.timer_map.write().insert(uid.clone(), task);
operations_to_add.push((uid, tid, interval));
}
DiffFlag::Mod(tid, interval) => {
// Remove old task first
if let Err(e) = delay_timer.remove_task(tid) {
let value = self.delay_timer.write().remove_task(tid);
if let Err(e) = value {
logging!(
warn,
Type::Timer,
@@ -245,7 +242,7 @@ impl Timer {
last_run: chrono::Local::now().timestamp(),
};
timer_map.insert(uid.clone(), task);
self.timer_map.write().insert(uid.clone(), task);
operations_to_add.push((uid, tid, interval));
}
}
@@ -255,8 +252,8 @@ impl Timer {
// Now perform async operations without holding locks
for (uid, tid, interval) in operations_to_add {
// Re-acquire locks for individual operations
let mut delay_timer = self.delay_timer.write();
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
let delay_timer = self.delay_timer.write();
if let Err(e) = self.add_task(&delay_timer, uid.clone(), tid, interval) {
logging_error!(Type::Timer, "Failed to add task for uid {}: {}", uid, e);
// Rollback on failure - remove from timer_map
@@ -373,7 +370,7 @@ impl Timer {
/// Add a timer task with better error handling
fn add_task(
&self,
delay_timer: &mut DelayTimer,
delay_timer: &DelayTimer,
uid: String,
tid: TaskID,
minutes: u64,

View File

@@ -1,5 +1,4 @@
use once_cell::sync::OnceCell;
use tauri::Emitter;
use tauri::tray::TrayIconBuilder;
use tauri_plugin_mihomo::models::Proxies;
use tokio::fs;
@@ -55,18 +54,17 @@ fn get_tray_click_debounce() -> &'static Mutex<Instant> {
fn should_handle_tray_click() -> bool {
let debounce_lock = get_tray_click_debounce();
let mut last_click = debounce_lock.lock();
let now = Instant::now();
if now.duration_since(*last_click) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) {
*last_click = now;
if now.duration_since(*debounce_lock.lock()) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) {
*debounce_lock.lock() = now;
true
} else {
logging!(
debug,
Type::Tray,
"托盘点击被防抖机制忽略,距离上次点击 {}ms",
now.duration_since(*last_click).as_millis()
now.duration_since(*debounce_lock.lock()).as_millis()
);
false
}
@@ -198,7 +196,7 @@ impl TrayState {
impl Default for Tray {
fn default() -> Self {
Tray {
Self {
last_menu_update: Mutex::new(None),
menu_updating: AtomicBool::new(false),
}
@@ -318,11 +316,9 @@ impl Tray {
.unwrap_or("rule")
.to_owned()
};
let profile_uid_and_name = Config::profiles()
.await
.latest_arc()
.all_profile_uid_and_name()
.unwrap_or_default();
let profiles_config = Config::profiles().await;
let profiles_arc = profiles_config.latest_arc();
let profile_uid_and_name = profiles_arc.all_profile_uid_and_name().unwrap_or_default();
let is_lightweight_mode = is_in_lightweight_mode();
match app_handle.tray_by_id("main") {
@@ -433,24 +429,6 @@ impl Tray {
Ok(())
}
/// 更新托盘显示状态的函数
pub async fn update_tray_display(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Tray, "应用正在退出,跳过托盘显示状态更新");
return Ok(());
}
let app_handle = handle::Handle::app_handle();
let _tray = app_handle
.tray_by_id("main")
.ok_or_else(|| anyhow::anyhow!("Failed to get main tray"))?;
// 更新菜单
self.update_menu().await?;
Ok(())
}
/// 更新托盘提示
pub async fn update_tooltip(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
@@ -476,7 +454,7 @@ impl Tray {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
if let Some(current_profile_uid) = profiles.get_current()
&& let Ok(profile) = profiles.get_item(&current_profile_uid)
&& let Ok(profile) = profiles.get_item(current_profile_uid)
{
current_profile_name = match &profile.name {
Some(profile_name) => profile_name.to_string(),
@@ -525,9 +503,7 @@ impl Tray {
logging!(debug, Type::Tray, "应用正在退出,跳过托盘局部更新");
return Ok(());
}
// self.update_menu().await?;
// 更新轻量模式显示状态
self.update_tray_display().await?;
self.update_menu().await?;
self.update_icon().await?;
self.update_tooltip().await?;
Ok(())
@@ -572,72 +548,38 @@ impl Tray {
let tray = builder.build(app_handle)?;
tray.on_tray_icon_event(|_app_handle, event| {
// 忽略移动、进入和离开等无需处理的事件,避免不必要的刷新
match event {
TrayIconEvent::Move { .. }
| TrayIconEvent::Enter { .. }
| TrayIconEvent::Leave { .. } => {
return;
}
_ => {}
}
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Down,
..
} = event
{
AsyncHandler::spawn(|| async move {
let tray_event = { Config::verge().await.latest_arc().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into());
logging!(debug, Type::Tray, "tray event: {tray_event:?}");
AsyncHandler::spawn(|| async move {
let tray_event = { Config::verge().await.latest_arc().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into());
logging!(debug, Type::Tray, "tray event: {tray_event:?}");
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Down,
..
} = event
{
// 添加防抖检查,防止快速连击
if !should_handle_tray_click() {
return;
}
use std::future::Future;
use std::pin::Pin;
let fut: Pin<Box<dyn Future<Output = ()> + Send>> = match tray_event.as_str() {
"system_proxy" => Box::pin(async move {
feat::toggle_system_proxy().await;
}),
"tun_mode" => Box::pin(async move {
feat::toggle_tun_mode(None).await;
}),
"main_window" => Box::pin(async move {
match tray_event.as_str() {
"system_proxy" => feat::toggle_system_proxy().await,
"tun_mode" => feat::toggle_tun_mode(None).await,
"main_window" => {
if !lightweight::exit_lightweight_mode().await {
WindowManager::show_main_window().await;
};
}),
_ => Box::pin(async move {}),
}
_ => {}
};
fut.await;
}
});
});
}
});
tray.on_menu_event(on_menu_event);
Ok(())
}
// 托盘统一的状态更新函数
pub async fn update_all_states(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Tray, "应用正在退出,跳过托盘状态更新");
return Ok(());
}
// 确保所有状态更新完成
self.update_tray_display().await?;
// self.update_menu().await?;
self.update_icon().await?;
self.update_tooltip().await?;
Ok(())
}
}
fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<String, String> {
@@ -666,7 +608,7 @@ fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<String, String> {
async fn create_profile_menu_item(
app_handle: &AppHandle,
profile_uid_and_name: Vec<(String, String)>,
profile_uid_and_name: Vec<(&String, &String)>,
) -> Result<Vec<CheckMenuItem<Wry>>> {
let futures = profile_uid_and_name
.iter()
@@ -870,7 +812,7 @@ async fn create_tray_menu(
system_proxy_enabled: bool,
tun_mode_enabled: bool,
tun_mode_available: bool,
profile_uid_and_name: Vec<(String, String)>,
profile_uid_and_name: Vec<(&String, &String)>,
is_lightweight_mode: bool,
) -> Result<tauri::menu::Menu<Wry>> {
let current_proxy_mode = mode.unwrap_or("");
@@ -881,7 +823,7 @@ async fn create_tray_menu(
let profiles_ref = profiles_config.latest_arc();
profiles_ref
.get_current()
.and_then(|uid| profiles_ref.get_item(&uid).ok())
.and_then(|uid| profiles_ref.get_item(uid).ok())
.and_then(|profile| profile.selected.clone())
.unwrap_or_default()
};
@@ -1255,66 +1197,27 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
}
id if id.starts_with("proxy_") => {
// proxy_{group_name}_{proxy_name}
let parts: Vec<&str> = id.splitn(3, '_').collect();
if parts.len() == 3 && parts[0] == "proxy" {
let group_name = parts[1];
let proxy_name = parts[2];
match handle::Handle::mihomo()
.await
.select_node_for_group(group_name, proxy_name)
.await
{
Ok(_) => {
logging!(
info,
Type::Tray,
"切换代理成功: {} -> {}",
group_name,
proxy_name
);
let _ = handle::Handle::app_handle()
.emit("verge://refresh-proxy-config", ());
}
Err(e) => {
logging!(
error,
Type::Tray,
"切换代理失败: {} -> {}, 错误: {:?}",
group_name,
proxy_name,
e
);
// Fallback to IPC update
if (handle::Handle::mihomo()
.await
.select_node_for_group(group_name, proxy_name)
.await)
.is_ok()
{
logging!(
info,
Type::Tray,
"代理切换回退成功: {} -> {}",
group_name,
proxy_name
);
let app_handle = handle::Handle::app_handle();
let _ = app_handle.emit("verge://force-refresh-proxies", ());
}
}
}
}
let rest = match id.strip_prefix("proxy_") {
Some(r) => r,
None => return,
};
let (group_name, proxy_name) = match rest.split_once('_') {
Some((g, p)) => (g, p),
None => return,
};
feat::switch_proxy_node(group_name, proxy_name).await;
}
_ => {
logging!(
debug,
Type::Tray,
"Unhandled tray menu event: {:?}",
event.id
);
}
_ => {}
}
// Ensure tray state update is awaited and properly handled
if let Err(e) = Tray::global().update_all_states().await {
logging!(warn, Type::Tray, "Failed to update tray state: {e}");
}
// We dont expected to refresh tray state here
// as the inner handle function (SHOULD) already takes care of it
});
}

View File

@@ -16,7 +16,7 @@ pub struct CoreConfigValidator {
}
impl CoreConfigValidator {
pub fn new() -> Self {
pub const fn new() -> Self {
Self {
is_processing: AtomicBool::new(false),
}
@@ -275,41 +275,43 @@ impl CoreConfigValidator {
logging!(info, Type::Validate, "验证目录: {}", app_dir_str);
// 使用子进程运行clash验证配置
let output = app_handle
.shell()
.sidecar(clash_core.as_str())?
.args(["-t", "-d", app_dir_str, "-f", config_path])
.output()
.await?;
let command = app_handle.shell().sidecar(clash_core.as_str())?.args([
"-t",
"-d",
app_dir_str,
"-f",
config_path,
]);
let output = command.output().await?;
let stderr = std::string::String::from_utf8_lossy(&output.stderr);
let stdout = std::string::String::from_utf8_lossy(&output.stdout);
let status = &output.status;
let stderr = &output.stderr;
let stdout = &output.stdout;
// 检查进程退出状态和错误输出
let error_keywords = ["FATA", "fatal", "Parse config error", "level=fatal"];
let has_error =
!output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
let has_error = !status.success() || contains_any_keyword(stderr, &error_keywords);
logging!(info, Type::Validate, "-------- 验证结果 --------");
if !stderr.is_empty() {
logging!(info, Type::Validate, "stderr输出:\n{}", stderr);
logging!(info, Type::Validate, "stderr输出:\n{:?}", stderr);
}
if has_error {
logging!(info, Type::Validate, "发现错误,开始处理错误信息");
let error_msg = if !stdout.is_empty() {
stdout.into()
let error_msg: String = if !stdout.is_empty() {
str::from_utf8(stdout).unwrap_or_default().into()
} else if !stderr.is_empty() {
stderr.into()
} else if let Some(code) = output.status.code() {
format!("验证进程异常退出,退出码: {code}")
str::from_utf8(stderr).unwrap_or_default().into()
} else if let Some(code) = status.code() {
format!("验证进程异常退出,退出码: {code}").into()
} else {
"验证进程被终止".into()
};
logging!(info, Type::Validate, "-------- 验证结束 --------");
Ok((false, error_msg.into())) // 返回错误消息给调用者处理
Ok((false, error_msg)) // 返回错误消息给调用者处理
} else {
logging!(info, Type::Validate, "验证成功");
logging!(info, Type::Validate, "-------- 验证结束 --------");
@@ -342,6 +344,23 @@ fn has_ext<P: AsRef<std::path::Path>>(path: P, ext: &str) -> bool {
.unwrap_or(false)
}
fn contains_any_keyword<'a>(buf: &'a [u8], keywords: &'a [&str]) -> bool {
for &kw in keywords {
let needle = kw.as_bytes();
if needle.is_empty() {
continue;
}
let mut i = 0;
while i + needle.len() <= buf.len() {
if &buf[i..i + needle.len()] == needle {
return true;
}
i += 1;
}
}
false
}
singleton_lazy!(
CoreConfigValidator,
CORECONFIGVALIDATOR,

View File

@@ -70,7 +70,7 @@ pub trait AsyncChainItemFrom {
}
impl AsyncChainItemFrom for Option<ChainItem> {
async fn from_async(item: &PrfItem) -> Option<ChainItem> {
async fn from_async(item: &PrfItem) -> Self {
let itype = item.itype.as_ref()?.as_str();
let file = item.file.clone()?;
let uid = item.uid.clone().unwrap_or_else(|| "".into());
@@ -116,22 +116,21 @@ impl AsyncChainItemFrom for Option<ChainItem> {
}
impl ChainItem {
/// 内建支持一些脚本
pub fn builtin() -> Vec<(ChainSupport, ChainItem)> {
pub fn builtin() -> Vec<(ChainSupport, Self)> {
// meta 的一些处理
let meta_guard =
ChainItem::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
// meta 1.13.2 alpn string 转 数组
let hy_alpn =
ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
let hy_alpn = Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
// meta 的一些处理
let meta_guard_alpha =
ChainItem::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
// meta 1.13.2 alpn string 转 数组
let hy_alpn_alpha =
ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
vec![
(ChainSupport::ClashMeta, hy_alpn),
@@ -154,8 +153,7 @@ impl ChainSupport {
match core {
Some(core) => matches!(
(self, core.as_str()),
(ChainSupport::ClashMeta, "verge-mihomo")
| (ChainSupport::ClashMetaAlpha, "verge-mihomo-alpha")
(Self::ClashMeta, "verge-mihomo") | (Self::ClashMetaAlpha, "verge-mihomo-alpha")
),
None => true,
}

View File

@@ -16,7 +16,7 @@ fn deep_merge(a: &mut Value, b: &Value) {
pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping {
let mut config = Value::from(config);
let merge = use_lowercase(merge.clone());
let merge = use_lowercase(merge);
deep_merge(&mut config, &Value::from(merge));

View File

@@ -44,6 +44,43 @@ struct ProfileItems {
profile_name: String,
}
impl Default for ProfileItems {
fn default() -> Self {
Self {
config: Default::default(),
profile_name: Default::default(),
merge_item: ChainItem {
uid: "".into(),
data: ChainType::Merge(Mapping::new()),
},
script_item: ChainItem {
uid: "".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
},
rules_item: ChainItem {
uid: "".into(),
data: ChainType::Rules(SeqMap::default()),
},
proxies_item: ChainItem {
uid: "".into(),
data: ChainType::Proxies(SeqMap::default()),
},
groups_item: ChainItem {
uid: "".into(),
data: ChainType::Groups(SeqMap::default()),
},
global_merge: ChainItem {
uid: "Merge".into(),
data: ChainType::Merge(Mapping::new()),
},
global_script: ChainItem {
uid: "Script".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
},
}
}
}
async fn get_config_values() -> ConfigValues {
let clash_config = { Config::clash().await.latest_arc().0.clone() };
@@ -89,18 +126,10 @@ async fn get_config_values() -> ConfigValues {
}
}
#[allow(clippy::cognitive_complexity)]
async fn collect_profile_items() -> ProfileItems {
// 从profiles里拿东西 - 先收集需要的数据,然后释放锁
let (
current,
merge_uid,
script_uid,
rules_uid,
proxies_uid,
groups_uid,
_current_profile_uid,
name,
) = {
let (current, merge_uid, script_uid, rules_uid, proxies_uid, groups_uid, name) = {
let current = {
let profiles = Config::profiles().await;
let profiles_clone = profiles.latest_arc();
@@ -109,13 +138,31 @@ async fn collect_profile_items() -> ProfileItems {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_arc();
let current_profile_uid = match profiles_ref.get_current() {
Some(uid) => uid.clone(),
None => return ProfileItems::default(),
};
let merge_uid = profiles_ref.current_merge().unwrap_or_default();
let script_uid = profiles_ref.current_script().unwrap_or_default();
let rules_uid = profiles_ref.current_rules().unwrap_or_default();
let proxies_uid = profiles_ref.current_proxies().unwrap_or_default();
let groups_uid = profiles_ref.current_groups().unwrap_or_default();
let current_profile_uid = profiles_ref.get_current().unwrap_or_default();
let current_item = match profiles_ref.get_item_arc(&current_profile_uid) {
Some(item) => item,
None => return ProfileItems::default(),
};
let merge_uid = current_item
.current_merge()
.unwrap_or_else(|| "Merge".into());
let script_uid = current_item
.current_script()
.unwrap_or_else(|| "Script".into());
let rules_uid = current_item
.current_rules()
.unwrap_or_else(|| "Rules".into());
let proxies_uid = current_item
.current_proxies()
.unwrap_or_else(|| "Proxies".into());
let groups_uid = current_item
.current_groups()
.unwrap_or_else(|| "Groups".into());
let name = profiles_ref
.get_item(&current_profile_uid)
@@ -130,7 +177,6 @@ async fn collect_profile_items() -> ProfileItems {
rules_uid,
proxies_uid,
groups_uid,
current_profile_uid,
name,
)
};
@@ -140,7 +186,7 @@ async fn collect_profile_items() -> ProfileItems {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
profiles.get_item(merge_uid).ok().cloned()
profiles.get_item(&merge_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
@@ -157,7 +203,7 @@ async fn collect_profile_items() -> ProfileItems {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
profiles.get_item(script_uid).ok().cloned()
profiles.get_item(&script_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
@@ -174,7 +220,7 @@ async fn collect_profile_items() -> ProfileItems {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
profiles.get_item(rules_uid).ok().cloned()
profiles.get_item(&rules_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
@@ -191,7 +237,7 @@ async fn collect_profile_items() -> ProfileItems {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
profiles.get_item(proxies_uid).ok().cloned()
profiles.get_item(&proxies_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
@@ -208,7 +254,7 @@ async fn collect_profile_items() -> ProfileItems {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
profiles.get_item(groups_uid).ok().cloned()
profiles.get_item(&groups_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
@@ -284,7 +330,7 @@ fn process_global_items(
if let ChainType::Script(script) = global_script.data {
let mut logs = vec![];
match use_script(script, config.to_owned(), profile_name.to_owned()) {
match use_script(script, config.to_owned(), profile_name) {
Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config));
config = res_config;

View File

@@ -62,7 +62,7 @@ pub fn use_script(
});"#,
));
let config = use_lowercase(config.clone());
let config = use_lowercase(config);
let config_str = serde_json::to_string(&config)?;
// 仅处理 name 参数中的特殊字符

View File

@@ -47,6 +47,7 @@ fn after_change_clash_mode() {
for connection in connections_array {
let _ = mihomo.close_connection(&connection.id).await;
}
drop(mihomo);
}
}
Err(err) => {

View File

@@ -3,7 +3,7 @@ use crate::{
core::{CoreManager, handle, hotkey, sysopt, tray},
logging_error,
module::lightweight,
utils::logging::Type,
utils::{draft::SharedBox, logging::Type},
};
use anyhow::Result;
use serde_yaml_ng::Mapping;
@@ -250,3 +250,9 @@ pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> {
}
Ok(())
}
pub async fn fetch_verge_config() -> Result<SharedBox<IVerge>> {
let draft = Config::verge().await;
let data = draft.data_arc();
Ok(data)
}

View File

@@ -2,23 +2,75 @@ use crate::{
cmd,
config::{Config, PrfItem, PrfOption, profiles::profiles_draft_update_item_safe},
core::{CoreManager, handle, tray},
logging,
logging, logging_error,
utils::logging::Type,
};
use anyhow::{Result, bail};
use smartstring::alias::String;
use tauri::Emitter;
/// Toggle proxy profile
pub async fn toggle_proxy_profile(profile_index: String) {
match cmd::patch_profiles_config_by_profile_index(profile_index).await {
logging_error!(
Type::Config,
cmd::patch_profiles_config_by_profile_index(profile_index).await
);
}
pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) {
match handle::Handle::mihomo()
.await
.select_node_for_group(group_name, proxy_name)
.await
{
Ok(_) => {
let result = tray::Tray::global().update_menu().await;
if let Err(err) = result {
logging!(error, Type::Tray, "更新菜单失败: {}", err);
}
logging!(
info,
Type::Tray,
"切换代理成功: {} -> {}",
group_name,
proxy_name
);
let _ = handle::Handle::app_handle().emit("verge://refresh-proxy-config", ());
let _ = tray::Tray::global().update_menu().await;
return;
}
Err(err) => {
logging!(error, Type::Tray, "{err}");
logging!(
error,
Type::Tray,
"切换代理失败: {} -> {}, 错误: {:?}",
group_name,
proxy_name,
err
);
}
}
match handle::Handle::mihomo()
.await
.select_node_for_group(group_name, proxy_name)
.await
{
Ok(_) => {
logging!(
info,
Type::Tray,
"代理切换回退成功: {} -> {}",
group_name,
proxy_name
);
let _ = tray::Tray::global().update_menu().await;
}
Err(err) => {
logging!(
error,
Type::Tray,
"代理切换最终失败: {} -> {}, 错误: {:?}",
group_name,
proxy_name,
err
);
}
}
}
@@ -91,13 +143,13 @@ async fn perform_profile_update(
let profiles = Config::profiles().await;
profiles.latest_arc().is_current_profile_index(uid)
};
let profile_name = {
let profiles = Config::profiles().await;
profiles
.latest_arc()
.get_name_by_uid(uid)
.unwrap_or_default()
};
let profiles = Config::profiles().await;
let profiles_arc = profiles.latest_arc();
let profile_name = profiles_arc
.get_name_by_uid(uid)
.cloned()
.unwrap_or_else(|| String::from("UnKown Profile"));
let mut last_err;
match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await {

View File

@@ -351,23 +351,21 @@ pub fn run() {
});
}
#[cfg(target_os = "macos")]
pub fn handle_window_destroyed() {
#[cfg(target_os = "macos")]
{
use crate::core::hotkey::SystemHotkey;
AsyncHandler::spawn(move || async move {
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ);
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW);
let is_enable_global_hotkey = Config::verge()
.await
.latest_arc()
.enable_global_hotkey
.unwrap_or(true);
if !is_enable_global_hotkey {
let _ = hotkey::Hotkey::global().reset();
}
});
}
use crate::core::hotkey::SystemHotkey;
AsyncHandler::spawn(move || async move {
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ);
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW);
let is_enable_global_hotkey = Config::verge()
.await
.latest_arc()
.enable_global_hotkey
.unwrap_or(true);
if !is_enable_global_hotkey {
let _ = hotkey::Hotkey::global().reset();
}
});
}
}
@@ -447,6 +445,7 @@ pub fn run() {
tauri::WindowEvent::Focused(focused) => {
event_handlers::handle_window_focus(focused);
}
#[cfg(target_os = "macos")]
tauri::WindowEvent::Destroyed => {
event_handlers::handle_window_destroyed();
}

View File

@@ -4,8 +4,8 @@ fn main() {
console_subscriber::init();
// Check for --no-tray command line argument
let args: Vec<String> = std::env::args().collect();
if args.contains(&"--no-tray".into()) {
#[cfg(target_os = "linux")]
if std::env::args().any(|x| x == "--no-tray") {
unsafe {
std::env::set_var("CLASH_VERGE_DISABLE_TRAY", "1");
}

View File

@@ -28,15 +28,15 @@ enum LightweightState {
impl From<u8> for LightweightState {
fn from(v: u8) -> Self {
match v {
1 => LightweightState::In,
2 => LightweightState::Exiting,
_ => LightweightState::Normal,
1 => Self::In,
2 => Self::Exiting,
_ => Self::Normal,
}
}
}
impl LightweightState {
fn as_u8(self) -> u8 {
const fn as_u8(self) -> u8 {
self as u8
}
}
@@ -79,40 +79,24 @@ pub fn is_in_lightweight_mode() -> bool {
}
async fn refresh_lightweight_tray_state() {
if let Err(err) = Tray::global().update_tray_display().await {
if let Err(err) = Tray::global().update_menu().await {
logging!(warn, Type::Lightweight, "更新托盘轻量模式状态失败: {err}");
}
}
pub async fn auto_lightweight_boot() -> Result<()> {
let verge_config = Config::verge().await;
let enable_auto = verge_config
.latest_arc()
let is_enable_auto = verge_config
.data_arc()
.enable_auto_light_weight_mode
.unwrap_or(false);
let is_silent_start = verge_config
.latest_arc()
.enable_silent_start
.unwrap_or(false);
let is_silent_start = verge_config.data_arc().enable_silent_start.unwrap_or(false);
if is_enable_auto {
enable_auto_light_weight_mode().await;
}
if is_silent_start {
logging!(info, Type::Lightweight, "静默启动:直接进入轻量模式");
let _ = entry_lightweight_mode().await;
return Ok(());
entry_lightweight_mode().await;
}
if !enable_auto {
logging!(info, Type::Lightweight, "未开启自动轻量模式,跳过初始化");
return Ok(());
}
logging!(
info,
Type::Lightweight,
"非静默启动:注册自动轻量模式监听器"
);
enable_auto_light_weight_mode().await;
Ok(())
}
@@ -135,7 +119,7 @@ pub fn disable_auto_light_weight_mode() {
pub async fn entry_lightweight_mode() -> bool {
if !try_transition(LightweightState::Normal, LightweightState::In) {
logging!(info, Type::Lightweight, "无需进入轻量模式,跳过调用");
logging!(debug, Type::Lightweight, "无需进入轻量模式,跳过调用");
refresh_lightweight_tray_state().await;
return false;
}
@@ -149,7 +133,7 @@ pub async fn entry_lightweight_mode() -> bool {
pub async fn exit_lightweight_mode() -> bool {
if !try_transition(LightweightState::In, LightweightState::Exiting) {
logging!(
info,
debug,
Type::Lightweight,
"轻量模式不在退出条件(可能已退出或正在退出),跳过调用"
);
@@ -171,10 +155,6 @@ pub async fn add_light_weight_timer() {
fn setup_window_close_listener() {
if let Some(window) = handle::Handle::get_window() {
let old_id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
if old_id != 0 {
window.unlisten(old_id);
}
let handler_id = window.listen("tauri://close-requested", move |_event| {
std::mem::drop(AsyncHandler::spawn(|| async {
if let Err(e) = setup_light_weight_timer().await {
@@ -196,21 +176,17 @@ fn cancel_window_close_listener() {
let id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
if id != 0 {
window.unlisten(id);
logging!(info, Type::Lightweight, "取消了窗口关闭监听");
logging!(debug, Type::Lightweight, "取消了窗口关闭监听");
}
}
}
fn setup_webview_focus_listener() {
if let Some(window) = handle::Handle::get_window() {
let old_id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
if old_id != 0 {
window.unlisten(old_id);
}
let handler_id = window.listen("tauri://focus", move |_event| {
log_err!(cancel_light_weight_timer());
logging!(
info,
debug,
Type::Lightweight,
"监听到窗口获得焦点,取消轻量模式计时"
);
@@ -224,7 +200,7 @@ fn cancel_webview_focus_listener() {
let id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
if id != 0 {
window.unlisten(id);
logging!(info, Type::Lightweight, "取消了窗口焦点监听");
logging!(debug, Type::Lightweight, "取消了窗口焦点监听");
}
}
}
@@ -236,14 +212,14 @@ async fn setup_light_weight_timer() -> Result<()> {
let once_by_minutes = Config::verge()
.await
.latest_arc()
.data_arc()
.auto_light_weight_minutes
.unwrap_or(10);
{
let timer_map = Timer::global().timer_map.read();
if timer_map.contains_key(LIGHT_WEIGHT_TASK_UID) {
logging!(warn, Type::Timer, "轻量模式计时器已存在,跳过创建");
logging!(debug, Type::Timer, "轻量模式计时器已存在,跳过创建");
return Ok(());
}
}
@@ -292,14 +268,17 @@ async fn setup_light_weight_timer() -> Result<()> {
}
fn cancel_light_weight_timer() -> Result<()> {
let mut timer_map = Timer::global().timer_map.write();
let delay_timer = Timer::global().delay_timer.write();
if let Some(task) = timer_map.remove(LIGHT_WEIGHT_TASK_UID) {
delay_timer
let value = Timer::global()
.timer_map
.write()
.remove(LIGHT_WEIGHT_TASK_UID);
if let Some(task) = value {
Timer::global()
.delay_timer
.write()
.remove_task(task.task_id)
.context("failed to remove timer task")?;
logging!(info, Type::Timer, "计时器已取消");
logging!(debug, Type::Timer, "计时器已取消");
}
Ok(())

View File

@@ -20,7 +20,7 @@ impl Drop for CommandChildGuard {
}
impl CommandChildGuard {
pub fn new(child: CommandChild) -> Self {
pub const fn new(child: CommandChild) -> Self {
Self(Some(child))
}

View File

@@ -6,6 +6,8 @@ use crate::{
use anyhow::Result;
use async_trait::async_trait;
use once_cell::sync::OnceCell;
#[cfg(unix)]
use std::iter;
use std::{fs, path::PathBuf};
use tauri::Manager;
@@ -226,8 +228,7 @@ pub fn get_encryption_key() -> Result<Vec<u8>> {
#[cfg(unix)]
pub fn ensure_mihomo_safe_dir() -> Option<PathBuf> {
["/tmp"]
.iter()
iter::once("/tmp")
.map(PathBuf::from)
.find(|path| path.exists())
.or_else(|| {

View File

@@ -43,18 +43,20 @@ impl<T: Clone> Draft<T> {
{
// 先获得写锁以创建或取出草稿 Arc 的可变引用位置
let mut guard = self.inner.write();
if guard.1.is_none() {
// 创建草稿 snapshotArc clonecheap
guard.1 = Some(Arc::clone(&guard.0));
}
// 此时 guaranteed: guard.1 is Some(Arc<Box<T>>)
#[allow(clippy::unwrap_used)]
let arc_box = guard.1.as_mut().unwrap();
let mut draft_arc = if guard.1.is_none() {
Arc::clone(&guard.0)
} else {
#[allow(clippy::unwrap_used)]
guard.1.take().unwrap()
};
drop(guard);
// Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box<T>(要求 T: Clone
let boxed = Arc::make_mut(arc_box); // &mut Box<T>
let boxed = Arc::make_mut(&mut draft_arc); // &mut Box<T>
// 对 Box<T> 解引用得到 &mut T
f(&mut **boxed)
let result = f(&mut **boxed);
// 恢复修改后的草稿 Arc
self.inner.write().1 = Some(draft_arc);
result
}
/// 将草稿提交到已提交位置(替换),并清除草稿
@@ -90,8 +92,7 @@ impl<T: Clone> Draft<T> {
let (new_local, res) = f(local).await?;
// 将新的 Box<T> 放到已提交位置(包进 Arc
let mut guard = self.inner.write();
guard.0 = Arc::new(new_local);
self.inner.write().0 = Arc::new(new_local);
Ok(res)
}

View File

@@ -1,4 +1,4 @@
use crate::{enhance::seq::SeqMap, logging, utils::logging::Type};
use crate::{config::with_encryption, enhance::seq::SeqMap, logging, utils::logging::Type};
use anyhow::{Context, Result, anyhow, bail};
use nanoid::nanoid;
use serde::{Serialize, de::DeserializeOwned};
@@ -13,7 +13,7 @@ pub async fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
let yaml_str = tokio::fs::read_to_string(path).await?;
Ok(serde_yaml_ng::from_str::<T>(&yaml_str)?)
Ok(with_encryption(|| async { serde_yaml_ng::from_str::<T>(&yaml_str) }).await?)
}
/// read mapping from yaml
@@ -65,7 +65,7 @@ pub async fn save_yaml<T: Serialize + Sync>(
data: &T,
prefix: Option<&str>,
) -> Result<()> {
let data_str = serde_yaml_ng::to_string(data)?;
let data_str = with_encryption(|| async { serde_yaml_ng::to_string(data) }).await?;
let yaml_str = match prefix {
Some(prefix) => format!("{prefix}\n\n{data_str}"),

View File

@@ -1,4 +1,4 @@
#[cfg(not(feature = "tracing"))]
// #[cfg(not(feature = "tracing"))]
#[cfg(not(feature = "tauri-dev"))]
use crate::utils::logging::NoModuleFilter;
use crate::{
@@ -49,7 +49,9 @@ pub async fn init_logger() -> Result<()> {
#[cfg(feature = "tracing")]
spec.module("tauri", log::LevelFilter::Debug);
#[cfg(feature = "tracing")]
spec.module("wry", log::LevelFilter::Debug);
spec.module("wry", log::LevelFilter::Off);
#[cfg(feature = "tracing")]
spec.module("tauri_plugin_mihomo", log::LevelFilter::Off);
let spec = spec.build();
let logger = Logger::with(spec)
@@ -67,6 +69,12 @@ pub async fn init_logger() -> Result<()> {
);
#[cfg(not(feature = "tracing"))]
let logger = logger.filter(Box::new(NoModuleFilter(&["wry", "tauri"])));
#[cfg(feature = "tracing")]
let logger = logger.filter(Box::new(NoModuleFilter(&[
"wry",
"tauri_plugin_mihomo",
"kode_bridge",
])));
let _handle = logger.start()?;
@@ -498,7 +506,7 @@ pub fn init_scheme() -> Result<()> {
Ok(())
}
#[cfg(target_os = "macos")]
pub fn init_scheme() -> Result<()> {
pub const fn init_scheme() -> Result<()> {
Ok(())
}

View File

@@ -19,7 +19,7 @@ struct IntelGpuDetection {
}
impl IntelGpuDetection {
fn should_disable_dmabuf(&self) -> bool {
const fn should_disable_dmabuf(&self) -> bool {
self.intel_is_primary || self.inconclusive
}
}
@@ -41,7 +41,7 @@ enum NvidiaDmabufDisableReason {
}
impl NvidiaGpuDetection {
fn disable_reason(&self, session: &SessionEnv) -> Option<NvidiaDmabufDisableReason> {
const fn disable_reason(&self, session: &SessionEnv) -> Option<NvidiaDmabufDisableReason> {
if !session.is_wayland {
return None;
}
@@ -144,11 +144,11 @@ impl DmabufOverrides {
}
}
fn has_env_override(&self) -> bool {
const fn has_env_override(&self) -> bool {
self.dmabuf_override.is_some()
}
fn should_override_env(&self, decision: &DmabufDecision) -> bool {
const fn should_override_env(&self, decision: &DmabufDecision) -> bool {
if self.user_preference.is_some() {
return true;
}

View File

@@ -36,24 +36,24 @@ pub enum Type {
impl fmt::Display for Type {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Type::Cmd => write!(f, "[Cmd]"),
Type::Core => write!(f, "[Core]"),
Type::Config => write!(f, "[Config]"),
Type::Setup => write!(f, "[Setup]"),
Type::System => write!(f, "[System]"),
Type::Service => write!(f, "[Service]"),
Type::Hotkey => write!(f, "[Hotkey]"),
Type::Window => write!(f, "[Window]"),
Type::Tray => write!(f, "[Tray]"),
Type::Timer => write!(f, "[Timer]"),
Type::Frontend => write!(f, "[Frontend]"),
Type::Backup => write!(f, "[Backup]"),
Type::File => write!(f, "[File]"),
Type::Lightweight => write!(f, "[Lightweight]"),
Type::Network => write!(f, "[Network]"),
Type::ProxyMode => write!(f, "[ProxMode]"),
Type::Validate => write!(f, "[Validate]"),
Type::ClashVergeRev => write!(f, "[ClashVergeRev]"),
Self::Cmd => write!(f, "[Cmd]"),
Self::Core => write!(f, "[Core]"),
Self::Config => write!(f, "[Config]"),
Self::Setup => write!(f, "[Setup]"),
Self::System => write!(f, "[System]"),
Self::Service => write!(f, "[Service]"),
Self::Hotkey => write!(f, "[Hotkey]"),
Self::Window => write!(f, "[Window]"),
Self::Tray => write!(f, "[Tray]"),
Self::Timer => write!(f, "[Timer]"),
Self::Frontend => write!(f, "[Frontend]"),
Self::Backup => write!(f, "[Backup]"),
Self::File => write!(f, "[File]"),
Self::Lightweight => write!(f, "[Lightweight]"),
Self::Network => write!(f, "[Network]"),
Self::ProxyMode => write!(f, "[ProxMode]"),
Self::Validate => write!(f, "[Validate]"),
Self::ClashVergeRev => write!(f, "[ClashVergeRev]"),
}
}
}

View File

@@ -26,7 +26,7 @@ pub struct HttpResponse {
}
impl HttpResponse {
pub fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self {
pub const fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self {
Self {
status,
headers,
@@ -34,11 +34,11 @@ impl HttpResponse {
}
}
pub fn status(&self) -> StatusCode {
pub const fn status(&self) -> StatusCode {
self.status
}
pub fn headers(&self) -> &HeaderMap {
pub const fn headers(&self) -> &HeaderMap {
&self.headers
}
@@ -80,8 +80,7 @@ impl NetworkManager {
}
async fn record_connection_error(&self, error: &str) {
let mut last_error = self.last_connection_error.lock().await;
*last_error = Some((Instant::now(), error.into()));
*self.last_connection_error.lock().await = Some((Instant::now(), error.into()));
let mut count = self.connection_error_count.lock().await;
*count += 1;
@@ -89,13 +88,11 @@ impl NetworkManager {
async fn should_reset_clients(&self) -> bool {
let count = *self.connection_error_count.lock().await;
let last_error_guard = self.last_connection_error.lock().await;
if count > 5 {
return true;
}
if let Some((time, _)) = &*last_error_guard
if let Some((time, _)) = &*self.last_connection_error.lock().await
&& time.elapsed() < Duration::from_secs(30)
&& count > 2
{
@@ -119,18 +116,15 @@ impl NetworkManager {
accept_invalid_certs: bool,
timeout_secs: Option<u64>,
) -> Result<HttpClient> {
let proxy_uri_clone = proxy_uri.clone();
let headers_clone = default_headers.clone();
{
let mut builder = HttpClient::builder();
builder = match proxy_uri_clone {
builder = match proxy_uri {
Some(uri) => builder.proxy(Some(uri)),
None => builder.proxy(None),
};
for (name, value) in headers_clone.iter() {
for (name, value) in default_headers.iter() {
builder = builder.default_header(name, value);
}

View File

@@ -1,12 +1,14 @@
use std::time::Duration;
use anyhow::{Result, bail};
use percent_encoding::percent_decode_str;
use smartstring::alias::String;
use tauri::Url;
use crate::{
config::{PrfItem, profiles},
config::{Config, PrfItem, profiles},
core::handle,
logging, logging_error,
logging,
utils::logging::Type,
};
@@ -29,55 +31,79 @@ pub(super) async fn resolve_scheme(param: &str) -> Result<()> {
}
};
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
let name_owned: Option<String> = link_parsed
.query_pairs()
.find(|(key, _)| key == "name")
.map(|(_, value)| value.into_owned().into());
let name = name_owned.as_ref();
let (url_param, name) =
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
let name_owned: Option<String> = link_parsed
.query_pairs()
.find(|(key, _)| key == "name")
.map(|(_, value)| value.into_owned().into());
let url_param = if let Some(query) = link_parsed.query() {
let prefix = "url=";
if let Some(pos) = query.find(prefix) {
let raw_url = &query[pos + prefix.len()..];
Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string())
let url_param = if let Some(query) = link_parsed.query() {
let prefix = "url=";
if let Some(pos) = query.find(prefix) {
let raw_url = &query[pos + prefix.len()..];
Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string())
} else {
None
}
} else {
None
}
};
(url_param, name_owned)
} else {
None
(None, None)
};
match url_param {
Some(ref url) => {
logging!(info, Type::Config, "decoded subscription url: {url}");
match PrfItem::from_url(url.as_ref(), name, None, None).await {
Ok(mut item) => {
let uid = match item.uid.clone() {
Some(uid) => uid,
None => {
logging!(error, Type::Config, "Profile item missing UID");
handle::Handle::notice_message(
"import_sub_url::error",
"Profile item missing UID".to_string(),
);
return Ok(());
}
};
let result = profiles::profiles_append_item_safe(&mut item).await;
logging_error!(
Type::Config,
"failed to import subscription url: {:?}",
result
);
handle::Handle::notice_message("import_sub_url::ok", uid);
}
Err(e) => {
handle::Handle::notice_message("import_sub_url::error", e.to_string());
}
}
}
None => bail!("failed to get profile url"),
let url = if let Some(ref url) = url_param {
url
} else {
logging!(
error,
Type::Config,
"missing url parameter in deep link: {}",
param_str
);
return Ok(());
};
let mut item = match PrfItem::from_url(url, name.as_ref(), None, None).await {
Ok(item) => item,
Err(e) => {
logging!(
error,
Type::Config,
"failed to parse profile from url: {:?}",
e
);
handle::Handle::notice_message("import_sub_url::error", e.to_string());
return Ok(());
}
};
let uid = item.uid.clone().unwrap_or_default();
match profiles::profiles_append_item_safe(&mut item).await {
Ok(_) => {
Config::profiles().await.apply();
let _ = Config::profiles().await.data_arc().save_file().await;
handle::Handle::notice_message(
"import_sub_url::ok",
"", // 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
);
handle::Handle::refresh_verge();
handle::Handle::notify_profile_changed(uid.clone());
tokio::time::sleep(Duration::from_millis(100)).await;
handle::Handle::notify_profile_changed(uid);
}
Err(e) => {
logging!(
error,
Type::Config,
"failed to import subscription url: {:?}",
e
);
Config::profiles().await.discard();
handle::Handle::notice_message("import_sub_url::error", e.to_string());
return Ok(());
}
}

View File

@@ -54,9 +54,7 @@ fn get_ui_ready_notify() -> &'static Arc<Notify> {
// 更新UI准备阶段
pub fn update_ui_ready_stage(stage: UiReadyStage) {
let state = get_ui_ready_state();
let mut stage_lock = state.stage.write();
*stage_lock = stage;
*state.stage.write() = stage;
// 如果是最终阶段标记UI完全就绪
if stage == UiReadyStage::Ready {
mark_ui_ready();

View File

@@ -31,6 +31,8 @@ pub async fn check_singleton() -> Result<()> {
let client = ClientBuilder::new()
.timeout(Duration::from_millis(500))
.build()?;
// 需要确保 Send
#[allow(clippy::needless_collect)]
let argvs: Vec<std::string::String> = std::env::args().collect();
if argvs.len() > 1 {
#[cfg(not(target_os = "macos"))]

View File

@@ -81,6 +81,7 @@ fn should_handle_window_operation() -> bool {
if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) {
*last_operation = now;
drop(last_operation);
WINDOW_OPERATION_IN_PROGRESS.store(true, Ordering::Release);
logging!(info, Type::Window, "[防抖] 窗口操作被允许执行");
true

View File

@@ -71,8 +71,9 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
setOpen(false);
showNotice("success", t("Port settings saved"));
},
onError: () => {
showNotice("error", t("Failed to save port settings"));
onError: (e) => {
showNotice("error", e.message || t("Failed to save port settings"));
// showNotice("error", t("Failed to save port settings"));
},
},
);

View File

@@ -70,8 +70,8 @@ export const useClashInfo = () => {
if (patch["redir-port"]) {
const port = patch["redir-port"];
if (port < 1111) {
throw new Error("The port should not < 1111");
if (port < 1000) {
throw new Error("The port should not < 1000");
}
if (port > 65536) {
throw new Error("The port should not > 65536");
@@ -80,8 +80,8 @@ export const useClashInfo = () => {
if (patch["tproxy-port"]) {
const port = patch["tproxy-port"];
if (port < 1111) {
throw new Error("The port should not < 1111");
if (port < 1000) {
throw new Error("The port should not < 1000");
}
if (port > 65536) {
throw new Error("The port should not > 65536");
@@ -90,8 +90,8 @@ export const useClashInfo = () => {
if (patch["mixed-port"]) {
const port = patch["mixed-port"];
if (port < 1111) {
throw new Error("The port should not < 1111");
if (port < 1000) {
throw new Error("The port should not < 1000");
}
if (port > 65536) {
throw new Error("The port should not > 65536");
@@ -100,8 +100,8 @@ export const useClashInfo = () => {
if (patch["socks-port"]) {
const port = patch["socks-port"];
if (port < 1111) {
throw new Error("The port should not < 1111");
if (port < 1000) {
throw new Error("The port should not < 1000");
}
if (port > 65536) {
throw new Error("The port should not > 65536");
@@ -110,8 +110,8 @@ export const useClashInfo = () => {
if (patch["port"]) {
const port = patch["port"];
if (port < 1111) {
throw new Error("The port should not < 1111");
if (port < 1000) {
throw new Error("The port should not < 1000");
}
if (port > 65536) {
throw new Error("The port should not > 65536");

View File

@@ -11,7 +11,10 @@ export const handleNoticeMessage = (
) => {
const handlers: Record<string, () => void> = {
"import_sub_url::ok": () => {
navigate("/profile", { state: { current: msg } });
// 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
// 未来细分事件通知时,可以考虑传入订阅 ID 或其他标识符
// navigate("/profile", { state: { current: msg } });
navigate("/profile");
showNotice("success", t("Import Subscription Successful"));
},
"import_sub_url::error": () => {

View File

@@ -13,15 +13,15 @@ import {
sortableKeyboardCoordinates,
} from "@dnd-kit/sortable";
import {
CheckBoxOutlineBlankRounded,
CheckBoxRounded,
ClearRounded,
ContentPasteRounded,
DeleteRounded,
IndeterminateCheckBoxRounded,
LocalFireDepartmentRounded,
RefreshRounded,
TextSnippetOutlined,
CheckBoxOutlineBlankRounded,
CheckBoxRounded,
IndeterminateCheckBoxRounded,
DeleteRounded,
} from "@mui/icons-material";
import { LoadingButton } from "@mui/lab";
import { Box, Button, Divider, Grid, IconButton, Stack } from "@mui/material";

View File

@@ -801,7 +801,7 @@ interface IVergeConfig {
common_tray_icon?: boolean;
sysproxy_tray_icon?: boolean;
tun_tray_icon?: boolean;
enable_tray_speed?: boolean;
// enable_tray_speed?: boolean;
// enable_tray_icon?: boolean;
tray_inline_proxy_groups?: boolean;
enable_tun_mode?: boolean;