Watch
0
0
Fork
You've already forked hyperhive
0

screen relay: close the websocket when the vnc side hangs up and stop the surviving task

This commit is contained in:
damocles 2026-10-21 23:10:10 +02:00 • committed by mara
commit 9d1a8c0d9a
3 changed files with 62 additions and 5 deletions

1
Cargo.lock generated
View file

@ -1697,6 +1697,7 @@ dependencies = [
"tempfile",
"tokio",
"tokio-stream",
"tokio-tungstenite",
"tower",
"tower-http 0.7.0",
"tracing",

View file

@ -70,6 +70,9 @@ tracing.workspace = true
# refresh test waits out its one-minute interval on paused time.
[dev-dependencies]
tokio = { workspace = true, features = ["test-util"] }
# A WebSocket client for the screen relay teardown test; the same version
# axum's `ws` feature already pulls in.
tokio-tungstenite = "0.29"
tower = { version = "0.5", features = ["util"] }
# A layer that captures `swarm_queue`'s exhausted-connect log event in a test.
tracing-subscriber.workspace = true

View file

@ -55,7 +55,7 @@ async fn relay_ws_vnc(socket: axum::extract::ws::WebSocket, vnc_port: u16) {
let (mut ws_tx, mut ws_rx) = socket.split();
// WS → TCP
let ws_to_tcp = tokio::spawn(async move {
let mut ws_to_tcp = tokio::spawn(async move {
while let Some(Ok(msg)) = futures_util::StreamExt::next(&mut ws_rx).await {
match msg {
Message::Binary(data) if tcp_tx.write_all(&data).await.is_err() => {
@ -68,7 +68,7 @@ async fn relay_ws_vnc(socket: axum::extract::ws::WebSocket, vnc_port: u16) {
});
// TCP → WS
let tcp_to_ws = tokio::spawn(async move {
let mut tcp_to_ws = tokio::spawn(async move {
let mut buf = vec![0u8; 8192];
loop {
match tcp_rx.read(&mut buf).await {
@ -84,11 +84,64 @@ async fn relay_ws_vnc(socket: axum::extract::ws::WebSocket, vnc_port: u16) {
}
}
}
// The VNC side is gone: tell the browser instead of leaving it
// holding a connection that will never deliver anything.
let _ = ws_tx.send(Message::Close(None)).await;
});
// Wait for either direction to close, then let both tasks drop.
// When either direction ends, stop the other one too. Dropping a
// `JoinHandle` only detaches its task, so it is aborted explicitly.
tokio::select! {
_ = ws_to_tcp => {}
_ = tcp_to_ws => {}
_ = &mut ws_to_tcp => tcp_to_ws.abort(),
_ = &mut tcp_to_ws => ws_to_tcp.abort(),
}
}
#[cfg(test)]
mod tests {
use super::relay_ws_vnc;
use futures_util::StreamExt as _;
use std::time::Duration;
/// When the VNC server hangs up, the browser's WebSocket must be closed
/// rather than left open and silent.
#[tokio::test]
async fn the_websocket_closes_when_the_vnc_side_hangs_up() {
let vnc = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let vnc_port = vnc.local_addr().unwrap().port();
// Accept the relay's connection, then drop it at once.
tokio::spawn(async move {
let _ = vnc.accept().await;
});
let app = axum::Router::new().route(
"/ws",
axum::routing::get(move |ws: axum::extract::ws::WebSocketUpgrade| async move {
ws.on_upgrade(move |socket| relay_ws_vnc(socket, vnc_port))
}),
);
let http = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let http_port = http.local_addr().unwrap().port();
tokio::spawn(async move { axum::serve(http, app).await });
let (mut client, _) =
tokio_tungstenite::connect_async(format!("ws://127.0.0.1:{http_port}/ws"))
.await
.unwrap();
let ended = tokio::time::timeout(Duration::from_secs(5), async {
loop {
match client.next().await {
None | Some(Err(_) | Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => {
break;
}
Some(Ok(_)) => {}
}
}
})
.await;
assert!(
ended.is_ok(),
"websocket still open after the VNC side hung up"
);
}
}