From 101083e7f9c92c52464551d3f888dfa5894b44e8 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Tue, 14 Apr 2026 12:14:31 -0400 Subject: [PATCH] gpui_tokio: Use shutdown_background to avoid WASI panic on exit (#53904) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the Tokio runtime is dropped during Zed shutdown, it drives all spawned tasks to cancellation by polling them one last time. The `wasmtime-wasi` crate wraps child task `JoinHandle`s in an `AbortOnDropJoinHandle` whose `Future::poll` calls `.expect("child task panicked")` on the join result — but Tokio can also return `JoinError::Cancelled` (not just panics), causing the expect to panic with: ``` child task panicked: JoinError::Cancelled(Id(45)) ``` This showed up as Sentry issue ZED-69A (106 events, 85 users, 100% Windows, stable channel). The fix is to explicitly call `shutdown_background()` on the owned Tokio runtime during `GlobalTokio::drop`, which immediately drops all spawned tasks without polling them again, avoiding the wasmtime-wasi panic path entirely. Release Notes: - Fixed a crash on exit caused by the Tokio runtime shutdown triggering a panic in extension WASI tasks (Windows). --- crates/gpui_tokio/src/gpui_tokio.rs | 42 +++++++++++++---------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/crates/gpui_tokio/src/gpui_tokio.rs b/crates/gpui_tokio/src/gpui_tokio.rs index f6e1e0643b2c2a377c1bb8bd042f94c06d382709..d32843ae86f428a3f47f54e3387a75c5aedbcddc 100644 --- a/crates/gpui_tokio/src/gpui_tokio.rs +++ b/crates/gpui_tokio/src/gpui_tokio.rs @@ -17,37 +17,33 @@ pub fn init(cx: &mut App) { .build() .expect("Failed to initialize Tokio"); - cx.set_global(GlobalTokio::new(RuntimeHolder::Owned(runtime))); + let handle = runtime.handle().clone(); + cx.set_global(GlobalTokio { + owned_runtime: Some(runtime), + handle, + }); } /// Initializes the Tokio wrapper using a Tokio runtime handle. pub fn init_from_handle(cx: &mut App, handle: tokio::runtime::Handle) { - cx.set_global(GlobalTokio::new(RuntimeHolder::Shared(handle))); -} - -enum RuntimeHolder { - Owned(tokio::runtime::Runtime), - Shared(tokio::runtime::Handle), -} - -impl RuntimeHolder { - pub fn handle(&self) -> &tokio::runtime::Handle { - match self { - RuntimeHolder::Owned(runtime) => runtime.handle(), - RuntimeHolder::Shared(handle) => handle, - } - } + cx.set_global(GlobalTokio { + owned_runtime: None, + handle, + }); } struct GlobalTokio { - runtime: RuntimeHolder, + owned_runtime: Option, + handle: tokio::runtime::Handle, } impl Global for GlobalTokio {} -impl GlobalTokio { - fn new(runtime: RuntimeHolder) -> Self { - Self { runtime } +impl Drop for GlobalTokio { + fn drop(&mut self) { + if let Some(runtime) = self.owned_runtime.take() { + runtime.shutdown_background(); + } } } @@ -63,7 +59,7 @@ impl Tokio { R: Send + 'static, { cx.read_global(|tokio: &GlobalTokio, cx| { - let join_handle = tokio.runtime.handle().spawn(f); + let join_handle = tokio.handle.spawn(f); let abort_handle = join_handle.abort_handle(); let cancel = defer(move || { abort_handle.abort(); @@ -85,7 +81,7 @@ impl Tokio { R: Send + 'static, { cx.read_global(|tokio: &GlobalTokio, cx| { - let join_handle = tokio.runtime.handle().spawn(f); + let join_handle = tokio.handle.spawn(f); let abort_handle = join_handle.abort_handle(); let cancel = defer(move || { abort_handle.abort(); @@ -99,6 +95,6 @@ impl Tokio { } pub fn handle(cx: &App) -> tokio::runtime::Handle { - GlobalTokio::global(cx).runtime.handle().clone() + GlobalTokio::global(cx).handle.clone() } }