From e176147c65dc1990d1846dec8ab2b99cedde507b Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 11 Jun 2026 10:18:09 -0400 Subject: [PATCH] Fix terminal resize handling and cached updates --- install.sh | 31 ++++++++++++++++--- src/bin/dosh-client.rs | 69 +++++++++++++++++++++++++++++++++++++++--- src/bin/dosh-server.rs | 12 ++++++++ 3 files changed, 103 insertions(+), 9 deletions(-) diff --git a/install.sh b/install.sh index f89e128..8d8ffc8 100755 --- a/install.sh +++ b/install.sh @@ -10,6 +10,7 @@ prefix="${PREFIX:-$HOME/.local}" from_current=0 start_server=1 force_config=0 +update_cache="${DOSH_UPDATE_CACHE:-$HOME/.cache/dosh/source}" usage() { cat <<'EOF' @@ -22,11 +23,14 @@ Options: --dosh-host HOST UDP host for Dosh packets when different from SSH target --port PORT Dosh UDP port; default 50000 --prefix DIR Install prefix; default ~/.local + --update-cache DIR + Persistent source checkout for incremental updates --no-start Install server but do not start it --force-config Rewrite existing ~/.config/dosh/*.toml files Environment alternatives: - DOSH_REPO, DOSH_ROLE, DOSH_SERVER, DOSH_HOST, DOSH_PORT, PREFIX + DOSH_REPO, DOSH_ROLE, DOSH_SERVER, DOSH_HOST, DOSH_PORT, PREFIX, + DOSH_UPDATE_CACHE EOF } @@ -55,6 +59,10 @@ while [ "$#" -gt 0 ]; do prefix="$2" shift ;; + --update-cache) + update_cache="$2" + shift + ;; --from-current) from_current=1 ;; @@ -119,15 +127,28 @@ else echo "DOSH_REPO or --repo is required when running the installer from curl" >&2 exit 2 fi + case "$update_cache" in + ""|"/"|"$HOME"|"$HOME/"|"$HOME/.cache") + echo "refusing unsafe update cache path: $update_cache" >&2 + exit 2 + ;; + esac need git - tmpdir="$(mktemp -d)" - git clone --depth 1 "$repo" "$tmpdir/dosh" >/dev/null - src_dir="$tmpdir/dosh" + mkdir -p "$(dirname "$update_cache")" + if [ -d "$update_cache/.git" ]; then + git -C "$update_cache" remote set-url origin "$repo" + git -C "$update_cache" fetch --depth 1 origin main + git -C "$update_cache" checkout -q -B main FETCH_HEAD + else + rm -rf "$update_cache" + git clone --depth 1 --branch main "$repo" "$update_cache" >/dev/null + fi + src_dir="$update_cache" fi cd "$src_dir" if [ "$role" = "client" ]; then - cargo build --release --bin dosh-client --bin dosh-bench + cargo build --release --bin dosh-client else cargo build --release fi diff --git a/src/bin/dosh-client.rs b/src/bin/dosh-client.rs index 59769bb..e25eba5 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -9,7 +9,7 @@ use dosh::auth::{ use dosh::config::{expand_tilde, load_client_config, load_server_config}; use dosh::crypto; use dosh::protocol::{ - self, AttachOk, BootstrapAttachRequest, CLIENT_TO_SERVER, Frame, Input, PacketKind, + self, AttachOk, BootstrapAttachRequest, CLIENT_TO_SERVER, Frame, Input, PacketKind, Resize, ResumeRequest, SERVER_TO_CLIENT, TicketAttachBody, TicketAttachEnvelope, TicketAttachOkEnvelope, }; @@ -218,11 +218,16 @@ fn run_update(config: &dosh::config::ClientConfig) -> Result<()> { .unwrap_or_else(|| "https://git.palav.dev/Palav/dosh.git".to_string()); let raw_base = raw_base_from_repo(&repo); let installer = format!("{raw_base}/raw/branch/main/install.sh"); + let update_cache = dirs::cache_dir() + .unwrap_or_else(|| expand_tilde("~/.cache")) + .join("dosh") + .join("source"); let mut script = format!( - "curl -fsSL {} | DOSH_REPO={} DOSH_PORT={} sh -s -- client", + "curl -fsSL {} | DOSH_REPO={} DOSH_PORT={} DOSH_UPDATE_CACHE={} sh -s -- client", shell_word(&installer), shell_word(&repo), - config.update_port.unwrap_or(config.dosh_port) + config.update_port.unwrap_or(config.dosh_port), + shell_word(&update_cache.display().to_string()) ); if config.server != "user@example.com" { script.push_str(&format!(" --server {}", shell_word(&config.server))); @@ -230,7 +235,6 @@ fn run_update(config: &dosh::config::ClientConfig) -> Result<()> { if let Some(host) = &config.dosh_host { script.push_str(&format!(" --dosh-host {}", shell_word(host))); } - script.push_str(" --force-config"); let status = Command::new("sh") .arg("-lc") .arg(script) @@ -549,6 +553,8 @@ async fn run_terminal( let mut last_packet_at = Instant::now(); let mut status_visible = false; let mut status_tick = tokio::time::interval(Duration::from_secs(1)); + let mut resize_tick = tokio::time::interval(Duration::from_millis(250)); + let mut last_size = size().unwrap_or((80, 24)); if let Some(frame) = first_frame { render_frame(&frame)?; if frame.closed { @@ -597,6 +603,16 @@ async fn run_terminal( socket.send_to(&packet, addr).await?; } } + _ = resize_tick.tick() => { + if let Ok((cols, rows)) = size() + && (cols, rows) != last_size + { + last_size = (cols, rows); + if cred.mode != "view-only" { + send_resize(&socket, addr, &cred, &mut send_seq, cols, rows).await?; + } + } + } recv = socket.recv_from(&mut recv_buf) => { let (n, _) = recv?; last_packet_at = Instant::now(); @@ -647,6 +663,29 @@ async fn run_terminal( Ok(()) } +async fn send_resize( + socket: &UdpSocket, + addr: SocketAddr, + cred: &CachedCredential, + send_seq: &mut u64, + cols: u16, + rows: u16, +) -> Result<()> { + let body = protocol::to_body(&Resize { cols, rows })?; + let packet = protocol::encode_encrypted( + PacketKind::Resize, + cred.client_id, + *send_seq, + cred.last_rendered_seq, + &cred.session_key, + CLIENT_TO_SERVER, + &body, + )?; + *send_seq += 1; + socket.send_to(&packet, addr).await?; + Ok(()) +} + async fn send_ack( socket: &UdpSocket, addr: SocketAddr, @@ -733,12 +772,34 @@ struct RawMode; impl RawMode { fn enter() -> Result { enable_raw_mode()?; + let mut stdout = std::io::stdout(); + stdout.write_all(TERMINAL_CLEANUP)?; + stdout.flush()?; Ok(Self) } } impl Drop for RawMode { fn drop(&mut self) { + let mut stdout = std::io::stdout(); + let _ = stdout.write_all(TERMINAL_CLEANUP); + let _ = stdout.flush(); let _ = disable_raw_mode(); } } + +const TERMINAL_CLEANUP: &[u8] = concat!( + "\x1b[?1l", + "\x1b[0m", + "\x1b[?25h", + "\x1b[?1003l", + "\x1b[?1002l", + "\x1b[?1001l", + "\x1b[?1000l", + "\x1b[?1015l", + "\x1b[?1006l", + "\x1b[?1005l", + "\x1b[?2004l", + "\x1b[?1049l" +) +.as_bytes(); diff --git a/src/bin/dosh-server.rs b/src/bin/dosh-server.rs index a7fda72..9ecf651 100644 --- a/src/bin/dosh-server.rs +++ b/src/bin/dosh-server.rs @@ -258,6 +258,10 @@ async fn handle_bootstrap_attach( .sessions .get_mut(&req.bootstrap.session) .expect("session exists"); + if req.bootstrap.mode != "view-only" { + session.pty.resize(req.cols, req.rows)?; + session.parser.set_size(req.rows, req.cols); + } let client_id = crypto::random_16(); let snapshot = session.parser.screen().state_formatted(); let screen = session.parser.screen().clone(); @@ -362,6 +366,10 @@ async fn handle_ticket_attach( .sessions .get_mut(&req.session) .expect("session exists"); + if req.mode != "view-only" { + session.pty.resize(req.cols, req.rows)?; + session.parser.set_size(req.rows, req.cols); + } let client_id = crypto::random_16(); let snapshot = session.parser.screen().state_formatted(); let screen = session.parser.screen().clone(); @@ -456,6 +464,10 @@ async fn handle_resume( client.rows = req.rows; client.last_seen = Instant::now(); client.send_seq += 1; + if client.mode != "view-only" { + session.pty.resize(req.cols, req.rows)?; + session.parser.set_size(req.rows, req.cols); + } let snapshot = session.parser.screen().state_formatted(); client.last_screen = Some(session.parser.screen().clone()); (client.send_seq, session.output_seq, snapshot)