fn thread_line(id: u32, current: bool, name: &str, start: Option<&str>) -> String {
let mut line = format!("thread {id}");
if current {
line.push_str(" (this thread)");
}
if !name.is_empty() {
line.push_str(&format!(" \"{name}\""));
}
if let Some(start) = start {
line.push_str(&format!(", started in {start}"));
}
line
}
fn module_offset(module_path: &str, address: usize, base: usize) -> String {
let file = module_path.rsplit(['\\', '/']).next().unwrap_or(module_path);
format!("{file}+{:#x}", address.wrapping_sub(base))
}
#[cfg(windows)]
pub use imp::{end_process, process_threads};
#[cfg(windows)]
mod imp {
use super::{module_offset, thread_line};
use std::ffi::c_void;
use std::io::Write;
use std::time::Duration;
use windows_sys::Wdk::System::Threading::{NtQueryInformationThread, ThreadQuerySetWin32StartAddress};
use windows_sys::Win32::Foundation::{CloseHandle, LocalFree, INVALID_HANDLE_VALUE};
use windows_sys::Win32::Storage::FileSystem::WriteFile;
use windows_sys::Win32::System::Console::{GetStdHandle, STD_OUTPUT_HANDLE};
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Thread32First, Thread32Next, TH32CS_SNAPTHREAD, THREADENTRY32,
};
use windows_sys::Win32::System::LibraryLoader::{
GetModuleFileNameW, GetModuleHandleExW, GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
};
use windows_sys::Win32::System::Threading::{
GetCurrentProcessId, GetCurrentThreadId, GetThreadDescription, OpenThread, THREAD_QUERY_INFORMATION,
};
unsafe extern "C" {
fn atexit(callback: extern "C" fn()) -> i32;
fn exit(code: i32) -> !;
fn fflush(stream: *mut c_void) -> i32;
}
const WATCHDOG_PERIOD: Duration = Duration::from_secs(15);
fn traced() -> bool {
std::env::var_os("FERROUI_WINUI_TRACE").is_some()
}
fn raw_line(text: &str) {
let line = format!("{text}\n");
unsafe {
let handle = GetStdHandle(STD_OUTPUT_HANDLE);
if handle.is_null() || handle == INVALID_HANDLE_VALUE {
return;
}
let mut written = 0u32;
WriteFile(handle, line.as_ptr(), line.len() as u32, &mut written, std::ptr::null_mut());
}
}
pub fn process_threads() -> Vec<String> {
let mut list = Vec::new();
unsafe {
let (process, current) = (GetCurrentProcessId(), GetCurrentThreadId());
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if snapshot == INVALID_HANDLE_VALUE {
return list;
}
let mut entry: THREADENTRY32 = std::mem::zeroed();
entry.dwSize = size_of::<THREADENTRY32>() as u32;
let mut more = Thread32First(snapshot, &mut entry);
while more != 0 {
if entry.th32OwnerProcessID == process {
let id = entry.th32ThreadID;
let mut name = String::new();
let mut start = None;
let thread = OpenThread(THREAD_QUERY_INFORMATION, 0, id);
if !thread.is_null() {
let mut description: *mut u16 = std::ptr::null_mut();
if GetThreadDescription(thread, &mut description) >= 0 && !description.is_null() {
let mut length = 0;
while *description.add(length) != 0 {
length += 1;
}
name = String::from_utf16_lossy(std::slice::from_raw_parts(description, length));
LocalFree(description.cast());
}
let mut address: usize = 0;
let status = NtQueryInformationThread(
thread,
ThreadQuerySetWin32StartAddress,
(&raw mut address).cast(),
size_of::<usize>() as u32,
std::ptr::null_mut(),
);
if status >= 0 {
let mut module = std::ptr::null_mut();
let found = GetModuleHandleExW(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
address as *const u16,
&mut module,
);
start = Some(if found != 0 {
let mut file = [0u16; 512];
let length = GetModuleFileNameW(module, file.as_mut_ptr(), file.len() as u32) as usize;
module_offset(&String::from_utf16_lossy(&file[..length]), address, module as usize)
} else {
format!("{address:#x}")
});
}
CloseHandle(thread);
}
list.push(thread_line(id, id == current, &name, start.as_deref()));
}
more = Thread32Next(snapshot, &mut entry);
}
CloseHandle(snapshot);
}
list
}
fn print_threads(heading: &str) {
let threads = process_threads();
let mut out = std::io::stdout().lock();
let _ = writeln!(out, "[exit] {heading}: {} thread(s)", threads.len());
for thread in &threads {
let _ = writeln!(out, "[exit] {thread}");
}
let _ = out.flush();
}
extern "C" fn at_exit() {
print_threads("exit of the C runtime: the handlers registered with atexit run");
}
struct DetachMarker;
impl Drop for DetachMarker {
fn drop(&mut self) {
raw_line("[exit] ExitProcess: the other threads are ended, the detach callbacks run");
}
}
ferroui_base::ferro_thread_local! {
static DETACH_MARKER: DetachMarker = const { DetachMarker };
}
pub fn end_process(code: i32) -> ! {
let trace = traced();
if trace {
print_threads(&format!("main hands the end of the process (code {code}) to a thread of its own"));
unsafe { atexit(at_exit) };
let _ = std::thread::Builder::new().name("process end watchdog".to_owned()).spawn(|| {
let mut waited = Duration::ZERO;
loop {
std::thread::sleep(WATCHDOG_PERIOD);
waited += WATCHDOG_PERIOD;
print_threads(&format!("the process still runs {} s after main handed its end over", waited.as_secs()));
}
});
}
let _ = std::io::stdout().flush();
let _ = std::io::stderr().flush();
unsafe { fflush(std::ptr::null_mut()) };
let ending = std::thread::Builder::new().name("process end".to_owned()).spawn(move || {
if trace {
DETACH_MARKER.with(|_| {});
}
unsafe { exit(code) }
});
if ending.is_err() {
std::process::exit(code);
}
loop {
std::thread::park();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_thread_line_names_the_thread_and_where_it_started() {
assert_eq!(thread_line(7716, false, "", None), "thread 7716");
assert_eq!(
thread_line(12, true, "process end", Some("app.exe+0x10")),
"thread 12 (this thread) \"process end\", started in app.exe+0x10"
);
}
#[test]
fn a_start_address_is_printed_as_module_and_offset() {
assert_eq!(
module_offset(r"C:\Windows\System32\ntdll.dll", 0x7ff8_0001_2340, 0x7ff8_0000_0000),
"ntdll.dll+0x12340"
);
assert_eq!(module_offset("libGLESv2.dll", 0x1010, 0x1000), "libGLESv2.dll+0x10");
}
}