From f607be8777e3706c9023b6c5a3877565770189e0 Mon Sep 17 00:00:00 2001 From: Sarthak Singh <35749450+SarthakSingh31@users.noreply.github.com> Date: Mon, 1 Jul 2024 19:38:42 +0530 Subject: [PATCH] Handle `Ctrl+C` in the terminal properly (#14001) # Objective Fixes #13995. ## Solution Override the default `Ctrl+C` handler with one that sends `AppExit` event to every app with `TerminalCtrlCHandlerPlugin`. ## Testing Tested by running the `3d_scene` example and hitting `Ctrl+C` in the terminal. --- ## Changelog Handles `Ctrl+C` in the terminal gracefully. ## Migration Guide If you are overriding the `Ctrl+C` handler then you should call `TerminalCtrlCHandlerPlugin::gracefully_exit` from your handler. It will tell the app to exit. --- crates/bevy_app/Cargo.toml | 3 + crates/bevy_app/src/lib.rs | 4 + .../bevy_app/src/terminal_ctrl_c_handler.rs | 73 +++++++++++++++++++ crates/bevy_internal/src/default_plugins.rs | 5 ++ 4 files changed, 85 insertions(+) create mode 100644 crates/bevy_app/src/terminal_ctrl_c_handler.rs diff --git a/crates/bevy_app/Cargo.toml b/crates/bevy_app/Cargo.toml index 5f968f64af4ab..2ea2dc602763b 100644 --- a/crates/bevy_app/Cargo.toml +++ b/crates/bevy_app/Cargo.toml @@ -26,6 +26,9 @@ bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" } downcast-rs = "1.2.0" thiserror = "1.0" +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +ctrlc = "3.4.4" + [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = { version = "0.2" } web-sys = { version = "0.3", features = ["Window"] } diff --git a/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs index 38d8d0e280970..86bec47521e8d 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -14,6 +14,8 @@ mod plugin; mod plugin_group; mod schedule_runner; mod sub_app; +#[cfg(not(target_arch = "wasm32"))] +mod terminal_ctrl_c_handler; pub use app::*; pub use bevy_derive::DynamicPlugin; @@ -23,6 +25,8 @@ pub use plugin::*; pub use plugin_group::*; pub use schedule_runner::*; pub use sub_app::*; +#[cfg(not(target_arch = "wasm32"))] +pub use terminal_ctrl_c_handler::*; #[allow(missing_docs)] pub mod prelude { diff --git a/crates/bevy_app/src/terminal_ctrl_c_handler.rs b/crates/bevy_app/src/terminal_ctrl_c_handler.rs new file mode 100644 index 0000000000000..54e0bc5338ee2 --- /dev/null +++ b/crates/bevy_app/src/terminal_ctrl_c_handler.rs @@ -0,0 +1,73 @@ +use std::sync::atomic::{AtomicBool, Ordering}; + +use bevy_ecs::event::EventWriter; + +use crate::{App, AppExit, Plugin, Update}; + +pub use ctrlc; + +/// Indicates that all [`App`]'s should exit. +static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); + +/// Gracefully handles `Ctrl+C` by emitting a [`AppExit`] event. This plugin is part of the `DefaultPlugins`. +/// +/// ```no_run +/// # use bevy_app::{App, NoopPluginGroup as MinimalPlugins, PluginGroup, TerminalCtrlCHandlerPlugin}; +/// fn main() { +/// App::new() +/// .add_plugins(MinimalPlugins) +/// .add_plugins(TerminalCtrlCHandlerPlugin) +/// .run(); +/// } +/// ``` +/// +/// If you want to setup your own `Ctrl+C` handler, you should call the +/// [`TerminalCtrlCHandlerPlugin::gracefully_exit`] function in your handler if you want bevy to gracefully exit. +/// ```no_run +/// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup, TerminalCtrlCHandlerPlugin, ctrlc}; +/// fn main() { +/// // Your own `Ctrl+C` handler +/// ctrlc::set_handler(move || { +/// // Other clean up code ... +/// +/// TerminalCtrlCHandlerPlugin::gracefully_exit(); +/// }); +/// +/// App::new() +/// .add_plugins(DefaultPlugins) +/// .run(); +/// } +/// ``` +#[derive(Default)] +pub struct TerminalCtrlCHandlerPlugin; + +impl TerminalCtrlCHandlerPlugin { + /// Sends the [`AppExit`] event to all apps using this plugin to make them gracefully exit. + pub fn gracefully_exit() { + SHOULD_EXIT.store(true, Ordering::Relaxed); + } + + /// Sends a [`AppExit`] event when the user presses `Ctrl+C` on the terminal. + fn exit_on_flag(mut events: EventWriter) { + if SHOULD_EXIT.load(Ordering::Relaxed) { + events.send(AppExit::from_code(130)); + } + } +} + +impl Plugin for TerminalCtrlCHandlerPlugin { + fn build(&self, app: &mut App) { + let result = ctrlc::try_set_handler(move || { + Self::gracefully_exit(); + }); + match result { + Ok(()) => {} + Err(ctrlc::Error::MultipleHandlers) => { + bevy_utils::tracing::info!("Skipping installing `Ctrl+C` handler as one was already installed. Please call `TerminalCtrlCHandlerPlugin::gracefully_exit` in your own `Ctrl+C` handler if you want Bevy to gracefully exit on `Ctrl+C`."); + } + Err(err) => bevy_utils::tracing::warn!("Failed to set `Ctrl+C` handler: {err}"), + } + + app.add_systems(Update, TerminalCtrlCHandlerPlugin::exit_on_flag); + } +} diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index 0560d42a8151f..5412084381508 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -59,6 +59,11 @@ impl PluginGroup for DefaultPlugins { .add(bevy_window::WindowPlugin::default()) .add(bevy_a11y::AccessibilityPlugin); + #[cfg(not(target_arch = "wasm32"))] + { + group = group.add(bevy_app::TerminalCtrlCHandlerPlugin); + } + #[cfg(feature = "bevy_asset")] { group = group.add(bevy_asset::AssetPlugin::default());