From b584192a4ccc2f8fde8533f181669200c229ef4f Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Sun, 25 Jan 2026 15:02:29 +0530 Subject: [PATCH] terminal: Fix terminal freeze when child process is killed by signal (#47420) Closes #38168 Closes #42414 We were already using `ExitStatusExt::from_raw()` to construct an `ExitStatus`, but we were passing in the exit code from alacritty_terminal's `ChildExit` event. This worked fine on Windows where `from_raw()` expects an exit code, but on Unix, `from_raw()` ([read more](https://doc.rust-lang.org/std/os/unix/process/trait.ExitStatusExt.html#tymethod.from_raw)) expects a raw wait status from `waitpid()` and not an exit code. When a child process was killed by a signal (e.g., SIGSEGV), `ExitStatus::code()` returns `None` since only normal exits have an exit code. This caused the terminal to hang because we weren't properly detecting the exit. One fix would have been to remove the dependency on `ExitStatus` entirely, but using the raw wait status gives us more information, we can now detect exit codes, signal terminations, and more. The actual fix was upstream in `alacritty_terminal` to send the raw wait status instead of just the exit code. Currently using forked patch https://github.com/zed-industries/alacritty/tree/v0.16-child-exit-patch which is based on v0.25.1. Upstream PR: https://github.com/alacritty/alacritty/pull/8825 Release Notes: - Fixed terminal hanging when a child process is killed by a signal (e.g., SIGSEGV from null pointer dereference). --- Cargo.lock | 3 +- Cargo.toml | 2 +- crates/terminal/src/terminal.rs | 56 ++++++++++++++++++++++----------- 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 930289eb1e00dff1cef520f816407a7714191022..f8750e46f54394e148261c6c064422117288b236 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -511,8 +511,7 @@ dependencies = [ [[package]] name = "alacritty_terminal" version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46319972e74179d707445f64aaa2893bbf6a111de3a9af29b7eb382f8b39e282" +source = "git+https://github.com/zed-industries/alacritty?rev=936aee8761a17affc84ab418ae21306c27c26221#936aee8761a17affc84ab418ae21306c27c26221" dependencies = [ "base64 0.22.1", "bitflags 2.9.4", diff --git a/Cargo.toml b/Cargo.toml index 84fe05d2c150fc7503344e06830e926498fa292f..0ead3cee38719e7001c9fe6f0e2ec8152231963a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -459,7 +459,7 @@ ztracing_macro = { path = "crates/ztracing_macro" } agent-client-protocol = { version = "=0.9.3", features = ["unstable"] } aho-corasick = "1.1" -alacritty_terminal = "0.25.1" +alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "936aee8761a17affc84ab418ae21306c27c26221" } any_vec = "0.14" anyhow = "1.0.86" arrayvec = { version = "0.7.4", features = ["serde"] } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 5722e5cb2ccbe7649f031c65b7018f10797cbd92..4026a047df80627fbc7c7d96d9da605c2464e259 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -52,6 +52,8 @@ use theme::{ActiveTheme, Theme}; use urlencoding; use util::{paths::PathStyle, truncate_and_trailoff}; +#[cfg(unix)] +use std::os::unix::process::ExitStatusExt; use std::{ borrow::Cow, cmp::{self, min}, @@ -992,8 +994,8 @@ impl Terminal { .unwrap_or_else(|| to_alac_rgb(get_color_at_index(index, cx.theme().as_ref()))); self.write_to_pty(format(color).into_bytes()); } - AlacTermEvent::ChildExit(error_code) => { - self.register_task_finished(Some(error_code), cx); + AlacTermEvent::ChildExit(raw_status) => { + self.register_task_finished(Some(raw_status), cx); } } } @@ -2201,22 +2203,22 @@ impl Terminal { Task::ready(None) } - fn register_task_finished(&mut self, error_code: Option, cx: &mut Context) { - let e: Option = error_code.map(|code| { + fn register_task_finished(&mut self, raw_status: Option, cx: &mut Context) { + let exit_status: Option = raw_status.map(|value| { #[cfg(unix)] { - std::os::unix::process::ExitStatusExt::from_raw(code) + std::os::unix::process::ExitStatusExt::from_raw(value) } #[cfg(windows)] { - std::os::windows::process::ExitStatusExt::from_raw(code as u32) + std::os::windows::process::ExitStatusExt::from_raw(value as u32) } }); if let Some(tx) = &self.completion_tx { - tx.try_send(e).ok(); + tx.try_send(exit_status).ok(); } - if let Some(e) = e { + if let Some(e) = exit_status { self.child_exited = Some(e); } let task = match &mut self.task { @@ -2231,7 +2233,7 @@ impl Terminal { if task.status != TaskStatus::Running { return; } - match error_code { + match exit_status.and_then(|e| e.code()) { Some(error_code) => { task.status.register_task_exit(error_code); } @@ -2240,7 +2242,7 @@ impl Terminal { } }; - let (finished_successfully, task_line, command_line) = task_summary(task, error_code); + let (finished_successfully, task_line, command_line) = task_summary(task, exit_status); let mut lines_to_show = Vec::new(); if task.spawned_task.show_summary { lines_to_show.push(task_line.as_str()); @@ -2305,19 +2307,35 @@ pub fn row_to_string(row: &Row) -> String { } const TASK_DELIMITER: &str = "⏵ "; -fn task_summary(task: &TaskState, error_code: Option) -> (bool, String, String) { +fn task_summary(task: &TaskState, exit_status: Option) -> (bool, String, String) { let escaped_full_label = task .spawned_task .full_label .replace("\r\n", "\r") .replace('\n', "\r"); - let success = error_code == Some(0); - let task_line = match error_code { - Some(0) => format!("{TASK_DELIMITER}Task `{escaped_full_label}` finished successfully"), - Some(error_code) => format!( - "{TASK_DELIMITER}Task `{escaped_full_label}` finished with non-zero error code: {error_code}" - ), - None => format!("{TASK_DELIMITER}Task `{escaped_full_label}` finished"), + let task_label = |suffix: &str| format!("{TASK_DELIMITER}Task `{escaped_full_label}` {suffix}"); + let (success, task_line) = match exit_status { + Some(status) => { + let code = status.code(); + #[cfg(unix)] + let signal = status.signal(); + #[cfg(not(unix))] + let signal: Option = None; + + match (code, signal) { + (Some(0), _) => (true, task_label("finished successfully")), + (Some(code), _) => ( + false, + task_label(&format!("finished with exit code: {code}")), + ), + (None, Some(signal)) => ( + false, + task_label(&format!("terminated by signal: {signal}")), + ), + (None, None) => (false, task_label("finished")), + } + } + None => (false, task_label("finished")), }; let escaped_command_label = task .spawned_task @@ -2813,7 +2831,7 @@ mod tests { #[cfg(target_os = "windows")] assert_eq!(exit_status.code(), Some(1)); #[cfg(not(target_os = "windows"))] - assert_eq!(exit_status.code(), None); + assert_eq!(exit_status.code(), Some(127)); // code 127 means "command not found" on Unix } });