summaryrefslogtreecommitdiff
path: root/9proc/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-20 01:47:28 -0300
committerGabriel Schneider <[email protected]>2026-09-20 01:47:29 -0300
commit66f2e492c348677ab3050f5e378b9eb4c04c98ce (patch)
treecf06b32309eace8eb573925e9cf14e3dfc27bd66 /9proc/src
parent65209217b5b68f56bc0bd5bc6c4dce33911ded59 (diff)
downloadcloud9-66f2e492c348677ab3050f5e378b9eb4c04c98ce.tar.gz
cloud9-66f2e492c348677ab3050f5e378b9eb4c04c98ce.zip
9proc core becomes a backend of cloud9.fs; engine gains optional features
9proc's own fid table, walk loop and dir-read engine are replaced by cloud9.fs.Server; the tree (static, vars, providers) is served through the engine's Req/Reply contract with node ids that keep the old qid scheme. Providers may answer later by returning error.Again (parked in the engine, retried each step, Tflush -> EINTR); no new files are exposed. Engine (backward compatible, all opt-in via Backend.features / Options): create, remove, wstat, reference accounting for backends that count handles, a salted fid index, name_capacity 0 (names from getattr), Reply.ename for backend-chosen error text, Attr.path/version/atime. Engine-level error strings and the 217-byte msize floor now apply to 9proc; tests updated accordingly. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to '9proc/src')
-rw-r--r--9proc/src/core.zig1181
-rw-r--r--9proc/src/linux/probe.zig2
-rw-r--r--9proc/src/scratch.zig30
3 files changed, 552 insertions, 661 deletions
diff --git a/9proc/src/core.zig b/9proc/src/core.zig
index cead8f3..ce71809 100644
--- a/9proc/src/core.zig
+++ b/9proc/src/core.zig
@@ -1,12 +1,14 @@
-//! The freestanding 9P2000 introspection engine: a static tree generated at
+//! The freestanding 9P2000 introspection tree: a static tree generated at
//! comptime from a `Config` (README, /build, /comptime, /runtime/fn, /ctl,
-//! /vars) plus runtime `Provider`s mounted at the top level, served over a
-//! `cloud9.Server` connection. No allocator, no OS, no threads: every buffer is
-//! caller-owned (`Storage`, `Shared`, `Conn`), every table is sized at comptime.
-//! See docs/LIBRARY.md.
+//! /vars) plus runtime `Provider`s mounted at the top level, served as a
+//! backend of `cloud9.fs.Server` (the file-server engine, which owns fids,
+//! walks, directory cursors, permissions and Tflush). No allocator, no OS,
+//! no threads: every buffer is caller-owned (`Storage`, `Shared`, `Conn`),
+//! every table is sized at comptime. See docs/LIBRARY.md.
const std = @import("std");
const builtin = @import("builtin");
const cloud9 = @import("cloud9");
+const fs = cloud9.fs;
const vars = @import("vars.zig");
const Writer = std.Io.Writer;
@@ -50,6 +52,8 @@ pub const Config = struct {
/// slots so that reads at arbitrary offsets are consistent.
snapshot_slots: u8 = 8,
snapshot_bytes: u32 = 16 * 1024,
+ /// Reads a provider has parked with `error.Again`, per connection.
+ max_parked: u8 = 8,
};
/// Attributes of a provider node, filled by `VTable.stat` and `VTable.list`.
@@ -85,6 +89,16 @@ pub const NodeStat = struct {
/// `clunk(0)` as a no-op. `walk` must accept "." on any node, file or directory
/// (a fresh reference to the same node; the core clones fids with it), and ".."
/// on directories (except at the root, which the core resolves itself).
+///
+/// Answering later: `read` may return `error.Again` when nothing is there
+/// yet. The request then parks in the engine (up to `Config.max_parked` per
+/// connection; a further one fails with EAGAIN) and every later `Conn.step`
+/// asks the provider again, with the same handle and offset, until it
+/// answers; the fid stays open. A Tflush of a parked read answers it with
+/// "Interrupted system call"; a hangup or Tversion drops it and closes the
+/// file as usual. Nothing wakes a connection by itself: the platform layer
+/// steps it when its transport moves, so a provider that becomes ready has
+/// to make that happen (out of the core's hands).
pub const Provider = struct {
name: []const u8,
ctx: *anyopaque,
@@ -94,7 +108,7 @@ pub const Provider = struct {
pub const Handle = u64;
pub const root: Handle = 0;
- pub const Error = error{ NotFound, Exists, Perm, NotDir, IsDir, NotEmpty, BadOffset, NoSpace, Io, Unsupported, Excl };
+ pub const Error = error{ NotFound, Exists, Perm, NotDir, IsDir, NotEmpty, BadOffset, NoSpace, Io, Unsupported, Excl, Again };
pub const VTable = struct {
walk: *const fn (ctx: *anyopaque, parent: Handle, name: []const u8) Error!Handle,
@@ -102,6 +116,7 @@ pub const Provider = struct {
/// The `index`-th entry of `dir`; false when done.
list: *const fn (ctx: *anyopaque, dir: Handle, index: usize, out: *NodeStat) Error!bool,
open: *const fn (ctx: *anyopaque, h: Handle, mode: u8) Error!void,
+ /// `error.Again` parks the read; see `Provider`.
read: *const fn (ctx: *anyopaque, h: Handle, offset: u64, buf: []u8) Error!usize,
write: *const fn (ctx: *anyopaque, h: Handle, offset: u64, data: []const u8) Error!usize,
/// Returns the new node, already open with `mode`.
@@ -117,7 +132,8 @@ pub const Provider = struct {
};
};
-/// The Plan 9 error string for any error the engine or a provider can raise.
+/// The Plan 9 error string for any error the tree or a provider can raise
+/// (the engine's own refusals carry cloud9.fs's strings).
pub fn ename(err: anyerror) []const u8 {
return switch (err) {
error.NotFound, error.NoFile => "file does not exist",
@@ -144,10 +160,28 @@ pub fn ename(err: anyerror) []const u8 {
error.WriteFailed => "no space in buffer",
error.ReplyTooLarge => "reply too large for msize",
error.OutOfMemory => "out of memory",
+ error.Again => "would block",
else => "i/o error",
};
}
+/// The engine errno closest to an error, beside its `ename`.
+fn errno(err: anyerror) u16 {
+ return switch (err) {
+ error.NotFound, error.NoFile => fs.E.NOENT,
+ error.Perm, error.Excl => fs.E.PERM,
+ error.Exists => fs.E.EXIST,
+ error.NotEmpty => fs.E.NOTEMPTY,
+ error.NotDir => fs.E.NOTDIR,
+ error.IsDir => fs.E.ISDIR,
+ error.BadOffset, error.Invalid, error.BadValue, error.BadCommand, error.BadName => fs.E.INVAL,
+ error.NoSpace, error.WriteFailed, error.NoSnapshot => fs.E.NOSPC,
+ error.Unsupported => fs.E.NOSYS,
+ error.OutOfMemory => fs.E.NOMEM,
+ else => fs.E.IO,
+ };
+}
+
/// A "don't care" Twstat: every field left as it is.
pub const stat_dontcare: cloud9.Stat = .{
.type = 0xFFFF,
@@ -380,24 +414,26 @@ pub fn Server(comptime cfg: Config) type {
comptime {
for (flat) |f| if (f.node.kind == .dynamic and f.node.gen == null) @compileError("dynamic node without generator");
- std.debug.assert(cfg.msize >= cloud9.Server.msize_min);
- std.debug.assert(cfg.snapshot_slots > 0 and cfg.max_fids > 0);
+ std.debug.assert(cfg.msize >= fs.msize_min);
+ std.debug.assert(cfg.snapshot_slots > 0 and cfg.max_fids > 0 and cfg.max_parked > 0);
// Provider index 0xFE/0xFF would collide with the var/static qid tags.
std.debug.assert(cfg.max_providers < 0xFE);
}
- // -- qid paths ------------------------------------------------------
+ // -- node ids and qid paths ------------------------------------------
const static_tag: u64 = 0xFF << 56;
const var_tag: u64 = 0xFE << 56;
const handle_mask: u64 = (1 << 56) - 1;
+ /// The engine's root: static node 0.
+ const root_id: u64 = static_tag;
// -- storage --------------------------------------------------------
/// Per-connection buffers; the caller places one in static memory.
pub const Storage = struct {
in: [cfg.msize]u8,
- out: [cfg.msize]u8,
+ out: [@max(cfg.msize, 2 * fs.msize_min)]u8,
/// Staging area for read replies (directory records, provider and raw reads).
data: [cfg.msize]u8,
snapshots: [cfg.snapshot_slots][cfg.snapshot_bytes]u8,
@@ -426,9 +462,9 @@ pub fn Server(comptime cfg: Config) type {
/// Length of the current ctl result (in `ctl_bufs[ctl_cur]`).
ctl_len: u32 = 0,
ctl_version: u32 = 0,
- /// Entropy for the per-connection fid hash. The core mixes in a
+ /// Entropy for the per-connection fid index. The core mixes in a
/// connection counter and buffer addresses; a platform layer with a
- /// random source may set this once after `init` to make the seed
+ /// random source may set this once after `init` to make the salt
/// unpredictable even where addresses are static.
hash_seed: u32 = 0,
conn_seq: u32 = 0,
@@ -486,47 +522,44 @@ pub fn Server(comptime cfg: Config) type {
return null;
}
- // -- connection -----------------------------------------------------
+ // -- the engine and its backend ---------------------------------------
+ /// What the engine knows of this tree: the contract types and the
+ /// capabilities it may ask for. The requests themselves are served
+ /// by `Conn.serve`.
+ pub const Backend = struct {
+ pub const Req = fs.Req;
+ pub const Reply = fs.Reply;
+ pub const features: fs.Features = .{ .create = true, .remove = true, .wstat = true, .references = true };
+ };
+
+ /// The file-server engine this tree is a backend of: it owns the fid
+ /// table, permissions, directory cursors, Tflush and the releases a
+ /// dropped connection owes. Names are not kept per fid (`getattr`
+ /// answers them), so a fid costs the same whatever the name length.
+ pub const Engine = fs.Server(Backend, .{
+ .fid_capacity = cfg.max_fids,
+ .slot_capacity = cfg.max_parked,
+ .park_data_max = 0,
+ .name_capacity = 0,
+ .fid_index = true,
+ });
+
+ /// A reference to a node of the tree: the engine's node id decoded.
const NodeRef = union(enum) {
static: u32,
prov: struct { idx: u8, h: Provider.Handle },
@"var": struct { idx: u8, node: u32 },
};
- const Fid = struct {
- id: u32 = 0,
- used: bool = false,
- node: NodeRef = .{ .static = 0 },
- is_dir: bool = true,
- open: bool = false,
- mode: u8 = 0,
- rclose: bool = false,
- dir_offset: u64 = 0,
- dir_index: usize = 0,
- /// Snapshot slot of an open dynamic file.
- snap: ?u8 = null,
- /// Free-list link, meaningful while `!used`.
- next_free: u16 = no_slot,
- };
-
- const no_slot: u16 = std.math.maxInt(u16);
- /// The fid index is an open-addressing (linear probing) table from fid
- /// number to a slot of `Conn.fids`, sized to stay at most half full so
- /// that lookups are O(1) with any number of fids.
- const index_len: usize = std.math.ceilPowerOfTwoAssert(usize, @as(usize, cfg.max_fids) * 2);
- const index_mask: usize = index_len - 1;
- const index_shift: u5 = @intCast(32 - @as(usize, std.math.log2_int(usize, index_len)));
-
- /// MurmurHash3's 32-bit finalizer: every input bit affects every output bit.
- fn fmix32(x: u32) u32 {
- var h = x;
- h ^= h >> 16;
- h *%= 0x85EB_CA6B;
- h ^= h >> 13;
- h *%= 0xC2B2_AE35;
- h ^= h >> 16;
- return h;
+ fn nodeId(ref: NodeRef) u64 {
+ return switch (ref) {
+ .static => |idx| static_tag | idx,
+ .@"var" => |v| var_tag | (@as(u64, v.idx) << 32) | v.node,
+ // Provider ids are offset by one: the engine reads node 0 as
+ // "the node asked about", and provider 0's root would be 0.
+ .prov => |p| (@as(u64, p.idx + 1) << 56) | (p.h & handle_mask),
+ };
}
/// Everything the engine needs to know about a node for qid/stat.
@@ -537,240 +570,166 @@ pub fn Server(comptime cfg: Config) type {
atime: u32,
mtime: u32,
version: u32,
+ /// The qid path.
path: u64,
name: []const u8,
- fn qid(i: Info) cloud9.Qid {
- var t: u8 = if (i.is_dir) cloud9.qtdir else cloud9.qtfile;
- if (i.mode & cloud9.dmappend != 0) t |= cloud9.qtappend;
- if (i.mode & cloud9.dmexcl != 0) t |= cloud9.qtexcl;
- return .{ .type = t, .version = i.version, .path = i.path };
- }
-
- fn stat(i: Info) cloud9.Stat {
+ fn attr(i: Info, ref: NodeRef) fs.Attr {
+ const id = nodeId(ref);
return .{
- .type = 0,
- .dev = 0,
- .qid = i.qid(),
- .mode = i.mode,
- .atime = i.atime,
- .mtime = i.mtime,
- .length = i.length,
.name = i.name,
- .uid = cfg.name,
- .gid = cfg.name,
- .muid = cfg.name,
+ .node = id,
+ .dir = i.is_dir,
+ .size = i.length,
+ .mode = @truncate(i.mode),
+ .mtime = i.mtime,
+ .atime = i.atime,
+ .version = i.version,
+ .path = if (i.path != id) i.path else null,
+ .append = i.mode & cloud9.dmappend != 0,
+ .excl = i.mode & cloud9.dmexcl != 0,
};
}
};
- /// One 9P connection: a cloud9.Server plus a fid table and snapshot slots.
+ /// A directory entry as the engine's readdir record wants it.
+ const Entry = struct { path: u64, dir: bool, name: []const u8 };
+
+ /// The engine's handle of an open file that holds no snapshot slot.
+ const no_snapshot: u32 = std.math.maxInt(u32);
+
+ /// One 9P connection: the engine plus this tree's per-connection
+ /// state (snapshot slots).
pub const Conn = struct {
shared: *Shared,
storage: *Storage,
- server: cloud9.Server,
+ engine: Engine,
/// Largest msize this connection negotiates.
msize_cap: u32,
- fids: [cfg.max_fids]Fid = @splat(.{}),
- /// XORed into every fid number before hashing so that a client
- /// cannot precompute fid numbers that collide (which would turn the
- /// index back into a linear scan).
- hash_seed: u32,
- /// fid number -> slot of `fids` (`no_slot` = empty bucket).
- index: [index_len]u16 = @splat(no_slot),
- /// Head of the free list threaded through `Fid.next_free`.
- free_head: u16 = no_slot,
- /// Slots `high_water..` have never been used (bump allocation).
- high_water: u16 = 0,
- nfids: u16 = 0,
slot_used: [cfg.snapshot_slots]bool = @splat(false),
slot_len: [cfg.snapshot_slots]u32 = @splat(0),
- name_buf: [max_name]u8 = undefined,
pub fn init(shared: *Shared, storage: *Storage, msize: u32) Conn {
shared.conn_seq +%= 1;
const addr = @intFromPtr(storage) ^ (@intFromPtr(shared) << 7);
- const seed = fmix32(shared.hash_seed ^ (shared.conn_seq *% 0x9E37_79B1) ^ @as(u32, @truncate(addr)) ^ @as(u32, @truncate(addr >> 16)));
+ const seed = shared.hash_seed ^ (shared.conn_seq *% 0x9E37_79B1) ^ @as(u32, @truncate(addr)) ^ @as(u32, @truncate(addr >> 16));
+ const cap = @max(@min(msize, cfg.msize), fs.msize_min);
return .{
.shared = shared,
.storage = storage,
- .server = .init(.{ .in = &storage.in, .out = &storage.out }),
- .msize_cap = @max(@min(msize, cfg.msize), cloud9.Server.msize_min),
- .hash_seed = seed,
+ .engine = .init(.{ .in = storage.in[0..cap], .out = &storage.out, .root = root_id, .seed = seed }),
+ .msize_cap = cap,
};
}
- /// Fibonacci hashing of the (seeded) fid number into `index_len` buckets.
- fn fidHome(c: *const Conn, id: u32) usize {
- return @intCast(((id ^ c.hash_seed) *% 0x9E37_79B1) >> index_shift);
- }
-
/// Feeds transport bytes; returns how many were taken.
pub fn push(c: *Conn, bytes: []const u8) usize {
- return c.server.push(bytes);
+ return c.engine.push(bytes);
}
/// Bytes to send to the client.
pub fn output(c: *const Conn) []const u8 {
- return c.server.output();
+ return c.engine.output();
}
pub fn wrote(c: *Conn, n: usize) void {
- c.server.wrote(n);
+ c.engine.wrote(n);
+ }
+
+ /// Free space in the input buffer (one msize-sized frame at most).
+ pub fn inputRoom(c: *const Conn) usize {
+ return c.engine.protocol.in.len - c.engine.protocol.in_len;
}
/// Drops every fid (telling providers) and kills the session.
pub fn hangup(c: *Conn) void {
- c.resetFids();
- c.server.hangup();
+ c.engine.hangup();
+ while (c.engine.next()) |req| c.serve(req);
}
- /// Handles at most one request. Returns false when more input (or
- /// output drainage) is needed. `error.Protocol` is terminal.
+ /// Serves at most one request of the engine (a 9P request is one
+ /// or more of them), after re-asking providers about every parked
+ /// read. Returns false when more input (or output drainage) is
+ /// needed. `error.Protocol` is terminal.
pub fn step(c: *Conn) error{Protocol}!bool {
- const req = (c.server.receive() catch return error.Protocol) orelse return false;
- defer c.server.release();
- switch (req.msg) {
- .tversion => |m| {
- c.resetFids();
- c.server.negotiate(@min(m.msize, c.msize_cap), m.version) catch return error.Protocol;
- },
- else => {
- const reply = c.dispatch(req.msg) catch |e| cloud9.Msg{ .rerror = .{ .ename = ename(e) } };
- c.server.reply(req.tag, reply) catch |e| switch (e) {
- // The reply does not fit the negotiated msize (Rstat or a long
- // Rwalk at a tiny msize). receive() guarantees room for one
- // msize-sized message, so this is never backpressure: answer with
- // an Rerror (truncated to fit by cloud9). Rwalk, the only
- // variable-size reply that follows a state change, is size-checked
- // in walk() before anything is mutated.
- error.TooLarge => c.server.reply(req.tag, .{ .rerror = .{ .ename = ename(error.ReplyTooLarge) } }) catch return error.Protocol,
- else => return error.Protocol,
- };
- },
- }
+ if (c.engine.protocol.dead) return error.Protocol;
+ while (c.engine.retry()) |req| c.serve(req);
+ const req = c.engine.next() orelse {
+ if (c.engine.protocol.dead) return error.Protocol;
+ return false;
+ };
+ c.serve(req);
+ if (c.engine.protocol.dead) return error.Protocol;
return true;
}
/// Number of fids currently held.
pub fn fidCount(c: *const Conn) usize {
- return c.nfids;
+ return c.engine.fidCount();
}
- fn dispatch(c: *Conn, msg: cloud9.Msg) anyerror!cloud9.Msg {
- return switch (msg) {
- .tauth => error.AuthNotRequired,
- .tattach => |m| c.attach(m),
- .tflush => .rflush,
- .twalk => |m| c.walk(m),
- .topen => |m| c.open(m),
- .tcreate => |m| c.create(m),
- .tread => |m| c.read(m),
- .twrite => |m| c.write(m),
- .tclunk => |m| c.clunk(m),
- .tremove => |m| c.remove(m),
- .tstat => |m| c.stat(m),
- .twstat => |m| c.wstat(m),
- else => error.Protocol,
- };
- }
-
- // -- fid table --
-
- /// The index bucket holding `id`, if any.
- fn findBucket(c: *const Conn, id: u32) ?usize {
- var pos = c.fidHome(id);
- while (true) : (pos = (pos + 1) & index_mask) {
- const slot = c.index[pos];
- if (slot == no_slot) return null;
- if (c.fids[slot].id == id) return pos;
+ /// The provider handle a fid holds, if it holds a provider node.
+ pub fn providerHandle(c: *const Conn, fid: u32) ?Provider.Handle {
+ for (c.engine.fids) |f| {
+ if (!f.used or f.orphan or f.fid != fid) continue;
+ return switch (c.refOf(f.node) orelse return null) {
+ .prov => |p| p.h,
+ else => null,
+ };
}
+ return null;
}
- fn findFid(c: *Conn, id: u32) ?*Fid {
- const pos = c.findBucket(id) orelse return null;
- return &c.fids[c.index[pos]];
- }
-
- fn allocFid(c: *Conn, id: u32) !*Fid {
- if (c.findBucket(id) != null) return error.FidInUse;
- if (c.nfids >= cfg.max_fids) return error.TooManyFids;
- const slot: u16 = if (c.free_head != no_slot) blk: {
- const slot = c.free_head;
- c.free_head = c.fids[slot].next_free;
- break :blk slot;
- } else blk: {
- const slot = c.high_water;
- c.high_water += 1;
- break :blk slot;
- };
- c.fids[slot] = .{ .id = id, .used = true };
- var pos = c.fidHome(id);
- while (c.index[pos] != no_slot) pos = (pos + 1) & index_mask;
- c.index[pos] = slot;
- c.nfids += 1;
- return &c.fids[slot];
- }
-
- /// Removes `id` from the index (backward-shift deletion: no tombstones).
- fn unlinkFid(c: *Conn, id: u32) void {
- var i = c.findBucket(id).?;
- var j = i;
- while (true) {
- j = (j + 1) & index_mask;
- const slot = c.index[j];
- if (slot == no_slot) break;
- const k = c.fidHome(c.fids[slot].id);
- // The entry at j may move into the hole at i unless its home
- // lies in the cyclic interval (i, j].
- const stays = if (i <= j) (k > i and k <= j) else (k > i or k <= j);
- if (!stays) {
- c.index[i] = slot;
- i = j;
- }
- }
- c.index[i] = no_slot;
- }
+ // -- serving the engine --
- /// Releases everything a fid holds; the slot stays allocated.
- fn dropContents(c: *Conn, f: *Fid) void {
- if (f.snap) |s| c.slot_used[s] = false;
- f.snap = null;
- if (f.node == .prov) {
- const p = c.shared.providers[f.node.prov.idx];
- if (f.open) if (p.vtable.close) |close| close(p.ctx, f.node.prov.h);
- if (f.rclose and f.open) if (p.vtable.remove) |rm| rm(p.ctx, f.node.prov.h) catch {};
- p.vtable.clunk(p.ctx, f.node.prov.h);
- }
- f.open = false;
- f.rclose = false;
+ fn serve(c: *Conn, req: fs.Req) void {
+ var bytes: []const u8 = "";
+ const reply = c.dispatch(req, &bytes) catch |e| failing(req.tag, e);
+ c.engine.reply(&reply, bytes);
}
- fn freeFid(c: *Conn, f: *Fid) void {
- c.dropContents(f);
- c.unlinkFid(f.id);
- const slot: u16 = @intCast((@intFromPtr(f) - @intFromPtr(&c.fids)) / @sizeOf(Fid));
- f.* = .{ .next_free = c.free_head };
- c.free_head = slot;
- c.nfids -= 1;
+ fn failing(tag: u64, e: anyerror) fs.Reply {
+ if (e == error.Again) return .{ .tag = tag, .status = .again };
+ return .{ .tag = tag, .status = .err, .errno = errno(e), .ename = ename(e) };
}
- fn resetFids(c: *Conn) void {
- for (c.fids[0..c.high_water]) |*f| {
- if (f.used) c.dropContents(f);
- f.* = .{};
+ fn dispatch(c: *Conn, req: fs.Req, bytes: *[]const u8) anyerror!fs.Reply {
+ const ref = c.refOf(req.node) orelse return error.NotFound;
+ switch (req.op) {
+ .lookup => {
+ const l = try c.lookup(ref, req.data);
+ return .{ .tag = req.tag, .attr = l.info.attr(l.ref) };
+ },
+ .getattr => return .{ .tag = req.tag, .attr = (try c.info(ref)).attr(ref) },
+ .setattr => return c.setattr(req, ref),
+ .open => return if (req.create) c.create(req, ref) else c.open(req, ref),
+ .read => return c.read(req, ref, bytes),
+ .readdir => return c.readDir(req, ref, bytes),
+ .write => return c.write(req, ref),
+ .release => return c.release(req, ref),
}
- @memset(&c.index, no_slot);
- c.free_head = no_slot;
- c.high_water = 0;
- c.nfids = 0;
}
- /// Releases a provider handle that is not held by any fid.
- fn releaseRef(c: *Conn, ref: NodeRef) void {
- if (ref == .prov) {
- const p = c.shared.providers[ref.prov.idx];
- p.vtable.clunk(p.ctx, ref.prov.h);
+ /// Decodes an engine node id; null for one this tree never issued.
+ fn refOf(c: *const Conn, id: u64) ?NodeRef {
+ const top: u8 = @intCast(id >> 56);
+ switch (top) {
+ 0xFF => {
+ const idx = id & handle_mask;
+ if (idx >= flat_len) return null;
+ return .{ .static = @intCast(idx) };
+ },
+ 0xFE => {
+ const vidx: u8 = @truncate(id >> 32);
+ const node: u32 = @truncate(id);
+ if ((id >> 40) & 0xFFFF != 0 or vidx >= c.shared.nvars) return null;
+ if (node >= c.shared.vars[vidx].vt.nodes.len) return null;
+ return .{ .@"var" = .{ .idx = vidx, .node = node } };
+ },
+ else => {
+ if (top == 0 or top - 1 >= c.shared.nprov) return null;
+ return .{ .prov = .{ .idx = top - 1, .h = id & handle_mask } };
+ },
}
}
@@ -835,6 +794,10 @@ pub fn Server(comptime cfg: Config) type {
}
}
+ fn provPath(idx: u8, h: Provider.Handle, st: NodeStat) u64 {
+ return (@as(u64, idx) << 56) | ((if (st.path != 0) st.path else h) & handle_mask);
+ }
+
fn provInfo(pr: Provider, idx: u8, h: Provider.Handle, st: NodeStat) Info {
return .{
.is_dir = st.isDir(),
@@ -843,24 +806,23 @@ pub fn Server(comptime cfg: Config) type {
.atime = st.atime,
.mtime = st.mtime,
.version = st.version,
- .path = (@as(u64, idx) << 56) | ((if (st.path != 0) st.path else h) & handle_mask),
+ .path = provPath(idx, h, st),
.name = if (h == Provider.root) pr.name else st.name,
};
}
- /// Copies `st.name` into the connection so the reply cannot dangle.
- fn pinName(c: *Conn, st: cloud9.Stat) cloud9.Stat {
- var out = st;
- const n = @min(st.name.len, c.name_buf.len);
- @memcpy(c.name_buf[0..n], st.name[0..n]);
- out.name = c.name_buf[0..n];
- return out;
+ /// Releases a provider handle that no fid holds.
+ fn releaseRef(c: *Conn, ref: NodeRef) void {
+ if (ref == .prov) {
+ const p = c.shared.providers[ref.prov.idx];
+ p.vtable.clunk(p.ctx, ref.prov.h);
+ }
}
const Looked = struct { ref: NodeRef, info: Info };
/// Resolves `name` in the directory `ref`. A returned provider ref
- /// is a fresh handle the caller must release or retain.
+ /// is a fresh handle the engine will release exactly once.
fn lookup(c: *Conn, ref: NodeRef, name: []const u8) !Looked {
switch (ref) {
.static => |idx| {
@@ -909,34 +871,39 @@ pub fn Server(comptime cfg: Config) type {
}
}
- /// The i-th entry of directory `ref` as a Stat, or null past the end.
- /// The name borrows either static memory or the provider's NodeStat.
- fn entryStat(c: *Conn, ref: NodeRef, i: usize) !?cloud9.Stat {
+ /// The i-th entry of directory `ref`, or null past the end. The
+ /// name borrows static memory or the provider's NodeStat.
+ fn entry(c: *Conn, ref: NodeRef, i: usize) !?Entry {
switch (ref) {
.static => |idx| {
const f = flat[idx];
if (f.node.kind == .vars) {
if (i >= c.shared.nvars) return null;
- return (try c.info(.{ .@"var" = .{ .idx = @intCast(i), .node = 0 } })).stat();
+ return .{ .path = var_tag | (@as(u64, i) << 32), .dir = true, .name = c.shared.vars[i].name };
+ }
+ if (i < f.count) {
+ const ci = f.first + @as(u32, @intCast(i));
+ return .{ .path = static_tag | ci, .dir = flat[ci].node.isDir(), .name = flat[ci].node.name };
}
- if (i < f.count) return (try c.info(.{ .static = f.first + @as(u32, @intCast(i)) })).stat();
if (idx == 0) {
const pi = i - f.count;
if (pi >= c.shared.nprov) return null;
- return (try c.info(.{ .prov = .{ .idx = @intCast(pi), .h = Provider.root } })).stat();
+ return .{ .path = @as(u64, pi) << 56, .dir = true, .name = c.shared.providers[pi].name };
}
return null;
},
.@"var" => |v| {
- const n = c.shared.vars[v.idx].vt.nodes[v.node];
+ const vt = c.shared.vars[v.idx].vt;
+ const n = vt.nodes[v.node];
if (i >= n.count) return null;
- return (try c.info(.{ .@"var" = .{ .idx = v.idx, .node = n.first + @as(u32, @intCast(i)) } })).stat();
+ const ci = n.first + @as(u32, @intCast(i));
+ return .{ .path = var_tag | (@as(u64, v.idx) << 32) | ci, .dir = vt.nodes[ci].isDir(), .name = vt.nodes[ci].name };
},
.prov => |p| {
const pr = c.provider(p.idx);
var st: NodeStat = .{ .mode = 0 };
if (!try pr.vtable.list(pr.ctx, p.h, i, &st)) return null;
- return provInfo(pr, p.idx, st.handle, st).stat();
+ return .{ .path = provPath(p.idx, st.handle, st), .dir = st.isDir(), .name = st.name };
},
}
}
@@ -951,12 +918,11 @@ pub fn Server(comptime cfg: Config) type {
return error.NoSnapshot;
}
- /// (Re)generates the content of a dynamic file into its slot.
- fn generate(c: *Conn, f: *Fid) !void {
- const s = f.snap.?;
+ /// (Re)generates the content of a dynamic file into slot `s`.
+ fn generate(c: *Conn, ref: NodeRef, s: u8) !void {
var w: Writer = .fixed(&c.storage.snapshots[s]);
c.slot_len[s] = 0;
- switch (f.node) {
+ switch (ref) {
.static => |idx| try flat[idx].node.gen.?(c.shared.ctx, &w),
.@"var" => |v| {
const n = c.shared.vars[v.idx].vt.nodes[v.node];
@@ -983,162 +949,85 @@ pub fn Server(comptime cfg: Config) type {
};
}
- // -- request handlers --
-
- fn attach(c: *Conn, m: anytype) !cloud9.Msg {
- const f = try c.allocFid(m.fid);
- f.node = .{ .static = 0 };
- f.is_dir = true;
- return .{ .rattach = .{ .qid = (try c.info(f.node)).qid() } };
+ fn snapshot(c: *Conn, handle: u32) []const u8 {
+ return c.storage.snapshots[handle][0..c.slot_len[handle]];
}
- fn walk(c: *Conn, m: anytype) !cloud9.Msg {
- const f = c.findFid(m.fid) orelse return error.UnknownFid;
- if (m.newfid != m.fid and c.findFid(m.newfid) != null) return error.FidInUse;
- if (m.newfid != m.fid and c.nfids >= cfg.max_fids) return error.TooManyFids;
- if (m.nwname > 0 and f.open) return error.AlreadyOpen;
- // Cloning a fid onto itself changes nothing; in particular it must not
- // close an open fid or discard generated content.
- if (m.nwname == 0 and m.newfid == m.fid) return .{ .rwalk = .{ .nwqid = 0 } };
- // A full Rwalk must fit the negotiated msize; check before binding anything.
- if (cloud9.header_len + 2 + cloud9.qid_len * @as(usize, m.nwname) > c.server.msize) return error.ReplyTooLarge;
- var cur = f.node;
- var cur_is_dir = f.is_dir;
- var held = false; // cur is a provider handle obtained here, not the fid's
- var reply: cloud9.Msg = .{ .rwalk = .{ .nwqid = 0 } };
- const names = m.wname[0..m.nwname];
- for (names, 0..) |name, i| {
- if (!cur_is_dir) {
- if (i == 0) return error.NotDir;
- break;
- }
- const next = c.lookup(cur, name) catch |e| {
- if (i == 0) return e;
- break;
- };
- if (held) c.releaseRef(cur);
- cur = next.ref;
- cur_is_dir = next.info.is_dir;
- held = cur == .prov;
- reply.rwalk.wqid[i] = next.info.qid();
- reply.rwalk.nwqid += 1;
- }
- if (reply.rwalk.nwqid != names.len) {
- if (held) c.releaseRef(cur);
- return reply;
- }
- if (names.len == 0 and cur == .prov) {
- // A clone of a provider handle needs its own reference.
- const dup = try c.lookup(cur, ".");
- cur = dup.ref;
- cur_is_dir = dup.info.is_dir;
- held = true;
- }
- const target = if (m.newfid == m.fid) f else c.allocFid(m.newfid) catch |e| {
- if (held) c.releaseRef(cur);
- return e;
- };
- if (target == f) c.dropContents(f);
- target.node = cur;
- target.is_dir = cur_is_dir;
- return reply;
- }
+ // -- request handlers --
- fn open(c: *Conn, m: anytype) !cloud9.Msg {
- const f = c.findFid(m.fid) orelse return error.UnknownFid;
- if (f.open) return error.AlreadyOpen;
- const acc = m.mode & 3;
+ fn open(c: *Conn, req: fs.Req, ref: NodeRef) !fs.Reply {
+ const mode = req.omode;
+ const acc = mode & 3;
const want_write = acc == cloud9.owrite or acc == cloud9.ordwr;
- const trunc = m.mode & cloud9.otrunc != 0;
- if (f.is_dir and (want_write or trunc)) return error.IsDir;
- switch (f.node) {
+ const trunc = mode & cloud9.otrunc != 0;
+ switch (ref) {
.static => |idx| switch (flat[idx].node.kind) {
.dir, .vars, .ctl => {},
.static, .dynamic => if (want_write or trunc) return error.Perm,
},
.@"var" => |v| {
const n = c.shared.vars[v.idx].vt.nodes[v.node];
- if ((want_write or trunc) and !n.writable()) return error.Perm;
+ if (n.isDir()) {
+ if (want_write or trunc) return error.IsDir;
+ } else if ((want_write or trunc) and !n.writable()) return error.Perm;
},
.prov => |p| {
const pr = c.provider(p.idx);
- try pr.vtable.open(pr.ctx, p.h, m.mode);
+ try pr.vtable.open(pr.ctx, p.h, mode);
},
}
- const qid = (c.info(f.node) catch |e| {
+ const i = c.info(ref) catch |e| {
// The provider's open succeeded but its stat did not: undo the open.
- if (f.node == .prov) {
- const pr = c.provider(f.node.prov.idx);
- if (pr.vtable.close) |close| close(pr.ctx, f.node.prov.h);
+ if (ref == .prov) {
+ const pr = c.provider(ref.prov.idx);
+ if (pr.vtable.close) |close| close(pr.ctx, ref.prov.h);
}
return e;
- }).qid();
- if (c.isDynamic(f.node)) {
- f.snap = try c.takeSlot();
- c.generate(f) catch |e| {
- c.slot_used[f.snap.?] = false;
- f.snap = null;
- if (f.node == .prov) unreachable;
+ };
+ var handle: u32 = no_snapshot;
+ if (c.isDynamic(ref)) {
+ const s = try c.takeSlot();
+ c.generate(ref, s) catch |e| {
+ c.slot_used[s] = false;
return e;
};
+ handle = s;
}
- f.open = true;
- f.mode = m.mode;
- f.rclose = m.mode & cloud9.orclose != 0;
- f.dir_offset = 0;
- f.dir_index = 0;
- return .{ .ropen = .{ .qid = qid, .iounit = 0 } };
+ return .{ .tag = req.tag, .attr = i.attr(ref), .handle = handle };
}
- fn create(c: *Conn, m: anytype) !cloud9.Msg {
- const f = c.findFid(m.fid) orelse return error.UnknownFid;
- if (f.open) return error.AlreadyOpen;
- const p = switch (f.node) {
+ fn create(c: *Conn, req: fs.Req, ref: NodeRef) !fs.Reply {
+ const p = switch (ref) {
.prov => |p| p,
else => return error.Perm,
};
- if (!f.is_dir) return error.NotDir;
const pr = c.provider(p.idx);
const create_fn = pr.vtable.create orelse return error.Perm;
- try validName(m.name);
- const is_dir = m.perm & cloud9.dmdir != 0;
- const acc = m.mode & 3;
- if (is_dir and (acc != cloud9.oread or m.mode & cloud9.otrunc != 0)) return error.IsDir;
- const h = try create_fn(pr.ctx, p.h, m.name, m.perm, m.mode);
+ try validName(req.data);
+ const h = try create_fn(pr.ctx, p.h, req.data, req.perm, req.omode);
const node: NodeRef = .{ .prov = .{ .idx = p.idx, .h = h } };
- const qid = (c.info(node) catch |e| {
+ const i = c.info(node) catch |e| {
if (pr.vtable.close) |close| close(pr.ctx, h);
pr.vtable.clunk(pr.ctx, h);
return e;
- }).qid();
- c.dropContents(f);
- f.node = node;
- f.is_dir = is_dir;
- f.open = true;
- f.mode = m.mode;
- f.rclose = m.mode & cloud9.orclose != 0;
- f.dir_offset = 0;
- f.dir_index = 0;
- return .{ .rcreate = .{ .qid = qid, .iounit = 0 } };
+ };
+ return .{ .tag = req.tag, .attr = i.attr(node), .handle = no_snapshot };
}
- fn read(c: *Conn, m: anytype) !cloud9.Msg {
- const f = c.findFid(m.fid) orelse return error.UnknownFid;
- if (!f.open or (f.mode & 3) == cloud9.owrite) return error.NotOpen;
- const count: usize = @min(m.count, c.server.msize -| cloud9.iohdrsz, c.storage.data.len);
- if (f.is_dir) return c.readDir(f, m.offset, count);
+ fn read(c: *Conn, req: fs.Req, ref: NodeRef, bytes: *[]const u8) !fs.Reply {
const data = &c.storage.data;
- const src: []const u8 = switch (f.node) {
+ const count: usize = @min(req.size, data.len);
+ const src: []const u8 = switch (ref) {
.static => |idx| blk: {
const n = flat[idx].node;
switch (n.kind) {
.static => break :blk n.content,
.ctl => break :blk c.shared.ctlResult(),
.dynamic => {
- if (m.offset == 0) try c.generate(f);
- break :blk c.storage.snapshots[f.snap.?][0..c.slot_len[f.snap.?]];
+ if (req.off == 0) try c.generate(ref, @intCast(req.handle));
+ break :blk c.snapshot(req.handle);
},
- .dir, .vars => unreachable,
+ .dir, .vars => return error.IsDir,
}
},
.@"var" => |v| blk: {
@@ -1146,72 +1035,75 @@ pub fn Server(comptime cfg: Config) type {
switch (n.kind) {
.type_name, .size => break :blk n.content,
.value, .addr => {
- if (m.offset == 0) try c.generate(f);
- break :blk c.storage.snapshots[f.snap.?][0..c.slot_len[f.snap.?]];
+ if (req.off == 0) try c.generate(ref, @intCast(req.handle));
+ break :blk c.snapshot(req.handle);
},
.raw => break :blk c.varBase(v.idx, v.node)[0..n.size],
- .dir, .fields => unreachable,
+ .dir, .fields => return error.IsDir,
}
},
.prov => |p| {
const pr = c.provider(p.idx);
- const n = try pr.vtable.read(pr.ctx, p.h, m.offset, data[0..count]);
- return .{ .rread = .{ .data = data[0..@min(n, count)] } };
+ const n = try pr.vtable.read(pr.ctx, p.h, req.off, data[0..count]);
+ bytes.* = data[0..@min(n, count)];
+ return .{ .tag = req.tag };
},
};
- if (m.offset >= src.len) return .{ .rread = .{ .data = "" } };
- const off: usize = @intCast(m.offset);
+ if (req.off >= src.len) return .{ .tag = req.tag };
+ const off: usize = @intCast(req.off);
const n = @min(count, src.len - off);
- if (f.node == .@"var" and c.shared.vars[f.node.@"var".idx].vt.nodes[f.node.@"var".node].kind == .raw) {
+ if (ref == .@"var" and c.shared.vars[ref.@"var".idx].vt.nodes[ref.@"var".node].kind == .raw) {
// Copy out of the variable so the reply does not read live memory twice.
@memcpy(data[0..n], src[off..][0..n]);
- return .{ .rread = .{ .data = data[0..n] } };
+ bytes.* = data[0..n];
+ } else {
+ bytes.* = src[off..][0..n];
}
- return .{ .rread = .{ .data = src[off..][0..n] } };
+ return .{ .tag = req.tag };
}
- fn readDir(c: *Conn, f: *Fid, offset: u64, count: usize) !cloud9.Msg {
- if (offset == 0) {
- f.dir_offset = 0;
- f.dir_index = 0;
- } else if (offset != f.dir_offset) return error.BadOffset;
+ /// Stages `node:u64le dir:u8 len:u8 name` records from entry
+ /// `req.off` on, about as many as the engine can fit in `req.size`.
+ fn readDir(c: *Conn, req: fs.Req, ref: NodeRef, bytes: *[]const u8) !fs.Reply {
const data = &c.storage.data;
- var used: usize = 0;
- var i = f.dir_index;
- while (try c.entryStat(f.node, i)) |st| : (i += 1) {
- const rec = st.encode(data[used..count]) catch |e| switch (e) {
- error.NoSpace => break,
- else => return error.Io,
- };
- used += rec.len;
+ const who = c.engine.uname_len;
+ var n: usize = 0;
+ var est: usize = 0;
+ var i: usize = @intCast(req.off);
+ while (est < req.size) : (i += 1) {
+ const e = (try c.entry(ref, i)) orelse break;
+ if (e.name.len > 255 or n + 10 + e.name.len > data.len) break;
+ std.mem.writeInt(u64, data[n..][0..8], e.path, .little);
+ data[n + 8] = @intFromBool(e.dir);
+ data[n + 9] = @intCast(e.name.len);
+ @memcpy(data[n + 10 ..][0..e.name.len], e.name);
+ n += 10 + e.name.len;
+ est += cloud9.stat_fixed + 2 + e.name.len + 3 * who;
}
- f.dir_offset += used;
- f.dir_index = i;
- return .{ .rread = .{ .data = data[0..used] } };
+ bytes.* = data[0..n];
+ return .{ .tag = req.tag };
}
- fn write(c: *Conn, m: anytype) !cloud9.Msg {
- const f = c.findFid(m.fid) orelse return error.UnknownFid;
- const acc = f.mode & 3;
- if (!f.open or (acc != cloud9.owrite and acc != cloud9.ordwr)) return error.NotOpen;
- if (f.is_dir) return error.IsDir;
- switch (f.node) {
+ fn write(c: *Conn, req: fs.Req, ref: NodeRef) !fs.Reply {
+ switch (ref) {
.static => |idx| switch (flat[idx].node.kind) {
- .ctl => try c.ctlCommand(m.data),
+ .ctl => try c.ctlCommand(req.data),
+ .dir, .vars => return error.IsDir,
else => return error.Perm,
},
.@"var" => |v| {
const n = c.shared.vars[v.idx].vt.nodes[v.node];
+ if (n.isDir()) return error.IsDir;
const set = n.set orelse return error.Perm;
- try set(c.varBase(v.idx, v.node), m.data);
+ try set(c.varBase(v.idx, v.node), req.data);
},
.prov => |p| {
const pr = c.provider(p.idx);
- const n = try pr.vtable.write(pr.ctx, p.h, m.offset, m.data);
- return .{ .rwrite = .{ .count = @intCast(@min(n, m.data.len)) } };
+ const n = try pr.vtable.write(pr.ctx, p.h, req.off, req.data);
+ return .{ .tag = req.tag, .written = @intCast(@min(n, req.data.len)) };
},
}
- return .{ .rwrite = .{ .count = @intCast(m.data.len) } };
+ return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
/// Runs `cfg.ctl`; on success its output becomes the ctl result.
@@ -1227,60 +1119,45 @@ pub fn Server(comptime cfg: Config) type {
s.ctl_version +%= 1;
}
- fn clunk(c: *Conn, m: anytype) !cloud9.Msg {
- const f = c.findFid(m.fid) orelse return error.UnknownFid;
- c.freeFid(f);
- return .rclunk;
- }
-
- fn remove(c: *Conn, m: anytype) !cloud9.Msg {
- const f = c.findFid(m.fid) orelse return error.UnknownFid;
- defer c.freeFid(f); // Tremove always clunks
- f.rclose = false;
- switch (f.node) {
- .prov => |p| {
- const pr = c.provider(p.idx);
- const rm = pr.vtable.remove orelse return error.Perm;
- try rm(pr.ctx, p.h);
- },
- else => return error.Perm,
- }
- return .rremove;
- }
-
- fn stat(c: *Conn, m: anytype) !cloud9.Msg {
- const f = c.findFid(m.fid) orelse return error.UnknownFid;
- return .{ .rstat = .{ .stat = c.pinName((try c.info(f.node)).stat()) } };
- }
-
- fn wstat(c: *Conn, m: anytype) !cloud9.Msg {
- const f = c.findFid(m.fid) orelse return error.UnknownFid;
- const p = switch (f.node) {
+ /// A wstat, or the truncation hint of an OTRUNC open (which the
+ /// provider's `open` performs itself; nothing to do here).
+ fn setattr(c: *Conn, req: fs.Req, ref: NodeRef) !fs.Reply {
+ if (!req.set.any()) return .{ .tag = req.tag };
+ const p = switch (ref) {
.prov => |p| p,
else => return error.Perm,
};
const pr = c.provider(p.idx);
const ws = pr.vtable.wstat orelse return error.Perm;
- const cur = try c.info(f.node);
- const st = m.stat;
- const q = cur.qid();
- // Fields we cannot change must be "don't care" or unchanged.
- if (st.type != 0xFFFF and st.type != 0) return error.Perm;
- if (st.dev != 0xFFFF_FFFF and st.dev != 0) return error.Perm;
- if (st.qid.type != 0xFF and st.qid.type != q.type) return error.Perm;
- if (st.qid.version != 0xFFFF_FFFF and st.qid.version != q.version) return error.Perm;
- if (st.qid.path != 0xFFFF_FFFF_FFFF_FFFF and st.qid.path != q.path) return error.Perm;
- if (st.uid.len != 0 and !std.mem.eql(u8, st.uid, cfg.name)) return error.Perm;
- if (st.gid.len != 0 and !std.mem.eql(u8, st.gid, cfg.name)) return error.Perm;
- if (st.muid.len != 0 and !std.mem.eql(u8, st.muid, cfg.name)) return error.Perm;
- if (st.name.len != 0 and !std.mem.eql(u8, st.name, cur.name)) {
- if (p.h == Provider.root) return error.Perm;
- try validName(st.name);
- }
- if (st.length != 0xFFFF_FFFF_FFFF_FFFF and st.length != cur.length and cur.is_dir) return error.IsDir;
- if (st.mode != 0xFFFF_FFFF and (st.mode & cloud9.dmdir) != (cur.mode & cloud9.dmdir)) return error.Perm;
+ if (req.set.name and p.h == Provider.root) return error.Perm;
+ var st = stat_dontcare;
+ if (req.set.name) st.name = req.data;
+ if (req.set.mode) st.mode = req.perm;
+ if (req.set.mtime) st.mtime = req.mtime;
+ if (req.set.length) st.length = req.length;
try ws(pr.ctx, p.h, &st);
- return .rwstat;
+ return .{ .tag = req.tag, .attr = (try c.info(ref)).attr(ref) };
+ }
+
+ /// The engine lets go of a reference: closes the open handle it
+ /// came with, removes the node on Tremove or ORCLOSE, and clunks
+ /// the provider handle exactly once.
+ fn release(c: *Conn, req: fs.Req, ref: NodeRef) !fs.Reply {
+ if (req.opened and req.handle != no_snapshot) c.slot_used[req.handle] = false;
+ switch (ref) {
+ .prov => |p| {
+ const pr = c.provider(p.idx);
+ if (req.opened) if (pr.vtable.close) |close| close(pr.ctx, p.h);
+ var result: anyerror!void = {};
+ if (req.remove) {
+ if (pr.vtable.remove) |rm| result = rm(pr.ctx, p.h) else result = error.Perm;
+ }
+ pr.vtable.clunk(pr.ctx, p.h);
+ try result;
+ },
+ else => if (req.remove) return error.Perm,
+ }
+ return .{ .tag = req.tag };
}
};
@@ -1321,29 +1198,35 @@ pub fn Server(comptime cfg: Config) type {
try testing.expectEqualStrings("9P2000", v.version.version);
}
- /// One round trip; the result borrows the client input buffer until the next call.
- pub fn rpc(h: *Harness, req: cloud9.Client.Request) !cloud9.Client.Result {
- _ = try h.client.submit(req);
+ /// Moves bytes both ways and steps the connection until nothing
+ /// moves; true when anything did.
+ pub fn pump(h: *Harness) !bool {
+ var moved = false;
while (true) {
- var moved = false;
+ var again = false;
while (h.client.output().len > 0) {
const k = h.conn.push(h.client.output());
- h.client.wrote(k);
- moved = moved or k > 0;
- while (try h.conn.step()) {}
- while (h.conn.output().len > 0) {
- const n = h.client.push(h.conn.output());
- h.conn.wrote(n);
- moved = moved or n > 0;
- }
if (k == 0) break;
+ h.client.wrote(k);
+ again = true;
}
- while (try h.conn.step()) {}
+ while (try h.conn.step()) again = true;
while (h.conn.output().len > 0) {
const n = h.client.push(h.conn.output());
+ if (n == 0) break;
h.conn.wrote(n);
- moved = moved or n > 0;
+ again = true;
}
+ if (!again) return moved;
+ moved = true;
+ }
+ }
+
+ /// One round trip; the result borrows the client input buffer until the next call.
+ pub fn rpc(h: *Harness, req: cloud9.Client.Request) !cloud9.Client.Result {
+ _ = try h.client.submit(req);
+ while (true) {
+ const moved = try h.pump();
if (h.client.take()) |done| return done.result;
if (!moved) return error.Stuck;
}
@@ -1449,6 +1332,21 @@ pub fn Server(comptime cfg: Config) type {
const testing = std.testing;
+// The engine's own Rerror strings, for the conditions it decides itself.
+const e_unknown_fid = fs.e_unknown_fid;
+const e_fid_in_use = fs.e_fid_in_use;
+const e_too_many_fids = fs.e_too_many_fids;
+const e_bad_use = fs.e_bad_use;
+const e_already_open = fs.e_already_open;
+const e_bad_offset = fs.e_bad_offset;
+const e_perm = fs.e_perm;
+const e_not_dir = fs.e_not_dir;
+const e_wstat = fs.e_wstat;
+const e_illegal_name = fs.e_illegal_name;
+const e_small_msize = fs.e_small_msize;
+const e_count_small = fs.e_count_small;
+const e_interrupted = fs.e_interrupted;
+
const TestBuild = struct {
pub const zig_version: []const u8 = builtin.zig_version_string;
pub const target: []const u8 = "test-target";
@@ -1537,12 +1435,20 @@ const test_cfg: Config = .{
.max_vars = 4,
.snapshot_slots = 2,
.snapshot_bytes = 512,
+ .max_parked = 2,
};
const TS = Server(test_cfg);
+/// Whether `fid` is open, read from the engine's table.
+fn isOpen(c: *const TS.Conn, fid: u32) bool {
+ for (c.engine.fids) |f| if (f.used and !f.orphan and f.fid == fid) return f.open;
+ return false;
+}
+
/// A small in-memory provider: /prov/{hello,dir/{inner}} with create/remove/wstat,
-/// counting every handle reference so tests can check clunk discipline.
+/// counting every handle reference so tests can check clunk discipline. A
+/// read of `hello` parks (error.Again) while `park` is set.
const TestProv = struct {
const max_nodes = 16;
const Entry = struct {
@@ -1565,8 +1471,10 @@ const TestProv = struct {
nodes: [max_nodes]Entry = @splat(.{}),
total_refs: u32 = 0,
clunks: u32 = 0,
+ reads: u32 = 0,
fail_io: bool = false,
fail_stat: bool = false,
+ park: bool = false,
fn init() TestProv {
var p: TestProv = .{};
@@ -1664,6 +1572,8 @@ const TestProv = struct {
fn read(ctx: *anyopaque, h: Provider.Handle, offset: u64, buf: []u8) Provider.Error!usize {
const p = self(ctx);
const e = try p.node(h);
+ p.reads += 1;
+ if (p.park and h == 1) return error.Again;
if (offset >= e.len) return 0;
const n = @min(buf.len, e.len - @as(usize, @intCast(offset)));
@memcpy(buf[0..n], e.data[@intCast(offset)..][0..n]);
@@ -1799,16 +1709,21 @@ test "README, /build and the static tree read as expected" {
try testing.expectEqual(@as(usize, 7), names.len);
// static files are read-only; the static tree admits no creates or removes
try x.h.walkTo(1, &.{ "build", "target" });
- try x.h.expectFail(.{ .open = .{ .fid = 1, .mode = cloud9.owrite } }, "permission denied");
+ try x.h.expectFail(.{ .open = .{ .fid = 1, .mode = cloud9.owrite } }, e_perm);
try x.h.expectFail(.{ .remove = .{ .fid = 1 } }, "permission denied");
try x.h.walkTo(2, &.{"build"});
- try x.h.expectFail(.{ .create = .{ .fid = 2, .name = "nope", .perm = 0o644, .mode = cloud9.owrite } }, "permission denied");
- try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = stat_dontcare } }, "permission denied");
+ try x.h.expectFail(.{ .create = .{ .fid = 2, .name = "nope", .perm = 0o644, .mode = cloud9.owrite } }, e_perm);
+ // a wstat that changes nothing is answered by the engine without asking
+ _ = try x.h.ok(.{ .wstat = .{ .fid = 2, .stat = stat_dontcare } });
+ var ws = stat_dontcare;
+ ws.mtime = 5;
+ try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, "permission denied");
const st = try x.h.ok(.{ .stat = .{ .fid = 2 } });
try testing.expectEqualStrings("build", st.stat.name);
try testing.expectEqualStrings("tester", st.stat.uid);
try testing.expect(st.stat.qid.type & cloud9.qtdir != 0);
try testing.expectEqual(TS.build_secs, st.stat.mtime);
+ try testing.expectEqual(TS.build_secs, st.stat.atime);
try testing.expectEqual(@as(u32, 0), parseIso8601("1970-01-01T00:00:00Z").?);
try testing.expectEqual(@as(?u32, null), parseIso8601("unknown"));
}
@@ -1873,7 +1788,7 @@ test "runtime/fn calls the function at open and at each read from offset 0" {
try x.h.walkTo(4, &.{ "runtime", "fn", "fib30" });
_ = try x.h.ok(.{ .open = .{ .fid = 4, .mode = cloud9.oread } });
_ = try x.h.ok(.{ .clunk = .{ .fid = 1 } });
- _ = try x.h.ok(.{ .walk = .{ .fid = 4, .newfid = 5, .names = &.{} } });
+ try x.h.walkTo(5, &.{ "runtime", "fn", "fib30" });
_ = try x.h.ok(.{ .open = .{ .fid = 5, .mode = cloud9.oread } });
}
@@ -1895,14 +1810,13 @@ test "snapshot slot exhaustion is an Rerror and clunk frees the slot" {
_ = try x.h.ok(.{ .open = .{ .fid = 3, .mode = cloud9.oread } });
const r = try x.h.ok(.{ .read = .{ .fid = 3, .offset = 0, .count = 100 } });
try testing.expectEqualStrings("7", r.read);
- // cloning an open fid onto itself keeps it open and its content
- const w = try x.h.ok(.{ .walk = .{ .fid = 3, .newfid = 3, .names = &.{} } });
- try testing.expectEqual(@as(u16, 0), w.walk.nwqid);
+ // an open fid cannot be walked from, not even cloned, and stays open
+ try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 3, .names = &.{} } }, e_bad_use);
+ try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{} } }, e_bad_use);
+ try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{".."} } }, e_bad_use);
const r2 = try x.h.ok(.{ .read = .{ .fid = 3, .offset = 0, .count = 100 } });
try testing.expectEqualStrings("7", r2.read);
- try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{".."} } }, "file already open");
- _ = try x.h.ok(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{} } });
- try x.h.expectFail(.{ .read = .{ .fid = 4, .offset = 0, .count = 100 } }, "file not open");
+ try x.h.expectFail(.{ .read = .{ .fid = 4, .offset = 0, .count = 100 } }, e_unknown_fid);
}
test "ctl round trip" {
@@ -1946,7 +1860,7 @@ test "ctl round trip" {
try testing.expectEqual(@as(u64, 0), (try x.h.ok(.{ .stat = .{ .fid = 1 } })).stat.length);
// Tversion resets the ctl fid like any other
try x.h.version(4096);
- try x.h.expectFail(.{ .clunk = .{ .fid = 1 } }, "unknown fid");
+ try x.h.expectFail(.{ .clunk = .{ .fid = 1 } }, e_unknown_fid);
}
test "auth is not required and flush is answered" {
@@ -1956,7 +1870,7 @@ test "auth is not required and flush is answered" {
try x.h.expectFail(.{ .auth = .{ .afid = 5, .uname = "tester" } }, "authentication not required");
const f = try x.h.ok(.{ .flush = .{ .oldtag = 1 } });
try testing.expect(f == .flush);
- try x.h.expectFail(.{ .attach = .{ .fid = 0, .uname = "tester" } }, "fid in use");
+ try x.h.expectFail(.{ .attach = .{ .fid = 0, .uname = "tester" } }, e_fid_in_use);
}
test "vars: value/type/size/addr/raw, fields and writes" {
@@ -2027,24 +1941,24 @@ test "vars: value/type/size/addr/raw, fields and writes" {
try testing.expectEqualStrings("value", a_st.stat.name);
// non-scalar values, type/size/addr/raw and directories are read-only
try x.h.walkTo(3, &.{ "vars", "state", "value" });
- try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.owrite } }, "permission denied");
+ try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.owrite } }, e_perm);
_ = try x.h.ok(.{ .clunk = .{ .fid = 3 } });
try x.h.walkTo(4, &.{ "vars", "state", "f", "name", "value" });
- try x.h.expectFail(.{ .open = .{ .fid = 4, .mode = cloud9.owrite } }, "permission denied");
+ try x.h.expectFail(.{ .open = .{ .fid = 4, .mode = cloud9.owrite } }, e_perm);
_ = try x.h.ok(.{ .clunk = .{ .fid = 4 } });
try x.h.walkTo(5, &.{ "vars", "state" });
- try x.h.expectFail(.{ .open = .{ .fid = 5, .mode = cloud9.owrite } }, "is a directory");
- try x.h.expectFail(.{ .create = .{ .fid = 5, .name = "z", .perm = 0o644, .mode = cloud9.owrite } }, "permission denied");
+ try x.h.expectFail(.{ .open = .{ .fid = 5, .mode = cloud9.owrite } }, e_perm);
+ try x.h.expectFail(.{ .create = .{ .fid = 5, .name = "z", .perm = 0o644, .mode = cloud9.owrite } }, e_perm);
// .. climbs back out of the var tree; unknown names fail
const up = try x.h.ok(.{ .walk = .{ .fid = 5, .newfid = 6, .names = &.{ "f", "inner", "..", "..", "..", "..", "README" } } });
try testing.expectEqual(@as(u16, 7), up.walk.nwqid);
_ = try x.h.ok(.{ .clunk = .{ .fid = 6 } });
try x.h.walkTo(7, &.{"vars"});
try x.h.expectFail(.{ .walk = .{ .fid = 7, .newfid = 8, .names = &.{"nope"} } }, "file does not exist");
- try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = 8, .names = &.{"x"} } }, "file already open");
+ try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = 8, .names = &.{"x"} } }, e_bad_use);
_ = try x.h.ok(.{ .clunk = .{ .fid = 2 } });
try x.h.walkTo(2, &.{ "vars", "state", "f", "a", "value" });
- try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = 8, .names = &.{"x"} } }, "not a directory");
+ try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = 8, .names = &.{"x"} } }, e_not_dir);
}
test "provider: walk/list/stat/open/read/write/create/remove/wstat/clunk and error mapping" {
@@ -2083,13 +1997,13 @@ test "provider: walk/list/stat/open/read/write/create/remove/wstat/clunk and err
_ = try x.h.ok(.{ .clunk = .{ .fid = 1 } });
try testing.expectEqual(@as(u32, 0), x.prov.nodes[3].opens);
try testing.expectEqual(@as(u32, 0), x.prov.nodes[3].refs);
- // permission and kind errors come from the provider
+ // permission and kind errors: the engine's from the walked mode, the provider's as its own strings
try x.h.walkTo(3, &.{ "prov", "locked" });
- try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }, "permission denied");
+ try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }, e_perm);
try x.h.walkTo(4, &.{ "prov", "hello" });
- try x.h.expectFail(.{ .walk = .{ .fid = 4, .newfid = 5, .names = &.{"x"} } }, "not a directory");
+ try x.h.expectFail(.{ .walk = .{ .fid = 4, .newfid = 5, .names = &.{"x"} } }, e_not_dir);
try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = 5, .names = &.{"missing"} } }, "file does not exist");
- try x.h.expectFail(.{ .open = .{ .fid = 2, .mode = cloud9.owrite } }, "is a directory");
+ try x.h.expectFail(.{ .open = .{ .fid = 2, .mode = cloud9.owrite } }, e_perm);
// create in a provider directory: the fid becomes the new open file
const cr = try x.h.ok(.{ .create = .{ .fid = 2, .name = "new", .perm = 0o644, .mode = cloud9.ordwr } });
try testing.expectEqual(cloud9.qtfile, cr.create.qid.type);
@@ -2099,11 +2013,11 @@ test "provider: walk/list/stat/open/read/write/create/remove/wstat/clunk and err
try x.h.walkTo(6, &.{"prov"});
try x.h.expectFail(.{ .create = .{ .fid = 6, .name = "new", .perm = 0o644, .mode = cloud9.oread } }, "file already exists");
const long_name = [_]u8{'n'} ** (max_name + 1);
- try x.h.expectFail(.{ .create = .{ .fid = 6, .name = &long_name, .perm = 0o644, .mode = cloud9.oread } }, "bad file name");
- try x.h.expectFail(.{ .create = .{ .fid = 6, .name = "d", .perm = cloud9.dmdir | 0o755, .mode = cloud9.owrite } }, "is a directory");
+ try x.h.expectFail(.{ .create = .{ .fid = 6, .name = &long_name, .perm = 0o644, .mode = cloud9.oread } }, e_illegal_name);
+ try x.h.expectFail(.{ .create = .{ .fid = 6, .name = "d", .perm = cloud9.dmdir | 0o755, .mode = cloud9.owrite } }, e_perm);
const dr = try x.h.ok(.{ .create = .{ .fid = 6, .name = "d", .perm = cloud9.dmdir | 0o755, .mode = cloud9.oread } });
try testing.expectEqual(cloud9.qtdir, dr.create.qid.type);
- // wstat: rename, truncate, mode, mtime; immutable fields are refused
+ // wstat: rename, truncate, mode, mtime; immutable fields are refused by the engine
var ws = stat_dontcare;
ws.name = "renamed";
ws.length = 2;
@@ -2117,19 +2031,19 @@ test "provider: walk/list/stat/open/read/write/create/remove/wstat/clunk and err
try testing.expectEqual(@as(u32, 99), st2.stat.mtime);
ws = stat_dontcare;
ws.uid = "someone-else";
- try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, "permission denied");
+ try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, e_wstat);
ws = stat_dontcare;
ws.mode = cloud9.dmdir | 0o755;
- try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, "permission denied");
+ try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, e_wstat);
ws = stat_dontcare;
ws.name = "bad/name";
- try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, "bad file name");
+ try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, e_illegal_name);
ws.name = "..";
- try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, "bad file name");
+ try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, e_illegal_name);
ws = stat_dontcare;
ws.length = 5;
try x.h.walkTo(7, &.{ "prov", "d" });
- try x.h.expectFail(.{ .wstat = .{ .fid = 7, .stat = ws } }, "is a directory");
+ try x.h.expectFail(.{ .wstat = .{ .fid = 7, .stat = ws } }, e_wstat);
ws = stat_dontcare;
ws.name = "root2"; // the provider root cannot be renamed
try x.h.walkTo(14, &.{"prov"});
@@ -2139,7 +2053,7 @@ test "provider: walk/list/stat/open/read/write/create/remove/wstat/clunk and err
_ = try x.h.ok(.{ .clunk = .{ .fid = 6 } });
try x.h.walkTo(8, &.{ "prov", "dir" });
try x.h.expectFail(.{ .remove = .{ .fid = 8 } }, "directory not empty");
- try x.h.expectFail(.{ .clunk = .{ .fid = 8 } }, "unknown fid");
+ try x.h.expectFail(.{ .clunk = .{ .fid = 8 } }, e_unknown_fid);
_ = try x.h.ok(.{ .remove = .{ .fid = 2 } });
try x.h.walkTo(9, &.{"prov"});
try x.h.expectFail(.{ .walk = .{ .fid = 9, .newfid = 15, .names = &.{"renamed"} } }, "file does not exist");
@@ -2168,10 +2082,10 @@ test "provider: walk/list/stat/open/read/write/create/remove/wstat/clunk and err
// a partial walk releases the handles it obtained
const part = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 13, .names = &.{ "prov", "dir", "nope" } } });
try testing.expectEqual(@as(u16, 2), part.walk.nwqid);
- try x.h.expectFail(.{ .clunk = .{ .fid = 13 } }, "unknown fid");
+ try x.h.expectFail(.{ .clunk = .{ .fid = 13 } }, e_unknown_fid);
_ = try x.h.ok(.{ .clunk = .{ .fid = 12 } });
- for (x.h.conn.fids) |f| {
- if (f.used) _ = try x.h.ok(.{ .clunk = .{ .fid = f.id } });
+ for (x.h.conn.engine.fids) |f| {
+ if (f.used and !f.orphan) _ = try x.h.ok(.{ .clunk = .{ .fid = f.fid } });
}
try testing.expectEqual(@as(u32, 0), x.prov.total_refs);
}
@@ -2184,7 +2098,7 @@ test "directory reads across offsets, bad offset, and records never split" {
_ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } });
const first = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 4096 } });
try testing.expect(first.read.len > 0);
- try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 5, .count = 4096 } }, "bad offset");
+ try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 5, .count = 4096 } }, e_bad_offset);
// offset 0 restarts; the same bytes come back
const again = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 4096 } });
try testing.expectEqual(first.read.len, again.read.len);
@@ -2192,9 +2106,8 @@ test "directory reads across offsets, bad offset, and records never split" {
const names = try x.h.listDir(1, 80);
defer TS.Harness.freeNames(names);
try testing.expectEqual(@as(usize, 7), names.len);
- // a count too small for even one record returns nothing rather than splitting it
- const tiny = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 10 } });
- try testing.expectEqual(@as(usize, 0), tiny.read.len);
+ // a count too small for even one record is refused rather than splitting it
+ try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 0, .count = 10 } }, e_count_small);
// the same for provider and var directories
const pn = try x.h.listPath(&.{ "prov", "dir" });
defer TS.Harness.freeNames(pn);
@@ -2204,7 +2117,7 @@ test "directory reads across offsets, bad offset, and records never split" {
const vn = try x.h.listDir(2, 100);
defer TS.Harness.freeNames(vn);
try testing.expectEqual(@as(usize, 4), vn.len);
- try x.h.expectFail(.{ .read = .{ .fid = 2, .offset = 1, .count = 100 } }, "bad offset");
+ try x.h.expectFail(.{ .read = .{ .fid = 2, .offset = 1, .count = 100 } }, e_bad_offset);
}
test "Tversion mid-session resets fids and clunks every provider handle" {
@@ -2226,7 +2139,7 @@ test "Tversion mid-session resets fids and clunks every provider handle" {
try testing.expectEqual(@as(u32, 0), x.prov.nodes[3].opens);
try testing.expectEqual(before + 2, x.prov.clunks);
try testing.expect(!x.h.conn.slot_used[0] and !x.h.conn.slot_used[1]);
- try x.h.expectFail(.{ .clunk = .{ .fid = 1 } }, "unknown fid");
+ try x.h.expectFail(.{ .clunk = .{ .fid = 1 } }, e_unknown_fid);
_ = try x.h.ok(.{ .attach = .{ .fid = 0, .uname = "tester" } });
try x.h.walkTo(1, &.{ "prov", "hello" });
// hangup does the same
@@ -2235,24 +2148,34 @@ test "Tversion mid-session resets fids and clunks every provider handle" {
try testing.expectEqual(@as(usize, 0), x.h.conn.fidCount());
}
-test "a reply that does not fit msize is an Rerror, not a dead connection" {
+test "msize: below the engine's floor the connection dies; at the floor everything that fits is served" {
var x: Fixture = .{};
try x.init();
defer x.deinit();
- try x.h.version(64);
- _ = try x.h.ok(.{ .attach = .{ .fid = 0, .uname = "t" } });
- // Rstat of the root is ~70 bytes.
- try x.h.expectFail(.{ .stat = .{ .fid = 0 } }, "reply too large for msize");
- // Rwalk with 5 qids is 74 bytes; the walk must not bind newfid.
- try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{ ".", ".", ".", ".", "." } } }, "reply too large for msize");
- try x.h.expectFail(.{ .clunk = .{ .fid = 1 } }, "unknown fid");
- try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 0, .names = &.{ ".", ".", ".", ".", "." } } }, "reply too large for msize");
- const r = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{"README"} } });
- try testing.expectEqual(@as(u16, 1), r.walk.nwqid);
- _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } });
- const rd = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 40 } });
- try testing.expect(rd.read.len > 0 and rd.read.len <= 64 - cloud9.iohdrsz);
- _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } });
+ try testing.expectError(error.Protocol, x.h.version(64));
+
+ var y: Fixture = .{};
+ try y.init();
+ defer y.deinit();
+ try y.h.version(fs.msize_min);
+ _ = try y.h.ok(.{ .attach = .{ .fid = 0, .uname = "t" } });
+ const st = try y.h.ok(.{ .stat = .{ .fid = 0 } });
+ try testing.expectEqualStrings("/", st.stat.name);
+ // A full 16-element Rwalk is exactly msize_min.
+ const w = try y.h.ok(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &([_][]const u8{"."} ** 16) } });
+ try testing.expectEqual(@as(u16, 16), w.walk.nwqid);
+ _ = try y.h.ok(.{ .clunk = .{ .fid = 1 } });
+ // An Rstat that cannot fit is an Rerror, not a dead connection.
+ try y.h.walkTo(2, &.{"prov"});
+ const long_name = [_]u8{'n'} ** 180; // Tcreate fits; the Rstat (61 + 180 bytes) does not
+ _ = try y.h.ok(.{ .create = .{ .fid = 2, .name = &long_name, .perm = 0o644, .mode = cloud9.oread } });
+ try y.h.expectFail(.{ .stat = .{ .fid = 2 } }, e_small_msize);
+ _ = try y.h.ok(.{ .remove = .{ .fid = 2 } });
+ try y.h.walkTo(1, &.{"README"});
+ _ = try y.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } });
+ const rd = try y.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = fs.msize_min - cloud9.iohdrsz } });
+ try testing.expect(rd.read.len > 0 and rd.read.len <= fs.msize_min - cloud9.iohdrsz);
+ _ = try y.h.ok(.{ .clunk = .{ .fid = 1 } });
}
test "fid table is bounded per connection" {
@@ -2263,18 +2186,18 @@ test "fid table is bounded per connection" {
while (x.h.conn.fidCount() < test_cfg.max_fids) : (i += 1) {
_ = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = i, .names = &.{} } });
}
- try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = i, .names = &.{} } }, "too many fids");
- try x.h.expectFail(.{ .attach = .{ .fid = i, .uname = "tester" } }, "too many fids");
+ try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = i, .names = &.{} } }, e_too_many_fids);
+ try x.h.expectFail(.{ .attach = .{ .fid = i, .uname = "tester" } }, e_too_many_fids);
// self-walks and clunks still work at the limit
_ = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 0, .names = &.{"build"} } });
_ = try x.h.ok(.{ .clunk = .{ .fid = 1 } });
_ = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = i, .names = &.{} } });
- try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 2, .names = &.{"README"} } }, "fid in use");
- try x.h.expectFail(.{ .walk = .{ .fid = 1234, .newfid = 2, .names = &.{} } }, "unknown fid");
+ try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 2, .names = &.{"README"} } }, e_fid_in_use);
+ try x.h.expectFail(.{ .walk = .{ .fid = 1234, .newfid = 2, .names = &.{} } }, e_unknown_fid);
// a walk into a provider at the limit must not leak the handle
_ = try x.h.ok(.{ .clunk = .{ .fid = 2 } });
_ = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 2, .names = &.{ "..", "prov", "hello" } } });
- try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = i + 1, .names = &.{} } }, "too many fids");
+ try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = i + 1, .names = &.{} } }, e_too_many_fids);
try testing.expectEqual(@as(u32, 1), x.prov.total_refs);
}
@@ -2295,7 +2218,7 @@ test "Shared refuses more providers or vars than configured" {
try testing.expectError(error.Full, shared.expose("v4", &v[4]));
}
-/// A server with a large fid table for the index tests.
+/// A server with a large fid table for the churn test.
const big_cfg: Config = .{
.name = "big",
.msize = 8192,
@@ -2307,8 +2230,8 @@ const big_cfg: Config = .{
};
const BigS = Server(big_cfg);
-/// Fid numbers chosen to stress the index: dense low ids, ids with only high
-/// bits set, and ids counting down from 2^32-1 (all distinct for i < 2^20).
+/// Fid numbers chosen to stress the engine's index: dense low ids, ids with
+/// only high bits set, and ids counting down from 2^32-1 (all distinct for i < 2^20).
fn adversarialId(i: u32) u32 {
return switch (i % 3) {
0 => i * 8192 + 1,
@@ -2317,34 +2240,15 @@ fn adversarialId(i: u32) u32 {
};
}
-/// Every index bucket points at a used fid that finds itself, and every used
-/// fid is found: the invariant the hostile fid tests check after each phase.
-fn checkFidIndex(c: *BigS.Conn) !void {
- var indexed: usize = 0;
- for (c.index) |slot| {
- if (slot == BigS.no_slot) continue;
- indexed += 1;
- try testing.expect(c.fids[slot].used);
- try testing.expectEqual(&c.fids[slot], c.findFid(c.fids[slot].id).?);
- }
- var used: usize = 0;
- for (c.fids[0..c.high_water]) |*f| if (f.used) {
- used += 1;
- try testing.expectEqual(f, c.findFid(f.id).?);
- };
- for (c.fids[c.high_water..]) |*f| try testing.expect(!f.used);
- try testing.expectEqual(indexed, used);
- try testing.expectEqual(used, c.nfids);
-}
-
-test "fid index: thousands of fids, clunk in hostile orders, reuse, Tversion" {
+test "fid table: thousands of fids, clunk in hostile orders, reuse, Tversion" {
var ctx: TestCtx = .{};
var shared: BigS.Shared = .init(&ctx);
var prov = TestProv.init();
try shared.addProvider(prov.provider());
const storage = try testing.allocator.create(BigS.Storage);
defer testing.allocator.destroy(storage);
- var h: BigS.Harness = undefined;
+ const h = try testing.allocator.create(BigS.Harness);
+ defer testing.allocator.destroy(h);
try h.init(&shared, storage);
defer h.deinit();
const n: u32 = big_cfg.max_fids - 1; // fid 0 is the attach
@@ -2354,34 +2258,30 @@ test "fid index: thousands of fids, clunk in hostile orders, reuse, Tversion" {
}
try testing.expectEqual(@as(usize, n + 1), h.conn.fidCount());
try testing.expectEqual(n, prov.total_refs);
- try h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 0x7FFF_FFFF, .names = &.{} } }, "too many fids");
- try h.expectFail(.{ .walk = .{ .fid = 0, .newfid = adversarialId(5), .names = &.{} } }, "fid in use");
- try h.expectFail(.{ .attach = .{ .fid = adversarialId(7), .uname = "t" } }, "fid in use");
- try testing.expect(h.conn.findFid(0x7FFF_FFFF) == null);
- try testing.expect(h.conn.findFid(adversarialId(n)) == null);
- try checkFidIndex(&h.conn);
- // clunk every third fid, then the rest from the top: backward-shift deletion under churn
+ try h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 0x7FFF_FFFF, .names = &.{} } }, e_too_many_fids);
+ try h.expectFail(.{ .walk = .{ .fid = 0, .newfid = adversarialId(5), .names = &.{} } }, e_fid_in_use);
+ try h.expectFail(.{ .attach = .{ .fid = adversarialId(7), .uname = "t" } }, e_fid_in_use);
+ try h.expectFail(.{ .clunk = .{ .fid = 0x7FFF_FFFF } }, e_unknown_fid);
+ try h.expectFail(.{ .clunk = .{ .fid = adversarialId(n) } }, e_unknown_fid);
+ // clunk every third fid, then the rest from the top
i = 0;
while (i < n) : (i += 3) _ = try h.ok(.{ .clunk = .{ .fid = adversarialId(i) } });
- try checkFidIndex(&h.conn);
i = n;
while (i > 0) {
i -= 1;
if (i % 3 == 0) {
- try h.expectFail(.{ .clunk = .{ .fid = adversarialId(i) } }, "unknown fid");
+ try h.expectFail(.{ .clunk = .{ .fid = adversarialId(i) } }, e_unknown_fid);
} else {
_ = try h.ok(.{ .clunk = .{ .fid = adversarialId(i) } });
}
}
try testing.expectEqual(@as(usize, 1), h.conn.fidCount());
try testing.expectEqual(@as(u32, 0), prov.total_refs);
- try checkFidIndex(&h.conn);
- // the whole table is reusable after the churn, through the free list
+ // the whole table is reusable after the churn
i = 0;
while (i < n) : (i += 1) _ = try h.ok(.{ .walk = .{ .fid = 0, .newfid = n - i, .names = &.{} } });
- try h.expectFail(.{ .walk = .{ .fid = 0, .newfid = n + 1, .names = &.{} } }, "too many fids");
- try checkFidIndex(&h.conn);
- // pseudo-random alloc/free storm with verification
+ try h.expectFail(.{ .walk = .{ .fid = 0, .newfid = n + 1, .names = &.{} } }, e_too_many_fids);
+ // pseudo-random alloc/free storm
var prng = std.Random.DefaultPrng.init(0x9a11);
const rnd = prng.random();
var live: [n + 1]bool = @splat(true);
@@ -2395,18 +2295,17 @@ test "fid index: thousands of fids, clunk in hostile orders, reuse, Tversion" {
_ = try h.ok(.{ .walk = .{ .fid = 0, .newfid = id, .names = &.{"prov"} } });
}
live[id] = !live[id];
- if (round % 997 == 0) try checkFidIndex(&h.conn);
}
- try checkFidIndex(&h.conn);
+ var expected: usize = 1;
+ for (live) |l| expected += @intFromBool(l);
+ try testing.expectEqual(expected, h.conn.fidCount());
// Tversion drops everything and the table starts over, provider refs balanced
try h.version(big_cfg.msize);
try testing.expectEqual(@as(usize, 0), h.conn.fidCount());
try testing.expectEqual(@as(u32, 0), prov.total_refs);
- try testing.expectEqual(@as(u16, 0), h.conn.high_water);
- try checkFidIndex(&h.conn);
_ = try h.ok(.{ .attach = .{ .fid = 0xFFFF_FFFE, .uname = "t" } });
_ = try h.ok(.{ .walk = .{ .fid = 0xFFFF_FFFE, .newfid = 0, .names = &.{} } });
- try checkFidIndex(&h.conn);
+ try testing.expectEqual(@as(usize, 2), h.conn.fidCount());
}
test "open: a provider stat failure after a successful open closes the file again" {
@@ -2418,7 +2317,7 @@ test "open: a provider stat failure after a successful open closes the file agai
try x.h.expectFail(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }, "i/o error");
x.prov.fail_stat = false;
try testing.expectEqual(@as(u32, 0), x.prov.nodes[1].opens);
- try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 0, .count = 10 } }, "file not open");
+ try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 0, .count = 10 } }, e_bad_use);
_ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } });
try testing.expectEqual(@as(u32, 1), x.prov.nodes[1].opens);
// the same for create: a stat failure after the provider created the node releases it
@@ -2430,7 +2329,7 @@ test "open: a provider stat failure after a successful open closes the file agai
try testing.expectEqual(@as(u32, 0), e.opens);
try testing.expectEqual(@as(u32, 0), e.refs);
};
- try testing.expect(!x.h.conn.findFid(2).?.open);
+ try testing.expect(!isOpen(&x.h.conn, 2));
try testing.expectEqual(@as(u32, 1), x.prov.total_refs); // fid 1 only
}
@@ -2440,11 +2339,12 @@ test "fid state machine: open twice, walk from open, remove/clunk of open provid
defer x.deinit();
try x.h.walkTo(1, &.{ "prov", "dir", "inner" });
_ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.ordwr } });
- try x.h.expectFail(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }, "file already open");
- try x.h.expectFail(.{ .walk = .{ .fid = 1, .newfid = 2, .names = &.{"."} } }, "file already open");
- try x.h.expectFail(.{ .create = .{ .fid = 1, .name = "z", .perm = 0o644, .mode = cloud9.oread } }, "file already open");
- // a clone of an open fid is a fresh, unopened reference
- _ = try x.h.ok(.{ .walk = .{ .fid = 1, .newfid = 2, .names = &.{} } });
+ try x.h.expectFail(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }, e_already_open);
+ try x.h.expectFail(.{ .walk = .{ .fid = 1, .newfid = 2, .names = &.{"."} } }, e_bad_use);
+ try x.h.expectFail(.{ .walk = .{ .fid = 1, .newfid = 2, .names = &.{} } }, e_bad_use);
+ try x.h.expectFail(.{ .create = .{ .fid = 1, .name = "z", .perm = 0o644, .mode = cloud9.oread } }, e_already_open);
+ // a second, unopened reference to the same node
+ try x.h.walkTo(2, &.{ "prov", "dir", "inner" });
try testing.expectEqual(@as(u32, 2), x.prov.nodes[3].refs);
try testing.expectEqual(@as(u32, 1), x.prov.nodes[3].opens);
// walking newfid == fid with names on an unopened provider fid swaps the handle, refs balanced
@@ -2459,24 +2359,24 @@ test "fid state machine: open twice, walk from open, remove/clunk of open provid
_ = try x.h.ok(.{ .remove = .{ .fid = 1 } });
try testing.expectEqual(@as(u32, 0), x.prov.nodes[3].opens);
try testing.expectEqual(@as(u32, 1), x.prov.nodes[3].refs);
- try x.h.expectFail(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }, "unknown fid");
+ try x.h.expectFail(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }, e_unknown_fid);
_ = try x.h.ok(.{ .clunk = .{ .fid = 2 } });
try testing.expectEqual(@as(u32, 0), x.prov.total_refs);
// walking "." on a file fid is "not a directory" at the protocol level, without a provider walk
try x.h.walkTo(3, &.{ "prov", "hello" });
const before = x.prov.clunks;
- try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{"."} } }, "not a directory");
+ try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{"."} } }, e_not_dir);
try testing.expectEqual(before, x.prov.clunks);
try testing.expectEqual(@as(u32, 1), x.prov.total_refs);
// a partial walk through a file releases the handles it took
const part = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 5, .names = &.{ "prov", "hello", "x", "y" } } });
try testing.expectEqual(@as(u16, 2), part.walk.nwqid);
try testing.expectEqual(@as(u32, 1), x.prov.total_refs);
- try x.h.expectFail(.{ .clunk = .{ .fid = 5 } }, "unknown fid");
+ try x.h.expectFail(.{ .clunk = .{ .fid = 5 } }, e_unknown_fid);
// Tremove is always a clunk, even of a static node or when the provider refuses
try x.h.walkTo(6, &.{"README"});
try x.h.expectFail(.{ .remove = .{ .fid = 6 } }, "permission denied");
- try x.h.expectFail(.{ .clunk = .{ .fid = 6 } }, "unknown fid");
+ try x.h.expectFail(.{ .clunk = .{ .fid = 6 } }, e_unknown_fid);
_ = try x.h.ok(.{ .clunk = .{ .fid = 3 } });
try testing.expectEqual(@as(u32, 0), x.prov.total_refs);
}
@@ -2491,7 +2391,7 @@ test "snapshot slots: exhaust, hold, Tversion frees; reads past the end and at h
_ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } });
_ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oread } });
try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }, "too many open dynamic files");
- try testing.expect(!x.h.conn.findFid(3).?.open);
+ try testing.expect(!isOpen(&x.h.conn, 3));
// reads at offsets near 2^64 never trap (counts above msize are a raw-9P
// case: the cloud9 client refuses to send them; test/adv_core_hostile.py covers it)
const max_count = test_cfg.msize - cloud9.iohdrsz;
@@ -2522,23 +2422,26 @@ test "static and var nodes refuse create, remove and wstat; directories refuse w
var x: Fixture = .{};
try x.init();
defer x.deinit();
+ var ws = stat_dontcare;
+ ws.mtime = 1;
const dirs = [_][]const []const u8{ &.{}, &.{"build"}, &.{"comptime"}, &.{ "comptime", "types" }, &.{ "comptime", "types", "Layout" }, &.{"runtime"}, &.{ "runtime", "fn" }, &.{"vars"}, &.{ "vars", "state" }, &.{ "vars", "state", "f" }, &.{ "vars", "state", "f", "inner" } };
for (dirs, 0..) |d, k| {
const fid: u32 = @intCast(10 + k);
try x.h.walkTo(fid, d);
- try x.h.expectFail(.{ .create = .{ .fid = fid, .name = "x", .perm = 0o644, .mode = cloud9.owrite } }, "permission denied");
- try x.h.expectFail(.{ .wstat = .{ .fid = fid, .stat = stat_dontcare } }, "permission denied");
- try x.h.expectFail(.{ .open = .{ .fid = fid, .mode = cloud9.owrite } }, "is a directory");
- try x.h.expectFail(.{ .open = .{ .fid = fid, .mode = cloud9.oread | cloud9.otrunc } }, "is a directory");
+ try x.h.expectFail(.{ .create = .{ .fid = fid, .name = "x", .perm = 0o644, .mode = cloud9.owrite } }, e_perm);
+ try x.h.expectFail(.{ .wstat = .{ .fid = fid, .stat = ws } }, "permission denied");
+ _ = try x.h.ok(.{ .wstat = .{ .fid = fid, .stat = stat_dontcare } });
+ try x.h.expectFail(.{ .open = .{ .fid = fid, .mode = cloud9.owrite } }, e_perm);
+ try x.h.expectFail(.{ .open = .{ .fid = fid, .mode = cloud9.oread | cloud9.otrunc } }, e_perm);
try x.h.expectFail(.{ .remove = .{ .fid = fid } }, "permission denied");
- try x.h.expectFail(.{ .clunk = .{ .fid = fid } }, "unknown fid");
+ try x.h.expectFail(.{ .clunk = .{ .fid = fid } }, e_unknown_fid);
}
const files = [_][]const []const u8{ &.{"README"}, &.{ "build", "time" }, &.{ "comptime", "decls" }, &.{ "runtime", "pid" }, &.{ "runtime", "fn", "fib30" }, &.{"ctl"}, &.{ "vars", "state", "value" }, &.{ "vars", "state", "raw" }, &.{ "vars", "state", "f", "a", "value" }, &.{ "vars", "counter", "type" } };
for (files, 0..) |f, k| {
const fid: u32 = @intCast(30 + k);
try x.h.walkTo(fid, f);
- try x.h.expectFail(.{ .wstat = .{ .fid = fid, .stat = stat_dontcare } }, "permission denied");
- try x.h.expectFail(.{ .walk = .{ .fid = fid, .newfid = 99, .names = &.{".."} } }, "not a directory");
+ try x.h.expectFail(.{ .wstat = .{ .fid = fid, .stat = ws } }, "permission denied");
+ try x.h.expectFail(.{ .walk = .{ .fid = fid, .newfid = 99, .names = &.{".."} } }, e_not_dir);
try x.h.expectFail(.{ .remove = .{ .fid = fid } }, "permission denied");
}
// writes to a var value at a non-zero offset and with an empty payload
@@ -2551,35 +2454,62 @@ test "static and var nodes refuse create, remove and wstat; directories refuse w
try testing.expectEqual(@as(u32, 1), x.exposed.a);
_ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = std.math.maxInt(u64), .data = "77\n" } });
try testing.expectEqual(@as(u32, 77), x.exposed.a);
- // reads of a write-only fid are refused; OEXEC reads like OREAD
- try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 0, .count = 10 } }, "file not open");
+ // reads of a write-only fid are refused; the engine refuses OEXEC
+ try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 0, .count = 10 } }, e_bad_use);
try x.h.walkTo(2, &.{"README"});
- _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oexec } });
+ try x.h.expectFail(.{ .open = .{ .fid = 2, .mode = cloud9.oexec } }, e_perm);
+ _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oread } });
try testing.expect((try x.h.ok(.{ .read = .{ .fid = 2, .offset = 0, .count = 10 } })).read.len == 10);
}
-test "msize 24: every request that fits is answered, every reply that cannot fit is an Rerror" {
+test "a provider read that is not ready parks and is answered on a later step" {
var x: Fixture = .{};
try x.init();
defer x.deinit();
- try x.h.version(24);
- _ = try x.h.ok(.{ .attach = .{ .fid = 0, .uname = "u" } }); // Tattach 20, Rattach 20
- try x.h.expectFail(.{ .stat = .{ .fid = 0 } }, "reply too large"); // Rerror truncated to fit 24 bytes
- const w = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{"ctl"} } }); // Rwalk 22
- try testing.expectEqual(@as(u16, 1), w.walk.nwqid);
- try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 2, .names = &.{ ".", "." } } }, "reply too large");
- try x.h.expectFail(.{ .clunk = .{ .fid = 2 } }, "unknown fid");
- _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.ordwr } }); // Ropen 24
- try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 0, .data = "e" } }, "bad command"); // Twrite 24
- // the largest read the client may ask for is msize - iohdrsz = 0 bytes
- const r = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 0 } });
- try testing.expectEqual(@as(usize, 0), r.read.len);
- _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } });
- try x.h.walkTo(3, &.{"build"});
- _ = try x.h.ok(.{ .open = .{ .fid = 3, .mode = cloud9.oread } });
- const d = try x.h.ok(.{ .read = .{ .fid = 3, .offset = 0, .count = 0 } });
- try testing.expectEqual(@as(usize, 0), d.read.len); // no record fits in 0 bytes, nothing is split
- try x.h.expectFail(.{ .read = .{ .fid = 3, .offset = 1, .count = 0 } }, "bad offset");
+ try x.h.walkTo(1, &.{ "prov", "hello" });
+ _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } });
+ x.prov.park = true;
+ const reads = x.prov.reads;
+ const tag = try x.h.client.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } });
+ _ = try x.h.pump();
+ try testing.expectEqual(@as(?cloud9.Client.Done, null), x.h.client.take());
+ try testing.expect(x.prov.reads > reads);
+ // the connection keeps serving, and every step asks the provider again
+ const asked = x.prov.reads;
+ const st = try x.h.ok(.{ .stat = .{ .fid = 1 } });
+ try testing.expectEqualStrings("hello", st.stat.name);
+ try testing.expect(x.prov.reads > asked);
+ try testing.expectEqual(@as(?cloud9.Client.Done, null), x.h.client.take());
+ // a second parked read fills the slots; a third is refused at once
+ _ = try x.h.client.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } });
+ _ = try x.h.pump();
+ try testing.expectEqual(@as(?cloud9.Client.Done, null), x.h.client.take());
+ try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } }, fs.e_again);
+ // ready: the next step answers both, oldest first, with the original tags
+ x.prov.park = false;
+ _ = try x.h.pump();
+ const first = x.h.client.take() orelse return error.NoReply;
+ try testing.expectEqual(tag, first.tag);
+ try testing.expectEqualStrings("hello", first.result.read);
+ const second = x.h.client.take() orelse return error.NoReply;
+ try testing.expectEqualStrings("hello", second.result.read);
+ try testing.expectEqual(@as(?cloud9.Client.Done, null), x.h.client.take());
+ // a flushed parked read is interrupted; a hangup with one parked pays the provider
+ x.prov.park = true;
+ const parked = try x.h.client.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } });
+ _ = try x.h.pump();
+ _ = try x.h.client.submit(.{ .flush = .{ .oldtag = parked } });
+ _ = try x.h.pump();
+ const interrupted = x.h.client.take() orelse return error.NoReply;
+ try testing.expectEqual(parked, interrupted.tag);
+ try testing.expectEqualStrings(e_interrupted, interrupted.result.fail);
+ try testing.expect((x.h.client.take() orelse return error.NoReply).result == .flush);
+ _ = try x.h.client.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } });
+ _ = try x.h.pump();
+ try testing.expectEqual(@as(u32, 1), x.prov.nodes[1].opens);
+ x.h.conn.hangup();
+ try testing.expectEqual(@as(u32, 0), x.prov.nodes[1].opens);
+ try testing.expectEqual(@as(u32, 0), x.prov.total_refs);
}
test "Conn.init clamps the msize cap to [msize_min, cfg.msize]" {
@@ -2587,11 +2517,13 @@ test "Conn.init clamps the msize cap to [msize_min, cfg.msize]" {
var shared: TS.Shared = .init(&ctx);
var storage: TS.Storage = undefined;
const lo: TS.Conn = .init(&shared, &storage, 0);
- try testing.expectEqual(cloud9.Server.msize_min, lo.msize_cap);
+ try testing.expectEqual(fs.msize_min, lo.msize_cap);
const hi: TS.Conn = .init(&shared, &storage, std.math.maxInt(u32));
try testing.expectEqual(test_cfg.msize, hi.msize_cap);
const mid: TS.Conn = .init(&shared, &storage, 4096);
try testing.expectEqual(@as(u32, 4096), mid.msize_cap);
+ // two connections on the same Shared never share a fid-index salt
+ try testing.expect(lo.engine.hash_seed != hi.engine.hash_seed);
}
test "parseIso8601 rejects malformed stamps and never traps" {
@@ -2611,46 +2543,3 @@ test "parseIso8601 rejects malformed stamps and never traps" {
try testing.expectEqual(@as(u32, std.math.maxInt(u32)), parseIso8601("2106-02-07T06:28:15Z").?);
try testing.expectEqual(@as(?u32, null), parseIso8601("2106-02-07T06:28:16Z"));
}
-
-/// Multiplicative inverse of an odd 32-bit constant (Newton iteration).
-fn inverseMod32(a: u32) u32 {
- var x: u32 = a;
- for (0..5) |_| x *%= 2 -% a *% x;
- return x;
-}
-
-test "fid index: fid numbers crafted to collide under the public hash do not cluster a seeded connection" {
- var ctx: TestCtx = .{};
- var shared: BigS.Shared = .init(&ctx);
- const storage = try testing.allocator.create(BigS.Storage);
- defer testing.allocator.destroy(storage);
- var h: BigS.Harness = undefined;
- try h.init(&shared, storage);
- defer h.deinit();
- // two connections on the same Shared never share a seed
- const other: BigS.Conn = .init(&shared, storage, big_cfg.msize);
- try testing.expect(other.hash_seed != h.conn.hash_seed);
- // ids whose products with the golden ratio share their top bits: all one bucket when unseeded
- const inv = inverseMod32(0x9E37_79B1);
- try testing.expectEqual(@as(u32, 1), inv *% 0x9E37_79B1);
- const n: u32 = big_cfg.max_fids - 1;
- const base: u32 = 0x4242_0000;
- var i: u32 = 0;
- while (i < n) : (i += 1) {
- const id = (base + i) *% inv;
- try testing.expectEqual(@as(usize, base >> BigS.index_shift), @as(usize, @intCast((id *% 0x9E37_79B1) >> BigS.index_shift)));
- _ = try h.ok(.{ .walk = .{ .fid = 0, .newfid = id, .names = &.{} } });
- }
- try checkFidIndex(&h.conn);
- // the longest probe sequence in the seeded table is short; unseeded it would be ~n
- var worst: usize = 0;
- i = 0;
- while (i < n) : (i += 1) {
- const id = (base + i) *% inv;
- var pos = h.conn.fidHome(id);
- var steps: usize = 0;
- while (h.conn.fids[h.conn.index[pos]].id != id) : (pos = (pos + 1) & BigS.index_mask) steps += 1;
- worst = @max(worst, steps);
- }
- try testing.expect(worst < 64);
-}
diff --git a/9proc/src/linux/probe.zig b/9proc/src/linux/probe.zig
index 559d981..4db277d 100644
--- a/9proc/src/linux/probe.zig
+++ b/9proc/src/linux/probe.zig
@@ -427,7 +427,7 @@ pub fn Probe(comptime Srv: type) type {
/// Free space in the connection's input buffer (cloud9 keeps one
/// msize-sized frame; `push` copies at most this much).
fn inputRoom(conn: *const Srv.Conn) usize {
- return conn.server.in.len - conn.server.in_len;
+ return conn.inputRoom();
}
fn readClient(p: *Self, i: usize, hup: bool) void {
diff --git a/9proc/src/scratch.zig b/9proc/src/scratch.zig
index 2163a28..f666afb 100644
--- a/9proc/src/scratch.zig
+++ b/9proc/src/scratch.zig
@@ -7,6 +7,7 @@
//! the root is handle 0. Not internally synchronized (like `Shared`).
const std = @import("std");
const cloud9 = @import("cloud9");
+const fs = cloud9.fs;
const core = @import("core.zig");
const Allocator = std.mem.Allocator;
const Provider = core.Provider;
@@ -374,8 +375,7 @@ const Fixture = struct {
}
fn nodeOf(x: *Fixture, fid: u32) *Node {
- for (x.h.conn.fids) |f| if (f.used and f.id == fid) return x.scratch.node(f.node.prov.h);
- unreachable;
+ return x.scratch.node(x.h.conn.providerHandle(fid).?);
}
};
@@ -429,7 +429,7 @@ test "scratch create/write/read/rename/truncate/remove" {
_ = try x.h.ok(.{ .clunk = .{ .fid = 5 } });
try x.h.walkTo(6, &.{ "scratch", "d" });
try x.h.expectFail(.{ .remove = .{ .fid = 6 } }, "directory not empty");
- try x.h.expectFail(.{ .clunk = .{ .fid = 6 } }, "unknown fid"); // remove always clunks
+ try x.h.expectFail(.{ .clunk = .{ .fid = 6 } }, fs.e_unknown_fid); // remove always clunks
try x.h.walkTo(7, &.{ "scratch", "d", "inner" });
_ = try x.h.ok(.{ .remove = .{ .fid = 7 } });
try x.h.walkTo(8, &.{ "scratch", "d" });
@@ -469,7 +469,7 @@ test "walk of a missing name and walking a file" {
// a walk that fails past the first element is a partial Rwalk that leaves newfid unused
const r = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 4, .names = &.{ "scratch", "nope", "x" } } });
try testing.expectEqual(@as(u16, 1), r.walk.nwqid);
- try x.h.expectFail(.{ .clunk = .{ .fid = 4 } }, "unknown fid");
+ try x.h.expectFail(.{ .clunk = .{ .fid = 4 } }, fs.e_unknown_fid);
// .. from a file is not a directory; .. from the scratch root reaches the server root
try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 5, .names = &.{".."} } }, "not a directory");
try x.h.walkTo(5, &.{"scratch"});
@@ -504,10 +504,9 @@ test "directory read across consecutive offsets returns every record exactly onc
seen[idx] = true;
}
for (seen) |s| try testing.expect(s);
- try x.h.expectFail(.{ .read = .{ .fid = 2, .offset = 7, .count = 200 } }, "bad offset");
- // a read that cannot fit even one record returns nothing rather than splitting it
- const tiny = try x.h.ok(.{ .read = .{ .fid = 2, .offset = 0, .count = 30 } });
- try testing.expectEqual(@as(usize, 0), tiny.read.len);
+ try x.h.expectFail(.{ .read = .{ .fid = 2, .offset = 7, .count = 200 } }, fs.e_bad_offset);
+ // a read that cannot fit even one record is refused rather than splitting it
+ try x.h.expectFail(.{ .read = .{ .fid = 2, .offset = 0, .count = 30 } }, fs.e_count_small);
_ = try x.h.ok(.{ .clunk = .{ .fid = 2 } });
// Tversion resets every fid and every reference
try x.h.version(8192);
@@ -592,13 +591,16 @@ test "wstat with every field equal to the current stat changes nothing" {
try x.h.walkTo(1, &.{"scratch"});
_ = try x.h.ok(.{ .create = .{ .fid = 1, .name = "same", .perm = 0o640, .mode = cloud9.oread } });
const before = (try x.h.ok(.{ .stat = .{ .fid = 1 } })).stat;
- var copy = before;
+ // The engine owns type, dev, qid, atime and the owner names: those must
+ // be "don't care"; name, mode, mtime and length equal to the current
+ // stat reach the provider and change nothing.
+ var copy = dontcare;
var name_buf: [core.max_name]u8 = undefined;
@memcpy(name_buf[0..before.name.len], before.name);
copy.name = name_buf[0..before.name.len];
- copy.uid = "tester";
- copy.gid = "tester";
- copy.muid = "tester";
+ copy.mode = before.mode;
+ copy.mtime = before.mtime;
+ copy.length = before.length;
_ = try x.h.ok(.{ .wstat = .{ .fid = 1, .stat = copy } });
const after = (try x.h.ok(.{ .stat = .{ .fid = 1 } })).stat;
try testing.expectEqual(before.qid, after.qid);
@@ -619,7 +621,7 @@ test "wstat with every field equal to the current stat changes nothing" {
// the mode's directory bit is immutable, mtime is settable
st = dontcare;
st.mode = cloud9.dmdir | 0o640;
- try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = st } }, "permission denied");
+ try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = st } }, fs.e_wstat);
st = dontcare;
st.mtime = 12345;
_ = try x.h.ok(.{ .wstat = .{ .fid = 2, .stat = st } });
@@ -651,7 +653,7 @@ test "ORCLOSE removes on clunk and removed files stay readable through open fids
_ = try x.h.ok(.{ .clunk = .{ .fid = 3 } });
try x.h.walkTo(6, &.{"scratch"});
try x.h.expectFail(.{ .walk = .{ .fid = 6, .newfid = 7, .names = &.{"d"} } }, "file does not exist");
- try x.h.expectFail(.{ .create = .{ .fid = 5, .name = "x", .perm = 0o644, .mode = cloud9.oread } }, "unknown fid"); // remove clunked it
+ try x.h.expectFail(.{ .create = .{ .fid = 5, .name = "x", .perm = 0o644, .mode = cloud9.oread } }, fs.e_unknown_fid); // remove clunked it
}
test "qid paths are stable identities, not addresses: remove + recreate differ" {