diff --git a/Cargo.lock b/Cargo.lock index a1ed6da9..62a711e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3586,6 +3586,7 @@ version = "0.1.42" dependencies = [ "clap", "clap_complete", + "color-eyre", "komorebi-layouts", "komorebi-protocol", ] diff --git a/crates/komorebic-core/Cargo.toml b/crates/komorebic-core/Cargo.toml index 2901be3e..78178f70 100644 --- a/crates/komorebic-core/Cargo.toml +++ b/crates/komorebic-core/Cargo.toml @@ -7,5 +7,6 @@ edition = "2024" [dependencies] clap = { workspace = true } clap_complete = "4" +color-eyre = { workspace = true } komorebi-layouts = { path = "../../komorebi-layouts" } komorebi-protocol = { path = "../komorebi-protocol", default-features = false } diff --git a/crates/komorebic-core/src/launcher.rs b/crates/komorebic-core/src/launcher.rs new file mode 100644 index 00000000..3264d165 --- /dev/null +++ b/crates/komorebic-core/src/launcher.rs @@ -0,0 +1,152 @@ +//! `komorebic start` and `komorebic stop`: the part of them that is the same on every platform. +//! +//! Starting the daemon is the same dance everywhere: look whether it is running, and if it is +//! not, start it, wait a few seconds, look again, and give up after three attempts by running it +//! in front of the user so that what it says on its standard error can be read. What differs is +//! how the daemon is started (a `Command`, a PowerShell script), how its process is found, and +//! what is done once it is up (companions such as whkd and the bar, the licence text), which each +//! platform writes by hand around [`start`]. + +use color_eyre::eyre; +use komorebi_protocol::message::SocketMessage; +use std::io::Write; +use std::time::Duration; + +/// How many times the daemon is started before `start` gives up. +pub const ATTEMPTS: usize = 3; + +/// How long to wait after each start before looking whether the daemon is running. +pub const WAIT: Duration = Duration::from_secs(3); + +/// What `start` needs of a platform. +pub trait Launcher { + /// What the daemon is called in what is printed (`komorebi`, `komorebi.exe`). + fn daemon_name(&self) -> &str; + + /// Start the daemon in the background. + fn spawn(&mut self) -> eyre::Result<()>; + + /// Look at the processes and say whether the daemon is among them. + fn is_running(&mut self) -> eyre::Result; + + /// Run the daemon in front of the user and print what it says on its standard error. + fn diagnose(&mut self) -> eyre::Result<()>; +} + +/// Start the daemon unless it is running, waiting [`WAIT`] after each of at most [`ATTEMPTS`] +/// starts. Returns whether it is running; if it is not, it has been run in front of the user +/// ([`Launcher::diagnose`]) and the caller has nothing more to do. +pub fn start(launcher: &mut impl Launcher) -> eyre::Result { + start_waiting(launcher, WAIT) +} + +/// [`start`] with another waiting time. +pub fn start_waiting(launcher: &mut impl Launcher, wait: Duration) -> eyre::Result { + let name = launcher.daemon_name().to_string(); + let mut attempts = 0; + let mut running = launcher.is_running()?; + + while !running && attempts < ATTEMPTS { + launcher.spawn()?; + + print!("Waiting for {name} to start... "); + std::io::stdout().flush()?; + std::thread::sleep(wait); + + if launcher.is_running()? { + println!("Started!"); + running = true; + } else { + println!("{name} did not start... Trying again"); + attempts += 1; + } + } + + if !running { + println!("\nRunning {name} directly for detailed error output\n"); + launcher.diagnose()?; + } + + Ok(running) +} + +/// The message that stops the daemon, with or without restoring the windows it hid. +pub const fn stop_message(ignore_restore: bool) -> SocketMessage { + if ignore_restore { + SocketMessage::StopIgnoreRestore + } else { + SocketMessage::Stop + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A daemon that is up after it has been started `starts_needed` times. + struct Fake { + starts_needed: usize, + started: usize, + diagnosed: bool, + } + + impl Launcher for Fake { + fn daemon_name(&self) -> &str { + "komorebi" + } + + fn spawn(&mut self) -> eyre::Result<()> { + self.started += 1; + Ok(()) + } + + fn is_running(&mut self) -> eyre::Result { + Ok(self.started >= self.starts_needed) + } + + fn diagnose(&mut self) -> eyre::Result<()> { + self.diagnosed = true; + Ok(()) + } + } + + fn fake(starts_needed: usize) -> Fake { + Fake { + starts_needed, + started: 0, + diagnosed: false, + } + } + + #[test] + fn a_daemon_that_runs_is_not_started() { + let mut launcher = fake(0); + assert!(start_waiting(&mut launcher, Duration::ZERO).unwrap()); + assert_eq!(launcher.started, 0); + } + + #[test] + fn a_daemon_that_needs_a_second_try_is_started_twice() { + let mut launcher = fake(2); + assert!(start_waiting(&mut launcher, Duration::ZERO).unwrap()); + assert_eq!(launcher.started, 2); + assert!(!launcher.diagnosed); + } + + #[test] + fn a_daemon_that_never_starts_is_tried_three_times_and_diagnosed() { + let mut launcher = fake(usize::MAX); + assert!(!start_waiting(&mut launcher, Duration::ZERO).unwrap()); + assert_eq!(launcher.started, ATTEMPTS); + assert!(launcher.diagnosed); + } + + #[test] + fn stop_says_whether_to_restore() { + assert!(matches!(stop_message(false), SocketMessage::Stop)); + assert!(matches!( + stop_message(true), + SocketMessage::StopIgnoreRestore + )); + } +} diff --git a/crates/komorebic-core/src/lib.rs b/crates/komorebic-core/src/lib.rs index 628d6da2..7f2276d6 100644 --- a/crates/komorebic-core/src/lib.rs +++ b/crates/komorebic-core/src/lib.rs @@ -5,6 +5,7 @@ //! //! - [`args`] has the argument blocks the subcommands are built from. //! - [`cli`] has [`cli::Opts`], the command line of a platform, generic over its own subcommands. +//! - [`launcher`] has the part of `start` and `stop` that is the same on every platform. //! - [`messages`] has the subcommands that are one message to the daemon. //! - [`pan`] reads the stream of touchpad gestures that `komorebic pan --stream` is given. //! - [`platform`] has the subcommands of each platform that are not in [`messages`]. @@ -13,6 +14,7 @@ pub mod args; pub mod cli; pub mod docgen; +pub mod launcher; pub mod messages; pub mod pan; pub mod platform;