2022-01-08 10:20:36 +01:00
|
|
|
use std::convert::TryFrom;
|
2023-08-09 23:24:49 +02:00
|
|
|
|
2021-01-24 23:08:48 +01:00
|
|
|
use std::process::Stdio;
|
2020-12-18 10:27:44 +01:00
|
|
|
|
2022-01-08 10:20:36 +01:00
|
|
|
use async_trait::async_trait;
|
2021-01-24 23:08:48 +01:00
|
|
|
use futures::future::join3;
|
2022-01-08 10:20:36 +01:00
|
|
|
use serde::de::DeserializeOwned;
|
2022-07-30 07:13:09 +02:00
|
|
|
use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
|
2021-01-24 23:08:48 +01:00
|
|
|
use tokio::process::Command;
|
2020-12-16 05:21:26 +01:00
|
|
|
|
2022-07-30 07:13:09 +02:00
|
|
|
use super::error::{ColmenaError, ColmenaResult};
|
2021-11-21 08:34:52 +01:00
|
|
|
use super::job::JobHandle;
|
2022-07-30 07:13:09 +02:00
|
|
|
use super::nix::deployment::TargetNodeMap;
|
2023-08-09 23:24:49 +02:00
|
|
|
use super::nix::StorePath;
|
2020-12-18 10:27:44 +01:00
|
|
|
|
2022-04-25 09:03:57 +02:00
|
|
|
const NEWLINE: u8 = 0xa;
|
|
|
|
|
2021-11-21 08:34:52 +01:00
|
|
|
/// Non-interactive execution of an arbitrary command.
|
2021-01-24 23:08:48 +01:00
|
|
|
pub struct CommandExecution {
|
|
|
|
command: Command,
|
2021-11-21 08:34:52 +01:00
|
|
|
job: Option<JobHandle>,
|
2021-12-08 08:13:31 +01:00
|
|
|
hide_stdout: bool,
|
2021-01-24 23:08:48 +01:00
|
|
|
stdout: Option<String>,
|
|
|
|
stderr: Option<String>,
|
|
|
|
}
|
|
|
|
|
2022-01-08 10:20:36 +01:00
|
|
|
/// Helper extensions for Commands.
|
|
|
|
#[async_trait]
|
|
|
|
pub trait CommandExt {
|
|
|
|
/// Runs the command with stdout and stderr passed through to the user.
|
2022-01-08 10:20:36 +01:00
|
|
|
async fn passthrough(&mut self) -> ColmenaResult<()>;
|
2022-01-08 10:20:36 +01:00
|
|
|
|
|
|
|
/// Runs the command, capturing the output as a String.
|
2022-01-08 10:20:36 +01:00
|
|
|
async fn capture_output(&mut self) -> ColmenaResult<String>;
|
2022-01-08 10:20:36 +01:00
|
|
|
|
|
|
|
/// Runs the command, capturing deserialized output from JSON.
|
2022-07-30 07:13:09 +02:00
|
|
|
async fn capture_json<T>(&mut self) -> ColmenaResult<T>
|
|
|
|
where
|
|
|
|
T: DeserializeOwned;
|
2022-01-08 10:20:36 +01:00
|
|
|
|
|
|
|
/// Runs the command, capturing a single store path.
|
2022-01-08 10:20:36 +01:00
|
|
|
async fn capture_store_path(&mut self) -> ColmenaResult<StorePath>;
|
2022-01-08 10:20:36 +01:00
|
|
|
}
|
|
|
|
|
2021-01-24 23:08:48 +01:00
|
|
|
impl CommandExecution {
|
2021-02-10 04:28:45 +01:00
|
|
|
pub fn new(command: Command) -> Self {
|
2021-01-24 23:08:48 +01:00
|
|
|
Self {
|
|
|
|
command,
|
2021-11-21 08:34:52 +01:00
|
|
|
job: None,
|
2021-12-08 08:13:31 +01:00
|
|
|
hide_stdout: false,
|
2021-01-24 23:08:48 +01:00
|
|
|
stdout: None,
|
|
|
|
stderr: None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-21 08:34:52 +01:00
|
|
|
/// Sets the job associated with this execution.
|
|
|
|
pub fn set_job(&mut self, job: Option<JobHandle>) {
|
|
|
|
self.job = job;
|
2021-01-24 23:08:48 +01:00
|
|
|
}
|
|
|
|
|
2021-12-08 08:13:31 +01:00
|
|
|
/// Sets whether to hide stdout.
|
|
|
|
pub fn set_hide_stdout(&mut self, hide_stdout: bool) {
|
|
|
|
self.hide_stdout = hide_stdout;
|
|
|
|
}
|
|
|
|
|
2021-11-21 08:34:52 +01:00
|
|
|
/// Returns logs from the last invocation.
|
2021-01-24 23:08:48 +01:00
|
|
|
pub fn get_logs(&self) -> (Option<&String>, Option<&String>) {
|
|
|
|
(self.stdout.as_ref(), self.stderr.as_ref())
|
|
|
|
}
|
|
|
|
|
2021-11-21 08:34:52 +01:00
|
|
|
/// Runs the command.
|
2022-01-08 10:20:36 +01:00
|
|
|
pub async fn run(&mut self) -> ColmenaResult<()> {
|
2021-01-24 23:08:48 +01:00
|
|
|
self.command.stdin(Stdio::null());
|
|
|
|
self.command.stdout(Stdio::piped());
|
|
|
|
self.command.stderr(Stdio::piped());
|
|
|
|
|
|
|
|
self.stdout = Some(String::new());
|
|
|
|
self.stderr = Some(String::new());
|
|
|
|
|
|
|
|
let mut child = self.command.spawn()?;
|
|
|
|
|
|
|
|
let stdout = BufReader::new(child.stdout.take().unwrap());
|
|
|
|
let stderr = BufReader::new(child.stderr.take().unwrap());
|
|
|
|
|
2022-07-30 07:13:09 +02:00
|
|
|
let stdout_job = if self.hide_stdout {
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
self.job.clone()
|
|
|
|
};
|
2021-12-08 08:13:31 +01:00
|
|
|
|
2021-01-24 23:08:48 +01:00
|
|
|
let futures = join3(
|
2021-12-08 08:13:31 +01:00
|
|
|
capture_stream(stdout, stdout_job, false),
|
2021-11-21 08:34:52 +01:00
|
|
|
capture_stream(stderr, self.job.clone(), true),
|
2021-01-24 23:08:48 +01:00
|
|
|
child.wait(),
|
|
|
|
);
|
|
|
|
|
2021-12-05 10:14:12 +01:00
|
|
|
let (stdout, stderr, wait) = futures.await;
|
|
|
|
self.stdout = Some(stdout?);
|
|
|
|
self.stderr = Some(stderr?);
|
2021-01-24 23:08:48 +01:00
|
|
|
|
|
|
|
let exit = wait?;
|
|
|
|
|
|
|
|
if exit.success() {
|
|
|
|
Ok(())
|
|
|
|
} else {
|
2021-04-29 00:09:40 +02:00
|
|
|
Err(exit.into())
|
2021-01-24 23:08:48 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-08 10:20:36 +01:00
|
|
|
#[async_trait]
|
|
|
|
impl CommandExt for Command {
|
|
|
|
/// Runs the command with stdout and stderr passed through to the user.
|
2022-01-08 10:20:36 +01:00
|
|
|
async fn passthrough(&mut self) -> ColmenaResult<()> {
|
2022-07-30 07:13:09 +02:00
|
|
|
let exit = self.spawn()?.wait().await?;
|
2022-01-08 10:20:36 +01:00
|
|
|
|
|
|
|
if exit.success() {
|
|
|
|
Ok(())
|
|
|
|
} else {
|
|
|
|
Err(exit.into())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Captures output as a String.
|
2022-01-08 10:20:36 +01:00
|
|
|
async fn capture_output(&mut self) -> ColmenaResult<String> {
|
2022-01-08 10:20:36 +01:00
|
|
|
// We want the user to see the raw errors
|
|
|
|
let output = self
|
|
|
|
.stdout(Stdio::piped())
|
|
|
|
.stderr(Stdio::inherit())
|
|
|
|
.spawn()?
|
|
|
|
.wait_with_output()
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
if output.status.success() {
|
|
|
|
// FIXME: unwrap
|
|
|
|
Ok(String::from_utf8(output.stdout).unwrap())
|
|
|
|
} else {
|
|
|
|
Err(output.status.into())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Captures deserialized output from JSON.
|
2022-07-30 07:13:09 +02:00
|
|
|
async fn capture_json<T>(&mut self) -> ColmenaResult<T>
|
|
|
|
where
|
|
|
|
T: DeserializeOwned,
|
|
|
|
{
|
2022-01-08 10:20:36 +01:00
|
|
|
let output = self.capture_output().await?;
|
2022-01-08 10:20:36 +01:00
|
|
|
serde_json::from_str(&output).map_err(|_| ColmenaError::BadOutput {
|
2022-07-30 07:13:09 +02:00
|
|
|
output: output.clone(),
|
2022-01-08 10:20:36 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Captures a single store path.
|
2022-01-08 10:20:36 +01:00
|
|
|
async fn capture_store_path(&mut self) -> ColmenaResult<StorePath> {
|
2022-01-08 10:20:36 +01:00
|
|
|
let output = self.capture_output().await?;
|
|
|
|
let path = output.trim_end().to_owned();
|
|
|
|
StorePath::try_from(path)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[async_trait]
|
|
|
|
impl CommandExt for CommandExecution {
|
2022-01-08 10:20:36 +01:00
|
|
|
async fn passthrough(&mut self) -> ColmenaResult<()> {
|
2022-01-08 10:20:36 +01:00
|
|
|
self.run().await
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Captures output as a String.
|
2022-01-08 10:20:36 +01:00
|
|
|
async fn capture_output(&mut self) -> ColmenaResult<String> {
|
2022-01-08 10:20:36 +01:00
|
|
|
self.run().await?;
|
|
|
|
let (stdout, _) = self.get_logs();
|
|
|
|
|
|
|
|
Ok(stdout.unwrap().to_owned())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Captures deserialized output from JSON.
|
2022-07-30 07:13:09 +02:00
|
|
|
async fn capture_json<T>(&mut self) -> ColmenaResult<T>
|
|
|
|
where
|
|
|
|
T: DeserializeOwned,
|
|
|
|
{
|
2022-01-08 10:20:36 +01:00
|
|
|
let output = self.capture_output().await?;
|
2022-01-08 10:20:36 +01:00
|
|
|
serde_json::from_str(&output).map_err(|_| ColmenaError::BadOutput {
|
2022-07-30 07:13:09 +02:00
|
|
|
output: output.clone(),
|
2022-01-08 10:20:36 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Captures a single store path.
|
2022-01-08 10:20:36 +01:00
|
|
|
async fn capture_store_path(&mut self) -> ColmenaResult<StorePath> {
|
2022-01-08 10:20:36 +01:00
|
|
|
let output = self.capture_output().await?;
|
|
|
|
let path = output.trim_end().to_owned();
|
|
|
|
StorePath::try_from(path)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-30 07:13:09 +02:00
|
|
|
pub async fn capture_stream<R>(
|
|
|
|
mut stream: BufReader<R>,
|
|
|
|
job: Option<JobHandle>,
|
|
|
|
stderr: bool,
|
|
|
|
) -> ColmenaResult<String>
|
|
|
|
where
|
|
|
|
R: AsyncRead + Unpin,
|
2021-12-05 10:14:12 +01:00
|
|
|
{
|
2021-02-09 23:57:11 +01:00
|
|
|
let mut log = String::new();
|
|
|
|
|
|
|
|
loop {
|
2022-04-25 09:03:57 +02:00
|
|
|
let mut line = Vec::new();
|
|
|
|
let len = stream.read_until(NEWLINE, &mut line).await?;
|
|
|
|
let line = String::from_utf8_lossy(&line);
|
2021-02-09 23:57:11 +01:00
|
|
|
|
|
|
|
if len == 0 {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
let trimmed = line.trim_end();
|
2021-11-21 08:34:52 +01:00
|
|
|
|
|
|
|
if let Some(job) = &job {
|
|
|
|
if stderr {
|
2021-12-05 10:14:12 +01:00
|
|
|
job.stderr(trimmed.to_string())?;
|
2021-11-21 08:34:52 +01:00
|
|
|
} else {
|
2021-12-05 10:14:12 +01:00
|
|
|
job.stdout(trimmed.to_string())?;
|
2021-11-21 08:34:52 +01:00
|
|
|
}
|
|
|
|
}
|
2021-02-09 23:57:11 +01:00
|
|
|
|
|
|
|
log += trimmed;
|
|
|
|
log += "\n";
|
|
|
|
}
|
|
|
|
|
2021-12-05 10:14:12 +01:00
|
|
|
Ok(log)
|
2021-02-09 23:57:11 +01:00
|
|
|
}
|
2021-11-21 08:34:52 +01:00
|
|
|
|
|
|
|
pub fn get_label_width(targets: &TargetNodeMap) -> Option<usize> {
|
|
|
|
targets.keys().map(|n| n.len()).max()
|
|
|
|
}
|
2022-04-25 09:03:57 +02:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
use tokio::io::BufReader;
|
|
|
|
use tokio_test::block_on;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_capture_stream() {
|
|
|
|
let expected = "Hello\nWorld\n";
|
|
|
|
|
|
|
|
let stream = BufReader::new(expected.as_bytes());
|
2022-07-30 07:13:09 +02:00
|
|
|
let captured = block_on(async { capture_stream(stream, None, false).await.unwrap() });
|
2022-04-25 09:03:57 +02:00
|
|
|
|
|
|
|
assert_eq!(expected, captured);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_capture_stream_with_invalid_utf8() {
|
|
|
|
let stream = BufReader::new([0x80, 0xa].as_slice());
|
2022-07-30 07:13:09 +02:00
|
|
|
let captured = block_on(async { capture_stream(stream, None, false).await.unwrap() });
|
2022-04-25 09:03:57 +02:00
|
|
|
|
|
|
|
assert_eq!("\u{fffd}\n", captured);
|
|
|
|
}
|
|
|
|
}
|