Files
rsh/crates/rshc/src/cmd/connect.rs
2026-05-12 23:19:12 +09:00

165 lines
5.2 KiB
Rust

use crate::auth::AuthedClient;
use crate::cmd::connection;
use crate::ui;
use anyhow::{anyhow, Result};
use crossterm::terminal;
use rsh_types::{AttachIOFrame, OpReq, OpResp};
use std::io::Write;
use tokio::io::AsyncReadExt;
pub async fn run(
client: &AuthedClient,
session: String,
connection_id: Option<u64>,
no_pty: bool,
) -> Result<()> {
let conns = connection::fetch(client, Some(session.clone())).await?;
if conns.is_empty() {
return Err(anyhow!("no connections for session '{session}'"));
}
let target = match connection_id {
Some(id) => {
conns
.iter()
.find(|c| c.connection_id == id)
.ok_or_else(|| anyhow!("no connection {id} in session '{session}'"))?
.clone()
}
None => {
if conns.len() == 1 {
conns[0].clone()
} else {
let labels: Vec<String> = conns
.iter()
.map(|c| format!("#{} {}@{} ({})", c.connection_id, c.info.user, c.info.hostname, ui::fmt_time(c.connected_at)))
.collect();
let pick = inquire::Select::new("select connection:", labels.clone())
.prompt()
.map_err(|e| anyhow!("prompt: {e}"))?;
let idx = labels.iter().position(|l| l == &pick).unwrap();
conns[idx].clone()
}
}
};
let (cols, rows) = terminal::size().unwrap_or((80, 24));
let pty = !no_pty;
let (attach_id, mut resps) = client
.req_stream(OpReq::Attach {
session: session.clone(),
connection_id: Some(target.connection_id),
shell_id: None,
pty,
cols,
rows,
})
.await?;
let ready = resps.recv().await.ok_or_else(|| anyhow!("attach: no response"))?;
match ready {
OpResp::AttachReady { .. } => {}
OpResp::Err(e) => {
client.drop_stream(attach_id).await;
return Err(anyhow!(e));
}
other => {
client.drop_stream(attach_id).await;
return Err(anyhow!("unexpected: {other:?}"));
}
}
ui::print_info(&format!(
"attached to #{} ({}@{}) — Ctrl-] to detach",
target.connection_id, target.info.user, target.info.hostname,
));
if pty {
terminal::enable_raw_mode().ok();
}
let result = pump(client, &mut resps, pty).await;
if pty {
terminal::disable_raw_mode().ok();
}
client.drop_stream(attach_id).await;
println!();
result
}
pub async fn pump(
client: &AuthedClient,
resps: &mut tokio::sync::mpsc::Receiver<OpResp>,
pty: bool,
) -> Result<()> {
let mut stdin = tokio::io::stdin();
let mut buf = [0u8; 4096];
let mut resize_rx: Option<tokio::sync::mpsc::Receiver<(u16, u16)>> = if pty {
let (tx, rx) = tokio::sync::mpsc::channel(8);
spawn_resize_watch(tx);
Some(rx)
} else {
None
};
loop {
tokio::select! {
r = stdin.read(&mut buf) => {
let n = r?;
if n == 0 {
let _ = client.send_attach_io(AttachIOFrame::Eof).await;
continue;
}
if pty && buf[..n].iter().any(|&b| b == 0x1d) {
let _ = client.send_attach_io(AttachIOFrame::Kill).await;
return Ok(());
}
let _ = client.send_attach_io(AttachIOFrame::Stdin(buf[..n].to_vec())).await;
}
Some(resp) = resps.recv() => {
match resp {
OpResp::Stdout(b) => {
let mut out = std::io::stdout();
out.write_all(&b).ok();
out.flush().ok();
}
OpResp::Stderr(b) => {
let mut err = std::io::stderr();
err.write_all(&b).ok();
err.flush().ok();
}
OpResp::Exited { code } => {
ui::print_info(&format!("remote exited (code {:?})", code));
return Ok(());
}
OpResp::Err(e) => return Err(anyhow!(e)),
_ => {}
}
}
Some((cols, rows)) = async {
match resize_rx.as_mut() {
Some(r) => r.recv().await,
None => std::future::pending().await,
}
} => {
let _ = client.send_attach_io(AttachIOFrame::Resize { cols, rows }).await;
}
}
}
}
pub fn spawn_resize_watch(tx: tokio::sync::mpsc::Sender<(u16, u16)>) {
tokio::spawn(async move {
let mut last = terminal::size().unwrap_or((80, 24));
loop {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
if let Ok(sz) = terminal::size() {
if sz != last {
last = sz;
if tx.send(sz).await.is_err() {
break;
}
}
}
}
});
}