diff --git a/crates/pf-encode/src/enc/linux/pyrowave_remote.rs b/crates/pf-encode/src/enc/linux/pyrowave_remote.rs index 166c4aa3..f5bdfd34 100644 --- a/crates/pf-encode/src/enc/linux/pyrowave_remote.rs +++ b/crates/pf-encode/src/enc/linux/pyrowave_remote.rs @@ -380,7 +380,15 @@ fn handshake(mut link: Link, p: &Params, bitrate_bps: u64) -> Result // the process boundary. priority_intent: std::env::var("PYROWAVE_QUEUE_PRIORITY").ok(), }; - worker::send_eintr(link.sock.as_fd(), &hello, &[]).context("send Hello")?; + // Naming the handshake here is load-bearing, not decoration. A worker that dies during + // startup — no Vulkan 1.3 device, a missing feature, a half-installed binary that execs and + // exits — races this send: win the race and the death surfaces as the EOF the recv below + // reads, lose it and the socket is already closed, so the send takes EPIPE instead. One + // cause, so the operator must get one diagnosis; without this context the EPIPE side + // degrades the fallback warn to a bare "send Hello: Broken pipe (os error 32)", which names + // neither the worker nor the stage that failed. + worker::send_eintr(link.sock.as_fd(), &hello, &[]) + .context("encode worker handshake: send Hello (died on startup?)")?; let (ready, fds) = worker::recv_eintr::( link.sock.as_fd(), &mut link.rbuf, @@ -1043,8 +1051,16 @@ mod tests { .expect("a `false` binary on PATH") } - /// Ladder rung: the binary exists and runs but is not a worker. It exits at once, so the - /// handshake reads EOF — the same rung a worker that dies during Vulkan bring-up takes. + /// Ladder rung: the binary exists and runs but is not a worker — the same rung a worker that + /// dies during Vulkan bring-up takes. + /// + /// Which HALF of the handshake reports the death is a race this test deliberately does not + /// try to win: the child exits while the parent is still writing, so the send either lands in + /// a socket whose peer is still open (and the death surfaces as the EOF the recv reads) or + /// finds it already closed (EPIPE). CI, being slower and more loaded than a dev box, lands on + /// the EPIPE side often enough that pinning the EOF wording alone flakes — so what is pinned + /// is the operator-visible contract that holds on BOTH sides. The EPIPE half also has a + /// deterministic test of its own below; this one keeps a real spawn+exec in the ladder. #[test] fn a_worker_that_exits_immediately_is_a_handshake_failure() { let err = @@ -1056,6 +1072,22 @@ mod tests { ); } + /// The EPIPE half of the rung above, without the race: the peer is closed BEFORE the + /// handshake starts, so the Hello send cannot succeed. Deterministic, and it fails if anyone + /// trims the send's context back to a bare "send Hello" — which is exactly the drift that + /// made the spawn-driven test flake in CI. + #[test] + fn a_worker_that_died_before_hello_still_names_the_handshake() { + let (host, peer) = ipc::socketpair_seqpacket().unwrap(); + drop(peer); // the worker is gone before the host writes a byte + let err = handshake_on(host).unwrap_err(); + let text = format!("{err:#}"); + assert!( + text.contains("handshake"), + "the rung must name the handshake even when the death beats the Hello: {text}" + ); + } + /// Ladder rung: a binary that does not exist at all — an operator-set `PUNKTFUNK_ENCODE_WORKER` /// typo, or a half-installed package. #[test]