Skip to content

Commit 738f3d2

Browse files
authored
fix: preserve signal-based exit codes (128 + signal_number) (#161)
1 parent fce58e2 commit 738f3d2

File tree

2 files changed

+64
-5
lines changed

2 files changed

+64
-5
lines changed

src/shell/commands/executable.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,24 @@ impl ShellCommand for ExecutableCommand {
6565
loop {
6666
tokio::select! {
6767
result = child.wait() => match result {
68-
Ok(status) => return ExecuteResult::Continue(
69-
status.code().unwrap_or(1),
70-
Vec::new(),
71-
Vec::new(),
72-
),
68+
Ok(status) => {
69+
#[cfg(unix)]
70+
let exit_code = {
71+
use std::os::unix::process::ExitStatusExt;
72+
status.code().unwrap_or_else(|| {
73+
// Process was terminated by a signal, return 128 + signal_number per POSIX
74+
status.signal().map(|sig| 128 + sig).unwrap_or(1)
75+
})
76+
};
77+
#[cfg(not(unix))]
78+
let exit_code = status.code().unwrap_or(1);
79+
80+
return ExecuteResult::Continue(
81+
exit_code,
82+
Vec::new(),
83+
Vec::new(),
84+
);
85+
}
7386
Err(err) => {
7487
let _ = stderr.write_line(&format!("{}", err));
7588
return ExecuteResult::from_exit_code(1);

tests/integration_test.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1631,3 +1631,49 @@ fn no_such_file_error_text() -> &'static str {
16311631
"No such file or directory (os error 2)"
16321632
}
16331633
}
1634+
1635+
#[tokio::test]
1636+
#[cfg(unix)]
1637+
async fn signal_exit_codes() {
1638+
// Test SIGPIPE exit code (128 + 13 = 141)
1639+
TestBuilder::new()
1640+
.command("sh -c 'kill -PIPE $$'")
1641+
.assert_exit_code(141)
1642+
.run()
1643+
.await;
1644+
1645+
// Test SIGTERM exit code (128 + 15 = 143)
1646+
TestBuilder::new()
1647+
.command("sh -c 'kill -TERM $$'")
1648+
.assert_exit_code(143)
1649+
.run()
1650+
.await;
1651+
1652+
// Test SIGINT exit code (128 + 2 = 130)
1653+
TestBuilder::new()
1654+
.command("sh -c 'kill -INT $$'")
1655+
.assert_exit_code(130)
1656+
.run()
1657+
.await;
1658+
1659+
// Test SIGKILL exit code (128 + 9 = 137)
1660+
TestBuilder::new()
1661+
.command("sh -c 'kill -KILL $$'")
1662+
.assert_exit_code(137)
1663+
.run()
1664+
.await;
1665+
}
1666+
1667+
#[tokio::test]
1668+
#[cfg(unix)]
1669+
async fn sigpipe_from_pipeline() {
1670+
// Test that SIGPIPE from a closed pipe doesn't cause issues
1671+
// and the pipeline completes successfully
1672+
// `yes` outputs infinite "y\n" until SIGPIPE, `head -n 1` reads one line
1673+
TestBuilder::new()
1674+
.command("yes | head -n 1")
1675+
.assert_stdout("y\n")
1676+
.assert_exit_code(0)
1677+
.run()
1678+
.await;
1679+
}

0 commit comments

Comments
 (0)