From 237ad52befe90514fe597005c57d4cd895c12570 Mon Sep 17 00:00:00 2001 From: DuProcess <273172371+DuProcess@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:35:22 -0400 Subject: [PATCH] Flush queued input after reconnect contact --- Cargo.lock | 2 +- Cargo.toml | 2 +- Makefile | 20 +++++++++- scripts/verify-release-artifacts.sh | 46 +++++++++++++++++++++ src/bin/dosh-client.rs | 62 ++++++++++++++++++++++++++--- 5 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 scripts/verify-release-artifacts.sh diff --git a/Cargo.lock b/Cargo.lock index fc712ae..028de54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,7 +436,7 @@ dependencies = [ [[package]] name = "dosh" -version = "0.1.16" +version = "0.1.17" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index 5da60d2..af493e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dosh" -version = "0.1.16" +version = "0.1.17" edition = "2024" license = "MIT" diff --git a/Makefile b/Makefile index ea20bdc..780a5f7 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build test fmt clippy release-check install package-release package-release-linux package-release-windows publish-release bench-report bench-local bench-local-json bench-docker-ssh bench-docker-mosh fuzz-smoke fuzz-deep soak-local tui-harness +.PHONY: build test fmt clippy release-check 1.0-check reconnect-check hostile-network-check persistence-check package-check install package-release package-release-linux package-release-windows publish-release bench-report bench-local bench-local-json bench-docker-ssh bench-docker-mosh fuzz-smoke fuzz-deep soak-local tui-harness build: cargo build --release @@ -19,6 +19,24 @@ release-check: cargo test $(MAKE) tui-harness +1.0-check: release-check reconnect-check hostile-network-check persistence-check package-check + +reconnect-check: + cargo test --test integration_smoke resume_updates_udp_endpoint_for_roaming -- --nocapture + DOSH_SOAK_SECONDS=$${DOSH_SOAK_SECONDS:-300} cargo test --test integration_smoke sleep_roaming_soak_30m -- --ignored --nocapture + +hostile-network-check: + cargo test --test hostile_network -- --nocapture + +persistence-check: + cargo test --test integration_smoke session_survives_server_restart_same_shell_and_screen -- --nocapture + cargo test --test integration_smoke multiple_persistent_named_sessions_survive_restart_independently -- --nocapture + +package-check: + $(MAKE) package-release-linux + $(MAKE) package-release-windows + sh scripts/verify-release-artifacts.sh + install: sh install.sh --from-current diff --git a/scripts/verify-release-artifacts.sh b/scripts/verify-release-artifacts.sh new file mode 100644 index 0000000..1e7201d --- /dev/null +++ b/scripts/verify-release-artifacts.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env sh +set -eu + +repo_root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" +cd "$repo_root" + +version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | sed -n '1p')" +out_dir="${DOSH_PACKAGE_DIR:-target/dosh-release}" + +artifact_version() { + artifact="$1" + case "$artifact" in + *.tar.gz) + tar -xOf "$artifact" dosh/VERSION 2>/dev/null | tr -d '\r\n' + ;; + *.zip) + unzip -p "$artifact" dosh/VERSION 2>/dev/null | tr -d '\r\n' + ;; + *) + return 1 + ;; + esac +} + +failed=0 +for artifact in \ + "$out_dir/dosh-linux-x86_64.tar.gz" \ + "$out_dir/dosh-macos-aarch64.tar.gz" \ + "$out_dir/dosh-windows-x86_64.zip"; do + if [ ! -f "$artifact" ]; then + echo "missing release artifact: $artifact" >&2 + failed=1 + continue + fi + if [ ! -f "$artifact.sha256" ]; then + echo "missing release checksum: $artifact.sha256" >&2 + failed=1 + fi + actual="$(artifact_version "$artifact" || true)" + if [ "$actual" != "$version" ]; then + echo "artifact version mismatch: $artifact has ${actual:-unknown}, expected $version" >&2 + failed=1 + fi +done + +exit "$failed" diff --git a/src/bin/dosh-client.rs b/src/bin/dosh-client.rs index 8855114..3e47aaf 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -4610,6 +4610,22 @@ async fn run_terminal( } PacketKind::Pong => { last_packet_at = Instant::now(); + if should_flush_terminal_input_after_contact( + forward_only, + &cred.mode, + pending_user_input.is_empty(), + ) { + flush_pending_user_input( + &socket, + addr, + &cred, + &mut send_seq, + &mut predictor, + &mut pending_user_input, + &mut pending_user_input_bytes, + ) + .await?; + } } PacketKind::Rekey => { // Server-initiated transport rekey (spec ยง11). The Rekey is @@ -5439,6 +5455,14 @@ fn should_hold_during_startup_gate( mode == StartupGateMode::HoldAll || already_queued || should_hold_post_submit_input(bytes) } +fn should_flush_terminal_input_after_contact( + forward_only: bool, + mode: &str, + pending_empty: bool, +) -> bool { + !forward_only && !pending_empty && mode != "view-only" && mode != "forward-only" +} + async fn flush_pending_user_input( socket: &UdpSocket, addr: SocketAddr, @@ -7095,11 +7119,11 @@ mod tests { release_tag_download_url, release_tag_from_effective_url, release_version_from_tag, render_status_clear, render_status_overlay, requested_env, resolved_startup_command, retransmit_stream_opens, rewrite_forward_command, selected_predict_mode, selected_udp_host, - server_version_mismatch, should_hold_during_startup_gate, should_hold_post_submit_input, - split_after_command_submit, ssh_destination_host, ssh_username, ssh_with_user, - startup_command, status_ssh_target, strip_stale_mouse_reports, toml_bare_key_or_quoted, - update_check_requested, update_version_status, upsert_managed_block, valid_forward_host, - vscode_safe_alias, + server_version_mismatch, should_flush_terminal_input_after_contact, + should_hold_during_startup_gate, should_hold_post_submit_input, split_after_command_submit, + ssh_destination_host, ssh_username, ssh_with_user, startup_command, status_ssh_target, + strip_stale_mouse_reports, toml_bare_key_or_quoted, update_check_requested, + update_version_status, upsert_managed_block, valid_forward_host, vscode_safe_alias, }; use dosh::config::{ClientConfig, CommandExtension, HostConfig}; use dosh::native::EnvVar; @@ -7879,6 +7903,34 @@ mod tests { assert_eq!(post_submit_hold_duration(b"x"), POST_SUBMIT_ALL_INPUT_HOLD); } + #[test] + fn contact_flushes_pending_terminal_input_for_interactive_sessions() { + assert!(should_flush_terminal_input_after_contact( + false, "normal", false + )); + assert!(should_flush_terminal_input_after_contact( + false, + "read-write", + false + )); + assert!(!should_flush_terminal_input_after_contact( + false, "normal", true + )); + assert!(!should_flush_terminal_input_after_contact( + true, "normal", false + )); + assert!(!should_flush_terminal_input_after_contact( + false, + "view-only", + false + )); + assert!(!should_flush_terminal_input_after_contact( + false, + "forward-only", + false + )); + } + #[test] fn reconnect_refreshes_live_send_address_from_credentials() { let mut addr = "127.0.0.1:50000".parse().unwrap();