use std::io::{BufRead, IsTerminal, Write};
use std::process::Command;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Manager {
Apt,
Dnf,
Yum,
Pacman,
Zypper,
Apk,
}
const MANAGERS: &[(Manager, &str)] = &[
(Manager::Apt, "apt-get"),
(Manager::Dnf, "dnf"),
(Manager::Yum, "yum"),
(Manager::Pacman, "pacman"),
(Manager::Zypper, "zypper"),
(Manager::Apk, "apk"),
];
impl Manager {
pub fn detect(has: impl Fn(&str) -> bool) -> Option<Manager> {
MANAGERS
.iter()
.find(|(_, bin)| has(bin))
.map(|(manager, _)| *manager)
}
pub fn argv(self) -> Vec<String> {
let words: &[&str] = match self {
Manager::Apt => &["apt-get", "install", "-y", "sshfs"],
Manager::Dnf => &["dnf", "install", "-y", "fuse-sshfs"],
Manager::Yum => &["yum", "install", "-y", "fuse-sshfs"],
Manager::Pacman => &["pacman", "-S", "--noconfirm", "sshfs"],
Manager::Zypper => &["zypper", "--non-interactive", "install", "sshfs"],
Manager::Apk => &["apk", "add", "sshfs"],
};
words.iter().map(|w| w.to_string()).collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Plan {
Run(Vec<String>),
Tell(Vec<String>),
Unknown,
}
pub fn plan(manager: Option<Manager>, root: bool, sudo: bool) -> Plan {
let Some(manager) = manager else {
return Plan::Unknown;
};
let argv = manager.argv();
match (root, sudo) {
(true, _) => Plan::Run(argv),
(false, true) => Plan::Run(std::iter::once("sudo".to_string()).chain(argv).collect()),
(false, false) => Plan::Tell(std::iter::once("sudo".to_string()).chain(argv).collect()),
}
}
pub fn plan_here() -> Plan {
let root = unsafe { libc::geteuid() } == 0;
plan(
Manager::detect(crate::shim::is_command),
root,
crate::shim::is_command("sudo"),
)
}
pub fn installed() -> bool {
crate::shim::is_command("sshfs")
}
fn by_hand(plan: &Plan) -> String {
match plan {
Plan::Run(argv) | Plan::Tell(argv) => format!("Install it with:\n {}", argv.join(" ")),
Plan::Unknown => "Install it with your package manager: the package is `sshfs` \
(`fuse-sshfs` on Fedora and RHEL)."
.to_string(),
}
}
pub fn ensure() -> anyhow::Result<()> {
if installed() {
return Ok(());
}
let plan = plan_here();
let interactive = std::io::stdin().is_terminal() && std::io::stderr().is_terminal();
let argv = match &plan {
Plan::Run(argv) if interactive => argv.clone(),
_ => anyhow::bail!(
"cctop sandbox needs sshfs on this machine. {}",
by_hand(&plan)
),
};
let mut err = std::io::stderr();
let _ = write!(err, "sshfs is required. Install it now? [Y/n] ");
let _ = err.flush();
let mut answer = String::new();
std::io::stdin().lock().read_line(&mut answer)?;
if !says_yes(&answer) {
anyhow::bail!("sshfs was not installed. {}", by_hand(&plan));
}
eprintln!("cctop sandbox: running {}", argv.join(" "));
let status = Command::new(&argv[0]).args(&argv[1..]).status()?;
if !status.success() || !installed() {
anyhow::bail!(
"installing sshfs did not work ({status}). {}",
by_hand(&plan)
);
}
Ok(())
}
fn says_yes(answer: &str) -> bool {
matches!(
answer.trim().to_ascii_lowercase().as_str(),
"" | "y" | "yes"
)
}
#[cfg(test)]
mod tests {
use super::*;
fn argv(words: &[&str]) -> Vec<String> {
words.iter().map(|w| w.to_string()).collect()
}
#[test]
fn the_first_package_manager_on_the_path_is_the_one_used() {
assert_eq!(Manager::detect(|_| false), None);
assert_eq!(
Manager::detect(|b| b == "apt-get" || b == "dnf"),
Some(Manager::Apt)
);
assert_eq!(
Manager::detect(|b| b == "yum" || b == "dnf"),
Some(Manager::Dnf)
);
assert_eq!(Manager::detect(|b| b == "apk"), Some(Manager::Apk));
}
#[test]
fn each_distribution_gets_its_own_package_name_and_flags() {
let cases = [
(Manager::Apt, &["apt-get", "install", "-y", "sshfs"][..]),
(Manager::Dnf, &["dnf", "install", "-y", "fuse-sshfs"]),
(Manager::Yum, &["yum", "install", "-y", "fuse-sshfs"]),
(Manager::Pacman, &["pacman", "-S", "--noconfirm", "sshfs"]),
(
Manager::Zypper,
&["zypper", "--non-interactive", "install", "sshfs"],
),
(Manager::Apk, &["apk", "add", "sshfs"]),
];
for (manager, want) in cases {
assert_eq!(manager.argv(), argv(want), "{manager:?}");
}
}
#[test]
fn sudo_is_used_only_when_not_root_and_only_if_it_is_there() {
assert_eq!(
plan(Some(Manager::Apt), false, true),
Plan::Run(argv(&["sudo", "apt-get", "install", "-y", "sshfs"]))
);
assert_eq!(
plan(Some(Manager::Apk), true, false),
Plan::Run(argv(&["apk", "add", "sshfs"]))
);
assert_eq!(
plan(Some(Manager::Pacman), false, false),
Plan::Tell(argv(&["sudo", "pacman", "-S", "--noconfirm", "sshfs"]))
);
assert_eq!(plan(None, false, true), Plan::Unknown);
}
#[test]
fn an_empty_answer_is_yes_and_anything_else_but_yes_is_no() {
for yes in ["", "\n", "y\n", "Y", "yes", " YES \n"] {
assert!(says_yes(yes), "{yes:?}");
}
for no in ["n\n", "no", "nope", "q"] {
assert!(!says_yes(no), "{no:?}");
}
}
#[test]
fn the_hand_instruction_names_the_command_or_the_package() {
let told = by_hand(&plan(Some(Manager::Dnf), false, true));
assert!(told.contains("sudo dnf install -y fuse-sshfs"), "{told}");
assert!(by_hand(&Plan::Unknown).contains("fuse-sshfs"));
}
}