use std::ffi::OsString;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::mpsc::{self, Sender};
use std::thread;
const READ_BUF: usize = 4096;
mod stream;
pub use stream::{Stream, StreamPoll};
mod streamed;
pub use streamed::{Streamed, StreamedOutcome, StreamedPoll};
mod exec;
mod detach;
#[derive(Debug, thiserror::Error)]
pub enum CliError {
#[error("failed to spawn {binary}: {source}")]
Spawn {
binary: PathBuf,
source: std::io::Error,
},
#[error("child {0} stream was not captured")]
Stdio(&'static str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Binary {
Lernie,
Bl,
Bz,
}
impl Binary {
const fn env_and_default(self) -> (&'static str, &'static str) {
match self {
Binary::Lernie => ("LERNIE_BINARY", "lernie"),
Binary::Bl => ("BL_BINARY", "bl"),
Binary::Bz => ("BZ_BINARY", "bz"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Chunk {
Stdout(Vec<u8>),
Stderr(Vec<u8>),
Exited(ExitInfo),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExitInfo {
Code(i32),
Signal(i32),
Unknown,
}
impl ExitInfo {
pub fn shell_code(self) -> i32 {
match self {
ExitInfo::Code(c) => c,
ExitInfo::Signal(s) => 128 + s,
ExitInfo::Unknown => -1,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cli {
binary: PathBuf,
env: Vec<(String, String)>,
}
impl Cli {
pub fn new(binary: impl Into<PathBuf>) -> Self {
Self {
binary: binary.into(),
env: Vec::new(),
}
}
pub fn resolve(binary: Binary) -> Self {
Self::resolve_with(binary, |k| std::env::var_os(k))
}
pub fn resolve_in_world(binary: Binary, overrides: &[(String, String)]) -> Self {
Self::resolve(binary).with_env(overrides.to_vec())
}
pub(crate) fn with_env(mut self, env: Vec<(String, String)>) -> Self {
self.env = env;
self
}
pub(crate) fn and_env(&self, extra: Vec<(String, String)>) -> Self {
let mut cli = self.clone();
cli.env.extend(extra);
cli
}
fn resolve_with(binary: Binary, lookup: impl Fn(&str) -> Option<OsString>) -> Self {
let (env_var, default) = binary.env_and_default();
match lookup(env_var) {
Some(v) if !v.is_empty() => Self::new(PathBuf::from(v)),
_ => Self::new(default),
}
}
pub(crate) fn binary(&self) -> &Path {
&self.binary
}
pub fn run(&self, args: &[&str]) -> Result<Stream, CliError> {
self.run_streaming(None, &[], args)
}
pub fn run_in(&self, dir: &Path, args: &[&str]) -> Result<Stream, CliError> {
self.run_streaming(Some(dir), &[], args)
}
pub fn run_env(&self, env: &[(&str, &str)], args: &[&str]) -> Result<Stream, CliError> {
self.run_streaming(None, env, args)
}
fn run_streaming(
&self,
cwd: Option<&Path>,
env: &[(&str, &str)],
args: &[&str],
) -> Result<Stream, CliError> {
let mut cmd = Command::new(&self.binary);
cmd.args(args)
.envs(self.standing_env())
.envs(env.iter().copied())
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if let Some(dir) = cwd {
cmd.current_dir(dir);
}
let mut child = cmd.spawn().map_err(|e| CliError::Spawn {
binary: self.binary.clone(),
source: e,
})?;
let stdout = child.stdout.take().ok_or(CliError::Stdio("stdout"))?;
let stderr = child.stderr.take().ok_or(CliError::Stdio("stderr"))?;
let (tx, rx) = mpsc::channel();
let tx_err = tx.clone();
thread::spawn(move || pump(stdout, tx, Chunk::Stdout));
thread::spawn(move || pump(stderr, tx_err, Chunk::Stderr));
Ok(Stream::new(child, rx))
}
fn standing_env(&self) -> impl Iterator<Item = (&str, &str)> {
self.env.iter().map(|(k, v)| (k.as_str(), v.as_str()))
}
}
fn pump<R: Read>(mut reader: R, tx: Sender<Chunk>, wrap: fn(Vec<u8>) -> Chunk) {
let mut buf = [0u8; READ_BUF];
while pump_step(&mut reader, &tx, &mut buf, wrap) {}
}
fn pump_step<R: Read>(
reader: &mut R,
tx: &Sender<Chunk>,
buf: &mut [u8],
wrap: fn(Vec<u8>) -> Chunk,
) -> bool {
let Ok(n @ 1..) = reader.read(buf) else {
return false; };
let bytes = buf.get(..n).unwrap_or_default().to_vec(); tx.send(wrap(bytes)).is_ok()
}
mod sys;
#[cfg(test)]
mod tests;