fix(core): drain sidecar events before readiness probe to avoid startup stall

This commit is contained in:
oomeow
2026-09-18 21:26:31 +08:00
parent f6fdb34745
commit 4643f0e49a
+47 -31
View File
@@ -12,7 +12,13 @@ use anyhow::{Context as _, Result};
use clash_verge_logging::Type;
use log::Level;
use scopeguard::defer;
use std::path::Path;
use std::{
path::Path,
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
};
use tauri_plugin_mihomo::MihomoExt as _;
use tauri_plugin_shell::ShellExt as _;
@@ -208,6 +214,45 @@ impl CoreManager {
let pid = child.pid();
tracing::Span::current().record("pid", pid);
// The sidecar has to be drained from the moment it starts. tauri-plugin-shell buffers a
// single event, so an unread channel stalls its reader threads and the core blocks on a
// full stdout pipe before it ever creates the API socket.
let core_readiness_generation = Arc::new(AtomicU64::new(0));
let generation_slot = Arc::clone(&core_readiness_generation);
AsyncHandler::spawn(move || async move {
while let Some(event) = rx.recv().await {
match event {
tauri_plugin_shell::process::CommandEvent::Stdout(line)
| tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
let message = String::from_utf8_lossy(&line).into_owned();
Logger::global().writer_sidecar_log(Level::Error, &message);
CLASH_LOGGER.append_log(message);
}
tauri_plugin_shell::process::CommandEvent::Terminated(term) => {
let manager = Self::global();
// The generation is only published once the core is actually ready, so a
// termination before that must not invalidate a later core's readiness.
let generation = generation_slot.load(Ordering::Acquire);
if generation != 0 {
let _ = manager.invalidate_core_readiness_if(generation);
}
let message = if let Some(code) = term.code {
format!("Process terminated with code: {}", code)
} else if let Some(signal) = term.signal {
format!("Process terminated by signal: {}", signal)
} else {
String::from("Process terminated")
};
Logger::global().writer_sidecar_log(Level::Info, &message);
CLASH_LOGGER.clear_logs();
manager.clear_terminated_sidecar(pid).await;
break;
}
_ => {}
}
}
});
let readiness = poll_sidecar_readiness(SIDECAR_READINESS_ATTEMPTS, SIDECAR_READINESS_INTERVAL, || async {
tokio::time::timeout(SIDECAR_READINESS_PROBE_TIMEOUT, async {
handle::Handle::mihomo().get_version().await
@@ -231,39 +276,10 @@ impl CoreManager {
#[cfg(target_os = "windows")]
self.set_job_handle(Some(job));
self.set_running_child_sidecar(child);
let core_readiness_generation = self.mark_core_ready();
core_readiness_generation.store(self.mark_core_ready(), Ordering::Release);
self.core_started(RunningMode::Sidecar);
self.restore_selected_nodes().await;
AsyncHandler::spawn(move || async move {
while let Some(event) = rx.recv().await {
match event {
tauri_plugin_shell::process::CommandEvent::Stdout(line)
| tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
let message = String::from_utf8_lossy(&line).into_owned();
Logger::global().writer_sidecar_log(Level::Error, &message);
CLASH_LOGGER.append_log(message);
}
tauri_plugin_shell::process::CommandEvent::Terminated(term) => {
let manager = Self::global();
let _ = manager.invalidate_core_readiness_if(core_readiness_generation);
let message = if let Some(code) = term.code {
format!("Process terminated with code: {}", code)
} else if let Some(signal) = term.signal {
format!("Process terminated by signal: {}", signal)
} else {
String::from("Process terminated")
};
Logger::global().writer_sidecar_log(Level::Info, &message);
CLASH_LOGGER.clear_logs();
manager.clear_terminated_sidecar(pid).await;
break;
}
_ => {}
}
}
});
Ok(())
}