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 MAX_PENDING_USER_INPUT_BYTES: usize = 1024 * 1024;
|
||||||
const STARTUP_INPUT_HOLD: Duration = Duration::from_millis(750);
|
const STARTUP_INPUT_HOLD: Duration = Duration::from_millis(750);
|
||||||
const POST_SUBMIT_ALL_INPUT_HOLD: Duration = Duration::from_millis(120);
|
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
|
/// 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
|
||||||
@@ -4376,6 +4378,7 @@ async fn run_terminal(
|
|||||||
let mut pending_user_input_bytes = 0usize;
|
let mut pending_user_input_bytes = 0usize;
|
||||||
let mut startup_input_hold_until: Option<Instant> = None;
|
let mut startup_input_hold_until: Option<Instant> = None;
|
||||||
let mut startup_gate_mode = StartupGateMode::HoldControl;
|
let mut startup_gate_mode = StartupGateMode::HoldControl;
|
||||||
|
let mut stale_terminal_input_suppress_until: Option<Instant> = None;
|
||||||
if let Some(frame) = first_frame {
|
if let Some(frame) = first_frame {
|
||||||
if !forward_only {
|
if !forward_only {
|
||||||
render_frame(&frame)?;
|
render_frame(&frame)?;
|
||||||
@@ -4433,10 +4436,19 @@ async fn run_terminal(
|
|||||||
biased;
|
biased;
|
||||||
stdin_msg = stdin_rx.recv() => {
|
stdin_msg = stdin_rx.recv() => {
|
||||||
match stdin_msg {
|
match stdin_msg {
|
||||||
Some(bytes) => {
|
Some(mut bytes) => {
|
||||||
if input_matches_escape(&bytes, escape_key.as_deref()) {
|
if input_matches_escape(&bytes, escape_key.as_deref()) {
|
||||||
break;
|
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 cred.mode != "view-only" && cred.mode != "forward-only" {
|
||||||
if let Some(deadline) = startup_input_hold_until {
|
if let Some(deadline) = startup_input_hold_until {
|
||||||
if !predictor.alternate_screen
|
if !predictor.alternate_screen
|
||||||
@@ -4535,6 +4547,9 @@ async fn run_terminal(
|
|||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
refresh_live_addr(&mut addr, &cred)?;
|
refresh_live_addr(&mut addr, &cred)?;
|
||||||
|
arm_stale_terminal_input_suppression(
|
||||||
|
&mut stale_terminal_input_suppress_until,
|
||||||
|
);
|
||||||
if !forward_only {
|
if !forward_only {
|
||||||
render_frame(&frame)?;
|
render_frame(&frame)?;
|
||||||
predictor.observe_output(&frame.bytes);
|
predictor.observe_output(&frame.bytes);
|
||||||
@@ -4594,6 +4609,9 @@ async fn run_terminal(
|
|||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
refresh_live_addr(&mut addr, &cred)?;
|
refresh_live_addr(&mut addr, &cred)?;
|
||||||
|
arm_stale_terminal_input_suppression(
|
||||||
|
&mut stale_terminal_input_suppress_until,
|
||||||
|
);
|
||||||
if !forward_only {
|
if !forward_only {
|
||||||
render_frame(&frame)?;
|
render_frame(&frame)?;
|
||||||
predictor.observe_output(&frame.bytes);
|
predictor.observe_output(&frame.bytes);
|
||||||
@@ -4644,6 +4662,9 @@ async fn run_terminal(
|
|||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
refresh_live_addr(&mut addr, &cred)?;
|
refresh_live_addr(&mut addr, &cred)?;
|
||||||
|
arm_stale_terminal_input_suppression(
|
||||||
|
&mut stale_terminal_input_suppress_until,
|
||||||
|
);
|
||||||
if !forward_only {
|
if !forward_only {
|
||||||
render_frame(&frame)?;
|
render_frame(&frame)?;
|
||||||
predictor.observe_output(&frame.bytes);
|
predictor.observe_output(&frame.bytes);
|
||||||
@@ -4765,6 +4786,9 @@ async fn run_terminal(
|
|||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
refresh_live_addr(&mut addr, &cred)?;
|
refresh_live_addr(&mut addr, &cred)?;
|
||||||
|
arm_stale_terminal_input_suppression(
|
||||||
|
&mut stale_terminal_input_suppress_until,
|
||||||
|
);
|
||||||
if !forward_only {
|
if !forward_only {
|
||||||
render_frame(&frame)?;
|
render_frame(&frame)?;
|
||||||
predictor.observe_output(&frame.bytes);
|
predictor.observe_output(&frame.bytes);
|
||||||
@@ -5157,6 +5181,9 @@ async fn run_terminal(
|
|||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
refresh_live_addr(&mut addr, &cred)?;
|
refresh_live_addr(&mut addr, &cred)?;
|
||||||
|
arm_stale_terminal_input_suppression(
|
||||||
|
&mut stale_terminal_input_suppress_until,
|
||||||
|
);
|
||||||
if !forward_only {
|
if !forward_only {
|
||||||
render_frame(&frame)?;
|
render_frame(&frame)?;
|
||||||
predictor.observe_output(&frame.bytes);
|
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"
|
!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(
|
async fn flush_pending_user_input(
|
||||||
socket: &UdpSocket,
|
socket: &UdpSocket,
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
@@ -5623,6 +5662,7 @@ fn stale_mouse_report_len(bytes: &[u8], offset: usize) -> Option<usize> {
|
|||||||
match bytes.get(offset).copied()? {
|
match bytes.get(offset).copied()? {
|
||||||
0x1b if bytes.get(offset + 1) == Some(&b'[') => match bytes.get(offset + 2).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'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(b'<') => mouse_report_params_len(bytes, offset + 3, 2).map(|len| len + 3),
|
||||||
Some(byte) if byte.is_ascii_digit() => {
|
Some(byte) if byte.is_ascii_digit() => {
|
||||||
mouse_report_params_len(bytes, offset + 2, 2).map(|len| len + 2)
|
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() {
|
0x9b => match bytes.get(offset + 1).copied() {
|
||||||
Some(b'M') if offset + 5 <= bytes.len() => Some(5),
|
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(b'<') => mouse_report_params_len(bytes, offset + 2, 2).map(|len| len + 2),
|
||||||
Some(byte) if byte.is_ascii_digit() => {
|
Some(byte) if byte.is_ascii_digit() => {
|
||||||
mouse_report_params_len(bytes, offset + 1, 2).map(|len| len + 1)
|
mouse_report_params_len(bytes, offset + 1, 2).map(|len| len + 1)
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => 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,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5659,6 +5701,21 @@ fn mouse_report_params_len(bytes: &[u8], offset: usize, min_semicolons: usize) -
|
|||||||
None
|
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 {
|
fn stale_mouse_report_maybe_incomplete(bytes: &[u8], offset: usize) -> bool {
|
||||||
let Some(rest) = bytes.get(offset..) else {
|
let Some(rest) = bytes.get(offset..) else {
|
||||||
return false;
|
return false;
|
||||||
@@ -8495,6 +8552,18 @@ mod tests {
|
|||||||
assert_eq!(strip_stale_mouse_reports(b"152;1Mls\r"), b"ls\r");
|
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]
|
#[test]
|
||||||
fn non_mouse_escape_input_survives_stale_filter() {
|
fn non_mouse_escape_input_survives_stale_filter() {
|
||||||
assert_eq!(strip_stale_mouse_reports(b"\x1b[A"), b"\x1b[A");
|
assert_eq!(strip_stale_mouse_reports(b"\x1b[A"), b"\x1b[A");
|
||||||
|
|||||||
Reference in New Issue
Block a user