diff --git a/config.example.toml b/config.example.toml index 53267e21..00d4a205 100644 --- a/config.example.toml +++ b/config.example.toml @@ -13,6 +13,10 @@ # Do not ask to retry failed steps (default: false) #no_retry = true +# Run `sudo -v` to cache credentials at the start of the run; this avoids a +# blocking password prompt in the middle of a possibly-unattended run. +#pre_sudo = false + # Run inside tmux #run_in_tmux = true diff --git a/src/config.rs b/src/config.rs index b98910d9..e4a2dd63 100644 --- a/src/config.rs +++ b/src/config.rs @@ -269,6 +269,7 @@ pub struct Vim { #[serde(deny_unknown_fields)] /// Configuration file pub struct ConfigFile { + pre_sudo: Option, pre_commands: Option, post_commands: Option, commands: Option, @@ -947,6 +948,12 @@ impl Config { .unwrap_or(false) } + /// If `true`, `sudo` should be called after `pre_commands` in order to elevate at the + /// start of the session (and not in the middle). + pub fn pre_sudo(&self) -> bool { + self.config_file.pre_sudo.unwrap_or(false) + } + #[cfg(target_os = "linux")] pub fn npm_use_sudo(&self) -> bool { self.config_file diff --git a/src/main.rs b/src/main.rs index f4dbcecf..b62a50a9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,7 @@ mod self_renamer; #[cfg(feature = "self-update")] mod self_update; mod steps; +mod sudo; mod terminal; mod utils; @@ -82,7 +83,7 @@ fn run() -> Result<()> { let git = git::Git::new(); let mut git_repos = git::Repositories::new(&git); - let sudo = utils::sudo(); + let sudo = sudo::path(); let run_type = executor::RunType::new(config.dry_run()); let ctx = execution_context::ExecutionContext::new(run_type, &sudo, &git, &config, &base_dirs); @@ -119,6 +120,10 @@ fn run() -> Result<()> { } } + if config.pre_sudo() { + sudo::elevate(&ctx, sudo.as_ref())?; + } + let powershell = powershell::Powershell::new(); let should_run_powershell = powershell.profile().is_some() && config.should_run(Step::Powershell); diff --git a/src/steps/node.rs b/src/steps/node.rs index 4909867b..d9a71e58 100644 --- a/src/steps/node.rs +++ b/src/steps/node.rs @@ -4,6 +4,8 @@ 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")] @@ -93,7 +95,7 @@ impl NPM { fn upgrade(&self, run_type: RunType, use_sudo: bool) -> Result<()> { let args = ["update", self.global_location_arg()]; if use_sudo { - let sudo_option = 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()?; } else { diff --git a/src/sudo.rs b/src/sudo.rs new file mode 100644 index 00000000..0a66cf37 --- /dev/null +++ b/src/sudo.rs @@ -0,0 +1,34 @@ +use std::path::PathBuf; + +use color_eyre::eyre::Context; +use color_eyre::eyre::Result; + +use crate::command::CommandExt; +use crate::execution_context::ExecutionContext; +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")) +} + +/// 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")?; + } + + Ok(()) +} diff --git a/src/utils.rs b/src/utils.rs index 326e4ad6..486bab1f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -67,13 +67,6 @@ pub fn which + Debug>(binary_name: T) -> Option { } } -pub fn sudo() -> Option { - which("doas") - .or_else(|| which("sudo")) - .or_else(|| which("gsudo")) - .or_else(|| which("pkexec")) -} - pub fn editor() -> Vec { env::var("EDITOR") .unwrap_or_else(|_| String::from(if cfg!(windows) { "notepad" } else { "vi" }))