Skip to main content

ninep_client/
lib.rs

1//! Asynchronous 9P2000.L client.
2//!
3//! # Reconnection
4//!
5//! Fids and byte-range locks are connection-scoped. The client records their
6//! replay state, reconnects with backoff, and restores that state before
7//! releasing blocked requests. Reorder-sensitive mutations retain one op-id
8//! across resends. Resends stop at the protocol retry horizon; expiry returns a
9//! disconnect error with an ambiguous outcome.
10//!
11//! An op-id is local to one public mutation future and is not reusable after the
12//! future completes or is cancelled.
13//!
14//! Dropping a future after a fid- or lock-state request is dispatched retires
15//! the connection that carried it. If that connection is still current,
16//! requests wait for reconnect and replay before resuming.
17//!
18//! The negotiated message size is fixed for the logical session. Reconnect
19//! candidates must negotiate the same value.
20//!
21//! Replay restores recorded fids and locks before the replacement connection
22//! becomes live. An opened fid that no longer exists becomes an `ESTALE`
23//! tombstone; its operations fail without affecting other fids. Failure to
24//! restore a held lock terminates the logical session.
25
26use arc_swap::ArcSwap;
27use bytes::{Bytes, BytesMut};
28use dashmap::mapref::entry::Entry;
29use dashmap::{DashMap, DashSet};
30use futures::StreamExt;
31use futures::stream::FuturesUnordered;
32pub use ninep_proto::NOFID;
33use ninep_proto::retry::MUTATION_RETRY_HORIZON;
34use ninep_proto::slice_codec::{LOCK_BLOCKED, LOCK_SUCCESS};
35use ninep_proto::*;
36use std::collections::HashMap;
37#[cfg(not(target_arch = "wasm32"))]
38use std::net::{IpAddr, SocketAddr};
39#[cfg(not(target_arch = "wasm32"))]
40use std::path::{Path, PathBuf};
41use std::sync::Arc;
42use std::sync::Mutex;
43use std::sync::atomic::{AtomicBool, AtomicU16, AtomicU32, AtomicU64, Ordering};
44use std::time::Duration;
45#[cfg(not(target_arch = "wasm32"))]
46use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
47#[cfg(not(target_arch = "wasm32"))]
48use tokio::net::{TcpStream, UnixStream};
49use tokio::sync::{Notify, OwnedSemaphorePermit, Semaphore, mpsc, oneshot};
50#[cfg(not(target_arch = "wasm32"))]
51use tokio_util::codec::LengthDelimitedCodec;
52use tracing::{debug, info, warn};
53use uuid::Uuid;
54
55mod linux;
56mod runtime;
57#[cfg(target_arch = "wasm32")]
58mod web_transport;
59
60/// The 9P "no tag" sentinel. We never allocate it for a normal request.
61const NOTAG: u16 = 0xFFFF;
62/// Filesystem RPCs admitted concurrently by one client.
63const MAX_INFLIGHT_REQUESTS: usize = 1024;
64const _: () = assert!(MAX_INFLIGHT_REQUESTS < NOTAG as usize);
65/// Default TCP port used when a target omits one.
66#[cfg(not(target_arch = "wasm32"))]
67const DEFAULT_9P_PORT: u16 = 5564;
68
69const RECONNECT_BACKOFF_MIN: Duration = Duration::from_millis(50);
70const RECONNECT_BACKOFF_MAX: Duration = Duration::from_millis(500);
71/// Per-target dial and negotiation timeout.
72const PROBE_TIMEOUT: Duration = Duration::from_secs(3);
73/// Reply timeout before liveness checks begin.
74const REQUEST_TIMEOUT: Duration = Duration::from_secs(8);
75/// Sentinel stored while no receiver-owned liveness probe has a tag.
76const NO_LIVENESS_PROBE_TAG: u32 = u32::MAX;
77
78/// FIRST/RETRY state for one request future.
79#[derive(Default)]
80struct OpAttemptState {
81    /// Epoch of the first dispatched frame. `Some` marks subsequent frames as
82    /// retries and remains stable across writers.
83    origin_epoch: Option<u64>,
84    started: Option<runtime::Clock>,
85}
86
87impl OpAttemptState {
88    /// Dispatches a frame and records its ambiguity without an await boundary.
89    fn dispatch_frame<T>(
90        &mut self,
91        has_op_id: bool,
92        connection_epoch: u64,
93        dispatch: impl FnOnce(u8, u64) -> ClientResult<T>,
94    ) -> ClientResult<(u8, T)> {
95        let flags = if has_op_id && self.origin_epoch.is_some() {
96            P9_OP_FLAG_RETRY
97        } else {
98            0
99        };
100        let origin_epoch = if has_op_id {
101            self.origin_epoch.unwrap_or(connection_epoch)
102        } else {
103            0
104        };
105        let result = dispatch(flags, origin_epoch)?;
106        if has_op_id {
107            self.origin_epoch.get_or_insert(origin_epoch);
108            if self.started.is_none() {
109                self.started = Some(runtime::Clock::now());
110            }
111        }
112        Ok((flags, result))
113    }
114
115    fn proven_predispatch(&mut self, sent_flags: u8) {
116        if sent_flags & P9_OP_FLAG_RETRY == 0 {
117            self.origin_epoch = None;
118            self.started = None;
119        }
120    }
121
122    fn retry_budget(&self) -> ClientResult<Option<Duration>> {
123        let Some(started) = self.started.as_ref() else {
124            return Ok(None);
125        };
126        let elapsed = Duration::from_millis(started.elapsed_millis());
127        let retry_horizon = MUTATION_RETRY_HORIZON;
128        if elapsed >= retry_horizon {
129            return Err(ClientError::Disconnected);
130        }
131        Ok(Some(retry_horizon - elapsed))
132    }
133}
134
135/// Retires the connection when a cancelled stateful request may have changed
136/// server state that is absent from [`SessionState`].
137struct StatefulCancellationGuard<'a> {
138    client: &'a NinePClient,
139    dispatched_conn: &'a Mutex<Option<Arc<Conn>>>,
140    armed: bool,
141}
142
143impl StatefulCancellationGuard<'_> {
144    fn disarm(&mut self) {
145        self.armed = false;
146    }
147}
148
149impl Drop for StatefulCancellationGuard<'_> {
150    fn drop(&mut self) {
151        let conn = self
152            .armed
153            .then(|| self.dispatched_conn.lock().unwrap().take())
154            .flatten();
155        if let Some(conn) = conn {
156            self.client.force_reprobe(&conn);
157        }
158    }
159}
160
161#[derive(Debug)]
162pub enum ClientError {
163    /// The server returned an `Rlerror` with this Linux errno.
164    Errno(u32),
165    /// The connection was lost (or never established).
166    Disconnected,
167    /// The server sent a reply we did not expect for the request.
168    Unexpected(&'static str),
169    /// A message failed to (de)serialise.
170    Codec(CodecError),
171}
172
173impl std::fmt::Display for ClientError {
174    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175        match self {
176            ClientError::Errno(e) => write!(f, "server error: errno {e}"),
177            ClientError::Disconnected => write!(f, "9P connection lost"),
178            ClientError::Unexpected(m) => write!(f, "unexpected 9P reply to {m}"),
179            ClientError::Codec(e) => write!(f, "9P codec error: {e}"),
180        }
181    }
182}
183
184impl std::error::Error for ClientError {}
185
186impl ClientError {
187    /// Map to a Linux errno suitable for a FUSE reply. Transport-level problems
188    /// surface as `EIO`.
189    pub fn to_errno(&self) -> i32 {
190        match self {
191            ClientError::Errno(e) => *e as i32,
192            _ => linux::EIO,
193        }
194    }
195}
196
197pub type ClientResult<T> = Result<T, ClientError>;
198
199/// Applies the remaining retry horizon while no request is in flight.
200/// In-flight responses may settle after the horizon.
201async fn await_resend_bounded<T>(
202    attempt: &OpAttemptState,
203    future: impl std::future::Future<Output = T>,
204) -> ClientResult<T> {
205    match attempt.retry_budget()? {
206        Some(remaining) => runtime::timeout(remaining, future)
207            .await
208            .map_err(|_| ClientError::Disconnected),
209        None => Ok(future.await),
210    }
211}
212
213/// Cumulative wire traffic for this logical client across reconnects.
214#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
215pub struct TrafficStats {
216    pub bytes_sent: u64,
217    pub bytes_received: u64,
218    pub operations: u64,
219}
220
221#[derive(Default)]
222struct TrafficCounters {
223    bytes_sent: AtomicU64,
224    bytes_received: AtomicU64,
225    operations: AtomicU64,
226}
227
228mod ops;
229pub use ops::{DirEntryCookie, ReaddirState, SetattrBuilder, SetattrTime};
230
231/// A 9P endpoint. Multi-target clients probe for the serving leader.
232///
233/// Native IPv6 targets accept `[address]:port` and legacy unbracketed
234/// `address:port`. `fd00::10:5564` maps to `[fd00::10]:5564`;
235/// `[fd00::10:5564]` maps to the complete literal on the default port.
236#[derive(Clone, Debug, PartialEq, Eq)]
237pub enum Target {
238    #[cfg(not(target_arch = "wasm32"))]
239    Tcp(SocketAddr),
240    /// A `host:port` TCP endpoint resolved on each probe.
241    #[cfg(not(target_arch = "wasm32"))]
242    TcpHost(String),
243    #[cfg(not(target_arch = "wasm32"))]
244    Unix(PathBuf),
245    /// A browser WebSocket carrying one complete 9P frame per binary message.
246    #[cfg(target_arch = "wasm32")]
247    WebSocket(String),
248}
249
250impl Target {
251    /// Parse a comma-separated target set. Whitespace and empty segments are
252    /// ignored; an entirely empty set is rejected.
253    pub fn parse_list(spec: &str) -> Result<Vec<Self>, String> {
254        let targets = spec
255            .split(',')
256            .map(str::trim)
257            .filter(|spec| !spec.is_empty())
258            .map(str::parse)
259            .collect::<Result<Vec<_>, _>>()?;
260        if targets.is_empty() {
261            Err("no 9P target given".into())
262        } else {
263            Ok(targets)
264        }
265    }
266}
267
268#[cfg(not(target_arch = "wasm32"))]
269fn parse_numeric_tcp_endpoint(endpoint: &str) -> Option<SocketAddr> {
270    if let Ok(addr) = endpoint.parse::<SocketAddr>() {
271        return Some(addr);
272    }
273
274    // Brackets without a port unambiguously denote the complete IPv6 literal.
275    if let Some(literal) = endpoint
276        .strip_prefix('[')
277        .and_then(|literal| literal.strip_suffix(']'))
278        && let Ok(ip @ IpAddr::V6(_)) = literal.parse::<IpAddr>()
279    {
280        return Some(SocketAddr::new(ip, DEFAULT_9P_PORT));
281    }
282
283    // Legacy unbracketed IPv6 host:port splits only when the prefix is a valid
284    // IPv6 address.
285    if !endpoint.starts_with('[')
286        && let Some((host, port)) = endpoint.rsplit_once(':')
287        && let Ok(ip @ IpAddr::V6(_)) = host.parse::<IpAddr>()
288        && let Ok(port) = port.parse::<u16>()
289    {
290        return Some(SocketAddr::new(ip, port));
291    }
292
293    endpoint
294        .parse::<IpAddr>()
295        .ok()
296        .map(|ip| SocketAddr::new(ip, DEFAULT_9P_PORT))
297}
298
299impl std::str::FromStr for Target {
300    type Err = String;
301
302    fn from_str(spec: &str) -> Result<Self, Self::Err> {
303        let spec = spec.trim();
304        if spec.is_empty() {
305            return Err("empty 9P target".into());
306        }
307
308        #[cfg(target_arch = "wasm32")]
309        {
310            return if spec.starts_with("ws://") || spec.starts_with("wss://") {
311                Ok(Self::WebSocket(spec.into()))
312            } else {
313                Err(format!(
314                    "browser clients require a ws:// or wss:// target, got {spec:?}"
315                ))
316            };
317        }
318
319        #[cfg(not(target_arch = "wasm32"))]
320        {
321            if let Some(path) = spec.strip_prefix("unix:") {
322                return Ok(Self::Unix(path.strip_prefix("//").unwrap_or(path).into()));
323            }
324            let endpoint = spec.strip_prefix("tcp://").unwrap_or(spec);
325            if endpoint.starts_with('/') || endpoint.starts_with('.') {
326                return Ok(Self::Unix(endpoint.into()));
327            }
328            if let Some(addr) = parse_numeric_tcp_endpoint(endpoint) {
329                return Ok(Self::Tcp(addr));
330            }
331            Ok(Self::TcpHost(if endpoint.contains(':') {
332                endpoint.into()
333            } else {
334                format!("{endpoint}:{DEFAULT_9P_PORT}")
335            }))
336        }
337    }
338}
339
340#[cfg(test)]
341mod target_parse_tests {
342    use super::*;
343
344    #[test]
345    fn target_grammar_matrix() {
346        #[cfg(not(target_arch = "wasm32"))]
347        {
348            let cases = [
349                ("unix:///run/z.sock", r#"Unix("/run/z.sock")"#),
350                ("./z.sock", r#"Unix("./z.sock")"#),
351                ("tcp://127.0.0.1:6000", "Tcp(127.0.0.1:6000)"),
352                ("127.0.0.1", "Tcp(127.0.0.1:5564)"),
353                ("::1", "Tcp([::1]:5564)"),
354                ("leader.example", r#"TcpHost("leader.example:5564")"#),
355                ("leader.example:6000", r#"TcpHost("leader.example:6000")"#),
356            ];
357            for (spec, expected) in cases {
358                assert_eq!(format!("{:?}", spec.parse::<Target>().unwrap()), expected);
359            }
360            assert_eq!(
361                format!(
362                    "{:?}",
363                    Target::parse_list("retired.invalid, ,leader.example:6000,").unwrap()
364                ),
365                r#"[TcpHost("retired.invalid:5564"), TcpHost("leader.example:6000")]"#
366            );
367        }
368
369        #[cfg(target_arch = "wasm32")]
370        {
371            let targets = Target::parse_list("ws://node-a:5564, ,wss://node-b/9p").unwrap();
372            assert_eq!(targets.len(), 2);
373            assert!(matches!(&targets[0], Target::WebSocket(url) if url == "ws://node-a:5564"));
374            assert!("tcp://node-a:5564".parse::<Target>().is_err());
375        }
376
377        assert!(Target::parse_list(" , ").is_err());
378        assert!(" ".parse::<Target>().is_err());
379    }
380
381    #[cfg(not(target_arch = "wasm32"))]
382    #[test]
383    fn legacy_ipv6_ambiguity_matches_documented_mapping() {
384        let cases = [
385            ("::1", "[::1]:5564"),
386            ("::1:5564", "[::1]:5564"),
387            ("fd00::10", "[fd00::10]:5564"),
388            ("fd00::10:5564", "[fd00::10]:5564"),
389            ("tcp://fd00::10:6000", "[fd00::10]:6000"),
390            ("[fd00::10]:6000", "[fd00::10]:6000"),
391            ("[fd00::10]", "[fd00::10]:5564"),
392            ("tcp://[::1]", "[::1]:5564"),
393            ("[fd00::10:5564]", "[fd00::10:5564]:5564"),
394        ];
395
396        for (spec, expected) in cases {
397            assert_eq!(
398                spec.parse::<Target>(),
399                Ok(Target::Tcp(expected.parse::<SocketAddr>().unwrap())),
400                "target {spec:?}"
401            );
402        }
403    }
404}
405
406/// One transport and its reader/writer tasks.
407struct Conn {
408    writer_tx: mpsc::Sender<Vec<u8>>,
409    pending: DashMap<u16, PendingRequest>,
410    tag_ctr: AtomicU16,
411    /// Durability lineage returned by `Tgetlineage`.
412    lineage_token: AtomicU64,
413    /// Writer epoch returned by `Tgetlineage`; zero for standalone servers.
414    writer_epoch: AtomicU64,
415    /// Set by whichever of the reader/writer tasks first sees the socket fail.
416    dead: AtomicBool,
417    /// Number of complete framed replies received on this stream.
418    ///
419    /// A request snapshots this counter at the start of each reply window.
420    /// Progress on any tag proves the shared byte stream is still moving.
421    receive_generation: AtomicU64,
422    /// Tag retained until the one outstanding Tgetlineage response is drained.
423    liveness_probe_tag: AtomicU32,
424    /// Serializes explicit liveness probes.
425    probe_lock: tokio::sync::Mutex<()>,
426    /// Signals the (possibly idle) writer task to stop when the reader exits.
427    writer_shutdown: Notify,
428    /// Stops the reader when a healthy connection is discarded before install.
429    reader_shutdown: Notify,
430    counters: Arc<TrafficCounters>,
431}
432
433type AdmissionPermit = Arc<OwnedSemaphorePermit>;
434
435/// Response routing state retained until delivery or connection teardown.
436/// A dispatched request keeps its admission permit here so cancelling its
437/// future cannot release capacity while the reply remains unresolved.
438struct PendingRequest {
439    reply: oneshot::Sender<Bytes>,
440    _admission: Option<AdmissionPermit>,
441}
442
443impl Conn {
444    /// Stops both transport tasks.
445    fn shutdown(&self) {
446        self.dead.store(true, Ordering::Release);
447        self.liveness_probe_tag
448            .store(NO_LIVENESS_PROBE_TAG, Ordering::Release);
449        self.pending.clear();
450        self.reader_shutdown.notify_one();
451        self.writer_shutdown.notify_one();
452    }
453
454    /// Records receipt of one complete framed reply.
455    fn note_received_frame(&self) {
456        self.receive_generation.fetch_add(1, Ordering::Release);
457    }
458
459    fn receive_generation(&self) -> u64 {
460        self.receive_generation.load(Ordering::Acquire)
461    }
462
463    fn deliver(&self, frame: Bytes) {
464        self.counters
465            .bytes_received
466            .fetch_add(frame.len() as u64, Ordering::Relaxed);
467        if frame.len() < P9_HEADER_SIZE {
468            warn!(
469                "9P client: response frame too short ({} bytes)",
470                frame.len()
471            );
472            return;
473        }
474        self.note_received_frame();
475        let tag = u16::from_le_bytes([frame[5], frame[6]]);
476        let _ = self.liveness_probe_tag.compare_exchange(
477            u32::from(tag),
478            NO_LIVENESS_PROBE_TAG,
479            Ordering::AcqRel,
480            Ordering::Acquire,
481        );
482        if let Some((_, pending)) = self.pending.remove(&tag) {
483            let _ = pending.reply.send(frame);
484        } else {
485            debug!("9P client: response for unknown tag {tag}");
486        }
487    }
488
489    fn connection_lost(&self, reconnect: &Notify) {
490        self.shutdown();
491        reconnect.notify_waiters();
492    }
493}
494
495/// Owns a response slot until dispatch. Dispatched slots remain quarantined
496/// until response delivery or connection teardown prevents tag aliasing.
497struct PendingTag<C: std::ops::Deref<Target = Conn>> {
498    conn: C,
499    tag: u16,
500    dispatched: bool,
501}
502
503impl<C: std::ops::Deref<Target = Conn>> PendingTag<C> {
504    fn mark_dispatched(&mut self) {
505        self.dispatched = true;
506    }
507}
508
509impl<C: std::ops::Deref<Target = Conn>> Drop for PendingTag<C> {
510    fn drop(&mut self) {
511        if !self.dispatched {
512            self.conn.pending.remove(&self.tag);
513        }
514    }
515}
516
517/// Fid replay record keyed by stable inode identity.
518#[derive(Clone)]
519struct FidRecord {
520    inode_id: u64,
521    /// Stable inode the original Tattach resolved to.
522    root_inode: u64,
523    n_uname: u32,
524    /// Original `Tattach` username for name-derived credentials.
525    uname: Vec<u8>,
526    /// `Some(flags)` if open; replayed with a `Tlopen`.
527    opened: Option<u32>,
528}
529
530impl FidRecord {
531    fn replay_identity(&self) -> (u32, u64, Vec<u8>) {
532        (self.n_uname, self.root_inode, self.uname.clone())
533    }
534}
535
536#[derive(Clone)]
537struct LockRecord {
538    fid: u32,
539    lock_type: LockType,
540    start: u64,
541    length: u64,
542    proc_id: u32,
543    client_id: Vec<u8>,
544}
545
546/// The replayable session state: enough to rebuild every fid and lock.
547#[derive(Default, Clone)]
548struct SessionState {
549    fids: HashMap<u32, FidRecord>,
550    locks: Vec<LockRecord>,
551    /// Default root for the public `rebind` helper.
552    default_root: Option<u64>,
553}
554
555impl SessionState {
556    fn forget_fid(&mut self, fid: u32) {
557        self.fids.remove(&fid);
558        self.locks.retain(|lock| lock.fid != fid);
559    }
560}
561
562pub struct NinePClient {
563    /// Targets probed by the reconnect supervisor.
564    targets: Vec<Target>,
565    /// Current transport, swapped atomically by the reconnect supervisor.
566    conn: ArcSwap<Conn>,
567    /// False while a reconnect+replay is in progress; requests block until true.
568    live: AtomicBool,
569    /// Permanent replay errno; zero while replay remains possible.
570    terminal_errno: AtomicU32,
571    live_notify: Notify,
572    reconnect_notify: Arc<Notify>,
573    /// Negotiated once for the logical session. Reconnects must match it so an
574    /// ambiguous mutation can be resent byte-for-byte.
575    msize: u32,
576    /// Suppresses repeated message-size mismatch warnings for this logical session.
577    msize_mismatch_warned: Arc<AtomicBool>,
578    fid_ctr: AtomicU32,
579    fid_free: Mutex<Vec<u32>>,
580    /// Recorded fids (by inode id) and held locks, replayed on reconnect.
581    state: Mutex<SessionState>,
582    /// Opened fids that could not be restored and remain allocated by the caller.
583    stale_fids: DashSet<u32>,
584    /// Orders stateful response settlement against snapshot/replay/install.
585    session_transition: tokio::sync::Mutex<()>,
586    /// Dispatch window for filesystem RPCs. Session-maintenance exchanges use
587    /// the raw request path and do not consume permits.
588    request_admission: Arc<Semaphore>,
589    /// Per-inode durability obligations carried across reconnects.
590    unsynced: DashMap<u64, Unsynced>,
591    counters: Arc<TrafficCounters>,
592}
593
594/// One inode's durability obligation. `generation` prevents an fsync from clearing
595/// a concurrent write. `reported` keeps `ESTALE` visible until a replacement write.
596#[derive(Default)]
597struct Unsynced {
598    oldest: Option<u64>,
599    generation: u64,
600    reported: bool,
601}
602
603#[derive(Clone, Copy)]
604enum FsyncScope {
605    Inode(u64),
606    All,
607}
608
609impl FsyncScope {
610    fn wire_flag(self) -> u32 {
611        match self {
612            Self::Inode(_) => P9_FSYNC_INODE,
613            Self::All => 0,
614        }
615    }
616}
617
618impl Unsynced {
619    /// Records a write, preserving the oldest unreported lineage token.
620    fn note(&mut self, token: u64) {
621        if self.reported {
622            self.oldest = Some(token);
623            self.reported = false;
624        } else {
625            self.oldest = Some(self.oldest.map_or(token, |oldest| oldest.min(token)));
626        }
627        self.generation = self.generation.wrapping_add(1);
628    }
629
630    fn snapshot(&self) -> (Option<u64>, u64) {
631        (self.oldest, self.generation)
632    }
633
634    /// Clears the obligation if no write followed the fsync snapshot.
635    fn clear_if_unchanged(&mut self, generation: u64) {
636        if self.generation == generation {
637            self.oldest = None;
638            self.reported = false;
639        }
640    }
641
642    /// Marks a stale obligation reported if no write followed the snapshot.
643    fn report_if_unchanged(&mut self, generation: u64) {
644        if self.generation == generation {
645            self.reported = true;
646        }
647    }
648}
649
650impl NinePClient {
651    /// Connect to a 9P server over TCP and negotiate the protocol version.
652    #[cfg(not(target_arch = "wasm32"))]
653    pub async fn connect_tcp(addr: SocketAddr, requested_msize: u32) -> std::io::Result<Arc<Self>> {
654        Self::connect(vec![Target::Tcp(addr)], requested_msize)
655            .await
656            .map_err(|e| std::io::Error::other(e.to_string()))
657    }
658
659    /// Connect to a 9P server over a Unix domain socket and negotiate the version.
660    #[cfg(not(target_arch = "wasm32"))]
661    pub async fn connect_unix(
662        path: impl AsRef<Path>,
663        requested_msize: u32,
664    ) -> std::io::Result<Arc<Self>> {
665        Self::connect(
666            vec![Target::Unix(path.as_ref().to_path_buf())],
667            requested_msize,
668        )
669        .await
670        .map_err(|e| std::io::Error::other(e.to_string()))
671    }
672
673    /// Connect to the serving leader in a target set and re-probe on reconnect.
674    pub async fn connect_multi(
675        targets: Vec<Target>,
676        requested_msize: u32,
677    ) -> std::io::Result<Arc<Self>> {
678        Self::connect(targets, requested_msize)
679            .await
680            .map_err(|e| std::io::Error::other(e.to_string()))
681    }
682
683    /// Connect to 9P over a browser WebSocket.
684    #[cfg(target_arch = "wasm32")]
685    pub async fn connect_websocket(url: &str, requested_msize: u32) -> ClientResult<Arc<Self>> {
686        Self::connect(vec![Target::WebSocket(url.to_string())], requested_msize).await
687    }
688
689    async fn connect(targets: Vec<Target>, requested_msize: u32) -> ClientResult<Arc<Self>> {
690        let reconnect_notify = Arc::new(Notify::new());
691        let counters = Arc::new(TrafficCounters::default());
692        let msize_mismatch_warned = Arc::new(AtomicBool::new(false));
693        let (conn, msize) = Self::probe(
694            &targets,
695            requested_msize,
696            None,
697            Arc::clone(&reconnect_notify),
698            Arc::clone(&counters),
699            Arc::clone(&msize_mismatch_warned),
700        )
701        .await?;
702
703        let client = Arc::new(Self {
704            targets,
705            conn: ArcSwap::new(conn),
706            live: AtomicBool::new(true),
707            terminal_errno: AtomicU32::new(0),
708            live_notify: Notify::new(),
709            reconnect_notify,
710            msize,
711            msize_mismatch_warned,
712            fid_ctr: AtomicU32::new(1),
713            fid_free: Mutex::new(Vec::new()),
714            state: Mutex::new(SessionState::default()),
715            stale_fids: DashSet::new(),
716            session_transition: tokio::sync::Mutex::new(()),
717            request_admission: Arc::new(Semaphore::new(MAX_INFLIGHT_REQUESTS)),
718            unsynced: DashMap::new(),
719            counters,
720        });
721        client.spawn_supervisor();
722        Ok(client)
723    }
724
725    /// Open a fresh socket, spawn its reader/writer tasks and negotiate.
726    async fn connect_once(
727        target: &Target,
728        requested_msize: u32,
729        required_msize: Option<u32>,
730        reconnect_notify: Arc<Notify>,
731        counters: Arc<TrafficCounters>,
732        msize_mismatch_warned: Arc<AtomicBool>,
733    ) -> ClientResult<(Arc<Conn>, u32)> {
734        let transport = dial(target).await?;
735        let (writer_tx, writer_rx) = mpsc::channel::<Vec<u8>>(P9_CHANNEL_SIZE);
736        let conn = Arc::new(Conn {
737            writer_tx,
738            pending: DashMap::new(),
739            tag_ctr: AtomicU16::new(0),
740            lineage_token: AtomicU64::new(0),
741            writer_epoch: AtomicU64::new(0),
742            dead: AtomicBool::new(false),
743            receive_generation: AtomicU64::new(0),
744            liveness_probe_tag: AtomicU32::new(NO_LIVENESS_PROBE_TAG),
745            probe_lock: tokio::sync::Mutex::new(()),
746            writer_shutdown: Notify::new(),
747            reader_shutdown: Notify::new(),
748            counters,
749        });
750
751        match transport {
752            #[cfg(not(target_arch = "wasm32"))]
753            DialedTransport::Native { read, write } => {
754                spawn_writer(
755                    write,
756                    writer_rx,
757                    Arc::clone(&conn),
758                    Arc::clone(&reconnect_notify),
759                );
760                spawn_reader(read, Arc::clone(&conn), reconnect_notify);
761            }
762            #[cfg(target_arch = "wasm32")]
763            DialedTransport::WebSocket(io) => {
764                web_transport::spawn(io, writer_rx, Arc::clone(&conn), reconnect_notify);
765            }
766        }
767
768        match runtime::timeout(PROBE_TIMEOUT, negotiate_on(&conn, requested_msize)).await {
769            Ok(Ok(msize)) => {
770                if let Some(required) = required_msize
771                    && msize != required
772                {
773                    if !msize_mismatch_warned.swap(true, Ordering::AcqRel) {
774                        warn!(
775                            "9P reconnect candidate negotiated msize {msize}; logical session requires {required}"
776                        );
777                    } else {
778                        debug!(
779                            "9P reconnect candidate still negotiates msize {msize}; logical session requires {required}"
780                        );
781                    }
782                    conn.shutdown();
783                    return Err(ClientError::Unexpected("version"));
784                }
785                Ok((conn, msize))
786            }
787            // Healthy socket but the handshake failed/stalled; tear down so the fd is not leaked.
788            Ok(Err(e)) => {
789                conn.shutdown();
790                Err(e)
791            }
792            Err(_) => {
793                conn.shutdown();
794                Err(ClientError::Disconnected)
795            }
796        }
797    }
798
799    /// Probes all targets concurrently. Successful negotiation proves leadership.
800    async fn probe(
801        targets: &[Target],
802        requested_msize: u32,
803        required_msize: Option<u32>,
804        reconnect_notify: Arc<Notify>,
805        counters: Arc<TrafficCounters>,
806        msize_mismatch_warned: Arc<AtomicBool>,
807    ) -> ClientResult<(Arc<Conn>, u32)> {
808        let mut probes = FuturesUnordered::new();
809        for target in targets {
810            let target = target.clone();
811            let notify = Arc::clone(&reconnect_notify);
812            let counters = Arc::clone(&counters);
813            let warned = Arc::clone(&msize_mismatch_warned);
814            probes.push(async move {
815                Self::connect_once(
816                    &target,
817                    requested_msize,
818                    required_msize,
819                    notify,
820                    counters,
821                    warned,
822                )
823                .await
824            });
825        }
826
827        let mut last_err = None;
828        let mut winner = None;
829        while let Some(res) = probes.next().await {
830            match res {
831                Ok(triple) => {
832                    winner = Some(triple);
833                    break;
834                }
835                Err(e) => last_err = Some(e),
836            }
837        }
838
839        // Completed losing probes may own live transport tasks.
840        if !probes.is_empty() {
841            runtime::spawn(async move {
842                while let Some(res) = probes.next().await {
843                    if let Ok((conn, _)) = res {
844                        conn.shutdown();
845                    }
846                }
847            });
848        }
849
850        winner.ok_or_else(|| last_err.unwrap_or(ClientError::Disconnected))
851    }
852
853    /// Reconnects and replays indefinitely after transport loss.
854    fn spawn_supervisor(self: &Arc<Self>) {
855        let weak = Arc::downgrade(self);
856        let notify = Arc::clone(&self.reconnect_notify);
857        runtime::spawn(async move {
858            loop {
859                // Register before reading `dead` to prevent a lost notification.
860                loop {
861                    let notified = notify.notified();
862                    tokio::pin!(notified);
863                    notified.as_mut().enable();
864                    let this = match weak.upgrade() {
865                        Some(t) => t,
866                        None => return,
867                    };
868                    if this.conn.load().dead.load(Ordering::Acquire) {
869                        this.live.store(false, Ordering::Release);
870                        break;
871                    }
872                    drop(this);
873                    notified.await;
874                }
875
876                warn!("9P connection lost; reconnecting and replaying session…");
877                let mut backoff = RECONNECT_BACKOFF_MIN;
878                loop {
879                    let this = match weak.upgrade() {
880                        Some(t) => t,
881                        None => return,
882                    };
883                    match this.reconnect_once().await {
884                        Ok(()) => {
885                            this.live.store(true, Ordering::Release);
886                            this.live_notify.notify_waiters();
887                            info!("9P session reconnected; replay complete");
888                            break;
889                        }
890                        Err(e) => {
891                            if this.terminal_errno.load(Ordering::Acquire) != 0 {
892                                warn!("9P session replay failed permanently: {e}");
893                                return;
894                            }
895                            debug!("9P reconnect failed ({e}); retrying in {backoff:?}");
896                            drop(this);
897                            runtime::sleep(backoff).await;
898                            backoff = (backoff * 2).min(RECONNECT_BACKOFF_MAX);
899                        }
900                    }
901                }
902            }
903        });
904    }
905
906    /// Dials, replays, and installs one replacement connection.
907    async fn reconnect_once(self: &Arc<Self>) -> ClientResult<()> {
908        let msize = self.msize();
909        let (conn, msize) = Self::probe(
910            &self.targets,
911            msize,
912            Some(msize),
913            Arc::clone(&self.reconnect_notify),
914            Arc::clone(&self.counters),
915            Arc::clone(&self.msize_mismatch_warned),
916        )
917        .await?;
918
919        // Settlement and snapshot/replay/install are mutually exclusive.
920        let _transition = self.session_transition.lock().await;
921        debug_assert_eq!(msize, self.msize);
922        if let Err(error) = self.replay_supervised(&conn).await {
923            self.discard_replay_candidate(&conn).await;
924            return Err(error);
925        }
926        let old = self.conn.swap(conn);
927        old.dead.store(true, Ordering::Release);
928        old.writer_shutdown.notify_one();
929
930        Ok(())
931    }
932
933    /// Resets a failed candidate before close. The ordered `Tversion` reply
934    /// confirms release of replay-created server state.
935    async fn discard_replay_candidate(&self, conn: &Conn) {
936        match runtime::timeout(PROBE_TIMEOUT, version_on(conn, self.msize)).await {
937            Ok(Ok(_)) => {}
938            Ok(Err(error)) => {
939                debug!("9P replay candidate reset failed ({error}); closing connection");
940            }
941            Err(_) => {
942                debug!("9P replay candidate reset timed out; closing connection");
943            }
944        }
945        conn.shutdown();
946    }
947
948    /// Rebuild recorded state while the reconnect supervisor owns `self`.
949    async fn replay_supervised(self: &Arc<Self>, conn: &Conn) -> ClientResult<()> {
950        self.replay_inner(conn, REQUEST_TIMEOUT, Some(self)).await
951    }
952
953    #[cfg(test)]
954    async fn replay(&self, conn: &Conn) -> ClientResult<()> {
955        self.replay_inner(conn, REQUEST_TIMEOUT, None).await
956    }
957
958    #[cfg(test)]
959    async fn replay_with_rpc_timeout(
960        &self,
961        conn: &Conn,
962        rpc_timeout: Duration,
963    ) -> ClientResult<()> {
964        self.replay_inner(conn, rpc_timeout, None).await
965    }
966
967    /// Rebuilds recorded fids and locks on `conn`. Roots precede descendants.
968    async fn replay_inner(
969        &self,
970        conn: &Conn,
971        rpc_timeout: Duration,
972        supervisor_owner: Option<&Arc<Self>>,
973    ) -> ClientResult<()> {
974        let snapshot = self.state.lock().unwrap().clone();
975        let mut gone_unopened = Vec::new();
976        let mut stale_opened = Vec::new();
977
978        let (roots, descendants): (Vec<_>, Vec<_>) = snapshot
979            .fids
980            .iter()
981            .partition(|(_, rec)| rec.inode_id == rec.root_inode);
982        for (&fid, rec) in roots.into_iter().chain(descendants) {
983            let has_lock = snapshot.locks.iter().any(|lock| lock.fid == fid);
984            let restored = self.replay_fid(conn, fid, rec, rpc_timeout).await?;
985            if !restored {
986                if has_lock {
987                    return Err(self.replay_state_lost("fid with a held lock could not be rebound"));
988                }
989                if rec.opened.is_some() {
990                    stale_opened.push(fid);
991                } else {
992                    gone_unopened.push(fid);
993                }
994                continue;
995            }
996            if let Some(flags) = rec.opened {
997                match Self::send_replay_rpc(
998                    conn,
999                    Message::Tlopen(Tlopen { fid, flags }),
1000                    rpc_timeout,
1001                )
1002                .await
1003                {
1004                    Ok(Message::Rlopen(_)) => {}
1005                    Ok(_) => {
1006                        return Err(
1007                            self.replay_state_lost("opened fid returned a non-Rlopen reply")
1008                        );
1009                    }
1010                    Err(ClientError::Errno(errno)) if Self::replay_state_lost_errno(errno) => {
1011                        if has_lock {
1012                            return Err(self.replay_state_lost("locked fid could not be reopened"));
1013                        }
1014                        match Self::send_replay_rpc(
1015                            conn,
1016                            Message::Tclunk(Tclunk { fid }),
1017                            rpc_timeout,
1018                        )
1019                        .await
1020                        {
1021                            Ok(Message::Rclunk(_)) => {}
1022                            Ok(_) => return Err(ClientError::Unexpected("replay clunk")),
1023                            Err(error) => return Err(error),
1024                        }
1025                        stale_opened.push(fid);
1026                    }
1027                    Err(e) => return Err(e),
1028                }
1029            }
1030        }
1031
1032        self.replay_locks(conn, &snapshot.locks, rpc_timeout, supervisor_owner)
1033            .await?;
1034
1035        if !gone_unopened.is_empty() || !stale_opened.is_empty() {
1036            let mut state = self.state.lock().unwrap();
1037            for fid in gone_unopened.iter().chain(&stale_opened) {
1038                state.fids.remove(fid);
1039            }
1040            state.locks.retain(|lock| {
1041                !gone_unopened.contains(&lock.fid) && !stale_opened.contains(&lock.fid)
1042            });
1043            drop(state);
1044            for fid in &stale_opened {
1045                self.stale_fids.insert(*fid);
1046            }
1047        }
1048        Ok(())
1049    }
1050
1051    /// Reinstall the complete lock set, retrying conflicts as one unit.
1052    async fn replay_locks(
1053        &self,
1054        conn: &Conn,
1055        locks: &[LockRecord],
1056        rpc_timeout: Duration,
1057        supervisor_owner: Option<&Arc<Self>>,
1058    ) -> ClientResult<()> {
1059        let mut conflict_backoff = RECONNECT_BACKOFF_MIN;
1060        'lock_replay: loop {
1061            // The supervisor temporarily owns one strong reference while
1062            // reconnecting. If it is now the last owner, the mount/client was
1063            // dropped and no operation remains whose locks need restoration.
1064            if supervisor_owner.is_some_and(|owner| Arc::strong_count(owner) == 1) {
1065                conn.shutdown();
1066                return Err(ClientError::Disconnected);
1067            }
1068            // Install the complete recorded lock set or roll back its prefix.
1069            let mut acquired = Vec::new();
1070            for lk in locks {
1071                let body = Message::Tlock(Tlock {
1072                    fid: lk.fid,
1073                    lock_type: lk.lock_type,
1074                    flags: 0,
1075                    start: lk.start,
1076                    length: lk.length,
1077                    proc_id: lk.proc_id,
1078                    client_id: P9String::new(lk.client_id.clone()),
1079                });
1080                match Self::send_replay_rpc(conn, body, rpc_timeout).await {
1081                    Ok(Message::Rlock(r)) if r.status == LockStatus::Success.as_wire() => {
1082                        acquired.push(lk);
1083                    }
1084                    Ok(Message::Rlock(Rlock {
1085                        status: LOCK_BLOCKED,
1086                    }))
1087                    | Err(ClientError::Errno(linux::EAGAIN)) => {
1088                        let rolled_back = Self::rollback_replayed_locks(conn, &acquired).await;
1089                        if !rolled_back {
1090                            return Err(ClientError::Errno(linux::EAGAIN));
1091                        }
1092                        runtime::sleep(conflict_backoff).await;
1093                        conflict_backoff = (conflict_backoff * 2).min(RECONNECT_BACKOFF_MAX);
1094                        continue 'lock_replay;
1095                    }
1096                    Ok(Message::Rlock(_)) => {
1097                        Self::rollback_replayed_locks(conn, &acquired).await;
1098                        return Err(self.replay_state_lost("recorded lock was not reacquired"));
1099                    }
1100                    Ok(_) => {
1101                        Self::rollback_replayed_locks(conn, &acquired).await;
1102                        return Err(
1103                            self.replay_state_lost("lock replay returned a non-Rlock reply")
1104                        );
1105                    }
1106                    Err(ClientError::Errno(errno)) if Self::replay_state_lost_errno(errno) => {
1107                        Self::rollback_replayed_locks(conn, &acquired).await;
1108                        return Err(self.replay_state_lost("recorded lock was refused"));
1109                    }
1110                    Err(e) => {
1111                        Self::rollback_replayed_locks(conn, &acquired).await;
1112                        return Err(e);
1113                    }
1114                }
1115            }
1116            return Ok(());
1117        }
1118    }
1119
1120    /// Releases the lock prefix installed by this replay attempt.
1121    async fn rollback_replayed_locks(conn: &Conn, acquired: &[&LockRecord]) -> bool {
1122        if acquired.is_empty() {
1123            return true;
1124        }
1125        let rollback = async {
1126            let mut requests = FuturesUnordered::new();
1127            for lk in acquired.iter().rev() {
1128                requests.push(Self::send_raw_rpc(
1129                    conn,
1130                    Message::Tlock(Tlock {
1131                        fid: lk.fid,
1132                        lock_type: LockType::Unlock,
1133                        flags: 0,
1134                        start: lk.start,
1135                        length: lk.length,
1136                        proc_id: lk.proc_id,
1137                        client_id: P9String::new(lk.client_id.clone()),
1138                    }),
1139                ));
1140            }
1141            let mut acknowledged = true;
1142            while let Some(result) = requests.next().await {
1143                if !matches!(
1144                    result,
1145                    Ok(Message::Rlock(Rlock {
1146                        status: LOCK_SUCCESS
1147                    }))
1148                ) {
1149                    debug!("9P lock replay rollback was not acknowledged");
1150                    acknowledged = false;
1151                }
1152            }
1153            acknowledged
1154        };
1155        match runtime::timeout(PROBE_TIMEOUT, rollback).await {
1156            Ok(acknowledged) => acknowledged,
1157            Err(_) => {
1158                warn!("9P lock replay rollback timed out; discarding candidate session");
1159                false
1160            }
1161        }
1162    }
1163
1164    /// Rebinds one fid. `Ok(false)` denotes `ENOENT` or `ESTALE`.
1165    async fn replay_fid(
1166        &self,
1167        conn: &Conn,
1168        fid: u32,
1169        rec: &FidRecord,
1170        rpc_timeout: Duration,
1171    ) -> ClientResult<bool> {
1172        let body = Message::Trebind(Trebind {
1173            fid,
1174            inode_id: rec.inode_id,
1175            root_inode: rec.root_inode,
1176            flags: P9_REBIND_REPLAY
1177                | if rec.opened.is_some() {
1178                    P9_REBIND_OPENED
1179                } else {
1180                    0
1181                },
1182            uname: P9String::new(rec.uname.clone()),
1183            n_uname: rec.n_uname,
1184        });
1185        match Self::send_replay_rpc(conn, body, rpc_timeout).await {
1186            Ok(Message::Rrebind(_)) => Ok(true),
1187            Ok(_) => Err(ClientError::Unexpected("replay fid")),
1188            Err(ClientError::Errno(errno)) if Self::replay_state_lost_errno(errno) => Ok(false),
1189            Err(e) => Err(e),
1190        }
1191    }
1192
1193    fn replay_state_lost_errno(errno: u32) -> bool {
1194        matches!(errno, linux::ENOENT | linux::ESTALE)
1195    }
1196
1197    fn validate_fids(&self, fids: impl IntoIterator<Item = u32>) -> ClientResult<()> {
1198        if fids.into_iter().any(|fid| self.stale_fids.contains(&fid)) {
1199            return Err(ClientError::Errno(linux::ESTALE));
1200        }
1201        Ok(())
1202    }
1203
1204    /// The message size negotiated for the logical session.
1205    pub fn msize(&self) -> u32 {
1206        self.msize
1207    }
1208
1209    /// Whether requests currently have a live connection rather than waiting
1210    /// for reconnect/session replay.
1211    pub fn is_connected(&self) -> bool {
1212        self.live.load(Ordering::Acquire)
1213    }
1214
1215    /// Cumulative traffic across the lifetime of this logical client.
1216    pub fn traffic_stats(&self) -> TrafficStats {
1217        TrafficStats {
1218            bytes_sent: self.counters.bytes_sent.load(Ordering::Relaxed),
1219            bytes_received: self.counters.bytes_received.load(Ordering::Relaxed),
1220            operations: self.counters.operations.load(Ordering::Relaxed),
1221        }
1222    }
1223
1224    /// Resolve every inode a mutation will make responsible for durability.
1225    ///
1226    /// This runs before dispatch, while state still describes the request's
1227    /// input fids. That matters for `Tlcreate`, which changes its fid from the
1228    /// parent directory to the new child after the reply.
1229    fn mutation_inodes(&self, body: &Message) -> ClientResult<Vec<u64>> {
1230        if !body.is_mutation() {
1231            return Ok(Vec::new());
1232        }
1233        self.validate_fids(body.durability_fids())?;
1234        let state = self.state.lock().unwrap();
1235        let mut inodes = Vec::with_capacity(2);
1236        for fid in body.durability_fids() {
1237            let inode = state
1238                .fids
1239                .get(&fid)
1240                .map(|record| record.inode_id)
1241                .ok_or(ClientError::Errno(linux::EBADF))?;
1242            if !inodes.contains(&inode) {
1243                inodes.push(inode);
1244            }
1245        }
1246        Ok(inodes)
1247    }
1248
1249    /// Records an acknowledged mutation under its connection lineage.
1250    fn note_unsynced(&self, inode: u64, token: u64) {
1251        self.unsynced.entry(inode).or_default().note(token);
1252    }
1253
1254    /// Resolve a live fid to the stable inode identity used for durability.
1255    fn fid_inode(&self, fid: u32) -> ClientResult<u64> {
1256        self.validate_fids([fid])?;
1257        self.state
1258            .lock()
1259            .unwrap()
1260            .fids
1261            .get(&fid)
1262            .map(|record| record.inode_id)
1263            .ok_or(ClientError::Errno(linux::EBADF))
1264    }
1265
1266    /// Snapshot `(oldest token, generation)` of `inode` for a verified fsync.
1267    fn snapshot_unsynced(&self, inode: u64) -> (Option<u64>, u64) {
1268        self.unsynced
1269            .get(&inode)
1270            .map_or((None, 0), |u| u.snapshot())
1271    }
1272
1273    /// Clears an unchanged obligation after verified fsync. `remove_if` rechecks
1274    /// under the shard lock before deleting the empty entry.
1275    fn clear_unsynced_if_unchanged(&self, inode: u64, generation: u64) {
1276        if let Some(mut u) = self.unsynced.get_mut(&inode) {
1277            u.clear_if_unchanged(generation);
1278        }
1279        self.unsynced.remove_if(&inode, |_, u| u.oldest.is_none());
1280    }
1281
1282    /// Marks an unchanged obligation reported after `ESTALE`.
1283    fn report_unsynced_if_unchanged(&self, inode: u64, generation: u64) {
1284        if let Some(mut u) = self.unsynced.get_mut(&inode) {
1285            u.report_if_unchanged(generation);
1286        }
1287    }
1288
1289    /// Maximum data a single Tread/Treaddir response (Rread/Rreaddir) can carry
1290    /// within the negotiated msize: `msize - header - count`.
1291    pub fn max_io(&self) -> u32 {
1292        self.msize().saturating_sub(P9_IOHDRSZ)
1293    }
1294
1295    /// Maximum data a single Twrite *request* can carry within the negotiated
1296    /// msize. The Twrite header is larger than the Rread header, so this is
1297    /// smaller than [`Self::max_io`]; using max_io here would produce a frame a
1298    /// few bytes over msize that the server rejects.
1299    pub fn max_write_payload(&self) -> u32 {
1300        self.msize()
1301            .saturating_sub(P9_TWRITE_HDR)
1302            .saturating_sub(P9_OP_ENVELOPE_LEN as u32)
1303    }
1304
1305    /// Allocate a fresh fid (reusing a freed one when possible).
1306    pub fn alloc_fid(&self) -> u32 {
1307        if let Some(fid) = self.fid_free.lock().unwrap().pop() {
1308            return fid;
1309        }
1310        self.fid_ctr.fetch_add(1, Ordering::Relaxed)
1311    }
1312
1313    /// Return a fid to the free list. The caller must have clunked it already.
1314    pub fn free_fid(&self, fid: u32) {
1315        self.stale_fids.remove(&fid);
1316        self.fid_free.lock().unwrap().push(fid);
1317    }
1318
1319    /// Allocated fids not yet returned to the free list. Leak-accounting diagnostic.
1320    pub fn outstanding_fids(&self) -> usize {
1321        let allocated = self.fid_ctr.load(Ordering::Relaxed).saturating_sub(1) as usize;
1322        allocated.saturating_sub(self.fid_free.lock().unwrap().len())
1323    }
1324
1325    /// Records the first terminal replay failure.
1326    fn fail_session(&self, errno: u32) {
1327        let _ = self
1328            .terminal_errno
1329            .compare_exchange(0, errno, Ordering::AcqRel, Ordering::Acquire);
1330        self.live.store(false, Ordering::Release);
1331        self.live_notify.notify_waiters();
1332    }
1333
1334    fn replay_state_lost(&self, reason: &str) -> ClientError {
1335        warn!("9P session replay cannot preserve observed state: {reason}");
1336        self.fail_session(linux::ESTALE);
1337        ClientError::Errno(linux::ESTALE)
1338    }
1339
1340    /// Block until the connection is live (i.e. not mid-reconnect).
1341    async fn wait_until_live(&self) -> ClientResult<()> {
1342        let terminal_errno = self.terminal_errno.load(Ordering::Acquire);
1343        if terminal_errno != 0 {
1344            return Err(ClientError::Errno(terminal_errno));
1345        }
1346        if self.live.load(Ordering::Acquire) {
1347            return Ok(());
1348        }
1349        loop {
1350            let notified = self.live_notify.notified();
1351            tokio::pin!(notified);
1352            // Register the waiter *before* the check to avoid a lost wakeup.
1353            notified.as_mut().enable();
1354            let terminal_errno = self.terminal_errno.load(Ordering::Acquire);
1355            if terminal_errno != 0 {
1356                return Err(ClientError::Errno(terminal_errno));
1357            }
1358            if self.live.load(Ordering::Acquire) {
1359                return Ok(());
1360            }
1361            notified.await;
1362        }
1363    }
1364
1365    /// Register one response slot under an exact numeric tag. On collision the
1366    /// sender is returned so the allocator can try another tag.
1367    fn register_tag(
1368        conn: &Conn,
1369        tag: u16,
1370        tx: oneshot::Sender<Bytes>,
1371        admission: Option<AdmissionPermit>,
1372    ) -> Result<u16, oneshot::Sender<Bytes>> {
1373        match conn.pending.entry(tag) {
1374            Entry::Vacant(slot) => {
1375                slot.insert(PendingRequest {
1376                    reply: tx,
1377                    _admission: admission,
1378                });
1379                Ok(tag)
1380            }
1381            Entry::Occupied(_) => Err(tx),
1382        }
1383    }
1384
1385    /// Allocates and registers a tag. Exhaustion requires connection recycling.
1386    fn alloc_tag(
1387        conn: &Conn,
1388        mut tx: oneshot::Sender<Bytes>,
1389        admission: Option<&AdmissionPermit>,
1390    ) -> Result<u16, oneshot::Sender<Bytes>> {
1391        // Scan all 65,535 usable tags, including a cycle starting at NOTAG.
1392        for _ in 0..=usize::from(NOTAG) {
1393            let candidate = conn.tag_ctr.fetch_add(1, Ordering::Relaxed);
1394            if candidate == NOTAG {
1395                continue;
1396            }
1397            match Self::register_tag(conn, candidate, tx, admission.cloned()) {
1398                Ok(tag) => return Ok(tag),
1399                Err(returned) => tx = returned,
1400            }
1401        }
1402        Err(tx)
1403    }
1404
1405    /// Sends across reconnects and returns the accepted response connection.
1406    async fn send_request_on_current(
1407        &self,
1408        op_id: [u8; 16],
1409        body: &Message,
1410        attempt: &mut OpAttemptState,
1411        stateful_dispatched_conn: Option<&Mutex<Option<Arc<Conn>>>>,
1412        admission: &AdmissionPermit,
1413    ) -> ClientResult<(Message, Arc<Conn>)> {
1414        // Resolve before anything reaches the wire. Successful stateful
1415        // mutations may change a fid's binding before their public method
1416        // settles the response into replay state.
1417        let mutation_inodes = self.mutation_inodes(body)?;
1418        'resend: loop {
1419            self.validate_fids(body.request_fids())?;
1420            await_resend_bounded(attempt, self.wait_until_live()).await??;
1421            let conn = self.conn.load_full();
1422            self.validate_fids(body.request_fids())?;
1423
1424            // Reserve capacity before registering a response tag.
1425            let permit = match await_resend_bounded(attempt, conn.writer_tx.reserve()).await? {
1426                Ok(permit) => permit,
1427                Err(_) => {
1428                    runtime::yield_now().await;
1429                    continue;
1430                }
1431            };
1432
1433            // `live` may lag `dead`; reject the stale connection before registration.
1434            if conn.dead.load(Ordering::Acquire) {
1435                drop(permit);
1436                self.reconnect_notify.notify_waiters();
1437                runtime::yield_now().await;
1438                continue;
1439            }
1440
1441            let (otx, mut orx) = oneshot::channel();
1442            let tag = match Self::alloc_tag(&conn, otx, Some(admission)) {
1443                Ok(tag) => tag,
1444                Err(_) => {
1445                    // Teardown clears live and quarantined tags.
1446                    drop(permit);
1447                    self.force_reprobe(&conn);
1448                    runtime::yield_now().await;
1449                    continue;
1450                }
1451            };
1452            let mut pending = PendingTag {
1453                conn: Arc::clone(&conn),
1454                tag,
1455                dispatched: false,
1456            };
1457
1458            // `connection_lost` publishes `dead` before clearing registrations.
1459            // This second check closes the registration race.
1460            if conn.dead.load(Ordering::Acquire) {
1461                drop(permit);
1462                runtime::yield_now().await;
1463                continue;
1464            }
1465
1466            // Frames after the first possible dispatch carry RETRY.
1467            let has_op_id = op_id != [0u8; 16];
1468            let connection_epoch = conn.writer_epoch.load(Ordering::Relaxed);
1469            let (op_flags, ()) =
1470                attempt.dispatch_frame(has_op_id, connection_epoch, |op_flags, origin_epoch| {
1471                    let bytes = P9Message::encode_body(
1472                        tag,
1473                        MutationEnvelope {
1474                            op_id,
1475                            flags: op_flags,
1476                            origin_writer_epoch: origin_epoch,
1477                        },
1478                        body,
1479                        true,
1480                    )
1481                    .map_err(ClientError::Codec)?;
1482
1483                    // Registration-to-enqueue has no cancellation point.
1484                    pending.mark_dispatched();
1485                    if let Some(dispatched_conn) = stateful_dispatched_conn {
1486                        *dispatched_conn.lock().unwrap() = Some(Arc::clone(&conn));
1487                    }
1488                    permit.send(bytes);
1489                    Ok(())
1490                })?;
1491            // A later reply on this TCP stream proves that this request's
1492            // response was not lost; it may still be queued or executing.
1493            let mut observed_receive_generation = conn.receive_generation();
1494            let frame = loop {
1495                match runtime::timeout(REQUEST_TIMEOUT, &mut orx).await {
1496                    Ok(Ok(frame)) => break frame,
1497                    Ok(Err(_)) => {
1498                        // Lost the reply to a drop: wait for reconnect and resend.
1499                        runtime::yield_now().await;
1500                        continue 'resend;
1501                    }
1502                    Err(_) => {
1503                        let current_generation = conn.receive_generation();
1504                        if current_generation != observed_receive_generation {
1505                            observed_receive_generation = current_generation;
1506                            continue;
1507                        }
1508                        if Self::probe_connection(&conn, current_generation).await {
1509                            observed_receive_generation = conn.receive_generation();
1510                            continue;
1511                        }
1512                        self.force_reprobe(&conn);
1513                        runtime::yield_now().await;
1514                        continue 'resend;
1515                    }
1516                }
1517            };
1518
1519            // Responses never carry the private mutation envelope. Retain the
1520            // transport frame so bulk response payloads can borrow its storage.
1521            let msg = P9Message::from_owned_bytes_ctx(frame, false).map_err(ClientError::Codec)?;
1522            if let Message::Rlerror(ref e) = msg.body
1523                && matches!(e.ecode, P9_ENOTLEADER | P9_ENOTLEADER_CLEAN)
1524            {
1525                if e.ecode == P9_ENOTLEADER_CLEAN {
1526                    attempt.proven_predispatch(op_flags);
1527                    if let Some(dispatched_conn) = stateful_dispatched_conn {
1528                        dispatched_conn.lock().unwrap().take();
1529                    }
1530                }
1531                self.force_reprobe(&conn);
1532                runtime::yield_now().await;
1533                continue;
1534            }
1535            // Successful mutations create per-inode durability obligations.
1536            if !matches!(msg.body, Message::Rlerror(_)) {
1537                let token = conn.lineage_token.load(Ordering::Relaxed);
1538                for &inode in &mutation_inodes {
1539                    self.note_unsynced(inode, token);
1540                }
1541            }
1542            return Ok((msg.body, conn));
1543        }
1544    }
1545
1546    async fn acquire_request_admission(&self) -> ClientResult<AdmissionPermit> {
1547        Ok(Arc::new(
1548            Arc::clone(&self.request_admission)
1549                .acquire_owned()
1550                .await
1551                .map_err(|_| ClientError::Disconnected)?,
1552        ))
1553    }
1554
1555    /// Request path for operations that do not change replayable session state.
1556    async fn send_request(&self, body: Message) -> ClientResult<Message> {
1557        let admission = self.acquire_request_admission().await?;
1558        let op_id = if body.is_mutation() {
1559            Uuid::new_v4().into_bytes()
1560        } else {
1561            [0u8; 16]
1562        };
1563        let mut attempt = OpAttemptState::default();
1564        self.send_request_on_current(op_id, &body, &mut attempt, None, &admission)
1565            .await
1566            .map(|(response, _)| response)
1567    }
1568
1569    /// Claims settlement if `response_conn` is still current and live.
1570    async fn accept_stateful_response(
1571        &self,
1572        response_conn: &Arc<Conn>,
1573    ) -> Option<tokio::sync::MutexGuard<'_, ()>> {
1574        let transition = self.session_transition.lock().await;
1575        let current = self.conn.load_full();
1576        if self.live.load(Ordering::Acquire)
1577            && !response_conn.dead.load(Ordering::Acquire)
1578            && Arc::ptr_eq(response_conn, &current)
1579        {
1580            Some(transition)
1581        } else {
1582            None
1583        }
1584    }
1585
1586    /// Sends a session-state mutation. The returned guard covers local replay
1587    /// bookkeeping. Responses from replaced connections are resent.
1588    async fn send_stateful_request(
1589        &self,
1590        body: Message,
1591    ) -> ClientResult<(Message, tokio::sync::MutexGuard<'_, ()>)> {
1592        let admission = self.acquire_request_admission().await?;
1593        let op_id = if body.is_mutation() {
1594            Uuid::new_v4().into_bytes()
1595        } else {
1596            [0u8; 16]
1597        };
1598        let mut attempt = OpAttemptState::default();
1599        let dispatched_conn = Mutex::new(None);
1600        let mut cancellation = StatefulCancellationGuard {
1601            client: self,
1602            dispatched_conn: &dispatched_conn,
1603            armed: true,
1604        };
1605        let result = loop {
1606            let (response, response_conn) = match self
1607                .send_request_on_current(
1608                    op_id,
1609                    &body,
1610                    &mut attempt,
1611                    Some(&dispatched_conn),
1612                    &admission,
1613                )
1614                .await
1615            {
1616                Ok(response) => response,
1617                Err(error) => break Err(error),
1618            };
1619            if let Some(transition) = self.accept_stateful_response(&response_conn).await {
1620                break Ok((response, transition));
1621            }
1622        };
1623        // Errors and stale op-id results leave the server transition ambiguous.
1624        if !matches!(
1625            &result,
1626            Err(_)
1627                | Ok((
1628                    Message::Rlerror(Rlerror {
1629                        ecode: P9_EOPIDSTALE
1630                    }),
1631                    _
1632                ))
1633        ) {
1634            cancellation.disarm();
1635        }
1636        result
1637    }
1638
1639    /// Rechecks global receive progress, then runs one lease-gated probe.
1640    async fn probe_connection(conn: &Conn, observed_generation: u64) -> bool {
1641        Self::probe_connection_with_timeout(conn, observed_generation, PROBE_TIMEOUT).await
1642    }
1643
1644    async fn probe_connection_with_timeout(
1645        conn: &Conn,
1646        observed_generation: u64,
1647        probe_timeout: Duration,
1648    ) -> bool {
1649        if conn.dead.load(Ordering::Acquire) {
1650            return false;
1651        }
1652        if conn.receive_generation() != observed_generation {
1653            return true;
1654        }
1655        let _guard = conn.probe_lock.lock().await;
1656        if conn.dead.load(Ordering::Acquire) {
1657            return false;
1658        }
1659        if conn.receive_generation() != observed_generation {
1660            return true;
1661        }
1662        // A previous probe whose response has not arrived is already the
1663        // strongest request we can put on this stream. Do not accumulate
1664        // another quarantined tag behind it.
1665        if conn.liveness_probe_tag.load(Ordering::Acquire) != NO_LIVENESS_PROBE_TAG {
1666            return false;
1667        }
1668        match runtime::timeout(probe_timeout, query_lineage_token(conn)).await {
1669            Ok(Ok(())) => true,
1670            // The probe uses the same response queue and stream as normal
1671            // traffic. A response backlog can delay Rgetlineage past its own
1672            // deadline while other valid frames still prove this connection
1673            // is alive. The timed-out RPC keeps its dispatched registration
1674            // until its late reply is drained, so accepting that passive
1675            // evidence neither reuses its tag nor desynchronizes the stream.
1676            Err(_) => {
1677                !conn.dead.load(Ordering::Acquire)
1678                    && conn.receive_generation() != observed_generation
1679            }
1680            Ok(Err(_)) => false,
1681        }
1682    }
1683
1684    /// Tears down `conn` and wakes the reconnect supervisor.
1685    fn force_reprobe(&self, conn: &Arc<Conn>) {
1686        conn.shutdown();
1687        self.reconnect_notify.notify_waiters();
1688    }
1689
1690    /// A one-shot send on a specific connection, bypassing the live-gate and
1691    /// state recording. Used during reconnect to replay the session.
1692    async fn send_raw(conn: &Conn, body: Message) -> ClientResult<Message> {
1693        let track_liveness_probe = matches!(&body, Message::Tgetlineage(_));
1694        Self::send_raw_at_tag(conn, None, body, track_liveness_probe).await
1695    }
1696
1697    /// Sends on a specific connection with an allocated or exact tag.
1698    async fn send_raw_at_tag(
1699        conn: &Conn,
1700        exact_tag: Option<u16>,
1701        body: Message,
1702        track_liveness_probe: bool,
1703    ) -> ClientResult<Message> {
1704        let permit = conn
1705            .writer_tx
1706            .reserve()
1707            .await
1708            .map_err(|_| ClientError::Disconnected)?;
1709        if conn.dead.load(Ordering::Acquire) {
1710            drop(permit);
1711            return Err(ClientError::Disconnected);
1712        }
1713        let (otx, orx) = oneshot::channel();
1714        let tag = match exact_tag {
1715            Some(tag) => Self::register_tag(conn, tag, otx, None)
1716                .map_err(|_| ClientError::Unexpected("raw tag already registered"))?,
1717            None => match Self::alloc_tag(conn, otx, None) {
1718                Ok(tag) => tag,
1719                Err(_) => {
1720                    drop(permit);
1721                    conn.shutdown();
1722                    return Err(ClientError::Disconnected);
1723                }
1724            },
1725        };
1726        let mut pending = PendingTag {
1727            conn,
1728            tag,
1729            dispatched: false,
1730        };
1731        if conn.dead.load(Ordering::Acquire) {
1732            drop(permit);
1733            return Err(ClientError::Disconnected);
1734        }
1735        let bytes = match P9Message::new(tag, body).to_bytes() {
1736            Ok(b) => b,
1737            Err(e) => return Err(ClientError::Codec(e)),
1738        };
1739        if track_liveness_probe
1740            && conn
1741                .liveness_probe_tag
1742                .compare_exchange(
1743                    NO_LIVENESS_PROBE_TAG,
1744                    u32::from(tag),
1745                    Ordering::AcqRel,
1746                    Ordering::Acquire,
1747                )
1748                .is_err()
1749        {
1750            return Err(ClientError::Unexpected("liveness probe already pending"));
1751        }
1752        pending.mark_dispatched();
1753        permit.send(bytes);
1754        let frame = orx.await.map_err(|_| ClientError::Disconnected)?;
1755        let msg = P9Message::from_owned_bytes_ctx(frame, false).map_err(ClientError::Codec)?;
1756        if msg.tag != tag {
1757            return Err(ClientError::Unexpected("response tag"));
1758        }
1759        Ok(msg.body)
1760    }
1761
1762    /// [`Self::send_raw`] plus the `Rlerror -> Errno` mapping, so replay can tell
1763    /// "this object is gone" (a server error) from a genuine protocol desync.
1764    async fn send_raw_rpc(conn: &Conn, body: Message) -> ClientResult<Message> {
1765        match Self::send_raw(conn, body).await? {
1766            Message::Rlerror(e) => Err(ClientError::Errno(e.ecode)),
1767            other => Ok(other),
1768        }
1769    }
1770
1771    /// Bound one replay exchange without bounding the finite replay pass.
1772    async fn send_replay_rpc(
1773        conn: &Conn,
1774        body: Message,
1775        timeout: Duration,
1776    ) -> ClientResult<Message> {
1777        match runtime::timeout(timeout, Self::send_raw_rpc(conn, body)).await {
1778            Ok(result) => result,
1779            Err(_) => {
1780                // A timed-out dispatched tag remains quarantined until its
1781                // connection ends. Closing the candidate both releases it and
1782                // wakes transport tasks that may still own the frame.
1783                conn.shutdown();
1784                Err(ClientError::Disconnected)
1785            }
1786        }
1787    }
1788
1789    /// Issue a request, turning a returned `Rlerror` into [`ClientError::Errno`].
1790    async fn rpc(&self, body: Message) -> ClientResult<Message> {
1791        match self.send_request(body).await? {
1792            Message::Rlerror(e) => Err(ClientError::Errno(e.ecode)),
1793            other => Ok(other),
1794        }
1795    }
1796
1797    /// Stateful [`Self::rpc`]; the guard covers replay-state bookkeeping.
1798    async fn rpc_stateful(
1799        &self,
1800        body: Message,
1801    ) -> ClientResult<(Message, tokio::sync::MutexGuard<'_, ()>)> {
1802        let (response, transition) = self.send_stateful_request(body).await?;
1803        match response {
1804            Message::Rlerror(e) => Err(ClientError::Errno(e.ecode)),
1805            other => Ok((other, transition)),
1806        }
1807    }
1808
1809    pub async fn attach(
1810        &self,
1811        fid: u32,
1812        afid: u32,
1813        uname: &str,
1814        aname: &str,
1815        n_uname: u32,
1816    ) -> ClientResult<Qid> {
1817        let (resp, _transition) = self
1818            .rpc_stateful(Message::Tattach(Tattach {
1819                fid,
1820                afid,
1821                uname: P9String::new(uname.as_bytes().to_vec()),
1822                aname: P9String::new(aname.as_bytes().to_vec()),
1823                n_uname,
1824            }))
1825            .await?;
1826        match resp {
1827            Message::Rattach(r) => {
1828                let mut st = self.state.lock().unwrap();
1829                st.fids.insert(
1830                    fid,
1831                    FidRecord {
1832                        inode_id: r.qid.path,
1833                        root_inode: r.qid.path,
1834                        n_uname,
1835                        uname: uname.as_bytes().to_vec(),
1836                        opened: None,
1837                    },
1838                );
1839                st.default_root = Some(r.qid.path);
1840                Ok(r.qid)
1841            }
1842            _ => Err(ClientError::Unexpected("attach")),
1843        }
1844    }
1845
1846    /// Bind `fid` to an inode by id (no path walk), acting as `n_uname`. Used for
1847    /// legacy per-user fids. Because this form cannot carry a gid, callers that
1848    /// enforce server-side permissions should use [`Self::rebind_with_credentials`].
1849    pub async fn rebind(&self, fid: u32, inode_id: u64, n_uname: u32) -> ClientResult<Qid> {
1850        self.rebind_inner(fid, inode_id, n_uname, Vec::new()).await
1851    }
1852
1853    /// Bind `fid` to an inode by id with an authoritative filesystem identity.
1854    ///
1855    /// `groups` contains supplementary gids. Lists longer than
1856    /// [`P9_MAX_GROUPS`] are rejected with `E2BIG`; the exact encoded identity
1857    /// is retained for reconnect replay.
1858    pub async fn rebind_with_credentials(
1859        &self,
1860        fid: u32,
1861        inode_id: u64,
1862        uid: u32,
1863        gid: u32,
1864        groups: &[u32],
1865    ) -> ClientResult<Qid> {
1866        if groups.len() > P9_MAX_GROUPS {
1867            return Err(ClientError::Errno(linux::E2BIG));
1868        }
1869        self.rebind_with_credentials_inner(fid, inode_id, uid, gid, groups, true)
1870            .await
1871    }
1872
1873    /// Bind `fid` with a uid and primary gid when the caller cannot observe
1874    /// supplementary groups. The server still enforces owner permissions and
1875    /// accepts group access that an authoritative local permission check has
1876    /// already allowed.
1877    pub async fn rebind_with_primary_group(
1878        &self,
1879        fid: u32,
1880        inode_id: u64,
1881        uid: u32,
1882        gid: u32,
1883    ) -> ClientResult<Qid> {
1884        self.rebind_with_credentials_inner(fid, inode_id, uid, gid, &[], false)
1885            .await
1886    }
1887
1888    async fn rebind_with_credentials_inner(
1889        &self,
1890        fid: u32,
1891        inode_id: u64,
1892        uid: u32,
1893        gid: u32,
1894        groups: &[u32],
1895        groups_complete: bool,
1896    ) -> ClientResult<Qid> {
1897        let group_count = groups.len().min(P9_MAX_GROUPS);
1898        let mut payload = Vec::with_capacity(
1899            P9_REBIND_CREDENTIAL_HEADER_SIZE + group_count * std::mem::size_of::<u32>(),
1900        );
1901        payload.push(P9_REBIND_CREDENTIAL_SENTINEL);
1902        payload.push(P9_REBIND_CREDENTIAL_VERSION);
1903        payload.extend_from_slice(&gid.to_le_bytes());
1904        payload.push(
1905            group_count as u8
1906                | if groups_complete {
1907                    0
1908                } else {
1909                    P9_REBIND_CREDENTIAL_GROUPS_INCOMPLETE
1910                },
1911        );
1912        for group in &groups[..group_count] {
1913            payload.extend_from_slice(&group.to_le_bytes());
1914        }
1915        self.rebind_inner(fid, inode_id, uid, payload).await
1916    }
1917
1918    async fn rebind_inner(
1919        &self,
1920        fid: u32,
1921        inode_id: u64,
1922        n_uname: u32,
1923        uname: Vec<u8>,
1924    ) -> ClientResult<Qid> {
1925        let root_inode = self.state.lock().unwrap().default_root.unwrap_or(0);
1926        let (resp, _transition) = self
1927            .rpc_stateful(Message::Trebind(Trebind {
1928                fid,
1929                inode_id,
1930                root_inode,
1931                flags: 0,
1932                uname: P9String::new(uname.clone()),
1933                n_uname,
1934            }))
1935            .await?;
1936        match resp {
1937            Message::Rrebind(r) => {
1938                self.state.lock().unwrap().fids.insert(
1939                    fid,
1940                    FidRecord {
1941                        inode_id,
1942                        root_inode,
1943                        n_uname,
1944                        uname,
1945                        opened: None,
1946                    },
1947                );
1948                Ok(r.qid)
1949            }
1950            _ => Err(ClientError::Unexpected("rebind")),
1951        }
1952    }
1953
1954    pub async fn walk(&self, fid: u32, newfid: u32, names: &[&[u8]]) -> ClientResult<Vec<Qid>> {
1955        let wnames = names
1956            .iter()
1957            .map(|n| P9String::new(n.to_vec()))
1958            .collect::<Vec<_>>();
1959        let (resp, _transition) = self
1960            .rpc_stateful(Message::Twalk(Twalk {
1961                fid,
1962                newfid,
1963                nwname: wnames.len() as u16,
1964                wnames,
1965            }))
1966            .await?;
1967        match resp {
1968            Message::Rwalk(r) => {
1969                // Only a full walk creates `newfid` (a partial leaves it unset).
1970                if names.is_empty() || r.wqids.len() == names.len() {
1971                    let mut st = self.state.lock().unwrap();
1972                    let identity = st.fids.get(&fid).map(FidRecord::replay_identity);
1973                    let inode_id = if names.is_empty() {
1974                        st.fids.get(&fid).map(|rec| rec.inode_id)
1975                    } else {
1976                        r.wqids.last().map(|q| q.path)
1977                    };
1978                    if let (Some(inode_id), Some((n_uname, root_inode, uname))) =
1979                        (inode_id, identity)
1980                    {
1981                        st.fids.insert(
1982                            newfid,
1983                            FidRecord {
1984                                inode_id,
1985                                root_inode,
1986                                n_uname,
1987                                uname,
1988                                opened: None,
1989                            },
1990                        );
1991                    }
1992                }
1993                Ok(r.wqids)
1994            }
1995            _ => Err(ClientError::Unexpected("walk")),
1996        }
1997    }
1998
1999    /// Full walk plus the final stat in one round trip. Records `newfid` like
2000    /// [`Self::walk`].
2001    pub async fn walk_getattr(
2002        &self,
2003        fid: u32,
2004        newfid: u32,
2005        names: &[&[u8]],
2006    ) -> ClientResult<(Vec<Qid>, Stat)> {
2007        let wnames = names
2008            .iter()
2009            .map(|n| P9String::new(n.to_vec()))
2010            .collect::<Vec<_>>();
2011        let (resp, _transition) = self
2012            .rpc_stateful(Message::Twalkgetattr(Twalkgetattr {
2013                fid,
2014                newfid,
2015                nwname: wnames.len() as u16,
2016                wnames,
2017            }))
2018            .await?;
2019        match resp {
2020            Message::Rwalkgetattr(r) => {
2021                {
2022                    let mut st = self.state.lock().unwrap();
2023                    let identity = st.fids.get(&fid).map(FidRecord::replay_identity);
2024                    let inode_id = if names.is_empty() {
2025                        st.fids.get(&fid).map(|rec| rec.inode_id)
2026                    } else {
2027                        r.wqids.last().map(|q| q.path)
2028                    };
2029                    if let (Some(inode_id), Some((n_uname, root_inode, uname))) =
2030                        (inode_id, identity)
2031                    {
2032                        st.fids.insert(
2033                            newfid,
2034                            FidRecord {
2035                                inode_id,
2036                                root_inode,
2037                                n_uname,
2038                                uname,
2039                                opened: None,
2040                            },
2041                        );
2042                    }
2043                }
2044                Ok((r.wqids, r.stat))
2045            }
2046            _ => Err(ClientError::Unexpected("walk_getattr")),
2047        }
2048    }
2049
2050    pub async fn clunk(&self, fid: u32) -> ClientResult<()> {
2051        if self.stale_fids.contains(&fid) && self.clear_stale_fid(fid).await {
2052            return Ok(());
2053        }
2054        let (resp, _transition) = match self
2055            .send_stateful_request(Message::Tclunk(Tclunk { fid }))
2056            .await
2057        {
2058            Ok(result) => result,
2059            Err(error) => {
2060                if matches!(&error, ClientError::Errno(errno) if *errno == linux::ESTALE)
2061                    && self.clear_stale_fid(fid).await
2062                {
2063                    return Ok(());
2064                }
2065                return Err(error);
2066            }
2067        };
2068        // Remove local state before releasing the replay transition.
2069        let mut st = self.state.lock().unwrap();
2070        st.forget_fid(fid);
2071        drop(st);
2072        self.stale_fids.remove(&fid);
2073        match resp {
2074            Message::Rclunk(_) => Ok(()),
2075            Message::Rlerror(e) => Err(ClientError::Errno(e.ecode)),
2076            _ => Err(ClientError::Unexpected("clunk")),
2077        }
2078    }
2079
2080    async fn clear_stale_fid(&self, fid: u32) -> bool {
2081        let _transition = self.session_transition.lock().await;
2082        if self.terminal_errno.load(Ordering::Acquire) != 0 {
2083            return false;
2084        }
2085        if self.stale_fids.remove(&fid).is_none() {
2086            return false;
2087        }
2088        let mut state = self.state.lock().unwrap();
2089        state.forget_fid(fid);
2090        true
2091    }
2092
2093    pub async fn getattr(&self, fid: u32, mask: u64) -> ClientResult<Stat> {
2094        let resp = self
2095            .rpc(Message::Tgetattr(Tgetattr {
2096                fid,
2097                request_mask: mask,
2098            }))
2099            .await?;
2100        match resp {
2101            Message::Rgetattr(r) => Ok(r.stat),
2102            _ => Err(ClientError::Unexpected("getattr")),
2103        }
2104    }
2105
2106    pub async fn setattr(&self, ts: Tsetattr) -> ClientResult<()> {
2107        match self.rpc(Message::Tsetattr(ts)).await? {
2108            Message::Rsetattr(_) => Ok(()),
2109            _ => Err(ClientError::Unexpected("setattr")),
2110        }
2111    }
2112
2113    /// Atomically allocate, punch, or zero a file range through the negotiated
2114    /// ZeroFS-private Tfallocate request.
2115    pub async fn fallocate(
2116        &self,
2117        fid: u32,
2118        offset: u64,
2119        length: u64,
2120        mode: u32,
2121    ) -> ClientResult<()> {
2122        match self
2123            .rpc(Message::Tfallocate(Tfallocate {
2124                fid,
2125                offset,
2126                length,
2127                mode,
2128            }))
2129            .await?
2130        {
2131            Message::Rfallocate(_) => Ok(()),
2132            _ => Err(ClientError::Unexpected("fallocate")),
2133        }
2134    }
2135
2136    /// Like [`Self::setattr`] but the reply carries the post-op stat.
2137    pub async fn setattr_attr(&self, ts: Tsetattr) -> ClientResult<Stat> {
2138        match self.rpc(Message::Tsetattrattr(ts)).await? {
2139            Message::Rsetattrattr(r) => Ok(r.stat),
2140            _ => Err(ClientError::Unexpected("setattr_attr")),
2141        }
2142    }
2143
2144    pub async fn lopen(&self, fid: u32, flags: u32) -> ClientResult<(Qid, u32)> {
2145        let (response, _transition) = self
2146            .rpc_stateful(Message::Tlopen(Tlopen { fid, flags }))
2147            .await?;
2148        match response {
2149            Message::Rlopen(r) => {
2150                if let Some(rec) = self.state.lock().unwrap().fids.get_mut(&fid) {
2151                    rec.opened = Some(flags);
2152                }
2153                Ok((r.qid, r.iounit))
2154            }
2155            _ => Err(ClientError::Unexpected("lopen")),
2156        }
2157    }
2158
2159    /// Open `fid`'s inode on a fresh `newfid` in one round trip; `fid` is
2160    /// untouched.
2161    pub async fn lopenat(&self, fid: u32, newfid: u32, flags: u32) -> ClientResult<(Qid, u32)> {
2162        let (resp, _transition) = self
2163            .rpc_stateful(Message::Tlopenat(Tlopenat { fid, newfid, flags }))
2164            .await?;
2165        match resp {
2166            Message::Rlopenat(r) => {
2167                let mut st = self.state.lock().unwrap();
2168                if let Some((n_uname, root_inode, uname)) =
2169                    st.fids.get(&fid).map(FidRecord::replay_identity)
2170                {
2171                    st.fids.insert(
2172                        newfid,
2173                        FidRecord {
2174                            inode_id: r.qid.path,
2175                            root_inode,
2176                            n_uname,
2177                            uname,
2178                            opened: Some(flags),
2179                        },
2180                    );
2181                }
2182                Ok((r.qid, r.iounit))
2183            }
2184            _ => Err(ClientError::Unexpected("lopenat")),
2185        }
2186    }
2187
2188    /// Open `fid`'s inode on `newfid` like [`Self::lopenat`] and prefetch up to
2189    /// `count` bytes from offset 0 in the same round trip. Returns the qid, the
2190    /// iounit, the prefetched bytes, and whether they reach EOF (the whole file fit in
2191    /// `count`). The inline read is best-effort: a server-side read error yields empty
2192    /// data with `eof = false`, and the open still succeeds.
2193    pub async fn lopenatread(
2194        &self,
2195        fid: u32,
2196        newfid: u32,
2197        flags: u32,
2198        count: u32,
2199    ) -> ClientResult<(Qid, u32, Bytes, bool)> {
2200        let (resp, _transition) = self
2201            .rpc_stateful(Message::Tlopenatread(Tlopenatread {
2202                fid,
2203                newfid,
2204                flags,
2205                count,
2206            }))
2207            .await?;
2208        match resp {
2209            Message::Rlopenatread(r) => {
2210                let mut st = self.state.lock().unwrap();
2211                if let Some((n_uname, root_inode, uname)) =
2212                    st.fids.get(&fid).map(FidRecord::replay_identity)
2213                {
2214                    st.fids.insert(
2215                        newfid,
2216                        FidRecord {
2217                            inode_id: r.qid.path,
2218                            root_inode,
2219                            n_uname,
2220                            uname,
2221                            opened: Some(flags),
2222                        },
2223                    );
2224                }
2225                drop(st);
2226                Ok((r.qid, r.iounit, r.data.0, r.eof != 0))
2227            }
2228            _ => Err(ClientError::Unexpected("lopenatread")),
2229        }
2230    }
2231
2232    pub async fn lcreate(
2233        &self,
2234        fid: u32,
2235        name: &[u8],
2236        flags: u32,
2237        mode: u32,
2238        gid: u32,
2239    ) -> ClientResult<(Qid, u32)> {
2240        let (resp, _transition) = self
2241            .rpc_stateful(Message::Tlcreate(Tlcreate {
2242                fid,
2243                name: P9String::new(name.to_vec()),
2244                flags,
2245                mode,
2246                gid,
2247            }))
2248            .await?;
2249        match resp {
2250            Message::Rlcreate(r) => {
2251                // Replay uses the created inode and strips create-only flags.
2252                let reopen = flags & !(linux::O_CREAT | linux::O_EXCL | linux::O_TRUNC);
2253                let mut st = self.state.lock().unwrap();
2254                if let Some(rec) = st.fids.get_mut(&fid) {
2255                    rec.inode_id = r.qid.path;
2256                    rec.opened = Some(reopen);
2257                }
2258                Ok((r.qid, r.iounit))
2259            }
2260            _ => Err(ClientError::Unexpected("lcreate")),
2261        }
2262    }
2263
2264    /// Create and open `name` under `dfid`, returning the post-op stat in one
2265    /// round trip (`Tlcreateattr`).
2266    /// Unlike [`Self::lcreate`], `dfid` is left untouched (the file opens on
2267    /// `newfid`).
2268    pub async fn lcreateattr(
2269        &self,
2270        dfid: u32,
2271        newfid: u32,
2272        name: &[u8],
2273        flags: u32,
2274        mode: u32,
2275        gid: u32,
2276    ) -> ClientResult<(Stat, u32)> {
2277        let (resp, _transition) = self
2278            .rpc_stateful(Message::Tlcreateattr(Tlcreateattr {
2279                dfid,
2280                newfid,
2281                name: P9String::new(name.to_vec()),
2282                flags,
2283                mode,
2284                gid,
2285            }))
2286            .await?;
2287        match resp {
2288            Message::Rlcreateattr(r) => {
2289                let reopen = flags & !(linux::O_CREAT | linux::O_EXCL | linux::O_TRUNC);
2290                let mut st = self.state.lock().unwrap();
2291                if let Some((n_uname, root_inode, uname)) =
2292                    st.fids.get(&dfid).map(FidRecord::replay_identity)
2293                {
2294                    st.fids.insert(
2295                        newfid,
2296                        FidRecord {
2297                            inode_id: r.stat.qid.path,
2298                            root_inode,
2299                            n_uname,
2300                            uname,
2301                            opened: Some(reopen),
2302                        },
2303                    );
2304                }
2305                Ok((r.stat, r.iounit))
2306            }
2307            _ => Err(ClientError::Unexpected("lcreateattr")),
2308        }
2309    }
2310
2311    /// Read up to `size` bytes at `offset`, looping over multiple Tread requests
2312    /// when `size` exceeds the negotiated msize. Stops early on a short read (EOF).
2313    pub async fn read(&self, fid: u32, offset: u64, size: u32) -> ClientResult<Vec<u8>> {
2314        Ok(self.read_bytes(fid, offset, size).await?.into())
2315    }
2316
2317    /// Like [`Self::read`] but returns the payload as [`Bytes`]. The Rread
2318    /// payload already arrives as `Bytes`, so a single-round-trip read returns
2319    /// it with no copy; only a multi-chunk read concatenates.
2320    pub async fn read_bytes(&self, fid: u32, offset: u64, size: u32) -> ClientResult<Bytes> {
2321        if size == 0 {
2322            return Ok(Bytes::new());
2323        }
2324        let size = size as usize;
2325        let max = self.max_io().max(1) as usize;
2326        let first_count = size.min(max);
2327        let first = self.read_once(fid, offset, first_count as u32).await?;
2328        if size <= max || first.len() < first_count {
2329            return Ok(first);
2330        }
2331        // `size` can be `u32::MAX`; cap the initial allocation at two response chunks.
2332        let mut out = BytesMut::with_capacity(size.min(max.saturating_mul(2)));
2333        let mut off = offset + first.len() as u64;
2334        out.extend_from_slice(&first);
2335        while out.len() < size {
2336            let want = (size - out.len()).min(max);
2337            let data = self.read_once(fid, off, want as u32).await?;
2338            let got = data.len();
2339            out.extend_from_slice(&data);
2340            off += got as u64;
2341            if got < want {
2342                break;
2343            }
2344        }
2345        Ok(out.freeze())
2346    }
2347
2348    async fn read_once(&self, fid: u32, offset: u64, count: u32) -> ClientResult<Bytes> {
2349        let resp = self
2350            .rpc(Message::Tread(Tread { fid, offset, count }))
2351            .await?;
2352        match resp {
2353            Message::Rread(r) if r.data.len() <= count as usize => Ok(r.data.0),
2354            Message::Rread(_) => Err(ClientError::Unexpected("read count")),
2355            _ => Err(ClientError::Unexpected("read")),
2356        }
2357    }
2358
2359    /// Write all of `data` at `offset`, splitting into multiple Twrite requests
2360    /// when it exceeds the negotiated msize. Returns the total bytes written.
2361    pub async fn write(&self, fid: u32, offset: u64, data: &[u8]) -> ClientResult<u64> {
2362        let len = data.len();
2363        self.write_chunks(fid, offset, len, move |start, end| {
2364            Bytes::copy_from_slice(&data[start..end])
2365        })
2366        .await
2367    }
2368
2369    /// Like [`Self::write`], but takes ownership of an existing [`Bytes`] buffer.
2370    /// Chunk selection uses shallow `Bytes` slices, adding no staging copy
2371    /// before each request is serialized.
2372    pub async fn write_bytes(&self, fid: u32, offset: u64, data: Bytes) -> ClientResult<u64> {
2373        let len = data.len();
2374        self.write_chunks(fid, offset, len, move |start, end| data.slice(start..end))
2375            .await
2376    }
2377
2378    async fn write_chunks<F>(
2379        &self,
2380        fid: u32,
2381        offset: u64,
2382        len: usize,
2383        mut chunk: F,
2384    ) -> ClientResult<u64>
2385    where
2386        F: FnMut(usize, usize) -> Bytes,
2387    {
2388        let mut written = 0usize;
2389        let max = self.max_write_payload().max(1) as usize;
2390        while written < len {
2391            let end = written + (len - written).min(max);
2392            let data = chunk(written, end);
2393            debug_assert_eq!(data.len(), end - written);
2394            let attempted = data.len();
2395            let n = self.write_chunk(fid, offset + written as u64, data).await? as usize;
2396            written += n;
2397            if n < attempted {
2398                break;
2399            }
2400        }
2401        Ok(written as u64)
2402    }
2403
2404    async fn write_chunk(&self, fid: u32, offset: u64, data: Bytes) -> ClientResult<u32> {
2405        let attempted = u32::try_from(data.len())
2406            .expect("write chunks are bounded by the negotiated u32 msize");
2407        let resp = self
2408            .rpc(Message::Twrite(Twrite {
2409                fid,
2410                offset,
2411                count: attempted,
2412                data: P9Bytes::from(data),
2413            }))
2414            .await?;
2415        match resp {
2416            Message::Rwrite(r) if r.count <= attempted => Ok(r.count),
2417            Message::Rwrite(_) => Err(ClientError::Unexpected("write count")),
2418            _ => Err(ClientError::Unexpected("write")),
2419        }
2420    }
2421
2422    pub async fn readdir(&self, fid: u32, offset: u64, count: u32) -> ClientResult<Vec<DirEntry>> {
2423        let resp = self
2424            .rpc(Message::Treaddir(Treaddir { fid, offset, count }))
2425            .await?;
2426        match resp {
2427            Message::Rreaddir(r) => r.to_entries().map_err(ClientError::Codec),
2428            _ => Err(ClientError::Unexpected("readdir")),
2429        }
2430    }
2431
2432    /// Like [`Self::readdir`] but each entry carries its full stat
2433    /// (`Treaddirattr`).
2434    pub async fn readdirplus(
2435        &self,
2436        fid: u32,
2437        offset: u64,
2438        count: u32,
2439    ) -> ClientResult<Vec<DirEntryPlus>> {
2440        let resp = self
2441            .rpc(Message::Treaddirattr(Treaddirattr { fid, offset, count }))
2442            .await?;
2443        match resp {
2444            Message::Rreaddirattr(r) => r.to_entries().map_err(ClientError::Codec),
2445            _ => Err(ClientError::Unexpected("readdirplus")),
2446        }
2447    }
2448
2449    pub async fn mkdir(&self, dfid: u32, name: &[u8], mode: u32, gid: u32) -> ClientResult<Qid> {
2450        let resp = self
2451            .rpc(Message::Tmkdir(Tmkdir {
2452                dfid,
2453                name: P9String::new(name.to_vec()),
2454                mode,
2455                gid,
2456            }))
2457            .await?;
2458        match resp {
2459            Message::Rmkdir(r) => Ok(r.qid),
2460            _ => Err(ClientError::Unexpected("mkdir")),
2461        }
2462    }
2463
2464    /// Like [`Self::mkdir`] but the reply carries the new directory's full stat.
2465    pub async fn mkdir_attr(
2466        &self,
2467        dfid: u32,
2468        name: &[u8],
2469        mode: u32,
2470        gid: u32,
2471    ) -> ClientResult<Stat> {
2472        let resp = self
2473            .rpc(Message::Tmkdirattr(Tmkdir {
2474                dfid,
2475                name: P9String::new(name.to_vec()),
2476                mode,
2477                gid,
2478            }))
2479            .await?;
2480        match resp {
2481            Message::Rmkdirattr(r) => Ok(r.stat),
2482            _ => Err(ClientError::Unexpected("mkdir_attr")),
2483        }
2484    }
2485
2486    pub async fn symlink(
2487        &self,
2488        dfid: u32,
2489        name: &[u8],
2490        target: &[u8],
2491        gid: u32,
2492    ) -> ClientResult<Qid> {
2493        let resp = self
2494            .rpc(Message::Tsymlink(Tsymlink {
2495                dfid,
2496                name: P9String::new(name.to_vec()),
2497                symtgt: P9String::new(target.to_vec()),
2498                gid,
2499            }))
2500            .await?;
2501        match resp {
2502            Message::Rsymlink(r) => Ok(r.qid),
2503            _ => Err(ClientError::Unexpected("symlink")),
2504        }
2505    }
2506
2507    /// Like [`Self::symlink`] but the reply carries the new link's full stat.
2508    pub async fn symlink_attr(
2509        &self,
2510        dfid: u32,
2511        name: &[u8],
2512        target: &[u8],
2513        gid: u32,
2514    ) -> ClientResult<Stat> {
2515        let resp = self
2516            .rpc(Message::Tsymlinkattr(Tsymlink {
2517                dfid,
2518                name: P9String::new(name.to_vec()),
2519                symtgt: P9String::new(target.to_vec()),
2520                gid,
2521            }))
2522            .await?;
2523        match resp {
2524            Message::Rsymlinkattr(r) => Ok(r.stat),
2525            _ => Err(ClientError::Unexpected("symlink_attr")),
2526        }
2527    }
2528
2529    pub async fn mknod(
2530        &self,
2531        dfid: u32,
2532        name: &[u8],
2533        mode: u32,
2534        major: u32,
2535        minor: u32,
2536        gid: u32,
2537    ) -> ClientResult<Qid> {
2538        let resp = self
2539            .rpc(Message::Tmknod(Tmknod {
2540                dfid,
2541                name: P9String::new(name.to_vec()),
2542                mode,
2543                major,
2544                minor,
2545                gid,
2546            }))
2547            .await?;
2548        match resp {
2549            Message::Rmknod(r) => Ok(r.qid),
2550            _ => Err(ClientError::Unexpected("mknod")),
2551        }
2552    }
2553
2554    /// Like [`Self::mknod`] but the reply carries the new node's full stat.
2555    pub async fn mknod_attr(
2556        &self,
2557        dfid: u32,
2558        name: &[u8],
2559        mode: u32,
2560        major: u32,
2561        minor: u32,
2562        gid: u32,
2563    ) -> ClientResult<Stat> {
2564        let resp = self
2565            .rpc(Message::Tmknodattr(Tmknod {
2566                dfid,
2567                name: P9String::new(name.to_vec()),
2568                mode,
2569                major,
2570                minor,
2571                gid,
2572            }))
2573            .await?;
2574        match resp {
2575            Message::Rmknodattr(r) => Ok(r.stat),
2576            _ => Err(ClientError::Unexpected("mknod_attr")),
2577        }
2578    }
2579
2580    pub async fn readlink(&self, fid: u32) -> ClientResult<Vec<u8>> {
2581        match self.rpc(Message::Treadlink(Treadlink { fid })).await? {
2582            Message::Rreadlink(r) => Ok(r.target.data.to_vec()),
2583            _ => Err(ClientError::Unexpected("readlink")),
2584        }
2585    }
2586
2587    pub async fn link(&self, dfid: u32, fid: u32, name: &[u8]) -> ClientResult<()> {
2588        let resp = self
2589            .rpc(Message::Tlink(Tlink {
2590                dfid,
2591                fid,
2592                name: P9String::new(name.to_vec()),
2593            }))
2594            .await?;
2595        match resp {
2596            Message::Rlink(_) => Ok(()),
2597            _ => Err(ClientError::Unexpected("link")),
2598        }
2599    }
2600
2601    /// Like [`Self::link`] but the reply carries the linked inode's post-op stat
2602    /// (updated nlink).
2603    pub async fn link_attr(&self, dfid: u32, fid: u32, name: &[u8]) -> ClientResult<Stat> {
2604        let resp = self
2605            .rpc(Message::Tlinkattr(Tlink {
2606                dfid,
2607                fid,
2608                name: P9String::new(name.to_vec()),
2609            }))
2610            .await?;
2611        match resp {
2612            Message::Rlinkattr(r) => Ok(r.stat),
2613            _ => Err(ClientError::Unexpected("link_attr")),
2614        }
2615    }
2616
2617    pub async fn renameat(
2618        &self,
2619        olddirfid: u32,
2620        oldname: &[u8],
2621        newdirfid: u32,
2622        newname: &[u8],
2623    ) -> ClientResult<()> {
2624        let resp = self
2625            .rpc(Message::Trenameat(Trenameat {
2626                olddirfid,
2627                oldname: P9String::new(oldname.to_vec()),
2628                newdirfid,
2629                newname: P9String::new(newname.to_vec()),
2630            }))
2631            .await?;
2632        match resp {
2633            Message::Rrenameat(_) => Ok(()),
2634            _ => Err(ClientError::Unexpected("renameat")),
2635        }
2636    }
2637
2638    pub async fn unlinkat(&self, dirfid: u32, name: &[u8], flags: u32) -> ClientResult<()> {
2639        let resp = self
2640            .rpc(Message::Tunlinkat(Tunlinkat {
2641                dirfid,
2642                name: P9String::new(name.to_vec()),
2643                flags,
2644            }))
2645            .await?;
2646        match resp {
2647            Message::Runlinkat(_) => Ok(()),
2648            _ => Err(ClientError::Unexpected("unlinkat")),
2649        }
2650    }
2651
2652    async fn fsync_scope(&self, primary: u32, scope: FsyncScope) -> ClientResult<()> {
2653        self.validate_fids([primary])?;
2654        // Generations prevent the result from clearing writes concurrent with fsync.
2655        let mut token: Option<u64> = None;
2656        let mut snaps: Vec<(u64, u64)> = Vec::new();
2657        match scope {
2658            FsyncScope::Inode(inode) => {
2659                let (oldest, generation) = self.snapshot_unsynced(inode);
2660                token = oldest;
2661                snaps.push((inode, generation));
2662            }
2663            FsyncScope::All => {
2664                for entry in &self.unsynced {
2665                    let inode = *entry.key();
2666                    let (oldest, generation) = entry.snapshot();
2667                    if let Some(t) = oldest {
2668                        token = Some(token.map_or(t, |current| current.min(t)));
2669                    }
2670                    snaps.push((inode, generation));
2671                }
2672            }
2673        }
2674        match self
2675            .rpc(Message::Tfsyncdur(Tfsyncdur {
2676                fid: primary,
2677                datasync: scope.wire_flag(),
2678                token: token.unwrap_or(0),
2679            }))
2680            .await
2681        {
2682            Ok(Message::Rfsync(_)) => {
2683                for (inode, generation) in snaps {
2684                    self.clear_unsynced_if_unchanged(inode, generation);
2685                }
2686                Ok(())
2687            }
2688            Ok(_) => Err(ClientError::Unexpected("fsync")),
2689            Err(ClientError::Errno(e)) if e == linux::ESTALE => {
2690                // Preserve stale obligations until replacement writes arrive.
2691                for (inode, generation) in snaps {
2692                    self.report_unsynced_if_unchanged(inode, generation);
2693                }
2694                Err(ClientError::Errno(e))
2695            }
2696            Err(e) => Err(e),
2697        }
2698    }
2699
2700    /// Verified fsync for the inode named by `fid`, across every handle.
2701    pub async fn fsync(&self, fid: u32) -> ClientResult<()> {
2702        let inode = self.fid_inode(fid)?;
2703        self.fsync_scope(fid, FsyncScope::Inode(inode)).await
2704    }
2705
2706    /// Filesystem-wide barrier for this client's outstanding durability obligations.
2707    pub async fn fsync_all(&self, primary: u32) -> ClientResult<()> {
2708        self.fsync_scope(primary, FsyncScope::All).await
2709    }
2710
2711    pub async fn statfs(&self, fid: u32) -> ClientResult<Rstatfs> {
2712        match self.rpc(Message::Tstatfs(Tstatfs { fid })).await? {
2713            Message::Rstatfs(r) => Ok(r),
2714            _ => Err(ClientError::Unexpected("statfs")),
2715        }
2716    }
2717
2718    /// Acquire or release a POSIX record lock. Returns the lock status; note
2719    /// that a non-blocking conflict surfaces as `Err(ClientError::Errno(EAGAIN))`
2720    /// (the server replies `Rlerror`), whereas a blocking request that cannot be
2721    /// granted returns `Ok(LockStatus::Blocked)`.
2722    #[allow(clippy::too_many_arguments)]
2723    pub async fn lock(
2724        &self,
2725        fid: u32,
2726        lock_type: LockType,
2727        flags: u32,
2728        start: u64,
2729        length: u64,
2730        proc_id: u32,
2731        client_id: &[u8],
2732    ) -> ClientResult<LockStatus> {
2733        let (resp, _transition) = self
2734            .rpc_stateful(Message::Tlock(Tlock {
2735                fid,
2736                lock_type,
2737                flags,
2738                start,
2739                length,
2740                proc_id,
2741                client_id: P9String::new(client_id.to_vec()),
2742            }))
2743            .await?;
2744        match resp {
2745            Message::Rlock(r) => {
2746                let status = LockStatus::from_wire(r.status)
2747                    .ok_or(ClientError::Unexpected("lock status"))?;
2748                let mut st = self.state.lock().unwrap();
2749                match lock_type {
2750                    LockType::Unlock if status == LockStatus::Success => {
2751                        unlock_recorded_range(&mut st.locks, fid, start, length);
2752                    }
2753                    _ if status == LockStatus::Success => {
2754                        replace_recorded_lock(
2755                            &mut st.locks,
2756                            LockRecord {
2757                                fid,
2758                                lock_type,
2759                                start,
2760                                length,
2761                                proc_id,
2762                                client_id: client_id.to_vec(),
2763                            },
2764                        );
2765                    }
2766                    _ => {}
2767                }
2768                Ok(status)
2769            }
2770            _ => Err(ClientError::Unexpected("lock")),
2771        }
2772    }
2773
2774    /// Test for a conflicting POSIX record lock.
2775    pub async fn getlock(
2776        &self,
2777        fid: u32,
2778        lock_type: LockType,
2779        start: u64,
2780        length: u64,
2781        proc_id: u32,
2782        client_id: &[u8],
2783    ) -> ClientResult<Rgetlock> {
2784        let resp = self
2785            .rpc(Message::Tgetlock(Tgetlock {
2786                fid,
2787                lock_type,
2788                start,
2789                length,
2790                proc_id,
2791                client_id: P9String::new(client_id.to_vec()),
2792            }))
2793            .await?;
2794        match resp {
2795            Message::Rgetlock(r) => Ok(r),
2796            _ => Err(ClientError::Unexpected("getlock")),
2797        }
2798    }
2799}
2800
2801impl Drop for NinePClient {
2802    fn drop(&mut self) {
2803        // Reader and writer tasks retain `Arc<Conn>` until explicit shutdown.
2804        self.conn.load().shutdown();
2805        self.reconnect_notify.notify_waiters();
2806    }
2807}
2808
2809/// Applies an unlock to a recorded lock. Zero length extends to EOF.
2810fn subtract_lock_record(
2811    held: LockRecord,
2812    unlock_start: u64,
2813    unlock_length: u64,
2814) -> Vec<LockRecord> {
2815    ninep_proto::subtract_lock_range(held.start, held.length, unlock_start, unlock_length)
2816        .into_iter()
2817        .map(|(start, length)| LockRecord {
2818            start,
2819            length,
2820            ..held.clone()
2821        })
2822        .collect()
2823}
2824
2825fn unlock_recorded_range(locks: &mut Vec<LockRecord>, fid: u32, start: u64, length: u64) {
2826    let prior = std::mem::take(locks);
2827    for held in prior {
2828        if held.fid == fid {
2829            locks.extend(subtract_lock_record(held, start, length));
2830        } else {
2831            locks.push(held);
2832        }
2833    }
2834}
2835
2836/// Applies POSIX replacement semantics to this fid's recorded locks.
2837fn replace_recorded_lock(locks: &mut Vec<LockRecord>, replacement: LockRecord) {
2838    unlock_recorded_range(
2839        locks,
2840        replacement.fid,
2841        replacement.start,
2842        replacement.length,
2843    );
2844    locks.push(replacement);
2845}
2846
2847enum DialedTransport {
2848    #[cfg(not(target_arch = "wasm32"))]
2849    Native {
2850        read: Box<dyn AsyncRead + Unpin + Send>,
2851        write: Box<dyn AsyncWrite + Unpin + Send>,
2852    },
2853    #[cfg(target_arch = "wasm32")]
2854    WebSocket(web_transport::WebSocketIo),
2855}
2856
2857/// Open a connection to the target. Native sockets are byte streams and are
2858/// framed by the reader; browser WebSockets already carry complete frames.
2859async fn dial(target: &Target) -> ClientResult<DialedTransport> {
2860    match target {
2861        #[cfg(not(target_arch = "wasm32"))]
2862        Target::Tcp(addr) => {
2863            let stream = runtime::timeout(PROBE_TIMEOUT, TcpStream::connect(addr))
2864                .await
2865                .map_err(|_| ClientError::Disconnected)?
2866                .map_err(|_| ClientError::Disconnected)?;
2867            configure_tcp(stream)
2868        }
2869        #[cfg(not(target_arch = "wasm32"))]
2870        Target::TcpHost(endpoint) => {
2871            // Resolution is per probe and isolated to this target.
2872            let stream = runtime::timeout(PROBE_TIMEOUT, TcpStream::connect(endpoint.as_str()))
2873                .await
2874                .map_err(|_| ClientError::Disconnected)?
2875                .map_err(|_| ClientError::Disconnected)?;
2876            configure_tcp(stream)
2877        }
2878        #[cfg(not(target_arch = "wasm32"))]
2879        Target::Unix(path) => {
2880            let stream = runtime::timeout(PROBE_TIMEOUT, UnixStream::connect(path))
2881                .await
2882                .map_err(|_| ClientError::Disconnected)?
2883                .map_err(|_| ClientError::Disconnected)?;
2884            let (r, w) = stream.into_split();
2885            Ok(DialedTransport::Native {
2886                read: Box::new(r),
2887                write: Box::new(w),
2888            })
2889        }
2890        #[cfg(target_arch = "wasm32")]
2891        Target::WebSocket(url) => web_transport::connect(url)
2892            .await
2893            .map(DialedTransport::WebSocket),
2894    }
2895}
2896
2897#[cfg(not(target_arch = "wasm32"))]
2898fn configure_tcp(stream: TcpStream) -> ClientResult<DialedTransport> {
2899    stream.set_nodelay(true).ok();
2900    let keepalive = socket2::TcpKeepalive::new()
2901        .with_time(Duration::from_secs(45))
2902        .with_interval(Duration::from_secs(15))
2903        .with_retries(4);
2904    let _ = socket2::SockRef::from(&stream).set_tcp_keepalive(&keepalive);
2905    let (r, w) = stream.into_split();
2906    Ok(DialedTransport::Native {
2907        read: Box::new(r),
2908        write: Box::new(w),
2909    })
2910}
2911
2912#[cfg(all(test, not(target_arch = "wasm32")))]
2913mod target_dial_tests {
2914    use super::*;
2915    use tokio::io::AsyncReadExt;
2916    use tokio::net::TcpListener;
2917
2918    async fn recv_message(stream: &mut TcpStream) -> P9Message {
2919        let mut size = [0u8; P9_SIZE_FIELD_LEN];
2920        stream.read_exact(&mut size).await.unwrap();
2921        let frame_len = u32::from_le_bytes(size) as usize;
2922        let mut frame = Vec::with_capacity(frame_len);
2923        frame.extend_from_slice(&size);
2924        frame.resize(frame_len, 0);
2925        stream
2926            .read_exact(&mut frame[P9_SIZE_FIELD_LEN..])
2927            .await
2928            .unwrap();
2929        P9Message::from_bytes_ctx(&frame, false).unwrap()
2930    }
2931
2932    async fn send_message(stream: &mut TcpStream, tag: u16, body: Message) {
2933        stream
2934            .write_all(&P9Message::new(tag, body).to_bytes().unwrap())
2935            .await
2936            .unwrap();
2937    }
2938
2939    #[tokio::test]
2940    async fn hostname_lookup_failure_is_isolated_from_healthy_target_dials() {
2941        let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
2942        let healthy_addr = listener.local_addr().unwrap();
2943        let accept = tokio::spawn(async move { listener.accept().await.unwrap() });
2944
2945        let targets = [
2946            Target::TcpHost("not a valid hostname:5564".to_string()),
2947            Target::Tcp(healthy_addr),
2948        ];
2949        let mut dials = FuturesUnordered::new();
2950        for target in targets {
2951            dials.push(async move { dial(&target).await });
2952        }
2953
2954        let mut successes = 0;
2955        let mut failures = 0;
2956        while let Some(result) = dials.next().await {
2957            match result {
2958                Ok(transport) => {
2959                    successes += 1;
2960                    drop(transport);
2961                }
2962                Err(_) => failures += 1,
2963            }
2964        }
2965        assert_eq!(successes, 1);
2966        assert_eq!(failures, 1);
2967        accept.await.unwrap();
2968    }
2969
2970    #[tokio::test]
2971    async fn reconnect_rejects_a_smaller_negotiated_msize() {
2972        let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
2973        let addr = listener.local_addr().unwrap();
2974        let server = tokio::spawn(async move {
2975            let (mut stream, _) = listener.accept().await.unwrap();
2976            let version = recv_message(&mut stream).await;
2977            assert!(matches!(version.body, Message::Tversion(_)));
2978            send_message(
2979                &mut stream,
2980                version.tag,
2981                Message::Rversion(Rversion {
2982                    msize: 4096,
2983                    version: P9String::new(VERSION_9P2000L_ZEROFS.to_vec()),
2984                }),
2985            )
2986            .await;
2987
2988            let lineage = recv_message(&mut stream).await;
2989            assert!(matches!(lineage.body, Message::Tgetlineage(_)));
2990            send_message(
2991                &mut stream,
2992                lineage.tag,
2993                Message::Rgetlineage(Rgetlineage {
2994                    token: 1,
2995                    writer_epoch: 1,
2996                }),
2997            )
2998            .await;
2999        });
3000
3001        let result = NinePClient::connect_once(
3002            &Target::Tcp(addr),
3003            8192,
3004            Some(8192),
3005            Arc::new(Notify::new()),
3006            Arc::new(TrafficCounters::default()),
3007            Arc::new(AtomicBool::new(false)),
3008        )
3009        .await;
3010        assert!(matches!(result, Err(ClientError::Unexpected("version"))));
3011        server.await.unwrap();
3012    }
3013}
3014
3015/// Negotiates the required private dialect and verifies serving authority.
3016async fn negotiate_on(conn: &Conn, requested: u32) -> ClientResult<u32> {
3017    let negotiated = version_on(conn, requested).await?;
3018    query_lineage_token(conn).await?;
3019    debug!("ZeroFS 9P dialect negotiated, msize={negotiated}");
3020    Ok(negotiated)
3021}
3022
3023/// Performs the session-resetting `Tversion` exchange without a lineage query.
3024async fn version_on(conn: &Conn, requested: u32) -> ClientResult<u32> {
3025    // 9P requires NOTAG for Tversion.
3026    match NinePClient::send_raw_at_tag(
3027        conn,
3028        Some(NOTAG),
3029        Message::Tversion(Tversion {
3030            msize: requested,
3031            version: P9String::new(VERSION_9P2000L_ZEROFS.to_vec()),
3032        }),
3033        false,
3034    )
3035    .await?
3036    {
3037        Message::Rlerror(e) => Err(ClientError::Errno(e.ecode)),
3038        Message::Rversion(rv) => {
3039            if rv.version.data != VERSION_9P2000L_ZEROFS {
3040                warn!(
3041                    "server did not accept the required ZeroFS dialect: {:?}",
3042                    rv.version.as_str().unwrap_or("<non-UTF-8>")
3043                );
3044                return Err(ClientError::Unexpected("version"));
3045            }
3046            // Linux v9fs requires msize >= 4096.
3047            let negotiated = rv.msize.min(requested);
3048            if negotiated < 4096 {
3049                warn!("server negotiated msize {negotiated} below minimum 4096");
3050                return Err(ClientError::Unexpected("version"));
3051            }
3052            Ok(negotiated)
3053        }
3054        _ => Err(ClientError::Unexpected("version")),
3055    }
3056}
3057
3058/// Records the lease-gated durability lineage and writer epoch.
3059async fn query_lineage_token(conn: &Conn) -> ClientResult<()> {
3060    match NinePClient::send_raw(conn, Message::Tgetlineage(Tgetlineage)).await? {
3061        Message::Rgetlineage(r) => {
3062            conn.lineage_token.store(r.token, Ordering::Relaxed);
3063            conn.writer_epoch.store(r.writer_epoch, Ordering::Relaxed);
3064            Ok(())
3065        }
3066        Message::Rlerror(e) => Err(ClientError::Errno(e.ecode)),
3067        _ => Err(ClientError::Unexpected("getlineage")),
3068    }
3069}
3070
3071#[cfg(not(target_arch = "wasm32"))]
3072fn spawn_writer(
3073    write: Box<dyn AsyncWrite + Unpin + Send>,
3074    mut rx: mpsc::Receiver<Vec<u8>>,
3075    conn: Arc<Conn>,
3076    reconnect: Arc<Notify>,
3077) {
3078    runtime::spawn(async move {
3079        let mut writer = tokio::io::BufWriter::with_capacity(64 * 1024, write);
3080        loop {
3081            tokio::select! {
3082                biased;
3083                // Reader shutdown covers an idle writer.
3084                _ = conn.writer_shutdown.notified() => break,
3085                maybe = rx.recv() => {
3086                    let Some(frame) = maybe else { break };
3087                    conn.counters.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed);
3088                    conn.counters.operations.fetch_add(1, Ordering::Relaxed);
3089                    if writer.write_all(&frame).await.is_err() {
3090                        conn.shutdown();
3091                        break;
3092                    }
3093                    let mut failed = false;
3094                    while let Ok(more) = rx.try_recv() {
3095                        conn.counters.bytes_sent.fetch_add(more.len() as u64, Ordering::Relaxed);
3096                        conn.counters.operations.fetch_add(1, Ordering::Relaxed);
3097                        if writer.write_all(&more).await.is_err() {
3098                            failed = true;
3099                            break;
3100                        }
3101                    }
3102                    if failed {
3103                        conn.shutdown();
3104                        break;
3105                    }
3106                    if writer.flush().await.is_err() {
3107                        conn.shutdown();
3108                        break;
3109                    }
3110                }
3111            }
3112        }
3113        conn.shutdown();
3114        reconnect.notify_waiters();
3115    });
3116}
3117
3118#[cfg(not(target_arch = "wasm32"))]
3119fn spawn_reader(read: Box<dyn AsyncRead + Unpin + Send>, conn: Arc<Conn>, reconnect: Arc<Notify>) {
3120    runtime::spawn(async move {
3121        let mut framed = LengthDelimitedCodec::builder()
3122            .little_endian()
3123            .length_field_offset(0)
3124            .length_field_length(P9_SIZE_FIELD_LEN)
3125            .length_adjustment(0)
3126            .num_skip(0)
3127            .max_frame_length(P9_MAX_MSIZE as usize)
3128            .new_read(read);
3129
3130        loop {
3131            let next = tokio::select! {
3132                biased;
3133                // `shutdown` may discard a healthy connection.
3134                _ = conn.reader_shutdown.notified() => break,
3135                next = framed.next() => next,
3136            };
3137            let frame = match next {
3138                Some(Ok(buf)) => buf.freeze(),
3139                Some(Err(e)) => {
3140                    warn!("9P client read failed: {e}");
3141                    break;
3142                }
3143                None => break,
3144            };
3145            conn.deliver(frame);
3146        }
3147
3148        conn.connection_lost(&reconnect);
3149    });
3150}
3151
3152#[cfg(test)]
3153mod lock_range_tests {
3154    use super::*;
3155
3156    fn held(start: u64, length: u64) -> LockRecord {
3157        LockRecord {
3158            fid: 9,
3159            lock_type: LockType::WriteLock,
3160            start,
3161            length,
3162            proc_id: 42,
3163            client_id: b"client".to_vec(),
3164        }
3165    }
3166
3167    #[test]
3168    fn lock_record_subtraction_preserves_metadata() {
3169        let survivors = subtract_lock_record(held(10, 90), 30, 20);
3170        assert_eq!(
3171            survivors
3172                .iter()
3173                .map(|record| (record.start, record.length))
3174                .collect::<Vec<_>>(),
3175            vec![(10, 20), (50, 50)]
3176        );
3177        assert!(survivors.iter().all(|record| {
3178            record.fid == 9
3179                && matches!(record.lock_type, LockType::WriteLock)
3180                && record.proc_id == 42
3181                && record.client_id == b"client"
3182        }));
3183    }
3184
3185    #[test]
3186    fn subrange_relock_preserves_tails_for_replay() {
3187        let mut locks = vec![held(0, 100)];
3188        replace_recorded_lock(
3189            &mut locks,
3190            LockRecord {
3191                lock_type: LockType::ReadLock,
3192                start: 20,
3193                length: 30,
3194                ..held(0, 0)
3195            },
3196        );
3197        locks.sort_by_key(|lock| lock.start);
3198        let shape: Vec<_> = locks
3199            .iter()
3200            .map(|lock| {
3201                (
3202                    lock.start,
3203                    lock.length,
3204                    match lock.lock_type {
3205                        LockType::ReadLock => "read",
3206                        LockType::WriteLock => "write",
3207                        LockType::Unlock => "unlock",
3208                    },
3209                )
3210            })
3211            .collect();
3212        assert_eq!(
3213            shape,
3214            vec![(0, 20, "write"), (20, 30, "read"), (50, 50, "write")]
3215        );
3216    }
3217}
3218
3219#[cfg(test)]
3220mod durability_tracking_tests {
3221    use super::{FsyncScope, P9_FSYNC_INODE, Unsynced};
3222
3223    #[test]
3224    fn fsync_scope_owns_the_wire_scope_bit() {
3225        assert_eq!(FsyncScope::All.wire_flag(), 0);
3226        assert_eq!(FsyncScope::Inode(7).wire_flag(), P9_FSYNC_INODE);
3227    }
3228
3229    #[test]
3230    fn fsync_clears_the_obligation_when_quiescent() {
3231        let mut u = Unsynced::default();
3232        u.note(7);
3233        let (oldest, generation) = u.snapshot();
3234        assert_eq!(oldest, Some(7));
3235        u.clear_if_unchanged(generation);
3236        assert_eq!(u.snapshot().0, None);
3237    }
3238
3239    #[test]
3240    fn a_repeated_fsync_on_one_inode_keeps_failing_until_a_redo() {
3241        let mut u = Unsynced::default();
3242        u.note(1);
3243        let (oldest, generation) = u.snapshot();
3244        assert_eq!(oldest, Some(1));
3245        u.report_if_unchanged(generation);
3246        assert_eq!(
3247            u.snapshot().0,
3248            Some(1),
3249            "a reported loss must persist so a repeated fsync still fails"
3250        );
3251        u.note(2);
3252        assert_eq!(
3253            u.snapshot().0,
3254            Some(2),
3255            "a redo after a report advances the obligation (no livelock)"
3256        );
3257        let (_, gen2) = u.snapshot();
3258        u.clear_if_unchanged(gen2);
3259        assert_eq!(u.snapshot().0, None);
3260    }
3261
3262    #[test]
3263    fn an_estale_and_redo_on_one_inode_does_not_discharge_a_sibling_inode() {
3264        use std::collections::HashMap;
3265        let mut map: HashMap<u32, Unsynced> = HashMap::new();
3266        map.entry(10).or_default().note(1);
3267        map.entry(20).or_default().note(1);
3268        let (_, g) = map.get(&10).unwrap().snapshot();
3269        map.get_mut(&10).unwrap().report_if_unchanged(g);
3270        map.get_mut(&10).unwrap().note(2);
3271        let (_, g2) = map.get(&10).unwrap().snapshot();
3272        map.get_mut(&10).unwrap().clear_if_unchanged(g2);
3273        if map.get(&10).unwrap().snapshot().0.is_none() {
3274            map.remove(&10);
3275        }
3276        assert_eq!(
3277            map.get(&20).unwrap().snapshot().0,
3278            Some(1),
3279            "inode 10's ESTALE+redo+success cycle must not touch inode 20"
3280        );
3281    }
3282
3283    #[test]
3284    fn fsync_does_not_erase_a_write_from_its_window() {
3285        let mut u = Unsynced::default();
3286        u.note(7);
3287        let (_, generation) = u.snapshot();
3288        u.note(7);
3289        u.clear_if_unchanged(generation);
3290        assert_eq!(
3291            u.snapshot().0,
3292            Some(7),
3293            "a write racing the fsync must stay tracked"
3294        );
3295    }
3296
3297    #[test]
3298    fn oldest_token_is_kept_across_a_lineage_change() {
3299        let mut u = Unsynced::default();
3300        u.note(9);
3301        u.note(5);
3302        assert_eq!(u.snapshot().0, Some(5), "the oldest (riskiest) token wins");
3303    }
3304
3305    #[test]
3306    fn nothing_tracked_reports_none() {
3307        let u = Unsynced::default();
3308        assert_eq!(u.snapshot(), (None, 0));
3309    }
3310}
3311
3312#[cfg(test)]
3313fn test_conn_with_receiver() -> (Arc<Conn>, mpsc::Receiver<Vec<u8>>) {
3314    let (writer_tx, rx) = mpsc::channel(1);
3315    let conn = Arc::new(Conn {
3316        writer_tx,
3317        pending: DashMap::new(),
3318        tag_ctr: AtomicU16::new(0),
3319        lineage_token: AtomicU64::new(0),
3320        writer_epoch: AtomicU64::new(0),
3321        dead: AtomicBool::new(false),
3322        receive_generation: AtomicU64::new(0),
3323        liveness_probe_tag: AtomicU32::new(NO_LIVENESS_PROBE_TAG),
3324        probe_lock: tokio::sync::Mutex::new(()),
3325        writer_shutdown: Notify::new(),
3326        reader_shutdown: Notify::new(),
3327        counters: Arc::new(TrafficCounters::default()),
3328    });
3329    (conn, rx)
3330}
3331
3332#[cfg(test)]
3333mod session_transition_tests {
3334    use super::*;
3335
3336    type TestRequests = mpsc::Receiver<Vec<u8>>;
3337    const REPLAY: u8 = P9_REBIND_REPLAY;
3338    const OPEN_REPLAY: u8 = REPLAY | P9_REBIND_OPENED;
3339
3340    fn test_conn() -> Arc<Conn> {
3341        test_conn_with_receiver().0
3342    }
3343
3344    fn test_client(conn: Arc<Conn>) -> Arc<NinePClient> {
3345        test_client_with_limit(conn, MAX_INFLIGHT_REQUESTS)
3346    }
3347
3348    fn test_client_with_limit(conn: Arc<Conn>, max_inflight_requests: usize) -> Arc<NinePClient> {
3349        Arc::new(NinePClient {
3350            targets: Vec::new(),
3351            conn: ArcSwap::new(conn),
3352            live: AtomicBool::new(true),
3353            terminal_errno: AtomicU32::new(0),
3354            live_notify: Notify::new(),
3355            reconnect_notify: Arc::new(Notify::new()),
3356            msize: 8192,
3357            msize_mismatch_warned: Arc::new(AtomicBool::new(false)),
3358            fid_ctr: AtomicU32::new(1),
3359            fid_free: Mutex::new(Vec::new()),
3360            state: Mutex::new(SessionState::default()),
3361            stale_fids: DashSet::new(),
3362            session_transition: tokio::sync::Mutex::new(()),
3363            request_admission: Arc::new(Semaphore::new(max_inflight_requests)),
3364            unsynced: DashMap::new(),
3365            counters: Arc::new(TrafficCounters::default()),
3366        })
3367    }
3368
3369    fn track_test_fid(client: &NinePClient, fid: u32) {
3370        client
3371            .state
3372            .lock()
3373            .unwrap()
3374            .fids
3375            .insert(fid, inode_fid(u64::from(fid), 0, 0, None));
3376    }
3377
3378    #[test]
3379    fn mutation_obligations_are_derived_from_stable_inode_ids() {
3380        let client = test_client(test_conn());
3381        {
3382            let mut state = client.state.lock().unwrap();
3383            state.fids.insert(7, inode_fid(70, 0, 0, None));
3384            state.fids.insert(8, inode_fid(80, 0, 0, None));
3385        }
3386        let rename = Message::Trenameat(Trenameat {
3387            olddirfid: 7,
3388            oldname: P9String::new(b"old".to_vec()),
3389            newdirfid: 8,
3390            newname: P9String::new(b"new".to_vec()),
3391        });
3392        assert_eq!(client.mutation_inodes(&rename).unwrap(), vec![80, 70]);
3393
3394        client
3395            .state
3396            .lock()
3397            .unwrap()
3398            .fids
3399            .insert(8, inode_fid(70, 0, 0, None));
3400        assert_eq!(
3401            client.mutation_inodes(&rename).unwrap(),
3402            vec![70],
3403            "aliases of one inode must create one durability obligation"
3404        );
3405    }
3406
3407    #[test]
3408    fn recycling_a_fid_does_not_discard_its_inode_obligation() {
3409        let client = test_client(test_conn());
3410        client.note_unsynced(70, 5);
3411        client.free_fid(7);
3412        assert_eq!(client.snapshot_unsynced(70).0, Some(5));
3413    }
3414
3415    async fn next_request(requests: &mut TestRequests) -> Option<P9Message> {
3416        requests
3417            .recv()
3418            .await
3419            .map(|frame| P9Message::from_bytes_ctx(&frame, false).unwrap())
3420    }
3421
3422    async fn recv_request(requests: &mut TestRequests, description: &str) -> P9Message {
3423        next_request(requests).await.expect(description)
3424    }
3425
3426    async fn recv_op_request(requests: &mut TestRequests, description: &str) -> P9Message {
3427        let frame = requests.recv().await.expect(description);
3428        P9Message::from_bytes_ctx(&frame, true).unwrap()
3429    }
3430
3431    fn reply(conn: &Conn, tag: u16, body: Message) {
3432        conn.deliver(Bytes::from(P9Message::new(tag, body).to_bytes().unwrap()));
3433    }
3434
3435    fn qid(path: u64) -> Qid {
3436        Qid {
3437            type_: 0,
3438            version: 0,
3439            path,
3440        }
3441    }
3442
3443    fn inode_fid(inode_id: u64, root_inode: u64, n_uname: u32, opened: Option<u32>) -> FidRecord {
3444        FidRecord {
3445            inode_id,
3446            root_inode,
3447            n_uname,
3448            uname: Vec::new(),
3449            opened,
3450        }
3451    }
3452
3453    fn attach_fid(root_inode: u64, n_uname: u32) -> FidRecord {
3454        FidRecord {
3455            inode_id: root_inode,
3456            root_inode,
3457            n_uname,
3458            uname: Vec::new(),
3459            opened: None,
3460        }
3461    }
3462
3463    fn write_lock(fid: u32, start: u64, length: u64) -> LockRecord {
3464        LockRecord {
3465            fid,
3466            lock_type: LockType::WriteLock,
3467            start,
3468            length,
3469            proc_id: 1,
3470            client_id: b"owner".to_vec(),
3471        }
3472    }
3473
3474    #[test]
3475    fn forgetting_a_fid_removes_only_its_locks() {
3476        let mut state = SessionState::default();
3477        state.fids.insert(7, inode_fid(70, 0, 0, None));
3478        state.fids.insert(8, inode_fid(80, 0, 0, None));
3479        state.locks.extend([
3480            write_lock(7, 0, 10),
3481            write_lock(8, 0, 10),
3482            write_lock(7, 20, 10),
3483        ]);
3484
3485        state.forget_fid(7);
3486
3487        assert!(!state.fids.contains_key(&7));
3488        assert!(state.fids.contains_key(&8));
3489        assert_eq!(state.locks.len(), 1);
3490        assert_eq!(state.locks[0].fid, 8);
3491    }
3492
3493    async fn replay_rebind_error(
3494        client: &NinePClient,
3495        conn: &Arc<Conn>,
3496        mut requests: TestRequests,
3497        expected: (u32, u64, u64, u8),
3498        ecode: u32,
3499    ) -> ClientResult<()> {
3500        let responder_conn = Arc::clone(conn);
3501        let responder = tokio::spawn(async move {
3502            let request = recv_request(&mut requests, "rebind request").await;
3503            let Message::Trebind(rebind) = request.body else {
3504                panic!("expected Trebind");
3505            };
3506            let actual = (rebind.fid, rebind.inode_id, rebind.root_inode, rebind.flags);
3507            assert_eq!(actual, expected);
3508            reply(
3509                &responder_conn,
3510                request.tag,
3511                Message::Rlerror(Rlerror { ecode }),
3512            );
3513        });
3514        let result = client.replay(conn).await;
3515        responder.await.unwrap();
3516        result
3517    }
3518
3519    #[tokio::test]
3520    async fn progressing_fid_replay_has_no_aggregate_deadline() {
3521        let (conn, mut requests) = test_conn_with_receiver();
3522        let client = test_client(Arc::clone(&conn));
3523        {
3524            let mut state = client.state.lock().unwrap();
3525            state.fids.insert(1, attach_fid(10, 0));
3526            state.fids.insert(2, inode_fid(11, 10, 0, None));
3527        }
3528
3529        let responder_conn = Arc::clone(&conn);
3530        let responder = tokio::spawn(async move {
3531            for _ in 0..2 {
3532                let request = recv_request(&mut requests, "replay rebind").await;
3533                let Message::Trebind(rebind) = request.body else {
3534                    panic!("expected Trebind");
3535                };
3536                // Each exchange makes progress within its own budget. Their
3537                // finite sequence has no separate aggregate cutoff.
3538                runtime::sleep(Duration::from_millis(20)).await;
3539                reply(
3540                    &responder_conn,
3541                    request.tag,
3542                    Message::Rrebind(Rrebind {
3543                        qid: qid(rebind.inode_id),
3544                    }),
3545                );
3546            }
3547        });
3548
3549        client
3550            .replay_with_rpc_timeout(&conn, Duration::from_secs(1))
3551            .await
3552            .unwrap();
3553        responder.await.unwrap();
3554    }
3555
3556    #[tokio::test]
3557    async fn stalled_fid_replay_is_bounded_per_exchange() {
3558        let (conn, _requests) = test_conn_with_receiver();
3559        let client = test_client(Arc::clone(&conn));
3560        client
3561            .state
3562            .lock()
3563            .unwrap()
3564            .fids
3565            .insert(1, attach_fid(10, 0));
3566
3567        assert!(matches!(
3568            client
3569                .replay_with_rpc_timeout(&conn, Duration::from_millis(5))
3570                .await,
3571            Err(ClientError::Disconnected)
3572        ));
3573        assert!(conn.dead.load(Ordering::Acquire));
3574        assert!(conn.pending.is_empty());
3575    }
3576
3577    #[tokio::test]
3578    async fn reconnect_winner_rejects_an_old_stateful_response() {
3579        let old = test_conn();
3580        let client = test_client(Arc::clone(&old));
3581        let waiter_client = Arc::clone(&client);
3582        let waiter_old = Arc::clone(&old);
3583
3584        let reconnect = client.session_transition.lock().await;
3585        let waiter = tokio::spawn(async move {
3586            waiter_client
3587                .accept_stateful_response(&waiter_old)
3588                .await
3589                .is_some()
3590        });
3591        tokio::task::yield_now().await;
3592        assert!(
3593            !waiter.is_finished(),
3594            "response settlement must wait for reconnect's transition"
3595        );
3596
3597        old.dead.store(true, Ordering::Release);
3598        client.live.store(false, Ordering::Release);
3599        client.conn.store(test_conn());
3600        client.live.store(true, Ordering::Release);
3601        drop(reconnect);
3602
3603        assert!(
3604            !waiter.await.unwrap(),
3605            "a response from the replaced connection must be resent, not settled"
3606        );
3607    }
3608
3609    #[tokio::test]
3610    async fn settled_state_is_visible_to_the_next_replay_snapshot() {
3611        let conn = test_conn();
3612        let client = test_client(Arc::clone(&conn));
3613        let settlement = client
3614            .accept_stateful_response(&conn)
3615            .await
3616            .expect("current response should settle");
3617
3618        let replay_client = Arc::clone(&client);
3619        let snapshot = tokio::spawn(async move {
3620            let _reconnect = replay_client.session_transition.lock().await;
3621            replay_client.state.lock().unwrap().fids.contains_key(&7)
3622        });
3623        tokio::task::yield_now().await;
3624        assert!(
3625            !snapshot.is_finished(),
3626            "replay must wait until response bookkeeping completes"
3627        );
3628
3629        client
3630            .state
3631            .lock()
3632            .unwrap()
3633            .fids
3634            .insert(7, inode_fid(70, 0, 0, Some(0)));
3635        drop(settlement);
3636
3637        assert!(
3638            snapshot.await.unwrap(),
3639            "the replay snapshot must include the settled fid transition"
3640        );
3641    }
3642
3643    #[tokio::test]
3644    async fn permanent_replay_loss_wakes_waiters_with_estale() {
3645        let client = test_client(test_conn());
3646        client.live.store(false, Ordering::Release);
3647        let waiter_client = Arc::clone(&client);
3648        let waiter = tokio::spawn(async move { waiter_client.wait_until_live().await });
3649        tokio::task::yield_now().await;
3650        assert!(!waiter.is_finished());
3651
3652        client.fail_session(linux::ESTALE);
3653        assert!(matches!(
3654            waiter.await.unwrap(),
3655            Err(ClientError::Errno(errno)) if errno == linux::ESTALE
3656        ));
3657        assert!(matches!(
3658            client.wait_until_live().await,
3659            Err(ClientError::Errno(errno)) if errno == linux::ESTALE
3660        ));
3661    }
3662
3663    #[tokio::test]
3664    async fn discarded_replay_candidate_resets_session_before_shutdown() {
3665        let (conn, mut requests) = test_conn_with_receiver();
3666        let client = test_client(Arc::clone(&conn));
3667        let responder_conn = Arc::clone(&conn);
3668        let responder = tokio::spawn(async move {
3669            let request = recv_request(&mut requests, "candidate reset").await;
3670            assert_eq!(request.tag, NOTAG);
3671            assert!(matches!(request.body, Message::Tversion(_)));
3672            reply(
3673                &responder_conn,
3674                request.tag,
3675                Message::Rversion(Rversion {
3676                    msize: 8192,
3677                    version: P9String::new(VERSION_9P2000L_ZEROFS.to_vec()),
3678                }),
3679            );
3680            requests
3681        });
3682
3683        client.discard_replay_candidate(&conn).await;
3684        let mut requests = responder.await.unwrap();
3685        assert!(
3686            requests.try_recv().is_err(),
3687            "candidate reset must not issue a throwaway lineage query"
3688        );
3689        assert!(conn.dead.load(Ordering::Acquire));
3690    }
3691
3692    #[tokio::test]
3693    async fn replay_establishes_each_attach_root_before_its_explicit_descendants() {
3694        let (conn, mut requests) = test_conn_with_receiver();
3695        let client = test_client(Arc::clone(&conn));
3696        {
3697            let mut state = client.state.lock().unwrap();
3698            state.fids.insert(1, attach_fid(10, 1000));
3699            state.fids.insert(2, attach_fid(20, 2000));
3700            state.fids.insert(3, inode_fid(11, 10, 1000, None));
3701            state.fids.insert(4, inode_fid(21, 20, 2000, None));
3702        }
3703
3704        let responder_conn = Arc::clone(&conn);
3705        let responder = tokio::spawn(async move {
3706            let mut roots = std::collections::BTreeSet::new();
3707            let mut descendants = std::collections::BTreeSet::new();
3708            for index in 0..4 {
3709                let request = recv_request(&mut requests, "replay request").await;
3710                let Message::Trebind(rebind) = request.body else {
3711                    panic!("expected Trebind");
3712                };
3713                assert_eq!(
3714                    rebind.flags, P9_REBIND_REPLAY,
3715                    "every automatic fid replay is marked independently of open state"
3716                );
3717                if index < 2 {
3718                    assert_eq!(rebind.inode_id, rebind.root_inode);
3719                    roots.insert(rebind.root_inode);
3720                } else {
3721                    assert_ne!(rebind.inode_id, rebind.root_inode);
3722                    descendants.insert((rebind.inode_id, rebind.root_inode));
3723                }
3724                reply(
3725                    &responder_conn,
3726                    request.tag,
3727                    Message::Rrebind(Rrebind {
3728                        qid: qid(rebind.inode_id),
3729                    }),
3730                );
3731            }
3732            assert_eq!(roots, [10, 20].into_iter().collect());
3733            assert_eq!(descendants, [(11, 10), (21, 20)].into_iter().collect());
3734        });
3735
3736        client.replay(&conn).await.unwrap();
3737        responder.await.unwrap();
3738    }
3739
3740    #[tokio::test]
3741    async fn name_derived_credentials_survive_descendant_replay() {
3742        let (old_conn, mut old_requests) = test_conn_with_receiver();
3743        let client = test_client(Arc::clone(&old_conn));
3744        let responder_conn = Arc::clone(&old_conn);
3745        let responder = tokio::spawn(async move {
3746            let request = recv_request(&mut old_requests, "attach request").await;
3747            let Message::Tattach(attach) = request.body else {
3748                panic!("expected Tattach");
3749            };
3750            assert_eq!(attach.uname.as_str().unwrap(), "root");
3751            assert_eq!(attach.n_uname, u32::MAX);
3752            reply(
3753                &responder_conn,
3754                request.tag,
3755                Message::Rattach(Rattach {
3756                    qid: Qid {
3757                        type_: 0x80,
3758                        ..qid(10)
3759                    },
3760                }),
3761            );
3762
3763            let request = recv_request(&mut old_requests, "walk request").await;
3764            assert!(matches!(request.body, Message::Twalk(_)));
3765            reply(
3766                &responder_conn,
3767                request.tag,
3768                Message::Rwalk(Rwalk {
3769                    nwqid: 1,
3770                    wqids: vec![qid(11)],
3771                }),
3772            );
3773        });
3774
3775        client.attach(1, NOFID, "root", "", u32::MAX).await.unwrap();
3776        client.walk(1, 2, &[b"child"]).await.unwrap();
3777        responder.await.unwrap();
3778
3779        client.state.lock().unwrap().fids.remove(&1);
3780
3781        let (replay_conn, mut replay_requests) = test_conn_with_receiver();
3782        let responder_conn = Arc::clone(&replay_conn);
3783        let responder = tokio::spawn(async move {
3784            let request = recv_request(&mut replay_requests, "descendant rebind").await;
3785            let Message::Trebind(rebind) = request.body else {
3786                panic!("expected Trebind");
3787            };
3788            assert_eq!(rebind.fid, 2);
3789            assert_eq!((rebind.inode_id, rebind.root_inode), (11, 10));
3790            assert_eq!(rebind.uname.as_str().unwrap(), "root");
3791            assert_eq!(rebind.n_uname, u32::MAX);
3792            reply(
3793                &responder_conn,
3794                request.tag,
3795                Message::Rrebind(Rrebind { qid: qid(11) }),
3796            );
3797        });
3798
3799        client.replay(&replay_conn).await.unwrap();
3800        responder.await.unwrap();
3801    }
3802
3803    #[tokio::test]
3804    async fn binary_rebind_credentials_survive_replay_exactly() {
3805        let (old_conn, mut old_requests) = test_conn_with_receiver();
3806        let client = test_client(Arc::clone(&old_conn));
3807        let expected = vec![
3808            P9_REBIND_CREDENTIAL_SENTINEL,
3809            P9_REBIND_CREDENTIAL_VERSION,
3810            0xd0,
3811            0x07,
3812            0,
3813            0,
3814            2,
3815            0xd1,
3816            0x07,
3817            0,
3818            0,
3819            0xd2,
3820            0x07,
3821            0,
3822            0,
3823        ];
3824
3825        let responder_conn = Arc::clone(&old_conn);
3826        let expected_initial = expected.clone();
3827        let responder = tokio::spawn(async move {
3828            let request = recv_request(&mut old_requests, "credential rebind").await;
3829            let Message::Trebind(rebind) = request.body else {
3830                panic!("expected Trebind");
3831            };
3832            assert_eq!((rebind.fid, rebind.inode_id, rebind.n_uname), (7, 70, 1000));
3833            assert_eq!(rebind.flags, 0);
3834            assert_eq!(rebind.uname.data, expected_initial);
3835            reply(
3836                &responder_conn,
3837                request.tag,
3838                Message::Rrebind(Rrebind { qid: qid(70) }),
3839            );
3840        });
3841
3842        client
3843            .rebind_with_credentials(7, 70, 1000, 2000, &[2001, 2002])
3844            .await
3845            .unwrap();
3846        responder.await.unwrap();
3847
3848        let (replay_conn, mut replay_requests) = test_conn_with_receiver();
3849        let responder_conn = Arc::clone(&replay_conn);
3850        let responder = tokio::spawn(async move {
3851            let request = recv_request(&mut replay_requests, "credential replay").await;
3852            let Message::Trebind(rebind) = request.body else {
3853                panic!("expected Trebind");
3854            };
3855            assert_eq!((rebind.fid, rebind.inode_id, rebind.n_uname), (7, 70, 1000));
3856            assert_eq!(rebind.flags, P9_REBIND_REPLAY);
3857            assert_eq!(rebind.uname.data, expected);
3858            reply(
3859                &responder_conn,
3860                request.tag,
3861                Message::Rrebind(Rrebind { qid: qid(70) }),
3862            );
3863        });
3864
3865        client.replay(&replay_conn).await.unwrap();
3866        responder.await.unwrap();
3867    }
3868
3869    #[tokio::test]
3870    async fn primary_group_rebind_marks_incomplete_groups_through_replay() {
3871        let (old_conn, mut old_requests) = test_conn_with_receiver();
3872        let client = test_client(Arc::clone(&old_conn));
3873        let expected = vec![
3874            P9_REBIND_CREDENTIAL_SENTINEL,
3875            P9_REBIND_CREDENTIAL_VERSION,
3876            0xd0,
3877            0x07,
3878            0,
3879            0,
3880            P9_REBIND_CREDENTIAL_GROUPS_INCOMPLETE,
3881        ];
3882
3883        let responder_conn = Arc::clone(&old_conn);
3884        let expected_initial = expected.clone();
3885        let responder = tokio::spawn(async move {
3886            let request = recv_request(&mut old_requests, "primary-group rebind").await;
3887            let Message::Trebind(rebind) = request.body else {
3888                panic!("expected Trebind");
3889            };
3890            assert_eq!((rebind.fid, rebind.inode_id, rebind.n_uname), (7, 70, 1000));
3891            assert_eq!(rebind.flags, 0);
3892            assert_eq!(rebind.uname.data, expected_initial);
3893            reply(
3894                &responder_conn,
3895                request.tag,
3896                Message::Rrebind(Rrebind { qid: qid(70) }),
3897            );
3898        });
3899
3900        client
3901            .rebind_with_primary_group(7, 70, 1000, 2000)
3902            .await
3903            .unwrap();
3904        responder.await.unwrap();
3905
3906        let (replay_conn, mut replay_requests) = test_conn_with_receiver();
3907        let responder_conn = Arc::clone(&replay_conn);
3908        let responder = tokio::spawn(async move {
3909            let request = recv_request(&mut replay_requests, "primary-group replay").await;
3910            let Message::Trebind(rebind) = request.body else {
3911                panic!("expected Trebind");
3912            };
3913            assert_eq!(rebind.flags, P9_REBIND_REPLAY);
3914            assert_eq!(rebind.uname.data, expected);
3915            reply(
3916                &responder_conn,
3917                request.tag,
3918                Message::Rrebind(Rrebind { qid: qid(70) }),
3919            );
3920        });
3921
3922        client.replay(&replay_conn).await.unwrap();
3923        responder.await.unwrap();
3924    }
3925
3926    #[tokio::test]
3927    async fn credential_rebind_rejects_unrepresentable_group_lists() {
3928        let (conn, _requests) = test_conn_with_receiver();
3929        let client = test_client(conn);
3930        let groups = [2000; P9_MAX_GROUPS + 1];
3931
3932        assert!(matches!(
3933            client
3934                .rebind_with_credentials(7, 70, 1000, 2000, &groups)
3935                .await,
3936            Err(ClientError::Errno(code)) if code == linux::E2BIG
3937        ));
3938    }
3939
3940    #[tokio::test]
3941    async fn rooted_descendant_replays_after_its_attach_fid_was_clunked() {
3942        let (conn, mut requests) = test_conn_with_receiver();
3943        let client = test_client(Arc::clone(&conn));
3944        client
3945            .state
3946            .lock()
3947            .unwrap()
3948            .fids
3949            .insert(7, inode_fid(70, 10, 1000, None));
3950
3951        let responder_conn = Arc::clone(&conn);
3952        let responder = tokio::spawn(async move {
3953            let request = recv_request(&mut requests, "descendant rebind").await;
3954            let Message::Trebind(rebind) = request.body else {
3955                panic!("expected Trebind");
3956            };
3957            assert_eq!((rebind.inode_id, rebind.root_inode), (70, 10));
3958            reply(
3959                &responder_conn,
3960                request.tag,
3961                Message::Rrebind(Rrebind { qid: qid(70) }),
3962            );
3963        });
3964
3965        client.replay(&conn).await.unwrap();
3966        responder.await.unwrap();
3967        assert!(client.state.lock().unwrap().fids.contains_key(&7));
3968    }
3969
3970    #[tokio::test]
3971    async fn missing_opened_fid_is_quarantined_without_terminating_the_session() {
3972        let (conn, mut requests) = test_conn_with_receiver();
3973        let client = test_client(Arc::clone(&conn));
3974        client
3975            .state
3976            .lock()
3977            .unwrap()
3978            .fids
3979            .insert(7, inode_fid(70, 0, 0, Some(0)));
3980
3981        let responder_conn = Arc::clone(&conn);
3982        let responder = tokio::spawn(async move {
3983            let request = recv_request(&mut requests, "opened fid rebind").await;
3984            assert!(matches!(
3985                request.body,
3986                Message::Trebind(Trebind {
3987                    fid: 7,
3988                    inode_id: 70,
3989                    flags: OPEN_REPLAY,
3990                    ..
3991                })
3992            ));
3993            reply(
3994                &responder_conn,
3995                request.tag,
3996                Message::Rlerror(Rlerror {
3997                    ecode: linux::ESTALE,
3998                }),
3999            );
4000
4001            let request = recv_request(&mut requests, "unrelated read").await;
4002            assert!(matches!(
4003                request.body,
4004                Message::Tread(Tread {
4005                    fid: 8,
4006                    count: 1,
4007                    ..
4008                })
4009            ));
4010            reply(
4011                &responder_conn,
4012                request.tag,
4013                Message::Rread(Rread {
4014                    count: 1,
4015                    data: P9Bytes::from(vec![b'x']),
4016                }),
4017            );
4018        });
4019
4020        client.replay(&conn).await.unwrap();
4021        assert!(!client.state.lock().unwrap().fids.contains_key(&7));
4022        assert!(client.stale_fids.contains(&7));
4023        assert_eq!(client.terminal_errno.load(Ordering::Acquire), 0);
4024        assert!(matches!(
4025            client.read(7, 0, 1).await,
4026            Err(ClientError::Errno(errno)) if errno == linux::ESTALE
4027        ));
4028        assert_eq!(client.read(8, 0, 1).await.unwrap(), vec![b'x']);
4029        tokio::time::timeout(Duration::from_secs(1), client.clunk(7))
4030            .await
4031            .expect("stale clunk must not reach the server")
4032            .unwrap();
4033        assert!(!client.stale_fids.contains(&7));
4034        responder.await.unwrap();
4035    }
4036
4037    #[tokio::test]
4038    async fn read_bytes_retains_the_response_frame_allocation() {
4039        let (conn, mut requests) = test_conn_with_receiver();
4040        let client = test_client(Arc::clone(&conn));
4041        let request_client = Arc::clone(&client);
4042        let read = tokio::spawn(async move { request_client.read_bytes(7, 0, 7).await });
4043
4044        let request = recv_request(&mut requests, "read request").await;
4045        assert!(matches!(request.body, Message::Tread(_)));
4046        let frame = Bytes::from(
4047            P9Message::new(
4048                request.tag,
4049                Message::Rread(Rread {
4050                    count: 7,
4051                    data: P9Bytes::from(Bytes::from_static(b"payload")),
4052                }),
4053            )
4054            .to_bytes()
4055            .unwrap(),
4056        );
4057        let expected_payload_ptr = frame.as_ptr() as usize + P9_IOHDRSZ as usize;
4058        conn.deliver(frame);
4059
4060        let payload = read.await.unwrap().unwrap();
4061        assert_eq!(payload.as_ref(), b"payload");
4062        assert_eq!(payload.as_ptr() as usize, expected_payload_ptr);
4063    }
4064
4065    #[test]
4066    fn freeing_a_fid_clears_its_stale_tombstone() {
4067        let client = test_client(test_conn());
4068        client.stale_fids.insert(7);
4069        client.free_fid(7);
4070        assert!(!client.stale_fids.contains(&7));
4071    }
4072
4073    #[test]
4074    fn stale_tombstones_cover_source_destination_and_clunk_fids() {
4075        let client = test_client(test_conn());
4076        client.stale_fids.insert(7);
4077
4078        for request in [
4079            Message::Tlink(Tlink {
4080                dfid: 8,
4081                fid: 7,
4082                name: P9String::new(b"link".to_vec()),
4083            }),
4084            Message::Twalk(Twalk {
4085                fid: 8,
4086                newfid: 7,
4087                nwname: 0,
4088                wnames: Vec::new(),
4089            }),
4090            Message::Tclunk(Tclunk { fid: 7 }),
4091        ] {
4092            assert!(matches!(
4093                client.validate_fids(request.request_fids()),
4094                Err(ClientError::Errno(errno)) if errno == linux::ESTALE
4095            ));
4096        }
4097        assert!(
4098            client
4099                .validate_fids(
4100                    Message::Tread(Tread {
4101                        fid: 8,
4102                        offset: 0,
4103                        count: 1,
4104                    })
4105                    .request_fids()
4106                )
4107                .is_ok()
4108        );
4109    }
4110
4111    #[tokio::test]
4112    async fn clunk_does_not_mask_a_terminal_session_estale() {
4113        let client = test_client(test_conn());
4114        client.stale_fids.insert(7);
4115        client.fail_session(linux::ESTALE);
4116
4117        assert!(matches!(
4118            client.clunk(7).await,
4119            Err(ClientError::Errno(errno)) if errno == linux::ESTALE
4120        ));
4121        assert!(client.stale_fids.contains(&7));
4122    }
4123
4124    #[tokio::test]
4125    async fn failed_reopen_clunks_the_pending_rebind_fid() {
4126        let (conn, mut requests) = test_conn_with_receiver();
4127        let client = test_client(Arc::clone(&conn));
4128        client
4129            .state
4130            .lock()
4131            .unwrap()
4132            .fids
4133            .insert(7, inode_fid(70, 0, 0, Some(0)));
4134
4135        let responder_conn = Arc::clone(&conn);
4136        let responder = tokio::spawn(async move {
4137            let request = recv_request(&mut requests, "opened fid rebind").await;
4138            assert!(matches!(
4139                request.body,
4140                Message::Trebind(Trebind { fid: 7, .. })
4141            ));
4142            reply(
4143                &responder_conn,
4144                request.tag,
4145                Message::Rrebind(Rrebind { qid: qid(70) }),
4146            );
4147
4148            let request = recv_request(&mut requests, "fid reopen").await;
4149            assert!(matches!(
4150                request.body,
4151                Message::Tlopen(Tlopen { fid: 7, .. })
4152            ));
4153            reply(
4154                &responder_conn,
4155                request.tag,
4156                Message::Rlerror(Rlerror {
4157                    ecode: linux::ESTALE,
4158                }),
4159            );
4160
4161            let request = recv_request(&mut requests, "pending rebind fid clunk").await;
4162            assert!(matches!(request.body, Message::Tclunk(Tclunk { fid: 7 })));
4163            reply(&responder_conn, request.tag, Message::Rclunk(Rclunk));
4164        });
4165
4166        client.replay(&conn).await.unwrap();
4167        responder.await.unwrap();
4168        let state = client.state.lock().unwrap();
4169        assert!(!state.fids.contains_key(&7));
4170        drop(state);
4171        assert!(client.stale_fids.contains(&7));
4172        assert_eq!(client.terminal_errno.load(Ordering::Acquire), 0);
4173    }
4174
4175    #[tokio::test]
4176    async fn missing_locked_fid_is_a_terminal_replay_error() {
4177        let (conn, requests) = test_conn_with_receiver();
4178        let client = test_client(Arc::clone(&conn));
4179        {
4180            let mut state = client.state.lock().unwrap();
4181            state.fids.insert(7, inode_fid(70, 0, 0, None));
4182            state.locks.push(write_lock(7, 0, 0));
4183        }
4184
4185        assert!(matches!(
4186            replay_rebind_error(&client, &conn, requests, (7, 70, 0, REPLAY), linux::ENOENT)
4187                .await,
4188            Err(ClientError::Errno(errno)) if errno == linux::ESTALE
4189        ));
4190        {
4191            let state = client.state.lock().unwrap();
4192            assert!(state.fids.contains_key(&7));
4193            assert_eq!(state.locks.len(), 1);
4194        }
4195        assert_eq!(client.terminal_errno.load(Ordering::Acquire), linux::ESTALE);
4196    }
4197
4198    #[tokio::test]
4199    async fn missing_unopened_rebind_is_dropped() {
4200        let (conn, requests) = test_conn_with_receiver();
4201        let client = test_client(Arc::clone(&conn));
4202        client
4203            .state
4204            .lock()
4205            .unwrap()
4206            .fids
4207            .insert(7, inode_fid(70, 0, 0, None));
4208
4209        replay_rebind_error(&client, &conn, requests, (7, 70, 0, REPLAY), linux::ENOENT)
4210            .await
4211            .unwrap();
4212        assert!(!client.state.lock().unwrap().fids.contains_key(&7));
4213        assert_eq!(client.terminal_errno.load(Ordering::Acquire), 0);
4214    }
4215
4216    #[tokio::test]
4217    async fn operational_rebind_error_is_transient_and_preserves_fid() {
4218        let (conn, requests) = test_conn_with_receiver();
4219        let client = test_client(Arc::clone(&conn));
4220        client
4221            .state
4222            .lock()
4223            .unwrap()
4224            .fids
4225            .insert(7, inode_fid(70, 0, 0, None));
4226
4227        assert!(matches!(
4228            replay_rebind_error(
4229                &client,
4230                &conn,
4231                requests,
4232                (7, 70, 0, REPLAY),
4233                linux::EIO as u32,
4234            )
4235            .await,
4236            Err(ClientError::Errno(errno)) if errno == linux::EIO as u32
4237        ));
4238        assert!(client.state.lock().unwrap().fids.contains_key(&7));
4239        assert_eq!(client.terminal_errno.load(Ordering::Acquire), 0);
4240    }
4241
4242    #[tokio::test]
4243    async fn missing_unopened_attach_root_is_dropped_while_open_child_replays() {
4244        let (conn, mut requests) = test_conn_with_receiver();
4245        let client = test_client(Arc::clone(&conn));
4246        {
4247            let mut state = client.state.lock().unwrap();
4248            state.fids.insert(1, attach_fid(1, 0));
4249            state.fids.insert(2, inode_fid(70, 1, 0, Some(0)));
4250        }
4251
4252        let responder_conn = Arc::clone(&conn);
4253        let responder = tokio::spawn(async move {
4254            let request = recv_request(&mut requests, "root rebind request").await;
4255            assert!(matches!(
4256                request.body,
4257                Message::Trebind(Trebind {
4258                    inode_id: 1,
4259                    root_inode: 1,
4260                    flags: P9_REBIND_REPLAY,
4261                    ..
4262                })
4263            ));
4264            reply(
4265                &responder_conn,
4266                request.tag,
4267                Message::Rlerror(Rlerror {
4268                    ecode: linux::ENOENT,
4269                }),
4270            );
4271
4272            let request = recv_request(&mut requests, "opened child rebind").await;
4273            assert!(matches!(
4274                request.body,
4275                Message::Trebind(Trebind {
4276                    inode_id: 70,
4277                    root_inode: 1,
4278                    flags: P9_REBIND_KNOWN_FLAGS,
4279                    ..
4280                })
4281            ));
4282            reply(
4283                &responder_conn,
4284                request.tag,
4285                Message::Rrebind(Rrebind { qid: qid(70) }),
4286            );
4287
4288            let request = recv_request(&mut requests, "opened child reopen").await;
4289            assert!(matches!(
4290                request.body,
4291                Message::Tlopen(Tlopen { fid: 2, flags: 0 })
4292            ));
4293            reply(
4294                &responder_conn,
4295                request.tag,
4296                Message::Rlopen(Rlopen {
4297                    qid: qid(70),
4298                    iounit: 0,
4299                }),
4300            );
4301        });
4302
4303        client.replay(&conn).await.unwrap();
4304        responder.await.unwrap();
4305        let state = client.state.lock().unwrap();
4306        assert!(!state.fids.contains_key(&1));
4307        assert!(state.fids.contains_key(&2));
4308        drop(state);
4309        assert_eq!(client.terminal_errno.load(Ordering::Acquire), 0);
4310    }
4311
4312    #[tokio::test]
4313    async fn operational_attach_error_is_transient() {
4314        let (conn, requests) = test_conn_with_receiver();
4315        let client = test_client(Arc::clone(&conn));
4316        client
4317            .state
4318            .lock()
4319            .unwrap()
4320            .fids
4321            .insert(1, attach_fid(2, 0));
4322
4323        assert!(matches!(
4324            replay_rebind_error(&client, &conn, requests, (1, 2, 2, REPLAY), linux::EIO as u32)
4325            .await,
4326            Err(ClientError::Errno(errno)) if errno == linux::EIO as u32
4327        ));
4328        assert!(client.state.lock().unwrap().fids.contains_key(&1));
4329        assert_eq!(client.terminal_errno.load(Ordering::Acquire), 0);
4330    }
4331
4332    #[tokio::test]
4333    async fn persistent_lock_conflict_keeps_the_candidate_without_terminating_the_session() {
4334        let (conn, mut requests) = test_conn_with_receiver();
4335        let client = test_client(Arc::clone(&conn));
4336        client.state.lock().unwrap().locks.push(write_lock(7, 0, 0));
4337
4338        let responder_conn = Arc::clone(&conn);
4339        let responder = tokio::spawn(async move {
4340            while let Some(request) = next_request(&mut requests).await {
4341                assert!(matches!(request.body, Message::Tlock(_)));
4342                reply(
4343                    &responder_conn,
4344                    request.tag,
4345                    Message::Rlock(Rlock {
4346                        status: LockStatus::Blocked.as_wire(),
4347                    }),
4348                );
4349            }
4350        });
4351
4352        assert!(
4353            runtime::timeout(Duration::from_millis(100), client.replay(&conn))
4354                .await
4355                .is_err(),
4356            "a lock conflict should keep waiting on the restored candidate"
4357        );
4358        assert_eq!(client.terminal_errno.load(Ordering::Acquire), 0);
4359        responder.abort();
4360        let _ = responder.await;
4361    }
4362
4363    #[tokio::test]
4364    async fn supervised_lock_replay_stops_after_the_last_external_owner_drops() {
4365        let (conn, mut requests) = test_conn_with_receiver();
4366        let client = test_client(Arc::clone(&conn));
4367        client.state.lock().unwrap().locks.push(write_lock(7, 0, 0));
4368
4369        let replay_client = Arc::clone(&client);
4370        let replay_conn = Arc::clone(&conn);
4371        let replay =
4372            tokio::spawn(async move { replay_client.replay_supervised(&replay_conn).await });
4373
4374        let request = recv_request(&mut requests, "conflicting replay lock").await;
4375        assert!(matches!(request.body, Message::Tlock(_)));
4376        reply(
4377            &conn,
4378            request.tag,
4379            Message::Rlock(Rlock {
4380                status: LockStatus::Blocked.as_wire(),
4381            }),
4382        );
4383
4384        drop(client);
4385        assert!(matches!(
4386            runtime::timeout(Duration::from_secs(1), replay).await,
4387            Ok(Ok(Err(ClientError::Disconnected)))
4388        ));
4389        assert!(conn.dead.load(Ordering::Acquire));
4390    }
4391
4392    #[tokio::test]
4393    async fn refused_lock_replay_terminates_the_session() {
4394        let (conn, mut requests) = test_conn_with_receiver();
4395        let client = test_client(Arc::clone(&conn));
4396        client.state.lock().unwrap().locks.push(write_lock(7, 0, 0));
4397
4398        let responder_conn = Arc::clone(&conn);
4399        let responder = tokio::spawn(async move {
4400            let request = recv_request(&mut requests, "lock replay").await;
4401            assert!(matches!(request.body, Message::Tlock(_)));
4402            reply(
4403                &responder_conn,
4404                request.tag,
4405                Message::Rlock(Rlock {
4406                    status: LockStatus::LockError.as_wire(),
4407                }),
4408            );
4409        });
4410
4411        assert!(matches!(
4412            client.replay(&conn).await,
4413            Err(ClientError::Errno(errno)) if errno == linux::ESTALE
4414        ));
4415        responder.await.unwrap();
4416        assert_eq!(client.terminal_errno.load(Ordering::Acquire), linux::ESTALE);
4417    }
4418
4419    #[tokio::test]
4420    async fn lock_replay_retries_through_old_session_teardown() {
4421        let (conn, mut requests) = test_conn_with_receiver();
4422        let client = test_client(Arc::clone(&conn));
4423        client.state.lock().unwrap().locks.push(write_lock(7, 0, 0));
4424
4425        let responder_conn = Arc::clone(&conn);
4426        let responder = tokio::spawn(async move {
4427            let request = recv_request(&mut requests, "blocked lock acquisition").await;
4428            assert!(matches!(request.body, Message::Tlock(_)));
4429            reply(
4430                &responder_conn,
4431                request.tag,
4432                Message::Rlock(Rlock {
4433                    status: LockStatus::Blocked.as_wire(),
4434                }),
4435            );
4436
4437            let request = recv_request(&mut requests, "retried lock acquisition").await;
4438            assert!(matches!(
4439                request.body,
4440                Message::Tlock(Tlock {
4441                    lock_type: LockType::WriteLock,
4442                    ..
4443                })
4444            ));
4445            reply(
4446                &responder_conn,
4447                request.tag,
4448                Message::Rlock(Rlock {
4449                    status: LockStatus::Success.as_wire(),
4450                }),
4451            );
4452        });
4453
4454        client.replay(&conn).await.unwrap();
4455        responder.await.unwrap();
4456        assert_eq!(client.terminal_errno.load(Ordering::Acquire), 0);
4457    }
4458
4459    #[tokio::test]
4460    async fn raced_lock_conflict_rolls_back_the_acquired_prefix() {
4461        let (conn, mut requests) = test_conn_with_receiver();
4462        let client = test_client(Arc::clone(&conn));
4463        client
4464            .state
4465            .lock()
4466            .unwrap()
4467            .locks
4468            .extend([write_lock(7, 0, 10), write_lock(8, 20, 10)]);
4469
4470        let responder_conn = Arc::clone(&conn);
4471        let responder = tokio::spawn(async move {
4472            let request = recv_request(&mut requests, "first lock acquisition").await;
4473            assert!(matches!(
4474                request.body,
4475                Message::Tlock(Tlock {
4476                    fid: 7,
4477                    lock_type: LockType::WriteLock,
4478                    ..
4479                })
4480            ));
4481            reply(
4482                &responder_conn,
4483                request.tag,
4484                Message::Rlock(Rlock {
4485                    status: LockStatus::Success.as_wire(),
4486                }),
4487            );
4488
4489            let request = recv_request(&mut requests, "raced lock acquisition").await;
4490            assert!(matches!(
4491                request.body,
4492                Message::Tlock(Tlock {
4493                    fid: 8,
4494                    lock_type: LockType::WriteLock,
4495                    ..
4496                })
4497            ));
4498            reply(
4499                &responder_conn,
4500                request.tag,
4501                Message::Rlerror(Rlerror {
4502                    ecode: linux::EAGAIN,
4503                }),
4504            );
4505
4506            let request = recv_request(&mut requests, "prefix rollback").await;
4507            assert!(matches!(
4508                request.body,
4509                Message::Tlock(Tlock {
4510                    fid: 7,
4511                    lock_type: LockType::Unlock,
4512                    ..
4513                })
4514            ));
4515            reply(
4516                &responder_conn,
4517                request.tag,
4518                Message::Rlock(Rlock {
4519                    status: LockStatus::Success.as_wire(),
4520                }),
4521            );
4522
4523            for expected_fid in [7, 8] {
4524                let request = recv_request(&mut requests, "retry lock acquisition").await;
4525                let Message::Tlock(lock) = request.body else {
4526                    panic!("expected Tlock");
4527                };
4528                assert_eq!(lock.fid, expected_fid);
4529                assert!(matches!(lock.lock_type, LockType::WriteLock));
4530                reply(
4531                    &responder_conn,
4532                    request.tag,
4533                    Message::Rlock(Rlock {
4534                        status: LockStatus::Success.as_wire(),
4535                    }),
4536                );
4537            }
4538        });
4539
4540        client.replay(&conn).await.unwrap();
4541        responder.await.unwrap();
4542        assert_eq!(client.terminal_errno.load(Ordering::Acquire), 0);
4543    }
4544
4545    #[test]
4546    fn clean_rejection_of_first_frame_restarts_the_retry_horizon() {
4547        let mut attempt = OpAttemptState::default();
4548        let (first_flags, first_origin) = attempt
4549            .dispatch_frame(true, 7, |_, origin| Ok(origin))
4550            .unwrap();
4551        assert_eq!((first_flags, first_origin), (0, 7));
4552        assert!(attempt.started.is_some());
4553        attempt.proven_predispatch(first_flags);
4554        assert!(attempt.started.is_none());
4555        let (next_flags, next_origin) = attempt
4556            .dispatch_frame(true, 8, |_, origin| Ok(origin))
4557            .unwrap();
4558        assert_eq!(
4559            (next_flags, next_origin),
4560            (0, 8),
4561            "a definitive rejection of the sole FIRST may be routed as FIRST again"
4562        );
4563    }
4564
4565    #[test]
4566    fn clean_rejection_of_retry_does_not_erase_older_ambiguity() {
4567        let mut attempt = OpAttemptState::default();
4568        let (first_flags, first_origin) = attempt
4569            .dispatch_frame(true, 7, |_, origin| Ok(origin))
4570            .unwrap();
4571        assert_eq!((first_flags, first_origin), (0, 7));
4572
4573        let (retry_flags, retry_origin) = attempt
4574            .dispatch_frame(true, 8, |_, origin| Ok(origin))
4575            .unwrap();
4576        assert_eq!((retry_flags, retry_origin), (P9_OP_FLAG_RETRY, 7));
4577        assert!(attempt.started.is_some());
4578        attempt.proven_predispatch(retry_flags);
4579        assert!(attempt.started.is_some());
4580        let (next_flags, next_origin) = attempt
4581            .dispatch_frame(true, 9, |_, origin| Ok(origin))
4582            .unwrap();
4583        assert_eq!(
4584            (next_flags, next_origin),
4585            (P9_OP_FLAG_RETRY, 7),
4586            "a rejected RETRY cannot make the older lost FIRST unambiguous"
4587        );
4588    }
4589
4590    async fn assert_notleader_reroute(ecode: u32, expected_flags: u8, expected_epoch: u64) {
4591        let (old_conn, mut old_requests) = test_conn_with_receiver();
4592        old_conn.writer_epoch.store(7, Ordering::Relaxed);
4593        let client = test_client(Arc::clone(&old_conn));
4594        track_test_fid(&client, 7);
4595
4596        let request_client = Arc::clone(&client);
4597        let request = tokio::spawn(async move { request_client.write(7, 0, b"x").await });
4598        let first_frame = tokio::time::timeout(Duration::from_secs(1), old_requests.recv())
4599            .await
4600            .expect("FIRST request was not queued")
4601            .expect("old request channel closed");
4602        let first = P9Message::from_bytes_ctx(&first_frame, true).unwrap();
4603        let op_id = first.op_id;
4604        assert_ne!(op_id, [0u8; 16]);
4605        assert_eq!(first.op_flags, 0);
4606        assert_eq!(first.op_origin_epoch, 7);
4607        assert!(matches!(first.body, Message::Twrite(_)));
4608
4609        client.live.store(false, Ordering::Release);
4610        reply(&old_conn, first.tag, Message::Rlerror(Rlerror { ecode }));
4611        tokio::time::timeout(Duration::from_secs(1), async {
4612            while !old_conn.dead.load(Ordering::Acquire) {
4613                tokio::task::yield_now().await;
4614            }
4615        })
4616        .await
4617        .expect("not-leader response did not force a re-probe");
4618
4619        let (new_conn, mut new_requests) = test_conn_with_receiver();
4620        new_conn.writer_epoch.store(8, Ordering::Relaxed);
4621        client.conn.store(Arc::clone(&new_conn));
4622        client.live.store(true, Ordering::Release);
4623        client.live_notify.notify_waiters();
4624
4625        let rerouted_frame = tokio::time::timeout(Duration::from_secs(1), new_requests.recv())
4626            .await
4627            .expect("request was not rerouted")
4628            .expect("replacement request channel closed");
4629        let rerouted = P9Message::from_bytes_ctx(&rerouted_frame, true).unwrap();
4630        assert_eq!(rerouted.op_id, op_id);
4631        assert_eq!(
4632            rerouted.op_flags, expected_flags,
4633            "CLEAN must restore FIRST; an ambiguous rejection must retain RETRY"
4634        );
4635        assert_eq!(
4636            rerouted.op_origin_epoch, expected_epoch,
4637            "FIRST adopts the successor epoch; RETRY retains its origin epoch"
4638        );
4639        assert!(matches!(rerouted.body, Message::Twrite(_)));
4640
4641        reply(
4642            &new_conn,
4643            rerouted.tag,
4644            Message::Rwrite(Rwrite { count: 1 }),
4645        );
4646        assert_eq!(request.await.unwrap().unwrap(), 1);
4647    }
4648
4649    #[tokio::test]
4650    async fn clean_notleader_after_first_reroutes_same_op_id_as_first() {
4651        assert_notleader_reroute(P9_ENOTLEADER_CLEAN, 0, 8).await;
4652    }
4653
4654    #[tokio::test]
4655    async fn generic_notleader_after_first_reroutes_same_op_id_as_retry() {
4656        assert_notleader_reroute(P9_ENOTLEADER, P9_OP_FLAG_RETRY, 7).await;
4657    }
4658
4659    #[tokio::test]
4660    async fn write_bytes_chunks_at_the_msize_boundary() {
4661        let (conn, mut requests) = test_conn_with_receiver();
4662        let client = test_client(Arc::clone(&conn));
4663        track_test_fid(&client, 7);
4664        let max = client.max_write_payload();
4665        assert_eq!(
4666            max,
4667            client.msize() - P9_TWRITE_HDR - P9_OP_ENVELOPE_LEN as u32
4668        );
4669        let offset = 37;
4670        let mut payload = vec![b'x'; max as usize + 1];
4671        payload[max as usize] = b'y';
4672        let payload = Bytes::from(payload);
4673
4674        let request_client = Arc::clone(&client);
4675        let request =
4676            tokio::spawn(async move { request_client.write_bytes(7, offset, payload).await });
4677
4678        let first = recv_op_request(&mut requests, "first write request").await;
4679        let Message::Twrite(first_write) = first.body else {
4680            panic!("expected Twrite");
4681        };
4682        assert_eq!(first_write.offset, offset);
4683        assert_eq!(first_write.count, max);
4684        assert_eq!(first_write.data.len(), max as usize);
4685        assert!(first_write.data.iter().all(|byte| *byte == b'x'));
4686        reply(&conn, first.tag, Message::Rwrite(Rwrite { count: max }));
4687
4688        let second = recv_op_request(&mut requests, "second write request").await;
4689        let Message::Twrite(second_write) = second.body else {
4690            panic!("expected Twrite");
4691        };
4692        assert_eq!(second_write.offset, offset + u64::from(max));
4693        assert_eq!(second_write.count, 1);
4694        assert_eq!(second_write.data.as_ref(), b"y");
4695        reply(&conn, second.tag, Message::Rwrite(Rwrite { count: 1 }));
4696
4697        assert_eq!(request.await.unwrap().unwrap(), u64::from(max) + 1);
4698    }
4699
4700    #[tokio::test]
4701    async fn read_once_rejects_a_payload_larger_than_requested() {
4702        let (conn, mut requests) = test_conn_with_receiver();
4703        let client = test_client(Arc::clone(&conn));
4704
4705        let request_client = Arc::clone(&client);
4706        let request = tokio::spawn(async move { request_client.read_once(7, 0, 1).await });
4707        let sent = recv_request(&mut requests, "read request").await;
4708        assert!(matches!(sent.body, Message::Tread(Tread { count: 1, .. })));
4709        reply(
4710            &conn,
4711            sent.tag,
4712            Message::Rread(Rread {
4713                count: 2,
4714                data: P9Bytes::from(vec![b'x', b'y']),
4715            }),
4716        );
4717
4718        assert!(matches!(
4719            request.await.unwrap(),
4720            Err(ClientError::Unexpected("read count"))
4721        ));
4722    }
4723
4724    #[tokio::test]
4725    async fn write_chunk_rejects_a_count_larger_than_attempted() {
4726        let (conn, mut requests) = test_conn_with_receiver();
4727        let client = test_client(Arc::clone(&conn));
4728        track_test_fid(&client, 7);
4729
4730        let request_client = Arc::clone(&client);
4731        let request = tokio::spawn(async move {
4732            request_client
4733                .write_chunk(7, 0, Bytes::from_static(b"x"))
4734                .await
4735        });
4736        let sent = recv_op_request(&mut requests, "write request").await;
4737        assert!(matches!(
4738            sent.body,
4739            Message::Twrite(Twrite { count: 1, .. })
4740        ));
4741        reply(&conn, sent.tag, Message::Rwrite(Rwrite { count: 2 }));
4742
4743        assert!(matches!(
4744            request.await.unwrap(),
4745            Err(ClientError::Unexpected("write count"))
4746        ));
4747    }
4748
4749    #[tokio::test]
4750    async fn ambiguous_reply_loss_marks_the_next_frame_as_retry() {
4751        let (conn, mut requests) = test_conn_with_receiver();
4752        let client = test_client(Arc::clone(&conn));
4753        track_test_fid(&client, 7);
4754
4755        let responder_conn = Arc::clone(&conn);
4756        let responder = tokio::spawn(async move {
4757            let first = recv_op_request(&mut requests, "initial write request").await;
4758            let op_id = first.op_id;
4759            assert_ne!(op_id, [0u8; 16]);
4760            assert_eq!(first.op_flags, 0);
4761
4762            let (_, pending) = responder_conn
4763                .pending
4764                .remove(&first.tag)
4765                .expect("initial response slot");
4766            drop(pending);
4767
4768            let retry = recv_op_request(&mut requests, "retried write request").await;
4769            assert_eq!(retry.op_id, op_id);
4770            assert_eq!(retry.op_flags, P9_OP_FLAG_RETRY);
4771            reply(
4772                &responder_conn,
4773                retry.tag,
4774                Message::Rlerror(Rlerror {
4775                    ecode: P9_EOPIDSTALE,
4776                }),
4777            );
4778        });
4779
4780        assert!(matches!(
4781            client.write(7, 0, b"x").await,
4782            Err(ClientError::Errno(P9_EOPIDSTALE))
4783        ));
4784        responder.await.unwrap();
4785        assert!(conn.pending.is_empty());
4786    }
4787
4788    #[tokio::test]
4789    async fn cancelled_stateful_create_retires_its_session() {
4790        let (old_conn, mut old_requests) = test_conn_with_receiver();
4791        old_conn.writer_epoch.store(7, Ordering::Relaxed);
4792        let client = test_client(Arc::clone(&old_conn));
4793        client
4794            .state
4795            .lock()
4796            .unwrap()
4797            .fids
4798            .insert(7, inode_fid(1, 1, 0, None));
4799        let first_client = Arc::clone(&client);
4800        let first_task = tokio::spawn(async move {
4801            first_client
4802                .lcreate(7, b"child", linux::O_CREAT, 0o644, 0)
4803                .await
4804        });
4805        let first = recv_op_request(&mut old_requests, "FIRST create request").await;
4806        assert_eq!(first.op_flags, 0);
4807        assert_eq!(first.op_origin_epoch, 7);
4808        assert!(matches!(first.body, Message::Tlcreate(_)));
4809
4810        first_task.abort();
4811        let _ = first_task.await;
4812        assert!(
4813            old_conn.dead.load(Ordering::Acquire),
4814            "an unobserved stateful transition must retire its connection"
4815        );
4816        assert_eq!(client.state.lock().unwrap().fids[&7].inode_id, 1);
4817        old_conn.connection_lost(&client.reconnect_notify);
4818    }
4819
4820    #[tokio::test]
4821    async fn cancelled_stateful_request_does_not_retire_an_unseen_successor() {
4822        let (old_conn, mut old_requests) = test_conn_with_receiver();
4823        let client = test_client(Arc::clone(&old_conn));
4824        client
4825            .state
4826            .lock()
4827            .unwrap()
4828            .fids
4829            .insert(7, inode_fid(1, 1, 0, None));
4830
4831        let request_client = Arc::clone(&client);
4832        let request = tokio::spawn(async move {
4833            request_client
4834                .lcreate(7, b"child", linux::O_CREAT, 0o644, 0)
4835                .await
4836        });
4837        let sent = recv_op_request(&mut old_requests, "create request").await;
4838        assert!(matches!(sent.body, Message::Tlcreate(_)));
4839
4840        client.live.store(false, Ordering::Release);
4841        old_conn.connection_lost(&client.reconnect_notify);
4842        let (successor, _successor_requests) = test_conn_with_receiver();
4843        client.conn.store(Arc::clone(&successor));
4844
4845        request.abort();
4846        let _ = request.await;
4847        assert!(old_conn.dead.load(Ordering::Acquire));
4848        assert!(
4849            !successor.dead.load(Ordering::Acquire),
4850            "cancellation must not retire a connection that did not receive the request"
4851        );
4852    }
4853
4854    #[tokio::test]
4855    async fn cancelled_zero_id_lopen_retires_its_session() {
4856        let (conn, mut requests) = test_conn_with_receiver();
4857        let client = test_client(Arc::clone(&conn));
4858        client
4859            .state
4860            .lock()
4861            .unwrap()
4862            .fids
4863            .insert(7, inode_fid(42, 1, 0, None));
4864
4865        let request_client = Arc::clone(&client);
4866        let request = tokio::spawn(async move { request_client.lopen(7, 0).await });
4867        let sent = recv_request(&mut requests, "Tlopen request").await;
4868        assert!(matches!(sent.body, Message::Tlopen(Tlopen { fid: 7, .. })));
4869
4870        request.abort();
4871        let _ = request.await;
4872        assert!(
4873            conn.dead.load(Ordering::Acquire),
4874            "zero-id stateful cancellation must normalize the session"
4875        );
4876        assert_eq!(client.state.lock().unwrap().fids[&7].opened, None);
4877    }
4878
4879    #[tokio::test]
4880    async fn cancelling_stateful_create_before_enqueue_keeps_connection_live() {
4881        let (conn, _requests) = test_conn_with_receiver();
4882        conn.writer_tx
4883            .send(vec![0])
4884            .await
4885            .expect("test writer queue should accept its first frame");
4886        let client = test_client(Arc::clone(&conn));
4887        track_test_fid(&client, 7);
4888        let request_client = Arc::clone(&client);
4889        let request = tokio::spawn(async move {
4890            request_client
4891                .lcreate(7, b"child", linux::O_CREAT, 0o644, 0)
4892                .await
4893        });
4894        for _ in 0..4 {
4895            tokio::task::yield_now().await;
4896        }
4897        assert!(!request.is_finished());
4898        assert!(conn.pending.is_empty());
4899
4900        request.abort();
4901        let _ = request.await;
4902        assert!(
4903            !conn.dead.load(Ordering::Acquire),
4904            "pre-enqueue cancellation must not churn the live connection"
4905        );
4906    }
4907
4908    #[tokio::test]
4909    async fn cancelling_a_dispatched_request_quarantines_its_tag() {
4910        let (conn, mut requests) = test_conn_with_receiver();
4911        let client = test_client(Arc::clone(&conn));
4912        track_test_fid(&client, 7);
4913        let request_client = Arc::clone(&client);
4914        let request = tokio::spawn(async move { request_client.write(7, 0, b"x").await });
4915
4916        let first_frame = tokio::time::timeout(Duration::from_secs(1), requests.recv())
4917            .await
4918            .expect("request was not queued")
4919            .expect("request channel closed");
4920        let first = P9Message::from_bytes_ctx(&first_frame, true).unwrap();
4921        assert_eq!(conn.pending.len(), 1);
4922        request.abort();
4923        let _ = request.await;
4924        assert_eq!(
4925            conn.pending.len(),
4926            1,
4927            "a dispatched request's tag must remain quarantined"
4928        );
4929
4930        conn.tag_ctr.store(first.tag, Ordering::Relaxed);
4931        let second_client = Arc::clone(&client);
4932        let second_request = tokio::spawn(async move { second_client.write(7, 0, b"y").await });
4933        let second_frame = tokio::time::timeout(Duration::from_secs(1), requests.recv())
4934            .await
4935            .expect("second request was not queued")
4936            .expect("request channel closed");
4937        let second = P9Message::from_bytes_ctx(&second_frame, true).unwrap();
4938        assert_ne!(second.tag, first.tag);
4939
4940        reply(&conn, first.tag, Message::Rwrite(Rwrite { count: 1 }));
4941        tokio::task::yield_now().await;
4942        assert!(
4943            !second_request.is_finished(),
4944            "the late response must not complete the new request"
4945        );
4946
4947        reply(&conn, second.tag, Message::Rwrite(Rwrite { count: 1 }));
4948        assert_eq!(second_request.await.unwrap().unwrap(), 1);
4949        assert!(conn.pending.is_empty());
4950    }
4951
4952    #[tokio::test]
4953    async fn cancelling_before_writer_capacity_does_not_register_a_tag() {
4954        let (conn, _requests) = test_conn_with_receiver();
4955        conn.writer_tx
4956            .send(vec![0])
4957            .await
4958            .expect("test writer queue should accept its first frame");
4959
4960        let client = test_client(Arc::clone(&conn));
4961        track_test_fid(&client, 7);
4962        let request_client = Arc::clone(&client);
4963        let request = tokio::spawn(async move { request_client.write(7, 0, b"x").await });
4964        for _ in 0..4 {
4965            tokio::task::yield_now().await;
4966        }
4967        assert!(!request.is_finished());
4968        assert!(
4969            conn.pending.is_empty(),
4970            "waiting for writer capacity must happen before tag registration"
4971        );
4972        request.abort();
4973        let _ = request.await;
4974        assert!(conn.pending.is_empty());
4975    }
4976
4977    #[tokio::test]
4978    async fn admission_waits_locally_until_an_inflight_request_settles() {
4979        let (conn, mut requests) = test_conn_with_receiver();
4980        let client = test_client_with_limit(Arc::clone(&conn), 1);
4981        track_test_fid(&client, 7);
4982
4983        let first_client = Arc::clone(&client);
4984        let first = tokio::spawn(async move { first_client.write(7, 0, b"a").await });
4985        let first_request = recv_op_request(&mut requests, "first admitted request").await;
4986        assert_eq!(client.request_admission.available_permits(), 0);
4987
4988        let second_client = Arc::clone(&client);
4989        let second = tokio::spawn(async move { second_client.write(7, 1, b"b").await });
4990        for _ in 0..4 {
4991            tokio::task::yield_now().await;
4992        }
4993        assert!(
4994            requests.try_recv().is_err(),
4995            "the next request must remain local while the permit is held"
4996        );
4997
4998        reply(
4999            &conn,
5000            first_request.tag,
5001            Message::Rwrite(Rwrite { count: 1 }),
5002        );
5003        assert_eq!(first.await.unwrap().unwrap(), 1);
5004
5005        let second_request = recv_op_request(&mut requests, "second admitted request").await;
5006        reply(
5007            &conn,
5008            second_request.tag,
5009            Message::Rwrite(Rwrite { count: 1 }),
5010        );
5011        assert_eq!(second.await.unwrap().unwrap(), 1);
5012        assert_eq!(client.request_admission.available_permits(), 1);
5013    }
5014
5015    #[tokio::test]
5016    async fn cancelled_request_retains_admission_while_its_tag_is_quarantined() {
5017        let (conn, mut requests) = test_conn_with_receiver();
5018        let client = test_client_with_limit(Arc::clone(&conn), 1);
5019        track_test_fid(&client, 7);
5020
5021        let first_client = Arc::clone(&client);
5022        let first = tokio::spawn(async move { first_client.write(7, 0, b"a").await });
5023        let first_request = recv_op_request(&mut requests, "first request").await;
5024        first.abort();
5025        let _ = first.await;
5026        assert_eq!(client.request_admission.available_permits(), 0);
5027
5028        let second_client = Arc::clone(&client);
5029        let second = tokio::spawn(async move { second_client.write(7, 1, b"b").await });
5030        for _ in 0..4 {
5031            tokio::task::yield_now().await;
5032        }
5033        assert!(
5034            requests.try_recv().is_err(),
5035            "cancellation must not release capacity while the reply remains unresolved"
5036        );
5037
5038        // Delivery removes the abandoned registration. Its retained permit is
5039        // then released even though the response receiver no longer exists.
5040        reply(
5041            &conn,
5042            first_request.tag,
5043            Message::Rwrite(Rwrite { count: 1 }),
5044        );
5045        let second_request = recv_op_request(&mut requests, "second request").await;
5046        reply(
5047            &conn,
5048            second_request.tag,
5049            Message::Rwrite(Rwrite { count: 1 }),
5050        );
5051        assert_eq!(second.await.unwrap().unwrap(), 1);
5052    }
5053
5054    #[tokio::test]
5055    async fn reconnect_resend_keeps_the_original_admission_permit() {
5056        let (old_conn, mut old_requests) = test_conn_with_receiver();
5057        let client = test_client_with_limit(Arc::clone(&old_conn), 1);
5058        track_test_fid(&client, 7);
5059
5060        let first_client = Arc::clone(&client);
5061        let first = tokio::spawn(async move { first_client.write(7, 0, b"a").await });
5062        let first_request = recv_op_request(&mut old_requests, "first request").await;
5063        assert!(matches!(
5064            first_request.body,
5065            Message::Twrite(Twrite { offset: 0, .. })
5066        ));
5067
5068        let second_client = Arc::clone(&client);
5069        let second = tokio::spawn(async move { second_client.write(7, 1, b"b").await });
5070        for _ in 0..4 {
5071            tokio::task::yield_now().await;
5072        }
5073        assert!(old_requests.try_recv().is_err());
5074
5075        client.live.store(false, Ordering::Release);
5076        old_conn.connection_lost(&client.reconnect_notify);
5077        let (new_conn, mut new_requests) = test_conn_with_receiver();
5078        client.conn.store(Arc::clone(&new_conn));
5079        client.live.store(true, Ordering::Release);
5080        client.live_notify.notify_waiters();
5081
5082        let resent = recv_op_request(&mut new_requests, "resent first request").await;
5083        assert!(matches!(
5084            resent.body,
5085            Message::Twrite(Twrite { offset: 0, .. })
5086        ));
5087        assert!(
5088            new_requests.try_recv().is_err(),
5089            "a resend must retain its permit instead of admitting another operation"
5090        );
5091
5092        reply(&new_conn, resent.tag, Message::Rwrite(Rwrite { count: 1 }));
5093        assert_eq!(first.await.unwrap().unwrap(), 1);
5094
5095        let second_request = recv_op_request(&mut new_requests, "second request").await;
5096        assert!(matches!(
5097            second_request.body,
5098            Message::Twrite(Twrite { offset: 1, .. })
5099        ));
5100        reply(
5101            &new_conn,
5102            second_request.tag,
5103            Message::Rwrite(Rwrite { count: 1 }),
5104        );
5105        assert_eq!(second.await.unwrap().unwrap(), 1);
5106    }
5107
5108    #[test]
5109    fn undispatched_guard_releases_its_tag() {
5110        let conn = test_conn();
5111
5112        let (tx, _rx) = oneshot::channel();
5113        let tag = NinePClient::alloc_tag(&conn, tx, None).expect("tag allocation failed");
5114        let guard = PendingTag {
5115            conn: Arc::clone(&conn),
5116            tag,
5117            dispatched: false,
5118        };
5119        assert!(conn.pending.contains_key(&tag));
5120        drop(guard);
5121        assert!(conn.pending.is_empty());
5122    }
5123
5124    #[test]
5125    fn allocator_checks_every_wire_tag_after_skipping_notag() {
5126        let conn = test_conn();
5127
5128        for tag in 0..(NOTAG - 1) {
5129            let (tx, _rx) = oneshot::channel();
5130            assert!(NinePClient::register_tag(&conn, tag, tx, None).is_ok());
5131        }
5132        conn.tag_ctr.store(NOTAG, Ordering::Relaxed);
5133
5134        let (tx, _rx) = oneshot::channel();
5135        let tag =
5136            NinePClient::alloc_tag(&conn, tx, None).expect("allocator missed the last usable tag");
5137        assert_eq!(tag, NOTAG - 1);
5138    }
5139}
5140
5141#[cfg(test)]
5142mod liveness_tests {
5143    use super::*;
5144
5145    fn test_conn() -> Arc<Conn> {
5146        test_conn_with_receiver().0
5147    }
5148
5149    #[test]
5150    fn valid_frames_advance_connection_progress() {
5151        let conn = test_conn();
5152        assert_eq!(conn.receive_generation(), 0);
5153        conn.deliver(
5154            P9Message::new(17, Message::Rflush(Rflush))
5155                .to_bytes()
5156                .unwrap()
5157                .into(),
5158        );
5159        assert_eq!(conn.receive_generation(), 1);
5160    }
5161
5162    #[test]
5163    fn malformed_frames_do_not_prove_liveness() {
5164        let conn = test_conn();
5165        conn.deliver(Bytes::from_static(&[0; P9_HEADER_SIZE - 1]));
5166        assert_eq!(conn.receive_generation(), 0);
5167    }
5168
5169    fn response(tag: u16, body: Message) -> Bytes {
5170        P9Message::new(tag, body).to_bytes().unwrap().into()
5171    }
5172
5173    fn request_tag(frame: &[u8]) -> u16 {
5174        u16::from_le_bytes([frame[5], frame[6]])
5175    }
5176
5177    #[tokio::test]
5178    async fn a_quiet_probe_timeout_does_not_claim_liveness() {
5179        let (conn, mut requests) = test_conn_with_receiver();
5180        let observed = conn.receive_generation();
5181        let probe =
5182            NinePClient::probe_connection_with_timeout(&conn, observed, Duration::from_millis(5));
5183        let (alive, request) = tokio::join!(probe, requests.recv());
5184        let request = request.expect("probe request");
5185        let tag = request_tag(&request);
5186
5187        assert!(!alive);
5188        assert!(conn.pending.contains_key(&tag));
5189        assert!(
5190            !NinePClient::probe_connection_with_timeout(
5191                &conn,
5192                conn.receive_generation(),
5193                Duration::from_millis(5),
5194            )
5195            .await
5196        );
5197        assert!(
5198            runtime::timeout(Duration::from_millis(1), requests.recv())
5199                .await
5200                .is_err(),
5201            "an outstanding probe must suppress another probe"
5202        );
5203
5204        conn.deliver(response(
5205            tag,
5206            Message::Rgetlineage(Rgetlineage {
5207                token: 1,
5208                writer_epoch: 1,
5209            }),
5210        ));
5211        assert!(conn.pending.is_empty());
5212        assert_eq!(
5213            conn.liveness_probe_tag.load(Ordering::Acquire),
5214            NO_LIVENESS_PROBE_TAG
5215        );
5216    }
5217
5218    #[tokio::test]
5219    async fn unrelated_reply_proves_a_probe_connection_is_moving() {
5220        let (conn, mut requests) = test_conn_with_receiver();
5221        let observed = conn.receive_generation();
5222        let probe =
5223            NinePClient::probe_connection_with_timeout(&conn, observed, Duration::from_millis(5));
5224        let server = async {
5225            let request = requests.recv().await.expect("probe request");
5226            let tag = request_tag(&request);
5227            let other_tag = if tag == 0 { 1 } else { 0 };
5228            conn.deliver(response(other_tag, Message::Rflush(Rflush)));
5229            tag
5230        };
5231        let (alive, tag) = tokio::join!(probe, server);
5232
5233        assert!(alive);
5234        assert!(conn.pending.contains_key(&tag));
5235
5236        conn.deliver(response(
5237            tag,
5238            Message::Rgetlineage(Rgetlineage {
5239                token: 1,
5240                writer_epoch: 1,
5241            }),
5242        ));
5243        assert!(conn.pending.is_empty());
5244        assert_eq!(
5245            conn.liveness_probe_tag.load(Ordering::Acquire),
5246            NO_LIVENESS_PROBE_TAG
5247        );
5248    }
5249}