#[cfg(all(target_os = "linux", feature = "gtk-native"))]
fn gui_probe_allowed() -> bool {
if std::env::var("RW_GUI_PROBE_OK").ok().as_deref() == Some("1") {
return true;
}
println!(
"this probe opens real windows on the desktop it runs on; set RW_GUI_PROBE_OK=1 to stage \
it (and prefer a machine whose screen nobody is using)"
);
false
}
#[cfg(all(
not(alloc_frugal),
any(target_os = "linux", target_os = "macos", target_os = "windows")
))]
fn main() {
staged()
}
#[cfg(not(all(
not(alloc_frugal),
any(target_os = "linux", target_os = "macos", target_os = "windows")
)))]
fn main() {
println!(
"probe_cross_thread_quit: this profile has no platform event loop (alloc-frugal or a \
non-desktop target), so a cross-thread stop cannot be staged here."
);
}
#[cfg(all(
not(alloc_frugal),
any(target_os = "linux", target_os = "macos", target_os = "windows")
))]
fn arm_watchdog(
what: &'static str,
budget: std::time::Duration,
) -> std::sync::Arc<std::sync::atomic::AtomicBool> {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
let returned = Arc::new(AtomicBool::new(false));
let watched = returned.clone();
std::thread::spawn(move || {
let deadline = std::time::Instant::now() + budget;
while std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(50));
if watched.load(Ordering::SeqCst) {
return;
}
}
if !watched.load(Ordering::SeqCst) {
println!(
"\n FAIL {what}: `run()` did not return within {budget:?}; a stop request made \
from another thread was not honoured, so this session cannot end itself (the \
symptom this probe exists for). Exiting 3 to make that unambiguous."
);
std::process::exit(3);
}
});
returned
}
#[cfg(all(
not(alloc_frugal),
any(target_os = "linux", target_os = "macos", target_os = "windows")
))]
fn staged() {
#[cfg(all(target_os = "linux", feature = "gtk-native"))]
if !gui_probe_allowed() {
std::process::exit(2);
}
use std::time::{Duration, Instant};
const WATCHER_DELAY: Duration = Duration::from_millis(400);
const RETURN_BUDGET: Duration = Duration::from_secs(20);
let mut failures = 0usize;
let mut check = |held: bool, what: &str| {
if held {
println!(" OK {what}");
} else {
println!(" FAIL {what}");
failures += 1;
}
};
println!("=== cross-thread quit probe ===");
println!("backend : {}", rust_widgets::platform::backend_name());
println!("runtime gui mode : {:?}", rust_widgets::platform::runtime_gui_mode());
println!("dpi scale factor : {}", rust_widgets::platform::dpi_scale_factor());
println!("loop has ticked : {}", rust_widgets::platform::stop::loop_has_ticked());
println!("\nA. before any loop has run (fresh process)");
let answered = rust_widgets::quit();
println!(
" quit() answered {answered}, requested={}",
rust_widgets::platform::stop::is_requested()
);
check(
rust_widgets::platform::stop::is_requested(),
"a request made with no loop running is still recorded, so a loop started later honours it",
);
let _ = rust_widgets::platform::stop::take();
println!("\nB. loop running, request from another thread");
rust_widgets::init();
let window = rust_widgets::create_window("cross-thread quit probe", 40, 40, 480, 320);
println!(" window id = {window}");
check(window != 0, "a real window was created (the id a session is ended with)");
rust_widgets::show_widget(window);
let (answered_tx, answered_rx) = std::sync::mpsc::channel();
let watcher = std::thread::spawn(move || {
std::thread::sleep(WATCHER_DELAY);
let asked = rust_widgets::quit();
let _ = answered_tx.send(asked);
});
let watchdog = arm_watchdog("B. loop ended by a watcher thread", RETURN_BUDGET);
let run_started = Instant::now();
rust_widgets::run();
watchdog.store(true, std::sync::atomic::Ordering::SeqCst);
let run_took = run_started.elapsed();
let watcher_answered = answered_rx
.recv_timeout(RETURN_BUDGET)
.expect("the watcher thread must answer within the budget");
watcher.join().expect("the watcher thread must not panic");
println!(" run() returned after {run_took:?}");
println!(" watcher: quit() -> {watcher_answered}");
check(
run_took < RETURN_BUDGET,
"run() really returned (the loop ended instead of being killed)",
);
check(
watcher_answered,
"the watcher thread was told the request will be honoured (answering `false` would tell a \
CI job to kill its own process)",
);
check(
rust_widgets::platform::stop::loop_has_ticked(),
"the loop drove frames, so the request had a reader on the loop's own thread",
);
println!("\nC. the request was consumed by the loop it stopped");
check(
!rust_widgets::platform::stop::is_requested(),
"no request is left over, so a second session in this process is not stopped by the first",
);
#[cfg(all(target_os = "linux", feature = "gtk-native"))]
{
println!("\nD. composition of the window the session had open");
gtk_composition_report();
}
#[cfg(not(all(target_os = "linux", feature = "gtk-native")))]
{
println!(
"\nD. skipped: this build has no toolkit to ask about composition \
(`--features desktop,gtk-native` on a host with a display session stages it)"
);
}
println!("\nE. a session that ends itself: schedule_on_main(300ms, quit)");
{
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
static RAN_ON: AtomicU64 = AtomicU64::new(0);
const SCHEDULE_AFTER: Duration = Duration::from_millis(300);
let ran = Arc::new(AtomicBool::new(false));
let flag = ran.clone();
let asking_thread = format!("{:?}", std::thread::current().id());
let scheduled = rust_widgets::schedule_on_main(SCHEDULE_AFTER, move || {
RAN_ON.store(
format!("{:?}", std::thread::current().id())
.trim_start_matches("ThreadId(")
.trim_end_matches(')')
.parse()
.unwrap_or(0),
Ordering::SeqCst,
);
flag.store(true, Ordering::SeqCst);
let _ = rust_widgets::quit();
});
check(scheduled, "the running loop accepted the work (it reads the queue every frame)");
let watchdog = arm_watchdog("E. session ends itself at its deadline", RETURN_BUDGET);
let started = Instant::now();
rust_widgets::run();
watchdog.store(true, std::sync::atomic::Ordering::SeqCst);
let took = started.elapsed();
println!(" run() returned after {took:?}; callback ran={}", ran.load(Ordering::SeqCst));
check(
ran.load(Ordering::SeqCst),
"the callback ran (a queued item that never runs would leave the session unstoppable)",
);
check(took >= SCHEDULE_AFTER, "the callback did not run before its deadline");
check(
RAN_ON.load(Ordering::SeqCst)
== asking_thread
.trim_start_matches("ThreadId(")
.trim_end_matches(')')
.parse()
.unwrap_or(1),
"the callback ran on the loop's own thread, not on the thread that scheduled it",
);
}
let _ = rust_widgets::close_widget(window);
println!(
"\n{}",
if failures == 0 {
"probe_cross_thread_quit: OK (every check held)".to_string()
} else {
format!("probe_cross_thread_quit: {failures} check(s) failed")
}
);
std::process::exit(if failures == 0 { 0 } else { 1 });
}
#[cfg(all(target_os = "linux", feature = "gtk-native"))]
fn gtk_composition_report() {
use gtk::prelude::*;
let toplevels = gtk::Window::list_toplevels();
println!(" GTK toplevels still open: {}", toplevels.len());
for (index, widget) in toplevels.iter().enumerate() {
let Some(window) = widget.clone().downcast::<gtk::Window>().ok() else {
continue;
};
let (width, height) = window.size();
println!(
" [{index}] mapped={} visible={} realized={} title={:?} size={}x{} scale={}",
window.is_mapped(),
window.is_visible(),
window.is_realized(),
window.title().unwrap_or_default(),
width,
height,
window.scale_factor(),
);
}
println!(" read: `mapped=true` means GDK handed the window to the X server to composite.");
}