From 0782d901a3f1ae6f2a8ff004f9285d3f479368d5 Mon Sep 17 00:00:00 2001 From: jkooe <62494645+jkooe@users.noreply.github.com> Date: Sat, 17 Oct 2026 21:55:44 +0800 Subject: [PATCH] =?UTF-8?q?fix(console):=20=E6=B6=88=E9=99=A4=E4=B8=A4?= =?UTF-8?q?=E5=A4=84=E5=90=8C=E6=AD=A5=E9=98=BB=E5=A1=9E=E8=B0=83=E7=94=A8?= =?UTF-8?q?=20=E2=80=94=E2=80=94=20=E4=BF=AE=E3=80=8C=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E4=B8=80=E7=9B=B4=E8=BD=AC=E5=9C=88=E3=80=81PID=20=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E6=9B=B4=E6=96=B0=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 真机现象 被控端明明在跑,界面上「启动/停止/重启」三个按钮**同时**转圈不结束, PID 停在旧值(显示 30160,实际已是 22948),点导航也不切页。 只能重开控制台。 ## 根因一(臣上一版自己引入的) `looks_like_agent()` 用 `std::process::Command::output()` 调 PowerShell: **同步阻塞 + 无超时**。而这台机器上 PowerShell/CIM 本身就慢甚至偶发卡死 (实测 `Get-ScheduledTask` 2.0s、`Get-NetTCPConnection` 1.7s, 另有一次 CIM 查询直接 30s 超时)。 它在 `start()` 里被调用(复用判定),一旦卡住就**永久占住运行时线程** → `run()` 的 finally 里的 `refresh()` 永远走不到 → `busy` 卡死、状态不再更新。 连带 sidecar 的请求也拿不到执行机会,表现为「重连中」但审计里没有任何握手记录。 改:`looks_like_agent` 改 async + `tokio::process` + **3 秒超时**。 超时/失败一律**判为「不是被控端」**(拒绝接管)—— 宁可让用户看到「端口被占用」 去手工排查,也不能误杀别人的进程。 ## 根因二 `autostart::is_enabled()` 每次 `agent_status` 都会跑一次 `schtasks /Query` (实测约 2 秒)。每刷新一次界面白等 2 秒,叠加后同样表现为转圈不结束。 改:加 5 秒 TTL 缓存(自启是低频状态),并在 `set()` 后 `invalidate_cache()`, 保证用户点完开关立刻看到新值而不是 5 秒前的旧值。 验证:`cargo build` / `cargo clippy` 干净;`cargo test --lib` **21 项全绿**。 --- apps/console/src-tauri/src/agent.rs | 45 +++++++++++++++---------- apps/console/src-tauri/src/autostart.rs | 42 ++++++++++++++++++++++- 2 files changed, 69 insertions(+), 18 deletions(-) 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)