Skip to content

Commit

Permalink
fix: Add a way to terminate all child processes of the application un…
Browse files Browse the repository at this point in the history
…der certain conditions to prevent dangling processes
  • Loading branch information
PRTTMPRPHT committed Nov 22, 2024
1 parent 7bf0537 commit 8c9c92b
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 4 deletions.
2 changes: 2 additions & 0 deletions desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
] }

Expand Down
18 changes: 16 additions & 2 deletions desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Wry>;

Expand Down Expand Up @@ -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, .. } => {
Expand Down
4 changes: 4 additions & 0 deletions desktop/src-tauri/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<HANDLE> = OpenProcess(PROCESS_TERMINATE, false, payload.process_id.try_into().unwrap());
if handle.is_err() {
Expand Down
5 changes: 3 additions & 2 deletions desktop/src-tauri/src/system_tray.rs
Original file line number Diff line number Diff line change
@@ -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<dyn Fn(&AppHandle, State<AppState>)>;
Expand Down Expand Up @@ -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::<AppState>();
Expand Down
90 changes: 90 additions & 0 deletions desktop/src-tauri/src/util.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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::<PROCESSENTRY32>() 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::<String>();

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<HANDLE> = 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);
}
}
}

0 comments on commit 8c9c92b

Please sign in to comment.