fix(windows): prevent warp.exe from hanging after window closes (#10202)

When the user closes Warp on Windows, the window hides immediately but
`std::process::exit(0)` is only called after `on_will_terminate` returns.
`shutdown_all_pty_event_loops` (Windows-only) joins PTY event loop threads
with no timeout; if a thread fails to exit the process hangs indefinitely.

Add a 5-second timeout to `shutdown_event_loop` on Windows so that a stuck
PTY event loop thread never prevents the process from exiting. If the timeout
fires, the thread is left to be killed by `std::process::exit(0)`, and the OS
will close the ConPTY handle which signals OpenConsole to exit on its own.

Also surface mio `Waker::wake()` failures as logged errors instead of
silently swallowing them, making it easier to diagnose if wake-up failures
are the root cause of a stuck event loop on a given machine.

Co-Authored-By: Oz <oz-agent@warp.dev>
This commit is contained in:
Andy
2026-05-06 02:12:12 +00:00
parent 27c838b191
commit f4e43a8e8c
2 changed files with 35 additions and 3 deletions

View File

@@ -91,14 +91,14 @@ impl<T> Sender<T> {
/// This works the same way as [`mpsc::Sender::send`]. After sending the
/// value, it wakes upthe [`mio::poll::Poll`].
///
/// Note that I/O errors from waking up the [`mio::poll::Poll`] are
/// swallowed.
pub fn send(&self, t: T) -> Result<(), SendError<T>> {
self.tx.send(t)?;
let mut state = self.state.lock().unwrap();
if let Some(waker) = &mut state.waker {
let _ = waker.wake();
if let Err(e) = waker.wake() {
log::error!("PTY mio Waker::wake() failed: {e:?}; event loop may not wake up to process shutdown");
}
} else {
state.needs_wake_on_register = true;
}

View File

@@ -195,6 +195,38 @@ impl TerminalManager {
}
if let Some(join_handle) = self.event_loop_handle.take() {
// On Windows, poll with a timeout instead of blocking indefinitely.
// If the event loop hangs (e.g. due to a pipe read or mio waker failure),
// a stuck join would prevent on_will_terminate from returning and thus
// prevent std::process::exit(0) from ever being called — leaving the
// warp.exe process alive with no visible window (APP-3702).
//
// If we time out, std::process::exit(0) will still kill the thread.
// The ConPTY handle will be closed by the OS when the process exits,
// which signals OpenConsole to exit on its own.
#[cfg(windows)]
{
const SHUTDOWN_TIMEOUT: std::time::Duration =
std::time::Duration::from_secs(5);
const POLL_INTERVAL: std::time::Duration =
std::time::Duration::from_millis(10);
let deadline = std::time::Instant::now() + SHUTDOWN_TIMEOUT;
while !join_handle.is_finished() {
if std::time::Instant::now() >= deadline {
log::warn!(
"PTY event loop did not exit within {SHUTDOWN_TIMEOUT:?}; \
proceeding with app shutdown"
);
self.inactive_pty_reads_rx.close();
return;
}
std::thread::sleep(POLL_INTERVAL);
}
if let Err(e) = join_handle.join() {
log::error!("Failed to join event loop handle {e:?}");
}
}
#[cfg(not(windows))]
if let Err(e) = join_handle.join() {
log::error!("Failed to join event loop handle {e:?}");
}