Skip to main content

bsdkrun_sdk/
process.rs

1//! Run the `bsdkrun` CLI and capture its output.
2//!
3//! Every invocation is prepended with the global `--log-level` flag (default 0)
4//! so the SDK's captured output stays clean. Raise it for boot diagnostics.
5
6use std::io::{Read, Write};
7use std::process::{Command, Stdio};
8
9use crate::binary::resolve_binary;
10use crate::error::{Error, Result};
11
12/// The buffered result of a `bsdkrun` invocation.
13#[derive(Debug, Clone)]
14pub struct RawResult {
15    pub stdout: String,
16    pub stderr: String,
17    pub exit_code: i32,
18}
19
20fn with_globals(args: &[String], log_level: u32) -> Vec<String> {
21    let mut out = vec!["--log-level".to_string(), log_level.to_string()];
22    out.extend(args.iter().cloned());
23    out
24}
25
26/// Run `bsdkrun <args>` to completion, buffering stdout/stderr.
27///
28/// `env` is merged onto the current process environment. `stdin`, if given, is
29/// piped to the child (otherwise the child inherits ours, exactly as Python's
30/// `subprocess.run(input=None)` does). `log_level` sets bsdkrun's global
31/// `--log-level`.
32pub(crate) fn run_full(
33    args: &[String],
34    env: &[(String, String)],
35    stdin: Option<&[u8]>,
36    log_level: u32,
37) -> Result<RawResult> {
38    run_full_stream(args, env, stdin, log_level, None, None)
39}
40
41pub(crate) fn run_full_stream(
42    args: &[String],
43    env: &[(String, String)],
44    stdin: Option<&[u8]>,
45    log_level: u32,
46    mut on_stdout: Option<Box<dyn Write + Send>>,
47    mut on_stderr: Option<Box<dyn Write + Send>>,
48) -> Result<RawResult> {
49    let binary = resolve_binary()?;
50    let mut cmd = Command::new(binary);
51    cmd.args(with_globals(args, log_level));
52    for (key, value) in env {
53        cmd.env(key, value);
54    }
55    cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
56    if stdin.is_some() {
57        cmd.stdin(Stdio::piped());
58    }
59
60    let mut child = cmd.spawn()?;
61
62    // Feed stdin and drain stderr on their own threads so no pipe can fill up
63    // and deadlock against our sequential reads — the same job Python's
64    // `communicate()` does with its worker threads.
65    let stdin_thread = stdin.map(|bytes| {
66        let mut handle = child.stdin.take().expect("stdin was piped");
67        let owned = bytes.to_vec();
68        std::thread::spawn(move || {
69            let _ = handle.write_all(&owned);
70        })
71    });
72    let stderr_thread = {
73        let mut handle = child.stderr.take().expect("stderr was piped");
74        std::thread::spawn(move || {
75            let mut buf = Vec::new();
76            let mut chunk = [0_u8; 8192];
77            loop {
78                match handle.read(&mut chunk) {
79                    Ok(0) | Err(_) => break,
80                    Ok(n) => {
81                        buf.extend_from_slice(&chunk[..n]);
82                        if let Some(w) = on_stderr.as_mut() {
83                            let _ = w.write_all(&chunk[..n]);
84                        }
85                    }
86                }
87            }
88            buf
89        })
90    };
91
92    let mut stdout_buf = Vec::new();
93    if let Some(mut out) = child.stdout.take() {
94        let mut chunk = [0_u8; 8192];
95        loop {
96            let n = out.read(&mut chunk)?;
97            if n == 0 {
98                break;
99            }
100            stdout_buf.extend_from_slice(&chunk[..n]);
101            if let Some(w) = on_stdout.as_mut() {
102                w.write_all(&chunk[..n])?;
103            }
104        }
105    }
106    let stderr_buf = stderr_thread.join().unwrap_or_default();
107    if let Some(t) = stdin_thread {
108        let _ = t.join();
109    }
110    let status = child.wait()?;
111
112    Ok(RawResult {
113        stdout: String::from_utf8_lossy(&stdout_buf).into_owned(),
114        stderr: String::from_utf8_lossy(&stderr_buf).into_owned(),
115        exit_code: status.code().unwrap_or(-1),
116    })
117}
118
119/// Run `bsdkrun <args>` quietly (log level 0) and capture the result.
120pub fn run<I, S>(args: I) -> Result<RawResult>
121where
122    I: IntoIterator<Item = S>,
123    S: Into<String>,
124{
125    let argv: Vec<String> = args.into_iter().map(Into::into).collect();
126    run_full(&argv, &[], None, 0)
127}
128
129/// Like [`run`], but a non-zero exit becomes [`Error::CommandFailed`] tagged
130/// with `label`.
131pub fn run_checked<I, S>(args: I, label: &str) -> Result<RawResult>
132where
133    I: IntoIterator<Item = S>,
134    S: Into<String>,
135{
136    let argv: Vec<String> = args.into_iter().map(Into::into).collect();
137    checked(&argv, label)
138}
139
140pub(crate) fn checked(args: &[String], label: &str) -> Result<RawResult> {
141    let result = run_full(args, &[], None, 0)?;
142    if result.exit_code != 0 {
143        return Err(Error::CommandFailed {
144            exit_code: result.exit_code,
145            stdout: result.stdout,
146            stderr: result.stderr,
147            command: label.to_string(),
148        });
149    }
150    Ok(result)
151}
152
153/// Run `bsdkrun <args>` inheriting the parent's stdio (interactive).
154///
155/// Blocks until the child exits and returns its exit code. Used by
156/// [`crate::Sandbox::shell`].
157pub fn spawn<I, S>(args: I) -> Result<i32>
158where
159    I: IntoIterator<Item = S>,
160    S: Into<String>,
161{
162    let binary = resolve_binary()?;
163    let argv: Vec<String> = args.into_iter().map(Into::into).collect();
164    let status = Command::new(binary).args(with_globals(&argv, 0)).status()?;
165    Ok(status.code().unwrap_or(-1))
166}