diff --git a/src/app.rs b/src/app.rs index 0f927ab..384a953 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,5 @@ use crate::{ - config, + application_lifecycle, config, config::{window_definition::WindowName, AnchorPoint, WindowStacking}, eww_state, script_var_handler::*, @@ -60,6 +60,7 @@ pub struct App { } impl App { + /// Handle an EwwCommand event. pub fn handle_command(&mut self, event: EwwCommand) { log::debug!("Handling event: {:?}", &event); let result: Result<_> = try { @@ -78,10 +79,8 @@ impl App { } EwwCommand::KillServer => { log::info!("Received kill command, stopping server!"); - self.script_var_handler.stop_all(); - self.windows.drain().for_each(|(_, w)| w.close()); - // script_var_process::on_application_death(); - std::process::exit(0); + self.stop_application(); + crate::application_lifecycle::send_exit()?; } EwwCommand::CloseAll => { log::info!("Received close command, closing all windows"); @@ -119,6 +118,12 @@ impl App { crate::print_result_err!("while handling event", &result); } + fn stop_application(&mut self) { + self.script_var_handler.stop_all(); + self.windows.drain().for_each(|(_, w)| w.close()); + gtk::main_quit(); + } + fn update_state(&mut self, fieldname: VarName, value: PrimitiveValue) -> Result<()> { self.eww_state.update_variable(fieldname, value) } diff --git a/src/application_lifecycle.rs b/src/application_lifecycle.rs new file mode 100644 index 0000000..e012206 --- /dev/null +++ b/src/application_lifecycle.rs @@ -0,0 +1,43 @@ +//! Module concerned with handling the global application lifecycle of eww. +//! Currently, this only means handling application exit by providing a global +//! `recv_exit()` function which can be awaited to receive an event in case of application termination. + +use anyhow::*; +use tokio::sync::broadcast; + +lazy_static::lazy_static! { + static ref APPLICATION_EXIT_SENDER: broadcast::Sender<()> = broadcast::channel(2).0; +} + +/// Notify all listening tasks of the termination of the eww application process. +pub fn send_exit() -> Result<()> { + (&*APPLICATION_EXIT_SENDER) + .send(()) + .context("Failed to send exit lifecycle event")?; + Ok(()) +} + +/// Yields Ok(()) on application termination. Await on this in all long-running tasks +/// and perform any cleanup if necessary. +pub async fn recv_exit() -> Result<()> { + (&*APPLICATION_EXIT_SENDER) + .subscribe() + .recv() + .await + .context("Failed to receive lifecycle event") +} + +/// Select in a loop, breaking once a application termination event (see `crate::application_lifecycle`) is received. +#[macro_export] +macro_rules! loop_select_exiting { + ($($content:tt)*) => { + loop { + tokio::select! { + Ok(()) = crate::application_lifecycle::recv_exit() => { + break; + } + $($content)* + } + } + }; +} diff --git a/src/main.rs b/src/main.rs index b0d5f7b..faffbe0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ use anyhow::*; use std::{os::unix::net, path::PathBuf}; pub mod app; +pub mod application_lifecycle; pub mod client; pub mod config; pub mod eww_state; diff --git a/src/script_var_handler.rs b/src/script_var_handler.rs index 027689e..0e5b4d5 100644 --- a/src/script_var_handler.rs +++ b/src/script_var_handler.rs @@ -13,37 +13,8 @@ use tokio::{ }; use tokio_util::sync::CancellationToken; -#[derive(Debug, Eq, PartialEq)] -enum ScriptVarHandlerMsg { - AddVar(config::ScriptVar), - Stop(VarName), - StopAll, -} - -pub struct ScriptVarHandlerHandle { - msg_send: UnboundedSender, -} - -impl ScriptVarHandlerHandle { - pub fn add(&self, script_var: config::ScriptVar) { - self.msg_send.send(ScriptVarHandlerMsg::AddVar(script_var)).unwrap(); - } - - pub fn stop_for_variable(&self, name: VarName) { - self.msg_send.send(ScriptVarHandlerMsg::Stop(name)).unwrap(); - } - - pub fn stop_all(&self) { - self.msg_send.send(ScriptVarHandlerMsg::StopAll).unwrap(); - } -} - -/// Handler that manages running and updating [ScriptVar]s -struct ScriptVarHandler { - tail_handler: TailVarHandler, - poll_handler: PollVarHandler, -} - +/// Initialize the script var handler, and return a handle to that handler, which can be used to control +/// the script var execution. pub fn init(evt_send: UnboundedSender) -> Result { let (msg_send, mut msg_recv) = tokio::sync::mpsc::unbounded_channel(); let handle = ScriptVarHandlerHandle { msg_send }; @@ -55,8 +26,8 @@ pub fn init(evt_send: UnboundedSender) -> Result match msg { ScriptVarHandlerMsg::AddVar(var) => { handler.add(var).await; } @@ -66,16 +37,53 @@ pub fn init(evt_send: UnboundedSender) -> Result { handler.stop_all(); } - } - } + }, + else => break, + }; }; }) }); Ok(handle) } +/// Handle to the script-var handling system. +pub struct ScriptVarHandlerHandle { + msg_send: UnboundedSender, +} + +impl ScriptVarHandlerHandle { + /// Add a new script-var that should be executed. + pub fn add(&self, script_var: config::ScriptVar) { + self.msg_send.send(ScriptVarHandlerMsg::AddVar(script_var)).unwrap(); + } + + /// Stop the execution of a specific script-var. + pub fn stop_for_variable(&self, name: VarName) { + self.msg_send.send(ScriptVarHandlerMsg::Stop(name)).unwrap(); + } + + /// Stop the execution of all script-vars. + pub fn stop_all(&self) { + self.msg_send.send(ScriptVarHandlerMsg::StopAll).unwrap(); + } +} + +/// Message enum used by the ScriptVarHandlerHandle to communicate to the ScriptVarHandler +#[derive(Debug, Eq, PartialEq)] +enum ScriptVarHandlerMsg { + AddVar(config::ScriptVar), + Stop(VarName), + StopAll, +} + +/// Handler that manages running and updating [ScriptVar]s +struct ScriptVarHandler { + tail_handler: TailVarHandler, + poll_handler: PollVarHandler, +} + impl ScriptVarHandler { - pub async fn add(&mut self, script_var: config::ScriptVar) { + async fn add(&mut self, script_var: config::ScriptVar) { match script_var { config::ScriptVar::Poll(var) => self.poll_handler.start(var).await, config::ScriptVar::Tail(var) => self.tail_handler.start(var).await, @@ -83,27 +91,21 @@ impl ScriptVarHandler { } /// Stop the handler that is responsible for a given variable. - pub fn stop_for_variable(&mut self, name: &VarName) -> Result<()> { + fn stop_for_variable(&mut self, name: &VarName) -> Result<()> { log::debug!("Stopping script var process for variable {}", name); - self.tail_handler.stop_for_variable(name)?; - self.poll_handler.stop_for_variable(name)?; + self.tail_handler.stop_for_variable(name); + self.poll_handler.stop_for_variable(name); Ok(()) } /// stop all running scripts and schedules - pub fn stop_all(&mut self) { + fn stop_all(&mut self) { log::debug!("Stopping script-var-handlers"); self.tail_handler.stop_all(); self.poll_handler.stop_all(); } } -impl Drop for ScriptVarHandler { - fn drop(&mut self) { - self.stop_all(); - } -} - struct PollVarHandler { evt_send: UnboundedSender, poll_handles: HashMap, @@ -124,7 +126,7 @@ impl PollVarHandler { self.poll_handles.insert(var.name.clone(), cancellation_token.clone()); let evt_send = self.evt_send.clone(); tokio::spawn(async move { - crate::loop_select! { + crate::loop_select_exiting! { _ = cancellation_token.cancelled() => break, _ = tokio::time::sleep(var.interval) => { let result: Result<_> = try { @@ -136,19 +138,24 @@ impl PollVarHandler { }); } - pub fn stop_for_variable(&mut self, name: &VarName) -> Result<()> { + fn stop_for_variable(&mut self, name: &VarName) { if let Some(token) = self.poll_handles.remove(name) { log::debug!("stopped poll var {}", name); token.cancel() } - Ok(()) } - pub fn stop_all(&mut self) { + fn stop_all(&mut self) { self.poll_handles.drain().for_each(|(_, token)| token.cancel()); } } +impl Drop for PollVarHandler { + fn drop(&mut self) { + self.stop_all(); + } +} + struct TailVarHandler { evt_send: UnboundedSender, tail_process_handles: HashMap, @@ -178,27 +185,24 @@ impl TailVarHandler { .spawn() .unwrap(); let mut stdout_lines = BufReader::new(handle.stdout.take().unwrap()).lines(); - crate::loop_select! { + crate::loop_select_exiting! { _ = handle.wait() => break, _ = cancellation_token.cancelled() => break, - line = stdout_lines.next_line() => match line { - Ok(Some(line)) => { - let new_value = PrimitiveValue::from_string(line.to_owned()); - evt_send.send(EwwCommand::UpdateVars(vec![(var.name.to_owned(), new_value)])).unwrap(); - }, - Ok(None) => break, - Err(_e) => break, + Ok(Some(line)) = stdout_lines.next_line() => { + let new_value = PrimitiveValue::from_string(line.to_owned()); + evt_send.send(EwwCommand::UpdateVars(vec![(var.name.to_owned(), new_value)])).unwrap(); } + else => break, } + handle.kill().await.unwrap(); }); } - fn stop_for_variable(&mut self, name: &VarName) -> Result<()> { + fn stop_for_variable(&mut self, name: &VarName) { if let Some(token) = self.tail_process_handles.remove(name) { log::debug!("stopped tail var {}", name); token.cancel(); } - Ok(()) } fn stop_all(&mut self) { @@ -206,97 +210,8 @@ impl TailVarHandler { } } -pub mod script_var_process { - use anyhow::*; - use nix::{ - sys::{signal, wait}, - unistd::Pid, - }; - use std::{ffi::CString, io::BufReader, sync::Mutex}; - - lazy_static::lazy_static! { - static ref SCRIPT_VAR_CHILDREN: Mutex> = Mutex::new(Vec::new()); - } - - fn terminate_pid(pid: u32) -> Result<()> { - signal::kill(Pid::from_raw(pid as i32), signal::SIGTERM)?; - wait::waitpid(Pid::from_raw(pid as i32), None)?; - Ok(()) - } - - /// This function should be called in the signal handler, killing all child processes. - pub fn on_application_death() { - SCRIPT_VAR_CHILDREN.lock().unwrap().drain(..).for_each(|pid| { - let result = terminate_pid(pid); - crate::print_result_err!("While killing process '{}' during cleanup", &result); - }); - } - - pub struct ScriptVarProcess { - pub pid: i32, - pub stdout_reader: BufReader, - } - - impl ScriptVarProcess { - pub(super) fn run(command: &str) -> Result { - use nix::unistd::*; - use std::os::unix::io::AsRawFd; - - let pipe = filedescriptor::Pipe::new()?; - - match unsafe { fork()? } { - ForkResult::Parent { child, .. } => { - SCRIPT_VAR_CHILDREN.lock().unwrap().push(child.as_raw() as u32); - - close(pipe.write.as_raw_fd())?; - - Ok(ScriptVarProcess { - stdout_reader: BufReader::new(pipe.read), - pid: child.as_raw(), - }) - } - ForkResult::Child => { - let _ = setpgid(Pid::from_raw(0), Pid::from_raw(0)); - match unsafe { fork()? } { - ForkResult::Parent { .. } => { - simple_signal::set_handler(&[simple_signal::Signal::Int, simple_signal::Signal::Term], |_| { - let pgid = getpgid(Some(getpid())).unwrap(); - let _ = signal::killpg(pgid, nix::sys::signal::SIGKILL); - while nix::sys::wait::wait().unwrap().pid().is_some() {} - }); - loop {} - } - ForkResult::Child => { - close(pipe.read.as_raw_fd()).unwrap(); - dup2(pipe.write.as_raw_fd(), std::io::stdout().as_raw_fd()).unwrap(); - dup2(pipe.write.as_raw_fd(), std::io::stderr().as_raw_fd()).unwrap(); - execv( - CString::new("/bin/sh").unwrap().as_ref(), - &[ - CString::new("/bin/sh").unwrap(), - CString::new("-c").unwrap(), - CString::new(command).unwrap(), - ], - ) - .unwrap(); - unreachable!( - "Child fork called exec, thus the process was replaced by the command the user provided" - ); - } - } - } - } - } - - pub(super) fn kill(&self) -> Result<()> { - SCRIPT_VAR_CHILDREN.lock().unwrap().retain(|item| *item != self.pid as u32); - terminate_pid(self.pid as u32).context("Error manually killing tail-var script") - } - } - - impl Drop for ScriptVarProcess { - fn drop(&mut self) { - let _ = self.kill(); - } +impl Drop for TailVarHandler { + fn drop(&mut self) { + self.stop_all(); } } diff --git a/src/server.rs b/src/server.rs index 9f22782..5398bb9 100644 --- a/src/server.rs +++ b/src/server.rs @@ -14,10 +14,9 @@ pub fn initialize_server(should_detach: bool, action: opts::ActionWithServer) -> do_detach()?; } - simple_signal::set_handler(&[simple_signal::Signal::Int, simple_signal::Signal::Term], |_| { + simple_signal::set_handler(&[simple_signal::Signal::Int, simple_signal::Signal::Term], move |_| { println!("Shutting down eww daemon..."); - // script_var_handler::script_var_process::on_application_death(); - std::process::exit(0); + crate::application_lifecycle::send_exit().unwrap(); }); let config_file_path = crate::CONFIG_DIR.join("eww.xml"); @@ -65,14 +64,8 @@ pub fn initialize_server(should_detach: bool, action: opts::ActionWithServer) -> .build() .expect("Failed to initialize tokio runtime"); rt.block_on(async { - let filewatch_join_handle = { - let ui_send = ui_send.clone(); - tokio::spawn(async move { run_filewatch_thingy(config_file_path, scss_file_path, ui_send).await }) - }; - let ipc_server_join_handle = tokio::spawn(async move { async_server(ui_send.clone()).await }); - + // print out the response of this initial command, if there is any tokio::spawn(async { - // print out the response of this initial command, if there is any if let Some(mut response_recv) = maybe_response_recv { if let Ok(Some(response)) = tokio::time::timeout(Duration::from_millis(100), response_recv.recv()).await { println!("{}", response); @@ -80,7 +73,27 @@ pub fn initialize_server(should_detach: bool, action: opts::ActionWithServer) -> } }); - let result = tokio::try_join!(filewatch_join_handle, ipc_server_join_handle); + let filewatch_join_handle = { + let ui_send = ui_send.clone(); + tokio::spawn(async move { run_filewatch_thingy(config_file_path, scss_file_path, ui_send).await }) + }; + + let ipc_server_join_handle = { + let ui_send = ui_send.clone(); + tokio::spawn(async move { async_server(ui_send).await }) + }; + + let forward_lifecycle_to_app_handle = { + let ui_send = ui_send.clone(); + tokio::spawn(async move { + while let Ok(()) = crate::application_lifecycle::recv_exit().await { + let _ = ui_send.send(app::EwwCommand::KillServer); + } + }) + }; + + let result = tokio::try_join!(filewatch_join_handle, ipc_server_join_handle, forward_lifecycle_to_app_handle); + if let Err(e) = result { eprintln!("Eww exiting with error: {:?}", e); } @@ -94,20 +107,20 @@ pub fn initialize_server(should_detach: bool, action: opts::ActionWithServer) -> }); gtk::main(); + log::info!("main application thread finished"); Ok(()) } async fn async_server(evt_send: UnboundedSender) -> Result<()> { let listener = tokio::net::UnixListener::bind(&*crate::IPC_SOCKET_PATH)?; - loop { - tokio::select! { - connection = listener.accept() => match connection { - Ok((stream, _addr)) => handle_connection(stream, evt_send.clone()).await?, - Err(e) => eprintln!("Failed to connect to client: {:?}", e), - } + crate::loop_select_exiting! { + connection = listener.accept() => match connection { + Ok((stream, _addr)) => handle_connection(stream, evt_send.clone()).await?, + Err(e) => eprintln!("Failed to connect to client: {:?}", e), } } + Ok(()) } async fn handle_connection(mut stream: tokio::net::UnixStream, evt_send: UnboundedSender) -> Result<()> { @@ -121,7 +134,6 @@ async fn handle_connection(mut stream: tokio::net::UnixStream, evt_send: Unbound log::info!("received command from IPC: {:?}", &action); - // TODO clean up the maybe_response_recv let (command, maybe_response_recv) = action.into_eww_command(); evt_send.send(command)?; @@ -152,22 +164,26 @@ async fn run_filewatch_thingy>( let mut buffer = [0; 1024]; let mut event_stream = inotify.event_stream(&mut buffer)?; - while let Some(Ok(event)) = event_stream.next().await { - if event.wd == config_file_descriptor { - try_logging_errors!("handling change of config file" => { - log::info!("Reloading eww configuration"); - let new_eww_config = config::EwwConfig::read_from_file(config_file_path.as_ref())?; - evt_send.send(app::EwwCommand::ReloadConfig(new_eww_config))?; - }); - } else if event.wd == scss_file_descriptor { - try_logging_errors!("handling change of scss file" => { - log::info!("reloading eww css file"); - let eww_css = util::parse_scss_from_file(scss_file_path.as_ref())?; - evt_send.send(app::EwwCommand::ReloadCss(eww_css))?; - }); - } else { - eprintln!("Got inotify event for unknown thing: {:?}", event); + + crate::loop_select_exiting! { + Some(Ok(event)) = event_stream.next() => { + if event.wd == config_file_descriptor { + try_logging_errors!("handling change of config file" => { + log::info!("Reloading eww configuration"); + let new_eww_config = config::EwwConfig::read_from_file(config_file_path.as_ref())?; + evt_send.send(app::EwwCommand::ReloadConfig(new_eww_config))?; + }); + } else if event.wd == scss_file_descriptor { + try_logging_errors!("handling change of scss file" => { + log::info!("reloading eww css file"); + let eww_css = crate::util::parse_scss_from_file(scss_file_path.as_ref())?; + evt_send.send(app::EwwCommand::ReloadCss(eww_css))?; + }); + } else { + eprintln!("Got inotify event for unknown thing: {:?}", event); + } } + else => break, } Ok(()) }