Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
Conrad Irwin
d2da9108cd Tidy 2025-08-11 22:47:56 -06:00
Conrad Irwin
155e29f5d9 Better on_app_quit
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-08-11 22:27:49 -06:00
9 changed files with 146 additions and 103 deletions

View file

@ -7,6 +7,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
rc::{Rc, Weak}, rc::{Rc, Weak},
sync::{Arc, atomic::Ordering::SeqCst}, sync::{Arc, atomic::Ordering::SeqCst},
task::{Poll, Waker},
time::Duration, time::Duration,
}; };
@ -91,6 +92,36 @@ impl AppCell {
} }
Ok(AppRefMut(self.app.try_borrow_mut()?)) Ok(AppRefMut(self.app.try_borrow_mut()?))
} }
pub fn shutdown(self: &Rc<AppCell>) {
let mut futures = Vec::new();
let mut cx = self.borrow_mut();
for observer in cx.quit_observers.remove(&()) {
futures.push(observer(&mut cx));
}
cx.windows.clear();
cx.window_handles.clear();
cx.flush_effects();
let executor = cx.background_executor.clone();
drop(cx);
let waker = Waker::noop();
let mut future_cx = std::task::Context::from_waker(waker);
let futures = futures::future::join_all(futures);
futures::pin_mut!(futures);
let mut start = std::time::Instant::now();
while start.elapsed() < SHUTDOWN_TIMEOUT {
match futures.as_mut().poll(&mut future_cx) {
Poll::Pending => {
executor.tick();
}
Poll::Ready(_) => break,
}
}
}
} }
#[doc(hidden)] #[doc(hidden)]
@ -382,39 +413,13 @@ impl App {
platform.on_quit(Box::new({ platform.on_quit(Box::new({
let cx = app.clone(); let cx = app.clone();
move || { move || {
cx.borrow_mut().shutdown(); cx.shutdown();
} }
})); }));
app app
} }
/// Quit the application gracefully. Handlers registered with [`Context::on_app_quit`]
/// will be given 100ms to complete before exiting.
pub fn shutdown(&mut self) {
let mut futures = Vec::new();
for observer in self.quit_observers.remove(&()) {
futures.push(observer(self));
}
self.windows.clear();
self.window_handles.clear();
self.flush_effects();
self.quitting = true;
let futures = futures::future::join_all(futures);
if self
.background_executor
.block_with_timeout(SHUTDOWN_TIMEOUT, futures)
.is_err()
{
log::error!("timed out waiting on app_will_quit");
}
self.quitting = false;
}
/// Get the id of the current keyboard layout /// Get the id of the current keyboard layout
pub fn keyboard_layout(&self) -> &dyn PlatformKeyboardLayout { pub fn keyboard_layout(&self) -> &dyn PlatformKeyboardLayout {
self.keyboard_layout.as_ref() self.keyboard_layout.as_ref()

View file

@ -167,7 +167,7 @@ impl TestAppContext {
/// public so the macro can call it. /// public so the macro can call it.
pub fn quit(&self) { pub fn quit(&self) {
self.on_quit.borrow_mut().drain(..).for_each(|f| f()); self.on_quit.borrow_mut().drain(..).for_each(|f| f());
self.app.borrow_mut().shutdown(); self.app.shutdown();
} }
/// Register cleanup to run when the test ends. /// Register cleanup to run when the test ends.

View file

@ -384,10 +384,9 @@ impl BackgroundExecutor {
self.dispatcher.as_test().unwrap().advance_clock(duration) self.dispatcher.as_test().unwrap().advance_clock(duration)
} }
/// in tests, run one task. /// docs
#[cfg(any(test, feature = "test-support"))]
pub fn tick(&self) -> bool { pub fn tick(&self) -> bool {
self.dispatcher.as_test().unwrap().tick(false) self.dispatcher.tick(true)
} }
/// in tests, run all tasks that are ready to run. If after doing so /// in tests, run all tasks that are ready to run. If after doing so

View file

@ -545,6 +545,7 @@ pub trait PlatformDispatcher: Send + Sync {
fn now(&self) -> Instant { fn now(&self) -> Instant {
Instant::now() Instant::now()
} }
fn tick(&self, _: bool) -> bool;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
fn as_test(&self) -> Option<&TestDispatcher> { fn as_test(&self) -> Option<&TestDispatcher> {

View file

@ -4,6 +4,14 @@
use crate::{PlatformDispatcher, TaskLabel}; use crate::{PlatformDispatcher, TaskLabel};
use async_task::Runnable; use async_task::Runnable;
use block::{Block, ConcreteBlock, RcBlock};
use core_foundation::{
base::CFTypeRef,
runloop::{
CFRunLoopRef, CFRunLoopRunInMode, CFRunLoopWakeUp, kCFRunLoopCommonModes,
kCFRunLoopDefaultMode,
},
};
use objc::{ use objc::{
class, msg_send, class, msg_send,
runtime::{BOOL, YES}, runtime::{BOOL, YES},
@ -11,7 +19,9 @@ use objc::{
}; };
use parking::{Parker, Unparker}; use parking::{Parker, Unparker};
use parking_lot::Mutex; use parking_lot::Mutex;
use smol::io::BlockOn;
use std::{ use std::{
cell::Cell,
ffi::c_void, ffi::c_void,
ptr::{NonNull, addr_of}, ptr::{NonNull, addr_of},
sync::Arc, sync::Arc,
@ -64,11 +74,21 @@ impl PlatformDispatcher for MacDispatcher {
} }
fn dispatch_on_main_thread(&self, runnable: Runnable) { fn dispatch_on_main_thread(&self, runnable: Runnable) {
use core_foundation::runloop::CFRunLoopGetMain;
unsafe { unsafe {
dispatch_async_f( let mut runnable = Cell::new(Some(runnable));
dispatch_get_main_queue(), let main_run_loop = CFRunLoopGetMain();
runnable.into_raw().as_ptr() as *mut c_void, let block = ConcreteBlock::new(move || {
Some(trampoline), if let Some(runnable) = runnable.take() {
runnable.run();
}
})
.copy();
CFRunLoopPerformBlock(
main_run_loop,
kCFRunLoopDefaultMode as _,
&*block as *const Block<_, _> as _,
); );
} }
} }
@ -87,6 +107,13 @@ impl PlatformDispatcher for MacDispatcher {
} }
} }
fn tick(&self, background_only: bool) -> bool {
unsafe {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0., 0);
}
true
}
fn park(&self, timeout: Option<Duration>) -> bool { fn park(&self, timeout: Option<Duration>) -> bool {
if let Some(timeout) = timeout { if let Some(timeout) = timeout {
self.parker.lock().park_timeout(timeout) self.parker.lock().park_timeout(timeout)
@ -105,3 +132,7 @@ extern "C" fn trampoline(runnable: *mut c_void) {
let task = unsafe { Runnable::<()>::from_raw(NonNull::new_unchecked(runnable as *mut ())) }; let task = unsafe { Runnable::<()>::from_raw(NonNull::new_unchecked(runnable as *mut ())) };
task.run(); task.run();
} }
unsafe extern "C" {
fn CFRunLoopPerformBlock(rl: CFRunLoopRef, mode: CFTypeRef, block: *const c_void);
}

View file

@ -122,68 +122,6 @@ impl TestDispatcher {
} }
} }
pub fn tick(&self, background_only: bool) -> bool {
let mut state = self.state.lock();
while let Some((deadline, _)) = state.delayed.first() {
if *deadline > state.time {
break;
}
let (_, runnable) = state.delayed.remove(0);
state.background.push(runnable);
}
let foreground_len: usize = if background_only {
0
} else {
state
.foreground
.values()
.map(|runnables| runnables.len())
.sum()
};
let background_len = state.background.len();
let runnable;
let main_thread;
if foreground_len == 0 && background_len == 0 {
let deprioritized_background_len = state.deprioritized_background.len();
if deprioritized_background_len == 0 {
return false;
}
let ix = state.random.gen_range(0..deprioritized_background_len);
main_thread = false;
runnable = state.deprioritized_background.swap_remove(ix);
} else {
main_thread = state.random.gen_ratio(
foreground_len as u32,
(foreground_len + background_len) as u32,
);
if main_thread {
let state = &mut *state;
runnable = state
.foreground
.values_mut()
.filter(|runnables| !runnables.is_empty())
.choose(&mut state.random)
.unwrap()
.pop_front()
.unwrap();
} else {
let ix = state.random.gen_range(0..background_len);
runnable = state.background.swap_remove(ix);
};
};
let was_main_thread = state.is_main_thread;
state.is_main_thread = main_thread;
drop(state);
runnable.run();
self.state.lock().is_main_thread = was_main_thread;
true
}
pub fn deprioritize(&self, task_label: TaskLabel) { pub fn deprioritize(&self, task_label: TaskLabel) {
self.state self.state
.lock() .lock()
@ -267,6 +205,68 @@ impl PlatformDispatcher for TestDispatcher {
state.start_time + state.time state.start_time + state.time
} }
fn tick(&self, background_only: bool) -> bool {
let mut state = self.state.lock();
while let Some((deadline, _)) = state.delayed.first() {
if *deadline > state.time {
break;
}
let (_, runnable) = state.delayed.remove(0);
state.background.push(runnable);
}
let foreground_len: usize = if background_only {
0
} else {
state
.foreground
.values()
.map(|runnables| runnables.len())
.sum()
};
let background_len = state.background.len();
let runnable;
let main_thread;
if foreground_len == 0 && background_len == 0 {
let deprioritized_background_len = state.deprioritized_background.len();
if deprioritized_background_len == 0 {
return false;
}
let ix = state.random.gen_range(0..deprioritized_background_len);
main_thread = false;
runnable = state.deprioritized_background.swap_remove(ix);
} else {
main_thread = state.random.gen_ratio(
foreground_len as u32,
(foreground_len + background_len) as u32,
);
if main_thread {
let state = &mut *state;
runnable = state
.foreground
.values_mut()
.filter(|runnables| !runnables.is_empty())
.choose(&mut state.random)
.unwrap()
.pop_front()
.unwrap();
} else {
let ix = state.random.gen_range(0..background_len);
runnable = state.background.swap_remove(ix);
};
};
let was_main_thread = state.is_main_thread;
state.is_main_thread = main_thread;
drop(state);
runnable.run();
self.state.lock().is_main_thread = was_main_thread;
true
}
fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>) { fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>) {
{ {
let mut state = self.state.lock(); let mut state = self.state.lock();

View file

@ -136,6 +136,7 @@ impl DapStore {
breakpoint_store: Entity<BreakpointStore>, breakpoint_store: Entity<BreakpointStore>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
cx.on_app_quit(Self::shutdown_sessions).detach();
let mode = DapStoreMode::Local(LocalDapStore { let mode = DapStoreMode::Local(LocalDapStore {
fs, fs,
environment, environment,

View file

@ -650,12 +650,13 @@ impl HeadlessProject {
cx: AsyncApp, cx: AsyncApp,
) -> Result<proto::Ack> { ) -> Result<proto::Ack> {
cx.spawn(async move |cx| { cx.spawn(async move |cx| {
cx.update(|cx| { // todo!("come back to this")
// TODO: This is a hack, because in a headless project, shutdown isn't executed // cx.update(|cx| {
// when calling quit, but it should be. // // TODO: This is a hack, because in a headless project, shutdown isn't executed
cx.shutdown(); // // when calling quit, but it should be.
cx.quit(); // cx.shutdown();
}) // cx.quit();
// })
}) })
.detach(); .detach();

View file

@ -0,0 +1,5 @@
{"Put":{"state":"hello\nˇ\nworld"}}
{"Key":"i"}
{"Key":"ctrl-y"}
{"Key":"ctrl-e"}
{"Get":{"state":"hello\nhoˇ\nworld","mode":"Insert"}}