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    let binary = resolve_binary()?;
39    let mut cmd = Command::new(binary);
40    cmd.args(with_globals(args, log_level));
41    for (key, value) in env {
42        cmd.env(key, value);
43    }
44    cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
45    if stdin.is_some() {
46        cmd.stdin(Stdio::piped());
47    }
48
49    let mut child = cmd.spawn()?;
50
51    // Feed stdin and drain stderr on their own threads so no pipe can fill up
52    // and deadlock against our sequential reads — the same job Python's
53    // `communicate()` does with its worker threads.
54    let stdin_thread = stdin.map(|bytes| {
55        let mut handle = child.stdin.take().expect("stdin was piped");
56        let owned = bytes.to_vec();
57        std::thread::spawn(move || {
58            let _ = handle.write_all(&owned);
59        })
60    });
61    let stderr_thread = {
62        let mut handle = child.stderr.take().expect("stderr was piped");
63        std::thread::spawn(move || {
64            let mut buf = Vec::new();
65            let _ = handle.read_to_end(&mut buf);
66            buf
67        })
68    };
69
70    let mut stdout_buf = Vec::new();
71    if let Some(mut out) = child.stdout.take() {
72        out.read_to_end(&mut stdout_buf)?;
73    }
74    let stderr_buf = stderr_thread.join().unwrap_or_default();
75    if let Some(t) = stdin_thread {
76        let _ = t.join();
77    }
78    let status = child.wait()?;
79
80    Ok(RawResult {
81        stdout: String::from_utf8_lossy(&stdout_buf).into_owned(),
82        stderr: String::from_utf8_lossy(&stderr_buf).into_owned(),
83        exit_code: status.code().unwrap_or(-1),
84    })
85}
86
87/// Run `bsdkrun <args>` quietly (log level 0) and capture the result.
88pub fn run<I, S>(args: I) -> Result<RawResult>
89where
90    I: IntoIterator<Item = S>,
91    S: Into<String>,
92{
93    let argv: Vec<String> = args.into_iter().map(Into::into).collect();
94    run_full(&argv, &[], None, 0)
95}
96
97/// Like [`run`], but a non-zero exit becomes [`Error::CommandFailed`] tagged
98/// with `label`.
99pub fn run_checked<I, S>(args: I, label: &str) -> Result<RawResult>
100where
101    I: IntoIterator<Item = S>,
102    S: Into<String>,
103{
104    let argv: Vec<String> = args.into_iter().map(Into::into).collect();
105    checked(&argv, label)
106}
107
108pub(crate) fn checked(args: &[String], label: &str) -> Result<RawResult> {
109    let result = run_full(args, &[], None, 0)?;
110    if result.exit_code != 0 {
111        return Err(Error::CommandFailed {
112            exit_code: result.exit_code,
113            stdout: result.stdout,
114            stderr: result.stderr,
115            command: label.to_string(),
116        });
117    }
118    Ok(result)
119}
120
121/// Run `bsdkrun <args>` inheriting the parent's stdio (interactive).
122///
123/// Blocks until the child exits and returns its exit code. Used by
124/// [`crate::Sandbox::shell`].
125pub fn spawn<I, S>(args: I) -> Result<i32>
126where
127    I: IntoIterator<Item = S>,
128    S: Into<String>,
129{
130    let binary = resolve_binary()?;
131    let argv: Vec<String> = args.into_iter().map(Into::into).collect();
132    let status = Command::new(binary).args(with_globals(&argv, 0)).status()?;
133    Ok(status.code().unwrap_or(-1))
134}