main
John Lauer Add Rust Codex Desktop bridge with native app tools, auth reuse and plugin lifecycle 7aae933 1mo ago
use serde_json::{json, Value};
use std::{
    io::{Read, Write},
    process::{Command, Stdio},
    thread,
    time::{Duration, Instant},
};

pub fn hidden(command: &mut Command) -> &mut Command {
    #[cfg(windows)]
    {
        use std::os::windows::process::CommandExt;
        command.creation_flags(0x08000000);
    }
    command
}

// Drain both pipes while waiting: a full stderr pipe must never wedge a job.
pub fn run(mut command: Command, input: Option<&[u8]>, timeout: Duration) -> Result<Value, String> {
    hidden(&mut command)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());
    let mut child = command.spawn().map_err(|e| e.to_string())?;
    let mut stdin = child.stdin.take().unwrap();
    if let Some(input) = input {
        stdin.write_all(input).map_err(|e| e.to_string())?;
    }
    drop(stdin);
    let stdout = child.stdout.take().unwrap();
    let stderr = child.stderr.take().unwrap();
    fn drain(mut stream: impl Read) -> Vec<u8> {
        let mut saved = Vec::new();
        let mut buf = [0; 8192];
        while let Ok(n) = stream.read(&mut buf) {
            if n == 0 {
                break;
            }
            let keep = n.min(2_000_000usize.saturating_sub(saved.len()));
            saved.extend_from_slice(&buf[..keep]);
        }
        saved
    }
    let (out_tx, out_rx) = std::sync::mpsc::channel();
    let (err_tx, err_rx) = std::sync::mpsc::channel();
    thread::spawn(move || {
        let _ = out_tx.send(drain(stdout));
    });
    thread::spawn(move || {
        let _ = err_tx.send(drain(stderr));
    });
    let started = Instant::now();
    let mut timed_out = false;
    let status = loop {
        if let Some(status) = child.try_wait().map_err(|e| e.to_string())? {
            break status;
        }
        if started.elapsed() > timeout {
            timed_out = true;
            let _ = child.kill();
            break child.wait().map_err(|e| e.to_string())?;
        }
        thread::sleep(Duration::from_millis(100));
    };
    // A launched descendant can inherit pipes after the direct child exits.
    let out = out_rx.recv_timeout(Duration::from_secs(2));
    let err = err_rx.recv_timeout(Duration::from_secs(2));
    let incomplete = out.is_err() || err.is_err();
    Ok(
        json!({"success":status.success()&&!timed_out&&!incomplete,"exitCode":status.code(),
        "timedOut":timed_out,"outputIncomplete":incomplete,"stdout":String::from_utf8_lossy(&out.unwrap_or_default()),
        "stderr":String::from_utf8_lossy(&err.unwrap_or_default())}),
    )
}

pub fn powershell(script: &str, timeout: Duration) -> Result<Value, String> {
    use base64::Engine;
    let bytes: Vec<u8> = script.encode_utf16().flat_map(u16::to_le_bytes).collect();
    let mut cmd = Command::new("powershell.exe");
    cmd.args([
        "-NoLogo",
        "-NoProfile",
        "-NonInteractive",
        "-EncodedCommand",
        &base64::engine::general_purpose::STANDARD.encode(bytes),
    ]);
    run(cmd, None, timeout)
}

pub fn ps_quote(s: &str) -> String {
    format!("'{}'", s.replace('\'', "''"))
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn quotes_remain_literal() {
        assert_eq!(ps_quote("a'$(whoami);b"), "'a''$(whoami);b'");
    }
    #[cfg(unix)]
    #[test]
    fn drains_both_streams_and_reports_failure() {
        let mut c = Command::new("sh");
        c.args(["-c", "printf result; printf problem >&2; exit 7"]);
        let r = run(c, None, Duration::from_secs(2)).unwrap();
        assert_eq!(r["exitCode"], 7);
        assert_eq!(r["success"], false);
        assert_eq!(r["stdout"], "result");
        assert_eq!(r["stderr"], "problem");
    }
}