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
+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)
}