use lazy_static::lazy_static;
use papaya::HashMap;
use smallvec::SmallVec;
use smol_str::SmolStr;
use std::{ffi::OsStr, path::Path, sync::Arc};
use tokio::process::Command;
use tokio::sync::OnceCell;
lazy_static! {
pub static ref CMD: CmdCache = CmdCache::new();
static ref PATH_CACHE: HashMap<&'static str, bool> = HashMap::new();
}
#[derive(Debug, Clone)]
struct CmdOutput(Arc<OnceCell<Option<SmolStr>>>);
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
struct CmdKey(&'static str, SmallVec<[SmolStr; 4]>);
pub struct CmdCache {
cache: HashMap<CmdKey, CmdOutput>,
}
fn cmd_in_path(cmd: &'static str) -> bool {
let pinned = PATH_CACHE.pin();
*pinned.get_or_insert_with(cmd, || {
if cmd.contains(std::path::MAIN_SEPARATOR) {
Path::new(cmd).is_file()
} else {
std::env::var_os("PATH")
.map(|path_var| std::env::split_paths(&path_var).any(|dir| dir.join(cmd).is_file()))
.unwrap_or(false)
}
})
}
impl CmdCache {
fn new() -> Self {
Self {
cache: HashMap::new(),
}
}
fn make_key<I, S>(cmd: &'static str, args: I) -> CmdKey
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
CmdKey(
cmd,
args.into_iter()
.filter_map(|s| s.as_ref().to_str().map(Into::into))
.collect::<SmallVec<[_; 4]>>(),
)
}
pub async fn exec<I, S>(&self, cmd: &'static str, args: I) -> Option<SmolStr>
where
I: IntoIterator<Item = S> + Clone,
S: AsRef<OsStr>,
{
let key = Self::make_key(cmd, args.clone());
let cell = {
let pinned = self.cache.pin();
pinned
.get_or_insert_with(key, || {
let cell = Arc::new(OnceCell::new());
if !cmd_in_path(cmd) {
cell.set(None).ok();
}
CmdOutput(cell)
})
.clone()
};
cell.0
.get_or_init(|| async move {
let output = Command::new(cmd).args(args).output().await.ok()?;
if output.status.success() {
unsafe {
Some(SmolStr::new(
std::str::from_utf8_unchecked(&output.stdout).trim_end(),
))
}
} else {
None
}
})
.await
.clone()
}
}