diff --git a/apps/console/src-tauri/src/agent.rs b/apps/console/src-tauri/src/agent.rs index 1a99009..d480a20 100644 --- a/apps/console/src-tauri/src/agent.rs +++ b/apps/console/src-tauri/src/agent.rs @@ -306,30 +306,35 @@ pub async fn stop_pid(pid: u32) -> bool { /// 这层校验是**安全边界**:端口上跑的可能是任何东西(用户自己的服务、 /// 另一个应用)。只有确认是自家被控端才允许「复用」, /// 否则必须继续报「端口被占用」—— 绝不能把别人的进程当成自己的接管。 -pub fn looks_like_agent(pid: u32) -> bool { +/// +/// ## ⚠️ 必须异步 + 带超时(2026-10-17 真机踩到) +/// 初版用的是 `std::process::Command::output()`:**同步阻塞**且**无超时**。 +/// 在这台机器上 `Get-ScheduledTask` 要 2s、CIM 查询偶发卡死(实测有一次 30s 超时), +/// 于是 `start()` 被永久阻塞 —— 表现为**界面三个按钮一直转圈、PID 不再更新**, +/// 只能重开控制台。异步调用 + 3 秒超时后,最坏情况只是「放弃复用、照常报端口冲突」。 +/// +/// 超时/查询失败一律**判为「不是被控端」**(拒绝接管):宁可让用户看到 +/// 「端口被占用」去手工排查,也不能误杀别人的进程。 +pub async fn looks_like_agent(pid: u32) -> bool { if cfg!(windows) { let script = format!( "(Get-CimInstance Win32_Process -Filter \"ProcessId={pid}\" -ErrorAction SilentlyContinue).CommandLine" ); - let out = std::process::Command::new("powershell") + let fut = tokio::process::Command::new("powershell") .args(["-NoProfile", "-NonInteractive", "-Command", &script]) - .output() - .ok(); - match out { - Some(o) => { - let s = String::from_utf8_lossy(&o.stdout).to_lowercase(); - s.contains("agent.mjs") - } - None => false, + .output(); + match tokio::time::timeout(Duration::from_secs(3), fut).await { + Ok(Ok(o)) => String::from_utf8_lossy(&o.stdout).to_lowercase().contains("agent.mjs"), + // 超时或执行失败:无法确认,判为不是(安全优先) + _ => false, } } else { - let out = std::process::Command::new("ps") + let fut = tokio::process::Command::new("ps") .args(["-p", &pid.to_string(), "-o", "command="]) - .output() - .ok(); - match out { - Some(o) => String::from_utf8_lossy(&o.stdout).contains("agent.mjs"), - None => false, + .output(); + match tokio::time::timeout(Duration::from_secs(3), fut).await { + Ok(Ok(o)) => String::from_utf8_lossy(&o.stdout).contains("agent.mjs"), + _ => false, } } } @@ -548,7 +553,13 @@ impl AgentManager { // - 占用者确实是 nodeagent 被控端(命令行含 agent.mjs)→ **复用**: // 记 PID、置 RUNNING,`external=true`(无 Child 句柄,停止走 taskkill) // - 否则(用户自己的服务 / 别的应用)→ 仍然报错,绝不误接管 - if let Some(pid) = port_owner_pid(port).filter(|p| looks_like_agent(*p)) { + if let Some(pid) = { + // 先取占用者 PID,再**异步**确认它是不是自家被控端(带 3s 超时) + match port_owner_pid(port) { + Some(p) if looks_like_agent(p).await => Some(p), + _ => None, + } + } { { let mut inner = self.inner.lock().await; inner.child = None; diff --git a/apps/console/src-tauri/src/autostart.rs b/apps/console/src-tauri/src/autostart.rs index 7c90281..7dc52e2 100644 --- a/apps/console/src-tauri/src/autostart.rs +++ b/apps/console/src-tauri/src/autostart.rs @@ -189,7 +189,16 @@ fn run_powershell(script: &str) -> Result { } /// 自启是否已启用。 -pub fn is_enabled() -> bool { +/// +/// ## 为什么要缓存(2026-10-17 真机) +/// Windows 上 `schtasks /Query` 实测**约 2 秒**(`Get-ScheduledTask` 同样 2s), +/// 而这个函数每次 `agent_status` 都会被调用 —— 于是每刷新一次界面就白等 2 秒, +/// 叠加多次刷新后表现为「按钮转圈不结束、PID 不再更新」。 +/// +/// 自启开关是低频状态(用户手动切换才会变),故缓存 5 秒即可: +/// 切换自启后紧接着的那次 `status()` 可能读到旧值,但 `set_autostart` 本身 +/// 会把结果写回缓存(见 `invalidate_cache`),界面不会看到错的状态。 +fn is_enabled_uncached() -> bool { if cfg!(target_os = "macos") { macos_plist_path().map(|p| p.exists()).unwrap_or(false) } else if cfg!(windows) { @@ -203,6 +212,34 @@ pub fn is_enabled() -> bool { } } +/// 自启状态缓存:`(取值时刻 ms, 值)`。0 表示从未查过。 +static AUTOSTART_CACHE: std::sync::Mutex<(u64, bool)> = std::sync::Mutex::new((0, false)); +const AUTOSTART_TTL_MS: u64 = 5_000; + +fn now_ms() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0) +} + +pub fn is_enabled() -> bool { + { + let c = AUTOSTART_CACHE.lock().unwrap(); + if c.0 != 0 && now_ms().saturating_sub(c.0) < AUTOSTART_TTL_MS { + return c.1; + } + } + let v = is_enabled_uncached(); + *AUTOSTART_CACHE.lock().unwrap() = (now_ms(), v); + v +} + +/// 切换自启后立刻让缓存失效 —— 否则用户点完开关,界面会显示 5 秒前的旧值。 +pub fn invalidate_cache() { + *AUTOSTART_CACHE.lock().unwrap() = (0, false); +} + /// 读取自启状态的完整快照(供界面展示)。 pub fn status() -> Value { let exe = app_exe(); @@ -253,6 +290,9 @@ pub fn set(enabled: bool) -> Result { windows_schtasks(&exe, enabled)? }; + // 刚改过自启状态,缓存必然过期 —— 不清就会把 5 秒前的旧值回给界面 + invalidate_cache(); + let mut snap = status(); snap["detail"] = json!(detail); Ok(snap)