use std::cell::Cell;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Instant;
use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::System::Console::{
CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT, CTRL_SHUTDOWN_EVENT, SetConsoleCtrlHandler,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::UI::Shell::{DefSubclassProc, RemoveWindowSubclass, SetWindowSubclass};
use windows::Win32::UI::WindowsAndMessaging::{
CREATESTRUCTW, CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW,
EnumThreadWindows, GWLP_USERDATA, GetClassNameW, GetWindowLongPtrW, IsWindow, MSG,
MsgWaitForMultipleObjects, PM_REMOVE, PeekMessageW, PostQuitMessage, QS_ALLINPUT,
RegisterClassW, SetWindowLongPtrW, TranslateMessage, WINDOW_EX_STYLE, WM_CLOSE, WM_ENDSESSION,
WM_NCCREATE, WM_NCDESTROY, WM_QUERYENDSESSION, WM_QUIT, WNDCLASSW, WS_EX_TOOLWINDOW,
WS_OVERLAPPED,
};
use windows::core::BOOL;
use windows::core::{PCWSTR, w};
use super::{ConsoleClosing, SESSION_END_BUDGET, SessionEndNow};
struct State {
post: Box<dyn Fn(SessionEndNow)>,
give_up: Box<dyn Fn()>,
ending: Cell<bool>,
}
pub struct SessionEndWatch {
hwnd: HWND,
state: *mut State,
target: Option<(HWND, *mut OnClose)>,
}
type OnClose = Box<dyn Fn()>;
const WINIT_TARGET_CLASS: &str = "Winit Thread Event Target";
const TARGET_SUBCLASS_ID: usize = 0xFE_113_000;
impl SessionEndWatch {
pub fn new(
post: impl Fn(SessionEndNow) + 'static,
give_up: impl Fn() + 'static,
on_close: impl Fn() + 'static,
) -> Option<Self> {
let class = w!("TeksiloSessionEndWatch");
let state = Box::new(State {
post: Box::new(post),
give_up: Box::new(give_up),
ending: Cell::new(false),
});
unsafe {
let instance = GetModuleHandleW(PCWSTR::null()).ok()?;
let wc = WNDCLASSW {
lpfnWndProc: Some(session_end_proc),
hInstance: instance.into(),
lpszClassName: class,
..Default::default()
};
RegisterClassW(&wc);
let state = Box::into_raw(state);
match CreateWindowExW(
WINDOW_EX_STYLE(WS_EX_TOOLWINDOW.0),
class,
w!("Teksilo session end"),
WS_OVERLAPPED,
0,
0,
0,
0,
None,
None,
Some(instance.into()),
Some(state as *const std::ffi::c_void),
) {
Ok(hwnd) => Some(Self {
hwnd,
state,
target: keep_winit_target(Box::new(on_close)),
}),
Err(_) => {
drop(Box::from_raw(state));
None
}
}
}
}
}
impl SessionEndWatch {
pub fn keeps_winit_target(&self) -> bool {
self.target.is_some()
}
}
impl Drop for SessionEndWatch {
fn drop(&mut self) {
unsafe {
let _ = DestroyWindow(self.hwnd);
drop(Box::from_raw(self.state));
if let Some((target, on_close)) = self.target.take() {
if RemoveWindowSubclass(target, Some(target_proc), TARGET_SUBCLASS_ID).as_bool()
|| !IsWindow(Some(target)).as_bool()
{
drop(Box::from_raw(on_close));
}
}
}
}
}
fn keep_winit_target(on_close: OnClose) -> Option<(HWND, *mut OnClose)> {
unsafe extern "system" fn find(hwnd: HWND, found: LPARAM) -> BOOL {
let mut class = [0u16; 64];
unsafe {
let len = GetClassNameW(hwnd, &mut class) as usize;
if String::from_utf16_lossy(&class[..len]) == WINIT_TARGET_CLASS {
*(found.0 as *mut Option<HWND>) = Some(hwnd);
return BOOL(0);
}
}
BOOL(1)
}
let mut found: Option<HWND> = None;
unsafe {
let _ = EnumThreadWindows(
GetCurrentThreadId(),
Some(find),
LPARAM(&mut found as *mut Option<HWND> as isize),
);
let target = found?;
let on_close = Box::into_raw(Box::new(on_close));
if SetWindowSubclass(
target,
Some(target_proc),
TARGET_SUBCLASS_ID,
on_close as usize,
)
.as_bool()
{
Some((target, on_close))
} else {
drop(Box::from_raw(on_close));
None
}
}
}
unsafe extern "system" fn target_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
_id: usize,
on_close: usize,
) -> LRESULT {
unsafe {
if msg == WM_CLOSE {
(*(on_close as *const OnClose))();
return LRESULT(0);
}
DefSubclassProc(hwnd, msg, wparam, lparam)
}
}
impl std::fmt::Debug for SessionEndWatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SessionEndWatch").finish()
}
}
unsafe extern "system" fn session_end_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
unsafe {
match msg {
WM_NCCREATE => {
let create = &*(lparam.0 as *const CREATESTRUCTW);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, create.lpCreateParams as isize);
DefWindowProcW(hwnd, msg, wparam, lparam)
}
WM_NCDESTROY => {
SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0);
DefWindowProcW(hwnd, msg, wparam, lparam)
}
WM_QUERYENDSESSION => {
let state = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *const State;
if let Some(state) = state.as_ref()
&& !state.ending.replace(true)
{
if !end_session(state) {
state.ending.set(false);
}
}
LRESULT(1)
}
WM_ENDSESSION => {
let state = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *const State;
if wparam.0 != 0
&& let Some(state) = state.as_ref()
&& !state.ending.replace(true)
{
if !end_session(state) {
state.ending.set(false);
}
}
LRESULT(0)
}
WM_CLOSE => LRESULT(0),
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
}
fn end_session(state: &State) -> bool {
let done = Arc::new(AtomicBool::new(false));
let request = SessionEndNow::new(done.clone());
(state.post)(request.clone());
let deadline = Instant::now() + SESSION_END_BUDGET;
let mut msg = MSG::default();
while !done.load(Ordering::Acquire) {
let left = deadline.saturating_duration_since(Instant::now());
if left.is_zero() {
request.expire();
(state.give_up)();
return false;
}
unsafe {
MsgWaitForMultipleObjects(None, false, left.as_millis() as u32, QS_ALLINPUT);
while !done.load(Ordering::Acquire)
&& PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool()
{
if msg.message == WM_QUIT {
PostQuitMessage(msg.wParam.0 as i32);
if !done.load(Ordering::Acquire) {
request.expire();
(state.give_up)();
return false;
}
return true;
}
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
}
true
}
struct ConsoleCalls {
post: Box<dyn Fn(ConsoleClosing) + Send + Sync>,
give_up: Box<dyn Fn() + Send + Sync>,
}
static CONSOLE: std::sync::Mutex<Option<ConsoleCalls>> = std::sync::Mutex::new(None);
static REGISTERED: AtomicBool = AtomicBool::new(false);
pub struct ConsoleWatch {
_private: (),
}
impl ConsoleWatch {
pub fn new(
post: impl Fn(ConsoleClosing) + Send + Sync + 'static,
give_up: impl Fn() + Send + Sync + 'static,
) -> Option<Self> {
if !REGISTERED.load(Ordering::Acquire) {
unsafe { SetConsoleCtrlHandler(Some(console_handler), true) }.ok()?;
REGISTERED.store(true, Ordering::Release);
}
*CONSOLE.lock().unwrap_or_else(|e| e.into_inner()) = Some(ConsoleCalls {
post: Box::new(post),
give_up: Box::new(give_up),
});
Some(Self { _private: () })
}
}
impl Drop for ConsoleWatch {
fn drop(&mut self) {
*CONSOLE.lock().unwrap_or_else(|e| e.into_inner()) = None;
}
}
impl std::fmt::Debug for ConsoleWatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConsoleWatch").finish()
}
}
unsafe extern "system" fn console_handler(event: u32) -> BOOL {
let session_end = match event {
CTRL_CLOSE_EVENT => false,
CTRL_LOGOFF_EVENT | CTRL_SHUTDOWN_EVENT => true,
_ => return BOOL(0),
};
let done = Arc::new(AtomicBool::new(false));
{
let calls = CONSOLE.lock().unwrap_or_else(|e| e.into_inner());
let Some(calls) = calls.as_ref() else {
return BOOL(0);
};
(calls.post)(ConsoleClosing::new(session_end, done.clone()));
}
let deadline = Instant::now() + SESSION_END_BUDGET;
while !done.load(Ordering::Acquire) && Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(10));
}
if !done.load(Ordering::Acquire) {
if let Some(calls) = CONSOLE.lock().unwrap_or_else(|e| e.into_inner()).as_ref() {
(calls.give_up)();
}
return BOOL(1);
}
let grace = (Instant::now() + std::time::Duration::from_secs(1)).min(deadline);
while Instant::now() < grace {
std::thread::sleep(std::time::Duration::from_millis(10));
}
BOOL(1)
}