Skip to main content

zwire_host/
exec.rs

1//! Subprocess execution.
2//!
3//! `{"cmd":"exec","program":"git","args":["status"],...}` runs a program to
4//! completion and returns its exit code plus base64 stdout/stderr. This is the
5//! bridge that lets any client (emacs, a plugin, another language) drive the
6//! toolchain through the host instead of shelling out itself. The same core
7//! ([`run_raw`]) backs the background [`crate::jobs`] runner.
8use crate::proto::b64_encode;
9use crate::store::expand;
10use serde_json::{json, Value};
11use std::io::Write;
12use std::process::{Command, Stdio};
13
14/// Captured result of running a command to completion.
15pub struct ExecResult {
16    /// Exit code, or `None` if the process was killed by a signal.
17    pub code: Option<i32>,
18    /// Raw stdout bytes.
19    pub stdout: Vec<u8>,
20    /// Raw stderr bytes.
21    pub stderr: Vec<u8>,
22}
23
24/// Run a command to completion, returning raw captured output. Recognised
25/// fields: `program` (required), `args`, `cwd`, `env` (string→string, merged
26/// onto the inherited environment), `stdin` (UTF-8 fed to the child).
27pub fn run_raw(req: &Value) -> Result<ExecResult, String> {
28    let Some(program) = req["program"].as_str() else {
29        return Err("no_program".to_string());
30    };
31    let mut cmd = Command::new(program);
32    if let Some(args) = req["args"].as_array() {
33        for a in args {
34            if let Some(s) = a.as_str() {
35                cmd.arg(s);
36            }
37        }
38    }
39    if let Some(cwd) = req["cwd"].as_str() {
40        cmd.current_dir(expand(cwd));
41    }
42    if let Some(env) = req["env"].as_object() {
43        for (k, v) in env {
44            if let Some(s) = v.as_str() {
45                cmd.env(k, s);
46            }
47        }
48    }
49    cmd.stdin(Stdio::piped())
50        .stdout(Stdio::piped())
51        .stderr(Stdio::piped());
52
53    let mut child = cmd.spawn().map_err(|e| e.to_string())?;
54    // Write stdin if given, then close it so the child sees EOF.
55    if let Some(mut si) = child.stdin.take() {
56        if let Some(input) = req["stdin"].as_str() {
57            let _ = si.write_all(input.as_bytes());
58        }
59    }
60    let out = child.wait_with_output().map_err(|e| e.to_string())?;
61    Ok(ExecResult {
62        code: out.status.code(),
63        stdout: out.stdout,
64        stderr: out.stderr,
65    })
66}
67
68/// Run the `exec` request and return the reply object (base64 stdout/stderr).
69pub fn run(req: &Value) -> Value {
70    match run_raw(req) {
71        Ok(r) => json!({
72            "ok": true,
73            "code": r.code,
74            "stdout": b64_encode(&r.stdout),
75            "stderr": b64_encode(&r.stderr),
76        }),
77        Err(e) => json!({"ok": false, "err": e}),
78    }
79}