mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-08 11:02:08 +08:00
feat: add launch guard and secure multi-user service ownership (#7569)
* feat(service): isolate macOS launch and owner runtime * ci(windows): run owner security tests * chore(deps): use kode-bridge v0.4.1 tag * fix(macos): classify installed service state * fix(macos): classify marked unavailable service * fix(macos): gate core startup on service migration * fix(macos): scope service init to supported platforms * feat(macos): prompt before migrating installed service * fix(macos): shorten sidecar IPC path * build(service): pin helper protocol and artifacts * fix(core): require confirmed service and sidecar readiness * fix(macos): harden launch roots and singleton state * fix(ui): refresh migration state before restart * fix(macos): linearize service migration lifecycle * fix(ui): keep service migration state fresh * feat(service): track authenticated owner sessions * fix(service): preserve newer owner sessions * feat(macos): route service proxy changes through helper * fix(macos): serialize service proxy guard * fix(macos): close service proxy owner lifecycle * fix(macos): close remaining proxy stop races * fix(app): do not cancel controlled service stop * fix(lifecycle): keep controlled updates cancellation-safe * fix(macos): stage dev core for service * test(macos): exercise staged core cleanup * fix(dev): await controlled macOS exit * chore: update changelog * fix(macos): recover sidecar after service install failure Allow the existing sidecar to continue when service installation leaves the manager in InstallRequired, then settle the session state as SidecarAllowed. Show neutral status text while the migration dialog waits for its final state. * fix(macos): stage dev service tools before elevation * fix(macos): localize launch guard prompts * fix(service): address cross-platform integration issues * feat: support seamless service migration * fix: pass Windows service installer path safely * fix(prebuild): derive service release from Cargo dependency * fix(dev): preserve service installation state * fix: resolve clippy warnings in service lifecycle
This commit is contained in:
@@ -60,6 +60,11 @@ pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_embedded_server_port() -> CmdResult<u16> {
|
||||
crate::utils::server::embedded_server_port().stringify_err()
|
||||
}
|
||||
|
||||
/// 获取系统主机名
|
||||
#[tauri::command]
|
||||
pub fn get_system_hostname() -> String {
|
||||
|
||||
@@ -21,7 +21,6 @@ use clash_verge_logging::{Type, logging, logging_error};
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
@@ -276,12 +275,14 @@ async fn handle_update_error<E: std::fmt::Display>(
|
||||
Ok(ValidationOutcome::invalid_from_message(message))
|
||||
}
|
||||
|
||||
async fn handle_timeout(current_profile: Option<&String>) -> CmdResult<ValidationOutcome> {
|
||||
let timeout_msg: String = "配置更新超时(30秒),可能是配置验证或核心通信阻塞".into();
|
||||
logging!(error, Type::Cmd, "{}", timeout_msg);
|
||||
discard_and_restore(current_profile).await?;
|
||||
handle::Handle::notice_message("config_validate::timeout", timeout_msg.clone());
|
||||
Ok(ValidationOutcome::invalid_from_message(timeout_msg))
|
||||
async fn run_profile_config_update_transition<Update, UpdateFuture>(
|
||||
update_config: Update,
|
||||
) -> anyhow::Result<ValidationOutcome>
|
||||
where
|
||||
Update: FnOnce() -> UpdateFuture,
|
||||
UpdateFuture: std::future::Future<Output = anyhow::Result<ValidationOutcome>>,
|
||||
{
|
||||
update_config().await
|
||||
}
|
||||
|
||||
async fn perform_config_update(
|
||||
@@ -291,14 +292,12 @@ async fn perform_config_update(
|
||||
defer! {
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
||||
}
|
||||
let update_result =
|
||||
tokio::time::timeout(Duration::from_secs(30), CoreManager::global().update_config_forced()).await;
|
||||
let update_result = run_profile_config_update_transition(|| CoreManager::global().update_config_forced()).await;
|
||||
|
||||
match update_result {
|
||||
Ok(Ok(outcome)) if outcome.is_valid() => handle_success(current_value).await,
|
||||
Ok(Ok(outcome)) => handle_validation_failure(outcome, current_profile).await,
|
||||
Ok(Err(e)) => handle_update_error(e, current_profile).await,
|
||||
Err(_) => handle_timeout(current_profile).await,
|
||||
Ok(outcome) if outcome.is_valid() => handle_success(current_value).await,
|
||||
Ok(outcome) => handle_validation_failure(outcome, current_profile).await,
|
||||
Err(e) => handle_update_error(e, current_profile).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,9 +429,32 @@ pub async fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::commit_current_profile;
|
||||
use super::{commit_current_profile, run_profile_config_update_transition};
|
||||
use crate::config::{IProfiles, PrfItem};
|
||||
use crate::core::validate::ValidationOutcome;
|
||||
use clash_verge_draft::Draft;
|
||||
use std::{
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
task::Poll,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::sync::Barrier;
|
||||
|
||||
struct CancellationProbe {
|
||||
cancelled: Arc<AtomicBool>,
|
||||
completed: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Drop for CancellationProbe {
|
||||
fn drop(&mut self) {
|
||||
if !self.completed.load(Ordering::Acquire) {
|
||||
self.cancelled.store(true, Ordering::Release);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn profile(uid: &str) -> PrfItem {
|
||||
PrfItem {
|
||||
@@ -467,4 +489,42 @@ mod tests {
|
||||
assert!(committed.get_item("new").is_ok());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn profile_config_update_runs_past_former_deadline_without_cancellation() -> anyhow::Result<()> {
|
||||
let update_started = Arc::new(Barrier::new(2));
|
||||
let release_update = Arc::new(Barrier::new(2));
|
||||
let update_cancelled = Arc::new(AtomicBool::new(false));
|
||||
let update_completed = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let mut update = Box::pin(run_profile_config_update_transition({
|
||||
let update_started = Arc::clone(&update_started);
|
||||
let release_update = Arc::clone(&release_update);
|
||||
let update_cancelled = Arc::clone(&update_cancelled);
|
||||
let update_completed = Arc::clone(&update_completed);
|
||||
move || async move {
|
||||
let _probe = CancellationProbe {
|
||||
cancelled: update_cancelled,
|
||||
completed: Arc::clone(&update_completed),
|
||||
};
|
||||
update_started.wait().await;
|
||||
release_update.wait().await;
|
||||
update_completed.store(true, Ordering::Release);
|
||||
Ok(ValidationOutcome::Valid)
|
||||
}
|
||||
}));
|
||||
|
||||
assert!(matches!(futures::poll!(update.as_mut()), Poll::Pending));
|
||||
update_started.wait().await;
|
||||
tokio::time::advance(Duration::from_secs(31)).await;
|
||||
|
||||
assert!(matches!(futures::poll!(update.as_mut()), Poll::Pending));
|
||||
assert!(!update_cancelled.load(Ordering::Acquire));
|
||||
|
||||
release_update.wait().await;
|
||||
assert!(update.await?.is_valid());
|
||||
assert!(update_completed.load(Ordering::Acquire));
|
||||
assert!(!update_cancelled.load(Ordering::Acquire));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
use super::{CmdResult, StringifyErr as _};
|
||||
use crate::core::service::{self, SERVICE_MANAGER, ServiceStatus};
|
||||
use crate::core::{
|
||||
CoreManager,
|
||||
service::{self, SERVICE_MANAGER, ServiceStatus},
|
||||
};
|
||||
|
||||
async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) -> CmdResult {
|
||||
let manager = CoreManager::global();
|
||||
let _lifecycle = manager.lifecycle_lock.lock().await;
|
||||
if matches!(
|
||||
&status,
|
||||
ServiceStatus::ReinstallRequired | ServiceStatus::ForceReinstallRequired
|
||||
) {
|
||||
manager
|
||||
.controlled_stop_core_inner()
|
||||
.await
|
||||
.map_err(|error| format!("{op_type} Service failed: {error:#}"))?;
|
||||
}
|
||||
SERVICE_MANAGER
|
||||
.handle_service_status(status)
|
||||
.await
|
||||
@@ -15,7 +29,10 @@ pub async fn install_service() -> CmdResult {
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn uninstall_service() -> CmdResult {
|
||||
execute_service_operation_sync(ServiceStatus::UninstallRequired, "Uninstall").await
|
||||
CoreManager::global()
|
||||
.uninstall_service_and_start_sidecar()
|
||||
.await
|
||||
.map_err(|error| format!("Uninstall Service failed: {error:#}").into())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -30,6 +47,18 @@ pub async fn repair_service() -> CmdResult {
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn is_service_available() -> CmdResult<bool> {
|
||||
service::is_service_available().await.stringify_err()?;
|
||||
Ok(true)
|
||||
Ok(service::is_service_available().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_service_install_state() -> CmdResult<service::ServiceInstallState> {
|
||||
Ok(SERVICE_MANAGER.install_state().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn continue_with_sidecar() -> CmdResult {
|
||||
crate::core::CoreManager::global()
|
||||
.continue_with_sidecar()
|
||||
.await
|
||||
.stringify_err()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user