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 的场景:

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 命令字符串:

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 支持三种来源:

pub enum CmdStdin {
    Text(String),
    Bytes(Vec<u8>),
    File(PathBuf),
}
  • Text:按 UTF-8 文本写入。
  • Bytes:按原始字节写入。
  • File:将文件句柄直接绑定为子进程标准输入。

CmdOutput

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

字段说明:

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

使用方式

前台执行

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

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 传文本

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(())
}

后台执行

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::runCmdTool::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 / Bytesstdin 时,现在会在返回前先完成写入

如果上层真的要用它跑后台任务,建议增加一个任务句柄层,而不是直接暴露裸 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 错误不再静默吞掉

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

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