fix(linux): prevent core shutdown when worker threads exit
- Spawn Linux core processes on a dedicated, persistent thread instead of temporary background workers. - Preserve parent-death signaling to stop the core when the GUI exits. - Keep existing process management and Windows/macOS launch behavior unchanged. - Add regression tests for launcher thread exit, runtime shutdown, spawn failure recovery, and parent-process termination.
This commit is contained in:
1 parent
584c63bf2a
commit
08c915b18b
2 files changed
+174
-5
No files matched your search
@@ -2,6 +2,62 @@ use super::*;
|
||||
|
||||
pub(crate) static CORE_OPERATION_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
struct CoreSpawnRequest {
|
||||
command: Command,
|
||||
reply: std::sync::mpsc::SyncSender<io::Result<Child>>,
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
static CORE_PROCESS_SPAWNER: LazyLock<Result<std::sync::mpsc::Sender<CoreSpawnRequest>, String>> =
|
||||
LazyLock::new(|| {
|
||||
let (sender, receiver) = std::sync::mpsc::channel::<CoreSpawnRequest>();
|
||||
thread::Builder::new()
|
||||
.name("cpa-core-spawner".to_string())
|
||||
.spawn(move || {
|
||||
for mut request in receiver {
|
||||
configure_child_lifetime(&mut request.command);
|
||||
let result = request.command.spawn();
|
||||
if let Err(std::sync::mpsc::SendError(Ok(mut child))) =
|
||||
request.reply.send(result)
|
||||
{
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
}
|
||||
})
|
||||
.map_err(|error| format!("创建 CPA 内核启动线程失败: {error}"))?;
|
||||
Ok(sender)
|
||||
});
|
||||
|
||||
pub(crate) fn spawn_core_child(command: Command) -> Result<Child, String> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
spawn_core_child_on_lifetime_thread(command)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let mut command = command;
|
||||
command
|
||||
.spawn()
|
||||
.map_err(|error| format!("启动 CPA 内核失败: {error}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
pub(crate) fn spawn_core_child_on_lifetime_thread(command: Command) -> Result<Child, String> {
|
||||
let sender = CORE_PROCESS_SPAWNER.as_ref().map_err(Clone::clone)?;
|
||||
let (reply, result) = std::sync::mpsc::sync_channel(1);
|
||||
sender
|
||||
.send(CoreSpawnRequest { command, reply })
|
||||
.map_err(|_| "CPA 内核启动线程已退出".to_string())?;
|
||||
result
|
||||
.recv()
|
||||
.map_err(|_| "CPA 内核启动线程未返回启动结果".to_string())?
|
||||
.map_err(|error| format!("启动 CPA 内核失败: {error}"))
|
||||
}
|
||||
|
||||
async fn run_core_command(
|
||||
app: tauri::AppHandle,
|
||||
operation: fn(&CoreProcessState, &GuiConfigState) -> Result<CoreStatus, String>,
|
||||
@@ -1183,11 +1239,8 @@ pub(crate) fn start_core_process_inner(
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
configure_background_command(&mut command);
|
||||
configure_child_lifetime(&mut command);
|
||||
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.map_err(|err| format!("启动 CPA 内核失败: {err}"))?;
|
||||
let mut child = spawn_core_child(command)?;
|
||||
|
||||
if let Err(error) = wait_for_core_management_port(&mut child, management_address) {
|
||||
let _ = terminate_child(&mut child);
|
||||
@@ -1224,7 +1277,8 @@ pub(crate) fn wait_for_core_management_port(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn configure_child_lifetime(command: &mut Command) {
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
fn configure_child_lifetime(command: &mut Command) {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
|
||||
@@ -34,6 +34,121 @@ fn core_process_discovery_sleep_helper() {
|
||||
}
|
||||
}
|
||||
|
||||
fn core_child_sleep_command() -> Command {
|
||||
let mut command = Command::new(env::current_exe().unwrap());
|
||||
command
|
||||
.args([
|
||||
"--exact",
|
||||
"tests::core_runtime::core_process_discovery_sleep_helper",
|
||||
"--nocapture",
|
||||
])
|
||||
.env("EASYCLIPROXYAPI_PROCESS_DISCOVERY_TEST_HELPER", "1")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
configure_background_command(&mut command);
|
||||
command
|
||||
}
|
||||
|
||||
fn assert_core_child_survives(mut child: Child) {
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
let status = child.try_wait();
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
assert!(status.unwrap().is_none(), "core exited with its launcher");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn core_child_survives_launcher_thread_exit() {
|
||||
let child = thread::spawn(|| spawn_core_child(core_child_sleep_command()).unwrap())
|
||||
.join()
|
||||
.unwrap();
|
||||
assert_core_child_survives(child);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn core_child_survives_blocking_runtime_shutdown() {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.build()
|
||||
.unwrap();
|
||||
let child = runtime
|
||||
.block_on(runtime.spawn_blocking(|| spawn_core_child(core_child_sleep_command()).unwrap()))
|
||||
.unwrap();
|
||||
drop(runtime);
|
||||
assert_core_child_survives(child);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn core_child_spawner_remains_available_after_spawn_failure() {
|
||||
let missing_binary = agent_test_home("missing-core-spawner-binary").join(core_binary_name());
|
||||
assert!(spawn_core_child_on_lifetime_thread(Command::new(missing_binary)).is_err());
|
||||
let child =
|
||||
thread::spawn(|| spawn_core_child_on_lifetime_thread(core_child_sleep_command()).unwrap())
|
||||
.join()
|
||||
.unwrap();
|
||||
assert_core_child_survives(child);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn core_child_owner_process_helper() {
|
||||
if env::var_os("EASYCLIPROXYAPI_CORE_OWNER_TEST_HELPER").is_none() {
|
||||
return;
|
||||
}
|
||||
let mut command = core_child_sleep_command();
|
||||
command.stdout(Stdio::inherit());
|
||||
let _child = spawn_core_child(command).unwrap();
|
||||
println!("CORE_CHILD_READY");
|
||||
io::stdout().flush().unwrap();
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn core_child_stops_when_owner_process_is_killed() {
|
||||
use std::io::BufRead;
|
||||
|
||||
let mut owner = Command::new(env::current_exe().unwrap())
|
||||
.args([
|
||||
"--exact",
|
||||
"tests::core_runtime::core_child_owner_process_helper",
|
||||
"--nocapture",
|
||||
])
|
||||
.env("EASYCLIPROXYAPI_CORE_OWNER_TEST_HELPER", "1")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let mut output = io::BufReader::new(owner.stdout.take().unwrap());
|
||||
let mut ready = false;
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
match output.read_line(&mut line) {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(_) if line.trim() == "CORE_CHILD_READY" => {
|
||||
ready = true;
|
||||
break;
|
||||
}
|
||||
Ok(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
let killed = owner.kill();
|
||||
let waited = owner.wait();
|
||||
let shutdown_started = Instant::now();
|
||||
let mut remaining_output = String::new();
|
||||
let drained = output.read_to_string(&mut remaining_output);
|
||||
assert!(ready, "owner did not finish spawning its core child");
|
||||
killed.unwrap();
|
||||
waited.unwrap();
|
||||
drained.unwrap();
|
||||
assert!(
|
||||
shutdown_started.elapsed() < Duration::from_secs(5),
|
||||
"core kept its output pipe open after the owner was killed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn running_core_process_discovery_ignores_the_same_binary_name_in_another_directory() {
|
||||
let root = agent_test_home("running-core-process-scope");
|
||||
|
||||
Reference in new issue
Block a user