| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- use std::io;
- use std::process::{Command, Stdio};
- use std::time::Duration;
- use serde::{Deserialize, Serialize};
- use tokio::process::Command as TokioCommand;
- use tokio::runtime::Builder;
- use tokio::time::timeout;
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
- pub struct BashCommandInput {
- pub command: String,
- pub timeout: Option<u64>,
- pub description: Option<String>,
- #[serde(rename = "run_in_background")]
- pub run_in_background: Option<bool>,
- #[serde(rename = "dangerouslyDisableSandbox")]
- pub dangerously_disable_sandbox: Option<bool>,
- }
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
- pub struct BashCommandOutput {
- pub stdout: String,
- pub stderr: String,
- #[serde(rename = "rawOutputPath")]
- pub raw_output_path: Option<String>,
- pub interrupted: bool,
- #[serde(rename = "isImage")]
- pub is_image: Option<bool>,
- #[serde(rename = "backgroundTaskId")]
- pub background_task_id: Option<String>,
- #[serde(rename = "backgroundedByUser")]
- pub backgrounded_by_user: Option<bool>,
- #[serde(rename = "assistantAutoBackgrounded")]
- pub assistant_auto_backgrounded: Option<bool>,
- #[serde(rename = "dangerouslyDisableSandbox")]
- pub dangerously_disable_sandbox: Option<bool>,
- #[serde(rename = "returnCodeInterpretation")]
- pub return_code_interpretation: Option<String>,
- #[serde(rename = "noOutputExpected")]
- pub no_output_expected: Option<bool>,
- #[serde(rename = "structuredContent")]
- pub structured_content: Option<Vec<serde_json::Value>>,
- #[serde(rename = "persistedOutputPath")]
- pub persisted_output_path: Option<String>,
- #[serde(rename = "persistedOutputSize")]
- pub persisted_output_size: Option<u64>,
- }
- pub fn execute_bash(input: BashCommandInput) -> io::Result<BashCommandOutput> {
- if input.run_in_background.unwrap_or(false) {
- let child = Command::new("sh")
- .arg("-lc")
- .arg(&input.command)
- .stdin(Stdio::null())
- .stdout(Stdio::null())
- .stderr(Stdio::null())
- .spawn()?;
- return Ok(BashCommandOutput {
- stdout: String::new(),
- stderr: String::new(),
- raw_output_path: None,
- interrupted: false,
- is_image: None,
- background_task_id: Some(child.id().to_string()),
- backgrounded_by_user: Some(false),
- assistant_auto_backgrounded: Some(false),
- dangerously_disable_sandbox: input.dangerously_disable_sandbox,
- return_code_interpretation: None,
- no_output_expected: Some(true),
- structured_content: None,
- persisted_output_path: None,
- persisted_output_size: None,
- });
- }
- let runtime = Builder::new_current_thread().enable_all().build()?;
- runtime.block_on(execute_bash_async(input))
- }
- async fn execute_bash_async(input: BashCommandInput) -> io::Result<BashCommandOutput> {
- let mut command = TokioCommand::new("sh");
- command.arg("-lc").arg(&input.command);
- let output_result = if let Some(timeout_ms) = input.timeout {
- match timeout(Duration::from_millis(timeout_ms), command.output()).await {
- Ok(result) => (result?, false),
- Err(_) => {
- return Ok(BashCommandOutput {
- stdout: String::new(),
- stderr: format!("Command exceeded timeout of {timeout_ms} ms"),
- raw_output_path: None,
- interrupted: true,
- is_image: None,
- background_task_id: None,
- backgrounded_by_user: None,
- assistant_auto_backgrounded: None,
- dangerously_disable_sandbox: input.dangerously_disable_sandbox,
- return_code_interpretation: Some(String::from("timeout")),
- no_output_expected: Some(true),
- structured_content: None,
- persisted_output_path: None,
- persisted_output_size: None,
- });
- }
- }
- } else {
- (command.output().await?, false)
- };
- let (output, interrupted) = output_result;
- let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
- let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
- let no_output_expected = Some(stdout.trim().is_empty() && stderr.trim().is_empty());
- let return_code_interpretation = output.status.code().and_then(|code| {
- if code == 0 {
- None
- } else {
- Some(format!("exit_code:{code}"))
- }
- });
- Ok(BashCommandOutput {
- stdout,
- stderr,
- raw_output_path: None,
- interrupted,
- is_image: None,
- background_task_id: None,
- backgrounded_by_user: None,
- assistant_auto_backgrounded: None,
- dangerously_disable_sandbox: input.dangerously_disable_sandbox,
- return_code_interpretation,
- no_output_expected,
- structured_content: None,
- persisted_output_path: None,
- persisted_output_size: None,
- })
- }
- #[cfg(test)]
- mod tests {
- use super::{execute_bash, BashCommandInput};
- #[test]
- fn executes_simple_command() {
- let output = execute_bash(BashCommandInput {
- command: String::from("printf 'hello'"),
- timeout: Some(1_000),
- description: None,
- run_in_background: Some(false),
- dangerously_disable_sandbox: Some(false),
- })
- .expect("bash command should execute");
- assert_eq!(output.stdout, "hello");
- assert!(!output.interrupted);
- }
- }
|