use std::collections::VecDeque;
use std::env;
use std::fs;
use std::io::{self, BufRead, BufReader, Read, Write};
use std::mem;
use std::net::Shutdown;
use std::os::unix::fs::DirBuilderExt;
use std::os::unix::fs::FileTypeExt;
use std::os::unix::fs::MetadataExt;
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::net::{UnixListener, UnixStream};
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};
const WRITE_TIMEOUT: Duration = Duration::from_secs(5);
const WRITE_CHUNK: usize = 2 * 1024;
const WRITE_WAIT: Duration = Duration::from_millis(50);
const SHUTDOWN_FLUSH: Duration = Duration::from_millis(250);
const INPUT_QUEUE: usize = 256;
const ACK_QUEUE: usize = 1024;
const ACKABLE_INPUTS: usize = INPUT_QUEUE;
const ACCEPT_POLL: Duration = Duration::from_millis(50);
const SOCKET_PROBE_TIMEOUT: Duration = Duration::from_millis(250);
pub struct TariaLayer {
app_label: String,
socket_path: PathBuf,
inner: Option<Inner>,
bind_error: Option<io::Error>,
}
struct Inner {
shared: Arc<Shared>,
input_rx: Receiver<QueuedInput>,
listener: Option<JoinHandle<()>>,
socket_ident: Option<(u64, u64)>,
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 socket_path = absolutize(socket_path.into())?;
taria::socket::check_path_len(&socket_path)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err.to_string()))?;
if let Some(parent) = socket_path.parent()
&& !parent.as_os_str().is_empty()
{
ensure_private_dir(parent)?;
}
clear_socket_path(&socket_path)?;
let listener = UnixListener::bind(&socket_path)?;
if let Err(err) = listener.set_nonblocking(true) {
drop(listener);
let _ = fs::remove_file(&socket_path);
return Err(err);
}
let socket_ident = socket_ident(&socket_path);
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));
let handle = match handle {
Ok(handle) => handle,
Err(err) => {
let _ = fs::remove_file(&socket_path);
return Err(err);
}
};
Ok(Self {
app_label: app_label.to_string(),
socket_path,
inner: Some(Inner {
shared,
input_rx,
listener: Some(handle),
socket_ident,
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)),
}
}
fn bind_or_disabled_at(app_label: &str, socket_path: PathBuf) -> Self {
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<AgentInput> {
self.try_recv_with_id().map(|(_, input)| input)
}
pub fn recv_timeout(&self, timeout: Duration) -> Option<AgentInput> {
self.recv_timeout_with_id(timeout).map(|(_, input)| input)
}
pub fn try_recv_with_id(&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_with_id(&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)) {
while let Some(input) = self.try_recv() {
f(input);
}
}
pub fn drain_with_ids(&self, mut f: impl FnMut(InputId, AgentInput)) {
while let Some((id, input)) = self.try_recv_with_id() {
f(id, input);
}
}
pub fn drain_acking(&self, mut f: impl FnMut(AgentInput) -> InputStatus) {
while let Some((id, input)) = self.try_recv_with_id() {
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 dropped_inputs(&self) -> u64 {
self.inner.as_ref().map_or(0, |inner| {
inner.shared.dropped_inputs.load(Ordering::Relaxed)
})
}
pub fn stale_inputs(&self) -> u64 {
self.inner
.as_ref()
.map_or(0, |inner| inner.shared.stale_inputs.load(Ordering::Relaxed))
}
pub fn unknown_inputs(&self) -> u64 {
self.inner.as_ref().map_or(0, |inner| {
inner.shared.unknown_inputs.load(Ordering::Relaxed)
})
}
pub fn dropped_acks(&self) -> u64 {
self.inner
.as_ref()
.map_or(0, |inner| inner.shared.dropped_acks.load(Ordering::Relaxed))
}
pub fn truncated_snapshots(&self) -> u64 {
self.inner.as_ref().map_or(0, |inner| inner.truncated)
}
pub fn last_truncation(&self) -> Option<TreeTooDeep> {
self.inner.as_ref()?.last_truncation.clone()
}
pub fn oversized_snapshots(&self) -> u64 {
self.inner.as_ref().map_or(0, |inner| inner.oversized)
}
pub fn last_oversize(&self) -> Option<SnapshotTooLarge> {
self.inner.as_ref()?.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);
root.focused = !focused_below(&root);
if inner.is_newest_frame(&mut root) {
drop_iteratively(root);
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) {
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);
let mut state = inner.shared.lock_state();
state.latest = Some(snapshot);
state.epoch += 1;
drop(state);
inner.shared.cv.notify_all();
}
#[cfg(test)]
pub(crate) fn latest_snapshot(&self) -> Option<Snapshot> {
self.inner.as_ref()?.shared.lock_state().latest.clone()
}
}
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();
}
remove_own_socket(&self.socket_path, inner.socket_ident);
}
}
fn remove_own_socket(path: &Path, bound: Option<(u64, u64)>) {
if bound.is_some() && socket_ident(path) == bound {
let _ = fs::remove_file(path);
}
}
fn socket_ident(path: &Path) -> Option<(u64, u64)> {
let meta = fs::symlink_metadata(path).ok()?;
Some((meta.dev(), meta.ino()))
}
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>,
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 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 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 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(listener: UnixListener, shared: Arc<Shared>, input_tx: SyncSender<QueuedInput>) {
let mut waited = false;
loop {
if shared.is_shutdown() {
return;
}
match listener.accept() {
Ok((stream, _addr)) => {
if shared.is_shutdown() {
return;
}
if stream.set_nonblocking(false).is_err() {
continue;
}
let _ = serve_client(stream, &shared, &input_tx, waited);
waited = true;
}
Err(err) if err.kind() == io::ErrorKind::WouldBlock => {
waited = false;
thread::sleep(ACCEPT_POLL);
}
Err(_) => {
if shared.is_shutdown() {
return;
}
thread::sleep(ACCEPT_POLL);
}
}
}
}
fn serve_client(
stream: UnixStream,
shared: &Arc<Shared>,
input_tx: &SyncSender<QueuedInput>,
waited: bool,
) -> io::Result<()> {
stream.set_write_timeout(Some(WRITE_TIMEOUT))?;
let (initial_snapshot, initial_epoch, generation) = {
let mut state = shared.lock_state();
state.acks.clear();
state.generation += 1;
(state.latest.clone(), state.epoch, state.generation)
};
let alive = Arc::new(AtomicBool::new(true));
let (mut write_stream, reader) = match start_connection(
stream,
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 write_stream, shared, &alive, initial_epoch);
let _ = write_stream.shutdown(Shutdown::Both);
let _ = reader.join();
shared.clear_acks();
Ok(())
}
fn start_connection(
stream: UnixStream,
shared: &Arc<Shared>,
input_tx: &SyncSender<QueuedInput>,
initial_snapshot: Option<Snapshot>,
alive: &Arc<AtomicBool>,
generation: u64,
waited: bool,
) -> io::Result<(UnixStream, JoinHandle<()>)> {
let mut write_stream = stream.try_clone()?;
let starts_mid_line = if waited {
discard_unsolicited(&stream, shared)?
} else {
false
};
write_line(
&mut write_stream,
&AppToBridge::hello(shared.app_label.clone(), PROTOCOL_VERSION),
)?;
if let Some(snapshot) = initial_snapshot
&& !shared.is_shutdown()
{
write_line(&mut write_stream, &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(stream, input_tx, alive, shared, generation, starts_mid_line)
})?;
Ok((write_stream, reader))
}
fn discard_unsolicited(stream: &UnixStream, shared: &Shared) -> io::Result<bool> {
stream.set_nonblocking(true)?;
let mut unsolicited = Vec::new();
let read = read_unsolicited(stream, &mut unsolicited);
shared
.stale_inputs
.fetch_add(count_input_lines(&unsolicited), Ordering::Relaxed);
stream.set_nonblocking(false)?;
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: UnixStream,
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 UnixStream, 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 UnixStream,
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 UnixStream, 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"))?;
write_all_before(stream, &line, deadline)
}
fn flush_acks(stream: &mut UnixStream, 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 _ = write_all_before(stream, &lines, deadline);
}
fn write_all_before(
stream: &mut UnixStream,
mut bytes: &[u8],
deadline: Instant,
) -> io::Result<()> {
while !bytes.is_empty() {
let left = deadline.saturating_duration_since(Instant::now());
if left.is_zero() {
return Err(io::ErrorKind::TimedOut.into());
}
stream.set_write_timeout(Some(left.min(WRITE_WAIT)))?;
let chunk = bytes.get(..WRITE_CHUNK).unwrap_or(bytes);
match stream.write(chunk) {
Ok(0) => return Err(io::ErrorKind::WriteZero.into()),
Ok(n) => bytes = bytes.get(n..).unwrap_or_default(),
Err(err) if err.kind() == io::ErrorKind::Interrupted => {}
Err(err)
if matches!(
err.kind(),
io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
) => {}
Err(err) => return Err(err),
}
}
Ok(())
}
fn absolutize(path: PathBuf) -> io::Result<PathBuf> {
if path.is_absolute() {
Ok(path)
} else {
Ok(env::current_dir()?.join(path))
}
}
fn clear_socket_path(path: &Path) -> io::Result<()> {
let meta = match fs::symlink_metadata(path) {
Ok(meta) => meta,
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(err) => {
return Err(io::Error::new(
err.kind(),
format!("cannot inspect {}: {err}", path.display()),
));
}
};
if !meta.file_type().is_socket() {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!(
"{} already exists and is not a socket (it is {}); refusing to delete it to \
bind there. Point $TARIA_SOCK at a path taria may create.",
path.display(),
describe_file_type(&meta)
),
));
}
match probe_socket(path) {
SocketProbe::Stale => match fs::remove_file(path) {
Ok(()) => Ok(()),
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(io::Error::new(
err.kind(),
format!("cannot remove the stale socket {}: {err}", path.display()),
)),
},
SocketProbe::Live => Err(io::Error::new(
io::ErrorKind::AddrInUse,
format!(
"another instance is already serving the taria socket {}; refusing to take it \
over, because that would leave it running with no way for a bridge to reach \
it. To run a second instance alongside it, give it a socket of its own: set \
TARIA_SOCK to a different path.",
path.display()
),
)),
SocketProbe::Unknown(why) => Err(io::Error::new(
io::ErrorKind::AddrInUse,
format!(
"cannot tell whether another instance is serving the taria socket {} ({why}); \
refusing to remove a socket that may still be in use. Set TARIA_SOCK to a \
different path to bind elsewhere.",
path.display()
),
)),
}
}
fn describe_file_type(meta: &fs::Metadata) -> &'static str {
let kind = meta.file_type();
if kind.is_symlink() {
"a symlink"
} else if kind.is_dir() {
"a directory"
} else if kind.is_file() {
"a regular file"
} else {
"another kind of file"
}
}
enum SocketProbe {
Live,
Stale,
Unknown(String),
}
fn probe_socket(path: &Path) -> SocketProbe {
let (tx, rx) = mpsc::channel();
let probe_path = path.to_path_buf();
let spawned = thread::Builder::new()
.name("taria-socket-probe".into())
.spawn(move || {
let outcome = UnixStream::connect(&probe_path).map(|stream| {
let _ = stream.shutdown(Shutdown::Both);
});
let _ = tx.send(outcome);
});
if let Err(err) = spawned {
return SocketProbe::Unknown(format!("the probe thread could not start: {err}"));
}
match rx.recv_timeout(SOCKET_PROBE_TIMEOUT) {
Ok(Ok(())) => SocketProbe::Live,
Ok(Err(err))
if matches!(
err.kind(),
io::ErrorKind::ConnectionRefused | io::ErrorKind::NotFound
) =>
{
SocketProbe::Stale
}
Ok(Err(err)) => SocketProbe::Unknown(format!("connecting to it failed: {err}")),
Err(_) => SocketProbe::Unknown(format!(
"connecting to it did not finish within {SOCKET_PROBE_TIMEOUT:?}"
)),
}
}
fn ensure_private_dir(dir: &Path) -> io::Result<()> {
let mut builder = fs::DirBuilder::new();
builder.recursive(true);
builder.mode(0o700);
let _ = builder.create(dir);
let meta = fs::symlink_metadata(dir).map_err(|err| {
io::Error::new(
err.kind(),
format!("socket directory {} is unusable: {err}", dir.display()),
)
})?;
if !meta.file_type().is_dir() {
return Err(io::Error::other(format!(
"socket directory {} is not a real directory (it may be a symlink planted by \
another user to hijack the socket); refusing to use it",
dir.display()
)));
}
let uid = current_uid()?;
if meta.uid() != uid {
return Err(io::Error::other(format!(
"socket directory {} is owned by uid {} instead of the current user (uid {}); \
its owner could replace the socket to hijack the connection; refusing to use it",
dir.display(),
meta.uid(),
uid
)));
}
if meta.mode() & 0o077 != 0 {
return Err(io::Error::other(format!(
"socket directory {} is group/world-accessible (mode {:03o}); other users could \
replace the socket to hijack the connection; re-create it with mode 0700",
dir.display(),
meta.mode() & 0o777
)));
}
Ok(())
}
fn current_uid() -> io::Result<u32> {
if let Ok(meta) = fs::metadata("/proc/self") {
return Ok(meta.uid());
}
let probe = env::temp_dir().join(format!("taria-uid-probe-{}", std::process::id()));
let _ = fs::remove_file(&probe);
let file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(&probe)?;
let uid = file.metadata()?.uid();
drop(file);
let _ = fs::remove_file(&probe);
Ok(uid)
}
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(),
&user_identity(),
app_label,
)
}
fn invalid_label(err: taria::socket::InvalidAppLabel) -> io::Error {
io::Error::new(io::ErrorKind::InvalidInput, err.to_string())
}
fn user_identity() -> String {
if let Ok(meta) = fs::metadata("/proc/self") {
return meta.uid().to_string();
}
env::var("USER")
.or_else(|_| env::var("LOGNAME"))
.unwrap_or_else(|_| "default".to_string())
}
#[cfg(test)]
mod tests {
use std::os::unix::fs::PermissionsExt;
use super::*;
use crate::test_util::bind_test_layer;
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 {
tempfile::Builder::new()
.prefix(prefix)
.permissions(fs::Permissions::from_mode(0o700))
.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),
})
}
#[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);
}
#[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);
}
#[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);
}
#[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"));
}
#[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"
);
}
}
}
#[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.dropped_inputs(), 0);
assert_eq!(layer.stale_inputs(), 0);
assert_eq!(layer.unknown_inputs(), 0);
assert_eq!(layer.truncated_snapshots(), 0);
assert_eq!(layer.last_truncation(), None);
assert_eq!(layer.oversized_snapshots(), 0);
assert_eq!(layer.last_oversize(), None);
assert_eq!(layer.try_recv(), None);
assert_eq!(layer.try_recv_with_id(), None);
assert_eq!(layer.recv_timeout(Duration::from_millis(1)), None);
assert_eq!(layer.recv_timeout_with_id(Duration::from_millis(1)), None);
layer.ack(0, InputStatus::Ignored);
let mut drained = 0;
layer.drain(|_| drained += 1);
layer.drain_with_ids(|_, _| drained += 1);
layer.drain_acking(|_| {
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.truncated_snapshots(), 0);
assert_eq!(layer.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.truncated_snapshots(), 1);
let too_deep = layer.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.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.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.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.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.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.oversized_snapshots(), 0);
assert_eq!(layer.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.oversized_snapshots(), 1);
let too_large = layer.last_oversize().expect("the cut is recorded");
let unbounded = Snapshot::new(u64::MAX, root_around_draft("atsize", over));
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.oversized_snapshots(), 1);
layer.publish([draft(format!("{big}z"))]);
assert_eq!(layer.latest_snapshot().expect("published").seq, 1);
assert_eq!(layer.oversized_snapshots(), 1);
layer.publish([draft(format!("z{big}"))]);
assert_eq!(layer.latest_snapshot().expect("published").seq, 2);
assert_eq!(layer.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.oversized_snapshots(), 1);
let too_large = layer.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.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.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.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.truncated_snapshots(), 1);
assert_eq!(layer.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 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, 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 = 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(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");
}
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
}
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 = server;
let alive = AtomicBool::new(true);
writer_loop(&mut server, &shared, &alive, 0);
drop(server);
let _ = tx.send(());
}
});
rx
}
#[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, InputStatus::Delivered));
state.acks.push_back((4, 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(4, InputStatus::Delivered),
AppToBridge::ack(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");
}
#[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, InputStatus::Delivered));
state.acks.push_back((2, 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, 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(1, InputStatus::Delivered),
AppToBridge::ack(2, InputStatus::Delivered),
AppToBridge::ack(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());
}
#[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, 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(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());
}
#[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((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"
);
}
#[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"
);
}
#[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);
}
#[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");
}
#[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"
);
}
#[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 shared = test_shared("ackbound");
let overflow = 10;
for id in 0..(ACK_QUEUE + overflow) as InputId {
shared.queue_ack(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(overflow as InputId),
"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((ACK_QUEUE + overflow - 1) as InputId),
"the newest answer is the one an agent is still waiting on"
);
drop(state);
assert_eq!(
shared.dropped_acks.load(Ordering::Relaxed),
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, 5), "the live connection's input delivers");
shared.lock_state().acks.clear();
shared.queue_app_ack(5, InputStatus::Ignored);
assert_eq!(
shared.lock_state().acks.pop_front(),
Some((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(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 InputId) {
assert!(shared.deliver(generation, id));
}
shared.lock_state().acks.clear();
shared.queue_app_ack(100, InputStatus::Ignored);
assert!(shared.lock_state().acks.is_empty(), "id 100 was forgotten");
shared.queue_app_ack(101, InputStatus::Ignored);
assert_eq!(
shared.lock_state().acks.pop_front(),
Some((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);
assert_eq!(layer.recv_timeout_with_id(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));
}
}