From ecc0d258c38ff7e223c035c90e832917dcf88b15 Mon Sep 17 00:00:00 2001 From: Edouard Poitras Date: Fri, 16 Aug 2024 18:15:37 -0400 Subject: [PATCH 1/4] Implemented new Chain component and ChainCompletedEvent --- examples/chain_failure.rs | 53 +++++++++++++++ examples/chain_failure_delay_retries.rs | 73 +++++++++++++++++++++ examples/chain_retries_delay_cleanup.rs | 58 +++++++++++++++++ examples/run_all_examples.sh | 6 +- examples/simple_chain.rs | 51 +++++++++++++++ src/addons/chain.rs | 85 ++++++++++++++++++++++++- src/addons/cleanup.rs | 11 +++- src/lib.rs | 4 +- 8 files changed, 334 insertions(+), 7 deletions(-) create mode 100644 examples/chain_failure.rs create mode 100644 examples/chain_failure_delay_retries.rs create mode 100644 examples/chain_retries_delay_cleanup.rs create mode 100644 examples/simple_chain.rs diff --git a/examples/chain_failure.rs b/examples/chain_failure.rs new file mode 100644 index 0000000..7cb2a2b --- /dev/null +++ b/examples/chain_failure.rs @@ -0,0 +1,53 @@ +use bevy::prelude::*; +use bevy_local_commands::{ + BevyLocalCommandsPlugin, Chain, LocalCommand, ProcessCompleted, ProcessOutput, +}; + +fn main() { + App::new() + .add_plugins((MinimalPlugins, BevyLocalCommandsPlugin)) + .add_systems(Startup, setup) + .add_systems(Update, update) + .run(); +} + +fn setup(mut commands: Commands) { + // Create a chain of commands + let chain = Chain::new(vec![ + LocalCommand::new("sh").args(["-c", "echo 'First command' && sleep 1"]), + LocalCommand::new("commanddoesnotexist").args(["this should fail"]), // Failure + LocalCommand::new("sh").args(["-c", "echo 'Third command' && sleep 1"]), + // Same result with a failed running command + //LocalCommand::new("sh").args(["-c", "exit 1"]), // Failure + ]); + + // Spawn an entity with the Chain component + let id = commands.spawn(chain).id(); + println!("Spawned the chain as entity {id:?}"); +} + +fn update( + mut process_output_event: EventReader, + mut process_completed_event: EventReader, + chain_query: Query<(), With>, +) { + for process_output in process_output_event.read() { + for line in process_output.lines() { + println!("Output Line ({:?}): {line}", process_output.entity); + } + } + + for process_completed in process_completed_event.read() { + println!( + "Command {:?} completed (Success - {})", + process_completed.entity, + process_completed.exit_status.success() + ); + } + + // Check if there are no more Chain components (all chains completed) + if chain_query.is_empty() { + println!("Chain commands done. Exiting the app."); + std::process::exit(0); + } +} diff --git a/examples/chain_failure_delay_retries.rs b/examples/chain_failure_delay_retries.rs new file mode 100644 index 0000000..47bc492 --- /dev/null +++ b/examples/chain_failure_delay_retries.rs @@ -0,0 +1,73 @@ +use std::time::Duration; + +use bevy::prelude::*; +use bevy_local_commands::{ + BevyLocalCommandsPlugin, Chain, ChainCompletedEvent, Delay, LocalCommand, ProcessCompleted, + ProcessOutput, Retry, RetryEvent, +}; + +fn main() { + App::new() + .add_plugins((MinimalPlugins, BevyLocalCommandsPlugin)) + .add_systems(Startup, setup) + .add_systems(Update, update) + .run(); +} + +fn setup(mut commands: Commands) { + // Spawn an entity with the relevant components + let id = commands + .spawn(( + Chain::new(vec![ + LocalCommand::new("sh").args(["-c", "echo 'First command'"]), + LocalCommand::new("sh").args(["-c", "exit 1"]), // Failure + LocalCommand::new("sh").args(["-c", "echo 'Third command'"]), + ]), + Retry::Attempts(2), + Delay::Fixed(Duration::from_secs(2)), + )) + .id(); + println!("Spawned the chain as entity {id:?}"); +} + +fn update( + mut process_output_event: EventReader, + mut process_completed_event: EventReader, + mut process_chain_completed_event: EventReader, + mut process_retry_event: EventReader, + chain_query: Query<(), With>, +) { + for process_output in process_output_event.read() { + for line in process_output.lines() { + println!("Output Line ({:?}): {line}", process_output.entity); + } + } + + for process_retry in process_retry_event.read() { + println!( + "Command for entity {:?} failed, retrying ({} left)", + process_retry.entity, process_retry.retries_left, + ); + } + + for process_completed in process_completed_event.read() { + println!( + "Command {:?} completed (Success - {})", + process_completed.entity, + process_completed.exit_status.success() + ); + } + + for process_chain_completed in process_chain_completed_event.read() { + println!( + "Chain of commands completed for entity {} (Success - {})", + process_chain_completed.entity, process_chain_completed.success, + ); + } + + // Check if there is no more Chain component + if chain_query.is_empty() { + println!("Chain commands done. Exiting the app."); + std::process::exit(0); + } +} diff --git a/examples/chain_retries_delay_cleanup.rs b/examples/chain_retries_delay_cleanup.rs new file mode 100644 index 0000000..c26a8dd --- /dev/null +++ b/examples/chain_retries_delay_cleanup.rs @@ -0,0 +1,58 @@ +use std::time::Duration; + +use bevy::prelude::*; +use bevy_local_commands::{ + BevyLocalCommandsPlugin, Chain, Cleanup, Delay, LocalCommand, ProcessCompleted, ProcessOutput, + Retry, +}; + +fn main() { + App::new() + .add_plugins((MinimalPlugins, BevyLocalCommandsPlugin)) + .add_systems(Startup, setup) + .add_systems(Update, update) + .run(); +} + +fn setup(mut commands: Commands) { + // Spawn a entity with all addons + let id = commands + .spawn(( + Chain::new(vec![ + LocalCommand::new("sh").args(["-c", "echo 'First command'"]), + LocalCommand::new("sh").args(["-c", "echo 'Second command'"]), + LocalCommand::new("sh").args(["-c", "echo 'Third command'"]), + ]), + Retry::Attempts(2), + Delay::Fixed(Duration::from_secs(3)), + Cleanup::RemoveComponents, + )) + .id(); + println!("Spawned as entity {id:?}"); +} + +fn update( + mut process_output_event: EventReader, + mut process_completed_event: EventReader, + command_query: Query<(), (With, With, With, With)>, +) { + for process_output in process_output_event.read() { + for line in process_output.lines() { + println!("Output Line ({:?}): {line}", process_output.entity); + } + } + + for process_completed in process_completed_event.read() { + println!( + "Command {:?} completed (Success - {})", + process_completed.entity, + process_completed.exit_status.success() + ); + } + + // Check if our entity is done running chain commands + if command_query.is_empty() { + println!("All commands completed - cleanup performed. Exiting the app."); + std::process::exit(0); + } +} diff --git a/examples/run_all_examples.sh b/examples/run_all_examples.sh index 6f8b24b..139abd9 100755 --- a/examples/run_all_examples.sh +++ b/examples/run_all_examples.sh @@ -1,3 +1,6 @@ +cargo run --example chain_failure +cargo run --example chain_failure_delay_retries +cargo run --example chain_retries_delay_cleanup cargo run --example despawn_on_completion cargo run --example error cargo run --example input @@ -5,4 +8,5 @@ cargo run --example kill cargo run --example retries_and_delay_and_cleanup cargo run --example retries_and_delay cargo run --example retries_and_remove -cargo run --example simple \ No newline at end of file +cargo run --example simple +cargo run --example simple_chain \ No newline at end of file diff --git a/examples/simple_chain.rs b/examples/simple_chain.rs new file mode 100644 index 0000000..9b3f75c --- /dev/null +++ b/examples/simple_chain.rs @@ -0,0 +1,51 @@ +use bevy::prelude::*; +use bevy_local_commands::{ + BevyLocalCommandsPlugin, Chain, LocalCommand, ProcessCompleted, ProcessOutput, +}; + +fn main() { + App::new() + .add_plugins((MinimalPlugins, BevyLocalCommandsPlugin)) + .add_systems(Startup, setup) + .add_systems(Update, update) + .run(); +} + +fn setup(mut commands: Commands) { + // Create a chain of commands + let chain = Chain::new(vec![ + LocalCommand::new("sh").args(["-c", "echo 'First command' && sleep 1"]), + LocalCommand::new("sh").args(["-c", "echo 'Second command' && sleep 1"]), + LocalCommand::new("sh").args(["-c", "echo 'Third command' && sleep 1"]), + ]); + + // Spawn an entity with the Chain component + let id = commands.spawn(chain).id(); + println!("Spawned the chain as entity {id:?}"); +} + +fn update( + mut process_output_event: EventReader, + mut process_completed_event: EventReader, + chain_query: Query<(), With>, +) { + for process_output in process_output_event.read() { + for line in process_output.lines() { + println!("Output Line ({:?}): {line}", process_output.entity); + } + } + + for process_completed in process_completed_event.read() { + println!( + "Command {:?} completed (Success - {})", + process_completed.entity, + process_completed.exit_status.success() + ); + } + + // Check if there are no more Chain components (all chains completed) + if chain_query.is_empty() { + println!("All chain commands completed. Exiting the app."); + std::process::exit(0); + } +} diff --git a/src/addons/chain.rs b/src/addons/chain.rs index 4e793f9..20a7a07 100644 --- a/src/addons/chain.rs +++ b/src/addons/chain.rs @@ -1,5 +1,84 @@ +use crate::local_command::LocalCommand; +use crate::{Process, ProcessCompleted, ProcessError}; use bevy::prelude::*; -// Coming soon. -#[derive(Debug, Component)] -pub enum Chain {} +#[derive(Component)] +pub struct Chain { + pub(crate) commands: Vec, +} + +impl Chain { + pub fn new(commands: Vec) -> Self { + Self { commands } + } +} + +#[derive(Debug, Event)] +pub struct ChainCompletedEvent { + pub entity: Entity, + pub success: bool, +} + +pub fn chain_execution_system( + mut commands: Commands, + mut chain_query: Query<(Entity, &mut Chain)>, + no_local_command: Query<(), Without>, + mut process_completed_events: EventReader, + mut process_error_events: EventReader, + mut chain_completed_events: EventWriter, +) { + // Handle completed processes + for event in process_completed_events.read() { + if let Ok((entity, mut chain)) = chain_query.get_mut(event.entity) { + if event.exit_status.success() { + // If there are more commands in the chain, start the next one + if !chain.commands.is_empty() { + let local_command = chain.commands.remove(0); + commands + .entity(entity) + .insert(local_command) + .remove::(); + } else { + // If all commands are completed successfully, remove components + commands + .entity(entity) + .remove::<(LocalCommand, Process, Chain)>(); + chain_completed_events.send(ChainCompletedEvent { + entity, + success: true, + }); + } + } else { + // If the process was not successful, abandon the rest of the chain + commands + .entity(entity) + .remove::<(LocalCommand, Process, Chain)>(); + chain_completed_events.send(ChainCompletedEvent { + entity, + success: false, + }); + } + } + } + // Also consider ProcessError events as completed processes + for event in process_error_events.read() { + if let Ok((entity, _)) = chain_query.get_mut(event.entity) { + // Abandon the rest of the chain + commands + .entity(entity) + .remove::<(LocalCommand, Process, Chain)>(); + chain_completed_events.send(ChainCompletedEvent { + entity, + success: false, + }); + } + } + + // Start the first command for new Chain components without LocalCommand + for (entity, mut chain) in chain_query.iter_mut() { + if !chain.commands.is_empty() && no_local_command.get(entity).is_ok() { + let local_command = chain.commands.remove(0); + commands.entity(entity).insert(local_command); + } + } +} diff --git a/src/addons/cleanup.rs b/src/addons/cleanup.rs index f619a8b..71f8870 100644 --- a/src/addons/cleanup.rs +++ b/src/addons/cleanup.rs @@ -13,9 +13,16 @@ pub enum Cleanup { /// Processes without the Cleanup component are ignored. pub(crate) fn cleanup_completed_process( mut commands: Commands, - query: Query<(Entity, &LocalCommand, &Cleanup)>, + query: Query<(Entity, &LocalCommand, &Cleanup, Option<&Chain>)>, ) { - for (entity, local_command, cleanup) in query.iter() { + for (entity, local_command, cleanup, option_chain) in query.iter() { + // Cleanup does not work well with Chains (it tries to cleanup after every command) + // For now, skip cleanup if Chain of commands is not empty yet + if let Some(chain) = option_chain { + if !chain.commands.is_empty() { + continue; + } + } if let LocalCommandState::Done(_) = local_command.state { match cleanup { Cleanup::DespawnEntity => { diff --git a/src/lib.rs b/src/lib.rs index a6734cb..a7f93d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ mod local_command; mod process; mod systems; -pub use addons::chain::Chain; +pub use addons::chain::{Chain, ChainCompletedEvent}; pub use addons::cleanup::Cleanup; pub use addons::delay::Delay; pub use addons::retry::{Retry, RetryEvent}; @@ -72,6 +72,7 @@ impl Plugin for BevyLocalCommandsPlugin { .add_event::() .add_event::() .add_event::() + .add_event::() .add_systems(PreUpdate, addons::delay::apply_delay) .add_systems( Update, @@ -81,6 +82,7 @@ impl Plugin for BevyLocalCommandsPlugin { systems::handle_completed_process, addons::cleanup::cleanup_completed_process, addons::retry::retry_failed_process, + addons::chain::chain_execution_system, ) .chain(), ); From b27a32545298f97926554ba5046bb2e3d19c41d1 Mon Sep 17 00:00:00 2001 From: Edouard Poitras Date: Sun, 18 Aug 2024 10:43:14 -0400 Subject: [PATCH 2/4] Added chaining example in README.md --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index a823587..cf59226 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,23 @@ fn delay_process_start(mut commands: Commands) { } ``` +**Chaining:** + +```rust +fn chain_multiple_commands(mut commands: Commands) { + commands.spawn(( + Chain::new(vec![ + LocalCommand::new("sh").args(["-c", "echo 'First command'"]), + LocalCommand::new("sh").args(["-c", "echo 'Second command'"]), + LocalCommand::new("sh").args(["-c", "echo 'Third command'"]), + ]), + Retry::Attempts(2), // Retry applies to any link in the chain + Delay::Fixed(Duration::from_secs(3)), // Wait 3s between retries and chain commands + Cleanup::RemoveComponents // Remove Chain, Retry, Delay, and Cleanup components upon completion + )); +} +``` + ## Todo - [ ] Mac testing (not sure if it works yet) From 2cb5507339abd696f3634a0bb42866395797feb5 Mon Sep 17 00:00:00 2001 From: Edouard Poitras Date: Wed, 21 Aug 2024 21:32:03 -0400 Subject: [PATCH 3/4] Added windows support to chain examples --- examples/chain_failure.rs | 9 +++++++++ examples/chain_failure_delay_retries.rs | 18 +++++++++++++----- examples/chain_retries_delay_cleanup.rs | 18 +++++++++++++----- examples/simple_chain.rs | 7 +++++++ 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/examples/chain_failure.rs b/examples/chain_failure.rs index 7cb2a2b..d1bfa18 100644 --- a/examples/chain_failure.rs +++ b/examples/chain_failure.rs @@ -13,6 +13,7 @@ fn main() { fn setup(mut commands: Commands) { // Create a chain of commands + #[cfg(not(windows))] let chain = Chain::new(vec![ LocalCommand::new("sh").args(["-c", "echo 'First command' && sleep 1"]), LocalCommand::new("commanddoesnotexist").args(["this should fail"]), // Failure @@ -20,6 +21,14 @@ fn setup(mut commands: Commands) { // Same result with a failed running command //LocalCommand::new("sh").args(["-c", "exit 1"]), // Failure ]); + #[cfg(windows)] + let chain = Chain::new(vec![ + LocalCommand::new("powershell").args(["echo 'First command' && sleep 1"]), + LocalCommand::new("commanddoesnotexist").args(["this should fail"]), // Failure + LocalCommand::new("powershell").args(["echo 'Third command' && sleep 1"]), + // Same result with a failed running command + //LocalCommand::new("powershell").args(["exit 1"]), // Failure + ]); // Spawn an entity with the Chain component let id = commands.spawn(chain).id(); diff --git a/examples/chain_failure_delay_retries.rs b/examples/chain_failure_delay_retries.rs index 47bc492..2fe40f1 100644 --- a/examples/chain_failure_delay_retries.rs +++ b/examples/chain_failure_delay_retries.rs @@ -16,13 +16,21 @@ fn main() { fn setup(mut commands: Commands) { // Spawn an entity with the relevant components + #[cfg(not(windows))] + let chain = Chain::new(vec![ + LocalCommand::new("sh").args(["-c", "echo 'First command'"]), + LocalCommand::new("sh").args(["-c", "exit 1"]), // Failure + LocalCommand::new("sh").args(["-c", "echo 'Third command'"]), + ]); + #[cfg(windows)] + let chain = Chain::new(vec![ + LocalCommand::new("powershell").args(["echo 'First command'"]), + LocalCommand::new("powershell").args(["exit 1"]), // Failure + LocalCommand::new("powershell").args(["echo 'Third command'"]), + ]); let id = commands .spawn(( - Chain::new(vec![ - LocalCommand::new("sh").args(["-c", "echo 'First command'"]), - LocalCommand::new("sh").args(["-c", "exit 1"]), // Failure - LocalCommand::new("sh").args(["-c", "echo 'Third command'"]), - ]), + chain, Retry::Attempts(2), Delay::Fixed(Duration::from_secs(2)), )) diff --git a/examples/chain_retries_delay_cleanup.rs b/examples/chain_retries_delay_cleanup.rs index c26a8dd..6102e86 100644 --- a/examples/chain_retries_delay_cleanup.rs +++ b/examples/chain_retries_delay_cleanup.rs @@ -16,13 +16,21 @@ fn main() { fn setup(mut commands: Commands) { // Spawn a entity with all addons + #[cfg(not(windows))] + let chain = Chain::new(vec![ + LocalCommand::new("sh").args(["-c", "echo 'First command'"]), + LocalCommand::new("sh").args(["-c", "echo 'Second command'"]), + LocalCommand::new("sh").args(["-c", "echo 'Third command'"]), + ]); + #[cfg(windows)] + let chain = Chain::new(vec![ + LocalCommand::new("powershell").args(["echo 'First command'"]), + LocalCommand::new("powershell").args(["echo 'Second command'"]), + LocalCommand::new("powershell").args(["echo 'Third command'"]), + ]); let id = commands .spawn(( - Chain::new(vec![ - LocalCommand::new("sh").args(["-c", "echo 'First command'"]), - LocalCommand::new("sh").args(["-c", "echo 'Second command'"]), - LocalCommand::new("sh").args(["-c", "echo 'Third command'"]), - ]), + chain, Retry::Attempts(2), Delay::Fixed(Duration::from_secs(3)), Cleanup::RemoveComponents, diff --git a/examples/simple_chain.rs b/examples/simple_chain.rs index 9b3f75c..dc1d116 100644 --- a/examples/simple_chain.rs +++ b/examples/simple_chain.rs @@ -13,11 +13,18 @@ fn main() { fn setup(mut commands: Commands) { // Create a chain of commands + #[cfg(not(windows))] let chain = Chain::new(vec![ LocalCommand::new("sh").args(["-c", "echo 'First command' && sleep 1"]), LocalCommand::new("sh").args(["-c", "echo 'Second command' && sleep 1"]), LocalCommand::new("sh").args(["-c", "echo 'Third command' && sleep 1"]), ]); + #[cfg(windows)] + let chain = Chain::new(vec![ + LocalCommand::new("powershell").args(["echo 'First command' && sleep 1"]), + LocalCommand::new("powershell").args(["echo 'Second command' && sleep 1"]), + LocalCommand::new("powershell").args(["echo 'Third command' && sleep 1"]), + ]); // Spawn an entity with the Chain component let id = commands.spawn(chain).id(); From f82c0593e0bdabdfd19093ab86f678dde8194dfb Mon Sep 17 00:00:00 2001 From: Edouard Poitras Date: Fri, 23 Aug 2024 16:14:44 -0400 Subject: [PATCH 4/4] Now using impl IntoIterator --- src/addons/chain.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/addons/chain.rs b/src/addons/chain.rs index 20a7a07..c48f4b9 100644 --- a/src/addons/chain.rs +++ b/src/addons/chain.rs @@ -1,6 +1,7 @@ use crate::local_command::LocalCommand; use crate::{Process, ProcessCompleted, ProcessError}; use bevy::prelude::*; +use std::iter::IntoIterator; #[derive(Component)] pub struct Chain { @@ -8,8 +9,10 @@ pub struct Chain { } impl Chain { - pub fn new(commands: Vec) -> Self { - Self { commands } + pub fn new(commands: impl IntoIterator) -> Self { + Self { + commands: commands.into_iter().collect(), + } } }