Stop idle keepalives from repainting terminals
ci / test (push) Canceled after 0s
ci / fuzz-smoke (push) Canceled after 0s
ci / macos-client (macos-aarch64, macos-14) (push) Canceled after 0s
ci / macos-client (macos-x86_64, macos-13) (push) Canceled after 0s
ci / windows-client (push) Canceled after 0s
ci / package-release (linux-x86_64, ubuntu-latest, , , ) (push) Canceled after 0s
ci / package-release (macos-aarch64, macos-14, , , ) (push) Canceled after 0s
ci / package-release (macos-x86_64, macos-13, , , ) (push) Canceled after 0s
ci / package-release (windows-aarch64, windows-latest, aarch64, windows, aarch64-pc-windows-msvc) (push) Canceled after 0s
ci / package-release (windows-x86_64, windows-latest, , , ) (push) Canceled after 0s
ci / remote-bench (push) Canceled after 0s
ci / publish-gitea-release (push) Canceled after 0s
ci / test (push) Canceled after 0s
ci / fuzz-smoke (push) Canceled after 0s
ci / macos-client (macos-aarch64, macos-14) (push) Canceled after 0s
ci / macos-client (macos-x86_64, macos-13) (push) Canceled after 0s
ci / windows-client (push) Canceled after 0s
ci / package-release (linux-x86_64, ubuntu-latest, , , ) (push) Canceled after 0s
ci / package-release (macos-aarch64, macos-14, , , ) (push) Canceled after 0s
ci / package-release (macos-x86_64, macos-13, , , ) (push) Canceled after 0s
ci / package-release (windows-aarch64, windows-latest, aarch64, windows, aarch64-pc-windows-msvc) (push) Canceled after 0s
ci / package-release (windows-x86_64, windows-latest, , , ) (push) Canceled after 0s
ci / remote-bench (push) Canceled after 0s
ci / publish-gitea-release (push) Canceled after 0s
This commit is contained in:
@@ -10676,9 +10676,12 @@ const TERMINAL_SNAPSHOT_RESET: &[u8] = concat!(
|
|||||||
.as_bytes();
|
.as_bytes();
|
||||||
|
|
||||||
/// Seconds of silence from the server before the disconnect status line appears.
|
/// Seconds of silence from the server before the disconnect status line appears.
|
||||||
/// Short enough to give quick feedback on a lost link, long enough that a normal
|
/// Short enough to give quick feedback on a lost link, but later than the first
|
||||||
/// idle period (the run loop only pings after 2s of quiet) never flashes it.
|
/// idle ping. Using the same threshold as the first ping races its Pong: the bar
|
||||||
const DISCONNECT_STATUS_THRESHOLD_SECS: u64 = 2;
|
/// is painted just before the authenticated reply arrives, which then requests a
|
||||||
|
/// snapshot to restore row 1 and turns every healthy idle connection into a
|
||||||
|
/// periodic reconnect loop.
|
||||||
|
const DISCONNECT_STATUS_THRESHOLD_SECS: u64 = 3;
|
||||||
|
|
||||||
/// Mosh-style disconnect status bar with snapshot-backed restoration.
|
/// Mosh-style disconnect status bar with snapshot-backed restoration.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ struct CachedCredentialWire {
|
|||||||
enum ServerObservation {
|
enum ServerObservation {
|
||||||
BulkSent,
|
BulkSent,
|
||||||
Input(Vec<u8>),
|
Input(Vec<u8>),
|
||||||
|
Ping,
|
||||||
Reconnected,
|
Reconnected,
|
||||||
RenderResynced,
|
RenderResynced,
|
||||||
Resize(u16, u16),
|
Resize(u16, u16),
|
||||||
@@ -300,6 +301,87 @@ fn idle_reconnect_restores_snapshot_and_orders_reordered_frames() {
|
|||||||
assert!(status.success(), "Dosh client exited with {status:?}");
|
assert!(status.success(), "Dosh client exited with {status:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn authenticated_idle_pongs_do_not_trigger_snapshot_reconnects() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let home = dir.path().join("home");
|
||||||
|
let cache = dir.path().join("credentials");
|
||||||
|
fs::create_dir_all(home.join(".config/dosh")).unwrap();
|
||||||
|
fs::create_dir_all(&cache).unwrap();
|
||||||
|
|
||||||
|
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||||
|
socket
|
||||||
|
.set_read_timeout(Some(Duration::from_millis(100)))
|
||||||
|
.unwrap();
|
||||||
|
let port = socket.local_addr().unwrap().port();
|
||||||
|
write_client_fixture(&home, &cache, port, 5);
|
||||||
|
let config_path = home.join(".config/dosh/client.toml");
|
||||||
|
let mut config: ClientConfig =
|
||||||
|
toml::from_str(&fs::read_to_string(&config_path).unwrap()).unwrap();
|
||||||
|
config.disconnect_status = true;
|
||||||
|
fs::write(&config_path, toml::to_string(&config).unwrap()).unwrap();
|
||||||
|
|
||||||
|
let (observation_tx, observation_rx) = mpsc::channel();
|
||||||
|
let server = thread::spawn(move || run_idle_keepalive_server(socket, observation_tx));
|
||||||
|
|
||||||
|
let pty = NativePtySystem::default();
|
||||||
|
let pair = pty
|
||||||
|
.openpty(PtySize {
|
||||||
|
rows: 24,
|
||||||
|
cols: 80,
|
||||||
|
pixel_width: 0,
|
||||||
|
pixel_height: 0,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let mut reader = pair.master.try_clone_reader().unwrap();
|
||||||
|
let mut command = client_command(dir.path(), port);
|
||||||
|
command.env("HOME", home.to_string_lossy().to_string());
|
||||||
|
command.env("USERPROFILE", home.to_string_lossy().to_string());
|
||||||
|
command.env("APPDATA", home.to_string_lossy().to_string());
|
||||||
|
command.env("LOCALAPPDATA", home.to_string_lossy().to_string());
|
||||||
|
command.env("TERM", "xterm-256color");
|
||||||
|
let mut child = pair.slave.spawn_command(command).unwrap();
|
||||||
|
drop(pair.slave);
|
||||||
|
|
||||||
|
let output = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let reader_output = Arc::clone(&output);
|
||||||
|
let reader_thread = thread::spawn(move || {
|
||||||
|
let mut buf = [0u8; 4096];
|
||||||
|
loop {
|
||||||
|
match reader.read(&mut buf) {
|
||||||
|
Ok(0) | Err(_) => break,
|
||||||
|
Ok(n) => reader_output.lock().unwrap().extend_from_slice(&buf[..n]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let status = child.wait().unwrap();
|
||||||
|
drop(pair.master);
|
||||||
|
reader_thread.join().unwrap();
|
||||||
|
server.join().unwrap();
|
||||||
|
assert!(status.success(), "Dosh client exited with {status:?}");
|
||||||
|
|
||||||
|
let observations: Vec<_> = observation_rx.try_iter().collect();
|
||||||
|
assert!(
|
||||||
|
observations
|
||||||
|
.iter()
|
||||||
|
.filter(|event| matches!(event, ServerObservation::Ping))
|
||||||
|
.count()
|
||||||
|
>= 2,
|
||||||
|
"idle session did not exercise repeated authenticated keepalives: {observations:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!observations
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event, ServerObservation::Reconnected)),
|
||||||
|
"healthy idle pongs caused a snapshot reconnect: {observations:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!contains(&output.lock().unwrap(), b"[dosh] reconnecting"),
|
||||||
|
"healthy idle session flashed the disconnect overlay"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn renderer_overflow_resyncs_without_dropping_the_session() {
|
fn renderer_overflow_resyncs_without_dropping_the_session() {
|
||||||
const PROBE: &[u8] = b"DOSH_OVERFLOW_INPUT\r";
|
const PROBE: &[u8] = b"DOSH_OVERFLOW_INPUT\r";
|
||||||
@@ -713,6 +795,87 @@ fn run_reconnect_server(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_idle_keepalive_server(socket: UdpSocket, observations: mpsc::Sender<ServerObservation>) {
|
||||||
|
let started = Instant::now();
|
||||||
|
let mut peer = None;
|
||||||
|
let mut attached = false;
|
||||||
|
let mut server_seq = 1u64;
|
||||||
|
let mut buf = [0u8; 65535];
|
||||||
|
loop {
|
||||||
|
if started.elapsed() >= Duration::from_secs(7)
|
||||||
|
&& let Some(source) = peer
|
||||||
|
{
|
||||||
|
server_seq += 1;
|
||||||
|
send_frame(
|
||||||
|
&socket,
|
||||||
|
source,
|
||||||
|
PacketKind::Frame,
|
||||||
|
server_seq,
|
||||||
|
11,
|
||||||
|
b"DOSH_IDLE_KEEPALIVE_DONE",
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (n, source) = match socket.recv_from(&mut buf) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(err)
|
||||||
|
if matches!(
|
||||||
|
err.kind(),
|
||||||
|
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(err) => panic!("idle keepalive server receive failed: {err}"),
|
||||||
|
};
|
||||||
|
let packet = protocol::decode(&buf[..n]).unwrap();
|
||||||
|
match packet.header.kind {
|
||||||
|
PacketKind::ResumeRequest => {
|
||||||
|
let plain =
|
||||||
|
protocol::decrypt_body(&packet, &SESSION_KEY, CLIENT_TO_SERVER).unwrap();
|
||||||
|
let request: protocol::ResumeRequest = protocol::from_body(&plain).unwrap();
|
||||||
|
assert_eq!(request.session, SESSION);
|
||||||
|
if attached {
|
||||||
|
observations.send(ServerObservation::Reconnected).unwrap();
|
||||||
|
}
|
||||||
|
attached = true;
|
||||||
|
peer = Some(source);
|
||||||
|
send_frame(
|
||||||
|
&socket,
|
||||||
|
source,
|
||||||
|
PacketKind::ResumeOk,
|
||||||
|
server_seq,
|
||||||
|
10,
|
||||||
|
b"DOSH_IDLE_KEEPALIVE_READY",
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
PacketKind::Ping => {
|
||||||
|
protocol::decrypt_body(&packet, &SESSION_KEY, CLIENT_TO_SERVER).unwrap();
|
||||||
|
observations.send(ServerObservation::Ping).unwrap();
|
||||||
|
server_seq += 1;
|
||||||
|
let pong = protocol::encode_encrypted(
|
||||||
|
PacketKind::Pong,
|
||||||
|
CLIENT_ID,
|
||||||
|
server_seq,
|
||||||
|
packet.header.seq,
|
||||||
|
&SESSION_KEY,
|
||||||
|
SERVER_TO_CLIENT,
|
||||||
|
b"",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
socket.send_to(&pong, source).unwrap();
|
||||||
|
}
|
||||||
|
PacketKind::Ack => {}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn run_overflow_server(
|
fn run_overflow_server(
|
||||||
socket: UdpSocket,
|
socket: UdpSocket,
|
||||||
observations: mpsc::Sender<ServerObservation>,
|
observations: mpsc::Sender<ServerObservation>,
|
||||||
|
|||||||
Reference in New Issue
Block a user