mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-08 19:22:44 +08:00
refactor: Rewrite and improve sysproxy logic (#7744)
* fix(cmd): preserve anyhow context in coded errors Use alternate error formatting so coded failures retain the full anyhow context chain without changing their wire format. * fix(macos): skip redundant proxy disable writes When both proxy modes are off, read macOS state first and skip networksetup if the OS is already clean. Update the guard target only after a successful write. * feat(proxy): classify system proxy failures Attach stable, actionable proxy failure codes to anyhow chains while preserving the original diagnostic causes. * feat(cmd): return structured command failures Replace encoded error strings with serializable code and detail fields, and update frontend notice consumers to handle the structured shape. * fix(scripts): use the js-yaml namespace import Import js-yaml through its ESM namespace so the i18n cleanup script can access load and dump. * feat(proxy): localize classified proxy failures Map privilege and direct-fallback codes to localized explanations, and let mapped causes outrank generic operation keys in notices. * fix(ui): bound notice text and render failure details Limit resolved notice text without truncating clipboard content, and normalize structured failures embedded in translation parameters. * feat(proxy): persist unseen proxy failures Store unresolved classified failures by code, notify the frontend when the table changes, and show each failure sequence once when a visible window can receive it. * feat(proxy): report tray and hotkey toggle failures Record classified failures from tray and hotkey proxy toggles so they appear when a visible window is available. * fix(ui): restore the proxy switch after failed toggles Refresh frontend proxy state after a failed toggle so the switch reflects the rolled-back configuration. * feat(proxy): stop the guard after repeated failures Report guard outcomes, stop after the failure threshold, and persist the classified failure for the frontend. * fix(macos): preserve proxy during service handoff Stop a sidecar being handed to the service without clearing proxy state that the new owner must inherit. * feat(macos): classify misplaced sidecar state Distinguish a refused sidecar proxy write from a machine that still needs the privileged service installed. * feat(ui): recover misplaced sidecar notices Offer a restart action for macOS proxy failures caused by a ready service while the core still runs as a sidecar. * feat(ui): install the service from proxy notices Let privilege notices install the macOS service, restart the core, and report the runtime mode that was actually reached. * feat(cmd): attach requested proxy operations Carry the requested proxy direction with structured command failures when the originating command knows it. * feat(sysproxy): drain guards before OS access Wait for the local proxy guard to stop before OS reads, service handoff, or final writes, and refuse transitions when it does not drain. * feat(sysproxy): recover partial proxy writes Use write progress to choose whether to restore the old guard or compensate with a disabled proxy state, while preserving actionable errors. * feat(sysproxy): skip already-applied proxy writes Compare macOS proxy snapshots with global, PAC, and disabled targets so matching state avoids redundant privileged writes. * feat(proxy): report background proxy failures Persist and surface classified proxy failures from lifecycle paths that have no direct caller awaiting the result. * feat(ui): move proxy recovery into a dialog Guide users through installing or switching to the service after a refused proxy write, and suppress duplicate toast reports. * fix(ui): close proxy dialog after service recovery Dismiss the recovery dialog once the core reaches service mode, even when the original proxy request remains pending. * feat(ui): retry proxy request after service recovery Replay the requested proxy direction after the core reaches service mode, while leaving incidental restores unchanged. * fix(proxy): refresh state after successful apply Publish the proxy refresh from the shared apply path after releasing the lifecycle lock, and remove the tray-specific duplicate. * feat(ui): show proxy recovery progress Display installation, restart, and proxy-apply stages while keeping the primary action spinner visible during recovery. * feat(ui): route unavailable TUN through service recovery Let the TUN switch request service recovery and restore its requested state after the core restarts in service mode. * fix(ui): avoid duplicate TUN recovery errors Roll back deferred TUN toggles without reporting an error, and publish configuration refreshes from the transaction commit path. * chore: update changelog & pinlock * fix(core): resolve platform-specific clippy warnings * fix(core): detect Windows wildcard TCP conflicts Windows permits a specific exclusive bind beside an existing non-exclusive wildcard listener. Consult the system TCP listener table so the probe reports that overlap as a conflict.
This commit is contained in:
@@ -24,7 +24,9 @@ pub async fn delete_local_backup(filename: String) -> CmdResult<()> {
|
||||
/// Restore local backup
|
||||
#[tauri::command]
|
||||
pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
|
||||
feat::restore_local_backup(filename).await.stringify_err()
|
||||
feat::restore_local_backup(filename)
|
||||
.await
|
||||
.map_err(|error| super::proxy_aware_error(&error))
|
||||
}
|
||||
|
||||
/// Import local backup into the app's backup directory
|
||||
|
||||
+20
-25
@@ -1,4 +1,4 @@
|
||||
use super::{CmdResult, WithErrorCode as _, coded_error};
|
||||
use super::{CmdResult, CommandFailure, WithErrorCode as _, proxy_aware_coded_error, proxy_aware_error};
|
||||
use crate::feat;
|
||||
use crate::utils::{dirs, yaml_emitter};
|
||||
use crate::{
|
||||
@@ -34,7 +34,7 @@ pub async fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
||||
feat::patch_clash(&payload)
|
||||
.await
|
||||
.with_error_code("CLASH_CONFIG_UPDATE_FAILED")
|
||||
.map_err(|error| proxy_aware_coded_error(&error, "CLASH_CONFIG_UPDATE_FAILED"))
|
||||
}
|
||||
|
||||
/// 修改Clash模式
|
||||
@@ -59,7 +59,7 @@ pub async fn get_clash_mode() -> CmdResult<Option<String>> {
|
||||
|
||||
/// 切换Clash核心
|
||||
#[tauri::command]
|
||||
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
|
||||
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 {
|
||||
@@ -75,18 +75,19 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
|
||||
Ok(None)
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg: String = format!("Core changed but failed to restart: {err}").into();
|
||||
let failed = err.context("core changed but failed to restart");
|
||||
let error_msg: String = format!("{failed:#}").into();
|
||||
handle::Handle::notice_message("config_core::change_error", error_msg.clone());
|
||||
logging!(error, Type::Core, "{error_msg}");
|
||||
Ok(Some(coded_error("CORE_CHANGE_FAILED", error_msg)))
|
||||
Ok(Some(proxy_aware_coded_error(&failed, "CORE_CHANGE_FAILED")))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg: String = err;
|
||||
let error_msg: String = format!("{err:#}").into();
|
||||
logging!(error, Type::Core, "failed to change core: {error_msg}");
|
||||
handle::Handle::notice_message("config_core::change_error", error_msg.clone());
|
||||
Ok(Some(coded_error("CORE_CHANGE_FAILED", error_msg)))
|
||||
handle::Handle::notice_message("config_core::change_error", error_msg);
|
||||
Ok(Some(proxy_aware_coded_error(&err, "CORE_CHANGE_FAILED")))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,7 +98,7 @@ pub async fn start_core() -> CmdResult {
|
||||
let result = CoreManager::global()
|
||||
.start_core()
|
||||
.await
|
||||
.with_error_code("CORE_START_FAILED");
|
||||
.map_err(|error| proxy_aware_coded_error(&error, "CORE_START_FAILED"));
|
||||
if result.is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
@@ -111,7 +112,7 @@ pub async fn stop_core() -> CmdResult {
|
||||
let result = CoreManager::global()
|
||||
.stop_core()
|
||||
.await
|
||||
.with_error_code("CORE_STOP_FAILED");
|
||||
.map_err(|error| proxy_aware_coded_error(&error, "CORE_STOP_FAILED"));
|
||||
if result.is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
@@ -125,7 +126,7 @@ pub async fn restart_core() -> CmdResult {
|
||||
let result = CoreManager::global()
|
||||
.restart_core()
|
||||
.await
|
||||
.with_error_code("CORE_RESTART_FAILED");
|
||||
.map_err(|error| proxy_aware_coded_error(&error, "CORE_RESTART_FAILED"));
|
||||
if result.is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
@@ -195,26 +196,20 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
});
|
||||
|
||||
// 应用新配置
|
||||
CoreManager::global()
|
||||
.update_config_checked()
|
||||
.await
|
||||
.stringify_err_log(|err| {
|
||||
let err = format!("Failed to apply config with DNS: {err}");
|
||||
logging!(error, Type::Config, "{err}");
|
||||
})?;
|
||||
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 {
|
||||
// 当关闭DNS设置时,重新生成配置(不加载DNS配置文件)
|
||||
logging!(info, Type::Config, "DNS settings disabled, regenerating config");
|
||||
|
||||
CoreManager::global()
|
||||
.update_config_checked()
|
||||
.await
|
||||
.stringify_err_log(|err| {
|
||||
let err = format!("Failed to apply regenerated config: {err}");
|
||||
logging!(error, Type::Config, "{err}");
|
||||
})?;
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -11,5 +11,7 @@ pub async fn probe_listener(request: ListenerProbe) -> CmdResult<ListenerProbeOu
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_proxy_ports(settings: ProxyPortSettings) -> CmdResult<SaveProxyPortsOutcome> {
|
||||
feat::save_proxy_ports(settings).await.stringify_err()
|
||||
feat::save_proxy_ports(settings)
|
||||
.await
|
||||
.map_err(|error| super::proxy_aware_error(&error))
|
||||
}
|
||||
|
||||
+240
-11
@@ -1,12 +1,94 @@
|
||||
use anyhow::Result;
|
||||
use smartstring::alias::String;
|
||||
|
||||
pub type CmdResult<T = ()> = Result<T, String>;
|
||||
pub type CmdResult<T = ()> = Result<T, CommandFailure>;
|
||||
|
||||
const USER_ERROR_PREFIX: &str = "CVR_ERROR:";
|
||||
/// Structured command failure returned to the frontend.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct CommandFailure {
|
||||
/// Stable frontend identifier, when classified.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub code: Option<std::string::String>,
|
||||
/// Human-readable diagnostic detail.
|
||||
pub detail: std::string::String,
|
||||
/// Requested operation when known by the command.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub operation: Option<crate::core::notification::FailedOperation>,
|
||||
}
|
||||
|
||||
pub fn coded_error(code: &str, detail: impl std::fmt::Display) -> String {
|
||||
format!("{USER_ERROR_PREFIX}{code}\n{detail}").into()
|
||||
impl CommandFailure {
|
||||
/// A failure with a stable code.
|
||||
pub fn coded(code: &str, detail: impl std::fmt::Display) -> Self {
|
||||
Self {
|
||||
code: Some(code.to_owned()),
|
||||
detail: detail.to_string(),
|
||||
operation: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A failure with nothing to classify it by.
|
||||
pub fn plain(detail: impl std::fmt::Display) -> Self {
|
||||
Self {
|
||||
code: None,
|
||||
detail: detail.to_string(),
|
||||
operation: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach the requested operation when known.
|
||||
#[must_use]
|
||||
pub const fn asking_for(mut self, operation: Option<crate::core::notification::FailedOperation>) -> Self {
|
||||
self.operation = operation;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Preserve plain-string failures without inventing a code.
|
||||
impl From<&str> for CommandFailure {
|
||||
fn from(detail: &str) -> Self {
|
||||
Self::plain(detail)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::String> for CommandFailure {
|
||||
fn from(detail: std::string::String) -> Self {
|
||||
Self {
|
||||
code: None,
|
||||
detail,
|
||||
operation: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for CommandFailure {
|
||||
fn from(detail: String) -> Self {
|
||||
Self::plain(detail)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CommandFailure {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.detail)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn coded_error(code: &str, detail: impl std::fmt::Display) -> CommandFailure {
|
||||
CommandFailure::coded(code, detail)
|
||||
}
|
||||
|
||||
/// Prefer an actionable proxy classification over the operation's fallback code.
|
||||
pub fn proxy_aware_coded_error(error: &anyhow::Error, fallback: &str) -> CommandFailure {
|
||||
let code =
|
||||
crate::core::proxy_control::SysproxyFailure::from_chain(error).map_or(fallback, |failure| failure.code());
|
||||
CommandFailure::coded(code, format_args!("{error:#}"))
|
||||
}
|
||||
|
||||
/// Preserve uncoded failures unless the chain contains a proxy classification.
|
||||
pub fn proxy_aware_error(error: &anyhow::Error) -> CommandFailure {
|
||||
match crate::core::proxy_control::SysproxyFailure::from_chain(error) {
|
||||
Some(failure) => CommandFailure::coded(failure.code(), format_args!("{error:#}")),
|
||||
None => CommandFailure::plain(format_args!("{error:#}")),
|
||||
}
|
||||
}
|
||||
|
||||
// Command modules
|
||||
@@ -60,7 +142,7 @@ pub trait WithErrorCode<T> {
|
||||
|
||||
impl<T, E: std::fmt::Display> StringifyErr<T> for Result<T, E> {
|
||||
fn stringify_err(self) -> CmdResult<T> {
|
||||
self.map_err(|e| e.to_string().into())
|
||||
self.map_err(CommandFailure::plain)
|
||||
}
|
||||
|
||||
fn stringify_err_log<F>(self, log_fn: F) -> CmdResult<T>
|
||||
@@ -68,28 +150,175 @@ impl<T, E: std::fmt::Display> StringifyErr<T> for Result<T, E> {
|
||||
F: Fn(&str),
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let msg = String::from(e.to_string());
|
||||
log_fn(&msg);
|
||||
msg
|
||||
let failure = CommandFailure::plain(e);
|
||||
log_fn(&failure.detail);
|
||||
failure
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E: std::fmt::Display> WithErrorCode<T> for Result<T, E> {
|
||||
fn with_error_code(self, code: &str) -> CmdResult<T> {
|
||||
self.map_err(|error| coded_error(code, error))
|
||||
// Alternate formatting expands the full `anyhow` context chain.
|
||||
self.map_err(|error| CommandFailure::coded(code, format_args!("{error:#}")))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::coded_error;
|
||||
use super::{CommandFailure, WithErrorCode as _, coded_error, proxy_aware_coded_error, proxy_aware_error};
|
||||
use crate::core::proxy_control::SysproxyFailure;
|
||||
use anyhow::Context as _;
|
||||
|
||||
#[test]
|
||||
fn coded_error_preserves_stable_code_and_diagnostic_detail() {
|
||||
assert_eq!(
|
||||
coded_error("CORE_RESTART_FAILED", "connection refused"),
|
||||
"CVR_ERROR:CORE_RESTART_FAILED\nconnection refused"
|
||||
CommandFailure {
|
||||
code: Some("CORE_RESTART_FAILED".to_owned()),
|
||||
detail: "connection refused".to_owned(),
|
||||
operation: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_error_code_preserves_whole_anyhow_context_chain() {
|
||||
let source: anyhow::Result<()> = Err(anyhow::anyhow!("connection refused"))
|
||||
.context("failed to reach the mihomo core")
|
||||
.context("failed to restart the core");
|
||||
|
||||
let failure = source
|
||||
.with_error_code("CORE_RESTART_FAILED")
|
||||
.err()
|
||||
.unwrap_or_else(|| CommandFailure::plain("the call should have failed"));
|
||||
|
||||
assert_eq!(
|
||||
failure,
|
||||
CommandFailure {
|
||||
code: Some("CORE_RESTART_FAILED".to_owned()),
|
||||
detail: "failed to restart the core: failed to reach the mihomo core: connection refused".to_owned(),
|
||||
operation: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_error_code_leaves_plain_display_errors_untouched() {
|
||||
let source: Result<(), &str> = Err("plain failure");
|
||||
|
||||
let failure = source
|
||||
.with_error_code("PROFILE_READ_FAILED")
|
||||
.err()
|
||||
.unwrap_or_else(|| CommandFailure::plain("the call should have failed"));
|
||||
|
||||
assert_eq!(
|
||||
failure,
|
||||
CommandFailure {
|
||||
code: Some("PROFILE_READ_FAILED".to_owned()),
|
||||
detail: "plain failure".to_owned(),
|
||||
operation: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_classification_outranks_the_code_naming_the_operation() {
|
||||
let failure = anyhow::Error::new(sysproxy::Error::RequiresAdminPrivileges)
|
||||
.context(SysproxyFailure::PrivilegeRequired)
|
||||
.context("failed to restart the core");
|
||||
|
||||
let reported = proxy_aware_coded_error(&failure, "CORE_RESTART_FAILED");
|
||||
|
||||
assert_eq!(reported.code.as_deref(), Some("SYSPROXY_PRIVILEGE_REQUIRED"));
|
||||
assert!(reported.detail.contains("failed to restart the core"), "{reported:?}");
|
||||
assert!(reported.detail.contains("admin privileges"), "{reported:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_a_classification_the_callers_code_is_used() {
|
||||
let failure = anyhow::anyhow!("connection refused").context("failed to start the core");
|
||||
|
||||
let reported = proxy_aware_coded_error(&failure, "CORE_START_FAILED");
|
||||
|
||||
assert_eq!(
|
||||
reported,
|
||||
CommandFailure {
|
||||
code: Some("CORE_START_FAILED".to_owned()),
|
||||
detail: "failed to start the core: connection refused".to_owned(),
|
||||
operation: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commands_that_report_plain_text_stay_uncoded_without_a_classification() {
|
||||
let failure = anyhow::anyhow!("disk full").context("failed to save the configuration");
|
||||
|
||||
let reported = proxy_aware_error(&failure);
|
||||
|
||||
assert_eq!(
|
||||
reported,
|
||||
CommandFailure {
|
||||
code: None,
|
||||
detail: "failed to save the configuration: disk full".to_owned(),
|
||||
operation: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commands_that_report_plain_text_still_carry_a_classification() {
|
||||
let failure = anyhow::Error::new(SysproxyFailure::DirectFallback {
|
||||
detail: "service could not set the proxy".to_owned(),
|
||||
})
|
||||
.context("failed to apply the verge patch");
|
||||
|
||||
let reported = proxy_aware_error(&failure);
|
||||
|
||||
assert_eq!(reported.code.as_deref(), Some("SYSPROXY_DIRECT_FALLBACK"));
|
||||
assert!(
|
||||
reported.detail.contains("failed to apply the verge patch"),
|
||||
"{reported:?}"
|
||||
);
|
||||
assert!(
|
||||
reported.detail.contains("service could not set the proxy"),
|
||||
"{reported:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_command_that_knows_the_request_can_say_which_way_it_went() {
|
||||
let asking_on = CommandFailure::coded("SYSPROXY_PRIVILEGE_REQUIRED", "refused")
|
||||
.asking_for(Some(crate::core::notification::FailedOperation::SystemProxyEnable));
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(asking_on).unwrap_or(serde_json::Value::Null),
|
||||
serde_json::json!({
|
||||
"code": "SYSPROXY_PRIVILEGE_REQUIRED",
|
||||
"detail": "refused",
|
||||
"operation": "systemProxyEnable",
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(CommandFailure::coded("CORE_START_FAILED", "no").asking_for(None))
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
serde_json::json!({ "code": "CORE_START_FAILED", "detail": "no" })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_serialised_shape_is_what_the_frontend_reads() {
|
||||
assert_eq!(
|
||||
serde_json::to_value(CommandFailure::coded("CORE_START_FAILED", "connection refused"))
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
serde_json::json!({ "code": "CORE_START_FAILED", "detail": "connection refused" })
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(CommandFailure::plain("disk full")).unwrap_or(serde_json::Value::Null),
|
||||
serde_json::json!({ "detail": "disk full" })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ async fn restore_original(
|
||||
file_path: &std::path::Path,
|
||||
original_content: &str,
|
||||
original_existed: bool,
|
||||
) -> Result<(), String> {
|
||||
) -> CmdResult<()> {
|
||||
if original_existed {
|
||||
fs::write(file_path, original_content).await.stringify_err()
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{CmdResult, WithErrorCode as _};
|
||||
use super::{CmdResult, proxy_aware_coded_error};
|
||||
use crate::core::{
|
||||
CoreManager,
|
||||
service::{SERVICE_MANAGER, ServiceStatus},
|
||||
@@ -11,12 +11,15 @@ async fn execute_service_operation_sync(status: ServiceStatus, error_code: &str)
|
||||
&status,
|
||||
ServiceStatus::ReinstallRequired | ServiceStatus::ForceReinstallRequired
|
||||
) {
|
||||
manager.controlled_stop_core_inner().await.with_error_code(error_code)?;
|
||||
manager
|
||||
.controlled_stop_core_inner()
|
||||
.await
|
||||
.map_err(|error| proxy_aware_coded_error(&error, error_code))?;
|
||||
}
|
||||
SERVICE_MANAGER
|
||||
.handle_service_status(status)
|
||||
.await
|
||||
.with_error_code(error_code)
|
||||
.map_err(|error| proxy_aware_coded_error(&error, error_code))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -29,7 +32,7 @@ pub async fn uninstall_service() -> CmdResult {
|
||||
CoreManager::global()
|
||||
.uninstall_service_and_start_sidecar()
|
||||
.await
|
||||
.with_error_code("SERVICE_UNINSTALL_FAILED")
|
||||
.map_err(|error| proxy_aware_coded_error(&error, "SERVICE_UNINSTALL_FAILED"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -47,5 +50,5 @@ pub async fn continue_with_sidecar() -> CmdResult {
|
||||
crate::core::CoreManager::global()
|
||||
.continue_with_sidecar()
|
||||
.await
|
||||
.with_error_code("SERVICE_SIDECAR_FAILED")
|
||||
.map_err(|error| proxy_aware_coded_error(&error, "SERVICE_SIDECAR_FAILED"))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use crate::core::runstate::{RUN_STATE, RunStateView};
|
||||
use crate::core::{
|
||||
notification::{self, PendingFailure},
|
||||
runstate::{RUN_STATE, RunStateView},
|
||||
};
|
||||
|
||||
/// The whole Run State in one call.
|
||||
///
|
||||
@@ -9,3 +12,9 @@ use crate::core::runstate::{RUN_STATE, RunStateView};
|
||||
pub async fn get_runtime_state() -> Result<RunStateView, String> {
|
||||
Ok(RUN_STATE.settled().await.to_view())
|
||||
}
|
||||
|
||||
/// Return unresolved failures without consuming them.
|
||||
#[tauri::command]
|
||||
pub async fn get_pending_failures() -> Vec<PendingFailure> {
|
||||
notification::pending_failures()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
use super::CmdResult;
|
||||
use crate::{cmd::StringifyErr as _, config::IVerge, feat};
|
||||
use super::{CmdResult, proxy_aware_error};
|
||||
use crate::{
|
||||
cmd::StringifyErr as _,
|
||||
config::IVerge,
|
||||
core::notification::{self, FailedOperation},
|
||||
feat,
|
||||
};
|
||||
use clash_verge_draft::SharedDraft;
|
||||
|
||||
/// 获取Verge配置
|
||||
@@ -11,5 +16,47 @@ pub async fn get_verge_config() -> CmdResult<SharedDraft<IVerge>> {
|
||||
/// 修改Verge配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
|
||||
feat::patch_verge(&payload, false).await.stringify_err()
|
||||
let operation = system_proxy_operation(&payload);
|
||||
let result = match operation {
|
||||
Some(operation) => notification::asking_for(operation, Box::pin(feat::patch_verge(&payload, false))).await,
|
||||
None => feat::patch_verge(&payload, false).await,
|
||||
};
|
||||
result.map_err(|error| proxy_aware_error(&error).asking_for(operation))
|
||||
}
|
||||
|
||||
/// Extract a system proxy operation from a Verge patch.
|
||||
const fn system_proxy_operation(payload: &IVerge) -> Option<FailedOperation> {
|
||||
match payload.enable_system_proxy {
|
||||
Some(true) => Some(FailedOperation::SystemProxyEnable),
|
||||
Some(false) => Some(FailedOperation::SystemProxyDisable),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::system_proxy_operation;
|
||||
use crate::{config::IVerge, core::notification::FailedOperation};
|
||||
|
||||
#[test]
|
||||
fn only_a_system_proxy_toggle_says_which_way_it_was_going() {
|
||||
let asking_on = IVerge {
|
||||
enable_system_proxy: Some(true),
|
||||
..IVerge::default()
|
||||
};
|
||||
let asking_off = IVerge {
|
||||
enable_system_proxy: Some(false),
|
||||
..IVerge::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
system_proxy_operation(&asking_on),
|
||||
Some(FailedOperation::SystemProxyEnable)
|
||||
);
|
||||
assert_eq!(
|
||||
system_proxy_operation(&asking_off),
|
||||
Some(FailedOperation::SystemProxyDisable)
|
||||
);
|
||||
assert_eq!(system_proxy_operation(&IVerge::default()), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,5 +46,7 @@ pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
/// 从 WebDAV 恢复备份文件
|
||||
#[tauri::command]
|
||||
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
feat::restore_webdav_backup(filename).await.stringify_err()
|
||||
feat::restore_webdav_backup(filename)
|
||||
.await
|
||||
.map_err(|error| super::proxy_aware_error(&error))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user