use std::collections::{HashMap, VecDeque};
use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use crate::pal::error::{PalError, PalErrorKind};
use crate::pal::ids::PtyId;
use crate::pal::pseudoconsole::{Pseudoconsole, WindowSize};
struct PtyState {
size: WindowSize,
input: VecDeque<u8>,
output: VecDeque<u8>,
closed: bool,
withheld: bool,
read_failed: bool,
}
type CloseObserver = Arc<dyn Fn(PtyId) + Send + Sync>;
struct Inner {
next_id: AtomicU64,
ptys: Mutex<HashMap<PtyId, PtyState>>,
cond: Condvar,
on_close: Mutex<Option<CloseObserver>>,
}
#[derive(Clone)]
pub(crate) struct MemoryPseudoconsole {
inner: Arc<Inner>,
}
impl fmt::Debug for MemoryPseudoconsole {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MemoryPseudoconsole").finish()
}
}
impl MemoryPseudoconsole {
pub(crate) fn new() -> Self {
Self {
inner: Arc::new(Inner {
next_id: AtomicU64::new(1),
ptys: Mutex::new(HashMap::new()),
cond: Condvar::new(),
on_close: Mutex::new(None),
}),
}
}
pub(crate) fn on_close(&self, observer: impl Fn(PtyId) + Send + Sync + 'static) {
*self.inner.on_close.lock().expect("close observer lock") = Some(Arc::new(observer));
}
pub(crate) fn push_output(&self, pty: PtyId, data: &[u8]) {
let mut ptys = self.inner.ptys.lock().expect("pty map lock");
let state = ptys.get_mut(&pty).expect("pushing output into a live pty");
state.output.extend(data.iter().copied());
self.inner.cond.notify_all();
}
pub(crate) fn withhold_output(&self, pty: PtyId) {
let mut ptys = self.inner.ptys.lock().expect("pty map lock");
let state = ptys
.get_mut(&pty)
.expect("withholding output from a live pty");
state.withheld = true;
}
pub(crate) fn fail_output(&self, pty: PtyId) {
let mut ptys = self.inner.ptys.lock().expect("pty map lock");
let state = ptys.get_mut(&pty).expect("failing output from a live pty");
state.read_failed = true;
self.inner.cond.notify_all();
}
pub(crate) fn take_input(&self, pty: PtyId) -> Vec<u8> {
let mut ptys = self.inner.ptys.lock().expect("pty map lock");
let state = ptys.get_mut(&pty).expect("taking input from a live pty");
state.input.drain(..).collect()
}
pub(crate) fn size(&self, pty: PtyId) -> Option<WindowSize> {
let ptys = self.inner.ptys.lock().expect("pty map lock");
ptys.get(&pty).map(|state| state.size)
}
pub(crate) fn only_pty(&self) -> PtyId {
let ptys = self.inner.ptys.lock().expect("pty map lock");
let mut live = ptys.keys();
let only = *live.next().expect("exactly one live pty");
assert!(live.next().is_none(), "exactly one live pty");
only
}
}
impl Default for MemoryPseudoconsole {
fn default() -> Self {
Self::new()
}
}
impl Pseudoconsole for MemoryPseudoconsole {
fn create(&self, size: WindowSize) -> Result<PtyId, PalError> {
let id = PtyId(self.inner.next_id.fetch_add(1, Ordering::Relaxed));
self.inner.ptys.lock().expect("pty map lock").insert(
id,
PtyState {
size,
input: VecDeque::new(),
output: VecDeque::new(),
closed: false,
withheld: false,
read_failed: false,
},
);
Ok(id)
}
fn resize(&self, pty: PtyId, size: WindowSize) -> Result<(), PalError> {
let mut ptys = self.inner.ptys.lock().expect("pty map lock");
let state = ptys
.get_mut(&pty)
.ok_or_else(|| PalError::new(PalErrorKind::NotFound))?;
state.size = size;
Ok(())
}
fn write_input(&self, pty: PtyId, data: &[u8]) -> Result<(), PalError> {
let mut ptys = self.inner.ptys.lock().expect("pty map lock");
let state = ptys
.get_mut(&pty)
.ok_or_else(|| PalError::new(PalErrorKind::NotFound))?;
if state.closed {
return Err(PalError::new(PalErrorKind::NotFound));
}
state.input.extend(data.iter().copied());
self.inner.cond.notify_all();
Ok(())
}
#[cfg_attr(test, mutants::skip)]
fn read_output(&self, pty: PtyId) -> Result<Option<Vec<u8>>, PalError> {
let mut ptys = self.inner.ptys.lock().expect("pty map lock");
loop {
let Some(state) = ptys.get_mut(&pty) else {
return Err(PalError::new(PalErrorKind::NotFound));
};
if state.read_failed {
return Err(PalError::new(PalErrorKind::Other));
}
if !state.output.is_empty() && (state.closed || !state.withheld) {
return Ok(Some(state.output.drain(..).collect()));
}
if state.closed {
return Ok(None);
}
ptys = self.inner.cond.wait(ptys).expect("pty condvar");
}
}
fn finish(&self, pty: PtyId) {
let mut ptys = self.inner.ptys.lock().expect("pty map lock");
if let Some(state) = ptys.get_mut(&pty) {
state.closed = true;
}
self.inner.cond.notify_all();
}
fn close(&self, pty: PtyId) {
self.inner.ptys.lock().expect("pty map lock").remove(&pty);
self.inner.cond.notify_all();
let observer = self
.inner
.on_close
.lock()
.expect("close observer lock")
.clone();
if let Some(observer) = observer {
observer(pty);
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
#[test]
fn pumps_bytes_and_tracks_size() {
let host = MemoryPseudoconsole::new();
let pty = host
.create(WindowSize::new(80, 24).expect("a fixture size is not empty"))
.unwrap();
host.write_input(pty, b"in").unwrap();
assert_eq!(host.take_input(pty), b"in");
host.resize(
pty,
WindowSize::new(40, 10).expect("a fixture size is not empty"),
)
.unwrap();
assert_eq!(
host.size(pty),
Some(WindowSize::new(40, 10).expect("a fixture size is not empty"))
);
host.push_output(pty, b"out");
assert_eq!(
host.read_output(pty).unwrap().as_deref(),
Some(b"out".as_slice())
);
host.close(pty);
}
#[test]
fn a_finished_pty_reports_the_end_of_the_stream() {
let host = MemoryPseudoconsole::new();
let pty = host
.create(WindowSize::new(80, 24).expect("a fixture size is not empty"))
.unwrap();
host.push_output(pty, b"tail");
host.finish(pty);
assert_eq!(
host.read_output(pty).unwrap().as_deref(),
Some(b"tail".as_slice())
);
assert_eq!(host.read_output(pty).unwrap(), None);
host.close(pty);
}
#[test]
#[should_panic(expected = "live pty")]
fn pushing_into_an_unknown_pty_is_a_mistake_the_test_hears_about() {
MemoryPseudoconsole::new().push_output(PtyId(404), b"out");
}
}