Buffer input across reconnect
ci / test (push) Has been cancelled
ci / fuzz-smoke (push) Has been cancelled
ci / windows-client (push) Has been cancelled
ci / package-release (linux-x86_64, ubuntu-latest) (push) Has been cancelled
ci / package-release (macos-aarch64, macos-14) (push) Has been cancelled
ci / package-release (macos-x86_64, macos-13) (push) Has been cancelled
ci / package-release (windows-x86_64, windows-latest) (push) Has been cancelled
ci / remote-bench (push) Has been cancelled
ci / test (push) Has been cancelled
ci / fuzz-smoke (push) Has been cancelled
ci / windows-client (push) Has been cancelled
ci / package-release (linux-x86_64, ubuntu-latest) (push) Has been cancelled
ci / package-release (macos-aarch64, macos-14) (push) Has been cancelled
ci / package-release (macos-x86_64, macos-13) (push) Has been cancelled
ci / package-release (windows-x86_64, windows-latest) (push) Has been cancelled
ci / remote-bench (push) Has been cancelled
This commit is contained in:
+128
-8
@@ -46,6 +46,7 @@ use tokio::net::{TcpListener, TcpStream, UdpSocket};
|
|||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
const STREAM_INITIAL_WINDOW: usize = 1024 * 1024;
|
const STREAM_INITIAL_WINDOW: usize = 1024 * 1024;
|
||||||
|
const MAX_PENDING_USER_INPUT_BYTES: usize = 1024 * 1024;
|
||||||
|
|
||||||
/// Sentinel `target_host` the server uses on a server-initiated `StreamOpen` that
|
/// Sentinel `target_host` the server uses on a server-initiated `StreamOpen` that
|
||||||
/// represents an SSH-agent connection (rather than a TCP target). The client
|
/// represents an SSH-agent connection (rather than a TCP target). The client
|
||||||
@@ -2650,6 +2651,8 @@ async fn run_terminal(
|
|||||||
let mut stream_sent_data: HashMap<u64, BTreeMap<u64, PendingStreamChunk>> = HashMap::new();
|
let mut stream_sent_data: HashMap<u64, BTreeMap<u64, PendingStreamChunk>> = HashMap::new();
|
||||||
let mut stream_next_recv_offset: HashMap<u64, u64> = HashMap::new();
|
let mut stream_next_recv_offset: HashMap<u64, u64> = HashMap::new();
|
||||||
let mut stream_recv_pending: HashMap<u64, BTreeMap<u64, Vec<u8>>> = HashMap::new();
|
let mut stream_recv_pending: HashMap<u64, BTreeMap<u64, Vec<u8>>> = HashMap::new();
|
||||||
|
let mut pending_user_input: VecDeque<Vec<u8>> = VecDeque::new();
|
||||||
|
let mut pending_user_input_bytes = 0usize;
|
||||||
if let Some(frame) = first_frame {
|
if let Some(frame) = first_frame {
|
||||||
if !forward_only {
|
if !forward_only {
|
||||||
render_frame(&frame)?;
|
render_frame(&frame)?;
|
||||||
@@ -2704,10 +2707,44 @@ async fn run_terminal(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if cred.mode != "view-only" && cred.mode != "forward-only" {
|
if cred.mode != "view-only" && cred.mode != "forward-only" {
|
||||||
|
if last_packet_at.elapsed() >= Duration::from_secs(2) {
|
||||||
|
queue_pending_user_input(
|
||||||
|
&mut pending_user_input,
|
||||||
|
&mut pending_user_input_bytes,
|
||||||
|
bytes,
|
||||||
|
)?;
|
||||||
|
if let Some(frame) = reconnect(
|
||||||
|
&socket,
|
||||||
|
&mut cred,
|
||||||
|
&mut send_seq,
|
||||||
|
last_size,
|
||||||
|
&mut frame_buffer,
|
||||||
|
&mut predictor,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
if !forward_only {
|
||||||
|
render_frame(&frame)?;
|
||||||
|
predictor.observe_output(&frame.bytes);
|
||||||
|
}
|
||||||
|
last_packet_at = Instant::now();
|
||||||
|
flush_pending_user_input(
|
||||||
|
&socket,
|
||||||
|
addr,
|
||||||
|
&cred,
|
||||||
|
&mut send_seq,
|
||||||
|
&mut predictor,
|
||||||
|
&mut pending_user_input,
|
||||||
|
&mut pending_user_input_bytes,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
predictor.observe_input(&bytes)?;
|
predictor.observe_input(&bytes)?;
|
||||||
send_input(&socket, addr, &cred, &mut send_seq, bytes).await?;
|
send_input(&socket, addr, &cred, &mut send_seq, bytes).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
None => break,
|
None => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2762,6 +2799,16 @@ async fn run_terminal(
|
|||||||
predictor.observe_output(&frame.bytes);
|
predictor.observe_output(&frame.bytes);
|
||||||
}
|
}
|
||||||
last_packet_at = Instant::now();
|
last_packet_at = Instant::now();
|
||||||
|
flush_pending_user_input(
|
||||||
|
&socket,
|
||||||
|
addr,
|
||||||
|
&cred,
|
||||||
|
&mut send_seq,
|
||||||
|
&mut predictor,
|
||||||
|
&mut pending_user_input,
|
||||||
|
&mut pending_user_input_bytes,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -2846,6 +2893,16 @@ async fn run_terminal(
|
|||||||
predictor.observe_output(&frame.bytes);
|
predictor.observe_output(&frame.bytes);
|
||||||
}
|
}
|
||||||
last_packet_at = Instant::now();
|
last_packet_at = Instant::now();
|
||||||
|
flush_pending_user_input(
|
||||||
|
&socket,
|
||||||
|
addr,
|
||||||
|
&cred,
|
||||||
|
&mut send_seq,
|
||||||
|
&mut predictor,
|
||||||
|
&mut pending_user_input,
|
||||||
|
&mut pending_user_input_bytes,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow!("server rejected session: {}", reject.reason));
|
return Err(anyhow!("server rejected session: {}", reject.reason));
|
||||||
@@ -3206,6 +3263,16 @@ async fn run_terminal(
|
|||||||
predictor.observe_output(&frame.bytes);
|
predictor.observe_output(&frame.bytes);
|
||||||
}
|
}
|
||||||
last_packet_at = Instant::now();
|
last_packet_at = Instant::now();
|
||||||
|
flush_pending_user_input(
|
||||||
|
&socket,
|
||||||
|
addr,
|
||||||
|
&cred,
|
||||||
|
&mut send_seq,
|
||||||
|
&mut predictor,
|
||||||
|
&mut pending_user_input,
|
||||||
|
&mut pending_user_input_bytes,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
} else if stale >= Duration::from_secs(2) {
|
} else if stale >= Duration::from_secs(2) {
|
||||||
let packet = protocol::encode_encrypted(
|
let packet = protocol::encode_encrypted(
|
||||||
@@ -3469,6 +3536,40 @@ async fn reconnect(
|
|||||||
Ok(Some(frame))
|
Ok(Some(frame))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn queue_pending_user_input(
|
||||||
|
pending: &mut VecDeque<Vec<u8>>,
|
||||||
|
pending_bytes: &mut usize,
|
||||||
|
bytes: Vec<u8>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let next = pending_bytes
|
||||||
|
.checked_add(bytes.len())
|
||||||
|
.ok_or_else(|| anyhow!("pending input buffer overflow"))?;
|
||||||
|
anyhow::ensure!(
|
||||||
|
next <= MAX_PENDING_USER_INPUT_BYTES,
|
||||||
|
"pending input buffer full while disconnected"
|
||||||
|
);
|
||||||
|
*pending_bytes = next;
|
||||||
|
pending.push_back(bytes);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn flush_pending_user_input(
|
||||||
|
socket: &UdpSocket,
|
||||||
|
addr: SocketAddr,
|
||||||
|
cred: &CachedCredential,
|
||||||
|
send_seq: &mut u64,
|
||||||
|
predictor: &mut Predictor,
|
||||||
|
pending: &mut VecDeque<Vec<u8>>,
|
||||||
|
pending_bytes: &mut usize,
|
||||||
|
) -> Result<()> {
|
||||||
|
while let Some(bytes) = pending.pop_front() {
|
||||||
|
*pending_bytes = pending_bytes.saturating_sub(bytes.len());
|
||||||
|
predictor.observe_input(&bytes)?;
|
||||||
|
send_input(socket, addr, cred, send_seq, bytes).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn send_input(
|
async fn send_input(
|
||||||
socket: &UdpSocket,
|
socket: &UdpSocket,
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
@@ -4766,19 +4867,21 @@ const TERMINAL_CLEANUP: &[u8] = concat!(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
DisconnectStatus, DynamicForward, FrameBuffer, LocalForward, PredictMode, Predictor,
|
DisconnectStatus, DynamicForward, FrameBuffer, LocalForward, MAX_PENDING_USER_INPUT_BYTES,
|
||||||
RemoteForward, SshConfig, StatusAction, auth_allows, cache_key, cache_server_prefix,
|
PredictMode, Predictor, RemoteForward, SshConfig, StatusAction, auth_allows, cache_key,
|
||||||
clear_cached_credentials, ensure_tui_safe_status_overlay, latest_release_download_url,
|
cache_server_prefix, clear_cached_credentials, ensure_tui_safe_status_overlay,
|
||||||
load_first_native_identity_with_prompt, parse_dynamic_forward, parse_local_forward,
|
latest_release_download_url, load_first_native_identity_with_prompt, parse_dynamic_forward,
|
||||||
parse_remote_forward, parse_ssh_config, raw_contains_host_table, recv_response_until,
|
parse_local_forward, parse_remote_forward, parse_ssh_config, queue_pending_user_input,
|
||||||
render_status_clear, render_status_overlay, requested_env, resolved_startup_command,
|
raw_contains_host_table, recv_response_until, render_status_clear, render_status_overlay,
|
||||||
rewrite_forward_command, ssh_destination_host, ssh_username, ssh_with_user,
|
requested_env, resolved_startup_command, rewrite_forward_command, ssh_destination_host,
|
||||||
startup_command, toml_bare_key_or_quoted, update_check_requested, valid_forward_host,
|
ssh_username, ssh_with_user, startup_command, toml_bare_key_or_quoted,
|
||||||
|
update_check_requested, valid_forward_host,
|
||||||
};
|
};
|
||||||
use dosh::config::{ClientConfig, CommandExtension, HostConfig};
|
use dosh::config::{ClientConfig, CommandExtension, HostConfig};
|
||||||
use dosh::native::EnvVar;
|
use dosh::native::EnvVar;
|
||||||
use dosh::protocol::{self, Frame, PacketKind};
|
use dosh::protocol::{self, Frame, PacketKind};
|
||||||
use ed25519_dalek::{SigningKey, VerifyingKey};
|
use ed25519_dalek::{SigningKey, VerifyingKey};
|
||||||
|
use std::collections::VecDeque;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -5579,6 +5682,23 @@ mod tests {
|
|||||||
assert!(similar.exists());
|
assert!(similar.exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pending_user_input_is_bounded_and_ordered() {
|
||||||
|
let mut pending = VecDeque::new();
|
||||||
|
let mut pending_bytes = 0usize;
|
||||||
|
queue_pending_user_input(&mut pending, &mut pending_bytes, b"abc".to_vec()).unwrap();
|
||||||
|
queue_pending_user_input(&mut pending, &mut pending_bytes, b"def".to_vec()).unwrap();
|
||||||
|
assert_eq!(pending_bytes, 6);
|
||||||
|
assert_eq!(pending.pop_front().unwrap(), b"abc");
|
||||||
|
assert_eq!(pending.pop_front().unwrap(), b"def");
|
||||||
|
|
||||||
|
let mut pending = VecDeque::new();
|
||||||
|
let mut pending_bytes = MAX_PENDING_USER_INPUT_BYTES;
|
||||||
|
assert!(queue_pending_user_input(&mut pending, &mut pending_bytes, b"x".to_vec()).is_err());
|
||||||
|
assert!(pending.is_empty());
|
||||||
|
assert_eq!(pending_bytes, MAX_PENDING_USER_INPUT_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Item 1: disconnect status line state machine ---
|
// --- Item 1: disconnect status line state machine ---
|
||||||
|
|
||||||
/// The status line stays hidden while the link is fresh and only appears once
|
/// The status line stays hidden while the link is fresh and only appears once
|
||||||
|
|||||||
Reference in New Issue
Block a user