diff --git a/Cargo.lock b/Cargo.lock index 999fc644a..e58ffd397 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4280,7 +4280,7 @@ dependencies = [ [[package]] name = "kcp-sys" version = "0.1.0" -source = "git+https://github.com/rustdesk-org/kcp-sys?branch=rustdesk-patches#023a0065398968989f2ddfcf5cc72bb886d02675" +source = "git+https://github.com/rustdesk-org/kcp-sys?branch=rustdesk-patches#938eda3e5e9757a612385503af7a6cb1189b2cdd" dependencies = [ "anyhow", "auto_impl", diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 56c4462ca..c7a48280a 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -896,9 +896,13 @@ class FfiModel with ChangeNotifier { final text = evt['text']; final link = evt['link']; + // The peer-gone detector reconnects under `restarting-show` rather than an error title, so + // it needs naming here too. By its own title, not the type: an explicitly restarted remote + // device reaches the same type from a path this change does not touch. if (isAndroid && _androidDocumentPickerActive && - title == 'Connection Error') { + (title == 'Connection Error' || + (type == 'restarting-show' && title == 'Connecting...'))) { _androidDocumentPickerInterruptedConnection = true; return; } diff --git a/libs/hbb_common b/libs/hbb_common index 55395c6fc..29cf7cbe4 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 55395c6fcbcb8dd4bc8d4e7ab4d7d7c8d1789b43 +Subproject commit 29cf7cbe4d38ce36020749f713fb066299f02431 diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index a49a2d841..9de20588e 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -15,6 +15,16 @@ use crate::{ // Restart msgbox text is kept as a legacy UI fallback; Flutter handles the type as a control event. const RESTART_REMOTE_DEVICE_NO_DATA_TIMEOUT: Duration = Duration::from_secs(5); const KCP_CLOSE_REASON_FLUSH_DELAY: Duration = Duration::from_millis(30); +// Deadline for the parting close-reason send once the peer is presumed gone; KCP waits for send +// capacity with no deadline of its own. +const KCP_CLOSE_REASON_GONE_DEADLINE: Duration = Duration::from_millis(500); +// Grace after ICE reports Disconnected, which it does ~5s after it stops hearing from the peer, +// for ~8s in total. Disconnected is transient by design, so this waits out a Wi-Fi roam or a +// sleep/wake rather than acting on the first hint. +const WEBRTC_SUSPECT_GRACE: Duration = Duration::from_secs(3); +// KCP gets no such hint, only how long since a packet arrived; its endpoint pings an idle peer +// about every 2s, so this is several missed pings, and matches the 8s WebRTC arrives at. +const KCP_PEER_SILENCE_LIMIT: Duration = Duration::from_secs(8); #[cfg(feature = "unix-file-copy-paste")] use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip}; use base::{ @@ -247,6 +257,9 @@ impl Remote { let _keep_it = client::hc_connection(feedback, rendezvous_server, token).await; let mut last_recv_time = Instant::now(); + let mut webrtc_suspect_since: Option = None; + let mut last_rx_progress = peer.rx_progress(); + let mut peer_gone = false; loop { tokio::select! { @@ -313,6 +326,37 @@ impl Remote { self.handler.msgbox("restarting-show", "Restarting remote device", "Connection in progress. Please wait.", ""); break; } + let rx_progress = peer.rx_progress(); + // `None` for transports that report none, and it never changes for a + // given one, so they are inert here. + let progressed = rx_progress != last_rx_progress; + last_rx_progress = rx_progress; + if peer.webrtc_disconnected() && !progressed { + webrtc_suspect_since.get_or_insert_with(Instant::now); + } else { + webrtc_suspect_since = None; + } + // Neither limit is a hard upper bound. A send is awaited inline in + // this loop, so one in progress delays this tick - bounded on WebRTC + // by the timeout the stream was built with, not bounded at all on + // KCP. The 30s watchdog above shares the loop and the same delay. + peer_gone = webrtc_suspect_since + .map_or(false, |since| since.elapsed() >= WEBRTC_SUSPECT_GRACE) + || kcp + .as_ref() + .and_then(|k| k.peer_silent_for()) + .map_or(false, |silent| silent >= KCP_PEER_SILENCE_LIMIT); + if peer_gone { + log::info!("Peer stopped answering, reconnecting"); + #[cfg(feature = "flutter")] + self.handler.msgbox("restarting-show", "Connecting...", "Connection in progress. Please wait.", ""); + // Sciter knows no `restarting-show` and would show a dialog that + // waits for a click, where the timeout this arrives ahead of is + // retryable and reconnects on its own. Keep that message for it. + #[cfg(not(feature = "flutter"))] + self.handler.msgbox("error", "Connection Error", "Timeout", ""); + break; + } let elapsed = fps_instant.elapsed().as_millis(); if elapsed < 1000 { continue; @@ -358,6 +402,11 @@ impl Remote { s.send(()).ok(); } if kcp.is_some() { + // Attempted rather than skipped even here: if the loss was one-way the peer + // does get it, and drops its side instead of waiting out its own timeout. + if peer_gone { + peer.set_send_timeout(KCP_CLOSE_REASON_GONE_DEADLINE.as_millis() as u64); + } // Send the close reason if it hasn't been sent yet, as KCP cannot detect the socket close event. self.send_close_reason(&mut peer, "kcp").await; // KCP does not send messages immediately, so wait to ensure the last message is sent. diff --git a/src/kcp_stream.rs b/src/kcp_stream.rs index 2c4cfe4bd..50a00296f 100644 --- a/src/kcp_stream.rs +++ b/src/kcp_stream.rs @@ -8,14 +8,15 @@ use hbb_common::{ tokio_util, ResultType, Stream, }; use kcp_sys::{ - endpoint::KcpEndpoint, + endpoint::{ConnId, KcpEndpoint}, packet_def::{KcpPacket, KcpPacketHeader}, stream, }; use std::{net::SocketAddr, sync::Arc}; pub struct KcpStream { - _endpoint: KcpEndpoint, + endpoint: KcpEndpoint, + conn_id: ConnId, stop_sender: Option>, } @@ -41,6 +42,14 @@ impl KcpStream { } } + /// How long since a valid packet was last received from the peer, or `None` once the + /// connection is gone. Answered by the KCP endpoint's own tasks, not by the session's read + /// loop, so it stays meaningful while that loop is busy sending a large message; and the + /// endpoint pings an idle peer often enough that silence here means the peer, not quiet. + pub fn peer_silent_for(&self) -> Option { + self.endpoint.peer_silent_for(&self.conn_id) + } + fn create_framed(stream: stream::KcpStream, local_addr: Option) -> Stream { Stream::Tcp(FramedStream( tokio_util::codec::Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), @@ -77,7 +86,8 @@ impl KcpStream { if let Some(stream) = stream::KcpStream::new(&endpoint, conn_id) { Ok(( Self { - _endpoint: endpoint, + endpoint, + conn_id, stop_sender: Some(stop_sender), }, Self::create_framed(stream, udp_socket.local_addr().ok()), @@ -108,7 +118,8 @@ impl KcpStream { if let Some(stream) = stream::KcpStream::new(&endpoint, conn_id) { Ok(( Self { - _endpoint: endpoint, + endpoint, + conn_id, stop_sender: Some(stop_sender), }, Self::create_framed(stream, udp_socket.local_addr().ok()),