//! Shell 执行器 — 通过 tokio::process 执行 shell 命令 use serde::{Deserialize, Serialize}; use std::process::Stdio; /// Shell 命令执行结果 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ShellResult { /// 标准输出 pub stdout: String, /// 标准错误 pub stderr: String, /// 退出码 pub exit_code: Option, /// 执行耗时(毫秒) pub duration_ms: u64, } /// Shell 类型选择 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum ShellType { /// Windows cmd.exe (默认 Windows) Cmd, /// PowerShell 5.x(Windows 自带,不支持 && 运算符) PowerShell, /// PowerShell 7(pwsh,支持 && 运算符;运行时探测,未装回退 PowerShell) Pwsh, /// Unix sh (默认非 Windows) Sh, } impl Default for ShellType { fn default() -> Self { // L1 环境感知:Windows 默认 PowerShell 系(非 Cmd)。PowerShell 对引号/$变量/Unicode 处理 // 远优于 cmd,从根上避 kms 类引号转义地狱(seq26-52 撞墙 20+ 次)。AI 写文件执行见 env_profile。 // BUG-260623-04:优先 pwsh(PS7,支持 && 运算符)——LLM 训练数据 Unix 多,普遍生成 `cd x && y`, // PS5 不支持 && 致命令失败(实测会话 6acb7f9b `cd ... && git init` InvalidEndOfLine)。 // 探测失败(未装 pwsh)回退 PS5。探测结果 OnceLock 缓存(只探一次)。 // 注:Default trait 为同步签名,这里只能读取已探测的缓存结果(若未探测则返回 false,退回 PowerShell)。 // 真实探测在异步入口 `execute()` 中调用 `probe_pwsh().await`。 if cfg!(windows) { if probe_pwsh_cached() { ShellType::Pwsh } else { ShellType::PowerShell } } else { ShellType::Sh } } } /// 读取 probe_pwsh 的缓存值(未探测返回 false)。供同步路径 `Default` 使用。 fn probe_pwsh_cached() -> bool { static CACHE: std::sync::OnceLock = std::sync::OnceLock::new(); CACHE.get().copied().unwrap_or(false) } /// 探测 pwsh(PowerShell 7)是否可用(OnceLock 缓存,只探一次)。 /// /// LLM 普遍生成 `&&`(Unix 习惯),仅 PS7+ 支持,Windows 自带 PS5 不支持。 /// 探测:成功 spawn `pwsh -Command exit 0` 即可用。同步阻塞仅一次(spawn 极快), /// Windows 加 CREATE_NO_WINDOW 防黑窗闪现。 /// /// CR-XX:异步化 —— 在异步上下文中通过 `tokio::task::spawn_blocking` 执行阻塞探测, /// 避免阻塞 tokio runtime。结果仍由 OnceLock 全局共享,只探测一次。 async fn probe_pwsh() -> bool { static CACHE: std::sync::OnceLock = std::sync::OnceLock::new(); if let Some(cached) = CACHE.get() { return *cached; } let result = tokio::task::spawn_blocking(|| { let mut cmd = std::process::Command::new("pwsh"); cmd.arg("-NoProfile").arg("-Command").arg("exit 0"); cmd.stdout(Stdio::null()).stderr(Stdio::null()); #[cfg(windows)] { use std::os::windows::process::CommandExt; cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW } cmd.status().map(|s| s.success()).unwrap_or(false) }) .await .unwrap_or(false); // 多任务竞态时以先到者为准,均等价 let _ = CACHE.set(result); result } /// Shell 命令执行请求 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ShellRequest { /// 要执行的命令 pub command: String, /// 工作目录 pub working_dir: Option, /// 环境变量 pub env: std::collections::HashMap, /// 超时时间(秒),None 表示不超时 pub timeout_secs: Option, /// Shell 类型(默认:Windows→Cmd, 非Windows→Sh) #[serde(default)] pub shell_type: Option, } /// 执行 Shell 命令 /// /// 支持超时(timeout_secs)、环境变量(env)、工作目录(working_dir), /// kill_on_drop(true) 保证超时后子进程不残留,shell_type 可选 Cmd/PowerShell/Sh。 pub async fn execute(request: ShellRequest) -> anyhow::Result { let start = std::time::Instant::now(); // 探测 pwsh(惰性 + OnceLock 全局缓存,只探一次),使后续 ShellType::default() 可读取缓存 #[cfg(windows)] let _ = probe_pwsh().await; let shell_type = request.shell_type.unwrap_or_default(); let mut cmd = match shell_type { ShellType::PowerShell => { let mut c = tokio::process::Command::new("powershell"); c.arg("-NoProfile").arg("-Command").arg(&request.command); c.stdout(Stdio::piped()).stderr(Stdio::piped()); c } ShellType::Pwsh => { // PS7(支持 && 运算符),同 PS5 参数语义。-NoProfile 避加载用户 profile(速度+确定性) let mut c = tokio::process::Command::new("pwsh"); c.arg("-NoProfile").arg("-Command").arg(&request.command); c.stdout(Stdio::piped()).stderr(Stdio::piped()); c } ShellType::Cmd => { let mut c = tokio::process::Command::new("cmd"); c.arg("/C").arg(&request.command); c.stdout(Stdio::piped()).stderr(Stdio::piped()); c } ShellType::Sh => { let mut c = tokio::process::Command::new("sh"); c.arg("-c").arg(&request.command); c.stdout(Stdio::piped()).stderr(Stdio::piped()); c } }; // CR-15-1: kill_on_drop(true) 让 Command 被 drop 时主动 kill 子进程。 // 配合 tokio::time::timeout 超时场景:超时 drop future → Command 析构 → kill 子进程, // 不再让超时后的命令变孤儿继续后台跑(长 hang 命令/死循环仍占资源)。 // 对齐 tool_registry.rs:514「进程已终止」文案名副其实。tokio 1.52.3 支持。 cmd.kill_on_drop(true); // B-260619-01: Windows 下创建子进程默认弹控制台窗口(cmd/powershell 黑窗闪现)。 // CREATE_NO_WINDOW(0x0800_0000) 标志抑制窗口创建,后台静默执行。 // tokio::process::Command 在 Windows 自带 creation_flags 方法(无需 std CommandExt trait)。 #[cfg(windows)] { cmd.creation_flags(0x0800_0000); } if let Some(dir) = &request.working_dir { cmd.current_dir(dir); } for (key, value) in &request.env { cmd.env(key, value); } let output = match request.timeout_secs { Some(secs) => tokio::time::timeout( std::time::Duration::from_secs(secs), cmd.output(), ) .await .map_err(|_| anyhow::anyhow!("命令执行超时({}s): {}", secs, request.command))??, None => cmd.output().await?, }; let duration = start.elapsed().as_millis() as u64; Ok(ShellResult { stdout: String::from_utf8_lossy(&output.stdout).to_string(), stderr: String::from_utf8_lossy(&output.stderr).to_string(), exit_code: output.status.code(), duration_ms: duration, }) }