use std::sync::{Arc, Mutex};
use portable_pty::{native_pty_system, CommandBuilder, MasterPty, PtySize};
use tokio::sync::broadcast;
pub struct PtyTerminal {
pub id: String,
pub command: String,
pub scrollback: Arc<Mutex<Vec<u8>>>,
pub output_tx: broadcast::Sender<Vec<u8>>,
pub input_tx: std::sync::mpsc::Sender<Vec<u8>>,
pub alive: Arc<Mutex<bool>>,
master: Arc<Mutex<Box<dyn MasterPty + Send>>>,
}
impl PtyTerminal {
pub fn spawn(
id: String,
container: &str,
command: &str,
rows: u16,
cols: u16,
) -> Result<Self, String> {
let pty = native_pty_system();
let pair = pty
.openpty(PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
})
.map_err(|e| {
let msg = format!("openpty failed: {e}");
tracing::error!("{msg}");
msg
})?;
let mut cmd = CommandBuilder::new("podman");
cmd.args(["exec", "-i", "-t", container, "sh", "-c", command]);
cmd.env("TERM", "xterm-256color");
let mut child = pair.slave.spawn_command(cmd).map_err(|e| {
let msg = format!("spawning podman exec -t in {container}: {e}");
tracing::error!("{msg}");
msg
})?;
drop(pair.slave);
let reader = pair
.master
.try_clone_reader()
.map_err(|e| format!("clone reader: {e}"))?;
let mut writer = pair
.master
.take_writer()
.map_err(|e| format!("take writer: {e}"))?;
let scrollback = Arc::new(Mutex::new(Vec::<u8>::new()));
let (output_tx, _) = broadcast::channel::<Vec<u8>>(64);
let (input_tx, input_rx) = std::sync::mpsc::channel::<Vec<u8>>();
let alive = Arc::new(Mutex::new(true));
{
let scrollback = scrollback.clone();
let output_tx = output_tx.clone();
std::thread::spawn(move || {
let mut reader = reader;
let mut buf = [0u8; 4096];
loop {
use std::io::Read;
match reader.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => {
let chunk = buf[..n].to_vec();
if let Ok(mut sb) = scrollback.lock() {
sb.extend_from_slice(&chunk);
if sb.len() > 64 * 1024 {
let cut = sb.len() - 64 * 1024;
sb.drain(..cut);
}
}
let _ = output_tx.send(chunk);
}
}
}
});
}
std::thread::spawn(move || {
use std::io::Write;
while let Ok(bytes) = input_rx.recv() {
if writer.write_all(&bytes).is_err() {
break;
}
let _ = writer.flush();
}
});
{
let alive = alive.clone();
std::thread::spawn(move || {
let _ = child.wait();
if let Ok(mut a) = alive.lock() {
*a = false;
}
});
}
Ok(Self {
id,
command: command.to_string(),
scrollback,
output_tx,
input_tx,
alive,
master: Arc::new(Mutex::new(pair.master)),
})
}
pub fn resize(&self, rows: u16, cols: u16) {
if let Ok(m) = self.master.lock() {
let _ = m.resize(PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
});
}
}
pub fn is_alive(&self) -> bool {
self.alive.lock().map(|a| *a).unwrap_or(false)
}
pub fn snapshot(&self) -> Vec<u8> {
self.scrollback
.lock()
.map(|s| s.clone())
.unwrap_or_default()
}
}