#[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 std::time::{Duration, Instant};
use calloop::timer::TimeoutAction;
use calloop::{Interest, RegistrationToken};
use crabshell_reactive::{Owner, 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;
const HALF_OPEN: Duration = if cfg!(test) {
Duration::from_millis(300)
} else {
Duration::from_secs(5)
};
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>,
served: Option<Owner>,
}
type OnLine = Rc<dyn Fn(&str)>;
#[derive(Default)]
struct State {
connection: Option<Connection>,
on_line: Option<OnLine>,
at: Option<(&'static str, &'static Location<'static>)>,
lines: Lines,
later: bool,
}
struct Connection {
stream: UnixStream,
reader: RegistrationToken,
writer: Option<RegistrationToken>,
outbox: Vec<u8>,
write_failed: Option<String>,
half_open: Option<RegistrationToken>,
}
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(),
served: None,
});
cx.owner().signal(CloseOnDrop(Rc::clone(&inner)));
inner.connect();
Socket { inner }
}
pub(crate) fn accepted(
cx: &Cx,
shared: &Rc<Shared>,
path: PathBuf,
stream: UnixStream,
) -> Option<Socket> {
let inner = Rc::new(Inner {
shared: Some(Rc::clone(shared)),
path,
connected: cx.signal(false),
state: RefCell::default(),
served: Some(cx.owner()),
});
cx.owner().signal(CloseOnDrop(Rc::clone(&inner)));
if let Err(error) = inner.attach(stream) {
let path = inner.path.display();
tracing::warn!(%path, %error, "can't serve a connection");
return None;
}
Some(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 {
self.handle("on_line", "\n", Rc::new(on_line))
}
#[track_caller]
pub fn on_split(self, marker: &str, on_split: impl Fn(&str) + 'static) -> Socket {
self.handle("on_split", marker, Rc::new(on_split))
}
#[track_caller]
fn handle(self, name: &'static str, marker: &str, handler: OnLine) -> Socket {
let mut state = self.inner.state.borrow_mut();
state.on_line = Some(handler);
state.at = Some((name, Location::caller()));
state.lines.set_marker(marker);
let waiting = state.lines.waiting();
drop(state);
if waiting {
self.inner.deliver_later();
}
self
}
pub fn write(&self, data: impl AsRef<[u8]>) {
self.inner.write(data.as_ref());
}
pub fn reconnect(&self) {
if self.inner.served.is_some() {
let path = self.inner.path.display();
return tracing::warn!(%path, "a connection a server accepted can't reconnect");
}
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 event_loop = self.shared.as_ref().and_then(|shared| shared.event_loop());
if event_loop.is_none() || !self.alive() {
return;
}
if let Err(error) = UnixStream::connect(&self.path).and_then(|stream| self.attach(stream)) {
let path = self.path.display();
tracing::warn!(%path, %error, "can't connect to the socket");
}
}
fn attach(self: &Rc<Self>, stream: UnixStream) -> std::io::Result<()> {
let event_loop = self
.shared
.as_ref()
.and_then(|shared| shared.event_loop())
.ok_or_else(|| std::io::Error::other("the loop is gone"))?;
stream.set_nonblocking(true)?;
let fd = stream.as_fd().try_clone_to_owned()?;
let inner = Rc::clone(self);
let read = Box::new(move || inner.on_readable());
let reader = event_loop
.insert_fd(fd, Interest::READ, read)
.ok_or_else(|| std::io::Error::other("the loop refused it"))?;
let mut state = self.state.borrow_mut();
state.lines.reset();
state.connection = Some(Connection {
stream,
reader,
writer: None,
outbox: Vec::new(),
write_failed: None,
half_open: None,
});
drop(state);
self.connected.try_set(true);
Ok(())
}
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 closed = match connection.stream.read(&mut chunk) {
Ok(0) if self.served.is_some() && !hung_up(&connection.stream) => {
drop(state);
self.half_close();
return self.deliver(true);
}
Ok(0) => Some(String::from("the other end closed it")),
Ok(read) => {
state.lines.append(&chunk[..read]);
None
}
Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted) => {
return;
}
Err(error) => Some(error.to_string()),
};
drop(state);
if let Some(why) = &closed {
self.drop_connection(why);
}
self.deliver(closed.is_some());
}
fn half_close(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;
};
event_loop.remove(connection.reader);
let inner = Rc::clone(self);
let expire = move || {
if let Some(connection) = inner.state.borrow_mut().connection.as_mut() {
connection.half_open = None;
}
inner.drop_connection("the client got no reply in time");
TimeoutAction::Drop
};
connection.half_open = event_loop.insert_timer(Instant::now() + HALF_OPEN, Box::new(expire));
let unbounded = connection.half_open.is_none();
drop(state);
if unbounded {
self.drop_connection("the client stopped sending");
}
}
fn deliver_later(self: &Rc<Self>) {
let Some(event_loop) = self.shared.as_ref().and_then(|shared| shared.event_loop()) else {
return;
};
if std::mem::replace(&mut self.state.borrow_mut().later, true) {
return;
}
let inner = Rc::clone(self);
let later = move || {
inner.state.borrow_mut().later = false;
inner.deliver(false);
TimeoutAction::Drop
};
event_loop.insert_timer(Instant::now(), Box::new(later));
}
fn deliver(&self, done: bool) {
loop {
let (line, on_line, at) = {
let mut state = self.state.borrow_mut();
let line = match state.lines.next(&self.path.display()) {
Some(line) => line,
None if done => match state.lines.rest(&self.path.display()) {
Some(line) => line,
None => return,
},
None => return,
};
(line, state.on_line.clone(), state.at)
};
let Some(on_line) = on_line else {
continue;
};
if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(|| on_line(&line))) {
let path = self.path.display();
let (name, at) = at.map_or(("on_line", String::new()), |(name, at)| {
(name, format!(" (called at {at})"))
});
let message = panic_message(payload.as_ref());
tracing::error!("socket {path} {name}{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;
}
}
}
}
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();
if connection.half_open.is_some() {
broken = Some(error.to_string());
}
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();
if asked {
state.lines.reset();
}
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);
for token in [connection.writer, connection.half_open]
.into_iter()
.flatten()
{
event_loop.remove(token);
}
if let Some(owner) = self.served.filter(|_| self.alive()) {
let gone = move || {
owner.dispose();
TimeoutAction::Drop
};
event_loop.insert_timer(Instant::now(), Box::new(gone));
}
}
if untrack(|| self.connected.try_with(|connected| *connected)) == Some(true) {
self.connected.try_set(false);
}
}
}
fn hung_up(stream: &UnixStream) -> bool {
use rustix::event::{PollFd, PollFlags, Timespec};
let mut fds = [PollFd::new(stream, PollFlags::empty())];
let now = Timespec::default();
rustix::event::poll(&mut fds, Some(&now)).is_err() || fds[0].revents().contains(PollFlags::HUP)
}