Add fresh default sessions and terminal status handling
This commit is contained in:
@@ -118,13 +118,19 @@ Attach:
|
|||||||
dosh palav
|
dosh palav
|
||||||
```
|
```
|
||||||
|
|
||||||
Use named sessions:
|
Plain `dosh palav` opens a fresh terminal session. Use named sessions when you want
|
||||||
|
to reattach to the same persistent terminal from multiple clients:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dosh --session work palav
|
dosh --session work palav
|
||||||
|
dosh --session logs palav
|
||||||
```
|
```
|
||||||
|
|
||||||
Press `Ctrl-]` to detach the current client while leaving the server session alive.
|
Press `Ctrl-]` to detach the current client while leaving the server session alive.
|
||||||
|
Typing `exit` in the remote shell closes that Dosh session and returns the client.
|
||||||
|
If UDP packets stop arriving, Dosh shows a blue status bar at the top of the
|
||||||
|
terminal with the disconnected duration and sends keepalive pings until traffic
|
||||||
|
returns.
|
||||||
|
|
||||||
If SSH and UDP use different public names, specify the UDP address:
|
If SSH and UDP use different public names, specify the UDP address:
|
||||||
|
|
||||||
|
|||||||
@@ -321,14 +321,9 @@ Reliability:
|
|||||||
Named sessions:
|
Named sessions:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dosh # attach default
|
dosh host # open a fresh generated session
|
||||||
dosh attach # attach default
|
dosh --session work host # attach/create named persistent session
|
||||||
dosh attach work
|
dosh --session work --view-only host
|
||||||
dosh attach work --view-only
|
|
||||||
dosh new work
|
|
||||||
dosh list
|
|
||||||
dosh list-clients [session]
|
|
||||||
dosh kill work
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Session behavior:
|
Session behavior:
|
||||||
@@ -336,6 +331,8 @@ Session behavior:
|
|||||||
- One PTY per session.
|
- One PTY per session.
|
||||||
- Sessions persist until killed or server exits.
|
- Sessions persist until killed or server exits.
|
||||||
- If a session has zero clients, the PTY keeps running.
|
- If a session has zero clients, the PTY keeps running.
|
||||||
|
- Plain client attaches use generated session names by default.
|
||||||
|
- `--session <name>` intentionally reuses or shares a persistent session.
|
||||||
- Configured sessions are prewarmed at daemon startup.
|
- Configured sessions are prewarmed at daemon startup.
|
||||||
- If a requested session does not exist:
|
- If a requested session does not exist:
|
||||||
- `attach` creates it only when `create_on_attach = true`.
|
- `attach` creates it only when `create_on_attach = true`.
|
||||||
@@ -457,7 +454,7 @@ server = "user@example.com"
|
|||||||
ssh_auth_command = "~/.local/bin/dosh-auth"
|
ssh_auth_command = "~/.local/bin/dosh-auth"
|
||||||
# ssh_port = 22
|
# ssh_port = 22
|
||||||
dosh_port = 50000
|
dosh_port = 50000
|
||||||
default_session = "default"
|
default_session = "new"
|
||||||
reconnect_timeout_secs = 5
|
reconnect_timeout_secs = 5
|
||||||
view_only = false
|
view_only = false
|
||||||
cache_attach_tickets = true
|
cache_attach_tickets = true
|
||||||
|
|||||||
+1
-1
@@ -54,7 +54,7 @@ $doshHostLine
|
|||||||
ssh_auth_command = "~/.local/bin/dosh-auth"
|
ssh_auth_command = "~/.local/bin/dosh-auth"
|
||||||
# ssh_port = 22
|
# ssh_port = 22
|
||||||
dosh_port = $Port
|
dosh_port = $Port
|
||||||
default_session = "default"
|
default_session = "new"
|
||||||
reconnect_timeout_secs = 5
|
reconnect_timeout_secs = 5
|
||||||
view_only = false
|
view_only = false
|
||||||
cache_attach_tickets = true
|
cache_attach_tickets = true
|
||||||
|
|||||||
+1
-1
@@ -218,7 +218,7 @@ $dosh_host_line
|
|||||||
ssh_auth_command = "~/.local/bin/dosh-auth"
|
ssh_auth_command = "~/.local/bin/dosh-auth"
|
||||||
# ssh_port = 22
|
# ssh_port = 22
|
||||||
dosh_port = $port
|
dosh_port = $port
|
||||||
default_session = "default"
|
default_session = "new"
|
||||||
reconnect_timeout_secs = 5
|
reconnect_timeout_secs = 5
|
||||||
view_only = false
|
view_only = false
|
||||||
cache_attach_tickets = true
|
cache_attach_tickets = true
|
||||||
|
|||||||
+80
-4
@@ -18,7 +18,7 @@ use std::fs;
|
|||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::net::{SocketAddr, ToSocketAddrs};
|
use std::net::{SocketAddr, ToSocketAddrs};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
use tokio::net::UdpSocket;
|
use tokio::net::UdpSocket;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
@@ -27,8 +27,10 @@ use tokio::sync::mpsc;
|
|||||||
struct Args {
|
struct Args {
|
||||||
#[arg()]
|
#[arg()]
|
||||||
server: Option<String>,
|
server: Option<String>,
|
||||||
#[arg(long, default_value = "default")]
|
#[arg(long)]
|
||||||
session: String,
|
session: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
new: bool,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
view_only: bool,
|
view_only: bool,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -73,7 +75,7 @@ async fn main() -> Result<()> {
|
|||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
let config = load_client_config(None).unwrap_or_default();
|
let config = load_client_config(None).unwrap_or_default();
|
||||||
let server = args.server.unwrap_or(config.server);
|
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 {
|
let mode = if args.view_only || config.view_only {
|
||||||
"view-only"
|
"view-only"
|
||||||
} else {
|
} else {
|
||||||
@@ -201,6 +203,7 @@ async fn main() -> Result<()> {
|
|||||||
output_seq: ok.initial_seq,
|
output_seq: ok.initial_seq,
|
||||||
bytes: ok.snapshot,
|
bytes: ok.snapshot,
|
||||||
snapshot: true,
|
snapshot: true,
|
||||||
|
closed: false,
|
||||||
};
|
};
|
||||||
if args.attach_only {
|
if args.attach_only {
|
||||||
render_frame(&first)?;
|
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(
|
fn ssh_bootstrap(
|
||||||
server: &str,
|
server: &str,
|
||||||
ssh_port: Option<u16>,
|
ssh_port: Option<u16>,
|
||||||
@@ -426,6 +447,7 @@ async fn try_ticket_attach(
|
|||||||
output_seq: ok.initial_seq,
|
output_seq: ok.initial_seq,
|
||||||
bytes: ok.snapshot.clone(),
|
bytes: ok.snapshot.clone(),
|
||||||
snapshot: true,
|
snapshot: true,
|
||||||
|
closed: false,
|
||||||
};
|
};
|
||||||
let mut next = cached.clone();
|
let mut next = cached.clone();
|
||||||
next.client_id = ok.client_id;
|
next.client_id = ok.client_id;
|
||||||
@@ -481,8 +503,14 @@ async fn run_terminal(
|
|||||||
let _raw = RawMode::enter()?;
|
let _raw = RawMode::enter()?;
|
||||||
let addr = resolve_addr(&cred.udp_host, cred.udp_port)?;
|
let addr = resolve_addr(&cred.udp_host, cred.udp_port)?;
|
||||||
let mut send_seq = 2u64;
|
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 {
|
if let Some(frame) = first_frame {
|
||||||
render_frame(&frame)?;
|
render_frame(&frame)?;
|
||||||
|
if frame.closed {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
cred.last_rendered_seq = frame.output_seq;
|
cred.last_rendered_seq = frame.output_seq;
|
||||||
send_ack(&socket, addr, &cred, &mut send_seq).await?;
|
send_ack(&socket, addr, &cred, &mut send_seq).await?;
|
||||||
}
|
}
|
||||||
@@ -528,6 +556,11 @@ async fn run_terminal(
|
|||||||
}
|
}
|
||||||
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();
|
||||||
|
if status_visible {
|
||||||
|
clear_status_bar()?;
|
||||||
|
status_visible = false;
|
||||||
|
}
|
||||||
let packet = protocol::decode(&recv_buf[..n])?;
|
let packet = protocol::decode(&recv_buf[..n])?;
|
||||||
match packet.header.kind {
|
match packet.header.kind {
|
||||||
PacketKind::Frame | PacketKind::ResumeOk => {
|
PacketKind::Frame | PacketKind::ResumeOk => {
|
||||||
@@ -536,11 +569,36 @@ async fn run_terminal(
|
|||||||
render_frame(&frame)?;
|
render_frame(&frame)?;
|
||||||
cred.last_rendered_seq = frame.output_seq;
|
cred.last_rendered_seq = frame.output_seq;
|
||||||
send_ack(&socket, addr, &cred, &mut send_seq).await?;
|
send_ack(&socket, addr, &cred, &mut send_seq).await?;
|
||||||
|
if frame.closed {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PacketKind::Pong => {}
|
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(())
|
Ok(())
|
||||||
@@ -588,6 +646,24 @@ fn render_frame(frame: &Frame) -> Result<()> {
|
|||||||
Ok(())
|
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 {
|
fn cache_path(root: &str, server: &str, session: &str, mode: &str) -> std::path::PathBuf {
|
||||||
let safe = format!("{server}_{session}_{mode}")
|
let safe = format!("{server}_{session}_{mode}")
|
||||||
.chars()
|
.chars()
|
||||||
|
|||||||
@@ -465,6 +465,7 @@ async fn handle_resume(
|
|||||||
output_seq,
|
output_seq,
|
||||||
bytes: snapshot,
|
bytes: snapshot,
|
||||||
snapshot: true,
|
snapshot: true,
|
||||||
|
closed: false,
|
||||||
};
|
};
|
||||||
let body = protocol::to_body(&frame)?;
|
let body = protocol::to_body(&frame)?;
|
||||||
let out = protocol::encode_encrypted(
|
let out = protocol::encode_encrypted(
|
||||||
@@ -614,6 +615,10 @@ async fn broadcast_output(
|
|||||||
socket: &Arc<UdpSocket>,
|
socket: &Arc<UdpSocket>,
|
||||||
output: PtyOutput,
|
output: PtyOutput,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
if output.exited {
|
||||||
|
return broadcast_session_exit(state, socket, output.session).await;
|
||||||
|
}
|
||||||
|
|
||||||
let sends = {
|
let sends = {
|
||||||
let mut locked = state.lock().expect("server state poisoned");
|
let mut locked = state.lock().expect("server state poisoned");
|
||||||
let scrollback = locked.config.scrollback;
|
let scrollback = locked.config.scrollback;
|
||||||
@@ -652,6 +657,7 @@ async fn broadcast_output(
|
|||||||
output_seq,
|
output_seq,
|
||||||
bytes,
|
bytes,
|
||||||
snapshot,
|
snapshot,
|
||||||
|
closed: false,
|
||||||
};
|
};
|
||||||
let body = protocol::to_body(&frame)?;
|
let body = protocol::to_body(&frame)?;
|
||||||
let packet = protocol::encode_encrypted(
|
let packet = protocol::encode_encrypted(
|
||||||
@@ -683,6 +689,48 @@ async fn broadcast_output(
|
|||||||
Ok(())
|
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(
|
async fn retransmit_pending(
|
||||||
state: &Arc<Mutex<ServerState>>,
|
state: &Arc<Mutex<ServerState>>,
|
||||||
socket: &Arc<UdpSocket>,
|
socket: &Arc<UdpSocket>,
|
||||||
|
|||||||
+1
-1
@@ -64,7 +64,7 @@ impl Default for ClientConfig {
|
|||||||
ssh_auth_command: Some("~/.local/bin/dosh-auth".to_string()),
|
ssh_auth_command: Some("~/.local/bin/dosh-auth".to_string()),
|
||||||
ssh_port: None,
|
ssh_port: None,
|
||||||
dosh_port: 50000,
|
dosh_port: 50000,
|
||||||
default_session: "default".to_string(),
|
default_session: "new".to_string(),
|
||||||
reconnect_timeout_secs: 5,
|
reconnect_timeout_secs: 5,
|
||||||
view_only: false,
|
view_only: false,
|
||||||
cache_attach_tickets: true,
|
cache_attach_tickets: true,
|
||||||
|
|||||||
@@ -255,6 +255,7 @@ pub struct Frame {
|
|||||||
pub output_seq: u64,
|
pub output_seq: u64,
|
||||||
pub bytes: Vec<u8>,
|
pub bytes: Vec<u8>,
|
||||||
pub snapshot: bool,
|
pub snapshot: bool,
|
||||||
|
pub closed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_body<T: Serialize>(value: &T) -> Result<Vec<u8>> {
|
pub fn to_body<T: Serialize>(value: &T) -> Result<Vec<u8>> {
|
||||||
|
|||||||
+18
-2
@@ -34,6 +34,7 @@ impl PtyHandle {
|
|||||||
pub struct PtyOutput {
|
pub struct PtyOutput {
|
||||||
pub session: String,
|
pub session: String,
|
||||||
pub bytes: Vec<u8>,
|
pub bytes: Vec<u8>,
|
||||||
|
pub exited: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn_pty_session(
|
pub fn spawn_pty_session(
|
||||||
@@ -75,14 +76,29 @@ pub fn spawn_pty_session(
|
|||||||
let mut buf = [0u8; 8192];
|
let mut buf = [0u8; 8192];
|
||||||
loop {
|
loop {
|
||||||
match reader.read(&mut buf) {
|
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) => {
|
Ok(n) => {
|
||||||
let _ = tx.send(PtyOutput {
|
let _ = tx.send(PtyOutput {
|
||||||
session: reader_session.clone(),
|
session: reader_session.clone(),
|
||||||
bytes: buf[..n].to_vec(),
|
bytes: buf[..n].to_vec(),
|
||||||
|
exited: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(_) => break,
|
Err(_) => {
|
||||||
|
let _ = tx.send(PtyOutput {
|
||||||
|
session: reader_session.clone(),
|
||||||
|
bytes: Vec::new(),
|
||||||
|
exited: true,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ fn attach_once(dir: &tempfile::TempDir, port: u16, cache: bool) -> std::process:
|
|||||||
let mut cmd = Command::new(client);
|
let mut cmd = Command::new(client);
|
||||||
cmd.arg("--local-auth")
|
cmd.arg("--local-auth")
|
||||||
.arg("--attach-only")
|
.arg("--attach-only")
|
||||||
|
.arg("--session")
|
||||||
|
.arg("default")
|
||||||
.arg("--dosh-port")
|
.arg("--dosh-port")
|
||||||
.arg(port.to_string())
|
.arg(port.to_string())
|
||||||
.arg("local")
|
.arg("local")
|
||||||
@@ -388,6 +390,77 @@ fn resize_updates_pty_size() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pty_exit_sends_closed_frame() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let port = free_udp_port();
|
||||||
|
let config = write_server_config(&dir, port);
|
||||||
|
let mut server = start_server(&dir, &config);
|
||||||
|
let (socket, bootstrap, ok) = direct_attach(&config, port, "read-write");
|
||||||
|
|
||||||
|
let input = Input {
|
||||||
|
bytes: b"exit\n".to_vec(),
|
||||||
|
};
|
||||||
|
send_encrypted(
|
||||||
|
&socket,
|
||||||
|
port,
|
||||||
|
PacketKind::Input,
|
||||||
|
ok.client_id,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
&bootstrap.session_key,
|
||||||
|
&protocol::to_body(&input).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut closed = false;
|
||||||
|
let deadline = std::time::Instant::now() + Duration::from_secs(3);
|
||||||
|
while std::time::Instant::now() < deadline {
|
||||||
|
if let Some((_header, frame)) = recv_frame(&socket, &bootstrap.session_key) {
|
||||||
|
if frame.closed {
|
||||||
|
closed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = server.kill();
|
||||||
|
let _ = server.wait();
|
||||||
|
|
||||||
|
assert!(closed, "expected closed frame after shell exit");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pty_sets_terminal_environment() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let port = free_udp_port();
|
||||||
|
let config = write_server_config(&dir, port);
|
||||||
|
let mut server = start_server(&dir, &config);
|
||||||
|
let (socket, bootstrap, ok) = direct_attach(&config, port, "read-write");
|
||||||
|
|
||||||
|
let input = Input {
|
||||||
|
bytes: b"printf \"$TERM\\n\"\n".to_vec(),
|
||||||
|
};
|
||||||
|
send_encrypted(
|
||||||
|
&socket,
|
||||||
|
port,
|
||||||
|
PacketKind::Input,
|
||||||
|
ok.client_id,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
&bootstrap.session_key,
|
||||||
|
&protocol::to_body(&input).unwrap(),
|
||||||
|
);
|
||||||
|
let text = collect_frame_text(&socket, &bootstrap.session_key, 2000);
|
||||||
|
|
||||||
|
let _ = server.kill();
|
||||||
|
let _ = server.wait();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
text.contains("xterm-256color"),
|
||||||
|
"expected TERM=xterm-256color, got {text:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resume_updates_udp_endpoint_for_roaming() {
|
fn resume_updates_udp_endpoint_for_roaming() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user