diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 7d0303a6c..3990caa12 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -70,6 +70,8 @@ windows = { version = "0.48", features = [ "Win32_Foundation", "Win32_UI_WindowsAndMessaging", "Win32_System", + "Win32_System_Diagnostics", + "Win32_System_Diagnostics_ToolHelp", "Win32_System_Threading", ] } diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs index ce70d6091..05f91f95b 100644 --- a/desktop/src-tauri/src/main.rs +++ b/desktop/src-tauri/src/main.rs @@ -39,6 +39,7 @@ use tauri::{ use tokio::sync::mpsc::{self, Sender}; use ui_messages::UiMessage; use workspaces::WorkspacesState; +use util::{kill_child_processes, QUIT_EXIT_CODE}; pub type AppHandle = tauri::AppHandle; @@ -155,19 +156,32 @@ fn main() -> anyhow::Result<()> { .build(ctx) .expect("error while building tauri application"); - info!("Run"); app.run(move |app_handle, event| { let exit_requested_tx = tx.clone(); match event { // Prevents app from exiting when last window is closed, leaving the system tray active - tauri::RunEvent::ExitRequested { api, .. } => { + tauri::RunEvent::ExitRequested { api, code, .. } => { + info!("Handling ExitRequested event."); + + // On windows, we want to kill all existing child processes to prevent dangling processes later down the line. + kill_child_processes(std::process::id()); + tauri::async_runtime::block_on(async move { if let Err(err) = exit_requested_tx.send(UiMessage::ExitRequested).await { error!("Failed to broadcast UI ready message: {:?}", err); } }); + + // Check if the user clicked "Quit" in the system tray, in which case we have to actually close. + if let Some(code) = code { + if code == QUIT_EXIT_CODE { + return + } + } + + // Otherwise, we stay alive in the system tray. api.prevent_exit(); } tauri::RunEvent::WindowEvent { event, label, .. } => { diff --git a/desktop/src-tauri/src/server.rs b/desktop/src-tauri/src/server.rs index 8af324c63..6b82e3d87 100644 --- a/desktop/src-tauri/src/server.rs +++ b/desktop/src-tauri/src/server.rs @@ -77,6 +77,10 @@ async fn signal_handler( { use windows::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE}; use windows::Win32::Foundation::{HANDLE, CloseHandle}; + use crate::util::kill_child_processes; + + kill_child_processes(payload.process_id as u32); + unsafe { let handle: windows::core::Result = OpenProcess(PROCESS_TERMINATE, false, payload.process_id.try_into().unwrap()); if handle.is_err() { diff --git a/desktop/src-tauri/src/system_tray.rs b/desktop/src-tauri/src/system_tray.rs index 0937a4c96..33bcfb4a5 100644 --- a/desktop/src-tauri/src/system_tray.rs +++ b/desktop/src-tauri/src/system_tray.rs @@ -1,10 +1,11 @@ -use crate::{workspaces::WorkspacesState, AppHandle, AppState, UiMessage}; +use crate::{util, workspaces::WorkspacesState, AppHandle, AppState, UiMessage}; use log::{error, warn}; use tauri::{ menu::{Menu, MenuBuilder, MenuEvent, MenuItem, Submenu, SubmenuBuilder}, tray::{TrayIcon, TrayIconEvent}, EventLoopMessage, Manager, State, Wry, }; +use util::QUIT_EXIT_CODE; pub trait SystemTrayIdentifier {} pub type SystemTrayClickHandler = Box)>; @@ -69,7 +70,7 @@ impl SystemTray { pub fn get_event_handler(&self) -> impl Fn(&AppHandle, MenuEvent) + Send + Sync { |app, event| match event.id.as_ref() { Self::QUIT_ID => { - std::process::exit(0); + app.exit(QUIT_EXIT_CODE) } Self::SHOW_DASHBOARD_ID => { let app_state = app.state::(); diff --git a/desktop/src-tauri/src/util.rs b/desktop/src-tauri/src/util.rs index fe12fa754..0504d01bc 100644 --- a/desktop/src-tauri/src/util.rs +++ b/desktop/src-tauri/src/util.rs @@ -1,4 +1,9 @@ use std::time::{Duration, Instant}; +use log::{info, error}; + +// Exit code for the window to signal that the application was quit by the user through the system tray +// and event handlers may not use prevent_exit(). +pub const QUIT_EXIT_CODE: i32 = 1337; /// `measure` the duration it took a function to execute. #[allow(dead_code)] @@ -11,3 +16,88 @@ where start.elapsed() } + +/// Kills all child processes of a pid on windows, does nothing on all the other OSs. +pub fn kill_child_processes(parent_pid: u32) { + #[cfg(windows)] + { + use windows::Win32::System::Threading::*; + use windows::Win32::System::Diagnostics::ToolHelp::*; + use windows::Win32::Foundation::*; + + info!("Trying to kill child processes of PID {}.", parent_pid); + + let snapshot: HANDLE = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0).unwrap() }; + + if snapshot.is_invalid() { + info!("Failed to take process snapshot."); + return; + } + + info!("Obtained process snapshot."); + + let mut process_entry: PROCESSENTRY32 = unsafe { std::mem::zeroed() }; + process_entry.dwSize = std::mem::size_of::() as u32; + + unsafe { + if Process32First(snapshot, &mut process_entry).as_bool() { + loop { + // Check if the process we're looking at is a *direct* child process. + if process_entry.th32ParentProcessID == parent_pid { + let pid = process_entry.th32ProcessID; + + // Extract zero-terminated string for the executable. + let exe_name = process_entry.szExeFile.iter() + .take_while(|&&ch| ch != 0) + .map(|&ch| ch as u8 as char) + .collect::(); + + info!( + "Found process with PID {} as child of PID {} ({}).", + pid, + parent_pid, + exe_name, + ); + + // Special exception: We do not clean up tauri's webviews. For now. + if exe_name == "msedgewebview2.exe" { + info!("Ignoring process PID {}.", pid); + } else { + // Recursively terminate children of children. + kill_child_processes(pid); + + // Obtain handle for the child process. + let child_process_handle: windows::core::Result = OpenProcess(PROCESS_TERMINATE, false, pid); + + if child_process_handle.is_err() { + error!("Unable to open process {}: {:?}", pid, child_process_handle.unwrap_err()); + } else { + let child_process_handle: HANDLE = child_process_handle.unwrap(); + + // Attempt to terminate the child process. + let close_result = TerminateProcess(child_process_handle, 1); + + // Clean up the handle. + CloseHandle(child_process_handle); + + if !close_result.as_bool() { + error!("Unable to terminate process {}", pid); + } else { + info!("Terminated process {}.", pid); + } + } + } + } + + // Obtain next process or end the loop if there is none available. + if !Process32Next(snapshot, &mut process_entry).as_bool() { + break; + } + } + } + + // Clean up the snapshot. + CloseHandle(snapshot); + } + } +}