use std::{
collections::HashMap,
env::vars,
fmt::Display,
io::Write,
process::{Command, ExitStatus, Stdio},
str::from_utf8,
};
use log::debug;
use crate::{Error, get_command};
#[derive(Debug)]
pub struct CommandOutput {
pub command: String,
pub status: ExitStatus,
pub stdout: String,
pub stderr: String,
}
impl Display for CommandOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{}", self.command)?;
writeln!(
f,
"⤷ status: {}",
self.status
.code()
.map(|code| code.to_string())
.unwrap_or("n/a".to_string())
)?;
writeln!(f, "⤷ stdout:\n{}", self.stdout)?;
writeln!(f, "⤷ stderr:\n{}", self.stderr)?;
Ok(())
}
}
pub fn run_command_as_user(
cmd: &str,
cmd_args: &[&str],
cmd_input: Option<&[u8]>,
env_list: &[&str],
envs: Option<HashMap<String, String>>,
user: &str,
) -> Result<CommandOutput, Error> {
let runuser_command = get_command("runuser")?;
debug!("Checking availability of command {cmd}");
get_command(cmd)?;
let mut envs_to_pass: HashMap<String, String> = HashMap::new();
if !env_list.is_empty() {
for env_var in vars().filter(|(key, _)| env_list.contains(&key.as_str())) {
envs_to_pass.insert(env_var.0, env_var.1);
}
}
if let Some(envs) = envs {
envs_to_pass.extend(envs);
}
let mut command = Command::new(runuser_command);
command.arg("--user").arg(user);
if !envs_to_pass.is_empty() {
command
.arg(format!(
"--whitelist-environment={}",
envs_to_pass
.keys()
.map(|key| key.as_str())
.collect::<Vec<&str>>()
.join(",")
))
.envs(&envs_to_pass);
}
command.arg("--").arg(cmd);
for cmd_arg in cmd_args {
command.arg(cmd_arg);
}
command
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(if cmd_input.is_none() {
Stdio::null()
} else {
Stdio::piped()
});
debug!("Running command {command:?}");
let mut command_child = command.spawn().map_err(|source| Error::CommandBackground {
command: format!("{command:?}"),
source,
})?;
if let Some(input) = cmd_input {
command_child
.stdin
.take()
.ok_or(Error::CommandAttachToStdin {
command: format!("{command:?}"),
})?
.write_all(input)
.map_err(|source| Error::CommandWriteToStdin {
command: format!("{command:?}"),
source,
})?;
}
let command_output = command_child
.wait_with_output()
.map_err(|source| Error::CommandExec {
command: format!("{command:?}"),
source,
})?;
Ok(CommandOutput {
status: command_output.status,
stdout: from_utf8(&command_output.stdout)
.map_err(|source| Error::Utf8String {
context: format!("processing the stdout of {command:?}"),
source,
})?
.to_string(),
stderr: from_utf8(&command_output.stderr)
.map_err(|source| Error::Utf8String {
context: format!("processing the stderr of {command:?}"),
source,
})?
.to_string(),
command: format!("{command:?}"),
})
}