use crate::types::{self, BoxedError};
use crate::{
operators::{self, Operator},
types::JoinHandle,
ProjectDefinition, SenderExt,
};
use derive_more::From;
use std::{
process::{self, Stdio},
sync,
};
use tokio::sync::broadcast;
#[derive(Debug, From)]
pub enum CargoShellError {
CargoCheckFailed,
CargoBinaryRunFailed,
ShellError(BoxedError),
}
impl std::error::Error for CargoShellError {}
impl core::fmt::Display for CargoShellError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
type CargoShellResult<T> = types::Result<T>;
pub struct CargoShellBuilder {
pub project: ProjectDefinition,
pub build_notifier: broadcast::Sender<()>,
pub file_notifications: broadcast::Sender<()>,
}
impl CargoShellBuilder {
pub fn shared(
project: ProjectDefinition,
build_notifier: broadcast::Sender<()>,
file_notifications: broadcast::Sender<()>,
) -> sync::Arc<Self> {
sync::Arc::new(Self {
project,
file_notifications,
build_notifier,
})
}
}
impl Clone for CargoShellBuilder {
fn clone(&self) -> Self {
Self {
project: self.project.clone(),
build_notifier: self.build_notifier.clone(),
file_notifications: self.file_notifications.clone(),
}
}
}
impl operators::Operator for sync::Arc<CargoShellBuilder> {
fn run(&self, mut signal: broadcast::Receiver<()>) -> JoinHandle<()> {
let handle = self.clone();
let mut recver = self.file_notifications.subscribe();
tokio::spawn(async move {
loop {
tokio::select! {
_ = recver.recv() => {
ewe_trace::info!("Received rebuilding signal for binary!");
match handle.build().await {
Ok(_) => {
ewe_trace::info!("Finished rebuilding binary!");
continue;
},
Err(err) => {
return Err(err);
}
}
},
_ = signal.recv() => {
ewe_trace::info!("Cancel signal received, shutting down!");
break;
}
}
}
Ok(())
})
}
}
impl CargoShellBuilder {
pub async fn build(&self) -> CargoShellResult<()> {
self.run_checks().await?;
self.run_build().await?;
self.build_notifier.send(())?;
Ok(())
}
async fn run_build(&self) -> CargoShellResult<()> {
ewe_trace::info!(
"Building project binary with cargo (project={}, binary={:?})",
self.project.crate_name,
self.project.run_arguments,
);
let mut binary_and_arguments = self.project.build_arguments.clone();
let binary_arguments = binary_and_arguments.split_off(1);
let mut command = tokio::process::Command::new(binary_and_arguments.pop().unwrap());
match command
.current_dir(self.project.workspace_root.clone())
.args(binary_arguments)
.output()
.await
{
Ok(result) => {
ewe_trace::info!(
"Running command `cargo build` (project={}, binary={:?})",
self.project.crate_name,
self.project.run_arguments,
);
if !result.status.success() {
ewe_trace::error!(
"Running command `cargo build` returned error (project={}, binary={:?})\n\t{:?}",
self.project.crate_name,
self.project.run_arguments,
String::from_utf8(result.stderr).expect("should correct decode error"),
);
return Err(Box::new(CargoShellError::CargoCheckFailed));
}
Ok(())
}
Err(err) => {
ewe_trace::error!(
"Failed command execution: `cargo build` (project={}, binary={:?}): {:?}",
self.project.crate_name,
self.project.run_arguments,
err,
);
Err(Box::new(CargoShellError::ShellError(Box::new(err))))
}
}
}
async fn run_checks(&self) -> CargoShellResult<()> {
let mut command = tokio::process::Command::new("cargo");
match command
.current_dir(self.project.workspace_root.clone())
.args(["check"])
.output()
.await
{
Ok(result) => {
ewe_trace::info!(
"Running command `cargo check` (project={}, binary={:?})",
self.project.crate_name,
self.project.run_arguments,
);
if !result.status.success() {
ewe_trace::error!(
"Running command `cargo check` returned error (project={}, binary={:?})\n\t{:?}",
self.project.crate_name,
self.project.run_arguments,
String::from_utf8(result.stderr).expect("should correct decode error"),
);
return Err(Box::new(CargoShellError::CargoCheckFailed));
}
Ok(())
}
Err(err) => {
ewe_trace::error!(
"Failed command execution: `cargo check` (project={}, binary={:?}): {:?}",
self.project.crate_name,
self.project.run_arguments,
err,
);
Err(Box::new(CargoShellError::ShellError(Box::new(err))))
}
}
}
}
pub struct BinaryApp {
project: ProjectDefinition,
running_notifications: broadcast::Sender<()>,
build_notifications: broadcast::Sender<()>,
}
impl Clone for BinaryApp {
fn clone(&self) -> Self {
Self {
project: self.project.clone(),
build_notifications: self.build_notifications.clone(),
running_notifications: self.build_notifications.clone(),
}
}
}
impl BinaryApp {
pub fn shared(
project: ProjectDefinition,
build_notifications: broadcast::Sender<()>,
running_notifications: broadcast::Sender<()>,
) -> sync::Arc<Self> {
sync::Arc::new(Self {
project,
build_notifications,
running_notifications,
})
}
}
impl Operator for sync::Arc<BinaryApp> {
fn run(&self, mut signal: broadcast::Receiver<()>) -> JoinHandle<()> {
let handle = self.clone();
let run_sender = self.running_notifications.clone();
let mut build_notifier = self.build_notifications.subscribe();
let wait_before_reload = self.project.wait_before_reload.clone();
tokio::spawn(async move {
let mut binary_handle: Option<process::Child> = None;
loop {
tokio::select! {
_ = build_notifier.recv() => {
if let Some(mut binary) = binary_handle {
ewe_trace::info!("Killing current version of binary");
binary.kill().expect("kill binary and re-starts");
}
ewe_trace::info!("Restarting latest version of binary");
binary_handle = Some(handle.run_binary().expect("re-run binary"));
ewe_trace::info!("Restart done!");
if let Err(_) = run_sender.send_in((), wait_before_reload.clone()).await {
ewe_trace::warn!("No one is listening for re-running messages");
}
continue;
},
_ = signal.recv() => {
ewe_trace::info!("Cancel signal received, shutting down!");
if let Some(mut binary) = binary_handle {
match binary.kill() {
Ok(_) => break,
Err(err) => return Err(Box::new(err).into()),
}
}
break;
}
}
}
Ok(())
})
}
}
impl BinaryApp {
fn run_binary(&self) -> types::Result<process::Child> {
ewe_trace::info!("Running binary from project={}", self.project);
let mut binary_and_arguments = self.project.run_arguments.clone();
let run_arguments = binary_and_arguments.split_off(1);
let mut command = process::Command::new(binary_and_arguments.pop().unwrap());
match command
.current_dir(self.project.workspace_root.clone())
.args(run_arguments)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
{
Ok(child) => {
ewe_trace::info!(
"Running command `cargo run` (binary={:?}, args={:?})",
self.project.crate_name,
self.project.run_arguments,
);
Ok(child)
}
Err(err) => {
ewe_trace::error!(
"Running command `cargo check` returned error (binary={:?}, args={:?})\n\t{:?}",
self.project.crate_name,
self.project.run_arguments,
err,
);
Err(Box::new(CargoShellError::ShellError(Box::new(err))))
}
}
}
}