#[cfg(test)]
mod tests;
use std::cell::RefCell;
use std::io::{ErrorKind, Read, Write};
use std::os::fd::AsFd;
use std::os::unix::net::UnixStream;
use std::panic::{self, AssertUnwindSafe, Location};
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Once;
use calloop::{Interest, RegistrationToken};
use crabshell_reactive::{ReadSignal, Signal, untrack};
use crabshell_ui::Cx;
use crate::Shared;
use crate::lines::{Lines, panic_message};
pub(crate) const MAX_OUTBOX: usize = 4 * 1024 * 1024;
pub(crate) struct Sockets(pub(crate) Rc<Shared>);
#[derive(Clone)]
pub struct Socket {
inner: Rc<Inner>,
}
struct Inner {
shared: Option<Rc<Shared>>,
path: PathBuf,
connected: Signal<bool>,
state: RefCell<State>,
}
type OnLine = Rc<dyn Fn(&str)>;
#[derive(Default)]
struct State {
connection: Option<Connection>,
on_line: Option<OnLine>,
at: Option<&'static Location<'static>>,
lines: Lines,
serial: u64,
}
struct Connection {
stream: UnixStream,
reader: RegistrationToken,
writer: Option<RegistrationToken>,
outbox: Vec<u8>,
write_failed: Option<String>,
}
pub(crate) fn socket(cx: &Cx, path: PathBuf) -> Socket {
let shared = cx.service::<Sockets>().map(|sockets| Rc::clone(&sockets.0));
if shared.is_none() {
static LOGGED: Once = Once::new();
LOGGED.call_once(|| {
tracing::warn!("cx.socket() needs the services the shell provides; it never connects");
});
}
let inner = Rc::new(Inner {
shared,
path,
connected: cx.signal(false),
state: RefCell::default(),
});
cx.owner().signal(CloseOnDrop(Rc::clone(&inner)));
inner.connect();
Socket { inner }
}
struct CloseOnDrop(Rc<Inner>);
impl Drop for CloseOnDrop {
fn drop(&mut self) {
let on_line = self.0.state.borrow_mut().on_line.take();
drop(on_line);
self.0.close();
}
}
impl Socket {
pub fn connected(&self) -> ReadSignal<bool> {
self.inner.connected.read_only()
}
#[track_caller]
pub fn on_line(self, on_line: impl Fn(&str) + 'static) -> Socket {
let mut state = self.inner.state.borrow_mut();
state.on_line = Some(Rc::new(on_line));
state.at = Some(Location::caller());
drop(state);
self
}
pub fn write(&self, data: impl AsRef<[u8]>) {
self.inner.write(data.as_ref());
}
pub fn reconnect(&self) {
self.inner.close();
self.inner.connect();
}
pub fn close(&self) {
self.inner.close();
}
}
impl Inner {
fn alive(&self) -> bool {
untrack(|| self.connected.try_with(|_| ()).is_some())
}
fn connect(self: &Rc<Self>) {
let Some(event_loop) = self.shared.as_ref().and_then(|shared| shared.event_loop()) else {
return;
};
if !self.alive() {
return;
}
let opened = UnixStream::connect(&self.path).and_then(|stream| {
stream.set_nonblocking(true)?;
let fd = stream.as_fd().try_clone_to_owned()?;
Ok((stream, fd))
});
let (stream, fd) = match opened {
Ok(opened) => opened,
Err(error) => {
let path = self.path.display();
return tracing::warn!(%path, %error, "can't connect to the socket");
}
};
let inner = Rc::clone(self);
let read = Box::new(move || inner.on_readable());
let Some(reader) = event_loop.insert_fd(fd, Interest::READ, read) else {
return;
};
let mut state = self.state.borrow_mut();
state.lines = Lines::default();
state.serial += 1;
state.connection = Some(Connection {
stream,
reader,
writer: None,
outbox: Vec::new(),
write_failed: None,
});
drop(state);
self.connected.try_set(true);
}
fn on_readable(self: &Rc<Self>) {
let mut chunk = [0; 64 * 1024];
let mut state = self.state.borrow_mut();
let Some(connection) = state.connection.as_mut() else {
return;
};
let (lines, closed) = match connection.stream.read(&mut chunk) {
Ok(0) => {
let last = state.lines.finish();
(
last.into_iter().collect(),
Some(String::from("the other end closed it")),
)
}
Ok(read) => (state.lines.push(&chunk[..read], &self.path.display()), None),
Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted) => {
return;
}
Err(error) => {
let last = state.lines.finish();
(last.into_iter().collect(), Some(error.to_string()))
}
};
let serial = state.serial;
drop(state);
if let Some(why) = closed {
self.drop_connection(&why);
}
self.deliver(lines, serial);
}
fn deliver(&self, lines: Vec<String>, serial: u64) {
let (on_line, at) = {
let state = self.state.borrow();
(state.on_line.clone(), state.at)
};
let Some(on_line) = on_line else {
return;
};
for line in lines {
if self.state.borrow().serial != serial {
return;
}
if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(|| on_line(&line))) {
let path = self.path.display();
let at = at.map_or_else(String::new, |at| format!(" (called at {at})"));
let message = panic_message(payload.as_ref());
tracing::error!("socket {path} on_line{at} panicked: {message}");
let mut state = self.state.borrow_mut();
if state
.on_line
.as_ref()
.is_some_and(|now| Rc::ptr_eq(now, &on_line))
{
state.on_line = None;
}
return;
}
}
}
fn write(self: &Rc<Self>, data: &[u8]) {
let mut state = self.state.borrow_mut();
let Some(connection) = state
.connection
.as_mut()
.filter(|connection| connection.write_failed.is_none())
else {
let path = self.path.display();
return tracing::warn!(%path, "the socket takes no writes now, so a write was dropped");
};
connection.outbox.extend_from_slice(data);
drop(state);
self.flush();
}
fn flush(self: &Rc<Self>) {
let Some(event_loop) = self.shared.as_ref().and_then(|shared| shared.event_loop()) else {
return;
};
let mut state = self.state.borrow_mut();
let Some(connection) = state.connection.as_mut() else {
return;
};
let mut broken = None;
while !connection.outbox.is_empty() {
match connection.stream.write(&connection.outbox) {
Ok(written) => {
connection.outbox.drain(..written);
}
Err(error) if error.kind() == ErrorKind::Interrupted => {}
Err(error) if error.kind() == ErrorKind::WouldBlock => break,
Err(error) => {
let path = self.path.display();
tracing::info!(%path, %error, "the socket takes no more writes");
connection.write_failed = Some(error.to_string());
connection.outbox = Vec::new();
break;
}
}
}
if connection.outbox.len() > MAX_OUTBOX {
broken = Some(String::from("the other end reads nothing it is sent"));
}
if broken.is_none() && connection.outbox.is_empty() {
connection.outbox.shrink_to(64 * 1024);
if let Some(writer) = connection.writer.take() {
event_loop.remove(writer);
}
} else if broken.is_none() && connection.writer.is_none() {
match connection.stream.as_fd().try_clone_to_owned() {
Ok(fd) => {
let inner = Rc::clone(self);
let write = Box::new(move || inner.flush());
connection.writer = event_loop.insert_fd(fd, Interest::WRITE, write);
}
Err(error) => broken = Some(error.to_string()),
}
}
drop(state);
if let Some(why) = broken {
self.drop_connection(&why);
}
}
fn drop_connection(&self, why: &str) {
if self.state.borrow().connection.is_some() {
let path = self.path.display();
tracing::info!(%path, why, "the socket closed");
}
self.end(false);
}
fn close(&self) {
self.end(true);
}
fn end(&self, asked: bool) {
let connection = {
let mut state = self.state.borrow_mut();
state.lines = Lines::default();
if asked {
state.serial += 1;
}
state.connection.take()
};
if let (Some(connection), Some(event_loop)) = (
connection,
self.shared.as_ref().and_then(|shared| shared.event_loop()),
) {
event_loop.remove(connection.reader);
if let Some(writer) = connection.writer {
event_loop.remove(writer);
}
}
if untrack(|| self.connected.try_with(|connected| *connected)) == Some(true) {
self.connected.try_set(false);
}
}
}