Better on_app_quit

Conrad Irwin and Antonio Scandurra created

Co-authored-by: Antonio Scandurra <me@as-cii.com>

Change summary

crates/gpui/src/app.rs                       |  35 +++++
crates/gpui/src/app/test_context.rs          |   2 
crates/gpui/src/executor.rs                  |   5 
crates/gpui/src/platform.rs                  |   1 
crates/gpui/src/platform/mac/dispatcher.rs   |  39 ++++++
crates/gpui/src/platform/test/dispatcher.rs  | 124 +++++++++++-----------
crates/project/src/debugger/dap_store.rs     |   1 
crates/remote_server/src/headless_project.rs |  13 +-
crates/vim/test_data/test_insert_ctrl_y.json |   5 
9 files changed, 147 insertions(+), 78 deletions(-)

Detailed changes

crates/gpui/src/app.rs 🔗

@@ -7,6 +7,7 @@ use std::{
     path::{Path, PathBuf},
     rc::{Rc, Weak},
     sync::{Arc, atomic::Ordering::SeqCst},
+    task::{Poll, Waker},
     time::Duration,
 };
 
@@ -91,6 +92,36 @@ impl AppCell {
         }
         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 dbg!(start.elapsed() < SHUTDOWN_TIMEOUT) {
+            match futures.as_mut().poll(&mut future_cx) {
+                Poll::Pending => {
+                    executor.tick();
+                }
+                Poll::Ready(_) => break,
+            }
+        }
+    }
 }
 
 #[doc(hidden)]
@@ -382,7 +413,7 @@ impl App {
         platform.on_quit(Box::new({
             let cx = app.clone();
             move || {
-                cx.borrow_mut().shutdown();
+                cx.shutdown();
             }
         }));
 
@@ -391,7 +422,7 @@ impl 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) {
+    pub fn shutdown_old(&mut self) {
         let mut futures = Vec::new();
 
         for observer in self.quit_observers.remove(&()) {

crates/gpui/src/app/test_context.rs 🔗

@@ -167,7 +167,7 @@ impl TestAppContext {
     /// public so the macro can call it.
     pub fn quit(&self) {
         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.

crates/gpui/src/executor.rs 🔗

@@ -384,10 +384,9 @@ impl BackgroundExecutor {
         self.dispatcher.as_test().unwrap().advance_clock(duration)
     }
 
-    /// in tests, run one task.
-    #[cfg(any(test, feature = "test-support"))]
+    /// docs
     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

crates/gpui/src/platform.rs 🔗

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

crates/gpui/src/platform/mac/dispatcher.rs 🔗

@@ -4,6 +4,14 @@
 
 use crate::{PlatformDispatcher, TaskLabel};
 use async_task::Runnable;
+use block::{Block, ConcreteBlock, RcBlock};
+use core_foundation::{
+    base::CFTypeRef,
+    runloop::{
+        CFRunLoopRef, CFRunLoopRunInMode, CFRunLoopWakeUp, kCFRunLoopCommonModes,
+        kCFRunLoopDefaultMode,
+    },
+};
 use objc::{
     class, msg_send,
     runtime::{BOOL, YES},
@@ -11,7 +19,9 @@ use objc::{
 };
 use parking::{Parker, Unparker};
 use parking_lot::Mutex;
+use smol::io::BlockOn;
 use std::{
+    cell::Cell,
     ffi::c_void,
     ptr::{NonNull, addr_of},
     sync::Arc,
@@ -64,11 +74,21 @@ impl PlatformDispatcher for MacDispatcher {
     }
 
     fn dispatch_on_main_thread(&self, runnable: Runnable) {
+        use core_foundation::runloop::CFRunLoopGetMain;
+
         unsafe {
-            dispatch_async_f(
-                dispatch_get_main_queue(),
-                runnable.into_raw().as_ptr() as *mut c_void,
-                Some(trampoline),
+            let mut runnable = Cell::new(Some(runnable));
+            let main_run_loop = CFRunLoopGetMain();
+            let block = ConcreteBlock::new(move || {
+                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 {
         if let Some(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 ())) };
     task.run();
 }
+
+unsafe extern "C" {
+    fn CFRunLoopPerformBlock(rl: CFRunLoopRef, mode: CFTypeRef, block: *const c_void);
+}

crates/gpui/src/platform/test/dispatcher.rs 🔗

@@ -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) {
         self.state
             .lock()
@@ -267,6 +205,68 @@ impl PlatformDispatcher for TestDispatcher {
         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>) {
         {
             let mut state = self.state.lock();

crates/project/src/debugger/dap_store.rs 🔗

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

crates/remote_server/src/headless_project.rs 🔗

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