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