Add fresh default sessions and terminal status handling
ci / test (push) Has been cancelled
ci / remote-bench (push) Has been cancelled

This commit is contained in:
Codex
2026-06-11 09:41:46 -04:00
parent 92f43df046
commit 0eca4f1cf4
10 changed files with 236 additions and 19 deletions
+80 -4
View File
@@ -18,7 +18,7 @@ use std::fs;
use std::io::{Read, Write};
use std::net::{SocketAddr, ToSocketAddrs};
use std::process::Command;
use std::time::{Duration, Instant};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tokio::net::UdpSocket;
use tokio::sync::mpsc;
@@ -27,8 +27,10 @@ use tokio::sync::mpsc;
struct Args {
#[arg()]
server: Option<String>,
#[arg(long, default_value = "default")]
session: String,
#[arg(long)]
session: Option<String>,
#[arg(long)]
new: bool,
#[arg(long)]
view_only: bool,
#[arg(long)]
@@ -73,7 +75,7 @@ async fn main() -> Result<()> {
let args = Args::parse();
let config = load_client_config(None).unwrap_or_default();
let server = args.server.unwrap_or(config.server);
let session = args.session;
let session = select_session(args.session.as_deref(), args.new, &config.default_session);
let mode = if args.view_only || config.view_only {
"view-only"
} else {
@@ -201,6 +203,7 @@ async fn main() -> Result<()> {
output_seq: ok.initial_seq,
bytes: ok.snapshot,
snapshot: true,
closed: false,
};
if args.attach_only {
render_frame(&first)?;
@@ -235,6 +238,24 @@ fn local_bootstrap(
)
}
fn select_session(requested: Option<&str>, force_new: bool, configured_default: &str) -> String {
if let Some(session) = requested {
return session.to_string();
}
if force_new || configured_default == "new" || configured_default.is_empty() {
return unique_session_name();
}
configured_default.to_string()
}
fn unique_session_name() -> String {
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
format!("term-{millis}-{}", std::process::id())
}
fn ssh_bootstrap(
server: &str,
ssh_port: Option<u16>,
@@ -426,6 +447,7 @@ async fn try_ticket_attach(
output_seq: ok.initial_seq,
bytes: ok.snapshot.clone(),
snapshot: true,
closed: false,
};
let mut next = cached.clone();
next.client_id = ok.client_id;
@@ -481,8 +503,14 @@ async fn run_terminal(
let _raw = RawMode::enter()?;
let addr = resolve_addr(&cred.udp_host, cred.udp_port)?;
let mut send_seq = 2u64;
let mut last_packet_at = Instant::now();
let mut status_visible = false;
let mut status_tick = tokio::time::interval(Duration::from_secs(1));
if let Some(frame) = first_frame {
render_frame(&frame)?;
if frame.closed {
return Ok(());
}
cred.last_rendered_seq = frame.output_seq;
send_ack(&socket, addr, &cred, &mut send_seq).await?;
}
@@ -528,6 +556,11 @@ async fn run_terminal(
}
recv = socket.recv_from(&mut recv_buf) => {
let (n, _) = recv?;
last_packet_at = Instant::now();
if status_visible {
clear_status_bar()?;
status_visible = false;
}
let packet = protocol::decode(&recv_buf[..n])?;
match packet.header.kind {
PacketKind::Frame | PacketKind::ResumeOk => {
@@ -536,11 +569,36 @@ async fn run_terminal(
render_frame(&frame)?;
cred.last_rendered_seq = frame.output_seq;
send_ack(&socket, addr, &cred, &mut send_seq).await?;
if frame.closed {
break;
}
}
PacketKind::Pong => {}
_ => {}
}
}
_ = status_tick.tick() => {
let stale = last_packet_at.elapsed();
if stale >= Duration::from_secs(2) {
draw_status_bar(&format!(
"DOSH disconnected for {}s | session {} | trying UDP",
stale.as_secs(),
cred.session
))?;
status_visible = true;
let packet = protocol::encode_encrypted(
PacketKind::Ping,
cred.client_id,
send_seq,
cred.last_rendered_seq,
&cred.session_key,
CLIENT_TO_SERVER,
b"",
)?;
send_seq += 1;
socket.send_to(&packet, addr).await?;
}
}
}
}
Ok(())
@@ -588,6 +646,24 @@ fn render_frame(frame: &Frame) -> Result<()> {
Ok(())
}
fn draw_status_bar(message: &str) -> Result<()> {
let mut stdout = std::io::stdout();
write!(
stdout,
"\x1b7\x1b[1;1H\x1b[44;97m\x1b[K {} \x1b[0m\x1b8",
message
)?;
stdout.flush()?;
Ok(())
}
fn clear_status_bar() -> Result<()> {
let mut stdout = std::io::stdout();
stdout.write_all(b"\x1b7\x1b[1;1H\x1b[K\x1b8")?;
stdout.flush()?;
Ok(())
}
fn cache_path(root: &str, server: &str, session: &str, mode: &str) -> std::path::PathBuf {
let safe = format!("{server}_{session}_{mode}")
.chars()
+48
View File
@@ -465,6 +465,7 @@ async fn handle_resume(
output_seq,
bytes: snapshot,
snapshot: true,
closed: false,
};
let body = protocol::to_body(&frame)?;
let out = protocol::encode_encrypted(
@@ -614,6 +615,10 @@ async fn broadcast_output(
socket: &Arc<UdpSocket>,
output: PtyOutput,
) -> Result<()> {
if output.exited {
return broadcast_session_exit(state, socket, output.session).await;
}
let sends = {
let mut locked = state.lock().expect("server state poisoned");
let scrollback = locked.config.scrollback;
@@ -652,6 +657,7 @@ async fn broadcast_output(
output_seq,
bytes,
snapshot,
closed: false,
};
let body = protocol::to_body(&frame)?;
let packet = protocol::encode_encrypted(
@@ -683,6 +689,48 @@ async fn broadcast_output(
Ok(())
}
async fn broadcast_session_exit(
state: &Arc<Mutex<ServerState>>,
socket: &Arc<UdpSocket>,
session_name: String,
) -> Result<()> {
let sends = {
let mut locked = state.lock().expect("server state poisoned");
let Some(mut session) = locked.sessions.remove(&session_name) else {
return Ok(());
};
session.output_seq += 1;
let output_seq = session.output_seq;
let mut sends = Vec::new();
for (client_id, mut client) in session.clients.drain() {
client.send_seq += 1;
let frame = Frame {
session: session_name.clone(),
output_seq,
bytes: b"\r\n[dosh session exited]\r\n".to_vec(),
snapshot: false,
closed: true,
};
let body = protocol::to_body(&frame)?;
let packet = protocol::encode_encrypted(
PacketKind::Frame,
client_id,
client.send_seq,
client.last_acked,
&client.session_key,
SERVER_TO_CLIENT,
&body,
)?;
sends.push((client.endpoint, packet));
}
sends
};
for (endpoint, packet) in sends {
socket.send_to(&packet, endpoint).await?;
}
Ok(())
}
async fn retransmit_pending(
state: &Arc<Mutex<ServerState>>,
socket: &Arc<UdpSocket>,
+1 -1
View File
@@ -64,7 +64,7 @@ impl Default for ClientConfig {
ssh_auth_command: Some("~/.local/bin/dosh-auth".to_string()),
ssh_port: None,
dosh_port: 50000,
default_session: "default".to_string(),
default_session: "new".to_string(),
reconnect_timeout_secs: 5,
view_only: false,
cache_attach_tickets: true,
+1
View File
@@ -255,6 +255,7 @@ pub struct Frame {
pub output_seq: u64,
pub bytes: Vec<u8>,
pub snapshot: bool,
pub closed: bool,
}
pub fn to_body<T: Serialize>(value: &T) -> Result<Vec<u8>> {
+18 -2
View File
@@ -34,6 +34,7 @@ impl PtyHandle {
pub struct PtyOutput {
pub session: String,
pub bytes: Vec<u8>,
pub exited: bool,
}
pub fn spawn_pty_session(
@@ -75,14 +76,29 @@ pub fn spawn_pty_session(
let mut buf = [0u8; 8192];
loop {
match reader.read(&mut buf) {
Ok(0) => break,
Ok(0) => {
let _ = tx.send(PtyOutput {
session: reader_session.clone(),
bytes: Vec::new(),
exited: true,
});
break;
}
Ok(n) => {
let _ = tx.send(PtyOutput {
session: reader_session.clone(),
bytes: buf[..n].to_vec(),
exited: false,
});
}
Err(_) => break,
Err(_) => {
let _ = tx.send(PtyOutput {
session: reader_session.clone(),
bytes: Vec::new(),
exited: true,
});
break;
}
}
}
})