agent-tools 0.1.0

agent command
Documentation
# `tools` crate README

## 概述

`tools` crate 当前提供了一个轻量级命令执行模块 `cmd`,用于在 Rust 代码中启动外部进程、传入工作目录和环境变量、写入标准输入、设置超时,以及按前台或后台模式运行命令。

当前公开接口位于:

- `tools::cmd::CmdTool`
- `tools::cmd::CmdRequest`
- `tools::cmd::ShellCmdRequest`
- `tools::cmd::CmdOutput`
- `tools::cmd::CmdStdin`

其中,crate 根还额外 re-export 了:

- `tools::CmdTool`
- `tools::CmdRequest`
- `tools::ShellCmdRequest`
- `tools::CmdOutput`
- `tools::CmdStdin`

## 适用场景

这个模块适合以下场景:

- 在业务代码中统一封装简单的命令调用
- 向命令注入少量文本、字节或文件作为 `stdin`
- 对短时命令设置执行超时
- 启动无需采集输出的后台任务

如果你的需求包含以下能力,当前实现还不够完整,建议扩展后再用于关键链路:

- 实时流式读取 `stdout` / `stderr`
- 可靠执行高输出量命令
- 强约束 shell 安全边界
- 后台任务生命周期管理
- 跨平台一致的进程组终止语义
- 二进制输出的无损采集

## API 说明

### `CmdRequest`

`CmdRequest` 用于执行普通命令,也就是明确区分 `program + args` 的场景:

```rust
pub struct CmdRequest {
    pub program: String,
    pub args: Vec<String>,
    pub cwd: Option<String>,
    pub env: Option<HashMap<String, String>>,
    pub timeout_ms: Option<u64>,
    pub fail_on_non_zero: bool,
    pub stdin: Option<CmdStdin>,
    pub background: bool,
}
```

字段说明:

- `program`:要执行的程序名。
- `args`:参数列表。
- `cwd`:子进程工作目录。
- `env`:额外环境变量。
- `timeout_ms`:超时时间,单位毫秒。
- `fail_on_non_zero`:是否将非零退出码视为错误。
- `stdin`:标准输入来源。
- `background`:是否后台启动。后台模式下当前实现不会采集输出。

### `ShellCmdRequest`

`ShellCmdRequest` 用于执行整段 shell 命令字符串:

```rust
pub struct ShellCmdRequest {
    pub command: String,
    pub cwd: Option<String>,
    pub env: Option<HashMap<String, String>>,
    pub timeout_ms: Option<u64>,
    pub fail_on_non_zero: bool,
    pub stdin: Option<CmdStdin>,
    pub background: bool,
}
```

字段说明:

- `command`:整段 shell 命令。
- 其余字段含义与 `CmdRequest` 一致。

### `CmdStdin`

`stdin` 支持三种来源:

```rust
pub enum CmdStdin {
    Text(String),
    Bytes(Vec<u8>),
    File(PathBuf),
}
```

- `Text`:按 UTF-8 文本写入。
- `Bytes`:按原始字节写入。
- `File`:将文件句柄直接绑定为子进程标准输入。

### `CmdOutput`

```rust
pub struct CmdOutput {
    pub stdout: String,
    pub stderr: String,
    pub exit_code: i32,
    pub pid: Option<u32>,
}
```

字段说明:

- `stdout`:前台执行时采集到的标准输出。
- `stderr`:前台执行时采集到的标准错误。
- `exit_code`:进程退出码。若因信号退出,当前实现会返回 `-1`- `pid`:仅后台模式返回子进程 PID。

## 使用方式

### 前台执行

```rust
use tools::{CmdRequest, CmdTool};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let output = CmdTool::run(CmdRequest {
        program: "echo".to_string(),
        args: vec!["hello".to_string()],
        cwd: None,
        env: None,
        timeout_ms: Some(1_000),
        fail_on_non_zero: true,
        stdin: None,
        background: false,
    })?;

    assert_eq!(output.exit_code, 0);
    assert_eq!(output.stdout.trim(), "hello");
    Ok(())
}
```

### 使用 shell

当命令包含管道、重定向、通配符或 shell 内建语法时,使用 `CmdTool::run_shell`:

```rust
use tools::{CmdTool, ShellCmdRequest};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let output = CmdTool::run_shell(ShellCmdRequest {
        command: "echo 'hello world' | grep world".to_string(),
        cwd: None,
        env: None,
        timeout_ms: Some(1_000),
        fail_on_non_zero: true,
        stdin: None,
        background: false,
    })?;

    assert_eq!(output.exit_code, 0);
    assert!(output.stdout.contains("world"));
    Ok(())
}
```

注意:

- 非 Windows 平台使用 `sh -c`,Windows 使用 `cmd.exe /c`- 推荐默认优先使用 `CmdTool::run`,只有确实需要 shell 语法时才用 `run_shell`
### 通过 `stdin` 传文本

```rust
use tools::cmd::CmdStdin;
use tools::{CmdRequest, CmdTool};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let output = CmdTool::run(CmdRequest {
        program: "cat".to_string(),
        args: vec![],
        cwd: None,
        env: None,
        timeout_ms: Some(1_000),
        fail_on_non_zero: true,
        stdin: Some(CmdStdin::Text("hello stdin".to_string())),
        background: false,
    })?;

    assert_eq!(output.stdout, "hello stdin");
    Ok(())
}
```

### 后台执行

```rust
use tools::{CmdRequest, CmdTool};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let output = CmdTool::run(CmdRequest {
        program: "sleep".to_string(),
        args: vec!["10".to_string()],
        cwd: None,
        env: None,
        timeout_ms: None,
        fail_on_non_zero: false,
        stdin: None,
        background: true,
    })?;

    assert!(output.pid.is_some());
    Ok(())
}
```

注意:

- 后台模式只表示“成功启动”,不表示任务已执行成功。
- 当前实现不会跟踪后台任务,也不会主动回收其退出状态。

## 错误语义

当前 crate 使用 `error` crate 的统一错误类型,`CmdTool::run` 和 `CmdTool::run_shell` 主要可能返回以下错误:

- IO 错误:命令不存在、文件打不开、进程创建失败、等待失败等
- 超时错误:超过 `timeout_ms` 后返回 `Command timed out`
- 非零退出码错误:当 `fail_on_non_zero = true` 时,返回 `Command failed with exit code N`

退出码语义如下:

- `fail_on_non_zero = false`:即使退出码非 0,也返回 `Ok(CmdOutput)`,由调用方自行检查 `exit_code`
- `fail_on_non_zero = true`:退出码非 0 时返回 `Err`

当前限制:

- 返回 `Err` 时,错误对象里只保留退出码,没有携带 `stdout` / `stderr`
- 如果你的上层需要失败输出,建议后续扩展结构化错误类型

## 生产环境注意事项

### 1. 已修复:大输出命令的阻塞风险

当前实现已经改为并发读取 `stdout` / `stderr`,不再是“先 `wait()` 再读输出”的模型,因此相比之前版本,已经规避了最典型的 pipe buffer 死锁问题。

不过仍有两个边界需要注意:

- 输出会完整读入内存,超大输出仍可能造成内存压力
- 当前没有提供流式消费接口

如果要用于不受控输出的命令,建议再增加输出大小限制或流式回调接口

### 2. 已修复:`stdin` / `stdout` / `stderr` 的 IO 错误静默吞掉

当前实现已经把这些 IO 操作改成显式错误返回,不再静默忽略:

- 写入 `stdin`
- 读取 `stdout`
- 读取 `stderr`

### 3. 部分缓解:非 UTF-8 输出不再直接丢失

当前实现会先按字节读取输出,再使用 `String::from_utf8_lossy` 转成文本。这意味着:

- 非 UTF-8 输出不再因为 `read_to_string` 失败而被直接吞掉
- 非法字节会被替换字符表示,文本查看更稳妥

但它仍然不是“无损二进制输出”方案。如果此模块要支持编译器产物、压缩流、图片或任意二进制命令输出,建议后续补充:

- `stdout_bytes` / `stderr_bytes`
- 或直接把输出主体改为 `Vec<u8>`,文本视图作为辅助层提供

### 4. 超时终止不够彻底

当前超时逻辑只对当前子进程执行 `kill()`。当使用 `run_shell` 时,常见问题是:

- 被 kill 的只是 shell 进程
- shell 拉起的孙子进程可能继续运行

如果需要生产级行为,建议按平台补齐:

- Unix 上使用进程组并 kill 整个进程组
- Windows 上使用 Job Object 或等价机制

### 5. 后台任务缺少生命周期管理

当前后台模式只返回 PID,不提供:

- 查询状态
- 回收退出码
- 终止任务
- 日志路径
- 标准输出持久化

另外,和旧版本相比,当前实现已经修复了一个实际 bug:

- 后台模式下传入 `Text` / `Bytes``stdin` 时,现在会在返回前先完成写入

如果上层真的要用它跑后台任务,建议增加一个任务句柄层,而不是直接暴露裸 PID。

### 6. shell 入口的安全边界需要明确

当前 `run_shell` 会把完整字符串交给 shell 解释。这意味着:

- 存在命令注入风险

建议在 API 层明确约束:

- 默认禁止不受信任输入进入 `command`
- README 和 doc comment 中明确这是高风险模式

## 建议补齐的能力

如果目标是“可在生产代码中稳定复用”的命令执行模块,建议优先补齐以下能力:

1. 非零退出码策略
2. 失败错误中保留 `stdout` / `stderr`
3. 二进制输出支持
4. 输出大小限制,避免内存被异常放大
5. 进程组级超时终止
6. 后台任务管理接口
7. 更完整的测试覆盖,包括大输出、shell 子进程超时、后台任务回收

## 当前测试覆盖

仓库内现有测试已经覆盖:

- 正常命令执行
- shell 命令执行
- 超时
- 不存在的命令
- `stdin` 文本 / 字节 / 文件
- 后台执行
- shell 管道
- 非零退出码的双模式处理
- 非 UTF-8 输出的 lossy 解码

但仍缺少更关键的生产级测试:

- 大输出下的压力测试
- `stdin` 写入失败时的行为
- shell 派生子进程在超时后的残留情况
- 后台进程退出后的资源回收

## 结论

当前 `cmd` 模块适合做一个轻量、可工作的命令调用封装原型;如果按生产级标准判断,它还没有完全达到“通用、安全、稳定”的命令执行基础设施水平。

这版实现已经把最直接的稳定性问题补上了两项:

- 并发读取输出,避免常见的大输出阻塞
- `stdin/stdout/stderr` 的 IO 错误不再静默吞掉

剩下最需要继续往生产级推进的点是:

- 超时场景下的进程组清理
- 失败错误中保留输出内容
- 后台任务的生命周期管理