Compare commits

...

39 Commits

68 changed files with 1733 additions and 1678 deletions

View File

@@ -103,7 +103,7 @@ jobs:
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.armhfp.rpm)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)

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,10 +54,10 @@
"@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",
"i18next": "^25.6.1",
"js-yaml": "^4.1.0",
"json-schema": "^0.4.0",
"lodash-es": "^4.17.21",
@@ -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}": [

822
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

@@ -7,13 +7,13 @@ use app_lib::config::IVerge;
use app_lib::utils::Draft as DraftNew;
/// 创建测试数据
fn make_draft() -> DraftNew<Box<IVerge>> {
let verge = Box::new(IVerge {
fn make_draft() -> DraftNew<IVerge> {
let verge = IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
..Default::default()
});
DraftNew::from(verge)
};
DraftNew::new(verge)
}
pub fn bench_draft(c: &mut Criterion) {
@@ -30,18 +30,17 @@ pub fn bench_draft(c: &mut Criterion) {
group.bench_function("data_mut", |b| {
b.iter(|| {
let draft = black_box(make_draft());
let mut data = draft.data_mut();
data.enable_tun_mode = Some(true);
black_box(&data.enable_tun_mode);
draft.edit_draft(|d| d.enable_tun_mode = Some(true));
black_box(&draft.latest_arc().enable_tun_mode);
});
});
group.bench_function("draft_mut_first", |b| {
b.iter(|| {
let draft = black_box(make_draft());
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
black_box(&d.enable_auto_launch);
draft.edit_draft(|d| d.enable_auto_launch = Some(false));
let latest = draft.latest_arc();
black_box(&latest.enable_auto_launch);
});
});
@@ -49,20 +48,24 @@ pub fn bench_draft(c: &mut Criterion) {
b.iter(|| {
let draft = black_box(make_draft());
{
let mut first = draft.draft_mut();
first.enable_tun_mode = Some(true);
black_box(&first.enable_tun_mode);
draft.edit_draft(|d| {
d.enable_tun_mode = Some(true);
});
let latest1 = draft.latest_arc();
black_box(&latest1.enable_tun_mode);
}
let mut second = draft.draft_mut();
second.enable_tun_mode = Some(false);
black_box(&second.enable_tun_mode);
draft.edit_draft(|d| {
d.enable_tun_mode = Some(false);
});
let latest2 = draft.latest_arc();
black_box(&latest2.enable_tun_mode);
});
});
group.bench_function("latest_ref", |b| {
group.bench_function("latest_arc", |b| {
b.iter(|| {
let draft = black_box(make_draft());
let latest = draft.latest_ref();
let latest = draft.latest_arc();
black_box(&latest.enable_auto_launch);
});
});
@@ -71,8 +74,9 @@ pub fn bench_draft(c: &mut Criterion) {
b.iter(|| {
let draft = black_box(make_draft());
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
draft.edit_draft(|d| {
d.enable_auto_launch = Some(false);
});
}
draft.apply();
black_box(&draft);
@@ -83,8 +87,9 @@ pub fn bench_draft(c: &mut Criterion) {
b.iter(|| {
let draft = black_box(make_draft());
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
draft.edit_draft(|d| {
d.enable_auto_launch = Some(false);
});
}
draft.discard();
black_box(&draft);
@@ -95,7 +100,7 @@ pub fn bench_draft(c: &mut Criterion) {
b.to_async(&rt).iter(|| async {
let draft = black_box(make_draft());
let _: Result<(), anyhow::Error> = draft
.with_data_modify::<_, _, _, anyhow::Error>(|mut box_data| async move {
.with_data_modify::<_, _, _>(|mut box_data| async move {
box_data.enable_auto_launch =
Some(!box_data.enable_auto_launch.unwrap_or(false));
Ok((box_data, ()))

View File

@@ -2,11 +2,11 @@ use super::CmdResult;
use crate::utils::dirs;
use crate::{
cmd::StringifyErr,
config::Config,
config::{ClashInfo, Config},
constants,
core::{CoreManager, handle, validate::CoreConfigValidator},
};
use crate::{config::*, feat, logging, utils::logging::Type};
use crate::{feat, logging, utils::logging::Type};
use compact_str::CompactString;
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
@@ -22,7 +22,7 @@ pub async fn copy_clash_env() -> CmdResult {
/// 获取Clash信息
#[tauri::command]
pub async fn get_clash_info() -> CmdResult<ClashInfo> {
Ok(Config::clash().await.latest_ref().get_client_info())
Ok(Config::clash().await.latest_arc().get_client_info())
}
/// 修改Clash配置
@@ -141,12 +141,6 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
/// 应用或撤销DNS配置
#[tauri::command]
pub async fn apply_dns_config(apply: bool) -> CmdResult {
use crate::{
config::Config,
core::{CoreManager, handle},
utils::dirs,
};
if apply {
// 读取DNS配置文件
let dns_path = dirs::app_home_dir()
@@ -175,7 +169,9 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
patch.insert("dns".into(), patch_config.into());
// 应用DNS配置到运行时配置
Config::runtime().await.draft_mut().patch_config(patch);
Config::runtime().await.edit_draft(|d| {
d.patch_config(patch);
});
// 重新生成配置
Config::generate().await.stringify_err_log(|err| {
@@ -193,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!(
@@ -216,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

@@ -1,5 +1,6 @@
use super::CmdResult;
use super::StringifyErr;
use crate::utils::draft::SharedBox;
use crate::{
config::{
Config, IProfiles, PrfItem, PrfOption,
@@ -23,11 +24,11 @@ use std::time::Duration;
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
#[tauri::command]
pub async fn get_profiles() -> CmdResult<IProfiles> {
pub async fn get_profiles() -> CmdResult<SharedBox<IProfiles>> {
logging!(debug, Type::Cmd, "获取配置文件列表");
let draft = Config::profiles().await;
let latest = draft.latest_ref();
Ok((**latest).clone())
let data = draft.data_arc();
Ok(data)
}
/// 增强配置文件
@@ -99,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())
}
@@ -119,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()),
}
}
}
}
@@ -132,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())
}
@@ -143,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();
@@ -172,7 +182,7 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
// 获取目标配置文件路径
let config_file_result = {
let profiles_config = Config::profiles().await;
let profiles_data = profiles_config.latest_ref();
let profiles_data = profiles_config.latest_arc();
match profiles_data.get_item(new_profile) {
Ok(item) => {
if let Some(file) = &item.file {
@@ -234,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(())
}
@@ -243,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(())
}
@@ -252,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(())
}
}
@@ -274,17 +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
.draft_mut()
.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,21 +394,22 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
);
// 保存当前配置,以便在验证失败时恢复
let current_profile = Config::profiles().await.latest_ref().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);
}
Config::profiles()
.await
.edit_draft(|d| d.patch_config(&profiles));
let _ = Config::profiles().await.draft_mut().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
@@ -426,7 +429,7 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm
pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
// 保存修改前检查是否有更新 update_interval
let profiles = Config::profiles().await;
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_ref().get_item(&index)
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_arc().get_item(&index)
&& let Some(new_option) = profile.option.as_ref()
{
let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval);
@@ -465,7 +468,7 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
#[tauri::command]
pub async fn view_profile(index: String) -> CmdResult {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_ref();
let profiles_ref = profiles.latest_arc();
let file = profiles_ref
.get_item(&index)
.stringify_err()?
@@ -488,7 +491,7 @@ pub async fn view_profile(index: String) -> CmdResult {
pub async fn read_profile_file(index: String) -> CmdResult<String> {
let item = {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_ref();
let profiles_ref = profiles.latest_arc();
PrfItem {
file: profiles_ref.get_item(&index).stringify_err()?.file.clone(),
..Default::default()

View File

@@ -8,14 +8,14 @@ use std::collections::HashMap;
/// 获取运行时配置
#[tauri::command]
pub async fn get_runtime_config() -> CmdResult<Option<Mapping>> {
Ok(Config::runtime().await.latest_ref().config.clone())
Ok(Config::runtime().await.latest_arc().config.clone())
}
/// 获取运行时YAML配置
#[tauri::command]
pub async fn get_runtime_yaml() -> CmdResult<String> {
let runtime = Config::runtime().await;
let runtime = runtime.latest_ref();
let runtime = runtime.latest_arc();
let config = runtime.config.as_ref();
config
@@ -31,19 +31,19 @@ pub async fn get_runtime_yaml() -> CmdResult<String> {
/// 获取运行时存在的键
#[tauri::command]
pub async fn get_runtime_exists() -> CmdResult<Vec<String>> {
Ok(Config::runtime().await.latest_ref().exists_keys.clone())
Ok(Config::runtime().await.latest_arc().exists_keys.clone())
}
/// 获取运行时日志
#[tauri::command]
pub async fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
Ok(Config::runtime().await.latest_ref().chain_logs.clone())
Ok(Config::runtime().await.latest_arc().chain_logs.clone())
}
#[tauri::command]
pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> CmdResult<String> {
let runtime = Config::runtime().await;
let runtime = runtime.latest_ref();
let runtime = runtime.latest_arc();
let config = runtime
.config
@@ -98,9 +98,7 @@ pub async fn update_proxy_chain_config_in_runtime(
) -> CmdResult<()> {
{
let runtime = Config::runtime().await;
let mut draft = runtime.draft_mut();
draft.update_proxy_chain_config(proxy_chain_config);
drop(draft);
runtime.edit_draft(|d| d.update_proxy_chain_config(proxy_chain_config));
runtime.apply();
}

View File

@@ -20,7 +20,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
// 在异步操作前获取必要元数据并释放锁
let (rel_path, is_merge_file) = {
let profiles = Config::profiles().await;
let profiles_guard = profiles.latest_ref();
let profiles_guard = profiles.latest_arc();
let item = profiles_guard.get_item(&index).stringify_err()?;
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
let path = item.file.clone().ok_or("file field is null")?;

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::*, feat};
use crate::{cmd::StringifyErr, config::IVerge, feat, utils::draft::SharedBox};
/// 获取Verge配置
#[tauri::command]
pub async fn get_verge_config() -> CmdResult<IVergeResponse> {
let verge = Config::verge().await;
let verge_data = {
let ref_data = verge.latest_ref();
ref_data.clone()
};
let verge_response = IVergeResponse::from(verge_data);
Ok(verge_response)
pub async fn get_verge_config() -> CmdResult<SharedBox<IVerge>> {
feat::fetch_verge_config().await.stringify_err()
}
/// 修改Verge配置

View File

@@ -1,5 +1,9 @@
use super::CmdResult;
use crate::{cmd::StringifyErr, config::*, core, feat};
use crate::{
cmd::StringifyErr,
config::{Config, IVerge},
core, feat,
};
use reqwest_dav::list_cmd::ListFile;
use smartstring::alias::String;
@@ -12,15 +16,11 @@ pub async fn save_webdav_config(url: String, username: String, password: String)
webdav_password: Some(password),
..IVerge::default()
};
Config::verge().await.draft_mut().patch_config(&patch);
Config::verge().await.edit_draft(|e| e.patch_config(&patch));
Config::verge().await.apply();
// 分离数据获取和异步调用
let verge_data = Config::verge().await.latest_ref().clone();
verge_data
.save_file()
.await
.map_err(|err| err.to_string())?;
let verge_data = Config::verge().await.latest_arc();
verge_data.save_file().await.stringify_err()?;
core::backup::WebDavClient::global().reset();
Ok(())
}

View File

@@ -300,7 +300,7 @@ impl IClashTemp {
// 检查 enable_external_controller 设置,用于运行时配置生成
let enable_external_controller = Config::verge()
.await
.latest_ref()
.latest_arc()
.enable_external_controller
.unwrap_or(false);

View File

@@ -15,40 +15,40 @@ use tokio::sync::OnceCell;
use tokio::time::sleep;
pub struct Config {
clash_config: Draft<Box<IClashTemp>>,
verge_config: Draft<Box<IVerge>>,
profiles_config: Draft<Box<IProfiles>>,
runtime_config: Draft<Box<IRuntime>>,
clash_config: Draft<IClashTemp>,
verge_config: Draft<IVerge>,
profiles_config: Draft<IProfiles>,
runtime_config: Draft<IRuntime>,
}
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 {
clash_config: Draft::from(Box::new(IClashTemp::new().await)),
verge_config: Draft::from(Box::new(IVerge::new().await)),
profiles_config: Draft::from(Box::new(IProfiles::new().await)),
runtime_config: Draft::from(Box::new(IRuntime::new())),
Self {
clash_config: Draft::new(IClashTemp::new().await),
verge_config: Draft::new(IVerge::new().await),
profiles_config: Draft::new(IProfiles::new().await),
runtime_config: Draft::new(IRuntime::new()),
}
})
.await
}
pub async fn clash() -> Draft<Box<IClashTemp>> {
pub async fn clash() -> Draft<IClashTemp> {
Self::global().await.clash_config.clone()
}
pub async fn verge() -> Draft<Box<IVerge>> {
pub async fn verge() -> Draft<IVerge> {
Self::global().await.verge_config.clone()
}
pub async fn profiles() -> Draft<Box<IProfiles>> {
pub async fn profiles() -> Draft<IProfiles> {
Self::global().await.profiles_config.clone()
}
pub async fn runtime() -> Draft<Box<IRuntime>> {
pub async fn runtime() -> Draft<IRuntime> {
Self::global().await.runtime_config.clone()
}
@@ -60,13 +60,15 @@ impl Config {
if !cmd::system::is_admin().unwrap_or_default()
&& service::is_service_available().await.is_err()
{
let verge = Config::verge().await;
verge.draft_mut().enable_tun_mode = Some(false);
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_ref().clone();
let verge_data = Self::verge().await.latest_arc();
logging_error!(Type::Core, verge_data.save_file().await);
}
@@ -83,11 +85,11 @@ impl Config {
// Ensure "Merge" and "Script" profile items exist, adding them if missing.
async fn ensure_default_profile_items() -> Result<()> {
let profiles = Self::profiles().await;
if profiles.latest_ref().get_item("Merge").is_err() {
if profiles.latest_arc().get_item("Merge").is_err() {
let merge_item = &mut PrfItem::from_merge(Some("Merge".into()))?;
profiles_append_item_safe(merge_item).await?;
}
if profiles.latest_ref().get_item("Script").is_err() {
if profiles.latest_arc().get_item("Script").is_err() {
let script_item = &mut PrfItem::from_script(Some("Script".into()))?;
profiles_append_item_safe(script_item).await?;
}
@@ -152,27 +154,27 @@ impl Config {
ConfigType::Check => dirs::app_home_dir()?.join(files::CHECK_CONFIG),
};
let runtime = Config::runtime().await;
let config = runtime
.latest_ref()
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.draft_mut() = IRuntime {
config: Some(config),
exists_keys,
chain_logs: logs,
};
Self::runtime().await.edit_draft(|d| {
*d = IRuntime {
config: Some(config),
exists_keys,
chain_logs: logs,
}
});
Ok(())
}
@@ -187,11 +189,11 @@ impl Config {
};
let operation = || async {
if Config::runtime().await.latest_ref().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 {
@@ -228,7 +230,7 @@ mod tests {
#[test]
#[allow(unused_variables)]
fn test_draft_size_non_boxed() {
let draft = Draft::from(IRuntime::new());
let draft = Draft::new(IRuntime::new());
let iruntime_size = std::mem::size_of_val(&draft);
assert_eq!(iruntime_size, std::mem::size_of::<Draft<IRuntime>>());
}
@@ -236,7 +238,7 @@ mod tests {
#[test]
#[allow(unused_variables)]
fn test_draft_size_boxed() {
let draft = Draft::from(Box::new(IRuntime::new()));
let draft = Draft::new(Box::new(IRuntime::new()));
let box_iruntime_size = std::mem::size_of_val(&draft);
assert_eq!(
box_iruntime_size,

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,
@@ -313,7 +320,9 @@ impl IVerge {
);
let config_draft = Config::verge().await;
**config_draft.draft_mut() = updated_config;
config_draft.edit_draft(|d| {
*d = updated_config;
});
config_draft.apply();
Ok(())
@@ -342,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()
@@ -437,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() {
@@ -522,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
}
@@ -543,162 +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,
}
}
}
impl From<Box<IVerge>> for IVergeResponse {
fn from(verge: Box<IVerge>) -> Self {
IVergeResponse::from(*verge)
}
}

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_ref().clone();
let verge = Config::verge().await.data_arc();
if verge.webdav_url.is_none()
|| verge.webdav_username.is_none()
|| verge.webdav_password.is_none()
@@ -99,11 +99,13 @@ impl WebDavClient {
let config = WebDavConfig {
url: verge
.webdav_url
.as_ref()
.cloned()
.unwrap_or_default()
.trim_end_matches('/')
.into(),
username: verge.webdav_username.unwrap_or_default(),
password: verge.webdav_password.unwrap_or_default(),
username: verge.webdav_username.as_ref().cloned().unwrap_or_default(),
password: verge.webdav_password.as_ref().cloned().unwrap_or_default(),
};
// 存储配置到 ArcSwapOption

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
}
@@ -387,7 +387,7 @@ impl EventDrivenProxyManager {
async fn get_proxy_config() -> ProxyConfig {
let (sys_enabled, pac_enabled, guard_enabled, guard_duration) = {
let verge_config = Config::verge().await;
let verge = verge_config.latest_ref();
let verge = verge_config.latest_arc();
(
verge.enable_system_proxy.unwrap_or(false),
verge.proxy_auto_config.unwrap_or(false),
@@ -406,7 +406,7 @@ impl EventDrivenProxyManager {
async fn get_expected_pac_config() -> Autoproxy {
let proxy_host = {
let verge_config = Config::verge().await;
let verge = verge_config.latest_ref();
let verge = verge_config.latest_arc();
verge
.proxy_host
.clone()
@@ -424,13 +424,13 @@ impl EventDrivenProxyManager {
let (verge_mixed_port, proxy_host) = {
let verge_config = Config::verge().await;
let verge_ref = verge_config.latest_ref();
let verge_ref = verge_config.latest_arc();
(verge_ref.verge_mixed_port, verge_ref.proxy_host.clone())
};
let default_port = {
let clash_config = Config::clash().await;
clash_config.latest_ref().get_mixed_port()
clash_config.latest_arc().get_mixed_port()
};
let port = verge_mixed_port.unwrap_or(default_port);
@@ -450,7 +450,7 @@ impl EventDrivenProxyManager {
use crate::constants::bypass;
let verge_config = Config::verge().await;
let verge = verge_config.latest_ref();
let verge = verge_config.latest_arc();
let use_default = verge.use_default_bypass.unwrap_or(true);
let custom = verge.system_proxy_bypass.as_deref().unwrap_or("");

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,
}
}
}
@@ -237,7 +237,7 @@ impl Hotkey {
let is_enable_global_hotkey = Config::verge()
.await
.latest_ref()
.latest_arc()
.enable_global_hotkey
.unwrap_or(true);
@@ -274,7 +274,7 @@ singleton_with_logging!(Hotkey, INSTANCE, "Hotkey");
impl Hotkey {
pub async fn init(&self, skip: bool) -> Result<()> {
let verge = Config::verge().await;
let enable_global_hotkey = !skip && verge.latest_ref().enable_global_hotkey.unwrap_or(true);
let enable_global_hotkey = !skip && verge.latest_arc().enable_global_hotkey.unwrap_or(true);
logging!(
debug,
@@ -284,7 +284,7 @@ impl Hotkey {
);
// Extract hotkeys data before async operations
let hotkeys = verge.latest_ref().hotkeys.as_ref().cloned();
let hotkeys = verge.latest_arc().hotkeys.as_ref().cloned();
if let Some(hotkeys) = hotkeys {
logging!(

View File

@@ -17,13 +17,15 @@ impl CoreManager {
use crate::constants::files::RUNTIME_CONFIG;
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
let clash_config = Config::clash().await.latest_ref().0.clone();
let clash_config = &Config::clash().await.latest_arc().0;
**Config::runtime().await.draft_mut() = IRuntime {
config: Some(clash_config.clone()),
exists_keys: vec![],
chain_logs: Default::default(),
};
Config::runtime().await.edit_draft(|d| {
*d = IRuntime {
config: Some(clash_config.to_owned()),
exists_keys: vec![],
chain_logs: Default::default(),
}
});
help::save_yaml(&runtime_path, &clash_config, Some("# Clash Verge Runtime")).await?;
handle::Handle::notice_message(error_key, error_msg);
@@ -135,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

@@ -47,10 +47,12 @@ impl CoreManager {
return Err(format!("Invalid clash core: {}", clash_core).into());
}
Config::verge().await.draft_mut().clash_core = clash_core.to_owned().into();
Config::verge().await.edit_draft(|d| {
d.clash_core = Some(clash_core.to_owned());
});
Config::verge().await.apply();
let verge_data = Config::verge().await.latest_ref().clone();
let verge_data = Config::verge().await.latest_arc();
verge_data.save_file().await.map_err(|e| e.to_string())?;
let run_path = Config::generate_file(ConfigType::Run)
@@ -66,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,
};
@@ -82,7 +85,7 @@ impl CoreManager {
let needs_service = Config::verge()
.await
.latest_ref()
.latest_arc()
.enable_tun_mode
.unwrap_or(false);

View File

@@ -32,7 +32,7 @@ impl CoreManager {
let config_file = Config::generate_file(crate::config::ConfigType::Run).await?;
let app_handle = handle::Handle::app_handle();
let clash_core = Config::verge().await.latest_ref().get_valid_clash_core();
let clash_core = Config::verge().await.latest_arc().get_valid_clash_core();
let config_dir = dirs::app_home_dir()?;
let (mut rx, child) = app_handle
@@ -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)
@@ -353,7 +353,7 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
logging!(info, Type::Service, "尝试使用现有服务启动核心");
let verge_config = Config::verge().await;
let clash_core = verge_config.latest_ref().get_valid_clash_core();
let clash_core = verge_config.latest_arc().get_valid_clash_core();
drop(verge_config);
let bin_ext = if cfg!(windows) { ".exe" } else { "" };
@@ -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

@@ -31,12 +31,12 @@ static DEFAULT_BYPASS: &str = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12
async fn get_bypass() -> String {
let use_default = Config::verge()
.await
.latest_ref()
.latest_arc()
.use_default_bypass
.unwrap_or(true);
let res = {
let verge = Config::verge().await;
let verge = verge.latest_ref();
let verge = verge.latest_arc();
verge.system_proxy_bypass.clone()
};
let custom_bypass = match res {
@@ -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),
@@ -124,17 +124,17 @@ impl Sysopt {
}
let port = {
let verge_port = Config::verge().await.latest_ref().verge_mixed_port;
let verge_port = Config::verge().await.latest_arc().verge_mixed_port;
match verge_port {
Some(port) => port,
None => Config::clash().await.latest_ref().get_mixed_port(),
None => Config::clash().await.latest_arc().get_mixed_port(),
}
};
let pac_port = IVerge::get_singleton_port();
let (sys_enable, pac_enable, proxy_host) = {
let verge = Config::verge().await;
let verge = verge.latest_ref();
let verge = verge.latest_arc();
(
verge.enable_system_proxy.unwrap_or(false),
verge.proxy_auto_config.unwrap_or(false),
@@ -266,7 +266,7 @@ impl Sysopt {
/// update the startup
pub async fn update_launch(&self) -> Result<()> {
let enable_auto_launch = { Config::verge().await.latest_ref().enable_auto_launch };
let enable_auto_launch = { Config::verge().await.latest_arc().enable_auto_launch };
let is_enable = enable_auto_launch.unwrap_or(false);
logging!(
info,

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),
@@ -100,7 +100,7 @@ impl Timer {
// Collect profiles that need immediate update
let profiles_to_update =
if let Some(items) = Config::profiles().await.latest_ref().get_items() {
if let Some(items) = Config::profiles().await.latest_arc().get_items() {
items
.iter()
.filter_map(|item| {
@@ -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
@@ -273,7 +270,7 @@ impl Timer {
async fn gen_map(&self) -> HashMap<String, u64> {
let mut new_map = HashMap::new();
if let Some(items) = Config::profiles().await.latest_ref().get_items() {
if let Some(items) = Config::profiles().await.latest_arc().get_items() {
for item in items.iter() {
if let Some(option) = item.option.as_ref()
&& let Some(allow_auto_update) = option.allow_auto_update
@@ -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,
@@ -427,7 +424,7 @@ impl Timer {
// Get the profile updated timestamp - now safe to await
let items = {
let profiles = Config::profiles().await;
let profiles_guard = profiles.latest_ref();
let profiles_guard = profiles.latest_arc();
match profiles_guard.get_items() {
Some(i) => i.clone(),
None => {
@@ -489,7 +486,7 @@ impl Timer {
match tokio::time::timeout(std::time::Duration::from_secs(40), async {
Self::emit_update_event(uid, true);
let is_current = Config::profiles().await.latest_ref().current.as_ref() == Some(uid);
let is_current = Config::profiles().await.latest_arc().current.as_ref() == Some(uid);
logging!(
info,
Type::Timer,

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;
@@ -25,6 +24,8 @@ use futures::future::join_all;
use parking_lot::Mutex;
use smartstring::alias::String;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::{
sync::atomic::{AtomicBool, Ordering},
@@ -55,18 +56,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
}
@@ -86,7 +86,7 @@ pub struct Tray {
impl TrayState {
pub async fn get_common_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_ref().clone();
let verge = Config::verge().await.latest_arc();
let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false);
if is_common_tray_icon
&& let Ok(Some(common_icon_path)) = find_target_icons("common")
@@ -123,7 +123,7 @@ impl TrayState {
}
pub async fn get_sysproxy_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_ref().clone();
let verge = Config::verge().await.latest_arc();
let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false);
if is_sysproxy_tray_icon
&& let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy")
@@ -160,7 +160,7 @@ impl TrayState {
}
pub async fn get_tun_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_ref().clone();
let verge = Config::verge().await.latest_arc();
let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false);
if is_tun_tray_icon
&& let Ok(Some(tun_icon_path)) = find_target_icons("tun")
@@ -198,7 +198,7 @@ impl TrayState {
impl Default for Tray {
fn default() -> Self {
Tray {
Self {
last_menu_update: Mutex::new(None),
menu_updating: AtomicBool::new(false),
}
@@ -243,7 +243,7 @@ impl Tray {
}
let app_handle = handle::Handle::app_handle();
let tray_event = { Config::verge().await.latest_ref().tray_event.clone() };
let tray_event = { Config::verge().await.latest_arc().tray_event.clone() };
let tray_event = tray_event.unwrap_or_else(|| "main_window".into());
let tray = app_handle
.tray_by_id("main")
@@ -303,7 +303,7 @@ impl Tray {
}
async fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> {
let verge = Config::verge().await.latest_ref().clone();
let verge = Config::verge().await.latest_arc();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
let tun_mode_available = cmd::system::is_admin().unwrap_or_default()
@@ -311,18 +311,16 @@ impl Tray {
let mode = {
Config::clash()
.await
.latest_ref()
.latest_arc()
.0
.get("mode")
.map(|val| val.as_str().unwrap_or("rule"))
.unwrap_or("rule")
.to_owned()
};
let profile_uid_and_name = Config::profiles()
.await
.data_mut()
.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") {
@@ -375,7 +373,7 @@ impl Tray {
}
};
let verge = Config::verge().await.latest_ref().clone();
let verge = Config::verge().await.latest_arc();
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
@@ -418,7 +416,7 @@ impl Tray {
}
};
let verge = Config::verge().await.latest_ref().clone();
let verge = Config::verge().await.latest_arc();
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
@@ -433,24 +431,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() {
@@ -460,7 +440,7 @@ impl Tray {
let app_handle = handle::Handle::app_handle();
let verge = Config::verge().await.latest_ref().clone();
let verge = Config::verge().await.latest_arc();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
@@ -474,9 +454,9 @@ impl Tray {
let mut current_profile_name = "None".into();
{
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
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 +505,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(())
@@ -552,7 +530,7 @@ impl Tray {
#[cfg(any(target_os = "macos", target_os = "windows"))]
let show_menu_on_left_click = {
let tray_event = { Config::verge().await.latest_ref().tray_event.clone() };
let tray_event = { Config::verge().await.latest_arc().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into());
tray_event.as_str() == "tray_menu"
};
@@ -583,7 +561,7 @@ impl Tray {
}
AsyncHandler::spawn(|| async move {
let tray_event = { Config::verge().await.latest_ref().tray_event.clone() };
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:?}");
@@ -598,9 +576,6 @@ impl Tray {
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;
@@ -622,22 +597,6 @@ impl Tray {
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 +625,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()
@@ -675,7 +634,7 @@ async fn create_profile_menu_item(
async move {
let is_current_profile = Config::profiles()
.await
.latest_ref()
.latest_arc()
.is_current_profile_index(profile_uid);
CheckMenuItem::with_id(
&app_handle,
@@ -870,7 +829,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("");
@@ -878,10 +837,10 @@ async fn create_tray_menu(
// 获取当前配置文件的选中代理组信息
let current_profile_selected = {
let profiles_config = Config::profiles().await;
let profiles_ref = profiles_config.latest_ref();
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()
};
@@ -924,7 +883,7 @@ async fn create_tray_menu(
.collect::<HashMap<String, usize>>()
});
let verge_settings = Config::verge().await.latest_ref().clone();
let verge_settings = Config::verge().await.latest_arc();
let show_proxy_groups_inline = verge_settings.tray_inline_proxy_groups.unwrap_or(false);
let version = env!("CARGO_PKG_VERSION");
@@ -1255,66 +1214,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),
}
@@ -266,7 +266,7 @@ impl CoreConfigValidator {
logging!(info, Type::Validate, "开始验证配置文件: {}", config_path);
let clash_core = Config::verge().await.latest_ref().get_valid_clash_core();
let clash_core = Config::verge().await.latest_arc().get_valid_clash_core();
logging!(info, Type::Validate, "使用内核: {}", clash_core);
let app_handle = handle::Handle::app_handle();
@@ -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,12 +44,49 @@ 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_ref().0.clone() };
let clash_config = { Config::clash().await.latest_arc().0.clone() };
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = {
let verge = Config::verge().await;
let verge = verge.latest_ref();
let verge = verge.latest_arc();
(
Some(verge.get_valid_clash_core()),
verge.enable_tun_mode.unwrap_or(false),
@@ -63,14 +100,14 @@ async fn get_config_values() -> ConfigValues {
#[cfg(not(target_os = "windows"))]
let redir_enabled = {
let verge = Config::verge().await;
let verge = verge.latest_ref();
let verge = verge.latest_arc();
verge.verge_redir_enabled.unwrap_or(false)
};
#[cfg(target_os = "linux")]
let tproxy_enabled = {
let verge = Config::verge().await;
let verge = verge.latest_ref();
let verge = verge.latest_arc();
verge.verge_tproxy_enabled.unwrap_or(false)
};
@@ -89,33 +126,43 @@ 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_ref().clone();
let profiles_clone = profiles.latest_arc();
profiles_clone.current_mapping().await.unwrap_or_default()
};
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_ref();
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,
)
};
@@ -139,8 +185,8 @@ async fn collect_profile_items() -> ProfileItems {
let merge_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(merge_uid).ok().cloned()
let profiles = profiles.latest_arc();
profiles.get_item(&merge_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
@@ -156,8 +202,8 @@ async fn collect_profile_items() -> ProfileItems {
let script_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(script_uid).ok().cloned()
let profiles = profiles.latest_arc();
profiles.get_item(&script_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
@@ -173,8 +219,8 @@ async fn collect_profile_items() -> ProfileItems {
let rules_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(rules_uid).ok().cloned()
let profiles = profiles.latest_arc();
profiles.get_item(&rules_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
@@ -190,8 +236,8 @@ async fn collect_profile_items() -> ProfileItems {
let proxies_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(proxies_uid).ok().cloned()
let profiles = profiles.latest_arc();
profiles.get_item(&proxies_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
@@ -207,8 +253,8 @@ async fn collect_profile_items() -> ProfileItems {
let groups_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(groups_uid).ok().cloned()
let profiles = profiles.latest_arc();
profiles.get_item(&groups_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
@@ -224,7 +270,7 @@ async fn collect_profile_items() -> ProfileItems {
let global_merge = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
let profiles = profiles.latest_arc();
profiles.get_item("Merge").ok().cloned()
};
if let Some(item) = item {
@@ -241,7 +287,7 @@ async fn collect_profile_items() -> ProfileItems {
let global_script = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
let profiles = profiles.latest_arc();
profiles.get_item("Script").ok().cloned()
};
if let Some(item) = item {
@@ -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;
@@ -394,7 +440,7 @@ async fn merge_default_config(
if key.as_str() == Some("external-controller") {
let enable_external_controller = Config::verge()
.await
.latest_ref()
.latest_arc()
.enable_external_controller
.unwrap_or(false);

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

@@ -78,7 +78,7 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
/// Restore WebDAV backup
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
let verge = Config::verge().await;
let verge_data = verge.latest_ref().clone();
let verge_data = verge.latest_arc();
let webdav_url = verge_data.webdav_url.clone();
let webdav_username = verge_data.webdav_username.clone();
let webdav_password = verge_data.webdav_password.clone();
@@ -243,7 +243,7 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
let (webdav_url, webdav_username, webdav_password) = {
let verge = Config::verge().await;
let verge = verge.latest_ref();
let verge = verge.latest_arc();
(
verge.webdav_url.clone(),
verge.webdav_username.clone(),

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) => {
@@ -72,10 +73,12 @@ pub async fn change_clash_mode(mode: String) {
{
Ok(_) => {
// 更新订阅
Config::clash().await.data_mut().patch_config(mapping);
Config::clash()
.await
.edit_draft(|d| d.patch_config(mapping));
// 分离数据获取和异步调用
let clash_data = Config::clash().await.data_mut().clone();
let clash_data = Config::clash().await.data_arc();
if clash_data.save_config().await.is_ok() {
handle::Handle::refresh_clash();
logging_error!(Type::Tray, tray::Tray::global().update_menu().await);
@@ -84,7 +87,7 @@ pub async fn change_clash_mode(mode: String) {
let is_auto_close_connection = Config::verge()
.await
.data_mut()
.data_arc()
.auto_close_connection
.unwrap_or(false);
if is_auto_close_connection {
@@ -102,7 +105,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
let tun_mode = Config::verge()
.await
.latest_ref()
.latest_arc()
.enable_tun_mode
.unwrap_or(false);

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;
@@ -12,8 +12,7 @@ use serde_yaml_ng::Mapping;
pub async fn patch_clash(patch: Mapping) -> Result<()> {
Config::clash()
.await
.draft_mut()
.patch_config(patch.clone());
.edit_draft(|d| d.patch_config(patch.clone()));
let res = {
// 激活订阅
@@ -25,7 +24,9 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
logging_error!(Type::Tray, tray::Tray::global().update_menu().await);
logging_error!(Type::Tray, tray::Tray::global().update_icon().await);
}
Config::runtime().await.draft_mut().patch_config(patch);
Config::runtime()
.await
.edit_draft(|d| d.patch_config(patch));
CoreManager::global().update_config().await?;
}
handle::Handle::refresh_clash();
@@ -35,7 +36,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
Ok(()) => {
Config::clash().await.apply();
// 分离数据获取和异步调用
let clash_data = Config::clash().await.data_mut().clone();
let clash_data = Config::clash().await.data_arc();
clash_data.save_config().await?;
Ok(())
}
@@ -190,7 +191,9 @@ async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<(
handle::Handle::refresh_clash();
}
if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 {
Config::verge().await.draft_mut().enable_global_hotkey = patch.enable_global_hotkey;
Config::verge()
.await
.edit_draft(|d| d.enable_global_hotkey = patch.enable_global_hotkey);
handle::Handle::refresh_verge();
}
if (update_flags & (UpdateFlags::Launch as i32)) != 0 {
@@ -227,7 +230,7 @@ async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<(
}
pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> {
Config::verge().await.draft_mut().patch_config(patch);
Config::verge().await.edit_draft(|d| d.patch_config(patch));
let update_flags = determine_update_flags(patch);
let process_flag_result: std::result::Result<(), anyhow::Error> = {
@@ -242,8 +245,14 @@ pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> {
Config::verge().await.apply();
if !not_save_file {
// 分离数据获取和异步调用
let verge_data = Config::verge().await.data_ref().clone();
let verge_data = Config::verge().await.data_arc();
verge_data.save_file().await?;
}
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
);
}
}
}
@@ -28,7 +80,7 @@ async fn should_update_profile(
ignore_auto_update: bool,
) -> Result<Option<(String, Option<PrfOption>)>> {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
let profiles = profiles.latest_arc();
let item = profiles.get_item(uid)?;
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
@@ -89,15 +141,15 @@ async fn perform_profile_update(
let mut merged_opt = PrfOption::merge(opt, option);
let is_current = {
let profiles = Config::profiles().await;
profiles.latest_ref().is_current_profile_index(uid)
};
let profile_name = {
let profiles = Config::profiles().await;
profiles
.latest_ref()
.get_name_by_uid(uid)
.unwrap_or_default()
profiles.latest_arc().is_current_profile_index(uid)
};
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

@@ -10,8 +10,8 @@ use tauri_plugin_clipboard_manager::ClipboardExt;
/// Toggle system proxy on/off
pub async fn toggle_system_proxy() {
let verge = Config::verge().await;
let enable = verge.latest_ref().enable_system_proxy.unwrap_or(false);
let auto_close_connection = verge.latest_ref().auto_close_connection.unwrap_or(false);
let enable = verge.latest_arc().enable_system_proxy.unwrap_or(false);
let auto_close_connection = verge.latest_arc().auto_close_connection.unwrap_or(false);
// 如果当前系统代理即将关闭且自动关闭连接设置为true则关闭所有连接
if enable
@@ -42,7 +42,7 @@ pub async fn toggle_system_proxy() {
/// Toggle TUN mode on/off
pub async fn toggle_tun_mode(not_save_file: Option<bool>) {
let enable = Config::verge().await.data_mut().enable_tun_mode;
let enable = Config::verge().await.latest_arc().enable_tun_mode;
let enable = enable.unwrap_or(false);
match super::patch_verge(
@@ -66,7 +66,7 @@ pub async fn copy_clash_env() {
Ok(ip) => ip.into(),
Err(_) => Config::verge()
.await
.latest_ref()
.latest_arc()
.proxy_host
.clone()
.unwrap_or_else(|| "127.0.0.1".into()),
@@ -76,7 +76,7 @@ pub async fn copy_clash_env() {
let port = {
Config::verge()
.await
.latest_ref()
.latest_arc()
.verge_mixed_port
.unwrap_or(7897)
};
@@ -84,7 +84,7 @@ pub async fn copy_clash_env() {
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}");
let cliboard = app_handle.clipboard();
let env_type = { Config::verge().await.latest_ref().env_type.clone() };
let env_type = { Config::verge().await.latest_arc().env_type.clone() };
let env_type = match env_type {
Some(env_type) => env_type,
None => {

View File

@@ -47,7 +47,7 @@ pub async fn clean_async() -> bool {
let tun_task = async {
let tun_enabled = Config::verge()
.await
.latest_ref()
.latest_arc()
.enable_tun_mode
.unwrap_or(false);
@@ -100,7 +100,7 @@ pub async fn clean_async() -> bool {
// 检查系统代理是否开启
let sys_proxy_enabled = Config::verge()
.await
.latest_ref()
.latest_arc()
.enable_system_proxy
.unwrap_or(false);
@@ -176,7 +176,7 @@ pub async fn clean_async() -> bool {
{
let sys_proxy_enabled = Config::verge()
.await
.latest_ref()
.latest_arc()
.enable_system_proxy
.unwrap_or(false);
@@ -316,7 +316,7 @@ pub async fn hide() {
let enable_auto_light_weight_mode = Config::verge()
.await
.data_mut()
.latest_arc()
.enable_auto_light_weight_mode
.unwrap_or(false);

View File

@@ -319,7 +319,7 @@ pub fn run() {
AsyncHandler::spawn(move || async move {
let is_enable_global_hotkey = Config::verge()
.await
.latest_ref()
.latest_arc()
.enable_global_hotkey
.unwrap_or(true);
@@ -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_ref()
.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
.data_mut()
let is_enable_auto = verge_config
.data_arc()
.enable_auto_light_weight_mode
.unwrap_or(false);
let is_silent_start = verge_config
.latest_ref()
.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_ref()
.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

@@ -1,179 +1,369 @@
use parking_lot::RwLock;
use std::sync::Arc;
use parking_lot::{
MappedRwLockReadGuard, MappedRwLockWriteGuard, RwLock, RwLockReadGuard,
RwLockUpgradableReadGuard, RwLockWriteGuard,
};
pub type SharedBox<T> = Arc<Box<T>>;
type DraftInner<T> = (SharedBox<T>, Option<SharedBox<T>>);
/// Draft 管理committed 与 optional draft 都以 Arc<Box<T>> 存储,
// (committed_snapshot, optional_draft_snapshot)
#[derive(Debug, Clone)]
pub struct Draft<T: Clone + ToOwned> {
inner: Arc<RwLock<(T, Option<T>)>>,
pub struct Draft<T: Clone> {
inner: Arc<RwLock<DraftInner<T>>>,
}
impl<T: Clone + ToOwned> From<T> for Draft<T> {
fn from(data: T) -> Self {
impl<T: Clone> Draft<T> {
pub fn new(data: T) -> Self {
Self {
inner: Arc::new(RwLock::new((data, None))),
inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))),
}
}
}
/// Implements draft management for `Box<T>`, allowing for safe concurrent editing and committing of draft data.
/// # Type Parameters
/// - `T`: The underlying data type, which must implement `Clone` and `ToOwned`.
///
/// # Methods
/// - `data_mut`: Returns a mutable reference to the committed data.
/// - `draft_mut`: Creates or retrieves a mutable reference to the draft data, cloning the committed data if no draft exists.
/// - `latest_ref`: Returns an immutable reference to the draft data if it exists, otherwise to the committed data.
/// - `apply`: Commits the draft data, replacing the committed data and returning the old committed value if a draft existed.
/// - `discard`: Discards the draft data and returns it if it existed.
impl<T: Clone + ToOwned> Draft<Box<T>> {
/// 正式数据视图
pub fn data_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
RwLockReadGuard::map(self.inner.read(), |inner| &inner.0)
/// 以 Arc<Box<T>> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc
pub fn data_arc(&self) -> SharedBox<T> {
let guard = self.inner.read();
Arc::clone(&guard.0)
}
/// 可写正式数据
pub fn data_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
RwLockWriteGuard::map(self.inner.write(), |inner| &mut inner.0)
/// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照
/// 这也是零拷贝:只 clone Arc不 clone T
pub fn latest_arc(&self) -> SharedBox<T> {
let guard = self.inner.read();
guard
.1
.as_ref()
.cloned()
.unwrap_or_else(|| Arc::clone(&guard.0))
}
/// 创建或获取草稿并返回可写引用
pub fn draft_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
let guard = self.inner.upgradable_read();
if guard.1.is_none() {
let mut guard = RwLockUpgradableReadGuard::upgrade(guard);
guard.1 = Some(guard.0.clone());
return RwLockWriteGuard::map(guard, |inner| {
inner.1.as_mut().unwrap_or_else(|| {
unreachable!("Draft was just created above, this should never fail")
})
});
}
// 已存在草稿,升级为写锁映射
RwLockWriteGuard::map(RwLockUpgradableReadGuard::upgrade(guard), |inner| {
inner
.1
.as_mut()
.unwrap_or_else(|| unreachable!("Draft should exist when guard.1.is_some()"))
})
/// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T
/// - 延迟拷贝:如果只有这一个 Arc 引用,则直接修改,不会克隆 T
/// - 若草稿被其他读者共享Arc::make_mut 会做一次 T.clone最小必要拷贝
pub fn edit_draft<F, R>(&self, f: F) -> R
where
F: FnOnce(&mut T) -> R,
{
// 先获得写锁以创建或取出草稿 Arc 的可变引用位置
let mut guard = self.inner.write();
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(&mut draft_arc); // &mut Box<T>
// 对 Box<T> 解引用得到 &mut T
let result = f(&mut **boxed);
// 恢复修改后的草稿 Arc
self.inner.write().1 = Some(draft_arc);
result
}
/// 零拷贝只读视图:返回草稿(若存在)或正式值
pub fn latest_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
RwLockReadGuard::map(self.inner.read(), |inner| {
inner.1.as_ref().unwrap_or(&inner.0)
})
}
/// 提交草稿,返回旧正式数据
/// 将草稿提交到已提交位置(替换),并清除草稿
pub fn apply(&self) {
let guard = self.inner.upgradable_read();
if guard.1.is_none() {
return;
}
let mut guard = RwLockUpgradableReadGuard::upgrade(guard);
if let Some(draft) = guard.1.take() {
guard.0 = draft;
let mut guard = self.inner.write();
if let Some(d) = guard.1.take() {
guard.0 = d;
}
}
/// 丢弃草稿,返回被丢弃的草稿
/// 丢弃草稿(如果存在)
pub fn discard(&self) {
self.inner.write().1.take();
let mut guard = self.inner.write();
guard.1 = None;
}
/// 异步修改正式数据,闭包直接获得 Box<T> 所有权
pub async fn with_data_modify<F, Fut, R, E>(&self, f: F) -> Result<R, E>
/// 异步地以拥有 Box<T> 的方式修改已提交数据:将克隆一次已提交数据到本地,
/// 异步闭包返回新的 Box<T>(替换已提交数据)和业务返回值 R。
pub async fn with_data_modify<F, Fut, R>(&self, f: F) -> Result<R, anyhow::Error>
where
T: Send + Sync + 'static,
F: FnOnce(Box<T>) -> Fut + Send,
Fut: std::future::Future<Output = Result<(Box<T>, R), E>> + Send,
E: From<anyhow::Error>,
Fut: std::future::Future<Output = Result<(Box<T>, R), anyhow::Error>> + Send,
{
// 克隆正式数据
let local = {
// 读取已提交快照cheap Arc clone, 然后得到 Box<T> 所有权 via clone
// 注意:为了让闭包接收 Box<T> 所有权,我们需要 clone 底层 T不可避免
let local: Box<T> = {
let guard = self.inner.read();
guard.0.clone()
// 将 Arc<Box<T>> 的 Box<T> clone 出来(会调用 T: Clone
(*guard.0).clone()
};
// 异步闭包执行,返回修改后的 Box<T> 和业务结果 R
let (new_local, res) = f(local).await?;
// 写回正式数据
let mut guard = self.inner.write();
guard.0 = new_local;
// 将新的 Box<T> 放到已提交位置(包进 Arc
self.inner.write().0 = Arc::new(new_local);
Ok(res)
}
}
#[test]
fn test_draft_box() {
use crate::config::IVerge;
#[cfg(test)]
mod tests {
use super::*;
use anyhow::anyhow;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
// 1. 创建 Draft<Box<IVerge>>
let verge = Box::new(IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
..IVerge::default()
});
let draft = Draft::from(verge);
// 2. 读取正式数据data_mut
{
let data = draft.data_mut();
assert_eq!(data.enable_auto_launch, Some(true));
assert_eq!(data.enable_tun_mode, Some(false));
#[derive(Clone, Debug, Default, PartialEq)]
struct IVerge {
enable_auto_launch: Option<bool>,
enable_tun_mode: Option<bool>,
}
// 3. 初次获取草稿draft_mut 会自动 clone 一份)
{
let draft_view = draft.draft_mut();
assert_eq!(draft_view.enable_auto_launch, Some(true));
assert_eq!(draft_view.enable_tun_mode, Some(false));
// Minimal single-threaded executor for immediately-ready futures
fn block_on_ready<F: Future>(fut: F) -> F::Output {
fn no_op_raw_waker() -> RawWaker {
fn clone(_: *const ()) -> RawWaker {
no_op_raw_waker()
}
fn wake(_: *const ()) {}
fn wake_by_ref(_: *const ()) {}
fn drop(_: *const ()) {}
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);
RawWaker::new(std::ptr::null(), &VTABLE)
}
let waker = unsafe { Waker::from_raw(no_op_raw_waker()) };
let mut cx = Context::from_waker(&waker);
let mut fut = Box::pin(fut);
loop {
match Pin::as_mut(&mut fut).poll(&mut cx) {
Poll::Ready(v) => return v,
Poll::Pending => std::thread::yield_now(),
}
}
}
// 4. 修改草稿
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
d.enable_tun_mode = Some(true);
#[test]
fn test_draft_basic_flow() {
let verge = IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
};
let draft = Draft::new(verge);
// 读取正式数据data_arc
{
let data = draft.data_arc();
assert_eq!(data.enable_auto_launch, Some(true));
assert_eq!(data.enable_tun_mode, Some(false));
}
// 修改草稿(使用 edit_draft
draft.edit_draft(|d| {
d.enable_auto_launch = Some(false);
d.enable_tun_mode = Some(true);
});
// 正式数据未变
{
let data = draft.data_arc();
assert_eq!(data.enable_auto_launch, Some(true));
assert_eq!(data.enable_tun_mode, Some(false));
}
// 草稿已变
{
let latest = draft.latest_arc();
assert_eq!(latest.enable_auto_launch, Some(false));
assert_eq!(latest.enable_tun_mode, Some(true));
}
// 提交草稿
draft.apply();
// 正式数据已更新
{
let data = draft.data_arc();
assert_eq!(data.enable_auto_launch, Some(false));
assert_eq!(data.enable_tun_mode, Some(true));
}
// 新一轮草稿并修改
draft.edit_draft(|d| {
d.enable_auto_launch = Some(true);
});
{
let latest = draft.latest_arc();
assert_eq!(latest.enable_auto_launch, Some(true));
assert_eq!(latest.enable_tun_mode, Some(true));
}
// 丢弃草稿
draft.discard();
// 丢弃后再次创建草稿,会从已提交重新 clone
{
draft.edit_draft(|d| {
// 原 committed 是 enable_auto_launch = Some(false)
assert_eq!(d.enable_auto_launch, Some(false));
// 再修改一下
d.enable_tun_mode = Some(false);
});
// 草稿中值已修改,但正式数据仍是 apply 后的值
let data = draft.data_arc();
assert_eq!(data.enable_auto_launch, Some(false));
assert_eq!(data.enable_tun_mode, Some(true));
}
}
// 正式数据未变
assert_eq!(draft.data_mut().enable_auto_launch, Some(true));
assert_eq!(draft.data_mut().enable_tun_mode, Some(false));
#[test]
fn test_arc_pointer_behavior_on_edit_and_apply() {
let draft = Draft::new(IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
});
// 草稿已变
{
let latest = draft.latest_ref();
assert_eq!(latest.enable_auto_launch, Some(false));
// 初始 latest == committed
let committed = draft.data_arc();
let latest = draft.latest_arc();
assert!(std::sync::Arc::ptr_eq(&committed, &latest));
// 第一次 edit由于与 committed 共享Arc::make_mut 会克隆
draft.edit_draft(|d| d.enable_tun_mode = Some(true));
let committed_after_first_edit = draft.data_arc();
let draft_after_first_edit = draft.latest_arc();
assert!(!std::sync::Arc::ptr_eq(
&committed_after_first_edit,
&draft_after_first_edit
));
// 提交会把 committed 指向草稿的 Arc
let prev_draft_ptr = std::sync::Arc::as_ptr(&draft_after_first_edit);
draft.apply();
let committed_after_apply = draft.data_arc();
assert_eq!(
std::sync::Arc::as_ptr(&committed_after_apply),
prev_draft_ptr
);
// 第二次编辑:此时草稿唯一持有(无其它引用),不应再克隆
// 获取草稿 Arc 的指针并立即丢弃本地引用,避免增加 strong_count
draft.edit_draft(|d| d.enable_auto_launch = Some(false));
let latest1 = draft.latest_arc();
let latest1_ptr = std::sync::Arc::as_ptr(&latest1);
drop(latest1); // 确保只有 Draft 内部持有草稿 Arc
// 再次编辑uniqueArc::make_mut 不应克隆)
draft.edit_draft(|d| d.enable_tun_mode = Some(false));
let latest2 = draft.latest_arc();
let latest2_ptr = std::sync::Arc::as_ptr(&latest2);
assert_eq!(latest1_ptr, latest2_ptr, "Unique edit should not clone Arc");
assert_eq!(latest2.enable_auto_launch, Some(false));
assert_eq!(latest2.enable_tun_mode, Some(false));
}
#[test]
fn test_discard_restores_latest_to_committed() {
let draft = Draft::new(IVerge {
enable_auto_launch: Some(false),
enable_tun_mode: Some(false),
});
// 创建草稿并修改
draft.edit_draft(|d| d.enable_auto_launch = Some(true));
let committed = draft.data_arc();
let latest = draft.latest_arc();
assert!(!std::sync::Arc::ptr_eq(&committed, &latest));
// 丢弃草稿后 latest 应回到 committed
draft.discard();
let committed2 = draft.data_arc();
let latest2 = draft.latest_arc();
assert!(std::sync::Arc::ptr_eq(&committed2, &latest2));
assert_eq!(latest2.enable_auto_launch, Some(false));
}
#[test]
fn test_edit_draft_returns_closure_result() {
let draft = Draft::new(IVerge::default());
let ret = draft.edit_draft(|d| {
d.enable_tun_mode = Some(true);
123usize
});
assert_eq!(ret, 123);
let latest = draft.latest_arc();
assert_eq!(latest.enable_tun_mode, Some(true));
}
// 5. 提交草稿
draft.apply();
#[test]
fn test_with_data_modify_ok_and_replaces_committed() {
let draft = Draft::new(IVerge {
enable_auto_launch: Some(false),
enable_tun_mode: Some(false),
});
// 正式数据已更新
{
let data = draft.data_mut();
assert_eq!(data.enable_auto_launch, Some(false));
assert_eq!(data.enable_tun_mode, Some(true));
// 使用 with_data_modify 异步(立即就绪)地更新 committed
let res = block_on_ready(draft.with_data_modify(|mut v| async move {
v.enable_auto_launch = Some(true);
Ok((Box::new(*v), "done")) // Dereference v to get Box<T>
}));
assert_eq!(
{
#[allow(clippy::unwrap_used)]
res.unwrap()
},
"done"
);
let committed = draft.data_arc();
assert_eq!(committed.enable_auto_launch, Some(true));
assert_eq!(committed.enable_tun_mode, Some(false));
}
// 6. 新建并修改下一轮草稿
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(true);
#[test]
fn test_with_data_modify_error_propagation() {
let draft = Draft::new(IVerge::default());
#[allow(clippy::unwrap_used)]
let err = block_on_ready(draft.with_data_modify(|v| async move {
drop(v);
Err::<(Box<IVerge>, ()), _>(anyhow!("boom"))
}))
.unwrap_err();
assert_eq!(format!("{err}"), "boom");
}
assert_eq!(draft.draft_mut().enable_auto_launch, Some(true));
// 7. 丢弃草稿
draft.discard();
#[test]
fn test_with_data_modify_does_not_touch_existing_draft() {
let draft = Draft::new(IVerge {
enable_auto_launch: Some(false),
enable_tun_mode: Some(false),
});
// 8. 草稿已被丢弃,新的 draft_mut() 会重新 clone
assert_eq!(draft.draft_mut().enable_auto_launch, Some(false));
// 创建草稿并修改
draft.edit_draft(|d| {
d.enable_auto_launch = Some(true);
d.enable_tun_mode = Some(true);
});
let draft_before = draft.latest_arc();
let draft_before_ptr = std::sync::Arc::as_ptr(&draft_before);
// 同时通过 with_data_modify 修改 committed
#[allow(clippy::unwrap_used)]
block_on_ready(draft.with_data_modify(|mut v| async move {
v.enable_auto_launch = Some(false); // 与草稿不同
Ok((Box::new(*v), ())) // Dereference v to get Box<T>
}))
.unwrap();
// 草稿应保持不变
let draft_after = draft.latest_arc();
assert_eq!(
std::sync::Arc::as_ptr(&draft_after),
draft_before_ptr,
"Existing draft should not be replaced by with_data_modify"
);
assert_eq!(draft_after.enable_auto_launch, Some(true));
assert_eq!(draft_after.enable_tun_mode, Some(true));
// 丢弃草稿后 latest == committed且 committed 为异步修改结果
draft.discard();
let latest = draft.latest_arc();
assert_eq!(latest.enable_auto_launch, Some(false));
assert_eq!(latest.enable_tun_mode, Some(false));
}
}

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

@@ -43,7 +43,7 @@ pub fn get_supported_languages() -> Vec<String> {
pub async fn current_language() -> String {
Config::verge()
.await
.latest_ref()
.latest_arc()
.language
.as_deref()
.map(String::from)

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::{
@@ -31,7 +31,7 @@ pub async fn init_logger() -> Result<()> {
// TODO 提供 runtime 级别实时修改
let (log_level, log_max_size, log_max_count) = {
let verge_guard = Config::verge().await;
let verge = verge_guard.latest_ref();
let verge = verge_guard.latest_arc();
(
verge.get_log_level(),
verge.app_log_max_size.unwrap_or(128),
@@ -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()?;
@@ -81,7 +89,7 @@ pub async fn init_logger() -> Result<()> {
pub async fn sidecar_writer() -> Result<FileLogWriter> {
let (log_max_size, log_max_count) = {
let verge_guard = Config::verge().await;
let verge = verge_guard.latest_ref();
let verge = verge_guard.latest_arc();
(
verge.app_log_max_size.unwrap_or(128),
verge.app_log_max_count.unwrap_or(8),
@@ -109,7 +117,7 @@ pub async fn sidecar_writer() -> Result<FileLogWriter> {
pub async fn service_writer_config() -> Result<WriterConfig> {
let (log_max_size, log_max_count) = {
let verge_guard = Config::verge().await;
let verge = verge_guard.latest_ref();
let verge = verge_guard.latest_arc();
(
verge.app_log_max_size.unwrap_or(128),
verge.app_log_max_count.unwrap_or(8),
@@ -134,7 +142,7 @@ pub async fn delete_log() -> Result<()> {
let auto_log_clean = {
let verge = Config::verge().await;
let verge = verge.latest_ref();
let verge = verge.latest_arc();
verge.auto_log_clean.unwrap_or(0)
};
@@ -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(())
}
@@ -509,7 +517,7 @@ pub async fn startup_script() -> Result<()> {
let app_handle = handle::Handle::app_handle();
let script_path = {
let verge = Config::verge().await;
let verge = verge.latest_ref();
let verge = verge.latest_arc();
verge.startup_script.clone().unwrap_or_else(|| "".into())
};

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);
}
@@ -165,10 +159,10 @@ impl NetworkManager {
ProxyType::None => None,
ProxyType::Localhost => {
let port = {
let verge_port = Config::verge().await.latest_ref().verge_mixed_port;
let verge_port = Config::verge().await.latest_arc().verge_mixed_port;
match verge_port {
Some(port) => port,
None => Config::clash().await.latest_ref().get_mixed_port(),
None => Config::clash().await.latest_arc().get_mixed_port(),
}
};
let proxy_scheme = format!("http://127.0.0.1:{port}");

View File

@@ -179,7 +179,7 @@ pub(super) async fn refresh_tray_menu() {
pub(super) async fn init_window() {
let is_silent_start = Config::verge()
.await
.latest_ref()
.latest_arc()
.enable_silent_start
.unwrap_or(false);
#[cfg(target_os = "macos")]

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

@@ -22,7 +22,7 @@ pub async fn build_new_window() -> Result<WebviewWindow, String> {
let app_handle = handle::Handle::app_handle();
let config = Config::verge().await;
let latest = config.latest_ref();
let latest = config.latest_arc();
let start_page = latest.start_page.as_deref().unwrap_or("/");
match tauri::WebviewWindowBuilder::new(

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"))]
@@ -89,15 +91,15 @@ pub fn embed_server() {
let clash_config = Config::clash().await;
let pac_content = verge_config
.latest_ref()
.latest_arc()
.pac_file_content
.clone()
.unwrap_or_else(|| DEFAULT_PAC.into());
let pac_port = verge_config
.latest_ref()
.latest_arc()
.verge_mixed_port
.unwrap_or_else(|| clash_config.latest_ref().get_mixed_port());
.unwrap_or_else(|| clash_config.latest_arc().get_mixed_port());
let pac = warp::path!("commands" / "pac").map(move || {
let processed_content = pac_content.replace("%mixed-port%", &format!("{pac_port}"));

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;