From 962944b74487b58c841f90dcf99bd66a7e45c26e Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Tue, 21 Jul 2026 14:37:54 -0400 Subject: [PATCH] fix(vterm): normalize Darwin PTY signal names portable-pty forwards libc strsignal text, which includes a numeric suffix on Darwin. Strip a verified numeric suffix before mapping known descriptions so terminal snapshots and exit annotations retain the protocol's symbolic SIGFOO form across platforms. Add focused coverage for glibc, Darwin, and unknown descriptions. --- src/process.rs | 56 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/src/process.rs b/src/process.rs index 17ae0e7..2b53bf3 100644 --- a/src/process.rs +++ b/src/process.rs @@ -753,29 +753,36 @@ impl TermStatus { } /// Map `libc::strsignal` description strings (as surfaced by -/// `portable-pty`) to symbolic SIGFOO names. Unknown descriptions pass -/// through unchanged — better to surface an unfamiliar string than to -/// fabricate a wrong name. Covers every signal in +/// `portable-pty`) to symbolic SIGFOO names. Darwin appends the signal +/// number (for example, `"Terminated: 15"`), while glibc returns only +/// the description. Unknown descriptions pass through unchanged — +/// better to surface an unfamiliar string than to fabricate a wrong +/// name. Covers every signal in /// [`super::lua_bindings::parse_signal`]'s accept-list plus the common /// fault signals that surface during process crashes. fn canonicalize_pty_signal_name(desc: &str) -> String { - match desc { - "Interrupt" => "SIGINT".to_owned(), - "Terminated" => "SIGTERM".to_owned(), - "Killed" => "SIGKILL".to_owned(), - "Hangup" => "SIGHUP".to_owned(), - "Quit" => "SIGQUIT".to_owned(), - "User defined signal 1" => "SIGUSR1".to_owned(), - "User defined signal 2" => "SIGUSR2".to_owned(), - "Aborted" => "SIGABRT".to_owned(), - "Segmentation fault" => "SIGSEGV".to_owned(), - "Floating point exception" => "SIGFPE".to_owned(), - "Illegal instruction" => "SIGILL".to_owned(), - "Broken pipe" => "SIGPIPE".to_owned(), - "Alarm clock" => "SIGALRM".to_owned(), - "Bus error" => "SIGBUS".to_owned(), - other => other.to_owned(), + let base = desc + .rsplit_once(": ") + .filter(|(_, number)| number.parse::().is_ok()) + .map_or(desc, |(description, _)| description); + match base { + "Interrupt" => "SIGINT", + "Terminated" => "SIGTERM", + "Killed" => "SIGKILL", + "Hangup" => "SIGHUP", + "Quit" => "SIGQUIT", + "User defined signal 1" => "SIGUSR1", + "User defined signal 2" => "SIGUSR2", + "Aborted" => "SIGABRT", + "Segmentation fault" => "SIGSEGV", + "Floating point exception" => "SIGFPE", + "Illegal instruction" => "SIGILL", + "Broken pipe" => "SIGPIPE", + "Alarm clock" => "SIGALRM", + "Bus error" => "SIGBUS", + _ => desc, } + .to_owned() } impl Default for ProcessSupervisor { @@ -2073,6 +2080,17 @@ mod tests { }) } + #[test] + fn pty_signal_names_are_canonical_across_libc_variants() { + assert_eq!(canonicalize_pty_signal_name("Terminated"), "SIGTERM"); + assert_eq!(canonicalize_pty_signal_name("Terminated: 15"), "SIGTERM"); + assert_eq!(canonicalize_pty_signal_name("Killed: 9"), "SIGKILL"); + assert_eq!( + canonicalize_pty_signal_name("Unknown signal: 99"), + "Unknown signal: 99" + ); + } + #[test] fn terminal_transactional_spawn_failure_has_no_event_or_process_residue() { let mut supervisor = ProcessSupervisor::new();