Skip to content

Fix terminal corruption on unexpected termination by adding signal handlers #10326

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/turborepo-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ nix = { version = "0.26.2", features = ["signal"] }
ratatui = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
signal-hook = "0.3.17"
thiserror = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
Expand All @@ -46,3 +47,4 @@ which = { workspace = true }
[target."cfg(windows)".dependencies]
clipboard-win = "5.3.1"
windows-sys = { version = "0.59", features = ["Win32_System_Console"] }
winapi = { version = "0.3.9", features = ["consoleapi"] }
86 changes: 86 additions & 0 deletions crates/turborepo-ui/src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ use std::{
collections::BTreeMap,
io::{self, Stdout, Write},
mem,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::Duration,
};

Expand Down Expand Up @@ -654,6 +658,9 @@ pub async fn run_app(
let (crossterm_tx, crossterm_rx) = mpsc::channel(1024);
input::start_crossterm_stream(crossterm_tx);

// Set up signal handlers for proper terminal cleanup on SIGINT, SIGTERM, etc.
let terminal_cleanup_state = setup_signal_handlers()?;

let (result, callback) =
match run_app_inner(&mut terminal, &mut app, receiver, crossterm_rx).await {
Ok(callback) => (Ok(()), callback),
Expand All @@ -663,11 +670,90 @@ pub async fn run_app(
}
};

// Reset the signal handlers to default since we're done with the TUI
reset_signal_handlers(terminal_cleanup_state);
cleanup(terminal, app, callback)?;

result
}

// Global state for terminal cleanup on signal
type TerminalCleanupState = Arc<AtomicBool>;

// Set up signal handlers to handle SIGINT, SIGTERM, etc.
fn setup_signal_handlers() -> io::Result<TerminalCleanupState> {
let cleanup_state = Arc::new(AtomicBool::new(false));
let cleanup_state_clone = cleanup_state.clone();

// Use a different approach for different platforms
#[cfg(unix)]
{
use signal_hook::{consts::*, flag};

// Set up handlers for SIGINT, SIGTERM, SIGHUP
flag::register(SIGINT, cleanup_state_clone.clone())?;
flag::register(SIGTERM, cleanup_state_clone.clone())?;
flag::register(SIGHUP, cleanup_state_clone.clone())?;

// Set up a thread to monitor signals and perform cleanup
let state = cleanup_state_clone.clone();
std::thread::spawn(move || {
loop {
if state.load(Ordering::SeqCst) {
// Emergency terminal cleanup on signal
let _ = emergency_terminal_cleanup();
break;
}

std::thread::sleep(std::time::Duration::from_millis(100));
}
});
}

#[cfg(windows)]
{
// On Windows, we can use SetConsoleCtrlHandler
use winapi::um::consoleapi::SetConsoleCtrlHandler;

extern "system" fn ctrl_handler(_: u32) -> i32 {
let _ = emergency_terminal_cleanup();
1 // Signal was handled
}

unsafe {
let result = SetConsoleCtrlHandler(Some(ctrl_handler), 1);
if result == 0 {
return Err(io::Error::last_os_error());
}
}
}

Ok(cleanup_state)
}

// Reset signal handlers to default
fn reset_signal_handlers(state: TerminalCleanupState) {
state.store(true, Ordering::SeqCst);
}

// Emergency terminal cleanup that is safe to call from a signal handler
fn emergency_terminal_cleanup() -> io::Result<()> {
// This cleanup is minimal and avoids any operations that are unsafe in signal
// handlers
let mut stdout = io::stdout();

// We need to use direct calls to avoid complex operations in signal handlers
crossterm::terminal::disable_raw_mode()?;

crossterm::execute!(
stdout,
crossterm::event::DisableMouseCapture,
crossterm::terminal::LeaveAlternateScreen
)?;

Ok(())
}

// Break out inner loop so we can use `?` without worrying about cleaning up the
// terminal.
async fn run_app_inner<B: Backend + std::io::Write>(
Expand Down
5 changes: 4 additions & 1 deletion crates/turborepo-vt100/src/grid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,10 @@ impl Grid {
}

pub fn set_scrollback(&mut self, rows: usize) {
self.scrollback_offset = rows.min(self.scrollback.len());
// Ensure we don't set the scrollback beyond what we actually have stored
// This helps prevent issues during terminal cleanup or unexpected termination
let max_available = self.scrollback.len();
self.scrollback_offset = rows.min(max_available);
}

pub fn clear_selection(&mut self) {
Expand Down
Loading