Skip to main content

crabshell_services/
socket.rs

1//! Unix sockets a config opens, as Quickshell's `Socket`: lines in, bytes
2//! out.
3
4#[cfg(test)]
5mod tests;
6
7use std::cell::RefCell;
8use std::io::{ErrorKind, Read, Write};
9use std::os::fd::AsFd;
10use std::os::unix::net::UnixStream;
11use std::panic::{self, AssertUnwindSafe, Location};
12use std::path::PathBuf;
13use std::rc::Rc;
14use std::sync::Once;
15use std::time::{Duration, Instant};
16
17use calloop::timer::TimeoutAction;
18use calloop::{Interest, RegistrationToken};
19use crabshell_reactive::{Owner, ReadSignal, Signal, untrack};
20use crabshell_ui::Cx;
21
22use crate::Shared;
23use crate::lines::{Lines, panic_message};
24
25/// The most bytes left to write before the socket is closed, so a program
26/// that reads nothing can't grow the shell without end.
27pub(crate) const MAX_OUTBOX: usize = 4 * 1024 * 1024;
28
29/// How long a server's connection stays open for a reply once its client
30/// stopped sending, as the shell's own IPC socket waits.
31const HALF_OPEN: Duration = if cfg!(test) {
32  Duration::from_millis(300)
33} else {
34  Duration::from_secs(5)
35};
36
37/// What `cx.socket` needs from the shell: the loop.
38pub(crate) struct Sockets(pub(crate) Rc<Shared>);
39
40/// A connection to a Unix socket, from `cx.socket(path)`. It connects at
41/// once, passes each line it reads to [`on_line`](Self::on_line), and
42/// writes what [`write`](Self::write) is given. A clone is cheap and
43/// shares the connection.
44///
45/// It closes when the context that opened it goes. When the other end
46/// closes it, [`connected`](Self::connected) turns `false` and it stays
47/// closed until [`reconnect`](Self::reconnect): nothing retries on its
48/// own, so a socket that isn't there never wakes the shell.
49///
50/// A [`SocketServer`](crate::SocketServer) hands each connection it
51/// accepts over as a `Socket` too, which can't reconnect.
52#[derive(Clone)]
53pub struct Socket {
54  inner: Rc<Inner>,
55}
56
57struct Inner {
58  /// `None` when the shell provided no services.
59  shared: Option<Rc<Shared>>,
60  path: PathBuf,
61  /// Lives under the opening context's owner, so it says when the socket
62  /// must close for good.
63  connected: Signal<bool>,
64  state: RefCell<State>,
65  /// For a connection a server accepted, its own context's owner, which
66  /// goes once the connection ends.
67  served: Option<Owner>,
68}
69
70/// What a config does with each line.
71type OnLine = Rc<dyn Fn(&str)>;
72
73#[derive(Default)]
74struct State {
75  connection: Option<Connection>,
76  on_line: Option<OnLine>,
77  /// Which method set `on_line` and where, for the log if it panics.
78  at: Option<(&'static str, &'static Location<'static>)>,
79  /// What was read and not passed on yet. Emptied by a close or a connect,
80  /// so lines read before one aren't passed on after it.
81  lines: Lines,
82  /// A next-turn delivery is on its way, and passes on all that waits.
83  later: bool,
84}
85
86struct Connection {
87  stream: UnixStream,
88  /// The loop source that waits for something to read.
89  reader: RegistrationToken,
90  /// The loop source that waits for room to write, while the socket
91  /// holds back part of `outbox`.
92  writer: Option<RegistrationToken>,
93  outbox: Vec<u8>,
94  /// Why writing failed, after which only reading goes on.
95  write_failed: Option<String>,
96  /// A server's client stopped sending but may read a reply: reading is
97  /// over, and this timer closes the connection after [`HALF_OPEN`].
98  half_open: Option<RegistrationToken>,
99}
100
101/// `cx.socket(path)`.
102pub(crate) fn socket(cx: &Cx, path: PathBuf) -> Socket {
103  let shared = cx.service::<Sockets>().map(|sockets| Rc::clone(&sockets.0));
104  if shared.is_none() {
105    static LOGGED: Once = Once::new();
106    LOGGED.call_once(|| {
107      tracing::warn!("cx.socket() needs the services the shell provides; it never connects");
108    });
109  }
110  let inner = Rc::new(Inner {
111    shared,
112    path,
113    connected: cx.signal(false),
114    state: RefCell::default(),
115    served: None,
116  });
117  // Dropped with the opening context's owner, which closes the socket.
118  cx.owner().signal(CloseOnDrop(Rc::clone(&inner)));
119  inner.connect();
120  Socket { inner }
121}
122
123/// A connection a server accepted on `path`, as a `Socket` under `cx`,
124/// whose owner goes on the loop's turn after the connection ends. `None`,
125/// logged, if the loop can't take it.
126pub(crate) fn accepted(
127  cx: &Cx,
128  shared: &Rc<Shared>,
129  path: PathBuf,
130  stream: UnixStream,
131) -> Option<Socket> {
132  let inner = Rc::new(Inner {
133    shared: Some(Rc::clone(shared)),
134    path,
135    connected: cx.signal(false),
136    state: RefCell::default(),
137    served: Some(cx.owner()),
138  });
139  cx.owner().signal(CloseOnDrop(Rc::clone(&inner)));
140  if let Err(error) = inner.attach(stream) {
141    let path = inner.path.display();
142    tracing::warn!(%path, %error, "can't serve a connection");
143    return None;
144  }
145  Some(Socket { inner })
146}
147
148/// Closes the socket when the opening context's owner drops it.
149struct CloseOnDrop(Rc<Inner>);
150
151impl Drop for CloseOnDrop {
152  fn drop(&mut self) {
153    // Dropped outside the borrow: `on_line` may hold this socket, which
154    // would keep it alive, and its drop may use it.
155    let on_line = self.0.state.borrow_mut().on_line.take();
156    drop(on_line);
157    self.0.close();
158  }
159}
160
161impl Socket {
162  /// Whether it is connected: `true` from a connect that worked until the
163  /// other end closes it, it breaks, or [`close`](Self::close).
164  pub fn connected(&self) -> ReadSignal<bool> {
165    self.inner.connected.read_only()
166  }
167
168  /// Calls `on_line` with each line read, without its `\n`, decoded as
169  /// UTF-8 with bad bytes replaced. What is left when the other end closes
170  /// counts as a last line. A line over 1 MiB is dropped and logged.
171  ///
172  /// It runs on the main thread. If it panics, that is logged with the
173  /// socket's path and where `on_line` was called, and it isn't called
174  /// again; the socket stays open.
175  #[track_caller]
176  pub fn on_line(self, on_line: impl Fn(&str) + 'static) -> Socket {
177    self.handle("on_line", "\n", Rc::new(on_line))
178  }
179
180  /// Like [`on_line`](Self::on_line), with lines that end at `marker`
181  /// instead of `\n`, such as `"\0"` or `"\r\n"`, as Quickshell's
182  /// `SplitParser`. An empty `marker` passes on each read as it comes, up
183  /// to 64 KiB, keeping a character the read cut off for the next.
184  ///
185  /// It replaces `on_line`, and everything not passed on yet is split at
186  /// the new marker: set from a handler, from the line after the one it
187  /// was given; set elsewhere, on the loop's next turn.
188  #[track_caller]
189  pub fn on_split(self, marker: &str, on_split: impl Fn(&str) + 'static) -> Socket {
190    self.handle("on_split", marker, Rc::new(on_split))
191  }
192
193  #[track_caller]
194  fn handle(self, name: &'static str, marker: &str, handler: OnLine) -> Socket {
195    let mut state = self.inner.state.borrow_mut();
196    state.on_line = Some(handler);
197    state.at = Some((name, Location::caller()));
198    state.lines.set_marker(marker);
199    let waiting = state.lines.waiting();
200    drop(state);
201    if waiting {
202      self.inner.deliver_later();
203    }
204    self
205  }
206
207  /// Sends `data`, all of it, as the other end takes it. While not
208  /// connected it is dropped, and that is logged. If more than 4 MiB wait
209  /// because the other end reads nothing, the socket is closed.
210  pub fn write(&self, data: impl AsRef<[u8]>) {
211    self.inner.write(data.as_ref());
212  }
213
214  /// Closes it and connects again, as after the other end restarted.
215  /// Lines the old connection read and `on_line` hasn't had yet are
216  /// dropped. A connection a server accepted can't: that is logged, and
217  /// it stays as it is.
218  pub fn reconnect(&self) {
219    if self.inner.served.is_some() {
220      let path = self.inner.path.display();
221      return tracing::warn!(%path, "a connection a server accepted can't reconnect");
222    }
223    self.inner.close();
224    self.inner.connect();
225  }
226
227  /// Closes it. [`reconnect`](Self::reconnect) opens it again. Lines read
228  /// and not yet passed on are dropped.
229  pub fn close(&self) {
230    self.inner.close();
231  }
232}
233
234impl Inner {
235  /// Whether the opening context is still there. Untracked, so an effect
236  /// that opens or reconnects a socket doesn't run again for it.
237  fn alive(&self) -> bool {
238    untrack(|| self.connected.try_with(|_| ()).is_some())
239  }
240
241  fn connect(self: &Rc<Self>) {
242    let event_loop = self.shared.as_ref().and_then(|shared| shared.event_loop());
243    if event_loop.is_none() || !self.alive() {
244      return;
245    }
246    if let Err(error) = UnixStream::connect(&self.path).and_then(|stream| self.attach(stream)) {
247      let path = self.path.display();
248      tracing::warn!(%path, %error, "can't connect to the socket");
249    }
250  }
251
252  /// Reads and writes `stream` from the loop from now on.
253  fn attach(self: &Rc<Self>, stream: UnixStream) -> std::io::Result<()> {
254    let event_loop = self
255      .shared
256      .as_ref()
257      .and_then(|shared| shared.event_loop())
258      .ok_or_else(|| std::io::Error::other("the loop is gone"))?;
259    stream.set_nonblocking(true)?;
260    let fd = stream.as_fd().try_clone_to_owned()?;
261    let inner = Rc::clone(self);
262    let read = Box::new(move || inner.on_readable());
263    let reader = event_loop
264      .insert_fd(fd, Interest::READ, read)
265      .ok_or_else(|| std::io::Error::other("the loop refused it"))?;
266    let mut state = self.state.borrow_mut();
267    state.lines.reset();
268    state.connection = Some(Connection {
269      stream,
270      reader,
271      writer: None,
272      outbox: Vec::new(),
273      write_failed: None,
274      half_open: None,
275    });
276    drop(state);
277    self.connected.try_set(true);
278    Ok(())
279  }
280
281  /// Reads what came, and passes each whole line on.
282  fn on_readable(self: &Rc<Self>) {
283    let mut chunk = [0; 64 * 1024];
284    let mut state = self.state.borrow_mut();
285    let Some(connection) = state.connection.as_mut() else {
286      return;
287    };
288    let closed = match connection.stream.read(&mut chunk) {
289      // As `printf cmd | socat - UNIX-CONNECT:path` does: the client
290      // stopped sending and waits for the reply.
291      Ok(0) if self.served.is_some() && !hung_up(&connection.stream) => {
292        drop(state);
293        self.half_close();
294        return self.deliver(true);
295      }
296      Ok(0) => Some(String::from("the other end closed it")),
297      Ok(read) => {
298        state.lines.append(&chunk[..read]);
299        None
300      }
301      Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted) => {
302        return;
303      }
304      // As a reset from a peer that closed with input unread: the end.
305      Err(error) => Some(error.to_string()),
306    };
307    drop(state);
308    // Closed first, so a handler may reconnect.
309    if let Some(why) = &closed {
310      self.drop_connection(why);
311    }
312    self.deliver(closed.is_some());
313  }
314
315  /// Stops reading a server's connection whose client stopped sending,
316  /// and keeps it open for the reply until the config closes it, a write
317  /// fails or [`HALF_OPEN`] passes.
318  fn half_close(self: &Rc<Self>) {
319    let Some(event_loop) = self.shared.as_ref().and_then(|shared| shared.event_loop()) else {
320      return;
321    };
322    let mut state = self.state.borrow_mut();
323    let Some(connection) = state.connection.as_mut() else {
324      return;
325    };
326    // At its end it would be readable for good.
327    event_loop.remove(connection.reader);
328    let inner = Rc::clone(self);
329    let expire = move || {
330      // Gone with this call, so the close doesn't remove it again.
331      if let Some(connection) = inner.state.borrow_mut().connection.as_mut() {
332        connection.half_open = None;
333      }
334      inner.drop_connection("the client got no reply in time");
335      TimeoutAction::Drop
336    };
337    connection.half_open = event_loop.insert_timer(Instant::now() + HALF_OPEN, Box::new(expire));
338    let unbounded = connection.half_open.is_none();
339    drop(state);
340    if unbounded {
341      self.drop_connection("the client stopped sending");
342    }
343  }
344
345  /// Passes on what waits at the loop's next turn, as a marker set outside
346  /// a handler asks.
347  fn deliver_later(self: &Rc<Self>) {
348    let Some(event_loop) = self.shared.as_ref().and_then(|shared| shared.event_loop()) else {
349      return;
350    };
351    if std::mem::replace(&mut self.state.borrow_mut().later, true) {
352      return;
353    }
354    let inner = Rc::clone(self);
355    let later = move || {
356      inner.state.borrow_mut().later = false;
357      inner.deliver(false);
358      TimeoutAction::Drop
359    };
360    event_loop.insert_timer(Instant::now(), Box::new(later));
361  }
362
363  /// Passes each whole line waiting to the handler set at the time, one at
364  /// a time with nothing borrowed, so one that sets another marker has the
365  /// rest split at it. When the other end is `done`, what is left goes
366  /// last. A close or a reconnect empties what waits, which ends it.
367  fn deliver(&self, done: bool) {
368    loop {
369      let (line, on_line, at) = {
370        let mut state = self.state.borrow_mut();
371        let line = match state.lines.next(&self.path.display()) {
372          Some(line) => line,
373          None if done => match state.lines.rest(&self.path.display()) {
374            Some(line) => line,
375            None => return,
376          },
377          None => return,
378        };
379        (line, state.on_line.clone(), state.at)
380      };
381      let Some(on_line) = on_line else {
382        continue;
383      };
384      if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(|| on_line(&line))) {
385        let path = self.path.display();
386        let (name, at) = at.map_or(("on_line", String::new()), |(name, at)| {
387          (name, format!(" (called at {at})"))
388        });
389        let message = panic_message(payload.as_ref());
390        tracing::error!("socket {path} {name}{at} panicked: {message}");
391        let mut state = self.state.borrow_mut();
392        // Unless it set a new one meanwhile.
393        if state
394          .on_line
395          .as_ref()
396          .is_some_and(|now| Rc::ptr_eq(now, &on_line))
397        {
398          state.on_line = None;
399        }
400      }
401    }
402  }
403
404  fn write(self: &Rc<Self>, data: &[u8]) {
405    let mut state = self.state.borrow_mut();
406    let Some(connection) = state
407      .connection
408      .as_mut()
409      .filter(|connection| connection.write_failed.is_none())
410    else {
411      let path = self.path.display();
412      return tracing::warn!(%path, "the socket takes no writes now, so a write was dropped");
413    };
414    connection.outbox.extend_from_slice(data);
415    drop(state);
416    self.flush();
417  }
418
419  /// Writes what the socket takes, and waits for room for the rest.
420  fn flush(self: &Rc<Self>) {
421    let Some(event_loop) = self.shared.as_ref().and_then(|shared| shared.event_loop()) else {
422      return;
423    };
424    let mut state = self.state.borrow_mut();
425    let Some(connection) = state.connection.as_mut() else {
426      return;
427    };
428    let mut broken = None;
429    while !connection.outbox.is_empty() {
430      match connection.stream.write(&connection.outbox) {
431        Ok(written) => {
432          connection.outbox.drain(..written);
433        }
434        Err(error) if error.kind() == ErrorKind::Interrupted => {}
435        Err(error) if error.kind() == ErrorKind::WouldBlock => break,
436        Err(error) => {
437          // The other end may still have sent something: stop writing
438          // and read on to its end.
439          let path = self.path.display();
440          tracing::info!(%path, %error, "the socket takes no more writes");
441          connection.write_failed = Some(error.to_string());
442          connection.outbox = Vec::new();
443          // Unless reading is over too, as for a half-open connection.
444          if connection.half_open.is_some() {
445            broken = Some(error.to_string());
446          }
447          break;
448        }
449      }
450    }
451    if connection.outbox.len() > MAX_OUTBOX {
452      broken = Some(String::from("the other end reads nothing it is sent"));
453    }
454    if broken.is_none() && connection.outbox.is_empty() {
455      // A big write's room isn't kept.
456      connection.outbox.shrink_to(64 * 1024);
457      if let Some(writer) = connection.writer.take() {
458        event_loop.remove(writer);
459      }
460    } else if broken.is_none() && connection.writer.is_none() {
461      match connection.stream.as_fd().try_clone_to_owned() {
462        Ok(fd) => {
463          let inner = Rc::clone(self);
464          let write = Box::new(move || inner.flush());
465          connection.writer = event_loop.insert_fd(fd, Interest::WRITE, write);
466        }
467        Err(error) => broken = Some(error.to_string()),
468      }
469    }
470    drop(state);
471    if let Some(why) = broken {
472      self.drop_connection(&why);
473    }
474  }
475
476  /// Ends the connection the other end ended or broke, and logs why.
477  /// Lines it read before still reach `on_line`.
478  fn drop_connection(&self, why: &str) {
479    if self.state.borrow().connection.is_some() {
480      let path = self.path.display();
481      tracing::info!(%path, why, "the socket closed");
482    }
483    self.end(false);
484  }
485
486  /// Closes it as the config asked, so lines read before are dropped.
487  fn close(&self) {
488    self.end(true);
489  }
490
491  fn end(&self, asked: bool) {
492    let connection = {
493      let mut state = self.state.borrow_mut();
494      if asked {
495        state.lines.reset();
496      }
497      state.connection.take()
498    };
499    if let (Some(connection), Some(event_loop)) = (
500      connection,
501      self.shared.as_ref().and_then(|shared| shared.event_loop()),
502    ) {
503      event_loop.remove(connection.reader);
504      for token in [connection.writer, connection.half_open]
505        .into_iter()
506        .flatten()
507      {
508        event_loop.remove(token);
509      }
510      // A server's connection never connects again, so its context goes,
511      // on the next turn: a handler that closed it may still use it.
512      if let Some(owner) = self.served.filter(|_| self.alive()) {
513        let gone = move || {
514          owner.dispose();
515          TimeoutAction::Drop
516        };
517        event_loop.insert_timer(Instant::now(), Box::new(gone));
518      }
519    }
520    if untrack(|| self.connected.try_with(|connected| *connected)) == Some(true) {
521      self.connected.try_set(false);
522    }
523  }
524}
525
526/// Whether the other end of `stream` is gone both ways, rather than only
527/// done sending. Asked at once, without waiting.
528fn hung_up(stream: &UnixStream) -> bool {
529  use rustix::event::{PollFd, PollFlags, Timespec};
530
531  let mut fds = [PollFd::new(stream, PollFlags::empty())];
532  let now = Timespec::default();
533  rustix::event::poll(&mut fds, Some(&now)).is_err() || fds[0].revents().contains(PollFlags::HUP)
534}