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