Files
clash-verge-rev/src-tauri/src/cmd/clash.rs
T
Tunglies 8328e34208 refactor(backend): declarative effects (#8128)
* refactor(core): type notice statuses behind the frozen wire contract

* refactor(utils): unified retry and strategy-list helpers

* refactor(feat): declarative effect registry for verge and clash patches

* refactor(core): derive refresh announcements from transaction commit

* refactor(core): fold tray, hotkey, autostart updates into effect ensures

* refactor(utils): migrate remaining bounded retry loops

* docs: add src-tauri architecture guardrails

* refactor(core): coalesce burst refresh announcements

* refactor(core): complete bounded service retry migration

* ci(backend): add architecture ban gates

* test(feat): freeze representative effect registry mappings

* docs(backend): record dead wire contract entries

* chore(core): document watcher exit conditions

* fix(backend): preserve applied state across later failures

* fix(config): retain runtime after an applied core replacement

* refactor(backend): flatten refresh publication and retry policies

* docs(backend): focus guardrails on architecture ownership

* refactor(effects): use one execution order

* fix(config): publish retained runtime and reconcile proxy settings
2026-10-07 07:38:55 +00:00

239 lines
8.0 KiB
Rust

use super::{CmdResult, CommandFailure, WithErrorCode as _, proxy_aware_coded_error, proxy_aware_error};
use crate::core::notify::NoticeStatus;
use crate::feat;
use crate::utils::{dirs, yaml_emitter};
use crate::{
cmd::StringifyErr as _,
config::{ClashInfo, Config, profiles::profiles_save_file_safe},
constants,
core::{
CoreManager, handle,
validate::{CoreConfigValidator, ValidationOutcome},
},
};
use clash_verge_logging::{Type, logging, logging_error};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use tokio::fs;
#[tauri::command]
pub async fn copy_clash_env() -> CmdResult {
feat::copy_clash_env().await;
Ok(())
}
#[tauri::command]
pub async fn get_clash_info() -> CmdResult<ClashInfo> {
Ok(Config::clash().await.data_arc().get_client_info())
}
#[tauri::command]
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
feat::patch_clash(&payload)
.await
.map_err(|error| proxy_aware_coded_error(&error, "CLASH_CONFIG_UPDATE_FAILED"))
}
#[tauri::command]
pub async fn patch_clash_mode(payload: String) -> CmdResult {
feat::change_clash_mode(payload)
.await
.with_error_code("CLASH_MODE_UPDATE_FAILED")
}
/// Reads the saved mode without depending on strict mihomo `/configs` deserialization.
#[tauri::command]
pub async fn get_clash_mode() -> CmdResult<Option<String>> {
Ok(Config::clash().await.data_arc().get_mode().map(Into::into))
}
#[tauri::command]
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<CommandFailure>> {
logging!(info, Type::Config, "changing core to {clash_core}");
match CoreManager::global().change_core(&clash_core).await {
Ok(_) => {
logging_error!(Type::Core, profiles_save_file_safe().await);
match CoreManager::global().restart_core().await {
Ok(_) => {
handle::Handle::notice(NoticeStatus::ConfigCoreChangeSuccess, clash_core.as_str());
handle::Handle::refresh_clash();
Ok(None)
}
Err(err) => {
let failed = err.context("core changed but failed to restart");
let error_msg: String = format!("{failed:#}").into();
handle::Handle::notice(NoticeStatus::ConfigCoreChangeError, error_msg.as_str());
logging!(error, Type::Core, "core changed but failed to restart: {error_msg}");
Ok(Some(proxy_aware_coded_error(&failed, "CORE_CHANGE_FAILED")))
}
}
}
Err(err) => {
let error_msg: String = format!("{err:#}").into();
logging!(error, Type::Core, "failed to change core: {error_msg}");
handle::Handle::notice(NoticeStatus::ConfigCoreChangeError, error_msg.as_str());
Ok(Some(proxy_aware_coded_error(&err, "CORE_CHANGE_FAILED")))
}
}
}
#[tauri::command]
pub async fn restart_core() -> CmdResult {
logging_error!(Type::Core, profiles_save_file_safe().await);
let result = CoreManager::global()
.restart_core()
.await
.map_err(|error| proxy_aware_coded_error(&error, "CORE_RESTART_FAILED"));
if result.is_ok() {
handle::Handle::refresh_clash();
}
result
}
/// Replaces the managed core binary atomically instead of letting mihomo overwrite itself.
#[tauri::command]
pub async fn upgrade_clash_core(force: bool) -> CmdResult<feat::CoreUpgradeReport> {
let report = feat::upgrade_core(force).await.with_error_code("CORE_UPGRADE_FAILED")?;
if report.upgraded {
handle::Handle::refresh_clash();
}
Ok(report)
}
#[tauri::command]
pub async fn test_delay(url: String) -> CmdResult<u32> {
let result = match feat::test_delay(url).await {
Ok(delay) => delay,
Err(e) => {
logging!(error, Type::Cmd, "get clash delay failed: {e:#}");
10000u32
}
};
Ok(result)
}
#[tauri::command]
pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
use crate::utils::dirs;
use tokio::fs;
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
let yaml_str = yaml_emitter::to_mihomo_config_string(&dns_config).stringify_err()?;
fs::write(&dns_path, yaml_str).await.stringify_err()?;
logging!(info, Type::Config, "DNS config saved to {dns_path:?}");
Ok(())
}
#[tauri::command]
pub fn take_dns_override_notice() -> bool {
crate::config::dns::take_dns_override_notice()
}
#[tauri::command]
pub async fn set_dns_override(
profile_uid: String,
enabled: bool,
confirmation: Option<String>,
) -> CmdResult<feat::DnsOverrideOutcome> {
feat::set_dns_override(profile_uid, enabled, confirmation)
.await
.map_err(|error| proxy_aware_coded_error(&error, "DNS_OVERRIDE_UPDATE_FAILED"))
}
#[tauri::command]
pub async fn apply_dns_config(apply: bool) -> CmdResult {
if apply {
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
if !dns_path.exists() {
logging!(warn, Type::Config, "DNS config file not found: {}", dns_path.display());
return Err("DNS config file not found".into());
}
let dns_yaml = fs::read_to_string(&dns_path).await.stringify_err_log(|e| {
logging!(
error,
Type::Config,
"Failed to read DNS config {}: {e}",
dns_path.display()
);
})?;
let patch_config = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml).stringify_err_log(|e| {
logging!(
error,
Type::Config,
"Failed to parse DNS config {}: {e}",
dns_path.display()
);
})?;
let mut patch = serde_yaml_ng::Mapping::new();
patch.insert("dns".into(), patch_config.into());
Config::runtime().await.edit_draft(|d| {
d.patch_config(&patch);
});
CoreManager::global().update_config_checked().await.map_err(|error| {
logging!(error, Type::Config, "failed to apply the config with DNS: {error:#}");
proxy_aware_error(&error)
})?;
logging!(info, Type::Config, "DNS config successfully applied");
} else {
logging!(info, Type::Config, "DNS settings disabled, regenerating config");
CoreManager::global().update_config_checked().await.map_err(|error| {
logging!(error, Type::Config, "failed to apply the regenerated config: {error:#}");
proxy_aware_error(&error)
})?;
logging!(info, Type::Config, "Config regenerated successfully");
}
logging_error!(Type::Config, Config::sync_dns_override().await);
handle::Handle::refresh_clash();
Ok(())
}
#[tauri::command]
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);
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)),
}
}
#[tauri::command]
pub async fn validate_dns_config() -> CmdResult<ValidationOutcome> {
let app_dir = dirs::app_home_dir().stringify_err()?;
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
let dns_path_str = dns_path.to_str().unwrap_or_default();
if !dns_path.exists() {
return Ok(ValidationOutcome::invalid_from_message("DNS config file not found"));
}
// A fragment, not a runnable config; the merged result is validated on apply.
CoreConfigValidator::validate_config_file_outcome(dns_path_str, Some(true))
.await
.stringify_err()
}
#[tauri::command]
pub async fn get_clash_logs() -> CmdResult<Vec<std::string::String>> {
let logs = CoreManager::global().get_clash_logs().await.unwrap_or_default();
Ok(logs)
}