1use std::sync::mpsc::{Receiver, channel};
17
18use crate::{Result, Waker};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum EndKind {
23 Quit,
25 Logout,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct EndRequest {
32 pub kind: EndKind,
33}
34
35pub const CAN_WAIT: bool = cfg!(target_os = "macos");
38
39pub fn listen(waker: Waker) -> Result<Receiver<EndRequest>> {
43 let (tx, rx) = channel();
44 imp::listen(tx, waker)?;
45 Ok(rx)
46}
47
48pub fn reply(allow: bool) {
52 imp::reply(allow);
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
59pub enum State {
60 #[default]
62 Idle,
63 Asking(EndKind),
65 Saving(EndKind),
67 Done,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum Event {
74 Requested {
77 kind: EndKind,
78 dirty: bool,
79 can_wait: bool,
80 },
81 Save,
83 Discard,
84 Cancel,
85 Saved,
87 SaveFailed,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum Effect {
93 Ask,
95 Save,
97 Shutdown { keep_recovery: bool },
100 Allow,
102 Refuse,
104}
105
106pub fn step(state: State, event: Event) -> (State, Vec<Effect>) {
108 use Effect::*;
109 match (state, event) {
110 (State::Done, Event::Requested { .. }) => (State::Done, vec![Allow]),
112 (State::Idle, Event::Requested { dirty: false, .. }) => (State::Done, vec![Shutdown { keep_recovery: false }, Allow]),
113 (State::Idle, Event::Requested { kind, dirty: true, can_wait: true }) => (State::Asking(kind), vec![Ask]),
114 (State::Idle, Event::Requested { dirty: true, can_wait: false, .. }) => (State::Done, vec![Shutdown { keep_recovery: true }, Allow]),
115 (State::Asking(kind), Event::Save) => (State::Saving(kind), vec![Save]),
116 (State::Asking(_), Event::Discard) | (State::Saving(_), Event::Saved) => (State::Done, vec![Shutdown { keep_recovery: false }, Allow]),
117 (State::Asking(_) | State::Saving(_), Event::Cancel) | (State::Saving(_), Event::SaveFailed) => (State::Idle, vec![Refuse]),
118 (s, _) => (s, vec![]),
120 }
121}
122
123#[cfg(all(target_os = "macos", feature = "session-end"))]
126#[allow(unsafe_code)]
127mod imp {
128 use std::ffi::CStr;
129 use std::sync::atomic::{AtomicBool, Ordering};
130 use std::sync::mpsc::Sender;
131 use std::sync::{Mutex, PoisonError};
132
133 use block2::RcBlock;
134 use objc2::runtime::{AnyClass, AnyObject, Imp, ProtocolObject, Sel};
135 use objc2::{MainThreadMarker, sel};
136 use objc2_app_kit::NSApplication;
137 use objc2_foundation::{NSAppleEventManager, NSRunLoop};
138
139 use super::*;
140 use crate::NativeError;
141
142 const TERMINATE_NOW: usize = 1;
144 const TERMINATE_LATER: usize = 2;
145
146 const fn four(c: &[u8; 4]) -> u32 {
147 u32::from_be_bytes(*c)
148 }
149 const QUIT_REASON: u32 = four(b"why?");
151
152 static SINK: Mutex<Option<(Sender<EndRequest>, Waker)>> = Mutex::new(None);
153 static PENDING: AtomicBool = AtomicBool::new(false);
155 static ALLOWED: AtomicBool = AtomicBool::new(false);
157
158 fn kind() -> EndKind {
160 let event = NSAppleEventManager::sharedAppleEventManager().currentAppleEvent();
161 let reason = event.and_then(|e| e.attributeDescriptorForKeyword(QUIT_REASON)).map(|d| d.enumCodeValue()).unwrap_or(0);
162 if reason == 0 { EndKind::Quit } else { EndKind::Logout }
163 }
164
165 fn answer() -> usize {
166 if ALLOWED.load(Ordering::SeqCst) {
167 return TERMINATE_NOW;
168 }
169 let sink = SINK.lock().unwrap_or_else(PoisonError::into_inner).clone();
170 let Some((tx, wake)) = sink else { return TERMINATE_NOW };
171 if PENDING.load(Ordering::SeqCst) {
172 return TERMINATE_LATER;
173 }
174 if tx.send(EndRequest { kind: kind() }).is_err() {
176 return TERMINATE_NOW;
177 }
178 PENDING.store(true, Ordering::SeqCst);
179 wake();
180 TERMINATE_LATER
181 }
182
183 extern "C-unwind" fn should_terminate(_this: *mut AnyObject, _cmd: Sel, _sender: *mut AnyObject) -> usize {
186 std::panic::catch_unwind(answer).unwrap_or(TERMINATE_NOW)
187 }
188
189 pub fn listen(tx: Sender<EndRequest>, waker: Waker) -> Result<()> {
190 let Some(mtm) = MainThreadMarker::new() else {
191 return Err(NativeError::Os("quit requests must be set up on the main thread".into()));
192 };
193 let app = NSApplication::sharedApplication(mtm);
194 let Some(delegate) = app.delegate() else {
195 return Err(NativeError::Os("the application has no delegate yet; set up quit requests once the event loop runs".into()));
196 };
197 let obj: &AnyObject = ProtocolObject::as_ref(&*delegate);
198 let cls: &AnyClass = obj.class();
199 let name = sel!(applicationShouldTerminate:);
200 let ours = should_terminate as extern "C-unwind" fn(*mut AnyObject, Sel, *mut AnyObject) -> usize;
201 if let Some(m) = cls.instance_method(name) {
202 let current = m.implementation();
203 if current as usize != ours as usize {
204 return Err(NativeError::Os("the application delegate already answers quit requests".into()));
205 }
206 } else {
207 const TYPES: &CStr = c"Q@:@"; let imp: Imp = unsafe { std::mem::transmute::<extern "C-unwind" fn(*mut AnyObject, Sel, *mut AnyObject) -> usize, Imp>(ours) };
212 let added = unsafe { objc2::ffi::class_addMethod(cls as *const AnyClass as *mut AnyClass, name, imp, TYPES.as_ptr()) };
216 if !added.as_bool() {
217 return Err(NativeError::Os("could not add the quit handler to the application delegate".into()));
218 }
219 app.setDelegate(Some(&delegate));
222 }
223 *SINK.lock().unwrap_or_else(PoisonError::into_inner) = Some((tx, waker));
224 Ok(())
225 }
226
227 pub fn reply(allow: bool) {
228 if allow {
229 ALLOWED.store(true, Ordering::SeqCst);
230 }
231 if !PENDING.swap(false, Ordering::SeqCst) {
232 return;
233 }
234 let block = RcBlock::new(move || {
239 if let Some(mtm) = MainThreadMarker::new() {
240 NSApplication::sharedApplication(mtm).replyToApplicationShouldTerminate(allow);
241 }
242 });
243 unsafe { NSRunLoop::mainRunLoop().performBlock(&block) };
246 }
247}
248
249#[cfg(all(target_os = "windows", feature = "session-end"))]
252#[allow(unsafe_code)]
253mod imp {
254 use std::sync::mpsc::Sender;
255 use std::sync::{Condvar, Mutex, Once, PoisonError};
256 use std::time::Duration;
257
258 use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
259 use windows::Win32::System::LibraryLoader::GetModuleHandleW;
260 use windows::Win32::UI::WindowsAndMessaging::{
261 CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW, MSG, RegisterClassW, TranslateMessage, WINDOW_EX_STYLE, WM_ENDSESSION,
262 WM_QUERYENDSESSION, WNDCLASSW, WS_OVERLAPPED,
263 };
264 use windows::core::w;
265
266 use super::*;
267 use crate::NativeError;
268
269 const FLUSH_WAIT: Duration = Duration::from_secs(4);
272
273 static SINK: Mutex<Option<(Sender<EndRequest>, Waker)>> = Mutex::new(None);
274 static REPLY: (Mutex<(bool, bool)>, Condvar) = (Mutex::new((false, false)), Condvar::new());
276
277 fn request() {
278 let sink = SINK.lock().unwrap_or_else(PoisonError::into_inner).clone();
279 let Some((tx, wake)) = sink else { return };
280 let mut r = REPLY.0.lock().unwrap_or_else(PoisonError::into_inner);
281 if r.1 || r.0 {
282 return;
283 }
284 if tx.send(EndRequest { kind: EndKind::Logout }).is_ok() {
285 r.0 = true;
286 drop(r);
287 wake();
288 }
289 }
290
291 fn wait_for_reply() {
292 let r = REPLY.0.lock().unwrap_or_else(PoisonError::into_inner);
293 if !r.0 {
294 return;
295 }
296 let _ = REPLY.1.wait_timeout_while(r, FLUSH_WAIT, |r| !r.1);
297 }
298
299 extern "system" fn wndproc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
300 let handled = std::panic::catch_unwind(|| match msg {
301 WM_QUERYENDSESSION => {
302 request();
303 Some(LRESULT(1))
305 }
306 WM_ENDSESSION => {
307 if wparam.0 != 0 {
308 wait_for_reply();
309 }
310 Some(LRESULT(0))
311 }
312 _ => None,
313 });
314 match handled {
315 Ok(Some(r)) => r,
316 _ => unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) },
318 }
319 }
320
321 fn run() {
322 let Ok(module) = (unsafe { GetModuleHandleW(None) }) else { return };
324 let class = WNDCLASSW { lpfnWndProc: Some(wndproc), hInstance: module.into(), lpszClassName: w!("AshlarSessionEnd"), ..Default::default() };
325 if unsafe { RegisterClassW(&class) } == 0 {
327 log::warn!("session end: the window class could not be registered");
328 return;
329 }
330 let hwnd = unsafe {
334 CreateWindowExW(WINDOW_EX_STYLE(0), w!("AshlarSessionEnd"), w!(""), WS_OVERLAPPED, 0, 0, 0, 0, None, None, Some(module.into()), None)
335 };
336 if hwnd.is_err() {
337 log::warn!("session end: the window could not be created");
338 return;
339 }
340 let mut msg = MSG::default();
341 while unsafe { GetMessageW(&mut msg, None, 0, 0) }.0 > 0 {
343 unsafe {
345 let _ = TranslateMessage(&msg);
346 DispatchMessageW(&msg);
347 }
348 }
349 }
350
351 pub fn listen(tx: Sender<EndRequest>, waker: Waker) -> Result<()> {
352 *SINK.lock().unwrap_or_else(PoisonError::into_inner) = Some((tx, waker));
353 static START: Once = Once::new();
354 let mut spawned = Ok(());
355 START.call_once(|| {
356 spawned = std::thread::Builder::new().name("ashlar-session-end".into()).spawn(run).map(drop);
357 });
358 spawned.map_err(|e| NativeError::Os(crate::os_text(e)))
359 }
360
361 pub fn reply(_allow: bool) {
362 let mut r = REPLY.0.lock().unwrap_or_else(PoisonError::into_inner);
363 r.1 = true;
364 REPLY.1.notify_all();
365 }
366}
367
368#[cfg(not(all(any(target_os = "macos", target_os = "windows"), feature = "session-end")))]
369mod imp {
370 use std::sync::mpsc::Sender;
371
372 use super::*;
373
374 pub fn listen(_tx: Sender<EndRequest>, _waker: Waker) -> Result<()> {
375 Err(crate::NativeError::Unsupported)
376 }
377 pub fn reply(_allow: bool) {}
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use Effect::*;
384
385 fn req(kind: EndKind, dirty: bool, can_wait: bool) -> Event {
386 Event::Requested { kind, dirty, can_wait }
387 }
388
389 fn run(events: &[Event]) -> (State, Vec<Effect>) {
391 events.iter().fold((State::Idle, vec![]), |(s, mut all), e| {
392 let (s, fx) = step(s, *e);
393 all.extend(fx);
394 (s, all)
395 })
396 }
397
398 #[test]
399 fn a_clean_quit_cleans_up_and_ends_at_once() {
400 for kind in [EndKind::Quit, EndKind::Logout] {
401 for can_wait in [true, false] {
402 assert_eq!(run(&[req(kind, false, can_wait)]), (State::Done, vec![Shutdown { keep_recovery: false }, Allow]));
403 }
404 }
405 }
406
407 #[test]
408 fn a_dirty_quit_asks_first() {
409 assert_eq!(run(&[req(EndKind::Quit, true, true)]), (State::Asking(EndKind::Quit), vec![Ask]));
410 assert_eq!(run(&[req(EndKind::Logout, true, true)]), (State::Asking(EndKind::Logout), vec![Ask]));
411 }
412
413 #[test]
414 fn save_then_saved_ends() {
415 let (s, fx) = run(&[req(EndKind::Quit, true, true), Event::Save, Event::Saved]);
416 assert_eq!(s, State::Done);
417 assert_eq!(fx, vec![Ask, Save, Shutdown { keep_recovery: false }, Allow]);
418 }
419
420 #[test]
421 fn a_failed_or_cancelled_save_stays_open() {
422 assert_eq!(run(&[req(EndKind::Quit, true, true), Event::Save, Event::SaveFailed]), (State::Idle, vec![Ask, Save, Refuse]));
423 assert_eq!(run(&[req(EndKind::Quit, true, true), Event::Save, Event::Cancel]), (State::Idle, vec![Ask, Save, Refuse]));
424 }
425
426 #[test]
427 fn discard_ends_without_keeping_recovery_data() {
428 assert_eq!(run(&[req(EndKind::Quit, true, true), Event::Discard]), (State::Done, vec![Ask, Shutdown { keep_recovery: false }, Allow]));
429 }
430
431 #[test]
432 fn cancel_stays_open_and_a_later_quit_asks_again() {
433 let (s, fx) = run(&[req(EndKind::Logout, true, true), Event::Cancel]);
434 assert_eq!((s, fx), (State::Idle, vec![Ask, Refuse]));
435 assert_eq!(step(s, req(EndKind::Quit, true, true)), (State::Asking(EndKind::Quit), vec![Ask]));
436 }
437
438 #[test]
439 fn a_logout_that_cant_wait_keeps_the_unsaved_work_for_recovery() {
440 assert_eq!(run(&[req(EndKind::Logout, true, false)]), (State::Done, vec![Shutdown { keep_recovery: true }, Allow]));
441 }
442
443 #[test]
444 fn repeated_requests_dont_stack_dialogs_and_stray_answers_do_nothing() {
445 let (s, fx) = run(&[req(EndKind::Quit, true, true), req(EndKind::Quit, true, true), req(EndKind::Logout, true, true)]);
446 assert_eq!((s, fx), (State::Asking(EndKind::Quit), vec![Ask]));
447 for e in [Event::Save, Event::Discard, Event::Cancel, Event::Saved, Event::SaveFailed] {
448 assert_eq!(step(State::Idle, e), (State::Idle, vec![]), "{e:?}");
449 }
450 assert_eq!(step(State::Saving(EndKind::Quit), req(EndKind::Quit, true, true)), (State::Saving(EndKind::Quit), vec![]));
451 }
452
453 #[test]
454 fn once_done_every_later_ask_is_allowed() {
455 assert_eq!(step(State::Done, req(EndKind::Quit, true, true)), (State::Done, vec![Allow]));
456 assert_eq!(step(State::Done, Event::Cancel), (State::Done, vec![]));
457 }
458
459 #[test]
460 fn listening_off_the_main_thread_or_unsupported_is_an_error_not_a_crash() {
461 let r = std::thread::spawn(|| listen(crate::no_waker()).map(drop)).join();
462 if cfg!(all(target_os = "windows", feature = "session-end")) {
463 assert!(matches!(r, Ok(Ok(()))));
465 } else {
466 assert!(matches!(r, Ok(Err(_))));
467 }
468 reply(false); }
470}