//! Adapters between Run State and the real or scripted machine it observes. use anyhow::{Context as _, Result}; use super::health::{PendingAction, RunState}; use super::probe::ServiceVersionReply; /// Everything Run State needs from outside itself. pub trait RunStateEnv: Send + Sync + 'static { fn probe_service_version(&self) -> impl Future> + Send; /// Returns an error when installation evidence cannot be inspected, not when it is absent. fn trusted_install_evidence(&self) -> impl Future> + Send; fn is_elevated(&self) -> bool; /// Keeps the PAC endpoint derived from Running Mode. fn set_pac_available(&self, available: bool); fn publish(&self, state: &RunState); fn run_privileged(&self, action: PendingAction) -> Result<()>; } #[derive(Debug, Default, Clone, Copy)] pub struct RealEnv; impl RunStateEnv for RealEnv { async fn probe_service_version(&self) -> Result { // The IPC client would otherwise back off for about two minutes before failing. #[cfg(windows)] if let Some(reason) = tokio::task::spawn_blocking(crate::core::service::service_stop_reason) .await .context("service status probe did not finish")?? { anyhow::bail!(reason); } let response = clash_verge_service_ipc::get_version().await?; let core = if response.code == 0 && response.data.as_ref().is_some_and(|info| { info.supports_client( clash_verge_service_ipc::ProtocolVersion::current(), clash_verge_service_ipc::MIN_REQUIRED_SERVICE_REVISION, ) }) { let name = format!( "{}{}", crate::config::Config::verge().await.latest_arc().get_valid_clash_core(), std::env::consts::EXE_SUFFIX ); let status = clash_verge_service_ipc::inspect_installation(&[clash_verge_service_ipc::CoreRequirement { name: name.clone(), sha256: None, }]) .await?; status .cores .into_iter() .find(|core| core.name == name) .map(|core| core.availability) } else { None }; Ok(ServiceVersionReply { core, code: response.code, message: response.message, protocol: response.data, }) } async fn trusted_install_evidence(&self) -> Result { let registered = tokio::task::spawn_blocking(crate::core::service::trusted_service_evidence) .await .context("service registration probe did not finish")??; // A helper that outlived its registration is a broken Service, not an absent one. #[cfg(unix)] if !registered && let Err(error) = clash_verge_service_ipc::execution::check_sidecar_available().await { return Ok(error .downcast_ref::() .is_some()); } Ok(registered) } fn is_elevated(&self) -> bool { // Startup may read Run State before the app handle exists; fail closed instead of panicking. crate::APP_HANDLE .get() .is_some_and(tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin) } fn set_pac_available(&self, available: bool) { crate::utils::server::set_pac_available(available); } fn publish(&self, state: &RunState) { // Reconciliation must run before the app handle exists and must not block this transition. crate::process::AsyncHandler::spawn(|| async { crate::feat::reconcile_tun_availability().await; }); let Some(app_handle) = crate::APP_HANDLE.get() else { return; }; tauri_plugin_clash_verge_sysinfo::set_app_core_mode(app_handle, state.mode.to_string()); crate::core::handle::Handle::notify_run_state(&state.to_view()); } fn run_privileged(&self, action: PendingAction) -> Result<()> { crate::core::service::run_privileged_service_action(action) } } #[cfg(test)] pub use fake::FakeEnv; #[cfg(test)] mod fake { use anyhow::{Result, anyhow}; use clash_verge_service_ipc::ProtocolInfo; use parking_lot::Mutex; use super::{PendingAction, RunState, RunStateEnv, ServiceVersionReply}; use crate::core::manager::RunningMode; /// A fail-closed scripted machine that records outbound effects. #[derive(Debug)] pub struct FakeEnv { version_replies: Mutex>>, evidence: Result, elevated: bool, probe_count: Mutex, pac_available: Mutex>, published: Mutex>, privileged_outcome: Mutex>, privileged_actions: Mutex>, } impl Default for FakeEnv { fn default() -> Self { Self { version_replies: Mutex::new(Vec::new()), evidence: Ok(false), elevated: false, probe_count: Mutex::new(0), pac_available: Mutex::new(None), published: Mutex::new(Vec::new()), privileged_outcome: Mutex::new(Ok(())), privileged_actions: Mutex::new(Vec::new()), } } } impl FakeEnv { #[must_use] pub fn new() -> Self { Self::default() } #[must_use] pub fn service_ready(self) -> Self { self.with_evidence(true).always_replying(Ok(ServiceVersionReply { core: Some(clash_verge_service_ipc::CoreAvailability::Ready), code: 0, message: "ok".to_owned(), protocol: Some(ProtocolInfo::current()), })) } #[must_use] pub fn service_version_mismatch(self) -> Self { self.with_evidence(true).always_replying(Ok(ServiceVersionReply { core: Some(clash_verge_service_ipc::CoreAvailability::Ready), code: 0, message: "ok".to_owned(), protocol: None, })) } #[must_use] pub fn service_unreachable(self) -> Self { self.with_evidence(true) .always_replying(Err("ipc transport refused".to_owned())) } #[must_use] pub fn evidence_unavailable(mut self) -> Self { self.evidence = Err("registry probe failed".to_owned()); self } #[must_use] pub const fn elevated(mut self) -> Self { self.elevated = true; self } #[must_use] pub fn with_evidence(mut self, exists: bool) -> Self { self.evidence = Ok(exists); self } /// Queue replies consumed one per probe; the last one repeats once exhausted. #[must_use] pub fn replying(self, replies: Vec>) -> Self { *self.version_replies.lock() = replies; self } #[must_use] pub fn always_replying(self, reply: Result) -> Self { self.replying(vec![reply]) } #[must_use] pub fn probe_count(&self) -> usize { *self.probe_count.lock() } #[must_use] pub fn pac_available(&self) -> Option { *self.pac_available.lock() } #[must_use] pub fn published_modes(&self) -> Vec { self.published.lock().iter().map(|state| state.mode).collect() } #[must_use] pub fn published(&self) -> Vec { self.published.lock().clone() } #[must_use] pub fn privileged_operations_fail(self, reason: &str) -> Self { *self.privileged_outcome.lock() = Err(reason.to_owned()); self } #[must_use] pub fn privileged_actions(&self) -> Vec { self.privileged_actions.lock().clone() } } impl RunStateEnv for FakeEnv { async fn probe_service_version(&self) -> Result { *self.probe_count.lock() += 1; let mut replies = self.version_replies.lock(); let reply = if replies.len() > 1 { replies.remove(0) } else { replies .first() .cloned() .unwrap_or_else(|| Err("no service configured".to_owned())) }; reply.map_err(|error| anyhow!(error)) } async fn trusted_install_evidence(&self) -> Result { self.evidence.clone().map_err(|error| anyhow!(error)) } fn is_elevated(&self) -> bool { self.elevated } fn set_pac_available(&self, available: bool) { *self.pac_available.lock() = Some(available); } fn publish(&self, state: &RunState) { self.published.lock().push(state.clone()); } fn run_privileged(&self, action: PendingAction) -> Result<()> { self.privileged_actions.lock().push(action); self.privileged_outcome.lock().clone().map_err(|error| anyhow!(error)) } } }