use std::io::Write;
use std::process::{Command, Output, Stdio};
use eyre::bail;
use crate::multi_progress_report::{MultiProgressReport, ProgressPauseGuard};
use eyre::Result;
use mise_settings::Settings;
use mise_util::cmd::CmdLineRunner;
pub fn is_root() -> bool {
#[cfg(unix)]
{
nix::unistd::geteuid().is_root()
}
#[cfg(not(unix))]
{
false
}
}
pub fn elevator() -> String {
if let Some(cmd) = Settings::get()
.system_packages
.elevate_with
.as_deref()
.map(str::trim)
.filter(|cmd| !cmd.is_empty())
{
return cmd.to_string();
}
["sudo", "run0", "doas"]
.into_iter()
.find(|cmd| mise_util::file::which(cmd).is_some())
.unwrap_or("sudo")
.to_string()
}
fn noninteractive_flag(elevator: &str) -> Option<&'static str> {
match std::path::Path::new(elevator).file_name()?.to_str()? {
"doas" => Some("-n"),
"run0" => Some("--no-ask-password"),
_ => None,
}
}
fn is_sudo(elevator: &str) -> bool {
std::path::Path::new(elevator)
.file_name()
.is_some_and(|name| name == "sudo")
}
pub fn argv(program: &str, args: &[String]) -> Vec<String> {
argv_with_env(program, args, &[])
}
pub fn argv_with_env(program: &str, args: &[String], envs: &[(String, String)]) -> Vec<String> {
if !is_root() && Settings::get().system_packages.sudo {
build_argv(program, args, envs, !console::user_attended_stderr())
} else {
std::iter::once(program.to_string())
.chain(args.iter().cloned())
.collect()
}
}
fn join_quoted<S: AsRef<str>>(words: &[S]) -> String {
words
.iter()
.map(|word| {
let word = word.as_ref();
match word.split_once('=') {
Some((name, value))
if !name.is_empty()
&& name
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-')) =>
{
format!("{name}={}", shell_words::quote(value))
}
_ => shell_words::quote(word).into_owned(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
fn manual_command(program: &str, args: &[String], envs: &[(String, String)]) -> String {
join_quoted(&build_argv(program, args, envs, false))
}
fn build_argv(
program: &str,
args: &[String],
envs: &[(String, String)],
noninteractive: bool,
) -> Vec<String> {
let elevator = elevator();
let flag = noninteractive_flag(&elevator);
let is_run0 = flag == Some("--no-ask-password");
let mut argv = vec![elevator];
argv.extend(flag.filter(|_| noninteractive).map(str::to_string));
if is_run0 {
argv.extend(envs.iter().map(|(k, v)| format!("--setenv={k}={v}")));
} else if !envs.is_empty() {
argv.push("env".to_string());
argv.extend(envs.iter().map(|(k, v)| format!("{k}={v}")));
}
argv.push(program.to_string());
argv.extend(args.iter().cloned());
argv
}
#[cfg(unix)]
pub fn subprocess_mode() -> &'static str {
if is_root() {
"interactive"
} else if !Settings::get().system_packages.sudo || mise_util::file::which("sudo").is_none() {
"deny"
} else if console::user_attended_stderr() {
"interactive"
} else {
"noninteractive"
}
}
fn pause_progress_for_child() -> Option<ProgressPauseGuard> {
MultiProgressReport::try_get().map(|report| report.pause_progress())
}
pub fn run(program: &str, args: &[String], envs: &[(String, String)]) -> Result<()> {
let argv = argv_with_env(program, args, envs);
let manual_cmd = manual_command(program, args, envs);
ensure_elevation_available(&manual_cmd)?;
attempt(&manual_cmd, || {
let _progress_pause = pause_progress_for_child();
log::info!("$ {}", argv.join(" "));
let mut cmd = CmdLineRunner::new(&argv[0]);
for arg in &argv[1..] {
cmd = cmd.arg(arg);
}
for (k, v) in envs {
cmd = cmd.env(k, v);
}
cmd.raw(true).execute()
})
}
#[cfg(unix)]
pub fn run_in_dir<Fd: std::os::fd::AsFd>(program: &str, args: &[String], dir: Fd) -> Result<()> {
use std::os::fd::AsRawFd;
use std::os::unix::process::CommandExt;
use eyre::WrapErr;
let argv = argv_with_env(program, args, &[]);
let manual_cmd = manual_command(program, args, &[]);
ensure_elevation_available(&manual_cmd)?;
attempt(&manual_cmd, || {
let _progress_pause = pause_progress_for_child();
log::info!("$ {}", argv.join(" "));
let raw = dir.as_fd().as_raw_fd();
let mut cmd = Command::new(&argv[0]);
cmd.args(&argv[1..]);
unsafe {
cmd.pre_exec(move || {
if nix::libc::fchdir(raw) == -1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
let status = cmd
.status()
.wrap_err_with(|| format!("failed to run {}", argv.join(" ")))?;
if !status.success() {
bail!("{} failed", argv.join(" "));
}
Ok(())
})
}
pub fn output(program: &str, args: &[String], envs: &[(String, String)]) -> Result<Output> {
let argv = argv_with_env(program, args, envs);
let manual_cmd = manual_command(program, args, envs);
ensure_elevation_available(&manual_cmd)?;
let interactive =
!is_root() && Settings::get().system_packages.sudo && console::user_attended_stderr();
if interactive && is_sudo(&argv[0]) {
let _progress_pause = pause_progress_for_child();
CmdLineRunner::new(&argv[0]).arg("-v").raw(true).execute()?;
}
let _progress_pause = (interactive && !is_sudo(&argv[0]))
.then(pause_progress_for_child)
.flatten();
log::info!("$ {}", argv.join(" "));
let mut cmd = Command::new(&argv[0]);
if interactive && !is_sudo(&argv[0]) {
cmd.stdin(Stdio::inherit());
}
let mut output = cmd
.args(&argv[1..])
.envs(envs.iter().map(|(key, value)| (key, value)))
.output()?;
if !output.status.success()
&& let Some(hint) = manual_hint(&manual_cmd)
{
output
.stderr
.extend_from_slice(format!("\n{hint}").as_bytes());
}
Ok(output)
}
pub fn run_with_input(program: &str, args: &[String], input: &[u8]) -> Result<()> {
run_with_reader(program, args, input)
}
pub fn run_with_reader(
program: &str,
args: &[String],
mut input: impl std::io::Read,
) -> Result<()> {
let argv = argv(program, args);
ensure_stdin_elevation_available()?;
let _progress_pause = pause_progress_for_child();
log::info!("$ {}", argv.join(" "));
let mut child = Command::new(&argv[0])
.args(&argv[1..])
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()?;
let mut stdin = child.stdin.take().expect("piped stdin is available");
let copied = std::io::copy(&mut input, &mut stdin);
drop(stdin);
let status = child.wait()?;
if !status.success() {
bail!(
"elevated helper `{program} {}` failed with {status}",
args.join(" ")
);
}
copied?;
Ok(())
}
pub fn run_with_input_output(program: &str, args: &[String], input: &[u8]) -> Result<Vec<u8>> {
let argv = argv(program, args);
ensure_stdin_elevation_available()?;
let _progress_pause = pause_progress_for_child();
log::info!("$ {}", argv.join(" "));
let mut child = Command::new(&argv[0])
.args(&argv[1..])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()?;
child
.stdin
.take()
.expect("piped stdin is available")
.write_all(input)?;
let output = child.wait_with_output()?;
if !output.status.success() {
bail!("elevated bootstrap helper failed with {}", output.status);
}
Ok(output.stdout)
}
fn attempt<T>(manual_cmd: &str, f: impl FnOnce() -> Result<T>) -> Result<T> {
f().map_err(|err| with_manual_hint(err, manual_cmd))
}
fn manual_hint(manual_cmd: &str) -> Option<String> {
let elevator = elevator();
if is_root() || console::user_attended_stderr() || noninteractive_flag(&elevator).is_none() {
return None;
}
Some(format!(
"if {elevator} needs a password, run manually:\n {manual_cmd}"
))
}
fn with_manual_hint(err: eyre::Report, manual_cmd: &str) -> eyre::Report {
match manual_hint(manual_cmd) {
Some(hint) => err.wrap_err(hint),
None => err,
}
}
fn root_fallback(manual_cmd: &str, elevator: &str) -> String {
let Ok(mut words) = shell_words::split(manual_cmd) else {
return manual_cmd.to_string();
};
if words.first().is_some_and(|word| word == elevator) {
words.remove(0);
}
let split = words
.iter()
.position(|w| !w.starts_with("--setenv="))
.unwrap_or(words.len());
let (envs, tail) = words.split_at(split);
let tail = join_quoted(tail);
if envs.is_empty() {
tail
} else {
let envs = envs.iter().map(|w| w.trim_start_matches("--setenv="));
format!("env {} {tail}", join_quoted(&envs.collect::<Vec<_>>()))
}
}
pub fn ensure_elevation_available(manual_cmd: &str) -> Result<()> {
let elevator = elevator();
check_elevation(
&elevator,
&format!("Run manually:\n {manual_cmd}"),
&format!("Run as root:\n {}", root_fallback(manual_cmd, &elevator)),
)
}
pub fn ensure_sudo_available(manual_cmd: &str) -> Result<()> {
check_elevation(
"sudo",
&format!("Run manually:\n {manual_cmd}"),
&format!("Run as root:\n {manual_cmd}"),
)
}
fn ensure_stdin_elevation_available() -> Result<()> {
const HINT: &str = "Re-run this mise command as root. The privileged helper \
receives its plan on stdin, so it cannot be run by itself.";
check_elevation(&elevator(), HINT, HINT)
}
fn check_elevation(elevator: &str, manual_hint: &str, root_hint: &str) -> Result<()> {
if is_root() {
return Ok(());
}
if !Settings::get().system_packages.sudo {
bail!("not running as root and system_packages.sudo is disabled. {manual_hint}");
}
if mise_util::file::which(elevator).is_none() {
bail!("{elevator} not found. {root_hint}");
}
if !console::user_attended_stderr() {
let ok = if is_sudo(elevator) {
Command::new(elevator)
.args(["-n", "true"])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|status| status.success())
.unwrap_or(false)
} else {
noninteractive_flag(elevator).is_some()
};
if !ok {
bail!("{elevator} requires a password but no TTY is available. {manual_hint}");
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn root_fallback_turns_run0_setenv_into_env() {
assert_eq!(
root_fallback(
"run0 --setenv=A=1 --setenv=B=2 apt-get -y install x",
"run0"
),
"env A=1 B=2 apt-get -y install x"
);
assert_eq!(
root_fallback("sudo env A=1 apt-get update", "sudo"),
"env A=1 apt-get update"
);
assert_eq!(
root_fallback("mise install --system", "sudo"),
"mise install --system"
);
}
#[test]
fn noninteractive_flag_knows_each_elevator() {
assert_eq!(noninteractive_flag("/usr/bin/doas"), Some("-n"));
assert_eq!(noninteractive_flag("run0"), Some("--no-ask-password"));
assert_eq!(noninteractive_flag("sudo"), None);
assert_eq!(noninteractive_flag("pkexec"), None);
assert!(is_sudo("/usr/bin/sudo"));
assert!(!is_sudo("run0"));
}
#[test]
fn pause_progress_for_child_suspends_until_every_guard_drops() {
let report = MultiProgressReport::get();
let baseline = report.progress_suspension_depth();
let outer = pause_progress_for_child();
assert!(outer.is_some(), "a live report must hand back a guard");
assert_eq!(report.progress_suspension_depth(), baseline + 1);
let inner = pause_progress_for_child();
assert_eq!(report.progress_suspension_depth(), baseline + 2);
drop(inner);
assert_eq!(
report.progress_suspension_depth(),
baseline + 1,
"the outer child still owns the terminal"
);
drop(outer);
assert_eq!(report.progress_suspension_depth(), baseline);
}
}