use std::io::{Read, Write};
use std::sync::{Arc, Mutex};
use anyhow::{Context, Result};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use portable_pty::{native_pty_system, CommandBuilder, MasterPty, PtySize};
use tokio::sync::mpsc;
use crate::event::AppEvent;
use crate::ssh::client::Host;
pub type SessionId = u64;
struct SendMasterPty(Box<dyn MasterPty>);
unsafe impl Send for SendMasterPty {}
impl std::ops::Deref for SendMasterPty {
type Target = dyn MasterPty;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
pub struct PtySession {
pub id: SessionId,
pub host_name: String,
screen: Arc<Mutex<vt100::Parser>>,
writer: Box<dyn Write + Send>,
master: SendMasterPty,
_child: Box<dyn portable_pty::Child + Send + Sync>,
reader_thread: Option<std::thread::JoinHandle<()>>,
}
impl PtySession {
pub fn spawn(
id: SessionId,
host: &Host,
cols: u16,
rows: u16,
tx: mpsc::Sender<AppEvent>,
) -> Result<Self> {
let pty_system = native_pty_system();
let size = PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
};
let pair = pty_system
.openpty(size)
.context("failed to open PTY pair")?;
let mut cmd = CommandBuilder::new("ssh");
cmd.args(["-o", "ConnectTimeout=10"]);
cmd.arg("-t");
if host.port != 22 {
cmd.args(["-p", &host.port.to_string()]);
}
if let Some(ref key) = host.identity_file {
cmd.args(["-i", key]);
}
if let Some(ref jump) = host.proxy_jump {
cmd.args(["-J", jump]);
}
cmd.arg(format!("{}@{}", host.user, host.hostname));
let child = pair
.slave
.spawn_command(cmd)
.context("failed to spawn SSH in PTY")?;
let writer = pair
.master
.take_writer()
.context("failed to get PTY writer")?;
let mut reader = pair
.master
.try_clone_reader()
.context("failed to clone PTY reader")?;
let master = SendMasterPty(pair.master);
let parser = Arc::new(Mutex::new(vt100::Parser::new(rows, cols, 1000)));
let parser_clone = Arc::clone(&parser);
let reader_thread = std::thread::Builder::new()
.name(format!("pty-reader-{id}"))
.spawn(move || {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut buf = [0u8; 4096];
loop {
match reader.read(&mut buf) {
Ok(0) | Err(_) => {
let _ = tx.blocking_send(AppEvent::PtyExited(id));
break;
}
Ok(n) => {
const CHUNK: usize = 256;
let mut off = 0;
while off < n {
let end = (off + CHUNK).min(n);
if let Ok(mut p) = parser_clone.lock() {
p.process(&buf[off..end]);
}
off = end;
}
let _ = tx.blocking_send(AppEvent::PtyOutput(id));
}
}
}
}));
if let Err(e) = result {
tracing::error!(session = id, "PTY reader thread panicked: {:?}", e);
let _ = tx.blocking_send(AppEvent::PtyExited(id));
}
})
.context("failed to spawn PTY reader thread")?;
tracing::info!("PTY session {} opened for host '{}'", id, host.name);
Ok(Self {
id,
host_name: host.name.clone(),
screen: parser,
writer,
master,
_child: child,
reader_thread: Some(reader_thread),
})
}
pub fn write_input(&mut self, data: &[u8]) -> Result<()> {
self.writer.write_all(data).context("PTY write failed")?;
self.writer.flush().context("PTY flush failed")
}
pub fn resize(&mut self, cols: u16, rows: u16) -> Result<()> {
self.master
.resize(PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
})
.context("PTY resize (TIOCSWINSZ) failed")?;
if let Ok(mut p) = self.screen.lock() {
p.set_size(rows, cols);
}
Ok(())
}
pub fn parser_arc(&self) -> Arc<Mutex<vt100::Parser>> {
Arc::clone(&self.screen)
}
}
impl Drop for PtySession {
fn drop(&mut self) {
if let Some(t) = self.reader_thread.take() {
drop(t); }
tracing::info!("PTY session {} closed", self.id);
}
}
pub struct PtyManager {
sessions: Vec<PtySession>,
next_id: u64,
}
impl PtyManager {
pub fn new() -> Self {
Self {
sessions: Vec::new(),
next_id: 1,
}
}
pub fn open(
&mut self,
host: &Host,
cols: u16,
rows: u16,
tx: mpsc::Sender<AppEvent>,
) -> Result<SessionId> {
let id = self.next_id;
self.next_id += 1;
let session = PtySession::spawn(id, host, cols, rows, tx)?;
self.sessions.push(session);
Ok(id)
}
pub fn write(&mut self, id: SessionId, data: &[u8]) -> Result<()> {
match self.sessions.iter_mut().find(|s| s.id == id) {
Some(s) => s.write_input(data),
None => Ok(()), }
}
pub fn resize(&mut self, id: SessionId, cols: u16, rows: u16) -> Result<()> {
if let Some(s) = self.sessions.iter_mut().find(|s| s.id == id) {
s.resize(cols, rows)?;
}
Ok(())
}
pub fn close(&mut self, id: SessionId) {
self.sessions.retain(|s| s.id != id);
}
pub fn shutdown(mut self) {
self.sessions.clear(); }
pub fn parser_for(&self, id: SessionId) -> Option<Arc<Mutex<vt100::Parser>>> {
self.sessions
.iter()
.find(|s| s.id == id)
.map(|s| s.parser_arc())
}
}
impl Default for PtyManager {
fn default() -> Self {
Self::new()
}
}
pub fn key_to_bytes(key: KeyEvent) -> Vec<u8> {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let alt = key.modifiers.contains(KeyModifiers::ALT);
match key.code {
KeyCode::Char(c) if ctrl => {
match c {
'a'..='z' => vec![c as u8 - b'a' + 1],
'A'..='Z' => vec![c as u8 - b'A' + 1],
'[' => vec![0x1b], '\\' => vec![0x1c],
']' => vec![0x1d],
'^' => vec![0x1e],
'_' => vec![0x1f],
'@' => vec![0x00], _ => vec![c as u8],
}
}
KeyCode::Char(c) if alt => {
let mut buf = [0u8; 4];
let s = c.encode_utf8(&mut buf);
let mut bytes = vec![0x1b];
bytes.extend_from_slice(s.as_bytes());
bytes
}
KeyCode::Char(c) => {
let mut buf = [0u8; 4];
let s = c.encode_utf8(&mut buf);
s.as_bytes().to_vec()
}
KeyCode::Enter => vec![b'\r'],
KeyCode::Backspace => vec![0x7f],
KeyCode::Tab => {
if key.modifiers.contains(KeyModifiers::SHIFT) {
vec![0x1b, b'[', b'Z'] } else {
vec![0x09]
}
}
KeyCode::Esc => vec![0x1b],
KeyCode::Up => vec![0x1b, b'[', b'A'],
KeyCode::Down => vec![0x1b, b'[', b'B'],
KeyCode::Right => vec![0x1b, b'[', b'C'],
KeyCode::Left => vec![0x1b, b'[', b'D'],
KeyCode::Home => vec![0x1b, b'[', b'H'],
KeyCode::End => vec![0x1b, b'[', b'F'],
KeyCode::Insert => vec![0x1b, b'[', b'2', b'~'],
KeyCode::Delete => vec![0x1b, b'[', b'3', b'~'],
KeyCode::PageUp => vec![0x1b, b'[', b'5', b'~'],
KeyCode::PageDown => vec![0x1b, b'[', b'6', b'~'],
KeyCode::F(1) => vec![0x1b, b'O', b'P'],
KeyCode::F(2) => vec![0x1b, b'O', b'Q'],
KeyCode::F(3) => vec![0x1b, b'O', b'R'],
KeyCode::F(4) => vec![0x1b, b'O', b'S'],
KeyCode::F(5) => vec![0x1b, b'[', b'1', b'5', b'~'],
KeyCode::F(6) => vec![0x1b, b'[', b'1', b'7', b'~'],
KeyCode::F(7) => vec![0x1b, b'[', b'1', b'8', b'~'],
KeyCode::F(8) => vec![0x1b, b'[', b'1', b'9', b'~'],
KeyCode::F(9) => vec![0x1b, b'[', b'2', b'0', b'~'],
KeyCode::F(10) => vec![0x1b, b'[', b'2', b'1', b'~'],
KeyCode::F(11) => vec![0x1b, b'[', b'2', b'3', b'~'],
KeyCode::F(12) => vec![0x1b, b'[', b'2', b'4', b'~'],
_ => vec![],
}
}