fix(console): 消除两处同步阻塞调用 —— 修「按钮一直转圈、PID 不再更新」
## 真机现象 被控端明明在跑,界面上「启动/停止/重启」三个按钮**同时**转圈不结束, 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 项全绿**。
This commit is contained in:
1 parent
9af6e0f73a
commit
0782d901a3
2 files changed
+69
-18
No files matched your search
@@ -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;
|
||||
|
||||
@@ -189,7 +189,16 @@ fn run_powershell(script: &str) -> Result<String, String> {
|
||||
}
|
||||
|
||||
/// 自启是否已启用。
|
||||
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<Value, String> {
|
||||
windows_schtasks(&exe, enabled)?
|
||||
};
|
||||
|
||||
// 刚改过自启状态,缓存必然过期 —— 不清就会把 5 秒前的旧值回给界面
|
||||
invalidate_cache();
|
||||
|
||||
let mut snap = status();
|
||||
snap["detail"] = json!(detail);
|
||||
Ok(snap)
|
||||
|
||||
Reference in new issue
Block a user