…t a single terminal frame
Hardening of the WebM shadow streamer surfaced by the integration harness.
- Header-write path now treats a closed destination channel as a clean shutdown
(Ok), matching the main encode loop. Previously a client that disconnected while
the initial WebM headers were still being written caused `webm_stream` to return
Err, which the caller turned into a spurious server_error to the client.
- spawn_sending_task now arbitrates the terminal frame with a shared AtomicBool so
the client receives at most one End/Error frame. Both the message-handler and the
control task could previously emit one on the same termination path.
- Consolidate the closed-channel detection (previously a fragile triple downcast in
two places) into ChannelWriterError::is_in_chain. The classifier walks the anyhow
chain and peeks inside io::Error via get_ref(), since io::Error::source() exposes
the inner error's source rather than the ChannelWriterError itself.
Adds two ignored, XMF-gated regression tests covering both fixes.
Summary
Implementation-level hardening of the WebM "shadow" live-streamer (
video-streamercrate). The streaming architecture is unchanged; these are correctness fixes to its termination/error handling, surfaced and verified with the crate's (ignored, XMF-gated) integration harness.Fixes
#0 — spurious
server_erroron early disconnect (header phase)A client that disconnected while
webm_streamwas still writing the initial WebM headers causedwebm_streamto returnErr. The header-write loop propagated the closed-channel error with a bare?, while the main encode loop already treated a closed channel as a clean shutdown. The caller (devolutions-gateway/src/streaming.rs) turns thatErrinto aserver_errorsent to the client — so any shadow session closed during the brief header phase got a bogus error. The header path now goes through the same closed-channel handling and exitsOk.This was intermittent (a scheduling race between the disconnect and the header writes); the existing
client_disconnect_exits_cleanlytest caught it only ~1-in-3 runs.#5 — duplicate terminal frame on shutdown
In
spawn_sending_task, both the message-handler task and the control task independently emitted a terminal frame (End/Error) on most termination paths (external shutdown, client disconnect, error), so the client could receive two terminal frames. A sharedAtomicBoolnow arbitrates: whoever flips it first sends the single terminal frame; the other skips. The clean producer-EOF path still emits exactly oneEnd.#8 — consolidate closed-channel detection
The closed-channel check was duplicated as a fragile triple downcast in two places. It is now a single
ChannelWriterError::is_in_chain. The classifier walks theanyhowerror chain and peeks insideio::Errorviaget_ref()— necessary becauseio::Error::source()exposes the inner error's source (here, nothing), not theChannelWriterErroritself.Tests
Two new
#[ignore], XMF-gated regression tests inwebm_stream_correctness.rs:early_disconnect_during_headers_exits_ok— disconnect during the header phase must exitOk(red→green against this fix).external_shutdown_emits_single_terminal_frame— at most one terminal frame on shutdown.Verification:
cargo clippy -p video-streamer --testsand--lib --features perf-diagnostics: clean.webm_stream_correctnesssuite (9 tests) passes serialized.is_in_chainreasoning, the AtomicBool gate with no zero-frame regression, and the header early-return contract were all verified).Notes / out of scope
DGATEWAY_LIB_XMF_PATHand a local WebM asset undercrates/video-streamer/testing-assets/(not committed; new crate-level.gitignorekeeps it out).--features benchfails to compile (bench_support.rs:74callsWebmPositionedIterator::newwith 1 arg vs 3). Not addressed here.spawn_blockingdecode path interruptible on shutdown.