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    on_stdout: Option<Box<dyn Write + Send>>,
47    on_stderr: Option<Box<dyn Write + Send>>,
48) -> Result<RawResult> {
49    let (stdout, stderr, exit_code) = run_raw(args, env, stdin, log_level, on_stdout, on_stderr)?;
50    Ok(RawResult {
51        stdout: String::from_utf8_lossy(&stdout).into_owned(),
52        stderr: String::from_utf8_lossy(&stderr).into_owned(),
53        exit_code,
54    })
55}
56
57/// The undecoded form of [`run_full_stream`]: `(stdout, stderr, exit code)`.
58fn run_raw(
59    args: &[String],
60    env: &[(String, String)],
61    stdin: Option<&[u8]>,
62    log_level: u32,
63    mut on_stdout: Option<Box<dyn Write + Send>>,
64    mut on_stderr: Option<Box<dyn Write + Send>>,
65) -> Result<(Vec<u8>, Vec<u8>, i32)> {
66    let binary = resolve_binary()?;
67    let mut cmd = Command::new(binary);
68    cmd.args(with_globals(args, log_level));
69    for (key, value) in env {
70        cmd.env(key, value);
71    }
72    cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
73    if stdin.is_some() {
74        cmd.stdin(Stdio::piped());
75    }
76
77    let mut child = cmd.spawn()?;
78
79    // Feed stdin and drain stderr on their own threads so no pipe can fill up
80    // and deadlock against our sequential reads — the same job Python's
81    // `communicate()` does with its worker threads.
82    let stdin_thread = stdin.map(|bytes| {
83        let mut handle = child.stdin.take().expect("stdin was piped");
84        let owned = bytes.to_vec();
85        std::thread::spawn(move || {
86            let _ = handle.write_all(&owned);
87        })
88    });
89    let stderr_thread = {
90        let mut handle = child.stderr.take().expect("stderr was piped");
91        std::thread::spawn(move || {
92            let mut buf = Vec::new();
93            let mut chunk = [0_u8; 8192];
94            loop {
95                match handle.read(&mut chunk) {
96                    Ok(0) | Err(_) => break,
97                    Ok(n) => {
98                        buf.extend_from_slice(&chunk[..n]);
99                        if let Some(w) = on_stderr.as_mut() {
100                            let _ = w.write_all(&chunk[..n]);
101                        }
102                    }
103                }
104            }
105            buf
106        })
107    };
108
109    let mut stdout_buf = Vec::new();
110    if let Some(mut out) = child.stdout.take() {
111        let mut chunk = [0_u8; 8192];
112        loop {
113            let n = out.read(&mut chunk)?;
114            if n == 0 {
115                break;
116            }
117            stdout_buf.extend_from_slice(&chunk[..n]);
118            if let Some(w) = on_stdout.as_mut() {
119                w.write_all(&chunk[..n])?;
120            }
121        }
122    }
123    let stderr_buf = stderr_thread.join().unwrap_or_default();
124    if let Some(t) = stdin_thread {
125        let _ = t.join();
126    }
127    let status = child.wait()?;
128
129    Ok((stdout_buf, stderr_buf, status.code().unwrap_or(-1)))
130}
131
132/// Like [`RawResult`], but stdout is left as bytes.
133#[derive(Debug, Clone)]
134pub struct BinaryResult {
135    pub stdout: Vec<u8>,
136    pub stderr: String,
137    pub exit_code: i32,
138}
139
140/// Run `bsdkrun <args>` and keep stdout as raw bytes.
141///
142/// [`run`] decodes stdout with `from_utf8_lossy`, which silently replaces every
143/// invalid byte with U+FFFD — fine for JSON, ruinous for `cp ID:path -` reading
144/// a PNG. Only stderr is decoded here, because it is always a message.
145pub fn run_binary<I, S>(args: I, stdin: Option<&[u8]>) -> Result<BinaryResult>
146where
147    I: IntoIterator<Item = S>,
148    S: Into<String>,
149{
150    let argv: Vec<String> = args.into_iter().map(Into::into).collect();
151    let (stdout, stderr, exit_code) = run_raw(&argv, &[], stdin, 0, None, None)?;
152    Ok(BinaryResult {
153        stdout,
154        stderr: String::from_utf8_lossy(&stderr).into_owned(),
155        exit_code,
156    })
157}
158
159/// Run `bsdkrun <args>` quietly (log level 0) and capture the result.
160pub fn run<I, S>(args: I) -> Result<RawResult>
161where
162    I: IntoIterator<Item = S>,
163    S: Into<String>,
164{
165    let argv: Vec<String> = args.into_iter().map(Into::into).collect();
166    run_full(&argv, &[], None, 0)
167}
168
169/// Like [`run`], but a non-zero exit becomes [`Error::CommandFailed`] tagged
170/// with `label`.
171pub fn run_checked<I, S>(args: I, label: &str) -> Result<RawResult>
172where
173    I: IntoIterator<Item = S>,
174    S: Into<String>,
175{
176    let argv: Vec<String> = args.into_iter().map(Into::into).collect();
177    checked(&argv, label)
178}
179
180pub(crate) fn checked(args: &[String], label: &str) -> Result<RawResult> {
181    let result = run_full(args, &[], None, 0)?;
182    if result.exit_code != 0 {
183        return Err(Error::CommandFailed {
184            exit_code: result.exit_code,
185            stdout: result.stdout,
186            stderr: result.stderr,
187            command: label.to_string(),
188        });
189    }
190    Ok(result)
191}
192
193/// Run `bsdkrun <args>` inheriting the parent's stdio (interactive).
194///
195/// Blocks until the child exits and returns its exit code. Used by
196/// [`crate::Sandbox::shell`].
197pub fn spawn<I, S>(args: I) -> Result<i32>
198where
199    I: IntoIterator<Item = S>,
200    S: Into<String>,
201{
202    let binary = resolve_binary()?;
203    let argv: Vec<String> = args.into_iter().map(Into::into).collect();
204    let status = Command::new(binary).args(with_globals(&argv, 0)).status()?;
205    Ok(status.code().unwrap_or(-1))
206}