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.
This commit is contained in:
Levi Neuwirth 2026-07-21 14:37:54 -04:00
parent 5d7f0078f1
commit 962944b744
1 changed files with 37 additions and 19 deletions

View File

@ -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::<u32>().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();