use assemble_core::exception::BuildException;
use assemble_core::logging::{Origin, LOGGING_CONTROL};
use assemble_core::prelude::{ProjectError, ProjectResult};
use assemble_core::project::VisitProject;
use assemble_core::{BuildResult, Project};
use log::Level;
use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::io::{BufWriter, ErrorKind, Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, ExitStatus, Stdio};
use std::str::Bytes;
use std::string::FromUtf8Error;
use std::sync::{Arc, RwLock};
use std::thread::JoinHandle;
use std::{io, thread};
use assemble_core::error::PayloadError;
#[derive(Debug, Default, Clone)]
pub enum Input {
#[default]
Null,
File(PathBuf),
Bytes(Vec<u8>),
}
impl From<&[u8]> for Input {
fn from(b: &[u8]) -> Self {
Self::Bytes(b.to_vec())
}
}
impl From<Vec<u8>> for Input {
fn from(c: Vec<u8>) -> Self {
Self::Bytes(c)
}
}
impl<'a> From<Bytes<'a>> for Input {
fn from(b: Bytes<'a>) -> Self {
Self::Bytes(b.collect())
}
}
impl From<String> for Input {
fn from(str: String) -> Self {
Self::from(str.bytes())
}
}
impl From<&str> for Input {
fn from(str: &str) -> Self {
Self::from(str.bytes())
}
}
impl From<&Path> for Input {
fn from(p: &Path) -> Self {
Self::File(p.to_path_buf())
}
}
impl From<PathBuf> for Input {
fn from(file: PathBuf) -> Self {
Self::File(file)
}
}
#[derive(Debug, Clone)]
pub enum Output {
Null,
File {
path: PathBuf,
append: bool,
},
Log(#[doc("The log level to emit output to")] Level),
Bytes,
}
impl Output {
pub fn new<P: AsRef<Path>>(path: P, append: bool) -> Self {
Self::File {
path: path.as_ref().to_path_buf(),
append,
}
}
}
impl From<Level> for Output {
fn from(lvl: Level) -> Self {
Output::Log(lvl)
}
}
impl From<&Path> for Output {
fn from(path: &Path) -> Self {
Self::File {
path: path.to_path_buf(),
append: false,
}
}
}
impl From<PathBuf> for Output {
fn from(path: PathBuf) -> Self {
Self::File {
path,
append: false,
}
}
}
impl Default for Output {
fn default() -> Self {
Self::Log(Level::Info)
}
}
#[derive(Debug, Default, Clone)]
pub struct ExecSpec {
pub working_dir: PathBuf,
pub executable: OsString,
pub clargs: Vec<OsString>,
pub env: HashMap<String, String>,
pub input: Input,
pub output: Output,
pub output_err: Output,
}
impl ExecSpec {
pub fn working_dir(&self) -> &Path {
&self.working_dir
}
pub fn executable(&self) -> &OsStr {
&self.executable
}
pub fn args(&self) -> &[OsString] {
&self.clargs[..]
}
pub fn env(&self) -> &HashMap<String, String> {
&self.env
}
pub fn execute_spec<P>(self, path: P) -> ProjectResult<ExecHandle>
where
P: AsRef<Path>,
{
let path = path.as_ref();
let working_dir = self.resolve_working_dir(path);
let origin = LOGGING_CONTROL.get_origin();
ExecHandle::create(self, &working_dir, origin)
}
fn resolve_working_dir(&self, path: &Path) -> PathBuf {
if self.working_dir().is_absolute() {
self.working_dir.to_path_buf()
} else {
path.join(&self.working_dir)
}
}
#[doc(hidden)]
#[deprecated]
pub(crate) fn execute(&mut self, _path: impl AsRef<Path>) -> io::Result<&Child> {
panic!("unimplemented")
}
#[deprecated]
pub fn finish(&mut self) -> io::Result<ExitStatus> {
panic!("unimplemented")
}
}
impl VisitProject<Result<(), io::Error>> for ExecSpec {
fn visit(&mut self, project: &Project) -> Result<(), io::Error> {
self.execute(project.project_dir()).map(|_| ())
}
}
pub struct ExecSpecBuilder {
pub working_dir: Option<PathBuf>,
pub executable: Option<OsString>,
pub clargs: Vec<OsString>,
pub env: HashMap<String, String>,
stdin: Input,
output: Output,
output_err: Output,
}
#[derive(Debug, thiserror::Error)]
#[error("{}", error)]
pub struct ExecSpecBuilderError {
error: String,
}
impl From<&str> for ExecSpecBuilderError {
fn from(s: &str) -> Self {
Self {
error: s.to_string(),
}
}
}
impl ExecSpecBuilder {
pub fn new() -> Self {
Self {
working_dir: Some(PathBuf::new()),
executable: None,
clargs: vec![],
env: Self::default_env(),
stdin: Input::default(),
output: Output::default(),
output_err: Output::Log(Level::Warn),
}
}
pub fn default_env() -> HashMap<String, String> {
std::env::vars().into_iter().collect()
}
pub fn with_env<I: IntoIterator<Item = (String, String)>>(&mut self, env: I) -> &mut Self {
self.env = env.into_iter().collect();
self
}
pub fn extend_env<I: IntoIterator<Item = (String, String)>>(&mut self, env: I) -> &mut Self {
self.env.extend(env);
self
}
pub fn add_env<'a>(&mut self, env: &str, value: impl Into<Option<&'a str>>) -> &mut Self {
self.env
.insert(env.to_string(), value.into().unwrap_or("").to_string());
self
}
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
self.clargs.push(arg.as_ref().to_os_string());
self
}
pub fn args<I, S: AsRef<OsStr>>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = S>,
{
self.clargs
.extend(args.into_iter().map(|s| s.as_ref().to_os_string()));
self
}
pub fn with_arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
self.arg(arg);
self
}
pub fn with_args<I, S: AsRef<OsStr>>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
{
self.args(args);
self
}
pub fn exec<E: AsRef<OsStr>>(&mut self, exec: E) -> &mut Self {
self.executable = Some(exec.as_ref().to_os_string());
self
}
pub fn with_exec<E: AsRef<OsStr>>(mut self, exec: E) -> Self {
self.exec(exec);
self
}
pub fn working_dir<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
self.working_dir = Some(path.as_ref().to_path_buf());
self
}
pub fn stdin<In>(&mut self, input: In) -> &mut Self
where
In: Into<Input>,
{
let input = input.into();
self.stdin = input;
self
}
pub fn with_stdin<In>(mut self, input: In) -> Self
where
In: Into<Input>,
{
self.stdin(input);
self
}
pub fn stdout<O>(&mut self, output: O) -> &mut Self
where
O: Into<Output>,
{
self.output = output.into();
self
}
pub fn with_stdout<O>(mut self, output: O) -> Self
where
O: Into<Output>,
{
self.stdout(output);
self
}
pub fn stderr<O>(&mut self, output: O) -> &mut Self
where
O: Into<Output>,
{
self.output_err = output.into();
self
}
pub fn with_stderr<O>(mut self, output: O) -> Self
where
O: Into<Output>,
{
self.stderr(output);
self
}
pub fn build(self) -> Result<ExecSpec, ExecSpecBuilderError> {
Ok(ExecSpec {
working_dir: self
.working_dir
.ok_or(ExecSpecBuilderError::from("Working directory not set"))?,
executable: self
.executable
.ok_or(ExecSpecBuilderError::from("Executable not set"))?,
clargs: self.clargs,
env: self.env,
input: self.stdin,
output: self.output,
output_err: self.output_err,
})
}
}
pub struct ExecHandle {
spec: ExecSpec,
output: Arc<RwLock<ExecSpecOutputHandle>>,
handle: JoinHandle<io::Result<ExitStatus>>,
}
impl ExecHandle {
fn create(spec: ExecSpec, working_dir: &Path, origin: Origin) -> ProjectResult<Self> {
let mut command = Command::new(&spec.executable);
command.current_dir(working_dir).env_clear().envs(&spec.env);
command.args(spec.args());
let input = match &spec.input {
Input::Null => Stdio::null(),
Input::File(file) => {
let file = File::open(file)?;
Stdio::from(file)
}
Input::Bytes(b) => {
let mut file = tempfile::tempfile()?;
file.write_all(&b[..])?;
Stdio::from(file)
}
};
command.stdin(input);
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
let realized_output = RealizedOutput::try_from(spec.output.clone())?;
let realized_output_err = RealizedOutput::try_from(spec.output.clone())?;
let output_handle = Arc::new(RwLock::new(ExecSpecOutputHandle {
origin,
realized_output: Arc::new(RwLock::new(BufWriter::new(realized_output))),
realized_output_err: Arc::new(RwLock::new(BufWriter::new(realized_output_err))),
}));
let join_handle = execute(command, &output_handle)?;
Ok(Self {
spec,
output: output_handle,
handle: join_handle,
})
}
pub fn wait(self) -> ProjectResult<ExecResult> {
let result = self
.handle
.join()
.map_err(|_| ProjectError::custom("Couldn't join thread"))??;
let output = self.output.read().map_err(PayloadError::new)?;
let bytes = output.bytes();
let bytes_err = output.bytes_err();
Ok(ExecResult {
code: result,
bytes,
bytes_err,
})
}
}
fn execute(
mut command: Command,
output: &Arc<RwLock<ExecSpecOutputHandle>>,
) -> ProjectResult<JoinHandle<io::Result<ExitStatus>>> {
trace!("attempting to execute command: {:?}", command);
trace!("working_dir: {:?}", command.get_current_dir());
trace!(
"env: {:#?}",
command
.get_envs()
.into_iter()
.map(|(key, val): (&OsStr, Option<&OsStr>)| ((
key.to_string_lossy().to_string(),
val.map(|v| v.to_string_lossy().to_string())
.unwrap_or_default()
)))
.collect::<HashMap<_, _>>()
);
let spawned = command.spawn()?;
let output = output.clone();
Ok(thread::spawn(move || {
let mut spawned = spawned;
let output = output;
let origin = output.read().unwrap().origin.clone();
let output_handle = output.write().expect("couldn't get output");
thread::scope(|scope| {
let mut stdout = spawned.stdout.take().unwrap();
let mut stderr = spawned.stderr.take().unwrap();
let output = output_handle.realized_output.clone();
let output_err = output_handle.realized_output_err.clone();
let origin1 = origin.clone();
let out_join = scope.spawn(move || -> io::Result<u64> {
LOGGING_CONTROL.with_origin(origin1, || {
let mut output = output.write().expect("couldnt get output");
io::copy(&mut stdout, &mut *output)
})
});
let err_join = scope.spawn(move || -> io::Result<u64> {
LOGGING_CONTROL.with_origin(origin, || {
let mut output = output_err.write().expect("couldnt get output");
io::copy(&mut stderr, &mut *output)
})
});
let out = spawned.wait()?;
out_join.join().map_err(|_| {
io::Error::new(ErrorKind::Interrupted, "emitting to output failed")
})??;
err_join.join().map_err(|_| {
io::Error::new(ErrorKind::Interrupted, "emitting to error failed")
})??;
Ok(out)
})
}))
}
struct ExecSpecOutputHandle {
origin: Origin,
realized_output: Arc<RwLock<BufWriter<RealizedOutput>>>,
realized_output_err: Arc<RwLock<BufWriter<RealizedOutput>>>,
}
impl ExecSpecOutputHandle {
pub fn bytes(&self) -> Option<Vec<u8>> {
if let RealizedOutput::Bytes(vec) = self.realized_output.read().unwrap().get_ref() {
Some(vec.clone())
} else {
None
}
}
pub fn bytes_err(&self) -> Option<Vec<u8>> {
if let RealizedOutput::Bytes(vec) = self.realized_output_err.read().unwrap().get_ref() {
Some(vec.clone())
} else {
None
}
}
}
impl Write for ExecSpecOutputHandle {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
LOGGING_CONTROL.with_origin(self.origin.clone(), || {
self.realized_output.write().unwrap().write(buf)
})
}
fn flush(&mut self) -> io::Result<()> {
LOGGING_CONTROL.with_origin(self.origin.clone(), || {
self.realized_output.write().unwrap().flush()
})
}
}
impl TryFrom<Output> for RealizedOutput {
type Error = io::Error;
fn try_from(value: Output) -> Result<Self, Self::Error> {
match value {
Output::Null => Ok(Self::Null),
Output::File { path, append } => {
let file = File::options()
.create(true)
.write(true)
.append(append)
.open(path)?;
Ok(Self::File(file))
}
Output::Log(log) => Ok(Self::Log {
lvl: log,
buffer: vec![],
}),
Output::Bytes => Ok(Self::Bytes(vec![])),
}
}
}
enum RealizedOutput {
Null,
File(File),
Log { lvl: Level, buffer: Vec<u8> },
Bytes(Vec<u8>),
}
impl Write for RealizedOutput {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
RealizedOutput::Null => Ok(buf.len()),
RealizedOutput::File(f) => f.write(buf),
RealizedOutput::Log { lvl: l, buffer } => {
buffer.extend(IntoIterator::into_iter(buf));
while let Some(pos) = buffer.iter().position(|&l| l == b'\n' || l == 0) {
let line = &buffer[..pos];
let string = String::from_utf8_lossy(line);
log!(*l, "{}", string);
buffer.drain(..=pos);
}
Ok(buf.len())
}
RealizedOutput::Bytes(b) => {
b.extend(buf);
Ok(buf.len())
}
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
RealizedOutput::File(file) => file.flush(),
RealizedOutput::Log { lvl, buffer } => {
while let Some(pos) = buffer.iter().position(|&l| l == b'\n' || l == 0) {
let line = &buffer[..pos];
let string = String::from_utf8_lossy(line);
log!(*lvl, "{}", string);
buffer.drain(..=pos);
}
Ok(())
}
_ => Ok(()),
}
}
}
pub struct ExecResult {
code: ExitStatus,
bytes: Option<Vec<u8>>,
bytes_err: Option<Vec<u8>>,
}
impl ExecResult {
pub fn code(&self) -> ExitStatus {
self.code
}
pub fn success(&self) -> bool {
self.code.success()
}
pub fn expect_success(self) -> BuildResult<Self> {
if !self.success() {
Err(BuildException::new("expected a successful return code").into())
} else {
Ok(self)
}
}
pub fn bytes(&self) -> Option<&[u8]> {
self.bytes.as_ref().map(|s| &s[..])
}
pub fn utf8_string(&self) -> Option<Result<String, FromUtf8Error>> {
self.bytes()
.map(|s| Vec::from_iter(s.iter().copied()))
.map(String::from_utf8)
}
pub fn bytes_err(&self) -> Option<&[u8]> {
self.bytes_err.as_ref().map(|s| &s[..])
}
pub fn utf8_string_err(&self) -> Option<Result<String, FromUtf8Error>> {
self.bytes_err()
.map(|s| Vec::from_iter(s.iter().copied()))
.map(String::from_utf8)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_exec_spec() {
let mut builder = ExecSpecBuilder::new();
builder.exec("echo").arg("hello, world");
let exec = builder.build().unwrap();
assert_eq!(exec.executable, "echo");
}
#[test]
fn can_execute_spec() {
let spec = ExecSpecBuilder::new()
.with_exec("echo")
.with_args(["hello", "world"])
.with_stdout(Output::Bytes)
.build()
.expect("Couldn't build exec spec");
let result = { spec }.execute_spec("/").expect("Couldn't create handle");
let wait = result.wait().expect("couldn't finish exec spec");
let bytes = String::from_utf8(wait.bytes.unwrap()).unwrap();
assert_eq!("hello world", bytes.trim());
}
#[test]
fn invalid_exec_can_be_detected() {
let spec = ExecSpecBuilder::new()
.with_exec("please-dont-exist")
.with_stdout(Output::Null)
.build()
.expect("couldn't build");
let spawn = spec.execute_spec("/");
assert!(matches!(spawn, Err(_)), "Should return an error");
}
#[test]
fn emit_to_log() {}
}