diff --git a/src/execution_context.rs b/src/execution_context.rs index 661e8bc8..757418c4 100644 --- a/src/execution_context.rs +++ b/src/execution_context.rs @@ -1,16 +1,17 @@ #![allow(dead_code)] use crate::executor::RunType; use crate::git::Git; +use crate::sudo::Sudo; use crate::utils::require_option; use crate::{config::Config, executor::Executor}; use color_eyre::eyre::Result; use directories::BaseDirs; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::Mutex; pub struct ExecutionContext<'a> { run_type: RunType, - sudo: &'a Option, + sudo: Option, git: &'a Git, config: &'a Config, base_dirs: &'a BaseDirs, @@ -23,7 +24,7 @@ pub struct ExecutionContext<'a> { impl<'a> ExecutionContext<'a> { pub fn new( run_type: RunType, - sudo: &'a Option, + sudo: Option, git: &'a Git, config: &'a Config, base_dirs: &'a BaseDirs, @@ -40,18 +41,7 @@ impl<'a> ExecutionContext<'a> { pub fn execute_elevated(&self, command: &Path, interactive: bool) -> Result { let sudo = require_option(self.sudo.clone(), "Sudo is required for this operation".into())?; - let mut cmd = self.run_type.execute(&sudo); - - if sudo.ends_with("sudo") { - cmd.arg("--preserve-env=DIFFPROG"); - } - - if interactive { - cmd.arg("-i"); - } - - cmd.arg(command); - Ok(cmd) + Ok(sudo.execute_elevated(self, command, interactive)) } pub fn run_type(&self) -> RunType { @@ -62,8 +52,8 @@ impl<'a> ExecutionContext<'a> { self.git } - pub fn sudo(&self) -> &Option { - self.sudo + pub fn sudo(&self) -> &Option { + &self.sudo } pub fn config(&self) -> &Config { diff --git a/src/main.rs b/src/main.rs index 9789b701..b77a1621 100644 --- a/src/main.rs +++ b/src/main.rs @@ -83,10 +83,10 @@ fn run() -> Result<()> { let git = git::Git::new(); let mut git_repos = git::Repositories::new(&git); - let sudo = sudo::path(); + let sudo = sudo::Sudo::detect(); let run_type = executor::RunType::new(config.dry_run()); - let ctx = execution_context::ExecutionContext::new(run_type, &sudo, &git, &config, &base_dirs); + let ctx = execution_context::ExecutionContext::new(run_type, sudo, &git, &config, &base_dirs); let mut runner = runner::Runner::new(&ctx); @@ -121,7 +121,9 @@ fn run() -> Result<()> { } if config.pre_sudo() { - sudo::elevate(&ctx, sudo.as_ref())?; + if let Some(sudo) = ctx.sudo() { + sudo.elevate(&ctx)?; + } } let powershell = powershell::Powershell::new(); @@ -202,17 +204,17 @@ fn run() -> Result<()> { #[cfg(target_os = "dragonfly")] runner.execute(Step::Pkg, "DragonFly BSD Packages", || { - dragonfly::upgrade_packages(sudo.as_ref(), run_type) + dragonfly::upgrade_packages(ctx.sudo().as_ref(), run_type) })?; #[cfg(target_os = "freebsd")] runner.execute(Step::Pkg, "FreeBSD Packages", || { - freebsd::upgrade_packages(&ctx, sudo.as_ref(), run_type) + freebsd::upgrade_packages(&ctx, ctx.sudo().as_ref(), run_type) })?; #[cfg(target_os = "openbsd")] runner.execute(Step::Pkg, "OpenBSD Packages", || { - openbsd::upgrade_packages(sudo.as_ref(), run_type) + openbsd::upgrade_packages(ctx.sudo().as_ref(), run_type) })?; #[cfg(target_os = "android")] @@ -383,7 +385,7 @@ fn run() -> Result<()> { runner.execute(Step::DebGet, "deb-get", || linux::run_deb_get(&ctx))?; runner.execute(Step::Toolbx, "toolbx", || toolbx::run_toolbx(&ctx))?; runner.execute(Step::Flatpak, "Flatpak", || linux::flatpak_update(&ctx))?; - runner.execute(Step::Snap, "snap", || linux::run_snap(sudo.as_ref(), run_type))?; + runner.execute(Step::Snap, "snap", || linux::run_snap(ctx.sudo().as_ref(), run_type))?; runner.execute(Step::Pacstall, "pacstall", || linux::run_pacstall(&ctx))?; runner.execute(Step::Pacdef, "pacdef", || linux::run_pacdef(&ctx))?; runner.execute(Step::Protonup, "protonup", || linux::run_protonup_update(&ctx))?; @@ -403,11 +405,11 @@ fn run() -> Result<()> { #[cfg(target_os = "linux")] { runner.execute(Step::System, "pihole", || { - linux::run_pihole_update(sudo.as_ref(), run_type) + linux::run_pihole_update(ctx.sudo().as_ref(), run_type) })?; runner.execute(Step::Firmware, "Firmware upgrades", || linux::run_fwupdmgr(&ctx))?; runner.execute(Step::Restarts, "Restarts", || { - linux::run_needrestart(sudo.as_ref(), run_type) + linux::run_needrestart(ctx.sudo().as_ref(), run_type) })?; } @@ -420,12 +422,12 @@ fn run() -> Result<()> { #[cfg(target_os = "freebsd")] runner.execute(Step::System, "FreeBSD Upgrade", || { - freebsd::upgrade_freebsd(sudo.as_ref(), run_type) + freebsd::upgrade_freebsd(ctx.sudo().as_ref(), run_type) })?; #[cfg(target_os = "openbsd")] runner.execute(Step::System, "OpenBSD Upgrade", || { - openbsd::upgrade_openbsd(sudo.as_ref(), run_type) + openbsd::upgrade_openbsd(ctx.sudo().as_ref(), run_type) })?; #[cfg(windows)] @@ -457,10 +459,10 @@ fn run() -> Result<()> { } #[cfg(target_os = "freebsd")] - freebsd::audit_packages(&sudo).ok(); + freebsd::audit_packages(ctx.sudo().as_ref()).ok(); #[cfg(target_os = "dragonfly")] - dragonfly::audit_packages(&sudo).ok(); + dragonfly::audit_packages(ctx.sudo().as_ref()).ok(); } let mut post_command_failed = false; diff --git a/src/steps/generic.rs b/src/steps/generic.rs index b6da5937..952fb8a2 100644 --- a/src/steps/generic.rs +++ b/src/steps/generic.rs @@ -202,7 +202,6 @@ pub fn run_juliaup(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { } run_type.execute(&juliaup).arg("update").status_checked() - } pub fn run_choosenim(ctx: &ExecutionContext) -> Result<()> { diff --git a/src/steps/node.rs b/src/steps/node.rs index 5455b4c7..f1e2e89d 100644 --- a/src/steps/node.rs +++ b/src/steps/node.rs @@ -4,7 +4,6 @@ use std::os::unix::fs::MetadataExt; use std::path::PathBuf; use std::process::Command; -use crate::sudo; use crate::utils::require_option; use color_eyre::eyre::Result; #[cfg(target_os = "linux")] @@ -13,9 +12,7 @@ use semver::Version; use tracing::debug; use crate::command::CommandExt; -use crate::executor::RunType; use crate::terminal::print_separator; -use crate::utils::sudo; use crate::utils::{require, PathExt}; use crate::{error::SkipStep, execution_context::ExecutionContext}; @@ -91,14 +88,17 @@ impl NPM { Version::parse(&version_str?).map_err(|err| err.into()) } - fn upgrade(&self, run_type: RunType, use_sudo: bool) -> Result<()> { + fn upgrade(&self, ctx: &ExecutionContext, use_sudo: bool) -> Result<()> { let args = ["update", self.global_location_arg()]; if use_sudo { - let sudo_option = sudo::path(); - let sudo = require_option(sudo_option, String::from("sudo is not installed"))?; - run_type.execute(sudo).arg(&self.command).args(args).status_checked()?; + let sudo = require_option(ctx.sudo().clone(), String::from("sudo is not installed"))?; + ctx.run_type() + .execute(sudo) + .arg(&self.command) + .args(args) + .status_checked()?; } else { - run_type.execute(&self.command).args(args).status_checked()?; + ctx.run_type().execute(&self.command).args(args).status_checked()?; } Ok(()) @@ -151,17 +151,18 @@ impl Yarn { .map(|s| PathBuf::from(s.stdout.trim())) } - fn upgrade(&self, run_type: RunType, use_sudo: bool) -> Result<()> { + fn upgrade(&self, ctx: &ExecutionContext, use_sudo: bool) -> Result<()> { let args = ["global", "upgrade"]; if use_sudo { - run_type - .execute("sudo") + let sudo = require_option(ctx.sudo().clone(), String::from("sudo is not installed"))?; + ctx.run_type() + .execute(sudo) .arg(self.yarn.as_ref().unwrap_or(&self.command)) .args(args) .status_checked()?; } else { - run_type.execute(&self.command).args(args).status_checked()?; + ctx.run_type().execute(&self.command).args(args).status_checked()?; } Ok(()) @@ -216,12 +217,12 @@ pub fn run_npm_upgrade(ctx: &ExecutionContext) -> Result<()> { #[cfg(target_os = "linux")] { - npm.upgrade(ctx.run_type(), should_use_sudo(&npm, ctx)?) + npm.upgrade(ctx, should_use_sudo(&npm, ctx)?) } #[cfg(not(target_os = "linux"))] { - npm.upgrade(ctx.run_type(), false) + npm.upgrade(ctx, false) } } @@ -232,12 +233,12 @@ pub fn run_pnpm_upgrade(ctx: &ExecutionContext) -> Result<()> { #[cfg(target_os = "linux")] { - pnpm.upgrade(ctx.run_type(), should_use_sudo(&pnpm, ctx)?) + pnpm.upgrade(ctx, should_use_sudo(&pnpm, ctx)?) } #[cfg(not(target_os = "linux"))] { - pnpm.upgrade(ctx.run_type(), false) + pnpm.upgrade(ctx, false) } } @@ -253,12 +254,12 @@ pub fn run_yarn_upgrade(ctx: &ExecutionContext) -> Result<()> { #[cfg(target_os = "linux")] { - yarn.upgrade(ctx.run_type(), should_use_sudo_yarn(&yarn, ctx)?) + yarn.upgrade(ctx, should_use_sudo_yarn(&yarn, ctx)?) } #[cfg(not(target_os = "linux"))] { - yarn.upgrade(ctx.run_type(), false) + yarn.upgrade(ctx, false) } } diff --git a/src/steps/os/archlinux.rs b/src/steps/os/archlinux.rs index 7c4e938c..e75fb855 100644 --- a/src/steps/os/archlinux.rs +++ b/src/steps/os/archlinux.rs @@ -10,6 +10,7 @@ use walkdir::WalkDir; use crate::command::CommandExt; use crate::error::TopgradeError; use crate::execution_context::ExecutionContext; +use crate::sudo::Sudo; use crate::utils::which; use crate::{config, Step}; @@ -110,7 +111,7 @@ impl Trizen { } pub struct Pacman { - sudo: PathBuf, + sudo: Sudo, executable: PathBuf, } @@ -229,7 +230,7 @@ impl ArchPackageManager for Pamac { pub struct Aura { executable: PathBuf, - sudo: PathBuf, + sudo: Sudo, } impl Aura { diff --git a/src/steps/os/dragonfly.rs b/src/steps/os/dragonfly.rs index b314a47f..63452a82 100644 --- a/src/steps/os/dragonfly.rs +++ b/src/steps/os/dragonfly.rs @@ -1,12 +1,13 @@ use crate::command::CommandExt; use crate::executor::RunType; +use crate::sudo::Sudo; use crate::terminal::print_separator; use crate::utils::require_option; use color_eyre::eyre::Result; use std::path::PathBuf; use std::process::Command; -pub fn upgrade_packages(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> { +pub fn upgrade_packages(sudo: Option<&Sudo>, run_type: RunType) -> Result<()> { let sudo = require_option(sudo, String::from("No sudo detected"))?; print_separator("DragonFly BSD Packages"); run_type @@ -15,7 +16,7 @@ pub fn upgrade_packages(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> .status_checked() } -pub fn audit_packages(sudo: &Option) -> Result<()> { +pub fn audit_packages(sudo: Option<&Sudo>) -> Result<()> { if let Some(sudo) = sudo { println!(); Command::new(sudo) diff --git a/src/steps/os/freebsd.rs b/src/steps/os/freebsd.rs index 8109057a..3cb06848 100644 --- a/src/steps/os/freebsd.rs +++ b/src/steps/os/freebsd.rs @@ -1,14 +1,14 @@ use crate::command::CommandExt; use crate::execution_context::ExecutionContext; use crate::executor::RunType; +use crate::sudo::Sudo; use crate::terminal::print_separator; use crate::utils::require_option; use crate::Step; use color_eyre::eyre::Result; -use std::path::PathBuf; use std::process::Command; -pub fn upgrade_freebsd(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> { +pub fn upgrade_freebsd(sudo: Option<&Sudo>, run_type: RunType) -> Result<()> { let sudo = require_option(sudo, String::from("No sudo detected"))?; print_separator("FreeBSD Update"); run_type @@ -17,7 +17,7 @@ pub fn upgrade_freebsd(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> .status_checked() } -pub fn upgrade_packages(ctx: &ExecutionContext, sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> { +pub fn upgrade_packages(ctx: &ExecutionContext, sudo: Option<&Sudo>, run_type: RunType) -> Result<()> { let sudo = require_option(sudo, String::from("No sudo detected"))?; print_separator("FreeBSD Packages"); @@ -30,7 +30,7 @@ pub fn upgrade_packages(ctx: &ExecutionContext, sudo: Option<&PathBuf>, run_type command.status_checked() } -pub fn audit_packages(sudo: &Option) -> Result<()> { +pub fn audit_packages(sudo: Option<&Sudo>) -> Result<()> { if let Some(sudo) = sudo { println!(); Command::new(sudo) diff --git a/src/steps/os/linux.rs b/src/steps/os/linux.rs index 25a40c0a..20d3653f 100644 --- a/src/steps/os/linux.rs +++ b/src/steps/os/linux.rs @@ -10,6 +10,7 @@ use crate::error::{SkipStep, TopgradeError}; use crate::execution_context::ExecutionContext; use crate::executor::RunType; use crate::steps::os::archlinux; +use crate::sudo::Sudo; use crate::terminal::{print_separator, print_warning}; use crate::utils::{require, require_option, which, PathExt}; use crate::Step; @@ -498,7 +499,7 @@ fn upgrade_neon(ctx: &ExecutionContext) -> Result<()> { Ok(()) } -pub fn run_needrestart(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> { +pub fn run_needrestart(sudo: Option<&Sudo>, run_type: RunType) -> Result<()> { let sudo = require_option(sudo, String::from("sudo is not installed"))?; let needrestart = require("needrestart")?; let distribution = Distribution::detect()?; @@ -603,7 +604,7 @@ pub fn flatpak_update(ctx: &ExecutionContext) -> Result<()> { Ok(()) } -pub fn run_snap(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> { +pub fn run_snap(sudo: Option<&Sudo>, run_type: RunType) -> Result<()> { let sudo = require_option(sudo, String::from("sudo is not installed"))?; let snap = require("snap")?; @@ -615,7 +616,7 @@ pub fn run_snap(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> { run_type.execute(sudo).arg(snap).arg("refresh").status_checked() } -pub fn run_pihole_update(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> { +pub fn run_pihole_update(sudo: Option<&Sudo>, run_type: RunType) -> Result<()> { let sudo = require_option(sudo, String::from("sudo is not installed"))?; let pihole = require("pihole")?; Path::new("/opt/pihole/update.sh").require()?; diff --git a/src/steps/os/windows.rs b/src/steps/os/windows.rs index 909847c9..987704aa 100644 --- a/src/steps/os/windows.rs +++ b/src/steps/os/windows.rs @@ -19,17 +19,16 @@ pub fn run_chocolatey(ctx: &ExecutionContext) -> Result<()> { print_separator("Chocolatey"); - let mut cmd = &choco; - let mut args = vec!["upgrade", "all"]; + let mut command = match ctx.sudo() { + Some(sudo) => { + let mut command = ctx.run_type().execute(sudo); + command.arg(choco); + command + } + None => ctx.run_type().execute(choco), + }; - if let Some(sudo) = ctx.sudo() { - cmd = sudo; - args.insert(0, "choco"); - } - - let mut command = ctx.run_type().execute(cmd); - - command.args(&args); + command.args(["upgrade", "all"]); if yes { command.arg("--yes"); diff --git a/src/sudo.rs b/src/sudo.rs index 0a66cf37..28c6fee9 100644 --- a/src/sudo.rs +++ b/src/sudo.rs @@ -1,3 +1,5 @@ +use std::ffi::OsStr; +use std::path::Path; use std::path::PathBuf; use color_eyre::eyre::Context; @@ -5,30 +7,102 @@ use color_eyre::eyre::Result; use crate::command::CommandExt; use crate::execution_context::ExecutionContext; +use crate::executor::Executor; use crate::terminal::print_separator; use crate::utils::which; -/// Get the path of the `sudo` utility. -/// -/// Detects `doas`, `sudo`, `gsudo`, or `pkexec`. -pub fn path() -> Option { - which("doas") - .or_else(|| which("sudo")) - .or_else(|| which("gsudo")) - .or_else(|| which("pkexec")) +#[derive(Clone, Debug)] +pub struct Sudo { + /// The path to the `sudo` binary. + path: PathBuf, + /// The type of program being used as `sudo`. + kind: SudoKind, } -/// Elevate permissions with `sudo`. -pub fn elevate(ctx: &ExecutionContext, sudo: Option<&PathBuf>) -> Result<()> { - if let Some(sudo) = sudo { - print_separator("Sudo"); - ctx.run_type() - .execute(sudo) - // TODO: Does this work with `doas`, `pkexec`, `gsudo`, GNU `sudo`...? - .arg("-v") - .status_checked() - .wrap_err("Failed to elevate permissions")?; +impl Sudo { + /// Get the `sudo` binary for this platform. + pub fn detect() -> Option { + which("doas") + .map(|p| (p, SudoKind::Doas)) + .or_else(|| which("sudo").map(|p| (p, SudoKind::Sudo))) + .or_else(|| which("gsudo").map(|p| (p, SudoKind::Gsudo))) + .or_else(|| which("pkexec").map(|p| (p, SudoKind::Pkexec))) + .map(|(path, kind)| Self { path, kind }) } - Ok(()) + /// Elevate permissions with `sudo`. + /// + /// This helps prevent blocking `sudo` prompts from stopping the run in the middle of a + /// step. + /// + /// See: https://github.com/topgrade-rs/topgrade/issues/205 + pub fn elevate(&self, ctx: &ExecutionContext) -> Result<()> { + print_separator("Sudo"); + let mut cmd = ctx.run_type().execute(self); + match self.kind { + SudoKind::Doas => { + // `doas` doesn't have anything like `sudo -v` to cache credentials, + // so we just execute a dummy `echo` command so we have something + // unobtrusive to run. + // See: https://man.openbsd.org/doas + cmd.arg("echo"); + } + SudoKind::Sudo => { + // From `man sudo` on macOS: + // -v, --validate + // Update the user's cached credentials, authenticating the user + // if necessary. For the sudoers plugin, this extends the sudo + // timeout for another 5 minutes by default, but does not run a + // command. Not all security policies support cached credentials. + cmd.arg("-v"); + } + SudoKind::Gsudo => { + // Shows current user, cache and console status. + // See: https://gerardog.github.io/gsudo/docs/usage + cmd.arg("status"); + } + SudoKind::Pkexec => { + // I don't think this does anything; `pkexec` usually asks for + // authentication every time, although it can be configured + // differently. + // + // See the note for `doas` above. + // + // See: https://linux.die.net/man/1/pkexec + cmd.arg("echo"); + } + } + cmd.status_checked().wrap_err("Failed to elevate permissions") + } + + /// Execute a command with `sudo`. + pub fn execute_elevated(&self, ctx: &ExecutionContext, command: &Path, interactive: bool) -> Executor { + let mut cmd = ctx.run_type().execute(self); + + if let SudoKind::Sudo = self.kind { + cmd.arg("--preserve-env=DIFFPROG"); + } + + if interactive { + cmd.arg("-i"); + } + + cmd.arg(command); + + cmd + } +} + +#[derive(Clone, Copy, Debug)] +enum SudoKind { + Doas, + Sudo, + Gsudo, + Pkexec, +} + +impl AsRef for Sudo { + fn as_ref(&self) -> &OsStr { + self.path.as_ref() + } }