Bound app-server stdio shutdown and handle Unix SIGTERM gracefully (#44523)

## Why

Stdio shutdown can hang on open pipes or blocked I/O, and Unix `SIGTERM` needs to run connection cleanup so owned commands terminate and session-end hooks execute.

## What changed

- Handle `SIGTERM` on a dedicated signal thread and close the stdio connection through the existing cleanup path.
- Move blocking stdin and stdout operations to dedicated threads so they do not hold the Tokio runtime open. Cancel message forwarding on `SIGTERM` to release blocked producers.
- When shutdown signal handling is enabled on Unix, start a shared 45-second watchdog on EOF or `SIGTERM`. Preserve the first deadline and exit with status `1` if cleanup or runtime teardown stalls, even when logging is blocked.

## Testing

Add Unix integration coverage for shutdown with open pipes, blocked stdout, blocked stderr, and a stalled filesystem write. Verify command and child-process termination, preserve the EOF deadline when `SIGTERM` follows, and extend session-end hook coverage to `SIGTERM`.

GitOrigin-RevId: 43a80b8277eb50519cdc293eb3b5dc2cad00b3aa
This commit is contained in:
jif authored and copyberry committed 2026-09-10 13:02:57 +00:00
1 parent ed6dde9fda
commit 713caa89f3
9 files changed
+501 -45

No files matched your search

@@ -192,6 +192,21 @@ impl TestAppServer {
self.process.wait().await
}
#[cfg(unix)]
pub fn send_sigterm(&self) -> anyhow::Result<()> {
let pid = self.process.id().context("app-server has no pid")?;
let status = std::process::Command::new("kill")
.args(["-TERM", &pid.to_string()])
.status()?;
ensure!(status.success(), "failed to signal app-server: {status}");
Ok(())
}
/// Waits for output without consuming it, for transport backpressure tests.
pub async fn peek_stdout(&mut self) -> std::io::Result<&[u8]> {
self.stdout.fill_buf().await
}
/// Closes stdio and waits for app-server's graceful thread teardown to finish.
pub async fn shutdown_gracefully(&mut self) -> std::io::Result<ExitStatus> {
drop(self.stdin.take());