use std::collections::BTreeMap;
use std::io;
use std::sync::{Arc, Mutex};
use bytes::Bytes;
use carte_api::attach::TermSize;
use tokio::sync::{broadcast, watch};
use super::runner::ProcLog;
use crate::process::Pty;
pub struct Terminal {
pty: Pty,
log: Arc<Mutex<ProcLog>>,
configured: TermSize,
output: broadcast::Sender<Bytes>,
clients: Mutex<Clients>,
writing: tokio::sync::Mutex<()>,
ended: watch::Sender<Option<String>>,
}
struct Clients {
next: u64,
sizes: BTreeMap<u64, Option<TermSize>>,
current: TermSize,
queries: CursorQueries,
answers: Answers,
}
impl Terminal {
pub(super) fn new(pty: Pty, log: Arc<Mutex<ProcLog>>, size: TermSize) -> Arc<Self> {
Arc::new(Terminal {
pty,
log,
configured: size,
output: broadcast::channel(1024).0,
clients: Mutex::new(Clients {
next: 0,
sizes: BTreeMap::new(),
current: size,
queries: CursorQueries::default(),
answers: Answers::default(),
}),
writing: tokio::sync::Mutex::new(()),
ended: watch::channel(None).0,
})
}
pub(super) fn output(self: &Arc<Self>, data: &[u8]) {
if self.output.receiver_count() > 0 {
let _ = self.output.send(Bytes::copy_from_slice(data));
}
let start_writer = {
let mut clients = self.clients.lock().unwrap();
let terminals = clients.terminals();
let unanswered = clients.queries.feed(data, &terminals);
clients.answers.add(unanswered)
};
if start_writer {
let terminal = self.clone();
tokio::spawn(async move { terminal.write_answers().await });
}
}
async fn write_answers(&self) {
loop {
let pending = self.clients.lock().unwrap().answers.take();
if pending == 0 {
return;
}
if self.write(&CURSOR_ANSWER.repeat(pending)).await.is_err() {
self.clients.lock().unwrap().answers.stop();
return;
}
}
}
pub(super) fn end(&self, message: String) {
self.ended.send_replace(Some(message));
}
pub fn join(self: &Arc<Self>, size: Option<TermSize>, replay: bool) -> Client {
let size = size.filter(|s| s.cols > 0 && s.rows > 0);
let (output, replay, id) = {
let log = self.log.lock().unwrap();
let output = self.output.subscribe();
let rows = size.unwrap_or(self.configured).rows;
let mut screen = if replay {
without_cursor_queries(&log.raw.last_lines(usize::from(rows)))
} else {
Vec::new()
};
screen.drain(..screen.len().saturating_sub(1024 * 1024));
let mut clients = self.clients.lock().unwrap();
clients.next += 1;
let id = clients.next;
clients.sizes.insert(id, size);
if size.is_some() && replay {
clients.queries.replayed_to(id);
}
self.fit(&mut clients);
(output, screen, id)
};
Client {
terminal: self.clone(),
id,
output,
replay,
ended: self.ended.subscribe(),
}
}
pub fn clients(&self) -> usize {
self.clients.lock().unwrap().sizes.len()
}
pub fn size(&self) -> TermSize {
self.clients.lock().unwrap().current
}
pub async fn write(&self, data: &[u8]) -> io::Result<()> {
let _turn = self.writing.lock().await;
self.pty.write(data).await
}
fn fit(&self, clients: &mut Clients) {
let size = clients
.sizes
.values()
.flatten()
.copied()
.reduce(|a, b| TermSize {
cols: a.cols.min(b.cols),
rows: a.rows.min(b.rows),
})
.unwrap_or(self.configured);
if size != clients.current && self.pty.resize(size.cols, size.rows).is_ok() {
clients.current = size;
}
}
}
impl Clients {
fn terminals(&self) -> Vec<u64> {
self.sizes
.iter()
.filter(|(_, size)| size.is_some())
.map(|(id, _)| *id)
.collect()
}
}
const CURSOR_QUERY: &[u8] = b"\x1b[6n";
const CURSOR_ANSWER: &[u8] = b"\x1b[1;1R";
#[derive(Debug, Default)]
struct CursorQueries {
matched: usize,
seen_by: Vec<u64>,
}
impl CursorQueries {
fn feed(&mut self, data: &[u8], terminals: &[u64]) -> usize {
let mut unanswered = 0;
for &byte in data {
if byte == CURSOR_QUERY[self.matched] {
self.matched += 1;
} else {
self.matched = usize::from(byte == CURSOR_QUERY[0]);
}
if self.matched == 1 {
self.seen_by.clear();
self.seen_by.extend_from_slice(terminals);
}
if self.matched == CURSOR_QUERY.len() {
self.matched = 0;
if !self.seen_by.iter().any(|id| terminals.contains(id)) {
unanswered += 1;
}
}
}
unanswered
}
fn replayed_to(&mut self, id: u64) {
if self.matched > 0 {
self.seen_by.push(id);
}
}
}
#[derive(Debug, Default)]
struct Answers {
pending: usize,
writing: bool,
}
impl Answers {
fn add(&mut self, n: usize) -> bool {
if n == 0 {
return false;
}
self.pending = (self.pending + n).min(4);
!std::mem::replace(&mut self.writing, true)
}
fn take(&mut self) -> usize {
let pending = std::mem::take(&mut self.pending);
self.writing = pending > 0;
pending
}
fn stop(&mut self) {
*self = Answers::default();
}
}
fn without_cursor_queries(data: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(data.len());
let mut rest = data;
while let Some(at) = rest
.windows(CURSOR_QUERY.len())
.position(|w| w == CURSOR_QUERY)
{
out.extend_from_slice(&rest[..at]);
rest = &rest[at + CURSOR_QUERY.len()..];
}
out.extend_from_slice(rest);
out
}
pub struct Client {
terminal: Arc<Terminal>,
id: u64,
pub output: broadcast::Receiver<Bytes>,
pub replay: Vec<u8>,
pub ended: watch::Receiver<Option<String>>,
}
impl Client {
pub async fn write(&self, data: &[u8]) -> io::Result<()> {
self.terminal.write(data).await
}
pub fn writer(&self) -> Writer {
Writer(self.terminal.clone())
}
pub fn resize(&self, size: TermSize) {
if size.cols == 0 || size.rows == 0 {
return;
}
let mut clients = self.terminal.clients.lock().unwrap();
clients.sizes.insert(self.id, Some(size));
self.terminal.fit(&mut clients);
}
}
pub struct Writer(Arc<Terminal>);
impl Writer {
pub async fn write(&self, data: &[u8]) -> io::Result<()> {
self.0.write(data).await
}
}
impl Drop for Client {
fn drop(&mut self) {
let mut clients = self.terminal.clients.lock().unwrap();
clients.sizes.remove(&self.id);
self.terminal.fit(&mut clients);
}
}
#[cfg(test)]
mod tests {
use super::*;
const OUTPUT: &[u8] =
b"\x1b[1G\xe2\x96\xbd\x1b[6n\x1b[1G\x1b[K\x1b[6n\x1b[?25l\x1b[0m>> \x1b[4G";
#[test]
fn finds_queries_however_the_output_is_split() {
for at in 0..=OUTPUT.len() {
let mut queries = CursorQueries::default();
let (one, two) = OUTPUT.split_at(at);
let found = queries.feed(one, &[]) + queries.feed(two, &[]);
assert_eq!(found, 2, "split at {at}");
}
let mut queries = CursorQueries::default();
let found: usize = OUTPUT.iter().map(|b| queries.feed(&[*b], &[])).sum();
assert_eq!(found, 2, "a byte at a time");
}
#[test]
fn other_sequences_are_no_queries() {
let mut queries = CursorQueries::default();
assert_eq!(
queries.feed(b"\x1b[16n \x1b[6;1R \x1b[?6n \x1b6n [6n \x1b[6", &[]),
0
);
assert_eq!(queries.feed(b"m \x1b\x1b[6n", &[]), 1);
}
#[test]
fn a_query_an_attached_terminal_got_whole_is_left_to_it() {
let mut queries = CursorQueries::default();
assert_eq!(queries.feed(b"\x1b[6n", &[1]), 0);
assert_eq!(queries.feed(b"\x1b[", &[1]), 0);
assert_eq!(queries.feed(b"6n", &[1, 2]), 0);
assert_eq!(queries.feed(b"\x1b[6n", &[]), 1, "nobody attached");
}
#[test]
fn a_query_whose_start_a_terminal_missed_is_answered() {
let mut queries = CursorQueries::default();
assert_eq!(queries.feed(b"\x1b[", &[]), 0);
assert_eq!(queries.feed(b"6n", &[1]), 1, "joined in the middle");
assert_eq!(queries.feed(b"\x1b[", &[1]), 0);
assert_eq!(queries.feed(b"6n", &[2]), 1, "left in the middle");
}
#[test]
fn a_terminal_that_joins_with_the_start_of_a_query_answers_it() {
let mut queries = CursorQueries::default();
assert_eq!(queries.feed(b"\x1b[", &[]), 0);
queries.replayed_to(1);
assert_eq!(queries.feed(b"6n", &[1]), 0);
}
#[test]
fn one_writer_writes_a_few_answers_at_most() {
let mut answers = Answers::default();
assert!(!answers.add(0), "nothing to write");
assert!(answers.add(1), "the first answer starts a writer");
for _ in 0..100 {
assert!(!answers.add(1), "the writer is still at it");
}
assert_eq!(answers.take(), 4);
assert!(!answers.add(2), "the writer comes back for more");
assert_eq!(answers.take(), 2);
assert_eq!(answers.take(), 0);
assert!(answers.add(1), "a writer that ran out has stopped");
answers.stop();
assert!(answers.add(1), "so has one that gave up");
}
#[test]
fn replays_leave_out_whole_queries() {
assert_eq!(
without_cursor_queries(OUTPUT),
b"\x1b[1G\xe2\x96\xbd\x1b[1G\x1b[K\x1b[?25l\x1b[0m>> \x1b[4G"
);
assert_eq!(without_cursor_queries(b">> \x1b[6"), b">> \x1b[6");
}
}