library: five protocols speak the envelope
The folded header stops being a rule in a document and becomes the layout on the wire. Verbs number from sixteen, leaving describe, enumerate, subscribe and unsubscribe reserved and answered the same way by every provider — none of them writes a line to do it. What each protocol used to carry in a field of its own now travels in the header: a vfs node and a display layer are the packet's target, and a reply opens with a status the envelope stamps rather than one each protocol spelled for itself. Display gains the most. One forty-byte request had served eleven verbs, so attach_scanout smuggled stride through x, refresh through y and format through colour, and every coordinate crossed as a bitcast. Per-operation structs end all three: the fields have their own names and their own signs, and the tile payload grows to 224 bytes because the prefix shrank. Scanout loses a message maximum of 64 it had no business declaring — it answers calls, and the floor for a call is 256 — and virtio-gpu stops hard-coding that number at its harness. Two changes are semantic rather than notational. A directory now ends at an entry with no name, because the fixed part of a reply always travels and a zero-length reply no longer exists to mean anything. And input joins the service harness, the last loop in the tree that answered no ping and heard no terminate; its subscriber table, its pruning and its fan-out are the same code, and a shutdown now asks it to stop instead of killing it. A new conformance case reads the registry's own listing and asks every protocol it finds for its name, its version and its verb count, then offers a verb nobody defines and requires -ENOSYS — the envelope's promise, checked against providers rather than against itself. What it cannot reach in that boot it names on the serial line instead of passing quietly. Suite 110/110.
This commit is contained in:
@@ -1,52 +1,27 @@
|
||||
//! The VFS wire protocol — the message format spoken between a client (via the file
|
||||
//! API) and the user-space VFS server over IPC. A request is a fixed `Request` header
|
||||
//! followed by an inline payload (a path, or write bytes); a reply is a fixed `Reply`
|
||||
//! header followed by an inline payload (read bytes, or a FileStatus). Everything fits
|
||||
//! in one IPC message (<= ipc MESSAGE_MAXIMUM = 256 bytes).
|
||||
//! The VFS wire protocol — what a client (through the file API,
|
||||
//! library/kernel/file-system.zig) says to a filesystem backend over IPC. Defined
|
||||
//! through the envelope (docs/os-development/protocol-namespace.md), so every
|
||||
//! packet begins with the folded `Header`: the verb in `Header.operation`, and
|
||||
//! **the open node id in `Header.target`** — the field that used to be
|
||||
//! `Request.node`. A path appears in the conversation once, at `open`; every
|
||||
//! packet after it addresses that integer.
|
||||
//!
|
||||
//! This is a danos-native contract, so it uses danos names throughout. The client
|
||||
//! side is `runtime.fs` (library/runtime/fs.zig), which programs use directly.
|
||||
//! This is a danos-native contract, so it uses danos names throughout. It is
|
||||
//! user-space only — the kernel knows nothing of files or paths; it only routes
|
||||
//! (`fs_resolve`) and moves the bytes. The backends that serve it today are the
|
||||
//! FAT server (system/services/fat/) and the protocol registry inside PID 1
|
||||
//! (system/services/init/), which is a *synthetic* backend: `/protocol` holds
|
||||
//! contracts rather than files.
|
||||
//!
|
||||
//! This is user-space only — the kernel knows nothing of files or paths; it only moves the bytes.
|
||||
//! Shared by library/runtime/fs.zig (the client) and the mount backends that serve it (today
|
||||
//! the fat server, system/services/fat/). The standalone user-space VFS server it was first
|
||||
//! written against has retired — path routing moved into the kernel (system/kernel/vfs.zig,
|
||||
//! fs_resolve) — but the protocol module outlived it.
|
||||
|
||||
//! **An `open` reply may carry a capability.** The vfs `open` request rides
|
||||
//! **An `open` reply may carry a capability.** The `open` request rides
|
||||
//! `ipc_call`, and the reply direction of a call can hand back an endpoint
|
||||
//! (`ipc.callCap`'s `Reply.cap`). A file backend never uses it — FAT answers
|
||||
//! with a node id and nothing else — but a *synthetic* backend does: opening a
|
||||
//! (`ipc.callCap`'s `Reply.cap`). A file backend never uses it — FAT answers with
|
||||
//! a node id and nothing else — but the registry does: opening a
|
||||
//! `NodeKind.protocol` node under `/protocol` returns the provider's endpoint,
|
||||
//! which is the channel (docs/os-development/protocol-namespace.md). The
|
||||
//! convention is per-backend, not per-operation: a client that did not ask a
|
||||
//! synthetic backend simply gets no capability back, exactly as today.
|
||||
//! which is the channel. The convention is per-backend, not per-operation: a
|
||||
//! client that did not ask a synthetic backend simply gets no capability back.
|
||||
|
||||
pub const Operation = enum(u32) {
|
||||
open, // open(path) -> node id (a synthetic backend may reply with a capability instead)
|
||||
close, // close(node)
|
||||
read, // read(node, offset, len) -> bytes
|
||||
write, // write(node, offset, bytes) -> count
|
||||
status, // status(node) -> FileStatus
|
||||
// Appended for the mount router (M5). Values stay stable, so existing clients
|
||||
// and the flat-ramfs tests are unaffected.
|
||||
readdir, // readdir(dir_node, cursor=offset) -> one DirectoryEntry (len==0 => EOF)
|
||||
mount, // mount(prefix payload, capability = backend endpoint)
|
||||
unmount, // unmount(prefix payload)
|
||||
// Appended for filesystem mutation (Phase 2). Path-based (the path is the
|
||||
// payload); a mounted backend handles them, the flat ramfs refuses them.
|
||||
mkdir, // mkdir(path payload) -> status
|
||||
unlink, // unlink(path payload) -> status
|
||||
// rename: the payload is the old path, a single 0x00 separator, then the new
|
||||
// path. Same-directory rename only (the router requires both under one mount).
|
||||
rename, // rename(old\0new payload) -> status
|
||||
// Appended for the protocol namespace (P2). The registry is a synthetic
|
||||
// backend mounted at /protocol: `open` establishes a channel and `readdir`
|
||||
// lists the bound names like any directory, so those two verbs need nothing
|
||||
// new — but *claiming* a name does. A file backend refuses it, alongside the
|
||||
// router verbs it does not implement either; only the registry implements it.
|
||||
bind, // bind(name payload, capability = the provider's endpoint) -> status
|
||||
};
|
||||
const envelope = @import("envelope");
|
||||
|
||||
/// The type of a filesystem node, aligned to the node-kind table
|
||||
/// (docs/file-system-development/file-system-hierarchy.md). Fills `FileStatus.kind` and
|
||||
@@ -67,38 +42,18 @@ pub const NodeKind = enum(u32) {
|
||||
protocol = 7,
|
||||
};
|
||||
|
||||
/// One directory entry, returned by `readdir`: a fixed header followed inline in
|
||||
/// the reply payload by `name_len` bytes of name. A zero-length reply is EOF.
|
||||
/// One directory entry: the fixed part of a `readdir` reply, followed inline by
|
||||
/// `name_len` bytes of name. **A zero `name_len` is end of directory** — the
|
||||
/// reply's own length cannot say so any more, because the envelope always sends
|
||||
/// the fixed part.
|
||||
pub const DirectoryEntry = extern struct {
|
||||
kind: u32, // a NodeKind
|
||||
name_len: u32,
|
||||
size: u64,
|
||||
kind: u32 = 0, // a NodeKind
|
||||
name_len: u32 = 0,
|
||||
size: u64 = 0,
|
||||
};
|
||||
|
||||
pub const directory_entry_size: usize = @sizeOf(DirectoryEntry);
|
||||
|
||||
/// Request header. `node` is the server-side open-file id (from a prior open);
|
||||
/// for `open` the path is the payload and `len` is its length. `offset`/`len`
|
||||
/// carry the read/write position and count.
|
||||
pub const Request = extern struct {
|
||||
operation: Operation,
|
||||
node: u64,
|
||||
offset: u64,
|
||||
len: u32,
|
||||
flags: u32,
|
||||
};
|
||||
|
||||
/// Reply header. `status` is 0 on success or a negative errno; `node` is the new
|
||||
/// open-file id (for `open`); `len` is the payload length (bytes read, or the
|
||||
/// FileStatus size).
|
||||
pub const Reply = extern struct {
|
||||
status: i32,
|
||||
_padding: u32 = 0,
|
||||
node: u64 = 0,
|
||||
len: u32 = 0,
|
||||
_padding2: u32 = 0,
|
||||
};
|
||||
|
||||
/// A file's metadata (the danos-native answer to a `status` request). The POSIX
|
||||
/// layer maps this onto `struct stat`.
|
||||
pub const FileStatus = extern struct {
|
||||
@@ -110,13 +65,84 @@ pub const FileStatus = extern struct {
|
||||
mtime: u64 = 0,
|
||||
};
|
||||
|
||||
pub const message_maximum: usize = 256;
|
||||
pub const request_size: usize = @sizeOf(Request);
|
||||
pub const reply_size: usize = @sizeOf(Reply);
|
||||
/// Largest inline payload that still fits one IPC message alongside a header.
|
||||
pub const maximum_payload: usize = message_maximum - request_size;
|
||||
// --- the per-operation request and reply parts ------------------------------
|
||||
//
|
||||
// Each names the bytes AFTER the prefix. Nothing here carries an operation or a
|
||||
// node id: those are the packet header's, folded in once.
|
||||
|
||||
/// Open flags (danos-native; `runtime.fs.OpenOptions` maps its booleans onto these).
|
||||
/// `open(flags)` with the path as the packet's tail. The one verb that spends a
|
||||
/// path; everything after it addresses the node id this returns.
|
||||
pub const Open = extern struct { flags: u32 = 0 };
|
||||
|
||||
/// The node id an `open` established — the integer every later packet puts in
|
||||
/// `Header.target`. Meaningful only between this client and this backend.
|
||||
pub const Opened = extern struct { node: u64 };
|
||||
|
||||
/// `read(offset, len)` on `Header.target`; the bytes come back as the reply tail.
|
||||
pub const Read = extern struct {
|
||||
offset: u64,
|
||||
len: u32,
|
||||
_padding: u32 = 0,
|
||||
};
|
||||
|
||||
/// `write(offset, len)` on `Header.target`, with the data as the packet's tail.
|
||||
pub const Write = extern struct {
|
||||
offset: u64,
|
||||
len: u32,
|
||||
_padding: u32 = 0,
|
||||
};
|
||||
|
||||
/// How many bytes a `write` actually took — it may be short.
|
||||
pub const Written = extern struct { count: u32 };
|
||||
|
||||
/// `readdir(cursor)` on `Header.target`: one entry per call, cursor-advanced.
|
||||
pub const Readdir = extern struct { cursor: u64 };
|
||||
|
||||
/// The contract, whole. Verbs number from `envelope.first_protocol_operation`
|
||||
/// (16) in this order; the reserved verbs below it mean what they mean
|
||||
/// everywhere. `readdir` stays a protocol verb rather than folding into the
|
||||
/// reserved `enumerate`: it enumerates the children of one *node*, where
|
||||
/// `enumerate` names a provider's targets.
|
||||
pub const Protocol = envelope.Define(.{
|
||||
.name = "vfs",
|
||||
.version = 1,
|
||||
.operations = &.{
|
||||
.{ .name = "open", .request = Open, .reply = Opened },
|
||||
.{ .name = "close" },
|
||||
.{ .name = "read", .request = Read },
|
||||
.{ .name = "write", .request = Write, .reply = Written },
|
||||
.{ .name = "status", .reply = FileStatus },
|
||||
.{ .name = "readdir", .request = Readdir, .reply = DirectoryEntry },
|
||||
// The mount router's two verbs. Path routing lives in the kernel now
|
||||
// (system/kernel/vfs.zig), so no backend implements either; they keep
|
||||
// their numbers so the vocabulary stays the one docs/vfs-protocol.md
|
||||
// describes.
|
||||
.{ .name = "mount" }, // tail = the prefix, capability = the backend's endpoint
|
||||
.{ .name = "unmount" }, // tail = the prefix
|
||||
// Filesystem mutation, path-based: the path is the packet's tail.
|
||||
.{ .name = "mkdir" },
|
||||
.{ .name = "unlink" },
|
||||
// rename: the tail is the old path, a single 0x00 separator, then the
|
||||
// new path. Same-directory rename only.
|
||||
.{ .name = "rename" },
|
||||
// The registry's claim verb (P2): the name is the tail and the
|
||||
// provider's endpoint rides the call as its capability. A file backend
|
||||
// refuses it; only init implements it.
|
||||
.{ .name = "bind" },
|
||||
},
|
||||
});
|
||||
|
||||
pub const Operation = Protocol.Operation;
|
||||
|
||||
/// What a backend sizes its buffers to — the call floor, as every protocol does.
|
||||
pub const message_maximum: usize = Protocol.message_maximum;
|
||||
|
||||
/// The most inline payload any request may carry: the floor less the header and
|
||||
/// the widest fixed request part, so one bound serves every verb (a path, write
|
||||
/// data, a read's answer).
|
||||
pub const maximum_payload: usize = envelope.packet_maximum - Protocol.request_maximum;
|
||||
|
||||
/// Open flags (danos-native; `file_system.OpenOptions` maps its booleans onto these).
|
||||
pub const create: u32 = 1;
|
||||
/// Open a directory (for readdir) rather than a file. A mounted backend uses
|
||||
/// this to open a directory node; the flat ramfs ignores it.
|
||||
@@ -126,7 +152,7 @@ pub const directory: u32 = 2;
|
||||
/// backend frees the old cluster chain; the flat ramfs ignores it.
|
||||
pub const truncate: u32 = 4;
|
||||
|
||||
test "protocol struct sizes and node kinds" {
|
||||
test "the stable wire values: node kinds, entry layout, and the verb numbering" {
|
||||
const std = @import("std");
|
||||
try std.testing.expectEqual(@as(u32, 0), @intFromEnum(NodeKind.regular));
|
||||
try std.testing.expectEqual(@as(u32, 1), @intFromEnum(NodeKind.directory));
|
||||
@@ -134,11 +160,34 @@ test "protocol struct sizes and node kinds" {
|
||||
try std.testing.expectEqual(@as(u32, 6), @intFromEnum(NodeKind.socket));
|
||||
try std.testing.expectEqual(@as(u32, 7), @intFromEnum(NodeKind.protocol));
|
||||
try std.testing.expectEqual(@as(usize, 16), @sizeOf(DirectoryEntry));
|
||||
// The appended operations keep the original values.
|
||||
try std.testing.expectEqual(@as(u32, 0), @intFromEnum(Operation.open));
|
||||
try std.testing.expectEqual(@as(u32, 4), @intFromEnum(Operation.status));
|
||||
try std.testing.expectEqual(@as(u32, 5), @intFromEnum(Operation.readdir));
|
||||
try std.testing.expectEqual(@as(u32, 10), @intFromEnum(Operation.rename));
|
||||
// The registry's claim verb, appended last with the protocol namespace.
|
||||
try std.testing.expectEqual(@as(u32, 11), @intFromEnum(Operation.bind));
|
||||
|
||||
// The numbering the envelope gives this protocol. These are NEW values: the
|
||||
// rebase moved every verb above the reserved range, so the old 0..11 are
|
||||
// gone and 16..27 are what the wire carries. Pinned because both sides of a
|
||||
// flag-day have to agree on them, not because they may never change again.
|
||||
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Operation.open));
|
||||
try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Operation.close));
|
||||
try std.testing.expectEqual(@as(u32, 18), @intFromEnum(Operation.read));
|
||||
try std.testing.expectEqual(@as(u32, 19), @intFromEnum(Operation.write));
|
||||
try std.testing.expectEqual(@as(u32, 20), @intFromEnum(Operation.status));
|
||||
try std.testing.expectEqual(@as(u32, 21), @intFromEnum(Operation.readdir));
|
||||
try std.testing.expectEqual(@as(u32, 26), @intFromEnum(Operation.rename));
|
||||
try std.testing.expectEqual(@as(u32, 27), @intFromEnum(Operation.bind));
|
||||
// The payload bound is what it always was, arrived at the other way round:
|
||||
// the header plus the widest fixed request part is 32 bytes of the floor.
|
||||
try std.testing.expectEqual(@as(usize, 224), maximum_payload);
|
||||
}
|
||||
|
||||
test "the node id rides the header, and a path rides the tail" {
|
||||
const std = @import("std");
|
||||
var buffer: [message_maximum]u8 = undefined;
|
||||
|
||||
const opening = Protocol.encodeRequest(.open, 0, .{ .flags = create }, "/a/b", &buffer).?;
|
||||
try std.testing.expectEqual(@as(u32, create), Protocol.decodeRequest(.open, opening).?.flags);
|
||||
try std.testing.expectEqualStrings("/a/b", Protocol.requestTail(.open, opening));
|
||||
try std.testing.expectEqual(@as(u64, 0), envelope.headerOf(opening).?.target);
|
||||
|
||||
const reading = Protocol.encodeRequest(.read, 7, .{ .offset = 512, .len = 64 }, &.{}, &buffer).?;
|
||||
try std.testing.expectEqual(@as(u64, 7), envelope.headerOf(reading).?.target);
|
||||
try std.testing.expectEqual(@as(u64, 512), Protocol.decodeRequest(.read, reading).?.offset);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user