Suppress stale terminal mouse reports after 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:
+71
-2
@@ -63,6 +63,8 @@ const STREAM_INITIAL_WINDOW: usize = 1024 * 1024;
|
||||
const MAX_PENDING_USER_INPUT_BYTES: usize = 1024 * 1024;
|
||||
const STARTUP_INPUT_HOLD: Duration = Duration::from_millis(750);
|
||||
const POST_SUBMIT_ALL_INPUT_HOLD: Duration = Duration::from_millis(120);
|
||||
const STALE_TERMINAL_INPUT_AFTER: Duration = Duration::from_secs(2);
|
||||
const POST_RECONNECT_STALE_INPUT_GRACE: Duration = Duration::from_secs(2);
|
||||
|
||||
/// Sentinel `target_host` the server uses on a server-initiated `StreamOpen` that
|
||||
/// represents an SSH-agent connection (rather than a TCP target). The client
|
||||
@@ -4376,6 +4378,7 @@ async fn run_terminal(
|
||||
let mut pending_user_input_bytes = 0usize;
|
||||
let mut startup_input_hold_until: Option<Instant> = None;
|
||||
let mut startup_gate_mode = StartupGateMode::HoldControl;
|
||||
let mut stale_terminal_input_suppress_until: Option<Instant> = None;
|
||||
if let Some(frame) = first_frame {
|
||||
if !forward_only {
|
||||
render_frame(&frame)?;
|
||||
@@ -4433,10 +4436,19 @@ async fn run_terminal(
|
||||
biased;
|
||||
stdin_msg = stdin_rx.recv() => {
|
||||
match stdin_msg {
|
||||
Some(bytes) => {
|
||||
Some(mut bytes) => {
|
||||
if input_matches_escape(&bytes, escape_key.as_deref()) {
|
||||
break;
|
||||
}
|
||||
if should_strip_stale_terminal_reports(
|
||||
last_packet_at.elapsed(),
|
||||
stale_terminal_input_suppress_until,
|
||||
) {
|
||||
bytes = strip_stale_mouse_reports(&bytes);
|
||||
if bytes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if cred.mode != "view-only" && cred.mode != "forward-only" {
|
||||
if let Some(deadline) = startup_input_hold_until {
|
||||
if !predictor.alternate_screen
|
||||
@@ -4535,6 +4547,9 @@ async fn run_terminal(
|
||||
.await?
|
||||
{
|
||||
refresh_live_addr(&mut addr, &cred)?;
|
||||
arm_stale_terminal_input_suppression(
|
||||
&mut stale_terminal_input_suppress_until,
|
||||
);
|
||||
if !forward_only {
|
||||
render_frame(&frame)?;
|
||||
predictor.observe_output(&frame.bytes);
|
||||
@@ -4594,6 +4609,9 @@ async fn run_terminal(
|
||||
.await?
|
||||
{
|
||||
refresh_live_addr(&mut addr, &cred)?;
|
||||
arm_stale_terminal_input_suppression(
|
||||
&mut stale_terminal_input_suppress_until,
|
||||
);
|
||||
if !forward_only {
|
||||
render_frame(&frame)?;
|
||||
predictor.observe_output(&frame.bytes);
|
||||
@@ -4644,6 +4662,9 @@ async fn run_terminal(
|
||||
.await?
|
||||
{
|
||||
refresh_live_addr(&mut addr, &cred)?;
|
||||
arm_stale_terminal_input_suppression(
|
||||
&mut stale_terminal_input_suppress_until,
|
||||
);
|
||||
if !forward_only {
|
||||
render_frame(&frame)?;
|
||||
predictor.observe_output(&frame.bytes);
|
||||
@@ -4765,6 +4786,9 @@ async fn run_terminal(
|
||||
.await?
|
||||
{
|
||||
refresh_live_addr(&mut addr, &cred)?;
|
||||
arm_stale_terminal_input_suppression(
|
||||
&mut stale_terminal_input_suppress_until,
|
||||
);
|
||||
if !forward_only {
|
||||
render_frame(&frame)?;
|
||||
predictor.observe_output(&frame.bytes);
|
||||
@@ -5157,6 +5181,9 @@ async fn run_terminal(
|
||||
.await?
|
||||
{
|
||||
refresh_live_addr(&mut addr, &cred)?;
|
||||
arm_stale_terminal_input_suppression(
|
||||
&mut stale_terminal_input_suppress_until,
|
||||
);
|
||||
if !forward_only {
|
||||
render_frame(&frame)?;
|
||||
predictor.observe_output(&frame.bytes);
|
||||
@@ -5562,6 +5589,18 @@ fn should_flush_terminal_input_after_contact(
|
||||
!forward_only && !pending_empty && mode != "view-only" && mode != "forward-only"
|
||||
}
|
||||
|
||||
fn should_strip_stale_terminal_reports(
|
||||
silent_for: Duration,
|
||||
suppress_until: Option<Instant>,
|
||||
) -> bool {
|
||||
silent_for >= STALE_TERMINAL_INPUT_AFTER
|
||||
|| suppress_until.is_some_and(|deadline| Instant::now() < deadline)
|
||||
}
|
||||
|
||||
fn arm_stale_terminal_input_suppression(suppress_until: &mut Option<Instant>) {
|
||||
*suppress_until = Some(Instant::now() + POST_RECONNECT_STALE_INPUT_GRACE);
|
||||
}
|
||||
|
||||
async fn flush_pending_user_input(
|
||||
socket: &UdpSocket,
|
||||
addr: SocketAddr,
|
||||
@@ -5623,6 +5662,7 @@ fn stale_mouse_report_len(bytes: &[u8], offset: usize) -> Option<usize> {
|
||||
match bytes.get(offset).copied()? {
|
||||
0x1b if bytes.get(offset + 1) == Some(&b'[') => match bytes.get(offset + 2).copied() {
|
||||
Some(b'M') if offset + 6 <= bytes.len() => Some(6),
|
||||
Some(b'I' | b'O') => Some(3),
|
||||
Some(b'<') => mouse_report_params_len(bytes, offset + 3, 2).map(|len| len + 3),
|
||||
Some(byte) if byte.is_ascii_digit() => {
|
||||
mouse_report_params_len(bytes, offset + 2, 2).map(|len| len + 2)
|
||||
@@ -5631,13 +5671,15 @@ fn stale_mouse_report_len(bytes: &[u8], offset: usize) -> Option<usize> {
|
||||
},
|
||||
0x9b => match bytes.get(offset + 1).copied() {
|
||||
Some(b'M') if offset + 5 <= bytes.len() => Some(5),
|
||||
Some(b'I' | b'O') => Some(2),
|
||||
Some(b'<') => mouse_report_params_len(bytes, offset + 2, 2).map(|len| len + 2),
|
||||
Some(byte) if byte.is_ascii_digit() => {
|
||||
mouse_report_params_len(bytes, offset + 1, 2).map(|len| len + 1)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
byte if byte.is_ascii_digit() => mouse_report_params_len(bytes, offset, 1),
|
||||
byte if byte.is_ascii_digit() => mouse_report_params_len(bytes, offset, 1)
|
||||
.or_else(|| orphan_mouse_suffix_len(bytes, offset)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -5659,6 +5701,21 @@ fn mouse_report_params_len(bytes: &[u8], offset: usize, min_semicolons: usize) -
|
||||
None
|
||||
}
|
||||
|
||||
fn orphan_mouse_suffix_len(bytes: &[u8], offset: usize) -> Option<usize> {
|
||||
if offset != 0 {
|
||||
return None;
|
||||
}
|
||||
let mut cursor = offset;
|
||||
while let Some(byte) = bytes.get(cursor).copied() {
|
||||
match byte {
|
||||
b'0'..=b'9' => cursor += 1,
|
||||
b'M' | b'm' if cursor > offset => return Some(cursor + 1 - offset),
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn stale_mouse_report_maybe_incomplete(bytes: &[u8], offset: usize) -> bool {
|
||||
let Some(rest) = bytes.get(offset..) else {
|
||||
return false;
|
||||
@@ -8495,6 +8552,18 @@ mod tests {
|
||||
assert_eq!(strip_stale_mouse_reports(b"152;1Mls\r"), b"ls\r");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_stale_mouse_suffixes_are_stripped_from_pending_input() {
|
||||
assert_eq!(strip_stale_mouse_reports(b"1M35;149;1Mls\r"), b"ls\r");
|
||||
assert_eq!(strip_stale_mouse_reports(b"10m"), b"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_focus_reports_are_stripped_from_pending_input() {
|
||||
assert_eq!(strip_stale_mouse_reports(b"\x1b[Ihello\x1b[O"), b"hello");
|
||||
assert_eq!(strip_stale_mouse_reports(b"\x9bIhello\x9bO"), b"hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_mouse_escape_input_survives_stale_filter() {
|
||||
assert_eq!(strip_stale_mouse_reports(b"\x1b[A"), b"\x1b[A");
|
||||
|
||||
Reference in New Issue
Block a user