clash-verge-rev/clash-verge-rev · error · anyhow::Error

core readiness changed while applying system proxy

Error message

core readiness changed while applying system proxy

What it means

Thrown by CoreManager::apply_proxy_after_start after the system proxy was applied. Before applying it captured a ProxyRestoreExpectation (running mode + core readiness generation + owner-monitor generation); after `proxy_control::apply().await` returned it re-checks that expectation. If any of the three signals changed, the just-applied proxy is rolled back (clear + stop_guard) and the function bails. It exists so the app never leaves the OS pointing at a proxy whose backing core has already moved underneath it.

Source

Thrown at src-tauri/src/core/manager/lifecycle.rs:412

    }

    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"))?;
        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?;
            anyhow::bail!("core readiness changed while applying system proxy");
        }
        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?;
            anyhow::bail!("core readiness changed while refreshing the system proxy guard");
        }
        Ok(())
    }

    /// 调用者须已持有 `lifecycle_lock`。
    async fn start_core_inner(&self) -> Result<()> {
        // 退出中不再启动新内核。

View on GitHub (pinned to 5cad0f2799)

Solutions

  1. Route every restart/config path through restart_core / restart_core_during_config_update so the config_update_in_progress guard + lifecycle_lock serialize lifecycle work; do not call start/stop directly.
  2. Retry the operation via restart_core — the expectation is re-captured on each attempt and a stable core will pass.
  3. Check owner-monitor / service health logs around the failure timestamp; if owner_monitor_generation changed, the privileged service restarted and the core must be handed back to it.
  4. If it reproduces, capture running_mode + both generation values before and after apply to identify which of the three signals is moving.

Example fix

// before: two callers both touching lifecycle
core.start_core().await?;
core.apply_proxy_after_start().await?; // races with a concurrent restart

// after: single serialized entry point
core.restart_core().await?; // holds config_update flag + lifecycle_lock end-to-end
Defensive patterns

Strategy: retry

Validate before calling

// Before apply, confirm no other config update is in flight and the core is stable.
if !core_manager.try_start_config_update() {
    return Err(anyhow::anyhow!("defer: a config update is already running"));
}
// capture an expectation and assert it is still live right before calling apply_proxy_after_start
let pre = (
    *core_manager.get_running_mode(),
    core_manager.current_core_readiness_generation(),
    crate::core::service::owner_monitor_generation(),
);
if pre.0 == RunningMode::NotRunning || pre.1.is_none() {
    return Err(anyhow::anyhow!("defer: core not ready before proxy apply"));
}

Try / catch

match core_manager.apply_proxy_after_start().await {
    Ok(()) => Ok(()),
    Err(e) if e.to_string().contains("core readiness changed while applying system proxy") => {
        // transient race against a concurrent lifecycle op; one retry via the serialized path
        core_manager.restart_core().await
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: Called right after a core start/restart finishes, while `lifecycle_lock` is held. The bail fires when, during the await on `proxy_control::apply()`, one of these changed: RunningMode transitioned away (e.g. core exited or was stopped), current_core_readiness_generation() bumped (core restarted under you), or crate::core::service::owner_monitor_generation() changed (the privileged service re-established ownership).

Common situations: A second lifecycle operation racing the first (another restart_core / change_core / activate that did not take the same config_update lock); the service manager dying and respawning mid-apply; the core crashing immediately after the readiness probe so its generation bumps; rapid config edits triggering overlapping apply calls.

Related errors


AI-assisted analysis of clash-verge-rev/clash-verge-rev@5cad0f2799 (2026-08-12). Data as JSON: /api/errors/6504a18f23f33185. Report an issue: GitHub.