openrunner 1.0.0

A Rust library for running OpenScript
Documentation
//! Core execution functions for running OpenScript code.

use crate::error::{Error, Result};
use crate::types::{ExecResult, IoOptions, ScriptOptions};
use std::io::Write;
use std::path::PathBuf;
use std::process::Stdio;
use std::time::Instant;
use tempfile::NamedTempFile;
use tokio::process::{Child, Command};
use tokio::time::timeout;

/// Run OpenScript code from a string and wait for completion.
///
/// This function creates a temporary file with the script content and executes it
/// using the configured OpenScript interpreter.
///
/// # Arguments
///
/// * `script` - The OpenScript code to execute
/// * `options` - Configuration options for script execution
///
/// # Returns
///
/// Returns an `ExecResult` containing the exit code, stdout, stderr, duration, and timeout status.
///
/// # Examples
///
/// ```rust
/// use openrunner::{run, ScriptOptions};
///
/// # #[tokio::main]
/// # async fn main() -> openrunner::Result<()> {
/// let options = ScriptOptions::new().openscript_path("/bin/sh");
/// let result = run("echo 'Hello, World!'", options).await?;
/// assert_eq!(result.exit_code, 0);
/// assert!(result.stdout.contains("Hello, World!"));
/// # Ok(())
/// # }
/// ```
pub async fn run(script: &str, options: ScriptOptions) -> Result<ExecResult> {
    let mut temp_file = NamedTempFile::new().map_err(|e| Error::TempFile(e.to_string()))?;
    writeln!(temp_file, "{}", script).map_err(|e| Error::TempFile(e.to_string()))?;
    let path = temp_file.path().to_path_buf();
    run_file(&path, options).await
}

/// Run OpenScript code from a file and wait for completion.
///
/// # Arguments
///
/// * `path` - Path to the OpenScript file to execute
/// * `options` - Configuration options for script execution
///
/// # Returns
///
/// Returns an `ExecResult` containing the execution results.
///
/// # Examples
///
/// ```rust
/// use openrunner::{run_file, ScriptOptions};
/// use std::path::PathBuf;
///
/// # #[tokio::main]
/// # async fn main() -> openrunner::Result<()> {
/// # use std::io::Write;
/// # let mut file = tempfile::NamedTempFile::new().unwrap();
/// # writeln!(file, "echo 'test'").unwrap();
/// # let script_path = file.path().to_path_buf();
/// let options = ScriptOptions::new().openscript_path("/bin/sh");
/// let result = run_file(&script_path, options).await?;
/// println!("Exit code: {}", result.exit_code);
/// # Ok(())
/// # }
/// ```
pub async fn run_file(path: &PathBuf, options: ScriptOptions) -> Result<ExecResult> {
    let child = spawn_file(path, options.clone()).await?;
    let start_time = Instant::now();

    if let Some(duration) = options.timeout {
        let child_id = child.id();
        match timeout(duration, child.wait_with_output()).await {
            Ok(Ok(output)) => {
                let duration = start_time.elapsed();
                let stdout = String::from_utf8(output.stdout)
                    .map_err(|e| Error::StdoutCapture(e.to_string()))?;
                let stderr = String::from_utf8(output.stderr)
                    .map_err(|e| Error::StderrCapture(e.to_string()))?;
                Ok(ExecResult {
                    exit_code: output.status.code().unwrap_or(-1),
                    stdout,
                    stderr,
                    duration,
                    timed_out: false,
                })
            }
            Ok(Err(e)) => Err(Error::Io(e)),
            Err(_) => {
                // Process timed out, try to kill it if we have the ID
                if let Some(id) = child_id {
                    // On Unix systems, try to kill the process group
                    #[cfg(unix)]
                    {
                        use std::process::Command as StdCommand;
                        let _ = StdCommand::new("kill").arg(format!("{}", id)).output();
                    }
                }
                Ok(ExecResult {
                    exit_code: -1,
                    stdout: String::new(),
                    stderr: String::new(),
                    duration: start_time.elapsed(),
                    timed_out: true,
                })
            }
        }
    } else {
        let output = child.wait_with_output().await?;
        let duration = start_time.elapsed();
        let stdout = String::from_utf8(output.stdout)
            .map_err(|e| Error::StdoutCapture(e.to_string()))?;
        let stderr = String::from_utf8(output.stderr)
            .map_err(|e| Error::StderrCapture(e.to_string()))?;
        Ok(ExecResult {
            exit_code: output.status.code().unwrap_or(-1),
            stdout,
            stderr,
            duration,
            timed_out: false,
        })
    }
}

/// Spawn an OpenScript process from a string without waiting for completion.
///
/// This function creates a temporary file with the script content and spawns
/// the OpenScript process, returning a `Child` handle for further interaction.
///
/// # Arguments
///
/// * `script` - The OpenScript code to execute
/// * `options` - Configuration options for script execution
///
/// # Returns
///
/// Returns a `Child` process handle that can be used to interact with the running process.
///
/// # Examples
///
/// ```rust
/// use openrunner::{spawn, ScriptOptions};
///
/// # #[tokio::main]
/// # async fn main() -> openrunner::Result<()> {
/// let options = ScriptOptions::new().openscript_path("/bin/sh");
/// let child = spawn("echo 'Background task'", options).await?;
/// let output = child.wait_with_output().await?;
/// println!("Output: {}", String::from_utf8_lossy(&output.stdout));
/// # Ok(())
/// # }
/// ```
pub async fn spawn(script: &str, options: ScriptOptions) -> Result<Child> {
    let mut temp_file = NamedTempFile::new().map_err(|e| Error::TempFile(e.to_string()))?;
    writeln!(temp_file, "{}", script).map_err(|e| Error::TempFile(e.to_string()))?;
    let path = temp_file.path().to_path_buf();
    spawn_file(&path, options).await
}

/// Spawn an OpenScript process from a file without waiting for completion.
///
/// # Arguments
///
/// * `path` - Path to the OpenScript file to execute
/// * `options` - Configuration options for script execution
///
/// # Returns
///
/// Returns a `Child` process handle.
pub async fn spawn_file(path: &PathBuf, options: ScriptOptions) -> Result<Child> {
    let openscript_path = options
        .openscript_path
        .unwrap_or_else(|| PathBuf::from("openscript"));

    let mut cmd = Command::new(&openscript_path);

    // If this looks like a shell, use -c flag to execute the script
    if let Some(path_str) = openscript_path.to_str() {
        if path_str.ends_with("sh") || path_str.ends_with("bash") || path_str.ends_with("zsh") {
            cmd.arg("-c");
            // Read the script content and pass it as an argument
            let script_content =
                std::fs::read_to_string(path).map_err(|_| Error::InvalidPath)?;
            cmd.arg(script_content);
            // For shell -c, we need to pass the shell name as $0, then the args as $1, $2, etc.
            cmd.arg("sh"); // This becomes $0
            cmd.args(&options.args); // These become $1, $2, etc.
        } else {
            cmd.arg(path);
            cmd.args(&options.args);
        }
    } else {
        cmd.arg(path);
        cmd.args(&options.args);
    }

    if let Some(cwd) = options.working_directory {
        cmd.current_dir(cwd);
    }

    if options.clear_env {
        cmd.env_clear();
    }
    cmd.envs(options.env_vars);

    cmd.stdin(convert_io(options.stdin));
    cmd.stdout(convert_io(options.stdout));
    cmd.stderr(convert_io(options.stderr));

    let child = cmd.spawn().map_err(|e| {
        if e.kind() == std::io::ErrorKind::NotFound {
            Error::OpenScriptNotFound
        } else {
            Error::CommandFailed(e.to_string())
        }
    })?;

    Ok(child)
}

/// Convert IoOptions to tokio Stdio.
fn convert_io(io_options: IoOptions) -> Stdio {
    match io_options {
        IoOptions::Inherit => Stdio::inherit(),
        IoOptions::Pipe => Stdio::piped(),
        IoOptions::Null => Stdio::null(),
    }
}