diff --git a/src-tauri/src/commands/maa_agent.rs b/src-tauri/src/commands/maa_agent.rs index 60a67574..7f7aa02f 100644 --- a/src-tauri/src/commands/maa_agent.rs +++ b/src-tauri/src/commands/maa_agent.rs @@ -7,7 +7,8 @@ use std::collections::HashMap; use std::fs::OpenOptions; use std::io::{BufRead, BufReader, Write}; use std::path::{Component, Path, PathBuf}; -use std::process::{Command, Stdio}; +use std::process::{Child, Command, Stdio}; +use std::sync::atomic::Ordering; use std::sync::{Arc, Mutex}; use std::thread; @@ -19,12 +20,83 @@ use maa_framework::controller::Controller; use maa_framework::resource::Resource; use maa_framework::tasker::Tasker; -use super::types::{AgentConfig, MaaState, TaskConfig}; +use super::types::{ + terminate_agent_child, AgentConfig, MaaState, PendingAgentState, TaskConfig, + AGENT_DISCONNECT_TIMEOUT_MS, +}; use super::utils::{emit_callback_event, get_logs_dir, handle_task_callback, normalize_path}; use regex::Regex; use std::sync::LazyLock; use std::time::{Duration, Instant}; +const AGENT_STOP_TIMEOUT: Duration = Duration::from_secs(5); + +struct PendingAgent { + maa_state: Arc, + identifier: String, + state: Arc, +} + +impl PendingAgent { + fn register(maa_state: Arc, identifier: String, instance_id: String) -> Self { + let state = Arc::new(PendingAgentState::new(instance_id)); + let mut pending = match maa_state.pending_agent_children.lock() { + Ok(pending) => pending, + Err(poisoned) => { + warn!("Pending agent children lock was poisoned; continuing registration"); + poisoned.into_inner() + } + }; + pending.insert(identifier.clone(), state.clone()); + drop(pending); + + Self { + maa_state, + identifier, + state, + } + } + + fn set_child(&self, child: Child) -> Result<(), Child> { + if self.state.cancelled.load(Ordering::Relaxed) { + return Err(child); + } + + let mut child_slot = match self.state.child.lock() { + Ok(child_slot) => child_slot, + Err(poisoned) => poisoned.into_inner(), + }; + if self.state.cancelled.load(Ordering::Relaxed) { + return Err(child); + } + *child_slot = Some(child); + Ok(()) + } + + fn take_child(&self) -> Option { + match self.state.child.lock() { + Ok(mut child) => child.take(), + Err(poisoned) => poisoned.into_inner().take(), + } + } + + fn terminate_child(&self) { + if let Some(child) = self.take_child() { + terminate_agent_child(child); + } + } +} + +impl Drop for PendingAgent { + fn drop(&mut self) { + let mut pending = match self.maa_state.pending_agent_children.lock() { + Ok(pending) => pending, + Err(poisoned) => poisoned.into_inner(), + }; + pending.remove(&self.identifier); + } +} + /// Agent 输出事件载荷 #[derive(Clone, serde::Serialize)] pub struct AgentOutputEvent { @@ -285,17 +357,28 @@ async fn start_single_agent( agent: AgentConfig, agent_index: usize, instance_id: String, + maa_state: Arc, cwd: String, tcp_compat_mode: bool, resource: Resource, controller: Controller, tasker: Tasker, pi_envs: Arc>, -) -> Result<(AgentClient, std::process::Child), String> { +) -> Result<(AgentClient, PendingAgent), String> { info!("[agent#{}] Starting agent: {:?}", agent_index, agent); // 将整个启动过程移入 spawn_blocking,避免阻塞 async runtime 线程 tauri::async_runtime::spawn_blocking(move || { + // 端点创建与 pending 登记必须相对退出清理原子化。退出流程会先设置 + // agent_shutdown_requested,再等待同一把锁,因此不会漏掉半创建的客户端。 + let lifecycle_guard = match maa_state.agent_lifecycle_lock.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + if maa_state.agent_shutdown_requested.load(Ordering::Relaxed) { + return Err("Application is shutting down".to_string()); + } + let mut client = if tcp_compat_mode { debug!("[agent#{}] Creating TCP agent client...", agent_index); AgentClient::create_tcp(0).or_else(|e| { @@ -320,6 +403,24 @@ async fn start_single_agent( .ok_or_else(|| format!("Failed to get identifier for agent #{}", agent_index))?; info!("[agent#{}] Agent socket_id: {}", agent_index, socket_id); + // AgentClient 创建 IPC 端点后立即登记 identifier。即使退出发生在 spawn 前, + // 全局清理也能精确删除这个尚未转入 InstanceRuntime 的 socket。 + let pending_agent = PendingAgent::register( + Arc::clone(&maa_state), + socket_id.clone(), + instance_id.clone(), + ); + drop(lifecycle_guard); + + if pending_agent + .maa_state + .agent_shutdown_requested + .load(Ordering::Relaxed) + || pending_agent.state.cancelled.load(Ordering::Relaxed) + { + return Err("Agent startup was interrupted by application shutdown".to_string()); + } + // 启动子进程 let mut args = agent.child_args.clone().unwrap_or_default(); args.push(socket_id.clone()); @@ -387,7 +488,7 @@ async fn start_single_agent( ); } - let mut child = cmd.spawn().map_err(|e| { + let child = cmd.spawn().map_err(|e| { let mut msg = format!( "Failed to spawn agent #{}: {} (path: {:?})", agent_index, e, exec_path @@ -398,15 +499,30 @@ async fn start_single_agent( msg })?; + // 退出线程和启动线程通过同一个 Option 槽位转移所有权。 + if let Err(child) = pending_agent.set_child(child) { + terminate_agent_child(child); + return Err("Agent startup was interrupted by application shutdown".to_string()); + } + let (pid, stdout, stderr) = { + let mut child_slot = match pending_agent.state.child.lock() { + Ok(child) => child, + Err(poisoned) => poisoned.into_inner(), + }; + let child = child_slot + .as_mut() + .ok_or("Agent startup was interrupted by application shutdown")?; + (child.id(), child.stdout.take(), child.stderr.take()) + }; + // agent 日志文件路径(延迟创建:仅在有实际输出时才打开文件) - let pid = child.id(); let log_filename = format!("mxu-agent-{}-{}.log", agent_index, pid); let agent_log_path = Arc::new(get_logs_dir().join(&log_filename)); let log_file: Arc>> = Arc::new(Mutex::new(None)); let output_batcher = AgentOutputBatcher::new(app.clone(), instance_id.clone()); // 在单独线程中读取 stdout - if let Some(stdout) = child.stdout.take() { + if let Some(stdout) = stdout { let lf = log_file.clone(); let lf_path = agent_log_path.clone(); let batcher = output_batcher.clone(); @@ -443,7 +559,7 @@ async fn start_single_agent( } // Stderr thread - if let Some(stderr) = child.stderr.take() { + if let Some(stderr) = stderr { let lf = log_file.clone(); let lf_path = agent_log_path.clone(); let batcher = output_batcher.clone(); @@ -489,8 +605,7 @@ async fn start_single_agent( if let Err(e) = client.connect() { error!("[agent#{}] Connection failed: {}", agent_index, e); - let _ = child.kill(); - let _ = child.wait(); + pending_agent.terminate_child(); return Err(e.to_string()); } @@ -499,15 +614,33 @@ async fn start_single_agent( // 注册 Agent sink if let Err(e) = client.register_sinks(resource, controller, tasker) { error!("[agent#{}] Failed to register sinks: {}", agent_index, e); - let _ = child.kill(); - let _ = child.wait(); + pending_agent.terminate_child(); return Err(e.to_string()); } - Ok((client, child)) + Ok((client, pending_agent)) }).await.map_err(|e| e.to_string())? } +fn cleanup_untracked_agents( + clients: Vec, + pending_agents: Vec, + children: Vec, +) { + for mut client in clients { + if client.set_timeout(AGENT_DISCONNECT_TIMEOUT_MS).is_ok() { + let _ = client.disconnect(); + } + } + + for pending_agent in pending_agents { + pending_agent.terminate_child(); + } + for child in children { + terminate_agent_child(child); + } +} + /// 启动任务的核心实现(Tauri invoke 和 HTTP handler 共享) pub async fn start_tasks_impl( app: tauri::AppHandle, @@ -522,6 +655,10 @@ pub async fn start_tasks_impl( ) -> Result, String> { info!("start_tasks_impl called"); + if maa_state.agent_shutdown_requested.load(Ordering::Relaxed) { + return Err("Application is shutting down".to_string()); + } + info!("instance_id: {}", instance_id); info!("tasks: {:?}", tasks); info!("agent_configs: {:?}", agent_configs); @@ -626,14 +763,20 @@ pub async fn start_tasks_impl( // 用于收集所有成功启动的 agent,失败时需要回滚清理 let mut new_clients = Vec::new(); - let mut new_children = Vec::new(); + let mut pending_agents = Vec::new(); for (idx, config) in configs.iter().enumerate() { + if maa_state.agent_shutdown_requested.load(Ordering::Relaxed) { + cleanup_untracked_agents(new_clients, pending_agents, Vec::new()); + return Err("Application is shutting down".to_string()); + } + let res_clone = resource.clone(); let ctrl_clone = controller.clone(); let tasker_clone = tasker.clone(); let app_handle = app.clone(); let inst_id = instance_id.clone(); + let maa_state_clone = Arc::clone(maa_state); let cwd_clone = cwd.clone(); let pi_envs_clone = Arc::clone(&pi_envs); @@ -642,6 +785,7 @@ pub async fn start_tasks_impl( config.clone(), idx, inst_id, + maa_state_clone, cwd_clone, tcp_compat_mode, res_clone, @@ -651,9 +795,9 @@ pub async fn start_tasks_impl( ) .await { - Ok((client, child)) => { + Ok((client, pending_agent)) => { new_clients.push(client); - new_children.push(child); + pending_agents.push(pending_agent); } Err(e) => { error!( @@ -662,25 +806,69 @@ pub async fn start_tasks_impl( ); // 回滚:清理已启动的 agent - for client in &new_clients { - let _ = client.disconnect(); - } - for mut child in new_children { - let _ = child.kill(); - let _ = child.wait(); - } + cleanup_untracked_agents(new_clients, pending_agents, Vec::new()); return Err(format!("Agent start failed: {}", e)); } } } + // pending -> instance 的最终转移与 stop/exit 使用同一把生命周期锁, + // 防止 stop 在 tracked 与 pending 两次清理之间漏掉刚完成连接的 Agent。 + let lifecycle_guard = match maa_state.agent_lifecycle_lock.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + // 保存所有 agent 状态到 instance - let mut instances = maa_state.instances.lock().map_err(|e| e.to_string())?; - if let Some(instance) = instances.get_mut(&instance_id) { - instance.agent_clients.extend(new_clients); - instance.agent_children.extend(new_children); + let mut instances = match maa_state.instances.lock() { + Ok(instances) => instances, + Err(poisoned) => { + warn!("MaaState instances lock was poisoned; continuing agent transfer"); + poisoned.into_inner() + } + }; + if maa_state.agent_shutdown_requested.load(Ordering::Relaxed) { + drop(instances); + cleanup_untracked_agents(new_clients, pending_agents, Vec::new()); + return Err("Application is shutting down".to_string()); + } + + if !instances.contains_key(&instance_id) { + drop(instances); + cleanup_untracked_agents(new_clients, pending_agents, Vec::new()); + return Err(format!( + "Instance '{}' was removed while starting agents", + instance_id + )); } + let mut new_children = Vec::with_capacity(pending_agents.len()); + let mut startup_interrupted = false; + for pending_agent in &pending_agents { + match pending_agent.take_child() { + Some(child) => new_children.push(child), + None => { + startup_interrupted = true; + break; + } + } + } + if startup_interrupted { + drop(instances); + cleanup_untracked_agents(new_clients, pending_agents, new_children); + return Err("Agent startup was interrupted by application shutdown".to_string()); + } + + let instance = instances + .get_mut(&instance_id) + .expect("instance existence checked while holding instances lock"); + instance.agent_clients.extend(new_clients); + instance.agent_children.extend(new_children); + // 句柄已经在 instances 锁内完成转移,现在可以从 pending 监督表移除。 + drop(pending_agents); + drop(instances); + drop(lifecycle_guard); + info!( "[start_tasks] All {} agent(s) started successfully", configs.len() @@ -798,10 +986,14 @@ pub async fn maa_start_tasks( .await } -/// 停止所有 Agent 的核心实现(Tauri invoke 和 HTTP handler 共享) -pub fn stop_agent_impl(maa_state: &Arc, instance_id: &str) -> Result<(), String> { +fn stop_agent_blocking(maa_state: &Arc, instance_id: &str) -> Result<(), String> { info!("stop_agent_impl called for instance: {}", instance_id); + let _lifecycle_guard = maa_state + .agent_lifecycle_lock + .lock() + .map_err(|e| e.to_string())?; + let (clients, children) = { let mut instances = maa_state.instances.lock().map_err(|e| e.to_string())?; let instance = instances.get_mut(instance_id).ok_or("Instance not found")?; @@ -812,60 +1004,93 @@ pub fn stop_agent_impl(maa_state: &Arc, instance_id: &str) -> Result<( ) }; + // 同一实例仍在 connect 的 Agent 也属于本次 stop;取消后它们无法再写回实例。 + maa_state.cleanup_pending_agents(Some(instance_id)); + if clients.is_empty() && children.is_empty() { debug!("[stop_agent] No agents to stop"); return Ok(()); } info!( - "[stop_agent] Stopping {} agent client(s) and {} child process(es) in background...", + "[stop_agent] Disconnecting {} agent client(s) and stopping {} child process(es)...", clients.len(), children.len() ); - thread::spawn(move || { - for client in clients { - let _ = client.disconnect(); + // 必须在返回前销毁 AgentClient。临时收紧 timeout,避免失联 Agent 无限阻塞 stop。 + for mut client in clients { + if let Err(e) = client.set_timeout(AGENT_DISCONNECT_TIMEOUT_MS) { + warn!("Failed to set agent disconnect timeout: {}", e); + continue; } + if let Err(e) = client.disconnect() { + warn!("Failed to disconnect agent client: {}", e); + } + } - for (i, mut child) in children.into_iter().enumerate() { - debug!("Waiting for agent process #{} to exit...", i); - - let start = std::time::Instant::now(); - let timeout = std::time::Duration::from_secs(5); - let mut exited = false; + // 所有子进程共享一个总 deadline,避免多个失联 Agent 产生 N * timeout 的等待。 + let deadline = Instant::now() + AGENT_STOP_TIMEOUT; + for (i, mut child) in children.into_iter().enumerate() { + debug!("Waiting for agent process #{} to exit...", i); + let mut exited = false; - while start.elapsed() < timeout { - match child.try_wait() { - Ok(Some(_)) => { - exited = true; - break; - } - Ok(None) => { - thread::sleep(std::time::Duration::from_millis(100)); - } - Err(e) => { - error!("Error waiting for agent #{}: {}", i, e); - break; - } + loop { + match child.try_wait() { + Ok(Some(_)) => { + exited = true; + break; + } + Ok(None) => {} + Err(e) => { + error!("Error waiting for agent #{}: {}", i, e); + break; } } - if !exited { - warn!("Agent process #{} did not exit in time, killing it...", i); - let _ = child.kill(); + let now = Instant::now(); + if now >= deadline { + break; + } + thread::sleep((deadline - now).min(Duration::from_millis(100))); + } + + if exited { + info!("Agent #{} child process exited", i); + continue; + } + + warn!("Agent process #{} did not exit in time, killing it...", i); + match child.kill() { + Ok(()) => { let _ = child.wait(); - } else { - info!("Background: Agent #{} child process exited", i); + } + Err(e) => { + // kill 失败时不再无界 wait;退出兜底也不会因此永久阻塞。 + error!("Failed to kill agent process #{}: {}", i, e); } } - }); + } Ok(()) } +/// 停止所有 Agent 的核心实现(Tauri invoke 和 HTTP handler 共享)。 +pub async fn stop_agent_impl( + maa_state: Arc, + instance_id: String, +) -> Result<(), String> { + tauri::async_runtime::spawn_blocking(move || stop_agent_blocking(&maa_state, &instance_id)) + .await + .map_err(|e| format!("Agent cleanup worker failed: {}", e))? +} + /// 停止所有 Agent 并断开连接 — Tauri invoke 入口,委托给 stop_agent_impl #[tauri::command] -pub fn maa_stop_agent(state: State<'_, Arc>, instance_id: String) -> Result<(), String> { - stop_agent_impl(&state, &instance_id) +pub async fn maa_stop_agent( + state: State<'_, Arc>, + instance_id: String, +) -> Result<(), String> { + let maa_state = state.inner().clone(); + stop_agent_impl(maa_state, instance_id).await } diff --git a/src-tauri/src/commands/types.rs b/src-tauri/src/commands/types.rs index f045b9e1..37618fce 100644 --- a/src-tauri/src/commands/types.rs +++ b/src-tauri/src/commands/types.rs @@ -5,7 +5,8 @@ use std::collections::{HashMap, HashSet, VecDeque}; use std::path::PathBuf; use std::process::Child; -use std::sync::Mutex; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; use std::time::Instant; use serde::{Deserialize, Serialize}; @@ -169,6 +170,33 @@ pub struct AllInstanceStates { pub cached_wlroots_sockets: Vec, } +pub(crate) const AGENT_DISCONNECT_TIMEOUT_MS: i64 = 1_000; + +pub(crate) fn terminate_agent_child(mut child: Child) { + if matches!(child.try_wait(), Ok(Some(_))) { + return; + } + if child.kill().is_ok() { + let _ = child.wait(); + } +} + +pub(crate) struct PendingAgentState { + pub(crate) instance_id: String, + pub(crate) child: Mutex>, + pub(crate) cancelled: AtomicBool, +} + +impl PendingAgentState { + pub(crate) fn new(instance_id: String) -> Self { + Self { + instance_id, + child: Mutex::new(None), + cancelled: AtomicBool::new(false), + } + } +} + /// 实例运行时状态(持有 MaaFramework 对象句柄) #[derive(Default)] pub struct InstanceRuntime { @@ -192,15 +220,16 @@ pub struct InstanceRuntime { impl Drop for InstanceRuntime { fn drop(&mut self) { // 断开并销毁所有 agent - for client in &self.agent_clients { - let _ = client.disconnect(); + for client in &mut self.agent_clients { + if client.set_timeout(AGENT_DISCONNECT_TIMEOUT_MS).is_ok() { + let _ = client.disconnect(); + } } self.agent_clients.clear(); // 终止并回收所有 agent 子进程 - for mut child in self.agent_children.drain(..) { - let _ = child.kill(); - let _ = child.wait(); + for child in self.agent_children.drain(..) { + terminate_agent_child(child); } if let Some(tasker) = self.tasker.take() { @@ -282,6 +311,12 @@ pub struct MaaState { pub lib_dir: Mutex>, pub resource_dir: Mutex>, pub instances: Mutex>, + /// 串行化 Agent 句柄的转移与清理,确保退出流程能等待正在进行的 stop。 + pub agent_lifecycle_lock: Mutex<()>, + /// 应用已进入退出清理阶段,不再接受新的 Agent 句柄写回。 + pub agent_shutdown_requested: AtomicBool, + /// 已生成子进程、但尚未完成连接并写回 InstanceRuntime 的 Agent。 + pub(crate) pending_agent_children: Mutex>>, /// 前置程序停止请求(用于中断等待退出) pub pre_action_stop_requests: Mutex>, /// Controller 连接池:相同配置的 Controller 复用同一个 MaaControllerHandle @@ -299,24 +334,183 @@ pub struct MaaState { } impl MaaState { - /// 清理所有实例的 agent 子进程 - pub fn cleanup_all_agent_children(&self) { - if let Ok(mut instances) = self.instances.lock() { - for (id, instance) in instances.iter_mut() { - for mut child in instance.agent_children.drain(..) { - log::info!("Killing agent child process for instance: {}", id); - if let Err(e) = child.kill() { + /// 销毁所有 AgentClient,然后终止对应的子进程。 + /// + /// 句柄先从共享状态中取出,避免在可能阻塞的原生调用期间持有 instances 锁。 + /// 该操作可重复调用;首轮清理后,后续调用不会再取得任何句柄。 + pub fn cleanup_all_agents(&self) { + self.agent_shutdown_requested.store(true, Ordering::Relaxed); + + let _lifecycle_guard = match self.agent_lifecycle_lock.lock() { + Ok(guard) => guard, + Err(poisoned) => { + log::warn!("Agent lifecycle lock was poisoned; continuing agent cleanup"); + poisoned.into_inner() + } + }; + + let agents = { + let mut instances = match self.instances.lock() { + Ok(instances) => instances, + Err(poisoned) => { + log::warn!("MaaState instances lock was poisoned; continuing agent cleanup"); + poisoned.into_inner() + } + }; + + instances + .iter_mut() + .filter_map(|(id, instance)| { + let clients = std::mem::take(&mut instance.agent_clients); + let children = std::mem::take(&mut instance.agent_children); + + if clients.is_empty() && children.is_empty() { + None + } else { + Some((id.clone(), clients, children)) + } + }) + .collect::>() + }; + + for (id, clients, children) in agents { + log::info!( + "Cleaning up {} agent client(s) and {} child process(es) for instance: {}", + clients.len(), + children.len(), + id + ); + + // 退出流程不能受 Agent 配置中的无限 RPC timeout 阻塞。直接 Drop 会销毁 + // MaaFramework Transceiver 并删除 IPC socket,随后再强制终止服务端进程。 + drop(clients); + + for mut child in children { + match child.try_wait() { + Ok(Some(_)) => continue, + Ok(None) => {} + Err(e) => { + log::warn!( + "Failed to query agent child process for instance {}: {:?}", + id, + e + ); + } + } + + match child.kill() { + Ok(()) => { + // kill 成功后回收子进程,避免 *nix 上产生僵尸进程。 + let _ = child.wait(); + } + Err(e) => { + // kill 失败时不再无界 wait,避免把应用退出永久卡住。 log::warn!( "Failed to kill agent child process for instance {}: {:?}", id, e ); } - // 回收子进程,避免 *nix 上产生僵尸进程 - let _ = child.wait(); } } } + + self.cleanup_pending_agents(None); + } + + /// 清理全部 pending Agent,或仅清理指定实例的 pending Agent。 + /// 调用方必须持有 agent_lifecycle_lock,防止端点创建与筛选交错。 + pub(crate) fn cleanup_pending_agents(&self, instance_id: Option<&str>) { + let pending_agents = { + let mut pending = match self.pending_agent_children.lock() { + Ok(pending) => pending, + Err(poisoned) => { + log::warn!( + "Pending agent children lock was poisoned; continuing agent cleanup" + ); + poisoned.into_inner() + } + }; + + if let Some(instance_id) = instance_id { + let identifiers = pending + .iter() + .filter(|(_, state)| state.instance_id == instance_id) + .map(|(identifier, _)| identifier.clone()) + .collect::>(); + identifiers + .into_iter() + .filter_map(|identifier| { + pending + .remove(&identifier) + .map(|state| (identifier, state)) + }) + .collect::>() + } else { + pending.drain().collect::>() + } + }; + + for (identifier, pending_agent) in pending_agents { + pending_agent.cancelled.store(true, Ordering::Relaxed); + let child = match pending_agent.child.lock() { + Ok(mut slot) => slot.take(), + Err(poisoned) => { + log::warn!( + "Pending agent child lock was poisoned for identifier: {}", + identifier + ); + poisoned.into_inner().take() + } + }; + + if let Some(child) = child { + log::info!( + "Terminating pending agent child process for identifier: {}", + identifier + ); + terminate_agent_child(child); + } + + remove_pending_agent_socket(&identifier); + } + } +} + +fn remove_pending_agent_socket(identifier: &str) { + // TCP identifier 是纯数字端口,不存在 socket 文件。 + if identifier.parse::().is_ok() { + return; + } + if !identifier + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || ch == '-') + { + log::warn!("Skipping invalid pending agent identifier: {}", identifier); + return; + } + + let mut socket_dirs = vec![std::env::temp_dir()]; + #[cfg(windows)] + { + // MaaFramework 在 Windows 上使用该 IPC 兼容目录。 + let legacy_dir = PathBuf::from("C:/Temp"); + if !socket_dirs.contains(&legacy_dir) { + socket_dirs.push(legacy_dir); + } + } + + for socket_dir in socket_dirs { + let socket_path = socket_dir.join(format!("maafw-agent-{}.sock", identifier)); + match std::fs::remove_file(&socket_path) { + Ok(()) => log::info!("Removed pending agent socket: {:?}", socket_path), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => log::warn!( + "Failed to remove pending agent socket {:?}: {}", + socket_path, + e + ), + } } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 03df7e7b..e1aac5c9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -318,15 +318,25 @@ pub fn run() { api.prevent_close(); } } - // 窗口销毁时清理所有 agent 子进程 + // 窗口销毁时提前清理所有 Agent;全局 Exit 事件会再次幂等兜底。 tauri::WindowEvent::Destroyed => { - if let Some(state) = window.try_state::>() { - state.cleanup_all_agent_children(); + if window.label() == "main" { + if let Some(state) = window.try_state::>() { + state.cleanup_all_agents(); + } } } _ => {} } }) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + .build(tauri::generate_context!()) + .expect("error while building tauri application") + .run(|app, event| { + // 覆盖托盘退出、process 插件退出、管理员重启等不一定销毁窗口的路径。 + if let tauri::RunEvent::Exit = event { + if let Some(state) = app.try_state::>() { + state.cleanup_all_agents(); + } + } + }); } diff --git a/src-tauri/src/web_server.rs b/src-tauri/src/web_server.rs index 9f90c7fd..ffeff1e1 100644 --- a/src-tauri/src/web_server.rs +++ b/src-tauri/src/web_server.rs @@ -1007,7 +1007,7 @@ async fn handle_stop_agent( State(state): State, axum::extract::Path(instance_id): axum::extract::Path, ) -> impl IntoResponse { - match stop_agent_impl(&state.maa_state, &instance_id) { + match stop_agent_impl(state.maa_state.clone(), instance_id).await { Ok(()) => Json(serde_json::json!({ "ok": true })).into_response(), Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR,