blob: 4fc2a69ad2043b44ed69994250318d279066656e [file] [log] [blame] [edit]
// Copyright 2023 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::{bail, Context, Result};
use log::info;
use std::{
fs::File,
io::{BufRead, BufReader, Read},
os::unix::fs::chown,
path::{Path, PathBuf},
process::{Command, Stdio},
thread,
};
use tar::Archive;
use xz2::bufread::XzDecoder;
/// Run a command and log its output (both stdout and stderr) at the
/// info level. An error is returned if the process fails to launch,
/// or if it exits non-zero.
pub fn execute_command(command: Command) -> Result<()> {
info!("Executing command: {command:?}");
execute_command_impl(command, |msg| info!("{msg}"))
}
/// Implementation for `execute_command`.
///
/// When a log is produced, the message is passed to the `log` function.
/// This allows tests to check the logs produced by the command.
fn execute_command_impl<L>(mut command: Command, log: L) -> Result<()>
where
L: Fn(String) + Clone + Send + 'static,
{
// Spawn the child with its output piped so that it can be logged.
let mut child = command
// The `Command` API doesn't have a convenient way to create a
// shared pipe for stdout/stderr, so create two pipes.
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.context("Failed to spawn command")?;
// OK to unwrap: stderr and stdout are set to capture above.
let mut stderr = child.stderr.take().unwrap();
let mut stdout = child.stdout.take().unwrap();
// Spawn two background threads, one to log stdout and one to log
// stderr. The threads will terminate when the output pipe is
// broken, which happen until when the child exits.
let log_clone = log.clone();
let stderr_thread = thread::spawn(move || log_lines_from_reader(&mut stderr, log_clone));
let stdout_thread = thread::spawn(move || log_lines_from_reader(&mut stdout, log));
stderr_thread.join().unwrap();
stdout_thread.join().unwrap();
// Wait for the child process to exit completely.
let status = child.wait().context("Failed to wait on process")?;
// Check the status to return an error if needed.
if !status.success() {
bail!("Process exited non-zero: {status:?}");
}
Ok(())
}
/// Read all lines from `reader` and log them with a ">>> " prefix.
///
/// This is used for logging output from a child process.
fn log_lines_from_reader<L>(reader: &mut dyn Read, log: L)
where
L: Fn(String),
{
let reader = BufReader::new(reader);
reader
.lines()
.map_while(Result::ok)
.for_each(|line| log(format!(">>> {line}")));
}
/// Uncompresses a tar from `src` to `dst`. In this case `src` needs to point to
/// a tar archive and `dst` to a folder where the items are unpacked to. This
/// also returns an `Vec<PathBuf>` of the entries that have been successfully
/// unpacked to `dst`. Please note that these paths are relative to `dst“.
pub fn uncompress_tar_xz(src: &Path, dst: &Path) -> Result<Vec<PathBuf>> {
let file = File::open(src).context("Unable to open tar archive")?;
let xz_decoder = XzDecoder::new(BufReader::new(file));
let mut result: Vec<PathBuf> = vec![];
let mut archive = Archive::new(xz_decoder);
for entry in archive
.entries()
.context("Unable to access all contents of the tar")?
{
let mut entry = entry.context("Unable to read entry of the tar")?;
entry
.unpack_in(dst)
.context("Unable to unpack entry of the tar")?;
result.push(
entry
.path()
.context("Unable to get tar entries path")?
.to_path_buf(),
);
}
Ok(result)
}
// Set User and Group Ids on the given path.
pub fn set_owner(path: &Path, uid: u32, gid: u32) -> Result<()> {
chown(path, Some(uid), Some(gid)).context(format!(
"Unable to set correct owner for {}",
path.display()
))
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
const FILE_CONTENTS: &[u8] = b"Hello World!";
const FILE_NAME: &str = "foo.txt";
const TAR_NAME: &str = "foo.tar.xz";
fn setup_tar_xz() -> Result<tempfile::TempDir> {
// First setup a tempdir.
let tempdir = tempfile::tempdir()?;
// Next create a file in there.
let file_path = tempdir.path().join(FILE_NAME);
std::fs::write(file_path, FILE_CONTENTS)?;
// Now create a tar.xz of that file.
let mut tar_cmd = Command::new("tar");
// Tell tar to (c)ompress an xz (J) of a (f)ile.
tar_cmd.arg("-cJf").arg(tempdir.path().join(TAR_NAME));
// Change dir to the temp path so that that the file is added
// without a directory prefix.
tar_cmd.arg("-C");
tar_cmd.arg(tempdir.path());
// We want to compress the newly created file.
tar_cmd.arg(FILE_NAME);
execute_command(tar_cmd)?;
Ok(tempdir)
}
#[test]
fn test_uncompress_tar_xz() -> Result<()> {
let tempdir = setup_tar_xz()?;
// Create a new dir where we uncompress to.
let new_dir_path = tempdir.path().join("uncompressed");
std::fs::create_dir(&new_dir_path)?;
// Uncompress the file.
let file_path = tempdir.path().join(TAR_NAME);
let result = uncompress_tar_xz(&file_path, &new_dir_path)?;
// Compare for equality.
assert_eq!(result, vec![Path::new(FILE_NAME)]);
let buf = std::fs::read(new_dir_path.join(&result[0]))?;
assert_eq!(&buf, FILE_CONTENTS);
Ok(())
}
#[test]
fn test_execute_bad_commands() {
// This fails even before executing the command because it doesn't exist.
let result = execute_command(Command::new("/this/does/not/exist"));
assert!(result.is_err());
// This fails due to a bad status code of the command.
let result = execute_command(Command::new("false"));
assert!(result.is_err());
// This succeeds.
let result = execute_command(Command::new("ls"));
assert!(result.is_ok());
}
/// Test that `execute_command_impl` logs the command's stdout and
/// stderr.
#[test]
fn test_execute_log() {
let output = Arc::new(Mutex::new(Vec::new()));
let output_clone = output.clone();
// Create a command that writes to both stdout and stderr.
let mut cmd = Command::new("sh");
cmd.arg("-c");
cmd.arg("echo write-to-stdout && >&2 echo write-to-stderr");
execute_command_impl(cmd, move |msg| output_clone.lock().unwrap().push(msg)).unwrap();
// Sort the lines to avoid depending on a specific output order.
// The stdout/stderr streams are not synchronized together.
let mut output: Vec<String> = output.lock().unwrap().clone();
output.sort();
assert_eq!(output, [">>> write-to-stderr", ">>> write-to-stdout"])
}
}