114 lines
3.5 KiB
Rust
114 lines
3.5 KiB
Rust
//! 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<i32>,
|
||
/// 执行耗时(毫秒)
|
||
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
|
||
PowerShell,
|
||
/// Unix sh (默认非 Windows)
|
||
Sh,
|
||
}
|
||
|
||
impl Default for ShellType {
|
||
fn default() -> Self {
|
||
if cfg!(windows) { ShellType::Cmd } else { ShellType::Sh }
|
||
}
|
||
}
|
||
|
||
/// Shell 命令执行请求
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct ShellRequest {
|
||
/// 要执行的命令
|
||
pub command: String,
|
||
/// 工作目录
|
||
pub working_dir: Option<String>,
|
||
/// 环境变量
|
||
pub env: std::collections::HashMap<String, String>,
|
||
/// 超时时间(秒),None 表示不超时
|
||
pub timeout_secs: Option<u64>,
|
||
/// Shell 类型(默认:Windows→Cmd, 非Windows→Sh)
|
||
#[serde(default)]
|
||
pub shell_type: Option<ShellType>,
|
||
}
|
||
|
||
/// 执行 Shell 命令
|
||
///
|
||
/// TODO: 完整实现,支持超时、环境变量、工作目录等
|
||
pub async fn execute(request: ShellRequest) -> anyhow::Result<ShellResult> {
|
||
let start = std::time::Instant::now();
|
||
|
||
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::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);
|
||
|
||
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!("命令执行超时: {}秒", secs))??,
|
||
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,
|
||
})
|
||
}
|