Files
clash-verge-rev/src-tauri/src/core/manager/lifecycle.rs
T

1588 lines
59 KiB
Rust

use super::{CoreManager, RunningMode};
use crate::config::{Config, IVerge};
use crate::core::handle::Handle;
use crate::core::manager::CLASH_LOGGER;
use crate::core::proxy_control::{self, SysproxyFailure};
use crate::core::service::{SERVICE_MANAGER, ServiceStatus};
use anyhow::Result;
use clash_verge_logging::{Type, logging};
use scopeguard::defer;
use smartstring::alias::String;
use std::path::Path;
#[cfg(target_os = "windows")]
use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin;
#[cfg(any(target_os = "windows", test))]
const fn should_wait_for_service(tun_enabled: bool, service_ready: bool, is_admin: bool) -> bool {
tun_enabled && !service_ready && !is_admin
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StartupDecision {
Service,
Sidecar,
Wait,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ProxyStopIntent {
Clear,
HandOverToService,
}
const fn proxy_stop_intent(is_macos: bool, running_mode: RunningMode, decision: StartupDecision) -> ProxyStopIntent {
match (is_macos, running_mode, decision) {
(true, RunningMode::Sidecar, StartupDecision::Service) => ProxyStopIntent::HandOverToService,
_ => ProxyStopIntent::Clear,
}
}
const fn startup_decision(status: &ServiceStatus, service_required: bool) -> StartupDecision {
match status {
ServiceStatus::Ready => StartupDecision::Service,
ServiceStatus::NotInstalled if !service_required => StartupDecision::Sidecar,
ServiceStatus::SidecarAllowed => StartupDecision::Sidecar,
ServiceStatus::Checking
| ServiceStatus::NotInstalled
| ServiceStatus::NeedsReinstall
| ServiceStatus::InstallRequired
| ServiceStatus::Unavailable(_) => StartupDecision::Wait,
ServiceStatus::UninstallRequired | ServiceStatus::ReinstallRequired | ServiceStatus::ForceReinstallRequired => {
StartupDecision::Wait
}
}
}
const fn can_allow_sidecar_for_session(running_mode: &RunningMode, service_status: &ServiceStatus) -> bool {
matches!(
(running_mode, service_status),
(
RunningMode::NotRunning,
ServiceStatus::NotInstalled
| ServiceStatus::NeedsReinstall
| ServiceStatus::InstallRequired
| ServiceStatus::Unavailable(_)
| ServiceStatus::SidecarAllowed
) | (
RunningMode::Sidecar,
ServiceStatus::InstallRequired | ServiceStatus::Unavailable(_) | ServiceStatus::SidecarAllowed
)
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ProxyRestoreExpectation {
mode: RunningMode,
core_readiness_generation: u64,
service_owner_generation: Option<u64>,
}
impl ProxyRestoreExpectation {
const fn capture(
mode: RunningMode,
core_readiness_generation: Option<u64>,
service_owner_generation: u64,
) -> Option<Self> {
let Some(core_readiness_generation) = core_readiness_generation else {
return None;
};
match mode {
RunningMode::Service => Some(Self {
mode,
core_readiness_generation,
service_owner_generation: Some(service_owner_generation),
}),
RunningMode::Sidecar => Some(Self {
mode,
core_readiness_generation,
service_owner_generation: None,
}),
RunningMode::NotRunning => None,
}
}
fn is_valid(
self,
current_mode: &RunningMode,
current_core_readiness_generation: Option<u64>,
current_service_owner_generation: u64,
) -> bool {
self.mode == *current_mode
&& Some(self.core_readiness_generation) == current_core_readiness_generation
&& self
.service_owner_generation
.is_none_or(|expected| expected == current_service_owner_generation)
}
}
async fn run_controlled_stop_transition<StopGuard, StopGuardFuture, Clear, ClearFuture, Stop, StopFuture>(
is_macos: bool,
running_mode: RunningMode,
proxy_intent: ProxyStopIntent,
stop_guard: StopGuard,
clear_proxy: Clear,
stop_core: Stop,
) -> Result<()>
where
StopGuard: FnOnce() -> StopGuardFuture,
StopGuardFuture: std::future::Future<Output = bool>,
Clear: FnOnce() -> ClearFuture,
ClearFuture: std::future::Future<Output = Result<()>>,
Stop: FnOnce() -> StopFuture,
StopFuture: std::future::Future<Output = Result<()>>,
{
match (running_mode, proxy_intent) {
(RunningMode::NotRunning, _) => {}
// A guard that will not stop must not block the core from stopping.
(RunningMode::Sidecar, ProxyStopIntent::HandOverToService) => warn_if_guard_lingers(stop_guard().await),
(RunningMode::Service, _) if is_macos => warn_if_guard_lingers(stop_guard().await),
(RunningMode::Service | RunningMode::Sidecar, _) => clear_proxy().await?,
}
stop_core().await
}
fn warn_if_guard_lingers(stopped: bool) {
if !stopped {
logging!(
warn,
Type::Core,
"the system proxy guard did not stop in time; stopping the core anyway"
);
}
}
async fn run_core_start_transition<Start, StartFuture, Ready, Apply, ApplyFuture>(
start_core: Start,
core_is_ready: Ready,
apply_proxy: Apply,
) -> Result<()>
where
Start: FnOnce() -> StartFuture,
StartFuture: std::future::Future<Output = Result<()>>,
Ready: FnOnce() -> bool,
Apply: FnOnce() -> ApplyFuture,
ApplyFuture: std::future::Future<Output = Result<()>>,
{
start_core().await?;
if core_is_ready() {
apply_proxy().await?;
}
Ok(())
}
async fn run_ready_core_start_transition<Start, StartFuture, Ready, Apply, ApplyFuture>(
start_core: Start,
core_is_ready: Ready,
apply_proxy: Apply,
) -> Result<()>
where
Start: FnOnce() -> StartFuture,
StartFuture: std::future::Future<Output = Result<()>>,
Ready: FnOnce() -> bool,
Apply: FnOnce() -> ApplyFuture,
ApplyFuture: std::future::Future<Output = Result<()>>,
{
start_core().await?;
if !core_is_ready() {
anyhow::bail!("core did not become ready after start");
}
apply_proxy().await
}
#[cfg(any(target_os = "windows", test))]
async fn run_service_start_with_sidecar_fallback<Start, StartFuture, Check, CheckFuture, Fallback, FallbackFuture>(
start_service: Start,
confirm_idle: Check,
start_sidecar: Fallback,
record_unavailable: impl FnOnce(std::string::String),
) -> Result<()>
where
Start: FnOnce() -> StartFuture,
StartFuture: std::future::Future<Output = Result<()>>,
Check: FnOnce() -> CheckFuture,
CheckFuture: std::future::Future<Output = Result<()>>,
Fallback: FnOnce(anyhow::Error) -> FallbackFuture,
FallbackFuture: std::future::Future<Output = Result<()>>,
{
let Err(error) = start_service().await else {
return Ok(());
};
let refusal = error.downcast_ref::<crate::core::service::ServiceStartRefusal>();
let location_refused = refusal.is_some_and(|refusal| {
refusal.code == clash_verge_service_ipc::ServiceErrorCode::InvalidInstallLocation as u16
});
if refusal.is_some() && !location_refused {
return Err(error);
}
// A lost start response can hide a running core; the shared check must resolve occupancy.
if let Err(probe_error) = confirm_idle().await {
let error = error.context(format!("Sidecar fallback was not safe: {probe_error:#}"));
record_unavailable(format!("{error:#}"));
return Err(error);
}
logging!(
warn,
Type::Core,
"Service could not start the core; falling back to Sidecar: {error:#}"
);
let reason = format!("{error:#}");
start_sidecar(error)
.await
.map_err(|error| error.context(format!("Sidecar fallback failed after Service failure: {reason}")))
}
async fn run_sidecar_termination_transition<Clear, ClearFuture, Terminate, TerminateFuture>(
clear_proxy: Clear,
terminate_sidecar: Terminate,
) -> Result<()>
where
Clear: FnOnce() -> ClearFuture,
ClearFuture: std::future::Future<Output = Result<()>>,
Terminate: FnOnce() -> TerminateFuture,
TerminateFuture: std::future::Future<Output = Result<()>>,
{
clear_proxy().await?;
terminate_sidecar().await
}
async fn run_service_config_replacement_transition<
StopGuard,
StopGuardFuture,
Clear,
ClearFuture,
Stop,
StopFuture,
Start,
StartFuture,
Ready,
Restore,
RestoreFuture,
>(
is_macos: bool,
stop_guard: StopGuard,
clear_proxy: Clear,
stop_core: Stop,
start_core: Start,
core_is_ready: Ready,
restore_proxy: Restore,
) -> Result<()>
where
StopGuard: FnOnce() -> StopGuardFuture,
StopGuardFuture: std::future::Future<Output = bool>,
Clear: FnOnce() -> ClearFuture,
ClearFuture: std::future::Future<Output = Result<()>>,
Stop: FnOnce() -> StopFuture,
StopFuture: std::future::Future<Output = Result<()>>,
Start: FnOnce() -> StartFuture,
StartFuture: std::future::Future<Output = Result<()>>,
Ready: FnOnce() -> bool,
Restore: FnOnce() -> RestoreFuture,
RestoreFuture: std::future::Future<Output = Result<()>>,
{
run_controlled_stop_transition(
is_macos,
RunningMode::Service,
ProxyStopIntent::Clear,
stop_guard,
clear_proxy,
stop_core,
)
.await?;
run_ready_core_start_transition(start_core, core_is_ready, restore_proxy).await
}
pub(super) async fn run_core_replacement_transition<Stop, StopFuture, Start, StartFuture, Restore, RestoreFuture>(
stop_core: Stop,
start_core: Start,
restore_proxy: Restore,
) -> Result<()>
where
Stop: FnOnce() -> StopFuture,
StopFuture: std::future::Future<Output = Result<()>>,
Start: FnOnce() -> StartFuture,
StartFuture: std::future::Future<Output = Result<()>>,
Restore: FnOnce() -> RestoreFuture,
RestoreFuture: std::future::Future<Output = Result<()>>,
{
stop_core().await?;
start_core().await?;
restore_proxy().await
}
async fn run_uninstall_transition<Uninstall, UninstallFuture, Prepare, PrepareFuture, Start, StartFuture>(
uninstall: Uninstall,
prepare_sidecar: Prepare,
start_sidecar: Start,
) -> Result<()>
where
Uninstall: FnOnce() -> UninstallFuture,
UninstallFuture: std::future::Future<Output = Result<()>>,
Prepare: FnOnce() -> PrepareFuture,
PrepareFuture: std::future::Future<Output = Result<()>>,
Start: FnOnce() -> StartFuture,
StartFuture: std::future::Future<Output = Result<()>>,
{
uninstall().await?;
prepare_sidecar().await?;
start_sidecar().await
}
#[cfg(target_os = "windows")]
enum HandoffOutcome {
NotReady,
Done,
Failed,
}
impl CoreManager {
async fn rollback_failed_start(&self) {
proxy_control::stop_guard().await;
self.core_stopped();
}
pub async fn start_core(&self) -> Result<()> {
if !self.try_start_config_update() {
anyhow::bail!("configuration update is already running");
}
defer! {
self.finish_config_update();
}
self.start_core_during_config_update().await
}
pub(crate) async fn start_core_during_config_update(&self) -> Result<()> {
let _life = self.lifecycle_lock.lock().await;
run_core_start_transition(
|| self.start_core_inner(),
|| {
!matches!(*self.get_running_mode(), RunningMode::NotRunning)
&& self.current_core_readiness_generation().is_some()
},
|| self.apply_proxy_after_start(),
)
.await
}
#[tracing::instrument(skip_all, level = "info", fields(status = tracing::field::Empty, tun_disabled = false, readiness_generation = tracing::field::Empty))]
pub async fn continue_with_sidecar(&self) -> Result<()> {
if !self.try_start_config_update() {
anyhow::bail!("configuration update is already running");
}
defer! {
self.finish_config_update();
}
// Lock order is config then lifecycle. disable_tun_and_persist commits the shared Verge
// draft without claiming it, so without this it can commit another transaction's staged patch.
let config_write = Config::lock_config_write().await;
let _life = self.lifecycle_lock.lock().await;
let status = SERVICE_MANAGER.current().await;
tracing::Span::current().record("status", tracing::field::debug(&status));
let mode = self.get_running_mode();
if !can_allow_sidecar_for_session(&mode, &status) {
anyhow::bail!("Sidecar continuation is not allowed from {mode:?} / {status:?}");
}
#[cfg(target_os = "windows")]
if matches!(*mode, RunningMode::NotRunning) {
clash_verge_service_ipc::execution::check_sidecar_available().await?;
}
if !matches!(status, ServiceStatus::SidecarAllowed) {
SERVICE_MANAGER.allow_sidecar_for_session()?;
}
// Settling on Sidecar is what makes the verdict final, so ask only once it is recorded.
// Elevation alone carries TUN on Sidecar; a Sidecar that cannot must write it off.
let prepared = async {
let tun_enabled = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false);
if crate::core::runstate::RUN_STATE
.state()
.tun_should_be_disabled(tun_enabled)
{
tracing::Span::current().record("tun_disabled", true);
Config::disable_tun_and_persist().await?;
}
Config::generate().await
}
.await;
drop(config_write);
if let Err(error) = prepared {
SERVICE_MANAGER.withdraw_sidecar_allowance();
return Err(error);
}
// Drop the guard last so an earlier failure cannot leave a running Sidecar unguarded.
proxy_control::stop_guard().await;
let result = async {
self.start_core_inner().await?;
if !matches!(*self.get_running_mode(), RunningMode::Sidecar)
|| self.current_core_readiness_generation().is_none()
{
anyhow::bail!("Sidecar did not become ready");
}
tracing::Span::current().record("readiness_generation", self.current_core_readiness_generation());
self.apply_proxy_after_start().await
}
.await;
if let Err(error) = result {
// Revoke only the failed Sidecar allowance; its startup says nothing about Service health.
SERVICE_MANAGER.withdraw_sidecar_allowance();
if let Err(cleanup_error) = self.rollback_failed_sidecar_transition().await {
return Err(proxy_control::rollback_failure(error, cleanup_error)
.context("failed to clear the proxy before rolling back"));
}
return Err(error);
}
Ok(())
}
#[tracing::instrument(skip_all, level = "info", fields(readiness_generation = tracing::field::Empty))]
pub async fn uninstall_service_and_start_sidecar(&self) -> Result<()> {
if !self.try_start_config_update() {
anyhow::bail!("configuration update is already running");
}
defer! {
self.finish_config_update();
}
// Lock order is config then lifecycle, for the unclaimed draft write below.
let _config_write = Config::lock_config_write().await;
let _life = self.lifecycle_lock.lock().await;
self.controlled_stop_core_inner().await?;
let result = run_uninstall_transition(
|| async {
SERVICE_MANAGER
.handle_service_status(ServiceStatus::UninstallRequired)
.await
},
|| async {
// Must be asked after the uninstall: until then the Service still makes TUN capable.
let tun_enabled = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false);
if crate::core::runstate::RUN_STATE
.state()
.tun_should_be_disabled(tun_enabled)
{
Config::disable_tun_and_persist().await?;
}
Config::generate().await
},
|| async {
self.start_core_inner().await?;
if !matches!(*self.get_running_mode(), RunningMode::Sidecar)
|| self.current_core_readiness_generation().is_none()
{
anyhow::bail!("Sidecar did not become ready after service uninstall");
}
tracing::Span::current().record("readiness_generation", self.current_core_readiness_generation());
self.apply_proxy_after_start().await
},
)
.await;
if let Err(error) = result {
if let Err(cleanup_error) = self.rollback_failed_sidecar_transition().await {
return Err(proxy_control::rollback_failure(error, cleanup_error)
.context("failed to clear the proxy before rolling back"));
}
return Err(error);
}
Ok(())
}
async fn rollback_failed_sidecar_transition(&self) -> Result<()> {
if matches!(*self.get_running_mode(), RunningMode::Sidecar) {
self.stop_sidecar_after_proxy_clear().await?;
}
self.rollback_failed_start().await;
Ok(())
}
async fn stop_sidecar_after_proxy_clear(&self) -> Result<()> {
run_sidecar_termination_transition(proxy_control::clear, || self.stop_core_by_sidecar_unprepared()).await
}
pub(super) async fn replace_service_core_with_config(&self, config_file: &Path) -> Result<()> {
run_service_config_replacement_transition(
cfg!(target_os = "macos"),
proxy_control::stop_guard,
proxy_control::clear,
|| self.stop_core_unprepared_inner(),
|| self.start_core_by_service_with_config(config_file),
|| {
matches!(*self.get_running_mode(), RunningMode::Service)
&& self.current_core_readiness_generation().is_some()
},
|| self.apply_proxy_after_start(),
)
.await
}
#[tracing::instrument(skip_all, level = "info", fields(mode = ?*self.get_running_mode(), readiness_generation = self.current_core_readiness_generation(), owner_generation = crate::core::service::owner_monitor_generation()))]
pub(crate) async fn apply_proxy_after_start(&self) -> Result<()> {
let expectation = ProxyRestoreExpectation::capture(
*self.get_running_mode(),
self.current_core_readiness_generation(),
crate::core::service::owner_monitor_generation(),
)
.ok_or_else(|| {
anyhow::anyhow!("cannot apply system proxy before core readiness").context(SysproxyFailure::CoreNotReady)
})?;
// At login the app usually beats the network. Leave the write to the network watcher
// rather than fail — only if the watcher is actually live; a user toggling while
// offline still fails fast.
#[cfg(target_os = "macos")]
if crate::core::network_watch::is_armed()
&& !crate::utils::resolve::is_resolve_done()
&& Config::verge()
.await
.latest_arc()
.enable_system_proxy
.unwrap_or_default()
&& !proxy_control::has_network_service().await
{
logging!(
info,
Type::Core,
"no network service yet; the system proxy is applied once one appears"
);
return Ok(());
}
proxy_control::apply().await?;
if !expectation.is_valid(
self.get_running_mode().as_ref(),
self.current_core_readiness_generation(),
crate::core::service::owner_monitor_generation(),
) {
let clear_result = proxy_control::clear().await;
proxy_control::stop_guard().await;
clear_result?;
return Err(anyhow::anyhow!("core readiness changed while applying system proxy")
.context(SysproxyFailure::CoreNotReady));
}
proxy_control::refresh_guard().await?;
if !expectation.is_valid(
self.get_running_mode().as_ref(),
self.current_core_readiness_generation(),
crate::core::service::owner_monitor_generation(),
) {
let clear_result = proxy_control::clear().await;
proxy_control::stop_guard().await;
clear_result?;
return Err(
anyhow::anyhow!("core readiness changed while refreshing the system proxy guard")
.context(SysproxyFailure::CoreNotReady),
);
}
// tell the window when a background apply lands
Handle::refresh_verge();
Ok(())
}
#[tracing::instrument(skip_all, level = "info", fields(decision = tracing::field::Empty))]
async fn start_core_inner(&self) -> Result<()> {
if Handle::global().is_exiting() {
return Ok(());
}
if !matches!(*self.get_running_mode(), RunningMode::NotRunning) {
logging!(
debug,
Type::Core,
"start_core called while a core is running; treated as no-op"
);
return Ok(());
}
let startup = self.prepare_startup().await;
tracing::Span::current().record("decision", tracing::field::debug(&startup));
if matches!(startup, StartupDecision::Wait) {
self.rollback_failed_start().await;
#[cfg(target_os = "windows")]
self.try_sidecar_after_service_unavailable().await?;
return Ok(());
}
if Handle::global().is_exiting() {
self.core_stopped();
return Ok(());
}
let result = match startup {
StartupDecision::Service => {
#[cfg(target_os = "windows")]
{
run_service_start_with_sidecar_fallback(
|| self.start_core_by_service(),
clash_verge_service_ipc::execution::check_sidecar_available,
|error| self.start_sidecar_after_service_failure(error),
|reason| {
crate::core::runstate::RUN_STATE
.observe(crate::core::runstate::ServiceHealth::Unavailable(reason));
},
)
.await
}
#[cfg(not(target_os = "windows"))]
self.start_core_by_service().await
}
StartupDecision::Sidecar => self.start_core_by_sidecar().await,
StartupDecision::Wait => Ok(()),
};
// A failed start must not leave a locally reported running mode.
if result.is_err() {
self.rollback_failed_start().await;
return result;
}
#[cfg(target_os = "windows")]
if matches!(*self.get_running_mode(), RunningMode::Sidecar) {
self.spawn_service_handoff_watcher().await;
}
result
}
#[cfg(target_os = "windows")]
async fn try_sidecar_after_service_unavailable(&self) -> Result<()> {
use crate::core::runstate::{RUN_STATE, ServiceHealth};
let state = RUN_STATE.state();
if state.pending.is_some() || state.op_in_flight || Handle::global().is_exiting() {
return Ok(());
}
let reason = match state.health {
ServiceHealth::Unavailable(reason) => reason,
ServiceHealth::VersionMismatch => "registered service is unavailable or incompatible".to_owned(),
_ => return Ok(()),
};
if let Err(error) = clash_verge_service_ipc::execution::check_sidecar_available().await {
logging!(
warn,
Type::Core,
"Service unavailable ({reason}); Sidecar fallback withheld: {error:#}"
);
return Ok(());
}
self.start_sidecar_after_service_failure(anyhow::anyhow!(reason)).await
}
#[cfg(target_os = "windows")]
async fn start_sidecar_after_service_failure(&self, error: anyhow::Error) -> Result<()> {
use crate::core::runstate::RUN_STATE;
if Handle::global().is_exiting() {
return Err(error.context("application exited before Sidecar fallback"));
}
let reason = format!("{error:#}");
RUN_STATE.allow_sidecar_after_service_refusal(reason.clone())?;
logging!(warn, Type::Core, "Starting Sidecar with Service unavailable: {reason}");
let result = self.start_core_by_sidecar().await;
if result.is_err() {
SERVICE_MANAGER.withdraw_sidecar_allowance();
self.rollback_failed_start().await;
} else {
crate::core::service::notify_service_fallback();
}
result.map_err(|error| error.context(format!("Service failure before Sidecar startup: {reason}")))
}
#[tracing::instrument(skip_all, level = "info", fields(mode = ?*self.get_running_mode()))]
pub async fn stop_core(&self) -> Result<()> {
let _life = self.lifecycle_lock.lock().await;
self.controlled_stop_core_inner().await
}
pub(crate) async fn controlled_stop_core_inner(&self) -> Result<()> {
self.controlled_stop_core_with_intent(ProxyStopIntent::Clear).await
}
async fn controlled_stop_core_with_intent(&self, proxy_intent: ProxyStopIntent) -> Result<()> {
let running_mode = *self.get_running_mode();
run_controlled_stop_transition(
cfg!(target_os = "macos"),
running_mode,
proxy_intent,
proxy_control::stop_guard,
proxy_control::clear,
|| self.stop_core_unprepared_inner(),
)
.await
}
async fn proxy_stop_intent(&self) -> ProxyStopIntent {
let running_mode = *self.get_running_mode();
if !cfg!(target_os = "macos") || !matches!(running_mode, RunningMode::Sidecar) {
return ProxyStopIntent::Clear;
}
proxy_stop_intent(cfg!(target_os = "macos"), running_mode, self.prepare_startup().await)
}
async fn stop_core_unprepared_inner(&self) -> Result<()> {
CLASH_LOGGER.clear_logs();
match *self.get_running_mode() {
RunningMode::Service => self.stop_core_by_service().await,
RunningMode::Sidecar => self.stop_core_by_sidecar_unprepared().await,
RunningMode::NotRunning => Ok(()),
}
}
#[tracing::instrument(skip_all, level = "info")]
pub async fn restart_core(&self) -> Result<()> {
if !self.try_start_config_update() {
anyhow::bail!("configuration update is already running");
}
defer! {
self.finish_config_update();
}
self.restart_core_during_config_update().await
}
pub(crate) async fn restart_core_during_config_update(&self) -> Result<()> {
let _life = self.lifecycle_lock.lock().await;
let proxy_intent = self.proxy_stop_intent().await;
run_core_replacement_transition(
|| self.controlled_stop_core_with_intent(proxy_intent),
|| async {
self.start_core_inner().await?;
if matches!(*self.get_running_mode(), RunningMode::NotRunning)
|| self.current_core_readiness_generation().is_none()
{
anyhow::bail!("core did not become ready after restart");
}
Ok(())
},
|| self.apply_proxy_after_start(),
)
.await
}
#[tracing::instrument(skip_all, level = "info", fields(core = %clash_core))]
pub async fn change_core(&self, clash_core: &String) -> Result<()> {
if !IVerge::VALID_CLASH_CORES.contains(&clash_core.as_str()) {
anyhow::bail!("invalid clash core: {clash_core}");
}
Config::verge().await.edit_draft(|d| {
d.clash_core = Some(clash_core.to_owned());
});
Config::verge().await.apply();
let verge_data = Config::verge().await.latest_arc();
verge_data.save_file().await?;
self.update_config_checked().await
}
async fn prepare_startup(&self) -> StartupDecision {
#[cfg(target_os = "windows")]
self.wait_for_service_if_needed().await;
let service_required = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false);
if service_required
&& matches!(SERVICE_MANAGER.current().await, ServiceStatus::NotInstalled)
&& SERVICE_MANAGER.require_install_for_session().is_err()
{
return StartupDecision::Wait;
}
startup_decision(&SERVICE_MANAGER.current().await, service_required)
}
#[cfg(target_os = "windows")]
async fn wait_for_service_if_needed(&self) {
use crate::{config::Config, constants::timing};
let tun_enabled = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false);
let service_ready = matches!(SERVICE_MANAGER.current().await, ServiceStatus::Ready);
let is_admin = is_current_app_handle_admin(Handle::app_handle());
if !should_wait_for_service(tun_enabled, service_ready, is_admin) {
if tun_enabled && !service_ready && is_admin {
logging!(
info,
Type::Core,
"service unavailable while app is elevated; starting sidecar immediately"
);
}
return;
}
if crate::core::runstate::RUN_STATE.state().service_usable() {
return;
}
// Failing to become ready is not fatal here: the caller falls back to Sidecar, and a
// transient transport failure must not overwrite the last confirmed install state.
let attempts = (timing::SERVICE_WAIT_MAX.as_millis() / timing::SERVICE_WAIT_INTERVAL.as_millis()) as usize;
let wait = crate::core::runstate::RUN_STATE.await_ready(attempts, timing::SERVICE_WAIT_INTERVAL);
let _ = tokio::time::timeout(timing::SERVICE_WAIT_MAX, wait).await;
}
#[cfg(target_os = "windows")]
async fn spawn_service_handoff_watcher(&self) {
use crate::constants::timing;
use crate::process::AsyncHandler;
use std::sync::atomic::Ordering;
use std::time::Instant;
// An accepted Sidecar is the user's decision for this session, not a wait for the Service.
let needs_service = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false)
&& !crate::core::runstate::RUN_STATE.state().sidecar_allowed;
if !needs_service {
return;
}
if self.handoff_watcher_running.swap(true, Ordering::AcqRel) {
return;
}
logging!(
info,
Type::Core,
"service not ready at startup; sidecar active, watching for handoff"
);
AsyncHandler::spawn(|| async move {
let manager = Self::global();
let started = Instant::now();
loop {
if started.elapsed() >= timing::SERVICE_HANDOFF_WINDOW {
logging!(
info,
Type::Core,
"service handoff window elapsed; staying in sidecar mode"
);
break;
}
tokio::time::sleep(timing::SERVICE_HANDOFF_INTERVAL).await;
if !matches!(*manager.get_running_mode(), RunningMode::Sidecar) {
break;
}
match manager.try_handoff_sidecar_to_service().await {
HandoffOutcome::Done => break,
HandoffOutcome::Failed => {
logging!(warn, Type::Core, "handoff attempt failed; staying in sidecar mode");
break;
}
HandoffOutcome::NotReady => {}
}
}
manager.handoff_watcher_running.store(false, Ordering::Release);
});
}
#[cfg(target_os = "windows")]
async fn refresh_service_readiness_for_handoff() -> bool {
if SERVICE_MANAGER.confirm_ready().await.is_err() {
return false;
}
crate::core::runstate::RUN_STATE.state().service_usable()
}
#[cfg(target_os = "windows")]
#[tracing::instrument(skip_all, level = "debug", fields(outcome = tracing::field::Empty))]
async fn try_handoff_sidecar_to_service(&self) -> HandoffOutcome {
// Before probing: a probe reply records an observation, which clears sidecar_allowed.
if crate::core::runstate::RUN_STATE.state().sidecar_allowed {
return HandoffOutcome::Done;
}
if !Self::refresh_service_readiness_for_handoff().await {
return HandoffOutcome::NotReady;
}
if !self.try_start_config_update() {
return HandoffOutcome::NotReady;
}
defer! {
self.finish_config_update();
}
// Lock order is config then lifecycle.
let _life = self.lifecycle_lock.lock().await;
if !matches!(*self.get_running_mode(), RunningMode::Sidecar)
|| !Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false)
|| crate::core::runstate::RUN_STATE.state().sidecar_allowed
{
return HandoffOutcome::Done;
}
logging!(
info,
Type::Core,
"service became ready; handing off from sidecar to service"
);
if let Err(error) = self.stop_sidecar_after_proxy_clear().await {
logging!(
error,
Type::Core,
"failed to clear proxy before sidecar handoff: {error:#}"
);
return HandoffOutcome::Failed;
}
let service_result = run_ready_core_start_transition(
|| self.start_core_by_service(),
|| {
matches!(*self.get_running_mode(), RunningMode::Service)
&& self.current_core_readiness_generation().is_some()
},
|| self.apply_proxy_after_start(),
)
.await;
let Err(service_error) = service_result else {
logging!(info, Type::Core, "handoff to service mode succeeded");
return HandoffOutcome::Done;
};
logging!(error, Type::Core, "handoff to service failed: {service_error:#}");
if let Err(cleanup_error) = self.controlled_stop_core_inner().await {
logging!(
error,
Type::Core,
"failed to stop service before sidecar fallback: {cleanup_error:#}"
);
return HandoffOutcome::Failed;
}
let sidecar_result = run_ready_core_start_transition(
|| self.start_core_by_sidecar(),
|| {
matches!(*self.get_running_mode(), RunningMode::Sidecar)
&& self.current_core_readiness_generation().is_some()
},
|| self.apply_proxy_after_start(),
)
.await;
if let Err(sidecar_error) = sidecar_result {
logging!(error, Type::Core, "sidecar fallback failed: {sidecar_error:#}");
if let Err(cleanup_error) = self.rollback_failed_sidecar_transition().await {
logging!(
error,
Type::Core,
"failed to clear proxy before sidecar rollback: {cleanup_error:#}"
);
}
} else {
logging!(
info,
Type::Core,
"sidecar fallback became ready and restored proxy state"
);
}
HandoffOutcome::Failed
}
}
#[cfg(test)]
mod tests {
use super::{
CoreManager, ProxyRestoreExpectation, ProxyStopIntent, StartupDecision, can_allow_sidecar_for_session,
run_controlled_stop_transition, run_core_replacement_transition, run_core_start_transition,
run_ready_core_start_transition, run_service_config_replacement_transition, run_sidecar_termination_transition,
run_uninstall_transition, should_wait_for_service, startup_decision,
};
use crate::core::{manager::RunningMode, service::ServiceStatus};
use parking_lot::Mutex;
use std::{
future,
sync::{
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
},
task::Poll,
};
use tokio::sync::{Barrier, Mutex as AsyncMutex};
struct FakeGuardCoordinator {
generation: AtomicU64,
operation_lock: AsyncMutex<()>,
}
impl FakeGuardCoordinator {
const fn new() -> Self {
Self {
generation: AtomicU64::new(1),
operation_lock: AsyncMutex::const_new(()),
}
}
async fn run_guard_request(&self, captured_generation: u64, calls: &Mutex<Vec<&'static str>>) -> bool {
let _operation = self.operation_lock.lock().await;
if self.generation.load(Ordering::Acquire) != captured_generation {
return false;
}
calls.lock().push("guard-reapply");
true
}
async fn stop_guard(&self, calls: &Mutex<Vec<&'static str>>) -> bool {
self.generation.fetch_add(1, Ordering::AcqRel);
let _operation = self.operation_lock.lock().await;
calls.lock().push("guard-stopped");
true
}
}
fn transition_step(
calls: &Arc<Mutex<Vec<&'static str>>>,
step: &'static str,
fail_at: Option<&'static str>,
) -> future::Ready<anyhow::Result<()>> {
calls.lock().push(step);
future::ready(if fail_at == Some(step) {
Err(anyhow::anyhow!("{step} failed"))
} else {
Ok(())
})
}
#[tokio::test]
async fn uninstall_transition_is_ordered_and_short_circuits_at_every_failure() {
let steps = ["uninstall", "prepare-sidecar", "start-sidecar"];
for fail_at in std::iter::once(None).chain(steps.map(Some)) {
let calls = Arc::new(Mutex::new(Vec::new()));
let result = run_uninstall_transition(
|| transition_step(&calls, steps[0], fail_at),
|| transition_step(&calls, steps[1], fail_at),
|| transition_step(&calls, steps[2], fail_at),
)
.await;
let expected_len = fail_at
.and_then(|failed| steps.iter().position(|step| *step == failed).map(|index| index + 1))
.unwrap_or(steps.len());
assert_eq!(&*calls.lock(), &steps[..expected_len]);
assert_eq!(result.is_err(), fail_at.is_some());
}
}
#[test]
fn only_a_macos_sidecar_on_its_way_to_the_service_leaves_the_proxy_alone() {
use super::proxy_stop_intent;
assert_eq!(
proxy_stop_intent(true, RunningMode::Sidecar, StartupDecision::Service),
ProxyStopIntent::HandOverToService
);
for (is_macos, mode, decision) in [
(true, RunningMode::Sidecar, StartupDecision::Sidecar),
(true, RunningMode::Sidecar, StartupDecision::Wait),
(true, RunningMode::Service, StartupDecision::Service),
(true, RunningMode::NotRunning, StartupDecision::Service),
(false, RunningMode::Sidecar, StartupDecision::Service),
] {
assert_eq!(
proxy_stop_intent(is_macos, mode, decision),
ProxyStopIntent::Clear,
"{is_macos} {mode:?} {decision:?}"
);
}
}
#[tokio::test]
async fn a_sidecar_handed_to_the_service_stops_its_guard_and_leaves_the_proxy_alone() {
let calls = Arc::new(Mutex::new(Vec::new()));
let result = run_controlled_stop_transition(
true,
RunningMode::Sidecar,
ProxyStopIntent::HandOverToService,
|| {
calls.lock().push("guard-stopped");
future::ready(true)
},
|| transition_step(&calls, "unexpected-proxy-clear", None),
|| transition_step(&calls, "core_stop", None),
)
.await;
assert!(result.is_ok());
assert_eq!(&*calls.lock(), &["guard-stopped", "core_stop"]);
}
#[tokio::test]
async fn a_guard_that_would_not_stop_does_not_block_the_core_from_stopping() {
for (mode, intent) in [
(RunningMode::Sidecar, ProxyStopIntent::HandOverToService),
(RunningMode::Service, ProxyStopIntent::Clear),
] {
let calls = Arc::new(Mutex::new(Vec::new()));
let result = run_controlled_stop_transition(
true,
mode,
intent,
|| future::ready(false),
|| transition_step(&calls, "proxy_clear", None),
|| transition_step(&calls, "core_stop", None),
)
.await;
assert!(result.is_ok(), "{mode:?} {intent:?}");
assert_eq!(&*calls.lock(), &["core_stop"], "{mode:?} {intent:?}");
}
}
#[tokio::test]
async fn an_ordinary_sidecar_stop_still_clears_even_off_macos() {
for is_macos in [true, false] {
let calls = Arc::new(Mutex::new(Vec::new()));
let result = run_controlled_stop_transition(
is_macos,
RunningMode::Sidecar,
ProxyStopIntent::Clear,
|| future::ready(true),
|| transition_step(&calls, "proxy_clear", None),
|| transition_step(&calls, "core_stop", None),
)
.await;
assert!(result.is_ok());
assert_eq!(&*calls.lock(), &["proxy_clear", "core_stop"]);
}
}
#[tokio::test]
async fn controlled_macos_service_stop_drains_guard_before_one_helper_transaction() {
let calls = Arc::new(Mutex::new(Vec::new()));
let result = run_controlled_stop_transition(
true,
RunningMode::Service,
ProxyStopIntent::Clear,
|| {
calls.lock().push("guard-stopped");
future::ready(true)
},
|| transition_step(&calls, "unexpected-proxy-clear", None),
|| transition_step(&calls, "service_stop_with_proxy_clear", None),
)
.await;
assert!(result.is_ok());
assert_eq!(&*calls.lock(), &["guard-stopped", "service_stop_with_proxy_clear"]);
}
#[tokio::test]
async fn failed_macos_service_stop_cannot_be_followed_by_a_stale_guard_write() {
let coordinator = Arc::new(FakeGuardCoordinator::new());
let calls = Arc::new(Mutex::new(Vec::new()));
let guard_started = Arc::new(Barrier::new(2));
let release_guard = Arc::new(Barrier::new(2));
let captured_generation = coordinator.generation.load(Ordering::Acquire);
let in_flight_guard = {
let coordinator = Arc::clone(&coordinator);
let calls = Arc::clone(&calls);
let guard_started = Arc::clone(&guard_started);
let release_guard = Arc::clone(&release_guard);
tokio::spawn(async move {
let _operation = coordinator.operation_lock.lock().await;
calls.lock().push("guard-started");
guard_started.wait().await;
release_guard.wait().await;
calls.lock().push("guard-finished");
})
};
guard_started.wait().await;
let mut stop = Box::pin(run_controlled_stop_transition(
true,
RunningMode::Service,
ProxyStopIntent::Clear,
|| coordinator.stop_guard(&calls),
|| transition_step(&calls, "unexpected-proxy-clear", None),
|| transition_step(&calls, "service-stop-failed", Some("service-stop-failed")),
));
assert!(matches!(futures::poll!(stop.as_mut()), Poll::Pending));
release_guard.wait().await;
assert!(in_flight_guard.await.is_ok());
assert!(stop.await.is_err());
assert!(!coordinator.run_guard_request(captured_generation, &calls).await);
assert_eq!(
&*calls.lock(),
&[
"guard-started",
"guard-finished",
"guard-stopped",
"service-stop-failed"
]
);
}
#[tokio::test]
async fn handoff_sidecar_cleanup_is_clear_gated_and_ordered() {
for fail_at in [None, Some("handoff-proxy-clear")] {
let calls = Arc::new(Mutex::new(Vec::new()));
let sidecar_alive = AtomicBool::new(true);
let result = run_sidecar_termination_transition(
|| transition_step(&calls, "handoff-proxy-clear", fail_at),
|| async {
sidecar_alive.store(false, Ordering::Release);
calls.lock().push("handoff-sidecar-stop");
Ok(())
},
)
.await;
if fail_at.is_some() {
assert!(result.is_err());
assert!(sidecar_alive.load(Ordering::Acquire));
assert_eq!(&*calls.lock(), &["handoff-proxy-clear"]);
} else {
assert!(result.is_ok());
assert!(!sidecar_alive.load(Ordering::Acquire));
assert_eq!(&*calls.lock(), &["handoff-proxy-clear", "handoff-sidecar-stop"]);
}
}
}
#[tokio::test]
async fn handoff_chosen_core_applies_proxy_only_after_required_readiness() {
let calls = Arc::new(Mutex::new(Vec::new()));
let result = run_ready_core_start_transition(
|| transition_step(&calls, "core-start", None),
|| {
calls.lock().push("core-ready");
true
},
|| transition_step(&calls, "proxy-apply", None),
)
.await;
assert!(result.is_ok());
assert_eq!(&*calls.lock(), &["core-start", "core-ready", "proxy-apply"]);
}
#[tokio::test]
async fn handoff_chosen_core_without_readiness_never_applies_proxy() {
let calls = Arc::new(Mutex::new(Vec::new()));
let result = run_ready_core_start_transition(
|| transition_step(&calls, "core-start", None),
|| {
calls.lock().push("core-not-ready");
false
},
|| transition_step(&calls, "unexpected-proxy-apply", None),
)
.await;
assert!(result.is_err());
assert_eq!(&*calls.lock(), &["core-start", "core-not-ready"]);
}
#[tokio::test]
async fn handoff_cleanup_failure_aborts_fallback_and_leaves_service_alive() {
let calls = Arc::new(Mutex::new(Vec::new()));
let service_alive = AtomicBool::new(true);
let result = run_controlled_stop_transition(
false,
RunningMode::Service,
ProxyStopIntent::Clear,
|| future::ready(true),
|| transition_step(&calls, "service-proxy-clear", Some("service-proxy-clear")),
|| {
service_alive.store(false, Ordering::Release);
transition_step(&calls, "service-stop", None)
},
)
.await;
assert!(result.is_err());
assert!(service_alive.load(Ordering::Acquire));
assert_eq!(&*calls.lock(), &["service-proxy-clear"]);
}
#[tokio::test]
async fn controlled_start_applies_proxy_only_after_core_is_ready() {
let calls = Arc::new(Mutex::new(Vec::new()));
let result = run_core_start_transition(
|| transition_step(&calls, "core_start", None),
|| true,
|| transition_step(&calls, "proxy_apply", None),
)
.await;
assert!(result.is_ok());
assert_eq!(&*calls.lock(), &["core_start", "proxy_apply"]);
}
#[tokio::test]
async fn controlled_start_does_not_apply_proxy_without_a_ready_core() {
let calls = Arc::new(Mutex::new(Vec::new()));
let result = run_core_start_transition(
|| transition_step(&calls, "core_start", None),
|| false,
|| transition_step(&calls, "proxy_apply", None),
)
.await;
assert!(result.is_ok());
assert_eq!(&*calls.lock(), &["core_start"]);
}
#[tokio::test]
async fn restart_start_failure_keeps_proxy_direct_guard_stopped_and_pac_unavailable() {
let proxy_is_direct = AtomicBool::new(false);
let guard_stopped = AtomicBool::new(false);
let pac_available = AtomicBool::new(true);
let restored_new_core = AtomicBool::new(false);
let result = run_core_replacement_transition(
|| {
proxy_is_direct.store(true, Ordering::Release);
guard_stopped.store(true, Ordering::Release);
pac_available.store(false, Ordering::Release);
future::ready(Ok(()))
},
|| future::ready(Err(anyhow::anyhow!("new core failed to start"))),
|| {
restored_new_core.store(true, Ordering::Release);
future::ready(Ok(()))
},
)
.await;
assert!(result.is_err());
assert!(proxy_is_direct.load(Ordering::Acquire));
assert!(guard_stopped.load(Ordering::Acquire));
assert!(!pac_available.load(Ordering::Acquire));
assert!(!restored_new_core.load(Ordering::Acquire));
}
#[tokio::test]
async fn service_config_replacement_is_controlled_and_generation_safe_on_every_platform() {
for is_macos in [false, true] {
let calls = Arc::new(Mutex::new(Vec::new()));
let manager = CoreManager::isolated();
let old_readiness_generation = manager.mark_core_ready();
manager.core_started(RunningMode::Service);
let owner_monitor_generation = AtomicU64::new(1);
let old_owner_monitor_generation = owner_monitor_generation.load(Ordering::Acquire);
let result = run_service_config_replacement_transition(
is_macos,
|| {
calls.lock().push("guard-stopped");
future::ready(true)
},
|| transition_step(&calls, "proxy-clear", None),
|| {
calls.lock().push("helper-stop");
owner_monitor_generation.fetch_add(1, Ordering::AcqRel);
manager.core_stopped();
future::ready(Ok(()))
},
|| {
calls.lock().push("service-start");
owner_monitor_generation.fetch_add(1, Ordering::AcqRel);
manager.mark_core_ready();
manager.core_started(RunningMode::Service);
if owner_monitor_generation
.compare_exchange(
old_owner_monitor_generation,
old_owner_monitor_generation.wrapping_add(1),
Ordering::AcqRel,
Ordering::Acquire,
)
.is_ok()
{
calls.lock().push("old-monitor-invalidated-readiness");
manager.invalidate_core_readiness();
} else {
calls.lock().push("old-monitor-rejected");
}
future::ready(Ok(()))
},
|| {
calls.lock().push("readiness-checked");
matches!(*manager.get_running_mode(), RunningMode::Service)
&& manager.current_core_readiness_generation().is_some()
&& manager.current_core_readiness_generation() != Some(old_readiness_generation)
},
|| transition_step(&calls, "proxy-apply", None),
)
.await;
assert!(result.is_ok());
let expected_preparation = if is_macos { "guard-stopped" } else { "proxy-clear" };
assert_eq!(
&*calls.lock(),
&[
expected_preparation,
"helper-stop",
"service-start",
"old-monitor-rejected",
"readiness-checked",
"proxy-apply",
]
);
}
}
#[test]
fn startup_waits_when_a_service_requirement_cannot_be_satisfied() {
assert_eq!(startup_decision(&ServiceStatus::Ready, true), StartupDecision::Service);
assert_eq!(
startup_decision(&ServiceStatus::NotInstalled, true),
StartupDecision::Wait
);
assert_eq!(
startup_decision(&ServiceStatus::NotInstalled, false),
StartupDecision::Sidecar
);
assert_eq!(
startup_decision(&ServiceStatus::SidecarAllowed, true),
StartupDecision::Sidecar
);
assert_eq!(
startup_decision(&ServiceStatus::NeedsReinstall, true),
StartupDecision::Wait
);
assert_eq!(
startup_decision(&ServiceStatus::Unavailable("broken".into()), true),
StartupDecision::Wait
);
assert_eq!(startup_decision(&ServiceStatus::Checking, true), StartupDecision::Wait);
}
#[test]
fn service_evidence_failures_never_silently_fall_back() {
assert_eq!(
startup_decision(&ServiceStatus::NeedsReinstall, false),
StartupDecision::Wait
);
assert_eq!(
startup_decision(&ServiceStatus::Unavailable("missing".into()), false),
StartupDecision::Wait
);
}
#[test]
fn service_wait_is_only_required_for_non_admin_tun() {
assert!(should_wait_for_service(true, false, false));
assert!(!should_wait_for_service(true, false, true));
assert!(!should_wait_for_service(true, true, false));
assert!(!should_wait_for_service(false, false, false));
}
#[test]
fn sidecar_session_allowance_is_cross_platform_and_explicit() {
let allowed_statuses = [
ServiceStatus::NotInstalled,
ServiceStatus::NeedsReinstall,
ServiceStatus::InstallRequired,
ServiceStatus::Unavailable("offline".into()),
ServiceStatus::SidecarAllowed,
];
for status in &allowed_statuses {
assert!(can_allow_sidecar_for_session(&RunningMode::NotRunning, status));
assert!(!can_allow_sidecar_for_session(&RunningMode::Service, status));
if !matches!(
status,
ServiceStatus::InstallRequired | ServiceStatus::Unavailable(_) | ServiceStatus::SidecarAllowed
) {
assert!(!can_allow_sidecar_for_session(&RunningMode::Sidecar, status));
} else {
assert!(can_allow_sidecar_for_session(&RunningMode::Sidecar, status));
}
}
let rejected_statuses = [
ServiceStatus::Checking,
ServiceStatus::Ready,
ServiceStatus::UninstallRequired,
ServiceStatus::ReinstallRequired,
ServiceStatus::ForceReinstallRequired,
];
for status in &rejected_statuses {
assert!(!can_allow_sidecar_for_session(&RunningMode::NotRunning, status));
}
}
#[tokio::test]
async fn blocked_windows_fallback_records_failure_without_starting_sidecar() {
use crate::core::runstate::{FakeEnv, RunStateStore, ServiceHealth};
for idle in [false, true] {
let store = RunStateStore::new(FakeEnv::new());
store.observe(ServiceHealth::Ready);
let started = AtomicBool::new(false);
let result = super::run_service_start_with_sidecar_fallback(
|| async {
Err(crate::core::service::ServiceStartRefusal {
code: clash_verge_service_ipc::ServiceErrorCode::InvalidInstallLocation as u16,
core_path: "verge-mihomo.exe".into(),
message: "no administrator-approved copy is installed".into(),
}
.into())
},
|| async move {
anyhow::ensure!(idle, "another core is running");
Ok(())
},
|_| async {
started.store(true, Ordering::SeqCst);
Ok(())
},
|reason| store.observe(ServiceHealth::Unavailable(reason)),
)
.await;
assert_eq!(started.load(Ordering::SeqCst), idle);
assert_eq!(result.is_ok(), idle);
assert!(!store.state().sidecar_allowed);
if !idle {
assert!(store.state().service_needs_attention());
assert!(!store.state().service_usable());
assert!(result.is_err_and(|error| format!("{error:#}").contains("another core is running")));
}
}
}
#[test]
fn failed_service_install_can_continue_with_an_existing_sidecar() {
assert!(can_allow_sidecar_for_session(
&RunningMode::Sidecar,
&ServiceStatus::InstallRequired,
));
}
#[test]
fn proxy_restore_requires_the_same_ready_core_generation() {
let sidecar =
ProxyRestoreExpectation::capture(RunningMode::Sidecar, Some(20), 8).unwrap_or_else(|| unreachable!());
assert!(sidecar.is_valid(&RunningMode::Sidecar, Some(20), 8));
assert!(!sidecar.is_valid(&RunningMode::Sidecar, Some(21), 8));
assert!(!sidecar.is_valid(&RunningMode::Sidecar, None, 8));
assert!(ProxyRestoreExpectation::capture(RunningMode::Sidecar, None, 8).is_none());
let service =
ProxyRestoreExpectation::capture(RunningMode::Service, Some(20), 8).unwrap_or_else(|| unreachable!());
assert!(service.is_valid(&RunningMode::Service, Some(20), 8));
assert!(!service.is_valid(&RunningMode::Service, Some(20), 9));
assert!(!service.is_valid(&RunningMode::NotRunning, Some(20), 8));
assert!(ProxyRestoreExpectation::capture(RunningMode::Service, None, 8).is_none());
assert!(ProxyRestoreExpectation::capture(RunningMode::NotRunning, Some(20), 8).is_none());
}
#[tokio::test]
async fn failed_start_rolls_back_even_from_service_mode() {
let manager = CoreManager::isolated();
manager.core_started(RunningMode::Service);
manager.rollback_failed_start().await;
assert_eq!(*manager.get_running_mode(), RunningMode::NotRunning);
manager.core_started(RunningMode::Sidecar);
manager.rollback_failed_start().await;
assert_eq!(*manager.get_running_mode(), RunningMode::NotRunning);
}
}