use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Command;
use foundation_core::valtron::{Ctx, execute, Stream, TaskIterator, TaskStatus};
pub use foundation_core::valtron::collect_one;
use crate::error::DeploymentError;
#[derive(Debug, Clone)]
pub enum ShellPending {
Spawning,
Running {
pid: u32,
},
}
#[derive(Debug, Clone)]
pub enum ShellDone {
Success {
exit_code: i32,
stdout: String,
stderr: String,
},
Failed {
exit_code: Option<i32>,
stdout: String,
stderr: String,
},
}
pub struct ShellExecutor {
command: String,
args: Vec<String>,
envs: Vec<(String, String)>,
working_dir: Option<PathBuf>,
}
impl ShellExecutor {
#[must_use]
pub fn new(command: &str) -> Self {
Self {
command: command.to_string(),
args: Vec::new(),
envs: Vec::new(),
working_dir: None,
}
}
#[must_use]
pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
self.args.push(arg.as_ref().to_string_lossy().into_owned());
self
}
#[must_use]
pub fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
for a in args {
self.args.push(a.as_ref().to_string_lossy().into_owned());
}
self
}
#[must_use]
pub fn env<K: AsRef<OsStr>, V: AsRef<OsStr>>(mut self, key: K, val: V) -> Self {
self.envs.push((
key.as_ref().to_string_lossy().into_owned(),
val.as_ref().to_string_lossy().into_owned(),
));
self
}
#[must_use]
pub fn current_dir<P: AsRef<Path>>(mut self, dir: P) -> Self {
self.working_dir = Some(dir.as_ref().to_path_buf());
self
}
pub fn execute(
self,
) -> Result<impl Iterator<Item = Stream<ShellDone, ShellPending>>, DeploymentError> {
let task = ShellTask {
command: self.command,
args: self.args,
envs: self.envs,
working_dir: self.working_dir,
phase: ShellPhase::NotStarted,
};
execute(task, None).map_err(|e| DeploymentError::ExecutorError {
reason: e.to_string(),
})
}
}
enum ShellPhase {
NotStarted,
Spawned { child: std::process::Child },
Done,
}
struct ShellTask {
command: String,
args: Vec<String>,
envs: Vec<(String, String)>,
working_dir: Option<PathBuf>,
phase: ShellPhase,
}
impl TaskIterator for ShellTask {
type Ready = ShellDone;
type Pending = ShellPending;
fn next_status(&mut self, _ctx: Ctx) -> Option<TaskStatus<ShellDone, ShellPending>> {
match std::mem::replace(&mut self.phase, ShellPhase::Done) {
ShellPhase::NotStarted => {
let mut cmd = Command::new(&self.command);
cmd.args(&self.args);
for (k, v) in &self.envs {
cmd.env(k, v);
}
if let Some(dir) = &self.working_dir {
cmd.current_dir(dir);
}
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
match cmd.spawn() {
Ok(child) => {
let pid = child.id();
self.phase = ShellPhase::Spawned { child };
Some(TaskStatus::Pending(ShellPending::Running { pid }))
}
Err(e) => {
self.phase = ShellPhase::Done;
Some(TaskStatus::Ready(ShellDone::Failed {
exit_code: None,
stdout: String::new(),
stderr: e.to_string(),
}))
}
}
}
ShellPhase::Spawned { child } => {
self.phase = ShellPhase::Done;
match child.wait_with_output() {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
let code = output.status.code();
if output.status.success() {
Some(TaskStatus::Ready(ShellDone::Success {
exit_code: code.unwrap_or(0),
stdout,
stderr,
}))
} else {
Some(TaskStatus::Ready(ShellDone::Failed {
exit_code: code,
stdout,
stderr,
}))
}
}
Err(e) => Some(TaskStatus::Ready(ShellDone::Failed {
exit_code: None,
stdout: String::new(),
stderr: e.to_string(),
})),
}
}
ShellPhase::Done => None,
}
}
}
#[derive(Debug, Clone)]
pub struct CollectedOutput {
pub exit_code: Option<i32>,
pub stdout: String,
pub stderr: String,
pub success: bool,
}
pub fn execute_and_collect(executor: ShellExecutor) -> Result<CollectedOutput, DeploymentError> {
let stream = executor.execute()?;
let result = collect_one(stream).ok_or_else(|| DeploymentError::ProcessFailed {
command: "shell".to_string(),
exit_code: None,
stdout: String::new(),
stderr: "no result from stream".to_string(),
})?;
match result {
ShellDone::Success {
exit_code,
stdout,
stderr,
} => Ok(CollectedOutput {
exit_code: Some(exit_code),
stdout,
stderr,
success: true,
}),
ShellDone::Failed {
exit_code,
stdout,
stderr,
} => Ok(CollectedOutput {
exit_code,
stdout,
stderr,
success: false,
}),
}
}