Skip to main content

mountos_native/
client.rs

1use crate::dir::Dir;
2use crate::error::{Error, Result};
3use crate::events::{lock, Chan, Event, EventKind, Hub, Logger};
4use crate::file::File;
5use crate::frame::{self, Frame, FLAG_ERROR, FLAG_NOTIFY, FLAG_RESPONSE, MAX_PAYLOAD_BYTES};
6use crate::wire::*;
7use prost::Message;
8use std::collections::{HashMap, HashSet};
9use std::fmt;
10use std::io;
11use std::net::Shutdown;
12use std::os::unix::net::UnixStream;
13use std::path::{Path, PathBuf};
14use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
15use std::sync::{Arc, Condvar, Mutex, OnceLock};
16use std::thread::JoinHandle;
17use std::time::{Duration, Instant};
18
19/// Client settings.
20#[derive(Clone)]
21pub struct Options {
22  /// Number of connections. Calls on different files can run in parallel up to this number.
23  pub pool_size: usize,
24  pub client_name: String,
25  /// Receives log lines: events nobody listens for, listener panics and dropped events.
26  /// The line has no prefix and no newline. When `None`, lines go to stderr with the prefix
27  /// "mountos: ". Pass your own function to route them elsewhere.
28  pub logger: Option<Logger>,
29  /// Longest a single request may take, from the end of the send to the reply. `Duration::ZERO`
30  /// (the default) means no limit. A call that runs over fails with `Error::Timeout`. When the
31  /// connection pipelines, only that call fails: its id is marked abandoned and the late reply is
32  /// dropped. Otherwise, or when too many ids are abandoned, the connection is dropped and dialed
33  /// again on the next call, and its files and watches are gone. A call that sends several
34  /// requests (a large read or write) gets the limit for each request.
35  pub call_timeout: Duration,
36  /// Most requests outstanding on one connection when the server pipelines. The server limit
37  /// (`max_inflight` in its Hello reply) also applies. Values are held between 1 and 1024. A
38  /// server without the pipelining capability gets one request at a time.
39  pub max_inflight: usize,
40}
41
42/// Default for `Options::max_inflight`.
43pub const DEFAULT_MAX_INFLIGHT: usize = 32;
44const MAX_INFLIGHT_CEILING: usize = 1024;
45
46/// Alias of `Options`.
47pub type ClientOptions = Options;
48
49impl Default for Options {
50  fn default() -> Options {
51    Options {
52      pool_size: 4,
53      client_name: concat!("mountos-native-rust/", env!("CARGO_PKG_VERSION")).into(),
54      logger: None,
55      call_timeout: Duration::ZERO,
56      max_inflight: DEFAULT_MAX_INFLIGHT,
57    }
58  }
59}
60
61impl fmt::Debug for Options {
62  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63    f.debug_struct("Options")
64      .field("pool_size", &self.pool_size)
65      .field("client_name", &self.client_name)
66      .field("logger", &self.logger.as_ref().map(|_| "<fn>"))
67      .field("call_timeout", &self.call_timeout)
68      .field("max_inflight", &self.max_inflight)
69      .finish()
70  }
71}
72
73fn aborted(msg: &str) -> Error {
74  Error::Io(io::Error::new(io::ErrorKind::ConnectionAborted, msg.to_string()))
75}
76
77/// Joins a thread unless it is the calling one, which would wait for itself.
78fn join_other(h: JoinHandle<()>) {
79  if h.thread().id() != std::thread::current().id() {
80    let _ = h.join();
81  }
82}
83
84/// Capability the server must report for a method, if any.
85fn required_capability(m: u16) -> Option<&'static str> {
86  match m {
87    method::LS => Some("ls_stat"),
88    method::STAT_BATCH | method::READ_BATCH | method::WRITE_BATCH | method::READ_FILE | method::WRITE_FILE => {
89      Some("bulk_io")
90    }
91    method::REMOVE_ALL => Some("remove_all"),
92    method::WATCH => Some("watch"),
93    method::WARM_OPEN | method::WARM_READ => Some("warm"),
94    _ => None,
95  }
96}
97
98/// Turns a socket write that hit the send timeout into `Error::Timeout`.
99fn write_timeout(e: Error) -> Error {
100  match e {
101    Error::Io(e) if matches!(e.kind(), io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut) => {
102      Error::Timeout(format!("send: {e}"))
103    }
104    e => e,
105  }
106}
107
108static CONN_SEQ: AtomicU64 = AtomicU64::new(1);
109
110/// One request in flight. The caller waits on it, the reader fills it.
111struct Waiter {
112  method: u16,
113  /// Queue that receives the notices of a watch started by this request.
114  watch: Option<Arc<Chan>>,
115  out: Mutex<Option<Result<Frame>>>,
116  cv: Condvar,
117}
118
119impl Waiter {
120  fn set(&self, r: Result<Frame>) {
121    *lock(&self.out) = Some(r);
122    self.cv.notify_one();
123  }
124
125  /// Waits for the result. `None` means the deadline passed first.
126  fn wait(&self, deadline: Option<Instant>) -> Option<Result<Frame>> {
127    let mut g = lock(&self.out);
128    loop {
129      if let Some(r) = g.take() {
130        return Some(r);
131      }
132      g = match deadline {
133        None => self.cv.wait(g).unwrap_or_else(|e| e.into_inner()),
134        Some(d) => {
135          let left = d.saturating_duration_since(Instant::now());
136          if left.is_zero() {
137            return None;
138          }
139          self.cv.wait_timeout(g, left).unwrap_or_else(|e| e.into_inner()).0
140        }
141      };
142    }
143  }
144}
145
146#[derive(Default)]
147struct Core {
148  pending: HashMap<u64, Arc<Waiter>>,
149  /// Ids whose caller gave up, with the method. A late reply is dropped (an Open, Opendir or
150  /// Watch reply also releases what it created).
151  abandoned: HashMap<u64, u16>,
152  next_id: u64,
153  /// Why the connection ended. Set once.
154  end: Option<Error>,
155}
156
157/// What the Hello reply settled.
158struct Info {
159  max_payload: usize,
160  /// Requests that may be outstanding at once.
161  limit: usize,
162  /// Capabilities the server reported. `None` when it reported none (an older server), which
163  /// allows every call.
164  caps: Option<HashSet<String>>,
165}
166
167/// One handshaken connection. Calls from many threads overlap up to the limit. Request frames are
168/// written under a mutex. One reader thread reads every frame: replies go to the waiting call by
169/// request id, notices go to the hub.
170pub(crate) struct Sock {
171  id: u64,
172  hub: Arc<Hub>,
173  /// Per-request limit, zero for none.
174  timeout: Duration,
175  writer: Mutex<UnixStream>,
176  /// Second handle on the socket. Closes the socket from another thread.
177  killer: UnixStream,
178  core: Mutex<Core>,
179  dead: AtomicBool,
180  /// Set when this side ends the connection on purpose.
181  closing: AtomicBool,
182  /// The server sent Closing, so the end that follows is expected.
183  saw_closing: AtomicBool,
184  info: OnceLock<Info>,
185  reader: Mutex<Option<JoinHandle<()>>>,
186  /// Threads that release a handle or watch created by a request that timed out.
187  releasers: Mutex<Vec<JoinHandle<()>>>,
188}
189
190impl Sock {
191  /// Connects and starts the reader. The handshake is separate so a caller can register the
192  /// socket for `close` before it blocks.
193  fn open(path: &Path, hub: &Arc<Hub>, timeout: Duration) -> Result<Arc<Sock>> {
194    let stream = UnixStream::connect(path)?;
195    if !timeout.is_zero() {
196      stream.set_write_timeout(Some(timeout))?;
197    }
198    let (rs, killer) = (stream.try_clone()?, stream.try_clone()?);
199    let sock = Arc::new(Sock {
200      id: CONN_SEQ.fetch_add(1, Ordering::SeqCst),
201      hub: hub.clone(),
202      timeout,
203      writer: Mutex::new(stream),
204      killer,
205      core: Mutex::new(Core::default()),
206      dead: AtomicBool::new(false),
207      closing: AtomicBool::new(false),
208      saw_closing: AtomicBool::new(false),
209      info: OnceLock::new(),
210      reader: Mutex::new(None),
211      releasers: Mutex::new(Vec::new()),
212    });
213    let (rsock, guard) = (sock.clone(), hub.thread_guard());
214    let h = std::thread::Builder::new()
215      .name("mountos-reader".into())
216      .spawn(move || {
217        let _guard = guard;
218        read_loop(rs, &rsock)
219      })?;
220    *lock(&sock.reader) = Some(h);
221    Ok(sock)
222  }
223
224  fn handshake(&self, name: &str, max_inflight: usize) -> Result<()> {
225    let req = HelloRequest {
226      client_name: name.into(),
227      min_version: 1,
228      max_version: 1,
229      capabilities: vec!["pipelining".into()],
230    };
231    let (resp, _): (HelloResponse, _) = self.call_msg(method::HELLO, &req, &[])?;
232    if resp.version != 1 {
233      return Err(Error::Protocol(format!("server chose version {}", resp.version)));
234    }
235    let mut max_payload = MAX_PAYLOAD_BYTES as usize;
236    if resp.max_payload_bytes > 0 {
237      max_payload = max_payload.min(resp.max_payload_bytes as usize);
238    }
239    let pipelined = resp.capabilities.iter().any(|c| c == "pipelining");
240    let limit = if pipelined {
241      max_inflight.min(resp.max_inflight as usize).max(1)
242    } else {
243      1
244    };
245    let caps = (!resp.capabilities.is_empty()).then(|| resp.capabilities.into_iter().collect());
246    let _ = self.info.set(Info {
247      max_payload,
248      limit,
249      caps,
250    });
251    Ok(())
252  }
253
254  pub(crate) fn max_payload(&self) -> usize {
255    self.info.get().map_or(MAX_PAYLOAD_BYTES as usize, |i| i.max_payload)
256  }
257
258  fn limit(&self) -> usize {
259    self.info.get().map_or(1, |i| i.limit)
260  }
261
262  fn is_dead(&self) -> bool {
263    self.dead.load(Ordering::SeqCst)
264  }
265
266  /// Ends the connection once: the first reason wins. Every waiting call gets `end`.
267  fn fail(&self, end: Error, report: bool) {
268    let waiters: Vec<Arc<Waiter>> = {
269      let mut core = lock(&self.core);
270      if core.end.is_some() {
271        return;
272      }
273      core.end = Some(end.dup());
274      self.dead.store(true, Ordering::SeqCst);
275      core.abandoned.clear();
276      core.pending.drain().map(|(_, w)| w).collect()
277    };
278    let _ = self.killer.shutdown(Shutdown::Both);
279    for w in waiters {
280      w.set(Err(end.dup()));
281    }
282    self.hub.conn_lost(self.id);
283    let expected = self.closing.load(Ordering::SeqCst) || self.saw_closing.load(Ordering::SeqCst);
284    if report && !expected {
285      self
286        .hub
287        .publish(None, Event::local(EventKind::ConnectionLost, 0, Some(end.dup())));
288    }
289  }
290
291  /// Ends the connection on purpose.
292  pub(crate) fn kill(&self) {
293    self.closing.store(true, Ordering::SeqCst);
294    self.fail(aborted("connection closed"), false);
295  }
296
297  /// Ends the connection because of a local fault. Waiting calls get a copy of `e`.
298  fn kill_with(&self, e: &Error) {
299    self.closing.store(true, Ordering::SeqCst);
300    self.fail(e.dup(), false);
301  }
302
303  /// Ends the connection and joins its threads.
304  fn finish(&self) {
305    self.kill();
306    let reader = lock(&self.reader).take();
307    if let Some(h) = reader {
308      join_other(h);
309    }
310    let releasers = std::mem::take(&mut *lock(&self.releasers));
311    for h in releasers {
312      join_other(h);
313    }
314  }
315
316  fn forget(&self, id: u64) {
317    lock(&self.core).pending.remove(&id);
318  }
319
320  /// Sends one request and waits for its response frame. The sizes and the capability are
321  /// checked before anything is sent, so those failures leave the connection intact.
322  fn call(&self, method: u16, req: &impl Message, payload: &[u8], watch: Option<Arc<Chan>>) -> Result<Frame> {
323    if let Some(cap) = required_capability(method) {
324      if self
325        .info
326        .get()
327        .and_then(|i| i.caps.as_ref())
328        .is_some_and(|c| !c.contains(cap))
329      {
330        return Err(Error::server(
331          "UNSUPPORTED_OPERATION",
332          &format!("the server does not report capability {cap}"),
333        ));
334      }
335    }
336    let meta = req.encode_to_vec();
337    frame::check_limits(meta.len(), payload.len())?;
338    let w = Arc::new(Waiter {
339      method,
340      watch,
341      out: Mutex::new(None),
342      cv: Condvar::new(),
343    });
344    let id = {
345      let mut core = lock(&self.core);
346      if let Some(e) = &core.end {
347        return Err(e.dup());
348      }
349      core.next_id += 1;
350      let id = core.next_id;
351      core.pending.insert(id, w.clone());
352      id
353    };
354    let sent = {
355      let mut s = lock(&self.writer);
356      frame::write_parts(&mut *s, 0, method, id, &meta, payload).map_err(write_timeout)
357    };
358    if let Err(e) = sent {
359      // A failed write may leave half a frame on the wire, so the connection is unusable.
360      self.forget(id);
361      self.kill_with(&aborted(&format!("send failed: {e}")));
362      return Err(e);
363    }
364    let deadline = (!self.timeout.is_zero()).then(|| Instant::now() + self.timeout);
365    let resp = match w.wait(deadline) {
366      Some(r) => r?,
367      None => self.give_up(id, method, &w)?,
368    };
369    if resp.flags & FLAG_ERROR != 0 {
370      let e = match WireError::decode(&resp.meta[..]) {
371        Ok(e) => e,
372        Err(e) => {
373          let e = Error::Protocol(format!("bad error meta: {e}"));
374          self.kill_with(&e);
375          return Err(e);
376        }
377      };
378      return Err(Error::Server {
379        code: e.code,
380        message: e.message,
381      });
382    }
383    Ok(resp)
384  }
385
386  /// Handles a call whose wait ran out. With room in the abandoned set the connection stays up
387  /// and the late reply is dropped. Otherwise the connection is cut.
388  fn give_up(&self, id: u64, method: u16, w: &Waiter) -> Result<Frame> {
389    let limit = self.limit();
390    let kept = {
391      let mut core = lock(&self.core);
392      if core.pending.remove(&id).is_none() {
393        None
394      } else if limit > 1 && core.abandoned.len() < limit * 4 {
395        core.abandoned.insert(id, method);
396        Some(true)
397      } else {
398        Some(false)
399      }
400    };
401    match kept {
402      // The reply or the failure won the race and is being delivered.
403      None => w.wait(None).unwrap_or_else(|| Err(aborted("connection closed"))),
404      Some(keep) => {
405        if !keep {
406          self.kill();
407        }
408        Err(Error::Timeout(format!(
409          "method {method} did not finish within {:?}",
410          self.timeout
411        )))
412      }
413    }
414  }
415
416  /// Like `call`, decoding the meta as `R` and returning the reply payload.
417  pub(crate) fn call_msg<R: Message + Default>(
418    &self,
419    method: u16,
420    req: &impl Message,
421    payload: &[u8],
422  ) -> Result<(R, Vec<u8>)> {
423    self.decode(self.call(method, req, payload, None)?)
424  }
425
426  /// Starts a watch. The reader routes the notices of the new watch to `chan` before the call returns.
427  pub(crate) fn call_watch(&self, req: &WatchRequest, chan: Arc<Chan>) -> Result<WatchResponse> {
428    Ok(self.decode(self.call(method::WATCH, req, &[], Some(chan))?)?.0)
429  }
430
431  fn decode<R: Message + Default>(&self, resp: Frame) -> Result<(R, Vec<u8>)> {
432    match R::decode(&resp.meta[..]) {
433      Ok(msg) => Ok((msg, resp.payload)),
434      Err(e) => {
435        let e = Error::Protocol(format!("bad response meta: {e}"));
436        self.kill_with(&e);
437        Err(e)
438      }
439    }
440  }
441
442  /// Routes one non-notice frame from the reader. An error is a protocol violation.
443  fn deliver(self: &Arc<Self>, hub: &Hub, f: Frame) -> Result<()> {
444    let unexpected = |f: &Frame| Error::Protocol(format!("unexpected frame: method {} id {}", f.method, f.request_id));
445    if f.flags & FLAG_RESPONSE == 0 {
446      return Err(unexpected(&f));
447    }
448    let waiter = {
449      let mut core = lock(&self.core);
450      match core.pending.get(&f.request_id).map(|w| w.method) {
451        Some(m) if m == f.method => core.pending.remove(&f.request_id),
452        Some(_) => return Err(unexpected(&f)),
453        None => {
454          if core.abandoned.get(&f.request_id) != Some(&f.method) {
455            return Err(unexpected(&f));
456          }
457          core.abandoned.remove(&f.request_id);
458          None
459        }
460      }
461    };
462    let ok = f.flags & (FLAG_RESPONSE | FLAG_ERROR) == FLAG_RESPONSE;
463    match waiter {
464      Some(w) => {
465        if ok && f.method == method::WATCH {
466          if let (Ok(r), Some(chan)) = (WatchResponse::decode(&f.meta[..]), &w.watch) {
467            hub.add_watch(r.watch_id, self.id, chan.clone());
468          }
469        }
470        w.set(Ok(f));
471      }
472      None if ok => self.release_late(f),
473      None => {}
474    }
475    Ok(())
476  }
477
478  /// A reply arrived for a request nobody waits for. An Open, Opendir or Watch created a server
479  /// resource, so a thread frees it. The reader never writes: it must keep reading.
480  fn release_late(self: &Arc<Self>, f: Frame) {
481    let job: Box<dyn FnOnce(&Sock) + Send> = match f.method {
482      method::OPEN => match OpenResponse::decode(&f.meta[..]) {
483        Ok(r) => Box::new(move |s| drop(s.call_msg::<Empty>(method::CLOSE, &CloseRequest { handle: r.handle }, &[]))),
484        Err(_) => return,
485      },
486      method::OPENDIR => match OpendirResponse::decode(&f.meta[..]) {
487        Ok(r) => {
488          Box::new(move |s| drop(s.call_msg::<Empty>(method::CLOSEDIR, &ClosedirRequest { handle: r.handle }, &[])))
489        }
490        Err(_) => return,
491      },
492      method::WATCH => match WatchResponse::decode(&f.meta[..]) {
493        Ok(r) => {
494          Box::new(move |s| drop(s.call_msg::<Empty>(method::UNWATCH, &UnwatchRequest { watch_id: r.watch_id }, &[])))
495        }
496        Err(_) => return,
497      },
498      _ => return,
499    };
500    if self.is_dead() {
501      return;
502    }
503    let (sock, guard) = (self.clone(), self.hub.thread_guard());
504    let spawned = std::thread::Builder::new()
505      .name("mountos-release".into())
506      .spawn(move || {
507        let _guard = guard;
508        job(&sock)
509      });
510    match spawned {
511      Ok(h) => {
512        let mut rs = lock(&self.releasers);
513        rs.retain(|h| !h.is_finished());
514        rs.push(h);
515      }
516      Err(e) => self.hub.log(&format!("could not start release of a late reply: {e}")),
517    }
518  }
519}
520
521/// Reads frames until the stream ends. Replies go to the waiting call, notices to the hub.
522fn read_loop(mut s: UnixStream, sock: &Arc<Sock>) {
523  let hub = sock.hub.clone();
524  let end = loop {
525    match frame::read(&mut s) {
526      Ok(Some(f)) if f.flags & FLAG_NOTIFY != 0 => on_notice(f, &hub, sock),
527      Ok(Some(f)) => {
528        if let Err(e) = sock.deliver(&hub, f) {
529          break e;
530        }
531      }
532      Ok(None) => break aborted("server closed the connection"),
533      Err(e) => break e,
534    }
535  };
536  let _ = s.shutdown(Shutdown::Both);
537  sock.fail(end, true);
538}
539
540fn on_notice(f: Frame, hub: &Hub, sock: &Sock) {
541  if f.method != method::NOTICE {
542    hub.log(&format!("ignored notify frame with method {}", f.method));
543    return;
544  }
545  let ev = match Notice::decode(&f.meta[..]) {
546    Ok(m) => match Event::from_wire(m) {
547      Some(ev) => ev,
548      None => {
549        hub.log("ignored notice of unknown kind");
550        return;
551      }
552    },
553    Err(e) => {
554      hub.log(&format!("ignored undecodable notice: {e}"));
555      return;
556    }
557  };
558  if ev.kind == EventKind::Closing {
559    sock.saw_closing.store(true, Ordering::SeqCst);
560  }
561  hub.publish(Some(sock.id), ev);
562}
563
564struct SlotState {
565  sock: Option<Arc<Sock>>,
566  /// Counts connects. A file remembers it to detect that its handle died with the old connection.
567  epoch: u64,
568}
569
570/// Wakes callers that wait for a free connection. Every release bumps the generation, so a
571/// waiter that read the generation before it scanned the pool cannot miss a release.
572#[derive(Default)]
573pub(crate) struct Gate {
574  generation: Mutex<u64>,
575  cv: Condvar,
576  /// Callers that found every connection busy and wait for the first to free.
577  waiting: AtomicUsize,
578}
579
580/// Marks a caller as waiting for any free connection until dropped.
581struct Waiting<'a>(&'a Gate);
582
583impl Drop for Waiting<'_> {
584  fn drop(&mut self) {
585    self.0.waiting.fetch_sub(1, Ordering::SeqCst);
586    self.0.bump();
587  }
588}
589
590impl Gate {
591  fn generation(&self) -> u64 {
592    *lock(&self.generation)
593  }
594
595  fn bump(&self) {
596    *lock(&self.generation) += 1;
597    self.cv.notify_all();
598  }
599
600  fn wait_change(&self, seen: u64) {
601    let mut g = lock(&self.generation);
602    while *g == seen {
603      g = self.cv.wait(g).unwrap_or_else(|e| e.into_inner());
604    }
605  }
606
607  fn wait_for_free(&self) -> Waiting<'_> {
608    self.waiting.fetch_add(1, Ordering::SeqCst);
609    Waiting(self)
610  }
611
612  /// Holds back a caller bound to one connection while others wait for any free one, so a long
613  /// operation that goes request after request cannot starve a small call.
614  fn let_waiters_pass(&self) {
615    loop {
616      let seen = self.generation();
617      if self.waiting.load(Ordering::SeqCst) == 0 {
618        return;
619      }
620      self.wait_change(seen);
621    }
622  }
623}
624
625/// Holds one of a slot's request places until dropped, then wakes callers that wait for room.
626struct Permit<'a>(&'a Slot);
627
628impl Drop for Permit<'_> {
629  fn drop(&mut self) {
630    self.0.outstanding.fetch_sub(1, Ordering::SeqCst);
631    self.0.gate.bump();
632  }
633}
634
635/// A lazily connected pool entry. Up to `limit` requests run on its connection at once. A
636/// request owns a place for one round trip only, never across the chunks of a large operation.
637pub(crate) struct Slot {
638  path: PathBuf,
639  name: String,
640  timeout: Duration,
641  max_inflight: usize,
642  hub: Arc<Hub>,
643  gate: Arc<Gate>,
644  /// Requests that hold a place.
645  outstanding: AtomicUsize,
646  /// Places available. One until the Hello reply says more.
647  limit: AtomicUsize,
648  closed: AtomicBool,
649  /// The current connection, reachable without the state lock so `close` can cut a dial or a
650  /// call in flight.
651  live: Mutex<Option<Arc<Sock>>>,
652  state: Mutex<SlotState>,
653}
654
655impl Slot {
656  fn try_reserve(&self) -> Option<Permit<'_>> {
657    let limit = self.limit.load(Ordering::SeqCst);
658    let mut cur = self.outstanding.load(Ordering::SeqCst);
659    loop {
660      if cur >= limit {
661        return None;
662      }
663      match self
664        .outstanding
665        .compare_exchange_weak(cur, cur + 1, Ordering::SeqCst, Ordering::SeqCst)
666      {
667        Ok(_) => return Some(Permit(self)),
668        Err(c) => cur = c,
669      }
670    }
671  }
672
673  /// Runs `f` on the connection once a place is free. With `want`, fails if the connection is
674  /// not the one with that epoch. A broken connection is replaced on the next call.
675  pub(crate) fn run<T>(&self, want: Option<u64>, f: impl FnOnce(&Sock) -> Result<T>) -> Result<(T, u64)> {
676    self.gate.let_waiters_pass();
677    let permit = loop {
678      let seen = self.gate.generation();
679      if self.closed.load(Ordering::SeqCst) {
680        return Err(aborted("client closed"));
681      }
682      if let Some(p) = self.try_reserve() {
683        break p;
684      }
685      self.gate.wait_change(seen);
686    };
687    self.exec(permit, want, f)
688  }
689
690  /// Runs `f` for a caller that already holds a place and releases it at the end.
691  fn exec<T>(&self, _permit: Permit<'_>, want: Option<u64>, f: impl FnOnce(&Sock) -> Result<T>) -> Result<(T, u64)> {
692    let (sock, epoch) = self.sock_for(want)?;
693    let r = f(&sock);
694    if matches!(r, Err(Error::Io(_)) | Err(Error::Protocol(_))) {
695      sock.kill();
696    }
697    r.map(|v| (v, epoch))
698  }
699
700  /// The live connection, dialed when there is none.
701  fn sock_for(&self, want: Option<u64>) -> Result<(Arc<Sock>, u64)> {
702    let mut st = lock(&self.state);
703    if self.closed.load(Ordering::SeqCst) {
704      return Err(aborted("client closed"));
705    }
706    let alive = st.sock.as_ref().is_some_and(|s| !s.is_dead());
707    if want.is_some_and(|e| !alive || st.epoch != e) {
708      return Err(aborted("connection lost, file handle is gone"));
709    }
710    if !alive {
711      if let Some(old) = st.sock.take() {
712        old.finish();
713      }
714      let s = Sock::open(&self.path, &self.hub, self.timeout)?;
715      *lock(&self.live) = Some(s.clone());
716      let shaken = if self.closed.load(Ordering::SeqCst) {
717        Err(aborted("client closed"))
718      } else {
719        s.handshake(&self.name, self.max_inflight)
720      };
721      if let Err(e) = shaken {
722        s.finish();
723        return Err(e);
724      }
725      self.limit.store(s.limit(), Ordering::SeqCst);
726      st.sock = Some(s);
727      st.epoch += 1;
728      self.gate.bump();
729    }
730    Ok((st.sock.clone().expect("connected above"), st.epoch))
731  }
732
733  /// Refuses new calls and cuts the connection, which ends the calls in flight.
734  fn close_start(&self) {
735    self.closed.store(true, Ordering::SeqCst);
736    if let Some(s) = lock(&self.live).as_ref() {
737      s.kill();
738    }
739    self.gate.bump();
740  }
741
742  /// Stops the connection's threads.
743  fn close_finish(&self) {
744    let s = lock(&self.state).sock.take();
745    if let Some(s) = s {
746      s.finish();
747    }
748  }
749}
750
751pub(crate) struct Inner {
752  slots: Vec<Arc<Slot>>,
753  gate: Arc<Gate>,
754  next: AtomicUsize,
755  pub(crate) hub: Arc<Hub>,
756  dispatcher: Mutex<Option<JoinHandle<()>>>,
757}
758
759impl Inner {
760  /// Ends every connection, reader, watch and the dispatcher. Safe to call twice.
761  fn shutdown(&self) {
762    for s in &self.slots {
763      s.close_start();
764    }
765    for s in &self.slots {
766      s.close_finish();
767    }
768    self.hub.shutdown();
769    let d = lock(&self.dispatcher).take();
770    if let Some(h) = d {
771      join_other(h);
772    }
773    self.hub.clear_listeners();
774  }
775}
776
777impl Drop for Inner {
778  fn drop(&mut self) {
779    self.shutdown();
780  }
781}
782
783/// Client for one mount. Cheap to clone, clones share the pool, the listeners and the event
784/// dispatcher. The last clone to drop closes everything. `close` does it at once for all clones.
785#[derive(Clone)]
786pub struct Client {
787  pub(crate) inner: Arc<Inner>,
788}
789
790impl Client {
791  /// Connects with default options (4 connections).
792  pub fn connect(path: impl AsRef<Path>) -> Result<Client> {
793    Client::connect_with(path, Options::default())
794  }
795
796  /// Connects with the given options. The first connection and handshake happen now,
797  /// so a bad socket or version mismatch fails here.
798  pub fn connect_with(path: impl AsRef<Path>, opts: Options) -> Result<Client> {
799    let n = opts.pool_size.max(1);
800    let hub = Arc::new(Hub::new(opts.logger.clone()));
801    let gate = Arc::new(Gate::default());
802    let slots: Vec<_> = (0..n)
803      .map(|_| {
804        Arc::new(Slot {
805          path: path.as_ref().to_path_buf(),
806          name: opts.client_name.clone(),
807          timeout: opts.call_timeout,
808          max_inflight: opts.max_inflight.clamp(1, MAX_INFLIGHT_CEILING),
809          hub: hub.clone(),
810          gate: gate.clone(),
811          outstanding: AtomicUsize::new(0),
812          limit: AtomicUsize::new(1),
813          closed: AtomicBool::new(false),
814          live: Mutex::new(None),
815          state: Mutex::new(SlotState { sock: None, epoch: 0 }),
816        })
817      })
818      .collect();
819    let (h, guard) = (hub.clone(), hub.thread_guard());
820    let dispatcher = std::thread::Builder::new()
821      .name("mountos-dispatch".into())
822      .spawn(move || {
823        let _guard = guard;
824        h.dispatch_loop()
825      })?;
826    let inner = Inner {
827      slots,
828      gate,
829      next: AtomicUsize::new(0),
830      hub,
831      dispatcher: Mutex::new(Some(dispatcher)),
832    };
833    // On failure, dropping `inner` stops the dispatcher and any reader.
834    inner.slots[0].run(None, |_| Ok(()))?;
835    Ok(Client { inner: Arc::new(inner) })
836  }
837
838  /// Ends all connections, watches and background threads. Later calls fail with an I/O error
839  /// of kind `ConnectionAborted`. Dropping the last clone does the same.
840  pub fn close(&self) {
841    self.inner.shutdown();
842  }
843
844  pub(crate) fn pool_size(&self) -> usize {
845    self.inner.slots.len()
846  }
847
848  /// Runs `f` on the connection with the fewest requests outstanding that has room. The scan
849  /// starts at a rotating position so equal choices spread out. When all are full it waits for whichever frees first. Returns the slot
850  /// and epoch the call ran on, for callers that bind a handle to it.
851  pub(crate) fn run_any<T>(&self, f: impl FnOnce(&Sock) -> Result<T>) -> Result<(T, &Arc<Slot>, u64)> {
852    let inner = &*self.inner;
853    let n = inner.slots.len();
854    let mut waiter = None;
855    loop {
856      let seen = inner.gate.generation();
857      let start = inner.next.fetch_add(1, Ordering::Relaxed) % n;
858      let mut best: Option<(&Arc<Slot>, usize)> = None;
859      for i in 0..n {
860        let slot = &inner.slots[(start + i) % n];
861        let out = slot.outstanding.load(Ordering::SeqCst);
862        if out >= slot.limit.load(Ordering::SeqCst) {
863          continue;
864        }
865        if best.as_ref().is_none_or(|(_, k)| out < *k) {
866          best = Some((slot, out));
867        }
868      }
869      if let Some((slot, _)) = best {
870        // Losing the race for the place means another caller took it, so scan again.
871        if let Some(permit) = slot.try_reserve() {
872          drop(waiter);
873          let (v, epoch) = slot.exec(permit, None, f)?;
874          return Ok((v, slot, epoch));
875        }
876        continue;
877      }
878      if waiter.is_none() {
879        // Register, then scan once more: a place may have freed in between.
880        waiter = Some(inner.gate.wait_for_free());
881        continue;
882      }
883      inner.gate.wait_change(seen);
884    }
885  }
886
887  pub(crate) fn unary<R: Message + Default>(&self, m: u16, req: &impl Message) -> Result<R> {
888    Ok(self.run_any(|c| c.call_msg::<R>(m, req, &[]))?.0 .0)
889  }
890
891  /// Opens a file. `flags` combine `open_flag` values. The file stays bound to the connection that opened it.
892  pub fn open(&self, path: &str, flags: u32, mode: u32) -> Result<File> {
893    let ((r, _), slot, epoch) = self.run_any(|c| {
894      c.call_msg::<OpenResponse>(
895        method::OPEN,
896        &OpenRequest {
897          path: path.into(),
898          flags,
899          mode,
900        },
901        &[],
902      )
903    })?;
904    Ok(File::new(slot.clone(), r.handle, epoch))
905  }
906
907  /// Sets times in nanoseconds since the epoch. `None` leaves that time unchanged.
908  pub fn utimens(&self, path: &str, atime_ns: Option<i64>, mtime_ns: Option<i64>) -> Result<()> {
909    self
910      .unary::<Empty>(
911        method::UTIMENS,
912        &UtimensRequest {
913          path: path.into(),
914          atime_ns: atime_ns.unwrap_or(0),
915          mtime_ns: mtime_ns.unwrap_or(0),
916          set_atime: atime_ns.is_some(),
917          set_mtime: mtime_ns.is_some(),
918        },
919      )
920      .map(drop)
921  }
922
923  /// Opens a directory for paged listing. The directory stays bound to the connection that opened it.
924  pub fn opendir(&self, path: &str) -> Result<Dir> {
925    let ((r, _), slot, epoch) =
926      self.run_any(|c| c.call_msg::<OpendirResponse>(method::OPENDIR, &OpendirRequest { path: path.into() }, &[]))?;
927    Ok(Dir::new(slot.clone(), r.handle, epoch))
928  }
929
930  /// Lists up to `max_entries` entries with their attributes. Pass an empty `cursor` to start,
931  /// then the returned cursor to continue. An empty returned cursor means the listing is complete.
932  pub fn ls(&self, path: &str, max_entries: u32, cursor: &str) -> Result<(Vec<DirEntry>, String)> {
933    let r: LsResponse = self.unary(
934      method::LS,
935      &LsRequest {
936        path: path.into(),
937        max_entries,
938        cursor: cursor.into(),
939      },
940    )?;
941    Ok((r.entries, r.next_cursor))
942  }
943}