use beet_core::prelude::*;
use beet_flow::prelude::*;
use std::path::PathBuf;
#[derive(EntityTargetEvent)]
pub struct StdOutLine {
pub line: String,
pub is_err: bool,
}
#[derive(Component)]
#[component(storage = "SparseSet")]
pub struct ChildHandle(async_process::Child);
impl Drop for ChildHandle {
fn drop(&mut self) { self.0.kill().ok(); }
}
pub fn poll_child_handles(
mut commands: Commands,
mut query: Populated<(Entity, &mut ChildHandle), With<Running>>,
) -> Result {
for (action, mut child_handle) in query.iter_mut() {
if let Some(status) = child_handle.0.try_status()? {
let outcome = match status.success() {
true => Outcome::Pass,
false => Outcome::Fail,
};
commands
.entity(action)
.remove::<ChildHandle>()
.trigger_target(outcome);
}
}
Ok(())
}
pub fn interrupt_child_handles(
ev: On<Outcome>,
mut commands: Commands,
query: Query<(), With<ChildHandle>>,
children: Query<&Children>,
) {
for child in children.iter_descendants_inclusive(ev.target()) {
if query.contains(child) {
commands.entity(child).remove::<ChildHandle>();
}
}
}
#[derive(Debug, Clone)]
pub struct CommandConfig {
cmd: String,
args: Vec<String>,
current_dir: Option<PathBuf>,
envs: Vec<(String, String)>,
}
impl CommandConfig {
pub fn parse(full_cmd: impl Into<String>) -> Self {
let cmd = full_cmd.into();
let mut args = cmd.split_whitespace();
let cmd = args
.next()
.expect("RawCommand::new requires at least one argument")
.to_string();
Self {
cmd,
args: args.map(|s| s.to_string()).collect(),
..default()
}
}
pub fn parse_shell(full_cmd: impl Into<String>) -> Self {
Self::from_parts("sh", vec!["-c".into(), full_cmd.into()])
}
pub fn from_parts(
cmd: impl AsRef<str>,
args: impl IntoIterator<Item = impl AsRef<str>>,
) -> Self {
Self {
cmd: cmd.as_ref().to_string(),
args: args.into_iter().map(|s| s.as_ref().to_string()).collect(),
..default()
}
}
pub fn arg(mut self, arg: impl AsRef<str>) -> Self {
self.args.push(arg.as_ref().to_string());
self
}
pub fn args(
mut self,
args: impl IntoIterator<Item = impl AsRef<str>>,
) -> Self {
self.args
.extend(args.into_iter().map(|s| s.as_ref().to_string()));
self
}
pub fn new(cmd: impl Into<String>) -> Self {
Self {
cmd: cmd.into(),
..default()
}
}
pub fn command(mut self, cmd: impl Into<String>) -> Self {
self.cmd = cmd.into();
self
}
pub fn current_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.current_dir = Some(dir.into());
self
}
pub fn env(
mut self,
key: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.envs.push((key.into(), value.into()));
self
}
pub fn from_cargo(cargo: &CargoBuildCmd) -> Self {
Self {
cmd: "cargo".into(),
args: cargo.get_args().iter().map(|s| s.to_string()).collect(),
..default()
}
}
pub fn into_action(self) -> impl Bundle {
OnSpawn::observe(
move |ev: On<GetOutcome>, mut cmd_runner: CommandRunner| {
cmd_runner.run(ev, self.clone())
},
)
}
}
impl Default for CommandConfig {
fn default() -> Self {
Self {
cmd: "true".into(),
args: default(),
current_dir: None,
envs: default(),
}
}
}
impl From<CargoBuildCmd> for CommandConfig {
fn from(cargo: CargoBuildCmd) -> Self { Self::from_cargo(&cargo) }
}
#[derive(SystemParam)]
pub struct CommandRunner<'w, 's> {
bevy_commands: Commands<'w, 's>,
pkg_config: Res<'w, PackageConfig>,
interruptable: Query<'w, 's, &'static ContinueRun>,
async_commands: AsyncCommands<'w, 's>,
}
impl CommandRunner<'_, '_> {
pub fn run(
&mut self,
ev: On<GetOutcome>,
cmd_config: impl Into<CommandConfig>,
) -> Result {
let CommandConfig {
cmd,
args,
current_dir,
envs,
} = cmd_config.into();
let action = ev.target();
let interruptable = self.interruptable.contains(action);
let envs = envs.clone().xtend(self.pkg_config.envs());
self.bevy_commands.entity(action).remove::<ChildHandle>();
let envs_pretty = envs
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join(" ");
info!("{} {} {}", envs_pretty, cmd, args.join(" "));
let mut cmd = async_process::Command::new(&cmd);
cmd.args(&args).envs(envs).kill_on_drop(true);
if let Some(dir) = current_dir {
cmd.current_dir(dir);
}
let mut child = cmd.spawn()?;
if interruptable {
self.bevy_commands
.entity(action)
.insert(ChildHandle(child));
} else {
self.async_commands.run(async move |world: AsyncWorld| {
let outcome = match child.status().await {
Ok(status) if status.success() => Outcome::Pass,
_ => Outcome::Fail,
};
world
.with_then(move |world| {
world.entity_mut(action).trigger_target(outcome);
})
.await;
});
}
Ok(())
}
}
#[cfg(test)]
mod test {
use crate::prelude::*;
use beet_core::prelude::*;
use beet_flow::prelude::*;
#[beet_core::test]
async fn works() {
let mut app = App::new();
app.add_plugins(CliPlugin)
.world_mut()
.spawn((Sequence, ExitOnEnd, children![
CommandConfig::parse("true").into_action()
]))
.trigger_target(GetOutcome);
app.run_async().await.xpect_eq(AppExit::Success);
}
#[beet_core::test]
async fn continue_run_pass() {
let mut app = App::new();
app.add_plugins(CliPlugin)
.world_mut()
.spawn((Sequence, ExitOnEnd, children![(
ContinueRun,
CommandConfig::parse("true").into_action()
)]))
.trigger_target(GetOutcome);
app.run_async().await.xpect_eq(AppExit::Success);
}
#[beet_core::test]
async fn continue_run_fail() {
let mut app = App::new();
app.add_plugins(CliPlugin)
.world_mut()
.spawn((Sequence, ExitOnEnd, children![(
ContinueRun,
CommandConfig::parse("false").into_action()
)]))
.trigger_target(GetOutcome);
app.run_async().await.xpect_eq(AppExit::from_code(1));
}
#[test]
fn interrupt_static() {
let mut app = App::new();
let entity = app
.add_plugins(CliPlugin)
.world_mut()
.spawn((Sequence, ExitOnFail, children![(
ContinueRun,
CommandConfig::parse("false").into_action()
)]))
.trigger_target(GetOutcome)
.id();
app.world_mut().flush();
app.world_mut()
.query_once::<&ChildHandle>()
.len()
.xpect_eq(1);
app.world_mut()
.entity_mut(entity)
.trigger_target(Outcome::Pass);
app.world_mut().flush();
app.world_mut()
.query_once::<&ChildHandle>()
.len()
.xpect_eq(0);
}
#[beet_core::test]
async fn interrupt_timed() {
let mut app = App::new();
let entity = app
.add_plugins(CliPlugin)
.world_mut()
.spawn((Sequence, ExitOnFail, children![(
ContinueRun,
CommandConfig::parse_shell("sleep 0.01 && false").into_action()
)]))
.trigger_target(GetOutcome)
.id();
app.world_mut().run_async(async move |world| {
time_ext::sleep_millis(2).await;
world.entity(entity).trigger_target(Outcome::Pass).await;
time_ext::sleep_millis(20).await;
world.write_message(AppExit::Success);
});
app.run_async().await.xpect_eq(AppExit::Success);
}
}