refactor: api cleanup (#7812)

* refactor(config): unify YAML persistence

* refactor(config): remove redundant config APIs

* refactor(config): remove unused config APIs

* refactor(config): narrow profile APIs

* fix(config): preserve profiles on invalid reorder

* refactor(startup): narrow resolve helpers

* refactor(config): unify mixed port defaults

* refactor(network): simplify response APIs

* refactor(config): make profile builders infallible

* refactor(config): make profile activation infallible

* refactor(api): simplify internal runtime APIs

* refactor: consolidate internal config and window APIs
This commit is contained in:
Tunglies
2026-08-25 07:19:17 +00:00
committed by GitHub
parent 272c09568c
commit b7c4f7d39b
29 changed files with 277 additions and 413 deletions
+5 -15
View File
@@ -203,27 +203,17 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
}
#[tauri::command]
pub fn check_dns_config_exists() -> CmdResult<bool> {
use crate::utils::dirs;
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
Ok(dns_path.exists())
}
#[tauri::command]
pub async fn get_dns_config_content() -> CmdResult<String> {
pub async fn get_dns_config_content() -> CmdResult<Option<String>> {
use crate::utils::dirs;
use tokio::fs;
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
if !fs::try_exists(&dns_path).await.stringify_err()? {
return Err("DNS config file not found".into());
match fs::read_to_string(&dns_path).await {
Ok(content) => Ok(Some(content.into())),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(CommandFailure::plain(error)),
}
let content = fs::read_to_string(&dns_path).await.stringify_err()?.into();
Ok(content)
}
#[tauri::command]
+2 -2
View File
@@ -177,7 +177,7 @@ pub async fn delete_profile(index: String) -> CmdResult {
}
};
if should_update {
logging_error!(Type::Config, profiles::activate_selected_nodes());
profiles::activate_selected_nodes();
}
drop(config_update_guard);
drop(profile_write_guard);
@@ -241,7 +241,7 @@ async fn handle_success(current_value: Option<&String>) -> CmdResult<ValidationO
.await
.stringify_err()?;
// Runtime refresh and tray rebuilding happen after saved node selections are restored.
profiles::activate_selected_nodes().stringify_err()?;
profiles::activate_selected_nodes();
if let Err(e) = profiles_save_file_safe().await {
logging!(warn, Type::Cmd, "Warning: 异步保存配置文件失败: {e}");
+1 -1
View File
@@ -107,7 +107,7 @@ async fn restore_original(
}
fn profile_affects_runtime(profiles: &IProfiles, index: &str) -> bool {
let Some(current_uid) = profiles.get_current() else {
let Some(current_uid) = profiles.current.as_ref() else {
return false;
};
if current_uid == index {
+11 -43
View File
@@ -1,4 +1,3 @@
use crate::config::Config;
use crate::constants::{network, tun as tun_const};
use crate::utils::dirs::{path_to_str, sidecar_ipc_path};
use crate::utils::{dirs, help};
@@ -118,7 +117,7 @@ impl IClashTemp {
let mixed_port = Self::guard_mixed_port(&config);
let socks_port = Self::guard_socks_port(&config);
let port = Self::guard_port(&config);
let ctrl = Self::guard_external_controller(&config);
let ctrl = Self::guard_server_ctrl(&config);
#[cfg(unix)]
let external_controller_unix = Self::guard_external_controller_ipc();
#[cfg(windows)]
@@ -154,16 +153,6 @@ impl IClashTemp {
Self::guard_mixed_port(&self.0)
}
#[allow(unused)]
pub fn get_socks_port(&self) -> u16 {
Self::guard_socks_port(&self.0)
}
#[allow(unused)]
pub fn get_port(&self) -> u16 {
Self::guard_port(&self.0)
}
pub fn get_client_info(&self) -> ClashInfo {
let config = &self.0;
@@ -192,7 +181,7 @@ impl IClashTemp {
})
}
#[cfg(not(target_os = "windows"))]
pub fn guard_redir_port(config: &Mapping) -> u16 {
fn guard_redir_port(config: &Mapping) -> u16 {
let mut port = config
.get("redir-port")
.and_then(|value| match value {
@@ -208,7 +197,7 @@ impl IClashTemp {
}
#[cfg(target_os = "linux")]
pub fn guard_tproxy_port(config: &Mapping) -> u16 {
fn guard_tproxy_port(config: &Mapping) -> u16 {
let mut port = config
.get("tproxy-port")
.and_then(|value| match value {
@@ -223,7 +212,7 @@ impl IClashTemp {
port
}
pub fn guard_mixed_port(config: &Mapping) -> u16 {
fn guard_mixed_port(config: &Mapping) -> u16 {
let raw_value = config.get("mixed-port");
let mut port = raw_value
@@ -232,16 +221,16 @@ impl IClashTemp {
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
_ => None,
})
.unwrap_or(7897);
.unwrap_or(network::ports::DEFAULT_MIXED);
if port == 0 {
port = 7897;
port = network::ports::DEFAULT_MIXED;
}
port
}
pub fn guard_socks_port(config: &Mapping) -> u16 {
fn guard_socks_port(config: &Mapping) -> u16 {
let mut port = config
.get("socks-port")
.and_then(|value| match value {
@@ -256,7 +245,7 @@ impl IClashTemp {
port
}
pub fn guard_port(config: &Mapping) -> u16 {
fn guard_port(config: &Mapping) -> u16 {
let mut port = config
.get("port")
.and_then(|value| match value {
@@ -271,7 +260,7 @@ impl IClashTemp {
port
}
pub fn guard_server_ctrl(config: &Mapping) -> String {
pub(super) fn guard_server_ctrl(config: &Mapping) -> String {
config
.get("external-controller")
.and_then(|value| match value.as_str() {
@@ -290,28 +279,7 @@ impl IClashTemp {
.unwrap_or_else(|| "127.0.0.1:9097".into())
}
pub fn guard_external_controller(config: &Mapping) -> String {
// 在初始化阶段,直接返回配置中的值,不进行额外检查
// 这样可以避免在配置加载期间的循环依赖
Self::guard_server_ctrl(config)
}
pub async fn guard_external_controller_with_setting(config: &Mapping) -> String {
// 检查 enable_external_controller 设置,用于运行时配置生成
let enable_external_controller = Config::verge()
.await
.latest_arc()
.enable_external_controller
.unwrap_or(false);
if enable_external_controller {
Self::guard_server_ctrl(config)
} else {
"".into()
}
}
pub fn guard_client_ctrl(config: &Mapping) -> String {
fn guard_client_ctrl(config: &Mapping) -> String {
let value = Self::guard_server_ctrl(config);
match SocketAddr::from_str(value.as_str()) {
Ok(mut socket) => {
@@ -324,7 +292,7 @@ impl IClashTemp {
}
}
pub fn guard_external_controller_ipc() -> String {
pub(crate) fn guard_external_controller_ipc() -> String {
// 总是使用当前的 IPC 路径,确保配置文件与运行时路径一致
sidecar_ipc_path()
.ok()
+8 -13
View File
@@ -27,7 +27,7 @@ use std::{
use tokio::sync::{Mutex, MutexGuard, OnceCell};
use tokio::time::sleep;
pub struct Config {
pub(crate) struct Config {
clash_config: Draft<IClashTemp>,
verge_config: Draft<IVerge>,
profiles_config: Draft<IProfiles>,
@@ -38,7 +38,7 @@ static TUN_SESSION_SUPPRESSED: AtomicBool = AtomicBool::new(false);
static CONFIG_WRITE_LOCK: Mutex<()> = Mutex::const_new(());
impl Config {
pub async fn global() -> &'static Self {
async fn global() -> &'static Self {
static CONFIG: OnceCell<Config> = OnceCell::const_new();
CONFIG
.get_or_init(|| async {
@@ -46,7 +46,7 @@ impl Config {
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()),
runtime_config: Draft::new(IRuntime::default()),
}
})
.await
@@ -73,11 +73,6 @@ impl Config {
CONFIG_WRITE_LOCK.lock().await
}
pub async fn init_config() -> Result<()> {
Self::init_config_before_window().await?;
Self::init_runtime_config().await
}
pub async fn init_config_before_window() -> Result<()> {
Self::ensure_default_profile_items().await?;
@@ -151,7 +146,7 @@ impl Config {
}
async fn ensure_default_profile_items_for(profiles: &Draft<IProfiles>) -> Result<()> {
if profiles.latest_arc().get_items().is_none() {
if profiles.latest_arc().items.is_none() {
logging!(
warn,
Type::Config,
@@ -161,11 +156,11 @@ impl Config {
}
if profiles.latest_arc().get_item("Merge").is_err() {
let merge_item = &mut PrfItem::from_merge(Some("Merge".into()))?;
let merge_item = &mut PrfItem::from_merge(Some("Merge".into()));
profiles_append_item_to_safe(profiles, merge_item).await?;
}
if profiles.latest_arc().get_item("Script").is_err() {
let script_item = &mut PrfItem::from_script(Some("Script".into()))?;
let script_item = &mut PrfItem::from_script(Some("Script".into()));
profiles_append_item_to_safe(profiles, script_item).await?;
}
Ok(())
@@ -381,7 +376,7 @@ fn collect_names(config: &Mapping, list_key: &str, out: &mut HashSet<String>) {
}
#[derive(Debug)]
pub enum ConfigType {
pub(crate) enum ConfigType {
Run,
Check,
}
@@ -405,7 +400,7 @@ mod tests {
tokio::fs::remove_dir_all(&profiles_dir).await?;
assert!(profile_was_preserved);
assert!(profiles.data_arc().get_items().is_none());
assert!(profiles.data_arc().items.is_none());
Ok(())
}
}
-3
View File
@@ -8,9 +8,6 @@ use anyhow::Result;
use super::Config;
use crate::core::handle::Handle;
/// The port mihomo listens on when nothing else is configured.
pub const DEFAULT_MIXED_PORT: u16 = 7897;
/// A startup fallback for this session only; persistence would make the port climb across launches.
static SESSION_FALLBACK: AtomicU16 = AtomicU16::new(0);
+2 -1
View File
@@ -10,7 +10,8 @@ pub mod runtime;
pub(crate) mod snapshot;
mod verge;
pub use self::{clash::*, config::*, encrypt::*, mixed_port::*, prfitem::*, profiles::*, verge::*};
pub(crate) use self::config::{Config, ConfigType};
pub use self::{clash::*, encrypt::*, mixed_port::*, prfitem::*, profiles::*, verge::*};
pub const DEFAULT_PAC: &str = r#"function FindProxyForURL(url, host) {
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
+1 -9
View File
@@ -36,14 +36,6 @@ static STARTUP_CORE_BLOCK_REASON: Lazy<Mutex<Option<String>>> = Lazy::new(|| Mut
impl Config {
pub(crate) async fn resolve_startup_mixed_port() -> Result<bool> {
Self::resolve_startup_mixed_port_inner().await
}
pub(crate) async fn retry_startup_mixed_port_fallback() -> Result<bool> {
Self::resolve_startup_mixed_port_inner().await
}
async fn resolve_startup_mixed_port_inner() -> Result<bool> {
let _config_write = Self::lock_config_write().await;
let clash = Self::clash().await.latest_arc();
let verge = Self::verge().await.latest_arc();
@@ -287,7 +279,7 @@ fn configured_listener_ports(clash: &IClashTemp, verge: &IVerge) -> HashSet<u16>
}
}
if let Ok(controller) = SocketAddr::from_str(IClashTemp::guard_external_controller(&clash.0).as_str()) {
if let Ok(controller) = SocketAddr::from_str(IClashTemp::guard_server_ctrl(&clash.0).as_str()) {
ports.insert(controller.port());
}
+37 -52
View File
@@ -104,7 +104,7 @@ pub struct PrfOption {
}
impl PrfOption {
pub fn merge(one: Option<&Self>, other: Option<&Self>) -> Option<Self> {
pub(crate) fn merge(one: Option<&Self>, other: Option<&Self>) -> Option<Self> {
match (one, other) {
(Some(a_ref), Some(b_ref)) => {
let mut result = a_ref.clone();
@@ -132,11 +132,7 @@ impl PrfOption {
impl PrfItem {
/// Builds an item from a partial value that must include `itype`.
pub async fn from(item: &Self, file_data: Option<String>) -> Result<Self> {
if item.itype.is_none() {
bail!("type should not be null");
}
pub(super) async fn from(item: &Self, file_data: Option<String>) -> Result<Self> {
let itype = item
.itype
.as_ref()
@@ -162,7 +158,7 @@ impl PrfItem {
}
}
pub async fn from_local(
async fn from_local(
name: String,
desc: String,
file_data: Option<String>,
@@ -179,27 +175,27 @@ impl PrfItem {
let mut groups = opt_ref.and_then(|o| o.groups.clone());
if merge.is_none() {
let merge_item = &mut Self::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 Self::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 Self::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 Self::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 Self::from_groups()?;
let groups_item = &mut Self::from_groups();
profiles::profiles_append_item_safe(groups_item).await?;
groups = groups_item.uid.clone();
}
@@ -227,7 +223,7 @@ impl PrfItem {
})
}
pub async fn from_url(
pub(crate) async fn from_url(
url: &str,
name: Option<&String>,
desc: Option<&String>,
@@ -257,7 +253,7 @@ impl PrfItem {
let url = fix_dirty_url(url)?;
let resp = match NetworkManager::new()
.get_with_interrupt(
.get(
url.as_str(),
proxy_type,
Some(timeout),
@@ -349,7 +345,7 @@ impl PrfItem {
let name = name
.map(|s| s.to_owned())
.unwrap_or_else(|| filename.map(|s| s.into()).unwrap_or_else(|| "Remote File".into()));
let data = resp.text_with_charset()?;
let data = resp.text();
let data = data.trim_start_matches('\u{feff}');
@@ -360,27 +356,27 @@ impl PrfItem {
}
if merge.is_none() {
let merge_item = &mut Self::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 Self::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 Self::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 Self::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 Self::from_groups()?;
let groups_item = &mut Self::from_groups();
profiles::profiles_append_item_safe(groups_item).await?;
groups = groups_item.uid.clone();
}
@@ -410,7 +406,7 @@ impl PrfItem {
})
}
pub fn from_merge(uid: Option<String>) -> Result<Self> {
pub(super) fn from_merge(uid: Option<String>) -> Self {
let (id, template) = if let Some(uid) = uid {
(uid, tmpl::ITEM_MERGE.into())
} else {
@@ -418,76 +414,76 @@ impl PrfItem {
};
let file = format!("{id}.yaml").into();
Ok(Self {
Self {
uid: Some(id),
itype: Some("merge".into()),
file: Some(file),
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(template),
..Default::default()
})
}
}
pub fn from_script(uid: Option<String>) -> Result<Self> {
pub(super) fn from_script(uid: Option<String>) -> Self {
let id = if let Some(uid) = uid {
uid
} else {
help::get_uid("s").into()
};
let file = format!("{id}.js").into(); // js ext
Ok(Self {
Self {
uid: Some(id),
itype: Some("script".into()),
file: Some(file),
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_SCRIPT.into()),
..Default::default()
})
}
}
pub fn from_rules() -> Result<Self> {
fn from_rules() -> Self {
let uid = help::get_uid("r").into();
let file = format!("{uid}.yaml").into(); // yaml ext
Ok(Self {
Self {
uid: Some(uid),
itype: Some("rules".into()),
file: Some(file),
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_RULES.into()),
..Default::default()
})
}
}
pub fn from_proxies() -> Result<Self> {
fn from_proxies() -> Self {
let uid = help::get_uid("p").into();
let file = format!("{uid}.yaml").into(); // yaml ext
Ok(Self {
Self {
uid: Some(uid),
itype: Some("proxies".into()),
file: Some(file),
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_PROXIES.into()),
..Default::default()
})
}
}
pub fn from_groups() -> Result<Self> {
fn from_groups() -> Self {
let uid = help::get_uid("g").into();
let file = format!("{uid}.yaml").into(); // yaml ext
Ok(Self {
Self {
uid: Some(uid),
itype: Some("groups".into()),
file: Some(file),
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_GROUPS.into()),
..Default::default()
})
}
}
pub async fn read_file(&self) -> Result<String> {
pub(crate) async fn read_file(&self) -> Result<String> {
let file = self
.file
.as_ref()
@@ -498,37 +494,26 @@ impl PrfItem {
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
Ok(content.into())
}
pub async fn save_file(&self, data: String) -> Result<()> {
let file = self
.file
.as_ref()
.ok_or_else(|| anyhow::anyhow!("could not find the file"))?;
let path = dirs::app_profiles_dir()?.join(file.as_str());
fs::write(path, data.as_bytes())
.await
.context("failed to save the file")
}
}
impl PrfItem {
pub fn current_merge(&self) -> Option<&String> {
pub(crate) fn current_merge(&self) -> Option<&String> {
self.option.as_ref().and_then(|o| o.merge.as_ref())
}
pub fn current_script(&self) -> Option<&String> {
pub(crate) fn current_script(&self) -> Option<&String> {
self.option.as_ref().and_then(|o| o.script.as_ref())
}
pub fn current_rules(&self) -> Option<&String> {
pub(crate) fn current_rules(&self) -> Option<&String> {
self.option.as_ref().and_then(|o| o.rules.as_ref())
}
pub fn current_proxies(&self) -> Option<&String> {
pub(crate) fn current_proxies(&self) -> Option<&String> {
self.option.as_ref().and_then(|o| o.proxies.as_ref())
}
pub fn current_groups(&self) -> Option<&String> {
pub(crate) fn current_groups(&self) -> Option<&String> {
self.option.as_ref().and_then(|o| o.groups.as_ref())
}
}
+79 -90
View File
@@ -63,14 +63,6 @@ pub struct IProfilePreview<'a> {
pub is_current: bool,
}
/// 清理结果
#[derive(Debug, Clone)]
pub struct CleanupResult {
pub total_files: usize,
pub deleted_files: usize,
pub failed_deletions: usize,
}
pub(crate) struct ProfileDeletePlan {
files: Vec<String>,
}
@@ -135,7 +127,7 @@ impl IProfiles {
}
pub async fn save_file(&self) -> Result<()> {
help::save_yaml_atomic(&dirs::profiles_path()?, self, Some("# Profiles Config for Clash Verge")).await
help::save_yaml(&dirs::profiles_path()?, self, Some("# Profiles Config for Clash Verge")).await
}
/// 只修改current,valid和chain
@@ -154,14 +146,6 @@ impl IProfiles {
}
}
pub const fn get_current(&self) -> Option<&String> {
self.current.as_ref()
}
pub const fn get_items(&self) -> Option<&Vec<PrfItem>> {
self.items.as_ref()
}
pub fn get_item(&self, uid: impl AsRef<str>) -> Result<&PrfItem> {
let uid_str = uid.as_ref();
@@ -178,7 +162,7 @@ impl IProfiles {
bail!("failed to get the profile item \"uid:{}\"", uid_str);
}
pub async fn append_item(&mut self, item: &mut PrfItem) -> Result<()> {
async fn append_item(&mut self, item: &mut PrfItem) -> Result<()> {
let uid = &item.uid;
if uid.is_none() {
bail!("the uid should not be null");
@@ -215,31 +199,23 @@ impl IProfiles {
Ok(())
}
pub async fn reorder(&mut self, active_id: &String, over_id: &String) -> Result<()> {
let mut items = self.items.take().unwrap_or_default();
let mut old_index = None;
let mut new_index = None;
for (i, _) in items.iter().enumerate() {
if items[i].uid.as_ref() == Some(active_id) {
old_index = Some(i);
}
if items[i].uid.as_ref() == Some(over_id) {
new_index = Some(i);
}
async fn reorder(&mut self, active_id: &str, over_id: &str) -> Result<()> {
{
let Some(items) = self.items.as_mut() else {
return Ok(());
};
let old_index = items.iter().rposition(|item| item.uid.as_deref() == Some(active_id));
let new_index = items.iter().rposition(|item| item.uid.as_deref() == Some(over_id));
let (Some(old_idx), Some(new_idx)) = (old_index, new_index) else {
return Ok(());
};
let item = items.remove(old_idx);
items.insert(new_idx, item);
}
let (old_idx, new_idx) = match (old_index, new_index) {
(Some(old), Some(new)) => (old, new),
_ => return Ok(()),
};
let item = items.remove(old_idx);
items.insert(new_idx, item);
self.items = Some(items);
self.save_file().await
}
pub async fn patch_item(&mut self, uid: &String, item: &PrfItem) -> Result<()> {
async fn patch_item(&mut self, uid: &str, item: &PrfItem) -> Result<()> {
if let Some(file) = &item.file {
Self::validate_profile_file(file)?;
}
@@ -247,7 +223,7 @@ impl IProfiles {
let mut items = self.items.take().unwrap_or_default();
for each in items.iter_mut() {
if each.uid.as_ref() == Some(uid) {
if each.uid.as_deref() == Some(uid) {
patch!(each, item, itype);
patch!(each, item, name);
patch!(each, item, desc);
@@ -284,7 +260,7 @@ impl IProfiles {
}
/// Updates fields returned by a remote profile refresh.
pub async fn update_item(&mut self, uid: &String, item: &mut PrfItem) -> Result<()> {
async fn update_item(&mut self, uid: &str, item: &mut PrfItem) -> Result<()> {
if self.items.is_none() {
self.items = Some(vec![]);
}
@@ -292,10 +268,8 @@ impl IProfiles {
let _ = self.get_item(uid)?;
if let Some(items) = self.items.as_mut() {
let some_uid = Some(uid.clone());
for each in items.iter_mut() {
if each.uid == some_uid {
if each.uid.as_deref() == Some(uid) {
each.extra = item.extra;
each.updated = item.updated;
each.home = item.home.to_owned();
@@ -343,8 +317,8 @@ impl IProfiles {
raised
}
pub(crate) fn plan_delete_item(&mut self, uid: &String) -> Result<(bool, ProfileDeletePlan)> {
let current = self.current.as_ref().unwrap_or(uid).clone();
pub(crate) fn plan_delete_item(&mut self, uid: &str) -> Result<(bool, ProfileDeletePlan)> {
let deleting_current = self.current.as_deref().is_none_or(|current| current == uid);
let delete_uids = self.get_item(uid)?.option.as_ref().map_or_else(Vec::new, |op| {
[
op.merge.clone(),
@@ -359,7 +333,7 @@ impl IProfiles {
let mut items = self.items.take().unwrap_or_default();
let mut files = Vec::new();
if let Some(file) = Self::take_item_file_by_uid(&mut items, Some(uid.as_str())) {
if let Some(file) = Self::take_item_file_by_uid(&mut items, Some(uid)) {
files.push(file);
}
@@ -369,7 +343,7 @@ impl IProfiles {
}
}
if current == *uid {
if deleting_current {
self.current = None;
for item in items.iter() {
if item.itype == Some("remote".into()) || item.itype == Some("local".into()) {
@@ -380,7 +354,7 @@ impl IProfiles {
}
self.items = Some(items);
Ok((current == *uid, ProfileDeletePlan { files }))
Ok((deleting_current, ProfileDeletePlan { files }))
}
/// 获取current指向的订阅内容
@@ -400,17 +374,13 @@ impl IProfiles {
}
}
pub fn is_current_profile_index(&self, index: &String) -> bool {
self.current.as_ref() == Some(index)
}
pub fn profiles_preview(&self) -> Option<Vec<IProfilePreview<'_>>> {
self.items.as_ref().map(|items| {
items
.iter()
.filter_map(|e| {
if let (Some(uid), Some(name)) = (e.uid.as_ref(), e.name.as_ref()) {
let is_current = self.is_current_profile_index(uid);
let is_current = self.current.as_ref() == Some(uid);
let preview = IProfilePreview { uid, name, is_current };
Some(preview)
} else {
@@ -421,17 +391,6 @@ impl IProfiles {
})
}
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.as_ref();
}
}
}
None
}
/// 以 app 中的 profile 列表为准,删除不再需要的文件
pub async fn cleanup_orphaned_files(&self) -> Result<()> {
let profiles_dir = dirs::app_profiles_dir()?;
@@ -495,19 +454,13 @@ impl IProfiles {
}
}
let result = CleanupResult {
total_files,
deleted_files,
failed_deletions,
};
logging!(
info,
Type::Config,
"Profile 文件清理完成: 总文件数={}, 删除文件数={}, 失败数={}",
result.total_files,
result.deleted_files,
result.failed_deletions
total_files,
deleted_files,
failed_deletions
);
Ok(())
@@ -581,15 +534,15 @@ impl IProfiles {
}
}
// These helpers avoid holding a parking_lot guard across `await`.
// These helpers serialize asynchronous operations against committed profile data.
use crate::config::Config;
pub async fn profiles_append_item_with_filedata_safe(item: &PrfItem, file_data: Option<String>) -> Result<()> {
pub(crate) async fn profiles_append_item_with_filedata_safe(item: &PrfItem, file_data: Option<String>) -> Result<()> {
let item = &mut PrfItem::from(item, file_data).await?;
profiles_append_item_safe(item).await
}
pub async fn profiles_append_item_safe(item: &mut PrfItem) -> Result<()> {
pub(crate) async fn profiles_append_item_safe(item: &mut PrfItem) -> Result<()> {
let profiles = Config::profiles().await;
profiles_append_item_to_safe(&profiles, item).await
}
@@ -603,7 +556,7 @@ pub(super) async fn profiles_append_item_to_safe(profiles: &Draft<IProfiles>, it
.await
}
pub async fn profiles_patch_item_safe(index: &String, item: &PrfItem) -> Result<()> {
pub(crate) async fn profiles_patch_item_safe(index: &str, item: &PrfItem) -> Result<()> {
Config::profiles()
.await
.with_data_modify(|mut profiles| async move {
@@ -613,7 +566,7 @@ pub async fn profiles_patch_item_safe(index: &String, item: &PrfItem) -> Result<
.await
}
pub async fn profiles_reorder_safe(active_id: &String, over_id: &String) -> Result<()> {
pub(crate) async fn profiles_reorder_safe(active_id: &str, over_id: &str) -> Result<()> {
Config::profiles()
.await
.with_data_modify(|mut profiles| async move {
@@ -623,7 +576,7 @@ pub async fn profiles_reorder_safe(active_id: &String, over_id: &String) -> Resu
.await
}
pub async fn profiles_save_file_safe() -> Result<()> {
pub(crate) async fn profiles_save_file_safe() -> Result<()> {
Config::profiles()
.await
.with_data_modify(|profiles| async move {
@@ -633,7 +586,7 @@ pub async fn profiles_save_file_safe() -> Result<()> {
.await
}
pub async fn profiles_draft_update_item_safe(index: &String, item: &mut PrfItem) -> Result<()> {
pub(crate) async fn profiles_update_item_safe(index: &str, item: &mut PrfItem) -> Result<()> {
Config::profiles()
.await
.with_data_modify(|mut profiles| async move {
@@ -765,7 +718,7 @@ fn reconcile_selected_nodes(
}
/// Cancels restoration so it cannot overwrite a newer selection or profile snapshot.
pub fn supersede_selected_activation() {
pub(crate) fn supersede_selected_activation() {
ACTIVATE_SELECTED_GENERATION.fetch_add(1, Ordering::AcqRel);
}
@@ -866,7 +819,7 @@ async fn update_tray_after_activation(generation: u64) {
}
/// Records a backend-made selection so the next core start restores it.
pub async fn record_selected_node(group_name: &str, node: &str) -> Result<()> {
pub(crate) async fn record_selected_node(group_name: &str, node: &str) -> Result<()> {
let group_name = String::from(group_name);
let node = String::from(node);
let recorded = Config::profiles()
@@ -918,7 +871,7 @@ fn remove_selected_node(selected: &mut Vec<PrfSelected>, group_name: &str) -> bo
selected.len() != original_len
}
pub async fn forget_selected_node(group_name: &str) -> Result<()> {
pub(crate) async fn forget_selected_node(group_name: &str) -> Result<()> {
let cleared = Config::profiles()
.await
.with_data_modify(move |mut profiles| async move {
@@ -1150,22 +1103,21 @@ async fn activate_selected_nodes_worker(
/// Whether an activation may also prune the records it cannot match.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SelectionRepair {
enum SelectionRepair {
/// Prune records after a fully loaded profile switch.
Prune,
/// Preserve records during startup while provider-backed groups may still be absent.
KeepRecords,
}
pub fn activate_selected_nodes() -> Result<()> {
pub(crate) fn activate_selected_nodes() {
// The first-pass signal is for callers that wait; a profile switch does not, and dropping
// the receiver simply makes the send a no-op.
drop(activate_selected_nodes_with(SelectionRepair::Prune));
Ok(())
}
/// Restores available selections before the system proxy points at the core, then retries the rest.
pub async fn restore_selected_nodes() {
pub(crate) async fn restore_selected_nodes() {
let first_pass = activate_selected_nodes_with(SelectionRepair::KeepRecords);
if tokio::time::timeout(SELECTED_NODES_FIRST_PASS_BUDGET, first_pass)
.await
@@ -1199,7 +1151,7 @@ fn activate_selected_nodes_with(repair: SelectionRepair) -> tokio::sync::oneshot
let result = async {
// A draft may be a profile switch that has not passed validation yet.
let profiles = Config::profiles().await.data_arc();
let current = profiles.get_current().context("no current profile running")?.clone();
let current = profiles.current.clone().context("no current profile running")?;
let selected = profiles
.get_item(&current)
.context("failed to get current profile")?
@@ -1262,6 +1214,43 @@ mod tests {
}
}
#[tokio::test]
async fn missing_reorder_ids_preserve_ordered_items() -> Result<()> {
let mut profiles = IProfiles {
current: Some("a".into()),
items: Some(vec![
PrfItem {
uid: Some("a".into()),
..PrfItem::default()
},
PrfItem {
uid: Some("b".into()),
..PrfItem::default()
},
PrfItem {
uid: Some("c".into()),
..PrfItem::default()
},
]),
};
let expected = Some(vec![Some("a"), Some("b"), Some("c")]);
profiles.reorder("missing", "b").await?;
let ordered_uids = profiles
.items
.as_ref()
.map(|items| items.iter().map(|item| item.uid.as_deref()).collect::<Vec<_>>());
assert_eq!(ordered_uids, expected);
profiles.reorder("a", "missing").await?;
let ordered_uids = profiles
.items
.as_ref()
.map(|items| items.iter().map(|item| item.uid.as_deref()).collect::<Vec<_>>());
assert_eq!(ordered_uids, expected);
Ok(())
}
#[test]
fn raising_intervals_only_touches_those_scheduled_too_often() {
let mut profiles = IProfiles {
@@ -1313,7 +1302,7 @@ mod tests {
]),
};
let (should_update, plan) = profiles.plan_delete_item(&"a".into())?;
let (should_update, plan) = profiles.plan_delete_item("a")?;
assert!(should_update);
assert_eq!(profiles.current.as_deref(), Some("b"));
-5
View File
@@ -16,11 +16,6 @@ pub struct IRuntime {
}
impl IRuntime {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn patch_config(&mut self, patch: &Mapping) {
let config = if let Some(config) = self.config.as_mut() {
+4 -5
View File
@@ -1,6 +1,7 @@
use crate::config::Config;
use crate::{
config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted},
constants::network,
utils::{dirs, help},
};
use anyhow::Result;
@@ -265,7 +266,7 @@ impl IVerge {
help::save_yaml(&config_path, &config, Some("# Clash Verge Config")).await?;
logging!(info, Type::Config, "配置文件修正完成,需要重新加载配置");
Self::reload_config_after_fix(config).await?;
Self::reload_config_after_fix(config).await;
} else {
logging!(info, Type::Config, "clash_core配置验证通过: {:?}", config.clash_core);
}
@@ -273,7 +274,7 @@ impl IVerge {
Ok(())
}
async fn reload_config_after_fix(updated_config: Self) -> Result<()> {
async fn reload_config_after_fix(updated_config: Self) {
logging!(
info,
Type::Config,
@@ -286,8 +287,6 @@ impl IVerge {
*d = updated_config;
});
config_draft.apply();
Ok(())
}
pub fn get_valid_clash_core(&self) -> String {
@@ -357,7 +356,7 @@ impl IVerge {
verge_tproxy_port: Some(7896),
#[cfg(target_os = "linux")]
verge_tproxy_enabled: Some(false),
verge_mixed_port: Some(7897),
verge_mixed_port: Some(network::ports::DEFAULT_MIXED),
verge_socks_port: Some(7898),
verge_socks_enabled: Some(false),
verge_port: Some(7899),
+1 -1
View File
@@ -263,7 +263,7 @@ impl CoreManager {
crate::config::Config::notify_startup_mixed_port_fallback();
return Err(start_error);
}
match crate::config::Config::retry_startup_mixed_port_fallback().await {
match crate::config::Config::resolve_startup_mixed_port().await {
Ok(true) => {
retries += 1;
logging!(
+3 -3
View File
@@ -153,7 +153,7 @@ impl Timer {
// Redundant with first_delay == ZERO, kept as the immediate path; mark_task_running
// no-ops whichever of the two arrives second.
let cur_timestamp = chrono::Local::now().timestamp();
if let Some(items) = Config::profiles().await.data_arc().get_items() {
if let Some(items) = Config::profiles().await.data_arc().items.as_ref() {
for item in items.iter() {
if let Some(option) = item.option.as_ref()
&& option.allow_auto_update.unwrap_or(true)
@@ -204,7 +204,7 @@ impl Timer {
}
async fn gen_map(&self) -> HashMap<String, TaskSchedule> {
if let Some(items) = Config::profiles().await.data_arc().get_items() {
if let Some(items) = Config::profiles().await.data_arc().items.as_ref() {
return Self::gen_map_from_items(items);
}
@@ -446,7 +446,7 @@ impl Timer {
let task_interval = *self.timer_map.read().get(uid)?;
let profiles = Config::profiles().await;
let profiles_guard = profiles.latest_arc();
let items = profiles_guard.get_items()?;
let items = profiles_guard.items.as_ref()?;
let profile = items.iter().find(|item| item.uid.as_deref() == Some(uid))?;
let updated = profile.updated.unwrap_or(0) as i64;
+1 -1
View File
@@ -294,7 +294,7 @@ impl Tray {
{
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
if let Some(current_profile_uid) = profiles.get_current()
if let Some(current_profile_uid) = profiles.current.as_ref()
&& let Ok(profile) = profiles.get_item(current_profile_uid)
{
current_profile_name = match &profile.name {
+3 -3
View File
@@ -54,21 +54,21 @@ impl AsyncChainItemFrom for Option<ChainItem> {
data: ChainType::Merge(help::read_mapping(&path).await.ok()?),
}),
"rules" => {
let seq_map = help::read_seq_map(&path).await.ok()?;
let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
Some(ChainItem {
uid,
data: ChainType::Rules(seq_map),
})
}
"proxies" => {
let seq_map = help::read_seq_map(&path).await.ok()?;
let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
Some(ChainItem {
uid,
data: ChainType::Proxies(seq_map),
})
}
"groups" => {
let seq_map = help::read_seq_map(&path).await.ok()?;
let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
Some(ChainItem {
uid,
data: ChainType::Groups(seq_map),
+1 -1
View File
@@ -158,7 +158,7 @@ async fn get_config_values() -> ConfigValues {
#[allow(clippy::cognitive_complexity)]
async fn collect_profile_items(profiles: &IProfiles) -> Result<ProfileItems> {
let current_profile_uid = match profiles.get_current().cloned() {
let current_profile_uid = match profiles.current.clone() {
Some(uid) => uid,
None => return Ok(ProfileItems::default()),
};
+3 -3
View File
@@ -185,13 +185,13 @@ async fn resolve_latest_version(alpha: bool) -> Result<(ProxyType, std::string::
for proxy in [ProxyType::Localhost, ProxyType::System, ProxyType::None] {
let attempt = NetworkManager::new()
.get_with_interrupt(&url, proxy, Some(VERSION_TIMEOUT_SECS), None, false)
.get(&url, proxy, Some(VERSION_TIMEOUT_SECS), None, false)
.await;
match attempt {
Ok(response) if response.status().is_success() => {
// An error page answered with 200 must not become a version, nor reach the URL.
let version = response.text_with_charset()?.trim().to_owned();
let version = response.text().trim().to_owned();
if !is_usable_version(&version) {
last_error = Some(anyhow!("{url} returned an unusable version"));
continue;
@@ -220,7 +220,7 @@ async fn download_package(proxy: ProxyType, alpha: bool, version: &str) -> Resul
logging!(info, Type::Core, "core upgrade: downloading {url}");
NetworkManager::new()
.get_bytes_with_interrupt(&url, proxy, Some(PACKAGE_TIMEOUT_SECS), MAX_PACKAGE_BYTES)
.get_bytes(&url, proxy, Some(PACKAGE_TIMEOUT_SECS), MAX_PACKAGE_BYTES)
.await
.with_context(|| format!("failed to download {url}"))
}
+8 -7
View File
@@ -1,6 +1,6 @@
use crate::{
cmd,
config::{Config, PrfItem, PrfOption, profiles::profiles_draft_update_item_safe},
config::{Config, PrfItem, PrfOption, profiles::profiles_update_item_safe},
core::{CoreManager, handle, tray, validate::ValidationOutcome},
utils::help::{mask_err, mask_url},
};
@@ -124,13 +124,14 @@ async fn perform_profile_update(
let mut merged_opt = PrfOption::merge(opt, option);
let is_current = {
let profiles = Config::profiles().await;
profiles.latest_arc().is_current_profile_index(uid)
profiles.latest_arc().current.as_ref() == Some(uid)
};
let profiles = Config::profiles().await;
let profiles_arc = profiles.latest_arc();
let profile_name = profiles_arc
.get_name_by_uid(uid)
.cloned()
.get_item(uid)
.ok()
.and_then(|item| item.name.clone())
.unwrap_or_else(|| String::from("UnKnown Profile"));
let mut last_err;
@@ -138,7 +139,7 @@ async fn perform_profile_update(
match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await {
Ok(mut item) => {
logging!(info, Type::Config, "[订阅更新] 更新订阅配置成功");
profiles_draft_update_item_safe(uid, &mut item).await?;
profiles_update_item_safe(uid, &mut item).await?;
return Ok(is_current);
}
Err(err) => {
@@ -158,7 +159,7 @@ async fn perform_profile_update(
match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await {
Ok(mut item) => {
logging!(info, Type::Config, "[订阅更新] 使用 Clash代理 更新订阅配置成功");
profiles_draft_update_item_safe(uid, &mut item).await?;
profiles_update_item_safe(uid, &mut item).await?;
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
drop(last_err);
return Ok(is_current);
@@ -180,7 +181,7 @@ async fn perform_profile_update(
match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await {
Ok(mut item) => {
logging!(info, Type::Config, "[订阅更新] 使用 系统代理 更新订阅配置成功");
profiles_draft_update_item_safe(uid, &mut item).await?;
profiles_update_item_safe(uid, &mut item).await?;
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
drop(last_err);
return Ok(is_current);
+2 -5
View File
@@ -80,10 +80,8 @@ mod app_init {
app.deep_link().on_open_url(|event| {
let urls = event.urls();
AsyncHandler::spawn(move || async move {
if let Some(url) = urls.first()
&& let Err(e) = resolve::resolve_scheme(url.as_ref()).await
{
logging!(error, Type::Setup, "Failed to resolve scheme: {}", e);
if let Some(url) = urls.first() {
resolve::resolve_scheme(url.as_ref()).await;
}
});
});
@@ -169,7 +167,6 @@ mod app_init {
cmd::forget_selected_node,
cmd::save_dns_config,
cmd::apply_dns_config,
cmd::check_dns_config_exists,
cmd::get_dns_config_content,
cmd::validate_dns_config,
cmd::get_clash_logs,
+7 -7
View File
@@ -3,7 +3,10 @@ use anyhow::Result;
use async_trait::async_trait;
use clash_verge_logging::{Type, logging};
use once_cell::sync::OnceCell;
use std::{fs, path::PathBuf};
use std::{
fs,
path::{Path, PathBuf},
};
use tauri::Manager as _;
#[cfg(not(feature = "verge-dev"))]
@@ -198,12 +201,9 @@ pub fn clash_latest_log() -> Result<PathBuf> {
}
}
pub fn path_to_str(path: &PathBuf) -> Result<&str> {
let path_str = path
.as_os_str()
.to_str()
.ok_or_else(|| anyhow::anyhow!("failed to get path from {:?}", path))?;
Ok(path_str)
pub fn path_to_str(path: &Path) -> Result<&str> {
path.to_str()
.ok_or_else(|| anyhow::anyhow!("failed to get path from {:?}", path))
}
pub fn get_encryption_key() -> Result<Vec<u8>> {
+53 -20
View File
@@ -1,15 +1,17 @@
use crate::{config::with_encryption, enhance::seq::SeqMap};
use crate::config::with_encryption;
use anyhow::{Context as _, Result, anyhow, bail};
use clash_verge_logging::{Type, logging};
use nanoid::nanoid;
use scopeguard::{ScopeGuard, guard};
use serde::{Serialize, de::DeserializeOwned};
use serde_yaml_ng::{Mapping, Value};
use std::{
path::{Path, PathBuf},
str::FromStr,
};
use tokio::io::AsyncWriteExt as _;
pub async fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
pub async fn read_yaml<T: DeserializeOwned>(path: &Path) -> Result<T> {
if !tokio::fs::try_exists(path).await.unwrap_or(false) {
bail!("file not found \"{}\"", path.display());
}
@@ -19,7 +21,7 @@ pub async fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
Ok(with_encryption(|| async { serde_yaml_ng::from_str::<T>(&yaml_str) }).await?)
}
pub async fn read_mapping(path: &PathBuf) -> Result<Mapping> {
pub async fn read_mapping(path: &Path) -> Result<Mapping> {
if !tokio::fs::try_exists(path).await.unwrap_or(false) {
bail!("file not found \"{}\"", path.display());
}
@@ -49,11 +51,7 @@ pub async fn read_mapping(path: &PathBuf) -> Result<Mapping> {
}
}
pub async fn read_seq_map(path: &PathBuf) -> Result<SeqMap> {
read_yaml(path).await
}
pub async fn save_yaml<T: Serialize + Sync>(path: &PathBuf, data: &T, prefix: Option<&str>) -> Result<()> {
pub async fn save_yaml<T: Serialize + Sync>(path: &Path, data: &T, prefix: Option<&str>) -> Result<()> {
let data_str = with_encryption(|| async { serde_yaml_ng::to_string(data) }).await?;
let yaml_str = match prefix {
@@ -61,20 +59,55 @@ pub async fn save_yaml<T: Serialize + Sync>(path: &PathBuf, data: &T, prefix: Op
None => data_str,
};
tokio::fs::write(path, yaml_str.as_bytes())
.await
.with_context(|| format!("failed to save file \"{}\"", path.display()))?;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
Ok(())
}
let (temporary, file) = loop {
let temporary = path.with_extension(format!("tmp-{}-{}", std::process::id(), nanoid!()));
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&temporary)
{
Ok(file) => break (temporary, file),
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(error) => {
return Err(error).with_context(|| {
format!(
"failed to create temporary file \"{}\" for \"{}\"",
temporary.display(),
path.display()
)
});
}
}
};
pub async fn save_yaml_atomic<T: Serialize + Sync>(path: &Path, data: &T, prefix: Option<&str>) -> Result<()> {
let temporary = path.with_extension("delete.tmp");
save_yaml(&temporary, data, prefix).await?;
if let Err(error) = super::server::replace_file_atomic(&temporary, path) {
let _ = tokio::fs::remove_file(&temporary).await;
return Err(error).with_context(|| format!("failed to replace file \"{}\"", path.display()));
let temporary = guard(temporary, |path| {
let _ = std::fs::remove_file(path);
});
let mut file = tokio::fs::File::from_std(file);
file.write_all(yaml_str.as_bytes())
.await
.with_context(|| format!("failed to write temporary file for \"{}\"", path.display()))?;
file.flush()
.await
.with_context(|| format!("failed to flush temporary file for \"{}\"", path.display()))?;
drop(file);
match tokio::fs::symlink_metadata(path).await {
Ok(metadata) if metadata.file_type().is_file() => {
tokio::fs::set_permissions(temporary.as_path(), metadata.permissions())
.await
.with_context(|| format!("failed to preserve permissions for \"{}\"", path.display()))?;
}
Ok(_) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => {
return Err(error).with_context(|| format!("failed to inspect file \"{}\"", path.display()));
}
}
std::fs::rename(temporary.as_path(), path)
.with_context(|| format!("failed to replace file \"{}\"", path.display()))?;
let _ = ScopeGuard::into_inner(temporary);
Ok(())
}
+1 -1
View File
@@ -7,7 +7,7 @@ pub mod init;
pub mod linux;
#[cfg(target_os = "macos")]
pub mod macos_launch_guard;
pub mod network;
pub(crate) mod network;
pub mod notification;
pub mod port;
pub mod resolve;
+6 -29
View File
@@ -18,7 +18,7 @@ pub struct HttpResponse {
}
impl HttpResponse {
pub const fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self {
const fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self {
Self { status, headers, body }
}
@@ -30,8 +30,8 @@ impl HttpResponse {
&self.headers
}
pub fn text_with_charset(&self) -> Result<&str> {
Ok(&self.body)
pub fn text(&self) -> &str {
&self.body
}
}
@@ -50,12 +50,6 @@ enum TlsRootMode {
pub struct NetworkManager;
impl Default for NetworkManager {
fn default() -> Self {
Self::new()
}
}
impl NetworkManager {
pub const fn new() -> Self {
Self
@@ -161,23 +155,6 @@ impl NetworkManager {
detail.contains("protocolversion") || detail.contains("protocol version")
}
pub async fn create_request(
&self,
proxy_type: ProxyType,
timeout_secs: Option<u64>,
user_agent: Option<String>,
accept_invalid_certs: bool,
) -> Result<Client> {
self.create_request_with_tls_mode(
proxy_type,
timeout_secs,
user_agent,
accept_invalid_certs,
TlsRootMode::PlatformVerifier,
)
.await
}
async fn get_with_tls_mode(
&self,
url: &str,
@@ -279,7 +256,7 @@ impl NetworkManager {
self.build_client(proxy_url, headers, accept_invalid_certs, timeout_secs, tls_root_mode)
}
pub async fn get_with_interrupt(
pub async fn get(
&self,
url: &str,
proxy_type: ProxyType,
@@ -355,8 +332,8 @@ impl NetworkManager {
Ok(body)
}
/// Downloads a binary body, mirroring the TLS fallback of [`Self::get_with_interrupt`].
pub async fn get_bytes_with_interrupt(
/// Downloads a binary body, mirroring the TLS fallback of [`Self::get`].
pub async fn get_bytes(
&self,
url: &str,
proxy_type: ProxyType,
+28 -33
View File
@@ -21,13 +21,13 @@ use clash_verge_logging::{Type, logging, logging_error};
use clash_verge_signal;
pub mod dns;
pub mod scheme;
pub mod window;
pub mod window_script;
mod scheme;
pub(crate) mod window;
mod window_script;
static RESOLVE_DONE: AtomicBool = AtomicBool::new(false);
pub fn init_work_dir_and_logger() -> anyhow::Result<()> {
pub(crate) fn init_work_dir_and_logger() -> anyhow::Result<()> {
AsyncHandler::block_on(async {
init_work_config().await;
logging!(info, Type::Setup, "Initializing logger");
@@ -36,16 +36,16 @@ pub fn init_work_dir_and_logger() -> anyhow::Result<()> {
})
}
pub fn resolve_setup_sync() {
pub(crate) fn resolve_setup_sync() {
AsyncHandler::spawn(|| async {
AsyncHandler::spawn_blocking(init_scheme);
AsyncHandler::spawn_blocking(init_embed_server);
AsyncHandler::spawn_blocking(server::embed_server);
#[cfg(target_os = "linux")]
AsyncHandler::spawn_blocking(watch_linux_theme_changed);
});
}
pub fn resolve_setup_async() {
pub(crate) fn resolve_setup_async() {
AsyncHandler::spawn(|| async {
logging!(info, Type::ClashVergeRev, "Version: {}", env!("CARGO_PKG_VERSION"));
@@ -100,21 +100,16 @@ pub async fn resolve_reset_async() -> Result<(), anyhow::Error> {
Ok(())
}
pub(super) fn init_scheme() {
fn init_scheme() {
logging_error!(Type::Setup, init::init_scheme());
}
pub async fn resolve_scheme(param: &str) -> Result<()> {
pub(crate) async fn resolve_scheme(param: &str) {
logging_error!(Type::Setup, scheme::resolve_scheme(param).await);
Ok(())
}
pub(super) fn init_embed_server() {
server::embed_server();
}
#[cfg(target_os = "linux")]
pub(crate) fn watch_linux_theme_changed() {
fn watch_linux_theme_changed() {
match dark_light::subscribe() {
Ok(watcher) => {
for mode in watcher.iter() {
@@ -137,28 +132,28 @@ pub(crate) fn watch_linux_theme_changed() {
}
}
pub(super) async fn init_resources() {
async fn init_resources() {
logging_error!(Type::Setup, init::init_resources().await);
}
pub(super) async fn init_startup_script() {
async fn init_startup_script() {
logging_error!(Type::Setup, init::startup_script().await);
}
pub(super) async fn init_timer() {
async fn init_timer() {
logging_error!(Type::Setup, Timer::global().init().await);
}
pub(super) async fn init_hotkey() {
async fn init_hotkey() {
let skip_register_hotkeys = !Config::verge().await.latest_arc().enable_global_hotkey.unwrap_or(true);
logging_error!(Type::Setup, Hotkey::global().init(skip_register_hotkeys).await);
}
pub(super) async fn init_auto_lightweight_boot() {
async fn init_auto_lightweight_boot() {
logging_error!(Type::Setup, auto_lightweight_boot().await);
}
pub(super) async fn init_auto_backup() {
async fn init_auto_backup() {
logging_error!(Type::Setup, AutoBackupManager::global().init().await);
}
@@ -184,37 +179,37 @@ async fn init_silent_updater() {
logging!(info, Type::Setup, "Silent updater initialized");
}
pub fn init_signal() {
pub(crate) fn init_signal() {
logging!(info, Type::Setup, "Initializing signal handlers...");
clash_verge_signal::register(feat::quit);
}
pub async fn init_work_config() {
async fn init_work_config() {
logging_error!(Type::Setup, init::init_config().await);
}
pub(super) async fn init_tray() {
async fn init_tray() {
logging_error!(Type::Setup, Tray::global().init().await);
}
pub(super) async fn init_verge_config() {
async fn init_verge_config() {
logging_error!(Type::Setup, Config::init_runtime_config().await);
}
pub(super) async fn init_verge_config_before_window() -> bool {
async fn init_verge_config_before_window() -> bool {
let result = Config::init_config_before_window().await;
let success = result.is_ok();
logging_error!(Type::Setup, result);
success
}
pub(super) async fn init_service_manager() {
async fn init_service_manager() {
clash_verge_service_ipc::set_config(Some(ServiceManager::config())).await;
SERVICE_MANAGER.detect_startup_status().await;
}
pub(super) async fn init_core_manager() -> bool {
async fn init_core_manager() -> bool {
match CoreManager::global().init().await {
Ok(initialized) => initialized,
Err(error) => {
@@ -224,27 +219,27 @@ pub(super) async fn init_core_manager() -> bool {
}
}
pub(super) async fn refresh_tray_menu() {
async fn refresh_tray_menu() {
logging_error!(Type::Setup, Tray::global().update_part().await);
}
pub(super) async fn init_window() {
async fn init_window() {
let is_silent_start = Config::verge().await.data_arc().enable_silent_start.unwrap_or(false);
WindowManager::create_window(!is_silent_start).await;
}
#[cfg(target_os = "macos")]
pub(super) async fn resolve_dock_show() {
async fn resolve_dock_show() {
let is_silent_start = Config::verge().await.data_arc().enable_silent_start.unwrap_or(false);
if is_silent_start {
Handle::global().set_activation_policy_accessory();
}
}
pub fn resolve_done() {
fn resolve_done() {
RESOLVE_DONE.store(true, Ordering::Release);
}
pub fn is_resolve_done() -> bool {
pub(crate) fn is_resolve_done() -> bool {
RESOLVE_DONE.load(Ordering::Acquire)
}
+1 -1
View File
@@ -130,7 +130,7 @@ async fn post_import_updates(uid: &String, had_current_profile: bool) {
false
} else {
let profiles = Config::profiles().await;
profiles.latest_arc().is_current_profile_index(uid)
profiles.latest_arc().current.as_ref() == Some(uid)
};
if should_update_core {
+3 -31
View File
@@ -6,7 +6,7 @@ use crate::{
utils::{dirs, window_manager::WindowManager},
};
use anyhow::{Context as _, Result, bail};
use clash_verge_logging::{Type, logging, logging_error};
use clash_verge_logging::{Type, logging};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use reqwest::ClientBuilder;
@@ -250,7 +250,7 @@ fn start_embedded_server(listener: tokio::net::TcpListener, token: String) {
));
}
AsyncHandler::spawn(|| async move {
logging_error!(Type::Setup, resolve::resolve_scheme(&query.param).await);
resolve::resolve_scheme(&query.param).await;
});
Ok::<_, warp::Rejection>(warp::reply::with_status("ok".to_string(), warp::http::StatusCode::OK))
});
@@ -426,35 +426,7 @@ fn write_instance_record(path: &Path, record: &InstanceRecord) -> Result<()> {
file.set_permissions(std::fs::Permissions::from_mode(0o600))?;
}
drop(file);
replace_file_atomic(&temporary, path).context("failed to replace singleton record")?;
Ok(())
}
#[cfg(not(windows))]
pub(crate) fn replace_file_atomic(source: &Path, destination: &Path) -> Result<()> {
std::fs::rename(source, destination)?;
Ok(())
}
#[cfg(windows)]
pub(crate) fn replace_file_atomic(source: &Path, destination: &Path) -> Result<()> {
use std::os::windows::ffi::OsStrExt as _;
use windows_sys::Win32::Storage::FileSystem::{MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH, MoveFileExW};
let mut source: Vec<u16> = source.as_os_str().encode_wide().collect();
source.push(0);
let mut destination: Vec<u16> = destination.as_os_str().encode_wide().collect();
destination.push(0);
if unsafe {
MoveFileExW(
source.as_ptr(),
destination.as_ptr(),
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
)
} == 0
{
return Err(std::io::Error::last_os_error().into());
}
std::fs::rename(&temporary, path).context("failed to replace singleton record")?;
Ok(())
}
+3 -24
View File
@@ -50,7 +50,7 @@ impl WindowManager {
handle::Handle::global().set_activation_policy_regular();
}
pub fn get_main_window_with_state() -> (Option<WebviewWindow<Wry>>, WindowState) {
fn get_main_window_with_state() -> (Option<WebviewWindow<Wry>>, WindowState) {
let Some(window) = Self::get_main_window() else {
return (None, WindowState::NotExist);
};
@@ -73,28 +73,7 @@ impl WindowManager {
}
pub fn get_main_window_state() -> WindowState {
match Self::get_main_window() {
Some(window) => {
let is_minimized = window.is_minimized().unwrap_or(false);
let is_visible = window.is_visible().unwrap_or(false);
let is_focused = window.is_focused().unwrap_or(false);
if is_minimized {
return WindowState::Minimized;
}
if !is_visible {
return WindowState::Hidden;
}
if is_focused {
WindowState::VisibleFocused
} else {
WindowState::VisibleUnfocused
}
}
None => WindowState::NotExist,
}
Self::get_main_window_with_state().1
}
pub fn get_main_window() -> Option<WebviewWindow<Wry>> {
@@ -262,7 +241,7 @@ impl WindowManager {
window.map(|w| w.is_focused().unwrap_or(false)).unwrap_or(false)
}
pub fn is_main_window_minimized(window: Option<&WebviewWindow<Wry>>) -> bool {
fn is_main_window_minimized(window: Option<&WebviewWindow<Wry>>) -> bool {
window.map(|w| w.is_minimized().unwrap_or(false)).unwrap_or(false)
}
+3 -4
View File
@@ -467,13 +467,12 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
const initDnsConfig = useCallback(async () => {
try {
const dnsConfigExists = await invoke<boolean>(
'check_dns_config_exists',
const dnsConfig = await invoke<string | null>(
'get_dns_config_content',
{},
)
if (dnsConfigExists) {
const dnsConfig = await invoke<string>('get_dns_config_content', {})
if (dnsConfig !== null) {
const config = yaml.load(dnsConfig) as any
updateValuesFromConfig(config)