From 56f64e1cc1db21c9854deba198f37b5fcdda57a3 Mon Sep 17 00:00:00 2001 From: jkooe <62494645+jkooe@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:16:07 +0800 Subject: [PATCH] =?UTF-8?q?docs(console):=20=E6=8A=8A=20verbatim=20?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E7=9A=84=E8=AF=81=E6=8D=AE=E7=B2=BE=E7=A1=AE?= =?UTF-8?q?=E5=88=B0=E3=80=8C=E8=84=9A=E6=9C=AC=E5=8F=82=E6=95=B0=E3=80=8D?= =?UTF-8?q?=E8=BF=99=E4=B8=80=E7=BB=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 真机二维四格实验(ProcessStartInfo,精确对应 Command::new(prog).arg(script)): G1 plain-prog + plain-script -> exit 0,正常 G2 plain-prog + VERBATIM-script -> exit 1,EISDIR lstat 'C:' G3 VERBATIM-prog + plain-script -> exit 0,正常 G4 VERBATIM-prog + VERBATIM -> exit 1,EISDIR lstat 'C:' 结论:炸的是「脚本参数」带 verbatim,不是「可执行文件」带 verbatim。 旁证:真机上被控端与 sidecar 的 program 都带 \\?\ 却长期存活。 因此改准注释口径:净化 script 是关键修复,净化 node 属防御性 (守住「凡交给外部进程的路径都干净」这条单一纪律,不靠记位置)。 行为零变化(纯注释 + CHANGELOG)。 --- CHANGELOG.md | 24 +++++++++++++++------ apps/console/src-tauri/src/agent.rs | 10 ++++++--- apps/console/src-tauri/src/sidecar.rs | 31 +++++++++++++++++++++------ 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 632a655..7b0cf2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -419,13 +419,25 @@ macOS 上同样两条却是绿的 —— 典型的「只在 Windows 暴露」。 改为**立刻**报「检查更新失败:**sidecar 已退出(stdout 关闭),请重试**」—— 说明被拉起的 sidecar **在请求进行中真的退出了**,只是这次被立刻发现。本节是它的真根因。 -**真机 A/B(决定性的一条)**:Windows 11 ARM64;同一个 `node.exe`、同一份 `sidecar.mjs`, -**只换脚本参数的写法**: +**真机 A/B(决定性的一条)**:Windows 11 ARM64;同一个 `node.exe`、同一份脚本, +用 `ProcessStartInfo` 下发(精确对应 Rust `Command::new(prog).arg(script)` 的形态), +**二维四格**各跑一遍 —— 要回答的是「炸的是哪一个维度」: -| 脚本参数 | 结果 | -|---|---| -| `C:\…\sidecar.mjs` | 2.5 秒返回 `check_update` 结果,进程常驻、心跳持续 | -| `\\?\C:\…\sidecar.mjs` | **0.47 秒即退出,exit 1**,**零 JS 执行** | +| # | `program` | 脚本参数 | 结果 | +|---|---|---|---| +| G1 | `C:\…\node.exe` | `C:\…\mini.mjs` | exit 0,正常输出 ← **= 修复后的形态** | +| G2 | `C:\…\node.exe` | `\\?\C:\…\mini.mjs` | **exit 1**,`EISDIR … lstat 'C:'` | +| G3 | `\\?\C:\…\node.exe` | `C:\…\mini.mjs` | exit 0,正常输出 | +| G4 | `\\?\C:\…\node.exe` | `\\?\C:\…\mini.mjs` | **exit 1**,`EISDIR … lstat 'C:'` | + +**炸的是「脚本参数」带 verbatim,不是「可执行文件」带 verbatim** —— G3 证明 `program` +位置带前缀 node 照常启动;真机上也有旁证:被控端与 sidecar 进程的 `program` 都带 `\\?\` +却长期存活(`Get-CimInstance Win32_Process` 的 CommandLine 可直接看到)。 +修复方向由此确定:**净化脚本参数是关键**,净化 node 属防御性。 + +另有一次更贴近原症状的观测(同一台机器、直接用真 `sidecar.mjs`): +普通路径 **2.5 秒**返回 `check_update` 结果、进程常驻且心跳持续; +`\\?\` 路径 **0.47 秒**即退出、exit 1、**零 JS 执行** —— 连 sidecar 自带的启动日志一行都没写。 「零 JS 执行」是实测出来的:sidecar 自己的启动日志一行没写、审计日志在 `agent.start` 之后 一片空白、判死探针文件 0 字节 —— 连第一行 JS 都没跑到。 diff --git a/apps/console/src-tauri/src/agent.rs b/apps/console/src-tauri/src/agent.rs index 09d0446..a24b0e0 100644 --- a/apps/console/src-tauri/src/agent.rs +++ b/apps/console/src-tauri/src/agent.rs @@ -435,9 +435,13 @@ impl AgentManager { ); // 进程边界:交给 node 的三条路径都先去掉 verbatim 前缀。 - // 被控端的脚本路径来自数据目录(本来就是普通路径),但 node 路径来自 - // `resource_dir()`(Windows 上是 `\\?\C:\…`)—— 与 2026-10-13「sidecar 秒退」 - // 同一类风险,故在此统一兜住,不依赖上游是否记得净化。 + // - `script` 来自数据目录(通常已是普通路径),但一旦带 `\\?\` 就**必炸** —— + // 脚本参数位置是 2026-10-13「sidecar 秒退」的致命维度(真机四格实验 G2 / G4: + // exit 1,`EISDIR ... lstat 'C:'`); + // - `node` 来自 `resource_dir()`(Windows 上是 `\\?\C:\…`),该位置实测**无害** + // (G3 照常启动),净化属防御性; + // - 两条都净化是为了守住「凡交给外部进程的路径都干净」这条单一纪律 —— + // 靠「记住哪个位置安全」省这一步,迟早会把安全的那个挪到致命的位置上。 let node = crate::sidecar::unverbatim(node.to_path_buf()); let script = crate::sidecar::unverbatim(script.to_path_buf()); let data_dir = crate::sidecar::unverbatim(data_dir.to_path_buf()); diff --git a/apps/console/src-tauri/src/sidecar.rs b/apps/console/src-tauri/src/sidecar.rs index 0fa2897..4f98179 100644 --- a/apps/console/src-tauri/src/sidecar.rs +++ b/apps/console/src-tauri/src/sidecar.rs @@ -43,12 +43,26 @@ use tokio::sync::oneshot; /// /// → **exit 1,且一行 JS 都来不及执行**(连 sidecar 自己的启动日志都写不出来)。 /// -/// 真机 A/B(Windows 11 ARM64;控制台 v2.0.3;同一个 `node.exe`、同一份 `sidecar.mjs`): +/// ## 真机 A/B —— 二维四格,精确到是「哪一维」出问题 +/// Windows 11 ARM64;同一个 `node.exe`、同一份最小脚本 `mini.mjs`;用 +/// `ProcessStartInfo` 下发,**精确对应 Rust `Command::new(prog).arg(script)` 的形态**: /// -/// | 脚本参数 | 结果 | -/// |---|---| -/// | `C:\…\sidecar.mjs` | 2.5 秒返回 `check_update` 结果,进程常驻 | -/// | `\\?\C:\…\sidecar.mjs` | **0.5 秒即退出,exit 1**,零 JS 执行 | +/// | # | `program` | 脚本参数 | 结果 | +/// |---|---|---|---| +/// | G1 | `C:\…\node.exe` | `C:\…\mini.mjs` | exit 0,正常输出(**= 修复后的形态**) | +/// | G2 | `C:\…\node.exe` | `\\?\C:\…\mini.mjs` | **exit 1**,`EISDIR … lstat 'C:'` | +/// | G3 | `\\?\C:\…\node.exe` | `C:\…\mini.mjs` | exit 0,正常输出 | +/// | G4 | `\\?\C:\…\node.exe` | `\\?\C:\…\mini.mjs` | **exit 1**,`EISDIR … lstat 'C:'` | +/// +/// **结论**:炸的是「**脚本参数**带 verbatim」,**不是**「可执行文件带 verbatim」—— +/// G3 证明 `program` 位置带前缀 node 照常启动;真机上也有旁证:被控端与 sidecar 进程的 +/// `program` 都带 `\\?\`(见 `Get-CimInstance Win32_Process` 的 CommandLine)却长期存活。 +/// 因此 `sidecar_command` 里**净化 `script` 才是关键修复**,净化 `node` 属防御性 +/// (幂等、无副作用;且 `\\?\` 路径不适合出现在计划任务 `-Execute` 之类场景)。 +/// +/// 另有一次更贴近原症状的观测(同一台机器,直接用真 `sidecar.mjs`): +/// 普通路径 **2.5 秒**返回 `check_update` 结果并常驻; +/// `\\?\` 路径 **0.5 秒**即退出、exit 1、**零 JS 执行**(连自带启动日志都没写出来)。 /// /// 控制台侧前后两种表现都由这一条派生: /// - 修存活检测**之前**:请求写进已死进程的管道 → 干等 45 秒 → 「check_update 超时」; @@ -310,8 +324,11 @@ impl Sidecar { /// /// 优先级:环境变量 `NODEAGENT_CONSOLE_NODE` → 随包 resource 的 `node` → 系统 `node`。 /// -/// ⚠️ 出口一律经 `unverbatim`:`resource_dir()` 在 Windows 上给的是 verbatim 路径, -/// 带 `\\?\` 的路径交给 node 会**启动即 EISDIR 秒退**(详见 `unverbatim` 的说明)。 +/// ⚠️ 出口一律经 `unverbatim`:`resource_dir()` 在 Windows 上给的是 verbatim 路径。 +/// 对本函数而言这是**防御性**净化 —— 真机四格实验(见 `unverbatim`)证明 `program` +/// 位置带 `\\?\` 本身无害(G3 照常启动)。之所以仍然净化,是为了守住「凡从本模块 +/// 出去的路径都干净」这一条**单一纪律**:靠「记住哪些位置安全」来省这一步, +/// 迟早会有人把它挪到参数位置而不自知(那才是 G2/G4 的必炸形态)。 pub fn resolve_node(resource_dir: Option<&Path>) -> Result { if let Ok(p) = std::env::var("NODEAGENT_CONSOLE_NODE") { if !p.is_empty() {