Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LocalCommand state management #36

Merged
merged 5 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:
run: sudo apt-get update; sudo apt-get install pkg-config libx11-dev libasound2-dev libudev-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev
- name: Run cargo test
run: cargo test
- name: Run cargo examples
run: ./examples/run_all_examples.sh

# Run cargo clippy -- -D warnings
clippy_check:
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bevy_local_commands"
version = "0.6.0"
version = "0.6.1"
edouardpoitras marked this conversation as resolved.
Show resolved Hide resolved
edition = "2021"
description = "Simple local shell commands for the Bevy game engine"
license = "MIT OR Apache-2.0"
Expand Down
34 changes: 26 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,36 @@ fn get_completed(mut process_completed_event: EventReader<ProcessCompleted>) {
}
```

**Retry and cleanup behavior:**
**Retries:**

```rust
fn retries_and_cleanup_on_completion(mut commands: Commands) {
fn retries(mut commands: Commands) {
commands.spawn((
LocalCommand::new("bash").args(["-c", "sleep 1 && invalid-command --that=fails"]),
// Attempt the command 3 times before giving up
// NOTE: The Retry component will be removed from the entity when no retries are left
Retry::Attempts(3)
// Cleanup::DespawnEntity will despawn the entity upon process completion.
// Cleanup::RemoveComponents will remove this crate's components upon process completion.
Cleanup::DespawnEntity
Retry::Attempts(3) // Attempt the command 3 times before giving up
));
}
```

**Cleanup:**

```rust
fn cleanup_on_completion(mut commands: Commands) {
commands.spawn((
LocalCommand::new("bash").args(["-c", "sleep 1"]),
Cleanup::DespawnEntity // Will despawn the entity upon process completion
// Cleanup::RemoveComponents // Will remove only this crate's components upon process completion
));
}
```

**Delay:**

```rust
fn delay_process_start(mut commands: Commands) {
commands.spawn((
LocalCommand::new("bash").args(["-c", "sleep 1"]),
Delay::Fixed(Duration::from_secs(2)), // Start the process after a 2s delay (applies to each retry)
));
}
```
Expand Down
6 changes: 5 additions & 1 deletion examples/kill.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,14 @@ fn startup(mut commands: Commands) {
println!("Spawned the command as entity {id:?}")
}

fn kill(mut active_processes: Query<(Entity, &mut Process)>) {
fn kill(mut active_processes: Query<(Entity, &mut Process)>, mut killed: Local<bool>) {
if *killed {
return;
}
for (entity, mut process) in active_processes.iter_mut() {
println!("Killing {entity:?}");
process.kill().unwrap();
*killed = true;
}
}

Expand Down
13 changes: 10 additions & 3 deletions examples/retries.rs → examples/retries_and_delay.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use bevy::prelude::*;
use bevy_local_commands::{
BevyLocalCommandsPlugin, LocalCommand, ProcessCompleted, Retry, RetryEvent,
BevyLocalCommandsPlugin, Delay, LocalCommand, ProcessCompleted, Retry, RetryEvent,
};
use std::time::Duration;

fn main() {
App::new()
Expand All @@ -22,8 +23,14 @@ fn startup(mut commands: Commands) {
"echo Sleeping for 1s && timeout 1 && THIS SHOULD FAIL",
]);

let id = commands.spawn((cmd, Retry::Attempts(3))).id();
println!("Spawned the command as entity {id:?} with 3 retries");
let id = commands
.spawn((
cmd,
Retry::Attempts(2),
Delay::Fixed(Duration::from_secs(2)),
))
.id();
println!("Spawned the command as entity {id:?} with 2 retries and a 2s delay");
}

fn update(
Expand Down
58 changes: 58 additions & 0 deletions examples/retries_and_delay_and_cleanup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use bevy::prelude::*;
use bevy_local_commands::{
BevyLocalCommandsPlugin, Cleanup, Delay, LocalCommand, ProcessCompleted, Retry, RetryEvent,
};
use std::time::Duration;

fn main() {
App::new()
.add_plugins((MinimalPlugins, BevyLocalCommandsPlugin))
.add_systems(Startup, startup)
.add_systems(Update, update)
.run();
}

fn startup(mut commands: Commands) {
// Choose the command based on the OS
#[cfg(not(windows))]
let cmd =
LocalCommand::new("sh").args(["-c", "echo Sleeping for 1s && sleep 1 && THIS SHOULD FAIL"]);
#[cfg(windows)]
let cmd = LocalCommand::new("cmd").args([
"/C",
"echo Sleeping for 1s && timeout 1 && THIS SHOULD FAIL",
]);

let id = commands
.spawn((
cmd,
Retry::Attempts(2),
Delay::Fixed(Duration::from_secs(2)),
Cleanup::RemoveComponents,
))
.id();
println!("Spawned the command as entity {id:?} with 2 retries and a 2s delay");
}

fn update(
mut process_completed_event: EventReader<ProcessCompleted>,
query: Query<&LocalCommand, With<Retry>>,
mut retry_events: EventReader<RetryEvent>,
) {
if let Some(process_completed) = process_completed_event.read().last() {
if let Ok(local_command) = query.get(process_completed.entity) {
println!(
"Command {:?} {:?} completed (Success - {})",
local_command.get_program(),
local_command.get_args(),
process_completed.exit_status.success()
);
} else {
println!("Retry component removed from entity, exiting");
std::process::exit(0);
}
}
for retry_event in retry_events.read() {
println!("Retry event triggered: {:?}", retry_event);
}
}
42 changes: 26 additions & 16 deletions examples/retries_and_remove.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use bevy::prelude::*;
use bevy_local_commands::{
BevyLocalCommandsPlugin, Cleanup, LocalCommand, ProcessCompleted, Retry,
BevyLocalCommandsPlugin, Cleanup, LocalCommand, Process, ProcessCompleted, Retry, RetryEvent,
};

fn main() {
Expand All @@ -19,27 +19,37 @@ fn startup(mut commands: Commands) {
let cmd = LocalCommand::new("cmd").args(["/C", "echo Sleeping for 1s && timeout 1 && INVALID"]);

let id = commands
.spawn((cmd, Retry::Attempts(3), Cleanup::DespawnEntity))
.spawn((cmd, Retry::Attempts(3), Cleanup::RemoveComponents))
.id();
println!("Spawned the command as temporary entity {id:?} with 3 retries");
}

fn update(
mut process_completed_event: EventReader<ProcessCompleted>,
query: Query<(&LocalCommand, &Retry)>, // We could also listen for RetryEvent to get retry status
mut retry_events: EventReader<RetryEvent>,
query: Query<(
Entity,
Option<&LocalCommand>,
Option<&Process>,
Option<&Retry>,
Option<&Cleanup>,
)>,
) {
if let Some(process_completed) = process_completed_event.read().last() {
if let Ok((local_command, retry)) = query.get(process_completed.entity) {
println!(
"Command {:?} {:?} completed (Success - {})",
local_command.get_program(),
local_command.get_args(),
process_completed.exit_status.success()
);
println!("Retries remaining: {:?}", retry);
} else {
println!("Can't find entity anymore, exiting");
std::process::exit(0);
}
for retry_event in retry_events.read() {
println!("Retry event triggered: {:?}", retry_event);
let components = query.get(retry_event.entity).unwrap();
assert!(components.1.is_some());
assert!(components.2.is_none());
assert!(components.3.is_some());
assert!(components.4.is_some());
}
for process_completed in process_completed_event.read() {
println!("{:?}", process_completed);
let components = query.get(process_completed.entity).unwrap();
assert!(components.1.is_none());
assert!(components.2.is_none());
assert!(components.3.is_none());
assert!(components.4.is_none());
std::process::exit(0);
}
}
8 changes: 8 additions & 0 deletions examples/run_all_examples.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
cargo run --example despawn_on_completion
cargo run --example error
cargo run --example input
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
5 changes: 5 additions & 0 deletions src/addons/chain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use bevy::prelude::*;

// Coming soon.
#[derive(Debug, Component)]
pub enum Chain {}
25 changes: 8 additions & 17 deletions src/addons/cleanup.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use bevy::prelude::*;

use crate::{LocalCommand, Process, ProcessCompleted, Retry};
use crate::{process::Process, Chain, Delay, LocalCommand, LocalCommandState, Retry};

#[derive(Debug, Component)]
pub enum Cleanup {
Expand All @@ -11,31 +11,22 @@ pub enum Cleanup {
/// Clean up any completed processes according to the Cleanup component.
///
/// Processes without the Cleanup component are ignored.
/// Takes into account the Retry component (will not perform cleanup until component is removed).
pub(crate) fn cleanup_completed_process(
mut commands: Commands,
query: Query<(&Cleanup, Option<&Retry>)>,
mut process_completed_events: EventReader<ProcessCompleted>,
query: Query<(Entity, &LocalCommand, &Cleanup)>,
) {
for process_completed_event in process_completed_events.read() {
if let Ok((cleanup, option_retry)) = query.get(process_completed_event.entity) {
// Don't cleanup a failed process if the Retry component is still attached to entity.
if !process_completed_event.exit_status.success() && option_retry.is_some() {
continue;
}
for (entity, local_command, cleanup) in query.iter() {
if let LocalCommandState::Done(_) = local_command.state {
match cleanup {
Cleanup::DespawnEntity => {
if let Some(mut entity_commands) =
commands.get_entity(process_completed_event.entity)
{
if let Some(mut entity_commands) = commands.get_entity(entity) {
entity_commands.despawn();
}
},
Cleanup::RemoveComponents => {
if let Some(mut entity_commands) =
commands.get_entity(process_completed_event.entity)
{
entity_commands.remove::<(Process, Cleanup, LocalCommand)>();
if let Some(mut entity_commands) = commands.get_entity(entity) {
entity_commands
.remove::<(Process, Chain, Delay, Retry, Cleanup, LocalCommand)>();
}
},
}
Expand Down
26 changes: 26 additions & 0 deletions src/addons/delay.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use bevy::prelude::*;
use std::time::Duration;

use crate::{process::Process, LocalCommand, LocalCommandState};

#[derive(Debug, Component)]
pub enum Delay {
Fixed(Duration),
}

/// Apply delay settings to entities with LocalCommand + Delay components that have yet to be processed.
///
/// State of LocalCommandState::Ready is required for the delay to be applied.
/// This system should run before the handle_new_command system.
pub(crate) fn apply_delay(mut query: Query<(&mut LocalCommand, &Delay), Without<Process>>) {
for (mut local_command, delay) in query.iter_mut() {
if local_command.state == LocalCommandState::Ready && local_command.delay.is_none() {
match delay {
Delay::Fixed(duration) => {
local_command.delay =
Some(Timer::from_seconds(duration.as_secs_f32(), TimerMode::Once));
},
}
}
}
}
2 changes: 2 additions & 0 deletions src/addons/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
pub mod chain;
pub mod cleanup;
pub mod delay;
pub mod retry;
42 changes: 13 additions & 29 deletions src/addons/retry.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use bevy::prelude::*;

use crate::{systems::spawn_process, LocalCommand, Process, ProcessCompleted};
use crate::{process::Process, LocalCommand, LocalCommandState};

#[derive(Debug, Component)]
pub enum Retry {
Expand All @@ -19,46 +19,30 @@ pub struct RetryEvent {
/// The Retry component is removed from the entity when retries are done.
pub(crate) fn retry_failed_process(
mut commands: Commands,
mut query: Query<(&mut LocalCommand, &mut Process, &mut Retry)>,
mut query: Query<(Entity, &mut LocalCommand, &mut Retry), With<Process>>,
mut retry_events: EventWriter<RetryEvent>,
mut process_completed_events: EventReader<ProcessCompleted>,
) {
for process_completed_event in process_completed_events.read() {
if process_completed_event.exit_status.success() {
continue;
}
if let Ok((mut local_command, mut process, mut retry)) =
query.get_mut(process_completed_event.entity)
{
for (entity, mut local_command, mut retry) in query.iter_mut() {
if local_command.state == LocalCommandState::Error {
match &mut *retry {
Retry::Attempts(retries) => {
if let Some(mut entity_commands) =
commands.get_entity(process_completed_event.entity)
{
if let Some(mut entity_commands) = commands.get_entity(entity) {
if *retries < 1 {
entity_commands.remove::<Retry>();
continue;
}

// Update the retry attempts
*retries -= 1;

// Spawn the process once again
match spawn_process(&mut local_command.command) {
Ok(new_process) => {
*process = new_process;
retry_events.send(RetryEvent {
entity: process_completed_event.entity,
retries_left: *retries,
});
},
Err(_) => {
error!(
"Failed to retry process: {:?} {:?}",
local_command.get_program(),
local_command.get_args()
);
},
}
commands.entity(entity).remove::<Process>();
local_command.delay = None;
local_command.state = LocalCommandState::Ready;
retry_events.send(RetryEvent {
entity,
retries_left: *retries,
});
}
},
}
Expand Down
Loading
Loading