use std::collections::VecDeque;
use std::env;
use std::fmt;
use std::io::{self, BufRead, BufReader, Read};
use std::mem;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::mpsc::{self, Receiver, SyncSender, TrySendError};
use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use taria::wire::{AppToBridge, BridgeToApp, InputId, InputStatus, MAX_LINE_BYTES};
use taria::{AgentInput, MAX_NODE_DEPTH, Node, PROTOCOL_VERSION, Role, Snapshot, TreeTooDeep};
use crate::FrameRecorder;
use crate::bound::{self, ROOT_BUDGET, SnapshotTooLarge};
use crate::transport::{self, Accepted, Connection, Listener, Reader, Writer};
const WRITE_TIMEOUT: Duration = Duration::from_secs(5);
const SHUTDOWN_FLUSH: Duration = Duration::from_millis(250);
const INPUT_QUEUE: usize = 256;
const ACK_QUEUE: usize = 1024;
const ACKABLE_INPUTS: usize = INPUT_QUEUE;
pub struct TariaLayer {
app_label: String,
socket_path: PathBuf,
inner: Option<Inner>,
bind_error: Option<io::Error>,
}
#[derive(Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct LayerStats {
pub dropped_inputs: u64,
pub stale_inputs: u64,
pub unknown_inputs: u64,
pub dropped_acks: u64,
pub truncated_snapshots: u64,
pub last_truncation: Option<TreeTooDeep>,
pub oversized_snapshots: u64,
pub last_oversize: Option<SnapshotTooLarge>,
}
struct Inner {
shared: Arc<Shared>,
input_rx: Receiver<QueuedInput>,
listener: Option<JoinHandle<()>>,
seq: u64,
last_root: Option<Node>,
last_raw: Option<Node>,
truncated: u64,
last_truncation: Option<TreeTooDeep>,
oversized: u64,
last_oversize: Option<SnapshotTooLarge>,
}
impl Inner {
fn is_newest_frame(&mut self, root: &mut Node) -> bool {
match self.last_raw.as_mut().or(self.last_root.as_mut()) {
Some(newest) => same_tree(root, newest),
None => false,
}
}
}
impl Drop for Inner {
fn drop(&mut self) {
for tree in [self.last_raw.take(), self.last_root.take()]
.into_iter()
.flatten()
{
drop_iteratively(tree);
}
}
}
struct QueuedInput {
generation: u64,
id: InputId,
input: AgentInput,
}
impl TariaLayer {
pub fn bind(app_label: &str) -> io::Result<Self> {
let socket_path = resolve_socket_path(app_label).map_err(invalid_label)?;
Self::bind_at(app_label, socket_path)
}
pub fn bind_at(app_label: &str, socket_path: impl Into<PathBuf>) -> io::Result<Self> {
let listener = Listener::bind(socket_path.into())?;
let socket_path = listener.path().to_path_buf();
let shared = Arc::new(Shared {
app_label: app_label.to_string(),
state: Mutex::new(State::default()),
cv: Condvar::new(),
dropped_inputs: AtomicU64::new(0),
stale_inputs: AtomicU64::new(0),
unknown_inputs: AtomicU64::new(0),
dropped_acks: AtomicU64::new(0),
});
let (input_tx, input_rx) = mpsc::sync_channel(INPUT_QUEUE);
let thread_shared = Arc::clone(&shared);
let handle = thread::Builder::new()
.name("taria-listener".into())
.spawn(move || accept_loop(listener, thread_shared, input_tx))?;
Ok(Self {
app_label: app_label.to_string(),
socket_path,
inner: Some(Inner {
shared,
input_rx,
listener: Some(handle),
seq: 0,
last_root: None,
last_raw: None,
truncated: 0,
last_truncation: None,
oversized: 0,
last_oversize: None,
}),
bind_error: None,
})
}
pub fn bind_or_disabled(app_label: &str) -> Self {
match resolve_socket_path(app_label) {
Ok(socket_path) => Self::bind_or_disabled_at(app_label, socket_path),
Err(err) => Self::disabled(app_label, PathBuf::new(), invalid_label(err)),
}
}
pub fn bind_or_disabled_at(app_label: &str, socket_path: impl Into<PathBuf>) -> Self {
let socket_path = socket_path.into();
match Self::bind_at(app_label, socket_path.clone()) {
Ok(layer) => layer,
Err(err) => Self::disabled(app_label, socket_path, err),
}
}
fn disabled(app_label: &str, socket_path: PathBuf, err: io::Error) -> Self {
Self {
app_label: app_label.to_string(),
socket_path,
inner: None,
bind_error: Some(err),
}
}
pub fn is_enabled(&self) -> bool {
self.inner.is_some()
}
pub fn bind_error(&self) -> Option<&io::Error> {
self.bind_error.as_ref()
}
pub fn socket_path(&self) -> &Path {
&self.socket_path
}
pub fn app_label(&self) -> &str {
&self.app_label
}
pub fn frame(&mut self) -> FrameRecorder<'_> {
FrameRecorder::new(self)
}
pub fn try_recv(&self) -> Option<(InputId, AgentInput)> {
let inner = self.inner.as_ref()?;
loop {
let queued = inner.input_rx.try_recv().ok()?;
if inner.shared.deliver(queued.generation, queued.id) {
return Some((queued.id, queued.input));
}
}
}
pub fn recv_timeout(&self, timeout: Duration) -> Option<(InputId, AgentInput)> {
let Some(inner) = self.inner.as_ref() else {
if Instant::now().checked_add(timeout).is_some() {
thread::sleep(timeout);
}
return None;
};
let deadline = Instant::now().checked_add(timeout);
loop {
let queued = match deadline {
Some(deadline) => {
let remaining = deadline.saturating_duration_since(Instant::now());
inner.input_rx.recv_timeout(remaining).ok()?
}
None => inner.input_rx.recv().ok()?,
};
if inner.shared.deliver(queued.generation, queued.id) {
return Some((queued.id, queued.input));
}
}
}
pub fn drain(&self, mut f: impl FnMut(AgentInput) -> InputStatus) {
while let Some((id, input)) = self.try_recv() {
let status = f(input);
if status != InputStatus::Delivered {
self.ack(id, status);
}
}
}
pub fn ack(&self, id: InputId, status: InputStatus) {
if let Some(inner) = self.inner.as_ref() {
inner.shared.queue_app_ack(id, status);
}
}
pub fn stats(&self) -> LayerStats {
let Some(inner) = self.inner.as_ref() else {
return LayerStats::default();
};
let shared = &inner.shared;
LayerStats {
dropped_inputs: shared.dropped_inputs.load(Ordering::Relaxed),
stale_inputs: shared.stale_inputs.load(Ordering::Relaxed),
unknown_inputs: shared.unknown_inputs.load(Ordering::Relaxed),
dropped_acks: shared.dropped_acks.load(Ordering::Relaxed),
truncated_snapshots: inner.truncated,
last_truncation: inner.last_truncation.clone(),
oversized_snapshots: inner.oversized,
last_oversize: inner.last_oversize.clone(),
}
}
pub fn publish(&mut self, nodes: impl IntoIterator<Item = Node>) {
self.publish_nodes(nodes.into_iter().collect());
}
pub(crate) fn publish_nodes(&mut self, nodes: Vec<Node>) {
let Some(inner) = self.inner.as_mut() else {
nodes.into_iter().for_each(drop_iteratively);
return;
};
let mut root = Node::new("app", Role::App)
.label(inner.shared.app_label.clone())
.children(nodes);
strip_disabled_actions(&mut root);
root.focused = !focused_below(&root);
if inner.is_newest_frame(&mut root) {
drop_iteratively(root);
inner.shared.restamp();
return;
}
let too_deep = root.check_depth().err();
let (mut tree, mut raw) = if too_deep.is_some() {
let mut cut = copy_within_depth(&mut root);
cut.focused = !focused_below(&cut);
(cut, Some(root))
} else {
(root, None)
};
let before = bound::measure_root(&tree);
let too_large = if before.is_some_and(|bytes| bytes <= ROOT_BUDGET) {
None
} else {
if raw.is_none() {
raw = Some(tree.clone());
}
let bounded = bound::cut_to_fit(&mut tree, ROOT_BUDGET, before);
tree.focused = !focused_below(&tree);
Some(bounded)
};
if let Some(replaced) = mem::replace(&mut inner.last_raw, raw) {
drop_iteratively(replaced);
}
if inner.last_root.as_ref() == Some(&tree) {
inner.shared.restamp();
return;
}
inner.seq += 1;
if let Some(too_deep) = too_deep {
inner.truncated = inner.truncated.saturating_add(1);
inner.last_truncation = Some(too_deep);
}
if let Some(bounded) = too_large {
inner.oversized = inner.oversized.saturating_add(1);
inner.last_oversize = Some(SnapshotTooLarge::new(bounded));
}
let snapshot = Snapshot::new(inner.seq, tree.clone());
inner.last_root = Some(tree);
inner.shared.publish(snapshot);
}
#[cfg(test)]
pub(crate) fn latest_snapshot(&self) -> Option<Snapshot> {
self.inner.as_ref()?.shared.lock_state().latest.clone()
}
}
impl fmt::Debug for TariaLayer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TariaLayer")
.field("app_label", &self.app_label)
.field("socket_path", &self.socket_path)
.field("enabled", &self.is_enabled())
.field("bind_error", &self.bind_error)
.field("seq", &self.inner.as_ref().map(|inner| inner.seq))
.finish_non_exhaustive()
}
}
impl Drop for TariaLayer {
fn drop(&mut self) {
let Some(inner) = self.inner.as_mut() else {
return;
};
{
let mut state = inner.shared.lock_state();
state.shutdown = true;
inner.shared.cv.notify_all();
}
if let Some(handle) = inner.listener.take() {
let _ = handle.join();
}
}
}
struct Shared {
app_label: String,
state: Mutex<State>,
cv: Condvar,
dropped_inputs: AtomicU64,
stale_inputs: AtomicU64,
unknown_inputs: AtomicU64,
dropped_acks: AtomicU64,
}
#[derive(Default)]
struct State {
latest: Option<Snapshot>,
latest_stamp: Option<(u64, InputId)>,
epoch: u64,
acks: VecDeque<(InputId, InputStatus)>,
delivered: VecDeque<InputId>,
delivered_generation: u64,
generation: u64,
shutdown: bool,
}
impl Shared {
fn lock_state(&self) -> MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
fn is_shutdown(&self) -> bool {
self.lock_state().shutdown
}
fn queue_ack(&self, id: InputId, status: InputStatus) {
let mut state = self.lock_state();
self.push_ack(&mut state, id, status);
drop(state);
self.cv.notify_all();
}
fn queue_app_ack(&self, id: InputId, status: InputStatus) {
let mut state = self.lock_state();
if !state.was_delivered(id) {
return;
}
self.push_ack(&mut state, id, status);
drop(state);
self.cv.notify_all();
}
fn push_ack(&self, state: &mut State, id: InputId, status: InputStatus) {
if state.acks.len() >= ACK_QUEUE {
state.acks.pop_front();
self.dropped_acks.fetch_add(1, Ordering::Relaxed);
}
state.acks.push_back((id, status));
}
fn retire_generation(&self) {
self.lock_state().generation += 1;
}
fn clear_acks(&self) {
self.lock_state().acks.clear();
}
fn publish(&self, mut snapshot: Snapshot) {
let mut guard = self.lock_state();
let state = &mut *guard;
let stamp = state.stamp();
snapshot.after_input = stamp.map(|(_, id)| id);
state.latest_stamp = stamp;
state.latest = Some(snapshot);
state.epoch += 1;
drop(guard);
self.cv.notify_all();
}
fn restamp(&self) {
let mut guard = self.lock_state();
let state = &mut *guard;
let stamp = state.stamp();
if stamp.is_none() || stamp == state.latest_stamp {
return;
}
let Some(latest) = state.latest.as_mut() else {
return;
};
latest.after_input = stamp.map(|(_, id)| id);
state.latest_stamp = stamp;
state.epoch += 1;
drop(guard);
self.cv.notify_all();
}
fn deliver(&self, generation: u64, id: InputId) -> bool {
let mut state = self.lock_state();
if state.generation != generation {
drop(state);
self.stale_inputs.fetch_add(1, Ordering::Relaxed);
return false;
}
state.record_delivered(generation, id);
self.push_ack(&mut state, id, InputStatus::Delivered);
drop(state);
self.cv.notify_all();
true
}
}
impl State {
fn record_delivered(&mut self, generation: u64, id: InputId) {
if self.delivered_generation != generation {
self.delivered.clear();
self.delivered_generation = generation;
}
if self.delivered.len() >= ACKABLE_INPUTS {
self.delivered.pop_front();
}
self.delivered.push_back(id);
}
fn was_delivered(&self, id: InputId) -> bool {
self.delivered_generation == self.generation && self.delivered.contains(&id)
}
fn stamp(&self) -> Option<(u64, InputId)> {
if self.delivered_generation != self.generation {
return None;
}
self.delivered.back().map(|&id| (self.generation, id))
}
}
fn focused_below(root: &Node) -> bool {
let mut pending = vec![root.children.iter()];
while let Some(siblings) = pending.last_mut() {
match siblings.next() {
Some(node) if node.focused => return true,
Some(node) => pending.push(node.children.iter()),
None => {
pending.pop();
}
}
}
false
}
fn strip_disabled_actions(root: &mut Node) {
let mut pending = vec![root];
while let Some(node) = pending.pop() {
if node.disabled {
node.actions.clear();
}
pending.extend(node.children.iter_mut());
}
}
fn same_tree(a: &mut Node, b: &mut Node) -> bool {
if !same_node(a, b) {
return false;
}
let mut pending = vec![a.children.iter_mut().zip(b.children.iter_mut())];
while let Some(pairs) = pending.last_mut() {
match pairs.next() {
Some((a, b)) => {
if !same_node(a, b) {
return false;
}
pending.push(a.children.iter_mut().zip(b.children.iter_mut()));
}
None => {
pending.pop();
}
}
}
true
}
fn same_node(a: &mut Node, b: &mut Node) -> bool {
if a.children.len() != b.children.len() {
return false;
}
let a_children = mem::take(&mut a.children);
let b_children = mem::take(&mut b.children);
let same = *a == *b;
a.children = a_children;
b.children = b_children;
same
}
pub(crate) fn drop_iteratively(root: Node) {
let mut pending = vec![root];
while let Some(mut node) = pending.pop() {
pending.extend(
node.children
.drain(..)
.filter(|child| !child.children.is_empty()),
);
}
}
fn shallow_copy(node: &mut Node) -> Node {
let children = mem::take(&mut node.children);
let copy = node.clone();
node.children = children;
copy
}
fn copy_within_depth(root: &mut Node) -> Node {
let mut stack = vec![(shallow_copy(root), root.children.iter_mut())];
while let Some((_, children)) = stack.last_mut() {
let next = children.next();
let level = stack.len() + 1;
match next {
Some(child) if level >= MAX_NODE_DEPTH => {
let copy = shallow_copy(child);
if let Some((parent, _)) = stack.last_mut() {
parent.children.push(copy);
}
}
Some(child) => {
let copy = shallow_copy(child);
stack.push((copy, child.children.iter_mut()));
}
None if stack.len() == 1 => break,
None => {
if let Some((done, _)) = stack.pop()
&& let Some((parent, _)) = stack.last_mut()
{
parent.children.push(done);
}
}
}
}
let copied = stack.pop().map(|(copy, _)| copy);
drop(stack);
copied.unwrap_or_else(|| shallow_copy(root))
}
fn accept_loop(mut listener: Listener, shared: Arc<Shared>, input_tx: SyncSender<QueuedInput>) {
while let Some(Accepted { connection, waited }) = listener.accept(|| shared.is_shutdown()) {
let _ = serve_client(connection, &shared, &input_tx, waited);
}
}
fn serve_client(
connection: Connection,
shared: &Arc<Shared>,
input_tx: &SyncSender<QueuedInput>,
waited: bool,
) -> io::Result<()> {
connection.set_write_timeout(WRITE_TIMEOUT)?;
let (initial_snapshot, initial_epoch, generation) = {
let mut state = shared.lock_state();
state.acks.clear();
state.generation += 1;
let initial = state.latest.clone().map(|mut snapshot| {
snapshot.after_input = None;
snapshot
});
(initial, state.epoch, state.generation)
};
let alive = Arc::new(AtomicBool::new(true));
let (mut writer, reader) = match start_connection(
connection,
shared,
input_tx,
initial_snapshot,
&alive,
generation,
waited,
) {
Ok(started) => started,
Err(err) => {
shared.retire_generation();
shared.clear_acks();
return Err(err);
}
};
writer_loop(&mut writer, shared, &alive, initial_epoch);
let _ = writer.shutdown();
let _ = reader.join();
shared.clear_acks();
Ok(())
}
fn start_connection(
connection: Connection,
shared: &Arc<Shared>,
input_tx: &SyncSender<QueuedInput>,
initial_snapshot: Option<Snapshot>,
alive: &Arc<AtomicBool>,
generation: u64,
waited: bool,
) -> io::Result<(Writer, JoinHandle<()>)> {
let (mut reader, mut writer) = connection.split()?;
let starts_mid_line = if waited {
discard_unsolicited(&mut reader, shared)?
} else {
false
};
write_line(
&mut writer,
&AppToBridge::hello(shared.app_label.clone(), PROTOCOL_VERSION).stamps(true),
)?;
if let Some(snapshot) = initial_snapshot
&& !shared.is_shutdown()
{
write_line(&mut writer, &AppToBridge::Snapshot(snapshot))?;
}
let reader = thread::Builder::new().name("taria-reader".into()).spawn({
let alive = Arc::clone(alive);
let shared = Arc::clone(shared);
let input_tx = input_tx.clone();
move || reader_loop(reader, input_tx, alive, shared, generation, starts_mid_line)
})?;
Ok((writer, reader))
}
fn discard_unsolicited(reader: &mut Reader, shared: &Shared) -> io::Result<bool> {
let mut unsolicited = Vec::new();
let read = reader.with_nonblocking_reads(|ready| {
let read = read_unsolicited(ready, &mut unsolicited);
shared
.stale_inputs
.fetch_add(count_input_lines(&unsolicited), Ordering::Relaxed);
read
})?;
read?;
Ok(unsolicited.last().is_some_and(|&byte| byte != b'\n'))
}
fn read_unsolicited(mut reader: impl Read, pending: &mut Vec<u8>) -> io::Result<()> {
let mut chunk = [0u8; 8 * 1024];
loop {
match reader.read(&mut chunk) {
Ok(0) => return Ok(()),
Ok(n) => {
let Some(read) = chunk.get(..n) else {
return Err(io::Error::other(
"a read reported more bytes than it was given",
));
};
let room = MAX_LINE_BYTES.saturating_sub(pending.len());
if n > room {
pending.extend_from_slice(read.get(..room).unwrap_or_default());
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"the client wrote more than {MAX_LINE_BYTES} bytes before its \
handshake"
),
));
}
pending.extend_from_slice(read);
}
Err(err) if err.kind() == io::ErrorKind::Interrupted => {}
Err(err) if err.kind() == io::ErrorKind::WouldBlock => return Ok(()),
Err(err) => return Err(err),
}
}
}
fn count_input_lines(bytes: &[u8]) -> u64 {
let inputs = bytes
.split_inclusive(|&byte| byte == b'\n')
.filter(|line| line.ends_with(b"\n"))
.filter(|line| {
matches!(
serde_json::from_slice::<BridgeToApp>(line),
Ok(BridgeToApp::Input { .. })
)
})
.count();
u64::try_from(inputs).unwrap_or(u64::MAX)
}
fn reader_loop(
stream: Reader,
input_tx: SyncSender<QueuedInput>,
alive: Arc<AtomicBool>,
shared: Arc<Shared>,
generation: u64,
mut starts_mid_line: bool,
) {
let mut reader = BufReader::new(stream);
let mut buf = Vec::new();
loop {
buf.clear();
let read = (&mut reader)
.take((MAX_LINE_BYTES + 1) as u64)
.read_until(b'\n', &mut buf);
let n = match read {
Ok(n) => n,
Err(_) => break,
};
if n == 0 {
break; }
if buf.last() != Some(&b'\n') {
break;
}
if std::mem::take(&mut starts_mid_line) {
continue;
}
let Ok(msg) = serde_json::from_slice::<BridgeToApp>(&buf) else {
continue;
};
let BridgeToApp::Input { id, input, .. } = msg else {
continue;
};
if matches!(input, AgentInput::Unknown) {
shared.unknown_inputs.fetch_add(1, Ordering::Relaxed);
shared.queue_ack(id, InputStatus::Ignored);
continue;
}
let queued = QueuedInput {
generation,
id,
input,
};
match input_tx.try_send(queued) {
Ok(()) => {}
Err(TrySendError::Full(_)) => {
shared.dropped_inputs.fetch_add(1, Ordering::Relaxed);
shared.queue_ack(id, InputStatus::Dropped);
}
Err(TrySendError::Disconnected(_)) => break,
}
}
let mut state = shared.lock_state();
state.generation += 1;
alive.store(false, Ordering::SeqCst);
shared.cv.notify_all();
drop(state);
}
fn writer_loop(stream: &mut Writer, shared: &Shared, alive: &AtomicBool, mut last_epoch: u64) {
loop {
let (acks, snapshot) = {
let mut state = shared.lock_state();
loop {
if !alive.load(Ordering::SeqCst) {
return;
}
if state.shutdown {
let queued = std::mem::take(&mut state.acks);
drop(state);
flush_acks(stream, queued);
return;
}
if !state.acks.is_empty() || state.epoch != last_epoch {
let acks = std::mem::take(&mut state.acks);
let snapshot = if state.epoch == last_epoch {
None
} else {
last_epoch = state.epoch;
state.latest.clone()
};
break (acks, snapshot);
}
state = shared
.cv
.wait(state)
.unwrap_or_else(PoisonError::into_inner);
}
};
let mut acks = acks.into_iter();
while let Some((id, status)) = acks.next() {
let unwritten = std::iter::once((id, status)).chain(&mut acks);
if stop_writing(stream, shared, alive, unwritten) {
return;
}
if write_line(stream, &AppToBridge::ack(id, status)).is_err() {
return;
}
}
let Some(snapshot) = snapshot else {
continue;
};
if stop_writing(stream, shared, alive, std::iter::empty()) {
return;
}
if write_line(stream, &AppToBridge::Snapshot(snapshot)).is_err() {
return;
}
}
}
fn stop_writing(
stream: &mut Writer,
shared: &Shared,
alive: &AtomicBool,
unwritten: impl IntoIterator<Item = (InputId, InputStatus)>,
) -> bool {
let mut state = shared.lock_state();
if !alive.load(Ordering::SeqCst) {
return true;
}
if !state.shutdown {
return false;
}
let queued = std::mem::take(&mut state.acks);
drop(state);
flush_acks(stream, unwritten.into_iter().chain(queued));
true
}
fn write_line(stream: &mut Writer, msg: &AppToBridge) -> io::Result<()> {
let mut line = serde_json::to_vec(msg).map_err(io::Error::other)?;
line.push(b'\n');
let deadline = Instant::now()
.checked_add(WRITE_TIMEOUT)
.ok_or_else(|| io::Error::other("the write deadline is past what the clock can hold"))?;
stream.write_all_before(&line, deadline)
}
fn flush_acks(stream: &mut Writer, acks: impl IntoIterator<Item = (InputId, InputStatus)>) {
let Some(deadline) = Instant::now().checked_add(SHUTDOWN_FLUSH) else {
return;
};
let mut lines = Vec::new();
for (id, status) in acks {
let Ok(line) = serde_json::to_vec(&AppToBridge::ack(id, status)) else {
break;
};
lines.extend_from_slice(&line);
lines.push(b'\n');
}
let _ = stream.write_all_before(&lines, deadline);
}
#[cfg(not(windows))]
fn resolve_socket_path(app_label: &str) -> Result<PathBuf, taria::socket::InvalidAppLabel> {
taria::socket::resolve_path(
env::var_os("TARIA_SOCK"),
env::var_os("XDG_RUNTIME_DIR"),
&env::temp_dir(),
&transport::user_identity(),
app_label,
)
}
#[cfg(not(windows))]
fn invalid_label(err: taria::socket::InvalidAppLabel) -> io::Error {
io::Error::new(io::ErrorKind::InvalidInput, err.to_string())
}
#[cfg(windows)]
fn resolve_socket_path(app_label: &str) -> io::Result<PathBuf> {
let user = transport::user_identity();
let taria_sock = env::var_os("TARIA_SOCK");
let overridden = taria_sock.as_ref().is_some_and(|name| !name.is_empty());
let resolved = taria::socket::resolve_pipe_name(
taria_sock,
user.as_deref().unwrap_or_default(),
app_label,
);
match (resolved, user) {
(Ok(name), _) => Ok(PathBuf::from(name)),
(Err(taria::socket::InvalidPipeName::User(_)), Err(err)) => Err(err),
(Err(err), _) => {
let message = match err {
taria::socket::InvalidPipeName::Label(_) => err.to_string(),
_ if overridden => format!("$TARIA_SOCK takes a pipe name on Windows: {err}"),
_ => err.to_string(),
};
Err(io::Error::new(io::ErrorKind::InvalidInput, message))
}
}
}
#[cfg(windows)]
fn invalid_label(err: io::Error) -> io::Error {
err
}
#[cfg(test)]
mod tests {
use std::fs;
#[cfg(unix)]
use std::io::Write;
#[cfg(unix)]
use std::os::unix::fs::{MetadataExt, PermissionsExt};
#[cfg(unix)]
use std::os::unix::net::{UnixListener, UnixStream};
use super::*;
use crate::test_util::bind_test_layer;
#[cfg(unix)]
use crate::transport::{absolutize, ensure_private_dir};
#[cfg(unix)]
fn scratch_path(name: &str) -> PathBuf {
let path = env::temp_dir().join(format!("taria-dirtest-{}-{name}", std::process::id()));
let _ = fs::remove_dir_all(&path);
let _ = fs::remove_file(&path);
path
}
fn private_temp_dir(prefix: &str) -> tempfile::TempDir {
let mut builder = tempfile::Builder::new();
builder.prefix(prefix);
#[cfg(unix)]
builder.permissions(fs::Permissions::from_mode(0o700));
builder.tempdir().expect("create test dir")
}
fn test_shared(app_label: &str) -> Arc<Shared> {
Arc::new(Shared {
app_label: app_label.to_string(),
state: Mutex::new(State::default()),
cv: Condvar::new(),
dropped_inputs: AtomicU64::new(0),
stale_inputs: AtomicU64::new(0),
unknown_inputs: AtomicU64::new(0),
dropped_acks: AtomicU64::new(0),
})
}
#[cfg(unix)]
#[test]
fn ensure_private_dir_accepts_fresh_0700_dir() {
let dir = scratch_path("fresh");
ensure_private_dir(&dir).expect("fresh private dir should be accepted");
let meta = fs::symlink_metadata(&dir).unwrap();
assert!(meta.file_type().is_dir());
assert_eq!(meta.mode() & 0o077, 0, "mode: {:03o}", meta.mode() & 0o777);
ensure_private_dir(&dir).expect("existing private dir should be accepted");
let _ = fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn ensure_private_dir_rejects_symlinked_dir() {
let target = scratch_path("symlink-target");
fs::create_dir_all(&target).unwrap();
fs::set_permissions(&target, fs::Permissions::from_mode(0o700)).unwrap();
let link = scratch_path("symlink-link");
std::os::unix::fs::symlink(&target, &link).unwrap();
let err = ensure_private_dir(&link).expect_err("symlinked dir must be rejected");
assert!(err.to_string().contains("symlink"), "err: {err}");
let _ = fs::remove_file(&link);
let _ = fs::remove_dir_all(&target);
}
#[cfg(unix)]
#[test]
fn ensure_private_dir_rejects_world_accessible_dir() {
let dir = scratch_path("world-writable");
fs::create_dir_all(&dir).unwrap();
fs::set_permissions(&dir, fs::Permissions::from_mode(0o777)).unwrap();
let err = ensure_private_dir(&dir).expect_err("0777 dir must be rejected");
assert!(err.to_string().contains("group/world"), "err: {err}");
let _ = fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn absolutize_joins_relative_paths_onto_cwd() {
let cwd = env::current_dir().unwrap();
let bare = absolutize(PathBuf::from("bare.sock")).unwrap();
assert!(bare.is_absolute());
assert_eq!(bare, cwd.join("bare.sock"));
assert_eq!(bare.parent(), Some(cwd.as_path()));
let nested = absolutize(PathBuf::from("sub/dir/app.sock")).unwrap();
assert_eq!(nested, cwd.join("sub/dir/app.sock"));
let absolute = absolutize(PathBuf::from("/already/abs.sock")).unwrap();
assert_eq!(absolute, PathBuf::from("/already/abs.sock"));
}
#[cfg(unix)]
#[test]
fn bind_at_bare_filename_vets_cwd() {
let cwd = env::current_dir().unwrap();
let name = format!("taria-bare-vet-{}.sock", std::process::id());
let result = TariaLayer::bind_at("bare-demo", &name);
match ensure_private_dir(&cwd) {
Ok(()) => {
let layer = result.expect("private cwd: bare filename should bind in it");
assert_eq!(layer.socket_path(), cwd.join(&name));
drop(layer); }
Err(_) => {
assert!(
result.is_err(),
"bare filename in an unvetted cwd must be refused, not bound"
);
assert!(
!cwd.join(&name).exists(),
"no socket may be created in an unvetted cwd"
);
}
}
}
#[cfg(unix)]
#[test]
fn bind_at_refuses_an_over_long_socket_path() {
let dir = private_temp_dir("taria-longpath-");
let path = dir.path().join(format!("{}.sock", "x".repeat(120)));
let Err(err) = TariaLayer::bind_at("toolong", &path) else {
panic!("over-long path must be refused, not bound");
};
let message = err.to_string();
assert!(
message.contains(&format!("{} byte", taria::socket::MAX_SOCKET_PATH_BYTES)),
"err: {message}"
);
assert!(message.contains("TARIA_SOCK"), "err: {message}");
assert!(!path.exists(), "no socket file may be left behind");
}
#[test]
fn a_label_that_is_not_a_file_name_binds_nothing() {
for label in ["/etc/cron.d/evil", "../../../tmp/pwn", "sub/dir"] {
let Err(err) = TariaLayer::bind(label) else {
panic!("bind accepted the label {label:?}");
};
assert_eq!(err.kind(), io::ErrorKind::InvalidInput, "label: {label}");
assert!(err.to_string().contains(label), "err: {err}");
let layer = TariaLayer::bind_or_disabled(label);
assert!(!layer.is_enabled(), "label: {label}");
assert_eq!(layer.app_label(), label, "the label is reported as given");
assert_eq!(
layer.socket_path(),
Path::new(""),
"a refused label resolves to no path"
);
let message = layer
.bind_error()
.expect("a disabled layer says why")
.to_string();
assert!(message.contains(label), "err: {message}");
}
}
#[test]
fn disabled_layer_is_inert() {
let dir = private_temp_dir("taria-disabled-");
let blocker = dir.path().join("not-a-directory");
fs::write(&blocker, b"").expect("create blocker file");
let socket_path = blocker.join("app.sock");
let mut layer = TariaLayer::bind_or_disabled_at("disabled", socket_path.clone());
assert!(!layer.is_enabled());
assert!(layer.bind_error().is_some(), "a disabled layer says why");
assert_eq!(layer.socket_path(), socket_path);
assert_eq!(layer.app_label(), "disabled");
assert_eq!(layer.stats(), LayerStats::default());
let printed = format!("{layer:?}");
assert!(
printed.contains("enabled: false, bind_error: Some("),
"{printed}"
);
assert!(printed.ends_with(", seq: None, .. }"), "{printed}");
assert_eq!(layer.try_recv(), None);
assert_eq!(layer.recv_timeout(Duration::from_millis(1)), None);
layer.ack(InputId::new(0), InputStatus::Ignored);
let mut drained = 0;
layer.drain(|_| {
drained += 1;
InputStatus::Ignored
});
assert_eq!(drained, 0, "a disabled layer has nothing to drain");
let mut rec = layer.frame();
rec.push(Node::new("pane", Role::Pane));
rec.publish();
layer.publish([Node::new("pane", Role::Pane)]);
assert_eq!(layer.latest_snapshot(), None);
drop(layer);
assert!(!socket_path.exists(), "a disabled layer creates no socket");
assert!(blocker.exists(), "dropping it must unlink nothing");
}
fn chain(depth: usize) -> Node {
let mut node = Node::new(format!("n{depth}"), Role::TreeItem);
for level in (1..depth).rev() {
node = Node::new(format!("n{level}"), Role::TreeItem).child(node);
}
node
}
#[test]
fn a_tree_at_the_depth_limit_is_published_untouched() {
let mut layer = bind_test_layer("taria-layer-", "atlimit");
layer.publish([chain(MAX_NODE_DEPTH - 1)]);
let root = layer
.latest_snapshot()
.expect("a tree at the limit publishes")
.root;
assert_eq!(root.depth(), MAX_NODE_DEPTH);
assert!(root.check_depth().is_ok());
assert_eq!(layer.stats().truncated_snapshots, 0);
assert_eq!(layer.stats().last_truncation, None);
}
#[test]
fn a_tree_over_the_depth_limit_is_cut_rather_than_dropped() {
let mut layer = bind_test_layer("taria-layer-", "deep");
assert!(layer.latest_snapshot().is_none(), "nothing published yet");
layer.publish([chain(MAX_NODE_DEPTH)]);
let snapshot = layer
.latest_snapshot()
.expect("a tree over the limit is published cut, never withheld");
assert_eq!(snapshot.seq, 1);
assert_eq!(snapshot.root.depth(), MAX_NODE_DEPTH);
assert!(snapshot.root.check_depth().is_ok());
let mut node = &snapshot.root;
for level in 1..MAX_NODE_DEPTH {
assert_eq!(node.children.len(), 1, "level {level} lost a sibling");
node = &node.children[0];
}
assert_eq!(node.id.0, format!("n{}", MAX_NODE_DEPTH - 1));
assert!(node.children.is_empty(), "the node at the limit is a leaf");
assert_eq!(layer.stats().truncated_snapshots, 1);
let too_deep = layer.stats().last_truncation.expect("the cut is recorded");
assert_eq!(too_deep.depth(), MAX_NODE_DEPTH + 1);
assert_eq!(too_deep.deepest().0, format!("n{MAX_NODE_DEPTH}"));
}
#[test]
fn a_cut_that_removes_the_only_focused_node_leaves_focus_on_the_root() {
let mut layer = bind_test_layer("taria-layer-", "deepfocus");
let mut deep = Node::new("leaf", Role::TreeItem).focused(true);
for level in (1..MAX_NODE_DEPTH).rev() {
deep = Node::new(format!("n{level}"), Role::TreeItem).child(deep);
}
layer.publish([deep]);
let root = layer.latest_snapshot().expect("published").root;
assert_eq!(layer.stats().truncated_snapshots, 1);
assert!(
root.focused,
"the focused node was cut, so the root has to carry focus"
);
assert!(!subtree_has_focus_below_root(&root));
}
fn subtree_has_focus_below_root(root: &Node) -> bool {
focused_below(root)
}
#[test]
fn a_tree_deeper_than_the_call_stack_is_compared_and_cut_without_recursing() {
const DEEP: usize = 100_000;
let mut deep = chain(DEEP);
let mut same = chain(DEEP);
let mut other = Node::new(format!("n{DEEP}"), Role::TreeItem).label("changed");
for level in (1..DEEP).rev() {
other = Node::new(format!("n{level}"), Role::TreeItem).child(other);
}
assert!(!focused_below(&deep));
assert!(same_tree(&mut deep, &mut same));
assert!(!same_tree(&mut deep, &mut other));
let cut = copy_within_depth(&mut deep);
assert_eq!(cut.depth(), MAX_NODE_DEPTH);
assert!(cut.check_depth().is_ok());
assert_eq!(deep.depth(), DEEP, "the tree as built is left whole");
for tree in [deep, same, other] {
drop_iteratively(tree);
}
}
fn on_a_small_stack(test: impl FnOnce() + Send + 'static) {
let outcome = thread::Builder::new()
.name("small-stack".into())
.stack_size(512 * 1024)
.spawn(test)
.expect("spawn a test thread")
.join();
if let Err(panic) = outcome {
std::panic::resume_unwind(panic);
}
}
#[test]
fn a_tree_deeper_than_the_call_stack_is_freed_without_recursing() {
const DEEP: usize = 100_000;
on_a_small_stack(|| {
let mut layer = bind_test_layer("taria-layer-", "deepfree");
layer.publish([chain(DEEP)]);
layer.publish([chain(DEEP)]);
assert_eq!(layer.latest_snapshot().expect("published").seq, 1);
layer.publish([chain(DEEP + 1)]);
assert_eq!(layer.latest_snapshot().expect("published").seq, 1);
layer.publish([chain(DEEP), Node::new("new", Role::Text)]);
assert_eq!(layer.latest_snapshot().expect("published").seq, 2);
assert_eq!(layer.stats().truncated_snapshots, 2);
layer.frame().push(chain(DEEP));
drop(layer);
let mut disabled =
TariaLayer::disabled("deepfree", PathBuf::new(), io::Error::other("test"));
disabled.publish([chain(DEEP)]);
});
}
#[test]
fn only_the_branch_that_runs_over_is_cut() {
let mut layer = bind_test_layer("taria-layer-", "deepwide");
layer.publish([
Node::new("shallow", Role::Text).label("kept"),
chain(MAX_NODE_DEPTH * 4),
]);
let root = layer.latest_snapshot().expect("published").root;
assert_eq!(root.depth(), MAX_NODE_DEPTH);
assert_eq!(root.children.len(), 2);
assert_eq!(root.children[0].id.0, "shallow");
assert_eq!(root.children[0].label.as_deref(), Some("kept"));
assert_eq!(layer.stats().truncated_snapshots, 1);
}
#[test]
fn the_same_over_deep_tree_published_twice_counts_once() {
let mut layer = bind_test_layer("taria-layer-", "deeptwice");
layer.publish([chain(MAX_NODE_DEPTH * 2)]);
layer.publish([chain(MAX_NODE_DEPTH * 2)]);
assert_eq!(layer.latest_snapshot().expect("published").seq, 1);
assert_eq!(layer.stats().truncated_snapshots, 1);
layer.publish([chain(MAX_NODE_DEPTH * 2), Node::new("new", Role::Text)]);
assert_eq!(layer.latest_snapshot().expect("published").seq, 2);
assert_eq!(layer.stats().truncated_snapshots, 2);
}
fn draft(value: String) -> Node {
Node::new("draft", Role::TextInput)
.value(value)
.focused(true)
}
fn root_around_draft(label: &str, value: String) -> Node {
Node::new("app", Role::App).label(label).child(draft(value))
}
fn line_len(snapshot: Snapshot) -> usize {
serde_json::to_vec(&AppToBridge::Snapshot(snapshot))
.expect("serialize a snapshot")
.len()
}
#[test]
fn a_tree_at_the_size_budget_is_published_untouched_and_one_byte_over_is_cut() {
let mut layer = bind_test_layer("taria-layer-", "atsize");
let base = serde_json::to_vec(&root_around_draft("atsize", String::new()))
.expect("serialize a root")
.len();
let at = "x".repeat(ROOT_BUDGET - base);
layer.publish([draft(at.clone())]);
let snapshot = layer
.latest_snapshot()
.expect("a tree at the budget publishes");
assert_eq!(snapshot.root, root_around_draft("atsize", at.clone()));
assert!(line_len(snapshot) <= MAX_LINE_BYTES);
assert_eq!(layer.stats().oversized_snapshots, 0);
assert_eq!(layer.stats().last_oversize, None);
let over = format!("{at}x");
layer.publish([draft(over.clone())]);
let snapshot = layer
.latest_snapshot()
.expect("a tree over is published cut");
assert_eq!(snapshot.seq, 2);
let value = snapshot.root.children[0].value.clone().expect("a value");
assert!(value.ends_with('\u{2026}'), "the cut is marked");
assert!(over.starts_with(value.trim_end_matches('\u{2026}')));
assert!(snapshot.root.children[0].focused, "focus is untouched");
assert!(line_len(snapshot) <= MAX_LINE_BYTES);
assert_eq!(layer.stats().oversized_snapshots, 1);
let too_large = layer.stats().last_oversize.expect("the cut is recorded");
let unbounded = Snapshot::new(u64::MAX, root_around_draft("atsize", over))
.after(InputId::new(u64::MAX));
assert_eq!(too_large.bytes(), line_len(unbounded));
assert_eq!(too_large.bytes(), MAX_LINE_BYTES + 1);
assert_eq!(too_large.limit(), MAX_LINE_BYTES);
assert_eq!(too_large.strings_trimmed(), 1);
assert_eq!(too_large.nodes_dropped(), 0);
assert_eq!(too_large.largest().0, "draft");
let message = too_large.to_string();
assert!(message.contains("`draft`"), "{message}");
assert!(message.contains(&MAX_LINE_BYTES.to_string()), "{message}");
}
#[test]
fn the_same_oversized_tree_published_twice_counts_once() {
let mut layer = bind_test_layer("taria-layer-", "bigtwice");
let big = "y".repeat(MAX_LINE_BYTES * 2);
layer.publish([draft(big.clone())]);
layer.publish([draft(big.clone())]);
assert_eq!(layer.latest_snapshot().expect("published").seq, 1);
assert_eq!(layer.stats().oversized_snapshots, 1);
layer.publish([draft(format!("{big}z"))]);
assert_eq!(layer.latest_snapshot().expect("published").seq, 1);
assert_eq!(layer.stats().oversized_snapshots, 1);
layer.publish([draft(format!("z{big}"))]);
assert_eq!(layer.latest_snapshot().expect("published").seq, 2);
assert_eq!(layer.stats().oversized_snapshots, 2);
}
#[test]
fn a_size_bound_that_drops_the_only_focused_node_leaves_focus_on_the_root() {
let mut layer = bind_test_layer("taria-layer-", "bigfocus");
let rows = (0..60_000).map(|n| {
Node::new(format!("row-{n:05}"), Role::ListItem)
.label(format!("Task {n:05}"))
.focused(n == 59_999)
});
layer.publish([Node::new("list", Role::List).children(rows)]);
let root = layer.latest_snapshot().expect("published").root;
assert_eq!(layer.stats().oversized_snapshots, 1);
let too_large = layer.stats().last_oversize.expect("recorded");
assert!(too_large.nodes_dropped() > 0, "the last row went");
assert!(
root.focused,
"the focused row was dropped, so the root has to carry focus"
);
assert!(!subtree_has_focus_below_root(&root));
}
#[test]
fn a_node_that_could_never_fit_leaves_the_draft_beside_it_whole() {
let mut layer = bind_test_layer("taria-layer-", "unfit");
let value = "d".repeat(300_000);
let chrome = [
Node::new("status", Role::Status).value("1 draft"),
Node::new("quit", Role::Button)
.label("Quit")
.action(taria::Action::Activate),
];
let mut nodes = vec![
Node::new("I".repeat(2_000_000), Role::Text),
draft(value.clone()),
];
nodes.extend(chrome.clone());
layer.publish(nodes);
let snapshot = layer.latest_snapshot().expect("published");
let ids: Vec<&str> = snapshot
.root
.children
.iter()
.map(|child| child.id.0.as_str())
.collect();
assert_eq!(ids, ["draft", "status", "quit"]);
assert_eq!(snapshot.root.children[0], draft(value.clone()));
assert_eq!(snapshot.root.children[1..], chrome);
let alone = line_len(Snapshot::new(1, root_around_draft("unfit", value)));
let line = line_len(snapshot);
assert!(line > alone && line <= MAX_LINE_BYTES, "{line} of {alone}");
let too_large = layer.stats().last_oversize.expect("recorded");
assert_eq!(too_large.strings_trimmed(), 0);
assert_eq!(too_large.nodes_dropped(), 1);
}
#[test]
fn an_unchanged_oversized_tree_is_bounded_once_however_often_it_is_drawn() {
let mut layer = bind_test_layer("taria-layer-", "bigstill");
let rows: Vec<Node> = (0..5000)
.map(|n| Node::new(format!("row-{n:05}"), Role::ListItem).label(format!("Task {n:05}")))
.collect();
let frame = |text: &str| {
[
draft(text.to_string()),
Node::new("list", Role::List).children(rows.clone()),
]
};
let big = "d".repeat(MAX_LINE_BYTES * 3);
let start = bound::runs();
let nodes = frame(&big);
let started = Instant::now();
layer.publish(nodes);
let first = started.elapsed();
let once = bound::runs();
assert_eq!(once.measured - start.measured, 1);
assert_eq!(once.cut - start.cut, 1);
let mut redrawn = Duration::ZERO;
for _ in 1..100 {
let nodes = frame(&big);
let started = Instant::now();
layer.publish(nodes);
redrawn += started.elapsed();
}
assert_eq!(
bound::runs(),
once,
"an unchanged frame is neither measured nor cut"
);
assert_eq!(layer.latest_snapshot().expect("published").seq, 1);
assert_eq!(layer.stats().oversized_snapshots, 1);
let per_frame = redrawn / 99;
eprintln!("first frame {first:?}, each unchanged frame after it {per_frame:?}");
assert!(
per_frame * 4 < first,
"an unchanged frame took {per_frame:?}, against {first:?} for the one bounded"
);
let edited = format!("{big}z");
for _ in 0..10 {
layer.publish(frame(&edited));
}
let after = bound::runs();
assert_eq!(after.cut - once.cut, 1);
assert_eq!(layer.latest_snapshot().expect("published").seq, 1);
assert_eq!(layer.stats().oversized_snapshots, 1);
}
#[test]
fn a_tree_that_fits_is_measured_once_and_held_once() {
let mut layer = bind_test_layer("taria-layer-", "fitstill");
let frame = || {
[Node::new("list", Role::List)
.children((0..100).map(|n| Node::new(format!("row-{n}"), Role::ListItem)))]
};
let start = bound::runs();
for _ in 0..10 {
layer.publish(frame());
}
let runs = bound::runs();
assert_eq!(runs.measured - start.measured, 1);
assert_eq!(runs.cut, start.cut);
let inner = layer.inner.as_ref().expect("enabled");
assert!(
inner.last_raw.is_none(),
"no second copy of a tree that fits"
);
assert_eq!(layer.latest_snapshot().expect("published").seq, 1);
layer.publish([draft("y".repeat(MAX_LINE_BYTES * 2))]);
let inner = layer.inner.as_ref().expect("enabled");
assert!(
inner.last_raw.is_some(),
"a cut tree keeps what it was cut from"
);
}
#[test]
fn a_deep_tree_cut_for_depth_and_then_for_size_still_fills_the_line() {
let mut layer = bind_test_layer("taria-layer-", "deepfill");
let leaves = |level: usize| {
(0..200).map(move |n| {
Node::new(format!("leaf-{level}-{n}"), Role::Text).label("x".repeat(250))
})
};
let mut branch = Node::new("n39", Role::TreeItem).children(leaves(39));
for level in (1..39).rev() {
branch = Node::new(format!("n{level}"), Role::TreeItem)
.children(leaves(level))
.child(branch);
}
layer.publish([branch]);
let snapshot = layer.latest_snapshot().expect("published");
assert_eq!(layer.stats().truncated_snapshots, 1);
assert_eq!(layer.stats().oversized_snapshots, 1);
assert_eq!(
snapshot.root.children[0].children.len(),
201,
"the first level is kept whole, branch and all"
);
let len = line_len(snapshot);
assert!(len <= MAX_LINE_BYTES);
assert!(
len * 10 >= MAX_LINE_BYTES * 9,
"{len} of {MAX_LINE_BYTES} bytes used"
);
}
#[test]
fn publish_wraps_nodes_like_the_recorder_does() {
let mut layer = bind_test_layer("taria-layer-", "publish");
layer.publish([
Node::new("a", Role::Text),
Node::new("b", Role::Button).focused(true),
]);
let root = layer.latest_snapshot().expect("published").root;
assert_eq!(root.id.0, "app");
assert_eq!(root.label.as_deref(), Some("publish"));
assert!(!root.focused, "a focused node unfocuses the auto root");
let ids: Vec<&str> = root.children.iter().map(|c| c.id.0.as_str()).collect();
assert_eq!(ids, ["a", "b"]);
}
#[test]
fn a_disabled_node_is_published_with_no_actions() {
let mut layer = bind_test_layer("taria-layer-", "disabled");
let save = |disabled: bool| {
Node::new("save", Role::Button)
.label("Save")
.disabled(disabled)
.focused(true)
.actions([taria::Action::Activate, taria::Action::Custom("x".into())])
};
let nested = Node::new("menu", Role::Menu).disabled(true).child(
Node::new("open", Role::MenuItem)
.disabled(true)
.action(taria::Action::Activate),
);
let quit = Node::new("quit", Role::Button).action(taria::Action::Activate);
let empty = Node::new("idle", Role::Button);
layer.publish([save(true), nested, quit.clone(), empty.clone()]);
let snapshot = layer.latest_snapshot().expect("published");
let root = &snapshot.root;
let saved = &root.children[0];
assert!(saved.actions.is_empty(), "{saved:?}");
assert!(saved.disabled);
assert!(saved.focused && saved.label.as_deref() == Some("Save"));
assert!(root.children[1].actions.is_empty());
assert!(root.children[1].children[0].actions.is_empty());
assert_eq!(root.children[2], quit);
assert_eq!(root.children[3], empty);
assert!(!root.children[3].disabled);
layer.publish([
save(true),
Node::new("menu", Role::Menu).disabled(true).child(
Node::new("open", Role::MenuItem)
.disabled(true)
.action(taria::Action::Activate),
),
quit.clone(),
empty.clone(),
]);
assert_eq!(layer.latest_snapshot().expect("published").seq, 1);
layer.publish([save(false)]);
let snapshot = layer.latest_snapshot().expect("published");
assert_eq!(snapshot.seq, 2);
assert_eq!(snapshot.root.children[0], save(false));
}
#[test]
fn a_number_that_is_not_finite_never_ships_or_breaks_the_dedupe() {
let mut layer = bind_test_layer("taria-layer-", "nan");
let bar = || {
let mut bar = Node::new("bar", Role::ProgressBar).range(f64::NAN, 0.0, f64::INFINITY);
bar.value_max = taria::Number::new(f64::NEG_INFINITY);
bar
};
layer.publish([bar()]);
layer.publish([bar()]);
layer.publish([bar()]);
let snapshot = layer.latest_snapshot().expect("published");
assert_eq!(snapshot.seq, 1, "the same frame drawn again is not resent");
let published = &snapshot.root.children[0];
assert_eq!(
(published.value_now, published.value_max),
(None, None),
"{published:?}"
);
assert_eq!(published.value_min.map(taria::Number::get), Some(0.0));
let line = serde_json::to_string(&snapshot).expect("serializes");
assert!(
!line.contains("NaN") && !line.contains("inf") && !line.contains("null"),
"{line}"
);
}
#[cfg(unix)]
#[test]
fn writer_sends_pending_acks_before_a_pending_snapshot() {
let (server, client) = UnixStream::pair().expect("socket pair");
client
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("read timeout");
let shared = test_shared("ackorder");
{
let mut state = shared.lock_state();
state.acks.push_back((7.into(), InputStatus::Delivered));
state.latest = Some(Snapshot::new(1, Node::new("app", Role::App)));
state.epoch = 1;
}
let alive = Arc::new(AtomicBool::new(true));
let writer = thread::spawn({
let shared = Arc::clone(&shared);
let alive = Arc::clone(&alive);
move || {
let mut server = Writer::from(server);
writer_loop(&mut server, &shared, &alive, 0);
}
});
let mut reader = BufReader::new(client);
let mut read_message = || {
let mut line = String::new();
let n = reader.read_line(&mut line).expect("read a message");
assert!(n > 0, "writer closed the stream");
serde_json::from_str::<AppToBridge>(&line).expect("parse a message")
};
assert_eq!(
read_message(),
AppToBridge::ack(InputId::new(7), InputStatus::Delivered),
"an agent that saw the snapshot first could not tell whether it \
already reflects its input"
);
let AppToBridge::Snapshot(snapshot) = read_message() else {
panic!("expected the snapshot after the ack");
};
assert_eq!(snapshot.seq, 1);
shared.lock_state().shutdown = true;
shared.cv.notify_all();
writer.join().expect("writer thread");
}
#[cfg(unix)]
fn fill_until_blocked(stream: &UnixStream) -> usize {
stream.set_nonblocking(true).expect("non-blocking");
let chunk = [b'x'; 4096];
let mut filled = 0;
loop {
match (&*stream).write(&chunk) {
Ok(n) => filled += n,
Err(err) if err.kind() == io::ErrorKind::WouldBlock => break,
Err(err) => panic!("filling the socket failed: {err}"),
}
}
stream.set_nonblocking(false).expect("blocking again");
filled
}
#[cfg(unix)]
fn spawn_writer(server: UnixStream, shared: &Arc<Shared>) -> Receiver<()> {
let (tx, rx) = mpsc::channel();
thread::spawn({
let shared = Arc::clone(shared);
move || {
let mut server = Writer::from(server);
let alive = AtomicBool::new(true);
writer_loop(&mut server, &shared, &alive, 0);
drop(server);
let _ = tx.send(());
}
});
rx
}
#[cfg(unix)]
#[test]
fn a_shutdown_writes_the_queued_acks_and_not_the_snapshot() {
let (server, client) = UnixStream::pair().expect("socket pair");
client
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("read timeout");
let shared = test_shared("exitflush");
{
let mut state = shared.lock_state();
state.acks.push_back((4.into(), InputStatus::Delivered));
state.acks.push_back((4.into(), InputStatus::Ignored));
state.latest = Some(Snapshot::new(1, Node::new("app", Role::App)));
state.epoch = 1;
state.shutdown = true;
}
let done = spawn_writer(server, &shared);
let mut reader = BufReader::new(client);
let mut lines = Vec::new();
loop {
let mut line = String::new();
if reader.read_line(&mut line).expect("read a message") == 0 {
break;
}
lines.push(serde_json::from_str::<AppToBridge>(&line).expect("parse a message"));
}
assert_eq!(
lines,
[
AppToBridge::ack(InputId::new(4), InputStatus::Delivered),
AppToBridge::ack(InputId::new(4), InputStatus::Ignored),
],
"every queued ack goes out, in order, and the snapshot behind them does not"
);
assert!(
done.recv_timeout(Duration::from_secs(5)).is_ok(),
"the writer returns once the acks are out"
);
assert!(shared.lock_state().acks.is_empty(), "the queue was taken");
}
#[cfg(unix)]
#[test]
fn a_shutdown_mid_batch_writes_the_rest_of_it_and_what_queued_since() {
let (server, client) = UnixStream::pair().expect("socket pair");
client
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("read timeout");
let junk = fill_until_blocked(&server);
let shared = test_shared("midbatch");
{
let mut state = shared.lock_state();
state.acks.push_back((1.into(), InputStatus::Delivered));
state.acks.push_back((2.into(), InputStatus::Delivered));
state.latest = Some(Snapshot::new(1, Node::new("app", Role::App)));
state.epoch = 1;
}
let done = spawn_writer(server, &shared);
let deadline = Instant::now() + Duration::from_secs(5);
while !shared.lock_state().acks.is_empty() {
assert!(Instant::now() < deadline, "the writer never took the batch");
thread::sleep(Duration::from_millis(1));
}
{
let mut state = shared.lock_state();
state.acks.push_back((3.into(), InputStatus::Ignored));
state.shutdown = true;
shared.cv.notify_all();
}
let mut reader = BufReader::new(client);
let mut skipped = vec![0; junk];
reader.read_exact(&mut skipped).expect("read past the junk");
let mut lines = Vec::new();
loop {
let mut line = String::new();
if reader.read_line(&mut line).expect("read a message") == 0 {
break;
}
lines.push(serde_json::from_str::<AppToBridge>(&line).expect("parse a message"));
}
assert_eq!(
lines,
[
AppToBridge::ack(InputId::new(1), InputStatus::Delivered),
AppToBridge::ack(InputId::new(2), InputStatus::Delivered),
AppToBridge::ack(InputId::new(3), InputStatus::Ignored),
],
"the batch in hand first, then what was queued behind it, and no snapshot"
);
assert!(done.recv_timeout(Duration::from_secs(5)).is_ok());
}
#[cfg(unix)]
#[test]
fn a_shutdown_while_the_last_ack_is_written_never_writes_the_snapshot() {
let (server, client) = UnixStream::pair().expect("socket pair");
client
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("read timeout");
let junk = fill_until_blocked(&server);
let shared = test_shared("lastack");
{
let mut state = shared.lock_state();
state.acks.push_back((1.into(), InputStatus::Delivered));
state.latest = Some(Snapshot::new(1, Node::new("app", Role::App)));
state.epoch = 1;
}
let done = spawn_writer(server, &shared);
let deadline = Instant::now() + Duration::from_secs(5);
while !shared.lock_state().acks.is_empty() {
assert!(Instant::now() < deadline, "the writer never took the batch");
thread::sleep(Duration::from_millis(1));
}
thread::sleep(Duration::from_millis(100));
{
let mut state = shared.lock_state();
state.shutdown = true;
shared.cv.notify_all();
}
let mut reader = BufReader::new(client);
let mut skipped = vec![0; junk];
reader.read_exact(&mut skipped).expect("read past the junk");
let mut lines = Vec::new();
loop {
let mut line = String::new();
if reader.read_line(&mut line).expect("read a message") == 0 {
break;
}
lines.push(serde_json::from_str::<AppToBridge>(&line).expect("parse a message"));
}
assert_eq!(
lines,
[AppToBridge::ack(InputId::new(1), InputStatus::Delivered)],
"the ack in hand goes out, and the snapshot behind it does not"
);
assert!(done.recv_timeout(Duration::from_secs(5)).is_ok());
}
#[cfg(unix)]
#[test]
fn the_shutdown_flush_gives_up_on_a_bridge_that_stopped_reading() {
let (server, client) = UnixStream::pair().expect("socket pair");
fill_until_blocked(&server);
server
.set_write_timeout(Some(WRITE_TIMEOUT))
.expect("write timeout");
let shared = test_shared("stuckflush");
{
let mut state = shared.lock_state();
for id in 0..20 {
state
.acks
.push_back((InputId::new(id), InputStatus::Delivered));
}
state.shutdown = true;
}
let done = spawn_writer(server, &shared);
let margin = Duration::from_secs(2);
done.recv_timeout(SHUTDOWN_FLUSH + margin)
.expect("the flush must give up on a stuck bridge within its bound");
drop(client);
}
struct Scripted(VecDeque<io::Result<&'static [u8]>>);
impl Read for Scripted {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let bytes = self
.0
.pop_front()
.expect("read again after it would have blocked")?;
buf[..bytes.len()].copy_from_slice(bytes);
Ok(bytes.len())
}
}
#[test]
fn input_written_before_the_hello_is_read_off_and_counted_by_the_line() {
let script = [
Ok(&br#"{"type":"input","id":1,"input":{"kind":"key","key":"q"}}"#[..]),
Err(io::ErrorKind::Interrupted.into()),
Ok(&b"\nnot json\n"[..]),
Ok(&br#"{"type":"input","id":2,"input":{"kind":"paste"}}"#[..]),
Ok(&b"\n{\"type\":\"bye\"}\n"[..]),
Ok(&br#"{"type":"input","id":3,"#[..]),
Err(io::ErrorKind::WouldBlock.into()),
];
let mut pending = Vec::new();
read_unsolicited(Scripted(script.into()), &mut pending)
.expect("an interrupted read is retried, and a would-block ends the pass");
assert!(
pending.ends_with(br#""id":3,"#),
"everything ready was read"
);
assert_eq!(count_input_lines(&pending), 2);
let mut pending = Vec::new();
read_unsolicited(&b"{}\n"[..], &mut pending).expect("EOF ends the pass");
assert_eq!(pending, b"{}\n");
let line = vec![b' '; MAX_LINE_BYTES];
read_unsolicited(line.as_slice(), &mut Vec::new()).expect("exactly the cap is accepted");
let flood = vec![b'\n'; MAX_LINE_BYTES + 1];
let mut pending = Vec::new();
let err = read_unsolicited(flood.as_slice(), &mut pending)
.expect_err("more than a line's worth before the hello is refused");
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
assert!(
pending.len() <= MAX_LINE_BYTES,
"and nothing past the cap is kept"
);
}
#[test]
fn every_complete_line_before_the_cap_is_counted() {
let line = b"{\"type\":\"input\",\"id\":1,\"input\":{\"kind\":\"key\",\"key\":\"q\"}}\n";
let fit = MAX_LINE_BYTES / line.len();
assert_ne!(
MAX_LINE_BYTES % line.len(),
0,
"the cap falls inside a line"
);
let flood = line.repeat(fit + 400);
let (first, rest) = flood.split_at(100);
let mut pending = Vec::new();
let err = read_unsolicited(first.chain(rest), &mut pending)
.expect_err("more than a line's worth before the hello is refused");
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
assert_eq!(pending.len(), MAX_LINE_BYTES, "kept up to the cap");
assert_eq!(
count_input_lines(&pending),
u64::try_from(fit).expect("fits"),
"every input that ends before the cap is counted, and the one it cuts is not"
);
}
#[cfg(unix)]
#[test]
fn drop_returns_even_when_the_listener_cannot_be_woken() {
let layer = bind_test_layer("taria-drophang-", "unwakeable");
fs::remove_file(layer.socket_path()).expect("remove the socket file");
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
drop(layer);
let _ = tx.send(());
});
assert!(
rx.recv_timeout(Duration::from_secs(5)).is_ok(),
"drop must not join a listener that can no longer be woken"
);
}
#[cfg(unix)]
#[test]
fn drop_neither_hangs_nor_unlinks_when_the_socket_was_replaced() {
let dir = private_temp_dir("taria-replaced-");
let path = dir.path().join("replaced.sock");
let layer = TariaLayer::bind_at("replaced", &path).expect("bind the first layer");
fs::remove_file(&path).expect("remove this layer's socket");
let usurper = UnixListener::bind(&path).expect("bind another listener at the path");
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
drop(layer);
let _ = tx.send(());
});
assert!(
rx.recv_timeout(Duration::from_secs(5)).is_ok(),
"drop must not wait on a listener no connection can reach any more"
);
assert!(
path.exists(),
"the socket at the path belongs to another listener; drop must leave it"
);
assert!(
UnixStream::connect(&path).is_ok(),
"and it must still be served"
);
drop(usurper);
}
#[cfg(unix)]
#[test]
fn binding_never_deletes_something_that_is_not_a_socket() {
let dir = private_temp_dir("taria-notasocket-");
let path = dir.path().join("config");
let content = b"Host example\n User me\n";
fs::write(&path, content).expect("create the file that must survive");
let Err(err) = TariaLayer::bind_at("notasocket", &path) else {
panic!("binding over a regular file must be refused");
};
assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
let message = err.to_string();
assert!(message.contains("not a socket"), "err: {message}");
assert!(message.contains("a regular file"), "err: {message}");
assert_eq!(
fs::read(&path).expect("the file is still there").as_slice(),
content,
"the file at the socket path must be untouched"
);
let link = dir.path().join("config-link");
std::os::unix::fs::symlink(&path, &link).expect("create the symlink");
let Err(err) = TariaLayer::bind_at("notasocket", &link) else {
panic!("binding over a symlink must be refused");
};
assert!(err.to_string().contains("a symlink"), "err: {err}");
assert!(link.exists(), "the symlink must be left in place");
}
#[cfg(unix)]
#[test]
fn a_socket_nothing_is_listening_on_is_replaced() {
let dir = private_temp_dir("taria-stalesock-");
let path = dir.path().join("stale.sock");
drop(UnixListener::bind(&path).expect("bind a listener to abandon"));
assert!(path.exists(), "the abandoned socket file stays behind");
let layer = TariaLayer::bind_at("stale", &path).expect("a dead socket is replaced");
assert!(layer.is_enabled());
assert!(
UnixStream::connect(&path).is_ok(),
"the socket bound over the stale one must serve"
);
}
#[cfg(unix)]
#[test]
fn binding_over_a_live_socket_is_refused_and_leaves_it_serving() {
let dir = private_temp_dir("taria-twice-");
let path = dir.path().join("twice.sock");
let first = TariaLayer::bind_at("twice", &path).expect("the first instance binds");
let Err(err) = TariaLayer::bind_at("twice", &path) else {
panic!("a second instance must not take over the first's socket");
};
assert_eq!(err.kind(), io::ErrorKind::AddrInUse);
let message = err.to_string();
assert!(message.contains("already serving"), "err: {message}");
assert!(
message.contains("TARIA_SOCK"),
"the error must say how to run a second instance: {message}"
);
let second = TariaLayer::bind_or_disabled_at("twice", path.clone());
assert!(!second.is_enabled(), "the second instance must not be live");
assert_eq!(
second.bind_error().map(io::Error::kind),
Some(io::ErrorKind::AddrInUse),
"and it must say why"
);
drop(second);
assert!(first.is_enabled());
assert!(
UnixStream::connect(&path).is_ok(),
"the first instance must still be reachable"
);
drop(first);
assert!(!path.exists(), "the first instance still owns its socket");
}
#[test]
fn the_ack_queue_is_bounded_and_drops_the_oldest_answers() {
let layer = bind_test_layer("taria-layer-", "ackbound");
let shared = &layer.inner.as_ref().expect("a bound layer").shared;
let overflow = 10;
for id in 0..(ACK_QUEUE + overflow) as u64 {
shared.queue_ack(InputId::new(id), InputStatus::Dropped);
}
let state = shared.lock_state();
assert_eq!(state.acks.len(), ACK_QUEUE, "the queue must stay bounded");
assert_eq!(
state.acks.front().map(|(id, _)| *id),
Some(InputId::new(overflow as u64)),
"the oldest answers are the ones given up; a waiter on those has \
long since timed out"
);
assert_eq!(
state.acks.back().map(|(id, _)| *id),
Some(InputId::new((ACK_QUEUE + overflow - 1) as u64)),
"the newest answer is the one an agent is still waiting on"
);
drop(state);
assert_eq!(
layer.stats().dropped_acks,
overflow as u64,
"each dropped answer must be counted once"
);
}
#[test]
fn an_app_ack_is_dropped_unless_its_input_belongs_to_the_live_connection() {
let shared = test_shared("appack");
shared.lock_state().generation = 7;
assert!(
shared.deliver(7, InputId::new(5)),
"the live connection's input delivers"
);
shared.lock_state().acks.clear();
shared.queue_app_ack(InputId::new(5), InputStatus::Ignored);
assert_eq!(
shared.lock_state().acks.pop_front(),
Some((InputId::new(5), InputStatus::Ignored)),
"refining the ack of a live input is the whole point of `ack`"
);
shared.retire_generation();
shared.lock_state().generation += 1;
shared.queue_app_ack(InputId::new(5), InputStatus::Ignored);
assert!(
shared.lock_state().acks.is_empty(),
"an id from a bridge that is gone must answer nobody"
);
let generation = shared.lock_state().generation;
for id in 100..=(100 + ACKABLE_INPUTS as u64) {
assert!(shared.deliver(generation, InputId::new(id)));
}
shared.lock_state().acks.clear();
shared.queue_app_ack(InputId::new(100), InputStatus::Ignored);
assert!(shared.lock_state().acks.is_empty(), "id 100 was forgotten");
shared.queue_app_ack(InputId::new(101), InputStatus::Ignored);
assert_eq!(
shared.lock_state().acks.pop_front(),
Some((InputId::new(101), InputStatus::Ignored)),
"everything still remembered stays answerable"
);
}
#[test]
fn a_disabled_layer_does_not_block_forever_on_an_unholdable_timeout() {
let dir = private_temp_dir("taria-disabledwait-");
let blocker = dir.path().join("not-a-directory");
fs::write(&blocker, b"").expect("create blocker file");
let layer = TariaLayer::bind_or_disabled_at("disabledwait", blocker.join("app.sock"));
assert!(!layer.is_enabled());
let started = Instant::now();
assert_eq!(layer.recv_timeout(Duration::MAX), None);
let waited = started.elapsed();
assert!(
waited < Duration::from_secs(1),
"a disabled layer waited {waited:?} on a timeout it can never serve"
);
let started = Instant::now();
assert_eq!(layer.recv_timeout(Duration::from_millis(50)), None);
assert!(started.elapsed() >= Duration::from_millis(50));
}
#[test]
fn focus_detection_reaches_every_level_below_the_root() {
let unfocused = Node::new("a", Role::Pane)
.focused(true)
.child(Node::new("b", Role::Text));
assert!(
!focused_below(&unfocused),
"the root's own flag is not below it"
);
let deep_focus = Node::new("a", Role::Pane)
.child(Node::new("b", Role::List).child(Node::new("c", Role::ListItem).focused(true)));
assert!(focused_below(&deep_focus));
}
}