Fix terminal resize handling and cached updates
ci / test (push) Has been cancelled
ci / remote-bench (push) Has been cancelled

This commit is contained in:
Codex
2026-06-11 10:18:09 -04:00
parent 66c4c97c9d
commit e176147c65
3 changed files with 103 additions and 9 deletions
+26 -5
View File
@@ -10,6 +10,7 @@ prefix="${PREFIX:-$HOME/.local}"
from_current=0 from_current=0
start_server=1 start_server=1
force_config=0 force_config=0
update_cache="${DOSH_UPDATE_CACHE:-$HOME/.cache/dosh/source}"
usage() { usage() {
cat <<'EOF' cat <<'EOF'
@@ -22,11 +23,14 @@ Options:
--dosh-host HOST UDP host for Dosh packets when different from SSH target --dosh-host HOST UDP host for Dosh packets when different from SSH target
--port PORT Dosh UDP port; default 50000 --port PORT Dosh UDP port; default 50000
--prefix DIR Install prefix; default ~/.local --prefix DIR Install prefix; default ~/.local
--update-cache DIR
Persistent source checkout for incremental updates
--no-start Install server but do not start it --no-start Install server but do not start it
--force-config Rewrite existing ~/.config/dosh/*.toml files --force-config Rewrite existing ~/.config/dosh/*.toml files
Environment alternatives: 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 EOF
} }
@@ -55,6 +59,10 @@ while [ "$#" -gt 0 ]; do
prefix="$2" prefix="$2"
shift shift
;; ;;
--update-cache)
update_cache="$2"
shift
;;
--from-current) --from-current)
from_current=1 from_current=1
;; ;;
@@ -119,15 +127,28 @@ else
echo "DOSH_REPO or --repo is required when running the installer from curl" >&2 echo "DOSH_REPO or --repo is required when running the installer from curl" >&2
exit 2 exit 2
fi fi
case "$update_cache" in
""|"/"|"$HOME"|"$HOME/"|"$HOME/.cache")
echo "refusing unsafe update cache path: $update_cache" >&2
exit 2
;;
esac
need git need git
tmpdir="$(mktemp -d)" mkdir -p "$(dirname "$update_cache")"
git clone --depth 1 "$repo" "$tmpdir/dosh" >/dev/null if [ -d "$update_cache/.git" ]; then
src_dir="$tmpdir/dosh" 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 fi
cd "$src_dir" cd "$src_dir"
if [ "$role" = "client" ]; then if [ "$role" = "client" ]; then
cargo build --release --bin dosh-client --bin dosh-bench cargo build --release --bin dosh-client
else else
cargo build --release cargo build --release
fi fi
+65 -4
View File
@@ -9,7 +9,7 @@ use dosh::auth::{
use dosh::config::{expand_tilde, load_client_config, load_server_config}; use dosh::config::{expand_tilde, load_client_config, load_server_config};
use dosh::crypto; use dosh::crypto;
use dosh::protocol::{ 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, ResumeRequest, SERVER_TO_CLIENT, TicketAttachBody, TicketAttachEnvelope,
TicketAttachOkEnvelope, 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()); .unwrap_or_else(|| "https://git.palav.dev/Palav/dosh.git".to_string());
let raw_base = raw_base_from_repo(&repo); let raw_base = raw_base_from_repo(&repo);
let installer = format!("{raw_base}/raw/branch/main/install.sh"); 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!( 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(&installer),
shell_word(&repo), 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" { if config.server != "user@example.com" {
script.push_str(&format!(" --server {}", shell_word(&config.server))); 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 { if let Some(host) = &config.dosh_host {
script.push_str(&format!(" --dosh-host {}", shell_word(host))); script.push_str(&format!(" --dosh-host {}", shell_word(host)));
} }
script.push_str(" --force-config");
let status = Command::new("sh") let status = Command::new("sh")
.arg("-lc") .arg("-lc")
.arg(script) .arg(script)
@@ -549,6 +553,8 @@ async fn run_terminal(
let mut last_packet_at = Instant::now(); let mut last_packet_at = Instant::now();
let mut status_visible = false; let mut status_visible = false;
let mut status_tick = tokio::time::interval(Duration::from_secs(1)); 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 { if let Some(frame) = first_frame {
render_frame(&frame)?; render_frame(&frame)?;
if frame.closed { if frame.closed {
@@ -597,6 +603,16 @@ async fn run_terminal(
socket.send_to(&packet, addr).await?; 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) => { recv = socket.recv_from(&mut recv_buf) => {
let (n, _) = recv?; let (n, _) = recv?;
last_packet_at = Instant::now(); last_packet_at = Instant::now();
@@ -647,6 +663,29 @@ async fn run_terminal(
Ok(()) 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( async fn send_ack(
socket: &UdpSocket, socket: &UdpSocket,
addr: SocketAddr, addr: SocketAddr,
@@ -733,12 +772,34 @@ struct RawMode;
impl RawMode { impl RawMode {
fn enter() -> Result<Self> { fn enter() -> Result<Self> {
enable_raw_mode()?; enable_raw_mode()?;
let mut stdout = std::io::stdout();
stdout.write_all(TERMINAL_CLEANUP)?;
stdout.flush()?;
Ok(Self) Ok(Self)
} }
} }
impl Drop for RawMode { impl Drop for RawMode {
fn drop(&mut self) { fn drop(&mut self) {
let mut stdout = std::io::stdout();
let _ = stdout.write_all(TERMINAL_CLEANUP);
let _ = stdout.flush();
let _ = disable_raw_mode(); 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();
+12
View File
@@ -258,6 +258,10 @@ async fn handle_bootstrap_attach(
.sessions .sessions
.get_mut(&req.bootstrap.session) .get_mut(&req.bootstrap.session)
.expect("session exists"); .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 client_id = crypto::random_16();
let snapshot = session.parser.screen().state_formatted(); let snapshot = session.parser.screen().state_formatted();
let screen = session.parser.screen().clone(); let screen = session.parser.screen().clone();
@@ -362,6 +366,10 @@ async fn handle_ticket_attach(
.sessions .sessions
.get_mut(&req.session) .get_mut(&req.session)
.expect("session exists"); .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 client_id = crypto::random_16();
let snapshot = session.parser.screen().state_formatted(); let snapshot = session.parser.screen().state_formatted();
let screen = session.parser.screen().clone(); let screen = session.parser.screen().clone();
@@ -456,6 +464,10 @@ async fn handle_resume(
client.rows = req.rows; client.rows = req.rows;
client.last_seen = Instant::now(); client.last_seen = Instant::now();
client.send_seq += 1; 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(); let snapshot = session.parser.screen().state_formatted();
client.last_screen = Some(session.parser.screen().clone()); client.last_screen = Some(session.parser.screen().clone());
(client.send_seq, session.output_seq, snapshot) (client.send_seq, session.output_seq, snapshot)