diff --git a/app/src/terminal/local_tty/mio_channel.rs b/app/src/terminal/local_tty/mio_channel.rs index 1c410726..fe83e48c 100644 --- a/app/src/terminal/local_tty/mio_channel.rs +++ b/app/src/terminal/local_tty/mio_channel.rs @@ -91,14 +91,14 @@ impl Sender { /// 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> { 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; } diff --git a/app/src/terminal/local_tty/terminal_manager.rs b/app/src/terminal/local_tty/terminal_manager.rs index e82558b2..40d6b74c 100644 --- a/app/src/terminal/local_tty/terminal_manager.rs +++ b/app/src/terminal/local_tty/terminal_manager.rs @@ -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:?}"); }