Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ use std::time::{Duration, Instant};
use tokio::sync::oneshot;

const NOTAG: u16 = 0xffff;
/// The most names one Twalk may carry (MAXWELEM in the 9p spec).
const MAX_WALK_NAMES: usize = 16;
const ROOT_FID: u32 = 1;

/// A decoded response frame: message type plus the body after the 7-byte header.
Expand Down Expand Up @@ -319,16 +321,34 @@ impl NineClient {
.ok_or_else(|| to_io("short Rattach").into())
}

/// Walk `names` from `fid` into the fresh `newfid`. `names` empty clones the fid (newfid points
/// at the same file). Returns the walked qids (one per name).
/// Walk `names` from `fid` to `newfid`.
/// Walk `names` from `fid` to `newfid`; no names makes `newfid` a copy of `fid`. Returns one
/// qid per name.
///
/// On `Ok`, `newfid` is established on the server and belongs to the caller, who must clunk it
/// when done. On `Err` it is not, and clunking it is a protocol error the server answers with
/// EIO: 9P2000 leaves `newfid` untouched unless the walk completes in full. A server reports a
/// name that did not resolve by returning fewer qids than asked for rather than an Rlerror, so
/// that short reply is mapped to ENOENT here to keep the rule "Err means no fid" exact.
/// EIO.
pub async fn walk(&self, fid: u32, newfid: u32, names: &[&str]) -> Result<Vec<Qid>, i32> {
// A Twalk carries at most MAX_WALK_NAMES names, so walk the rest from newfid in place.
let mut chunks = names.chunks(MAX_WALK_NAMES);
let mut qids = self
.walk_once(fid, newfid, chunks.next().unwrap_or(&[]))
.await?;
for chunk in chunks {
match self.walk_once(newfid, newfid, chunk).await {
Ok(more) => qids.extend(more),
Err(e) => {
let _ = self.clunk(newfid).await;
return Err(e);
}
}
}
Ok(qids)
}

/// One Twalk. 9P2000 leaves `newfid` untouched unless the walk completes in full. A server
/// reports a name that did not resolve by returning fewer qids than asked for rather than an
/// Rlerror, so that short reply is mapped to ENOENT to keep the rule "Err means no fid" exact.
async fn walk_once(&self, fid: u32, newfid: u32, names: &[&str]) -> Result<Vec<Qid>, i32> {
let mut w = W::new();
w.u32(fid).u32(newfid).u16(names.len() as u16);
for n in names {
Expand Down
36 changes: 36 additions & 0 deletions tests/diod_fuse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,42 @@ async fn external_growth_with_pinned_inode() {
.await;
}

/// Invalidating paths deeper than one `Twalk` can carry (16 names), existing or not, works and
/// doesn't break the connection.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn invalidate_deep_path() {
if let Some(reason) = skip_reason() {
eprintln!("SKIP invalidate_deep_path: {reason}");
return;
}
with_mount_full(Via::Tcp, Tuning::default(), |mut m| {
let dir = "d/".repeat(20);
std::fs::create_dir_all(m.export.join(&dir)).unwrap();
std::fs::write(m.export.join(format!("{dir}f")), b"old").unwrap();
assert_eq!(std::fs::read(m.mp.join(format!("{dir}f"))).unwrap(), b"old");

std::fs::write(m.export.join(format!("{dir}f")), b"new").unwrap();
writeln!(m.inval, "inval 1 {dir}f").unwrap();
let mut reply = String::new();
std::io::BufReader::new(&m.inval)
.read_line(&mut reply)
.unwrap();
assert_eq!(reply, "ok 1\n");

assert_eq!(std::fs::read(m.mp.join(format!("{dir}f"))).unwrap(), b"new");

// Fails in the second Twalk, after the first has established the fid.
writeln!(m.inval, "inval 2 {dir}missing/{dir}f").unwrap();
let mut reply = String::new();
std::io::BufReader::new(&m.inval)
.read_line(&mut reply)
.unwrap();
assert_eq!(reply, "ok 2\n");
assert_eq!(std::fs::read(m.mp.join(format!("{dir}f"))).unwrap(), b"new");
})
.await;
}

/// `stat` and a read through a second fd see every write made so far on a still-open writer, even
/// though the write pipeline has acknowledged writes the server may not have yet. Without the
/// writeback cache the kernel takes size and data from us, so we have to wait for those writes.
Expand Down
Loading