use std::io::Write;
use std::process::{Command, Output, Stdio};
use eyre::bail;
use crate::cmd::CmdLineRunner;
use crate::config::Settings;
use crate::result::Result;
use crate::ui::multi_progress_report::{MultiProgressReport, ProgressPauseGuard};
pub(crate) fn is_root() -> bool {
#[cfg(unix)]
{
nix::unistd::geteuid().is_root()
}
#[cfg(not(unix))]
{
false
}
}
pub(crate) fn argv(program: &str, args: &[String]) -> Vec<String> {
argv_with_env(program, args, &[])
}
pub(crate) fn argv_with_env(
program: &str,
args: &[String],
envs: &[(String, String)],
) -> Vec<String> {
let mut argv = vec![];
if !is_root() && Settings::get().system_packages.sudo {
argv.push("sudo".to_string());
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(crate) fn subprocess_mode() -> &'static str {
if is_root() {
"interactive"
} else if !Settings::get().system_packages.sudo || crate::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(crate) fn run(program: &str, args: &[String], envs: &[(String, String)]) -> Result<()> {
let argv = argv_with_env(program, args, envs);
let mut manual = vec!["sudo".to_string()];
if !envs.is_empty() {
manual.push("env".to_string());
manual.extend(envs.iter().map(|(k, v)| format!("{k}={v}")));
}
manual.push(program.to_string());
manual.extend(args.iter().cloned());
let manual_cmd = manual.join(" ");
ensure_elevation_available(&manual_cmd)?;
let _progress_pause = pause_progress_for_child();
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(crate) 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 = std::iter::once("sudo".to_string())
.chain(std::iter::once(program.to_string()))
.chain(args.iter().cloned())
.collect::<Vec<_>>()
.join(" ");
ensure_elevation_available(&manual_cmd)?;
let _progress_pause = pause_progress_for_child();
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(crate) fn output(program: &str, args: &[String], envs: &[(String, String)]) -> Result<Output> {
let argv = argv_with_env(program, args, envs);
let manual_cmd = std::iter::once("sudo".to_string())
.chain((!envs.is_empty()).then_some("env".to_string()))
.chain(envs.iter().map(|(key, value)| format!("{key}={value}")))
.chain(std::iter::once(program.to_string()))
.chain(args.iter().cloned())
.collect::<Vec<_>>()
.join(" ");
ensure_elevation_available(&manual_cmd)?;
if !is_root() && Settings::get().system_packages.sudo && console::user_attended_stderr() {
let _progress_pause = pause_progress_for_child();
CmdLineRunner::new("sudo").arg("-v").raw(true).execute()?;
}
info!("$ {}", argv.join(" "));
Ok(Command::new(&argv[0])
.args(&argv[1..])
.envs(envs.iter().map(|(key, value)| (key, value)))
.output()?)
}
pub(crate) fn run_with_input(program: &str, args: &[String], input: &[u8]) -> Result<()> {
let argv = argv(program, args);
let manual_cmd = std::iter::once("sudo".to_string())
.chain(std::iter::once(program.to_string()))
.chain(args.iter().cloned())
.collect::<Vec<_>>()
.join(" ");
ensure_elevation_available(&manual_cmd)?;
let _progress_pause = pause_progress_for_child();
info!("$ {}", argv.join(" "));
let mut child = Command::new(&argv[0])
.args(&argv[1..])
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()?;
child
.stdin
.take()
.expect("piped stdin is available")
.write_all(input)?;
let status = child.wait()?;
if !status.success() {
bail!("elevated bootstrap helper failed with {status}");
}
Ok(())
}
pub(crate) fn run_with_input_output(
program: &str,
args: &[String],
input: &[u8],
) -> Result<Vec<u8>> {
let argv = argv(program, args);
let manual_cmd = std::iter::once("sudo".to_string())
.chain(std::iter::once(program.to_string()))
.chain(args.iter().cloned())
.collect::<Vec<_>>()
.join(" ");
ensure_elevation_available(&manual_cmd)?;
let _progress_pause = pause_progress_for_child();
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 ensure_elevation_available(manual_cmd: &str) -> Result<()> {
if is_root() {
return Ok(());
}
if !Settings::get().system_packages.sudo {
bail!(
"not running as root and system_packages.sudo is disabled. Run manually:\n {manual_cmd}"
);
}
if crate::file::which("sudo").is_none() {
bail!(
"sudo not found. Run as root:\n {}",
manual_cmd.trim_start_matches("sudo ")
);
}
if !console::user_attended_stderr() {
let ok = Command::new("sudo")
.args(["-n", "true"])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|status| status.success())
.unwrap_or(false);
if !ok {
bail!(
"sudo requires a password but no TTY is available. Run manually:\n {manual_cmd}"
);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[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);
}
}