M5: VFS mount support — mount table + forwarding router

Turn the flat-ramfs VFS into a router: a mount table maps an absolute path prefix
(e.g. /mnt/usb) to a backend server's endpoint, and open/read/write/status/
readdir/close on a path under a mount are forwarded to that backend, which speaks
the same vfs-protocol. This is what a FAT filesystem mounts into.

- protocol: append readdir / mount / unmount operations, a NodeKind enum (the FSH
  file types) that now fills FileStatus.kind, a DirectoryEntry record, and a
  directory open flag. Appended values keep existing clients and tests unchanged.
- vfs.zig: a mount table, longest-prefix routing, forwarding of every op on a
  backend handle, mount/unmount handlers (the backend arrives as the call's
  capability), and release-on-death that also closes the backend's handles.
- path.zig: pure, host-tested mount-prefix matching that never captures a
  non-boundary like /mnt/usbextra.
- unistd: mount(), opendir / readdir / closedir clients.

Bare names still resolve in the flat ramfs — the backward-compat contract; the
vfs and vfs-client-death tests pass unchanged. End-to-end mount+read is exercised
by the FAT server (M6). Host units cover path matching and protocol sizes.
This commit is contained in:
Daniel Samson
2026-07-13 14:21:30 +01:00
parent 3fb9d5936a
commit 35e8921de8
5 changed files with 322 additions and 16 deletions
+2
View File
@@ -638,6 +638,8 @@ pub fn build(b: *std.Build) void {
"system/drivers/usb-hid/hid-report.zig", // HID boot-report keyboard/mouse decode "system/drivers/usb-hid/hid-report.zig", // HID boot-report keyboard/mouse decode
"system/drivers/usb-storage/bulk-only-transport.zig", // CBW/CSW wrapper sizes "system/drivers/usb-storage/bulk-only-transport.zig", // CBW/CSW wrapper sizes
"system/drivers/usb-storage/scsi.zig", // SCSI CDB encodings (big-endian) "system/drivers/usb-storage/scsi.zig", // SCSI CDB encodings (big-endian)
"system/services/vfs/path.zig", // mount-prefix path matching
"system/services/vfs/protocol.zig", // NodeKind / DirectoryEntry sizes + op values
}) |root| { }) |root| {
const mod_tests = b.addTest(.{ const mod_tests = b.addTest(.{
.root_module = b.createModule(.{ .root_module = b.createModule(.{
+58
View File
@@ -146,3 +146,61 @@ pub fn close(fd: i32) void {
_ = transact(request, &.{}, &.{}); _ = transact(request, &.{}, &.{});
f.used = false; f.used = false;
} }
/// Mount a filesystem backend (its server endpoint) at absolute path `target`;
/// the VFS then routes every path under `target` to that backend. Returns 0 or
/// -1. This is the one call that hands the VFS a capability (the backend).
pub fn mount(target: []const u8, backend: usize) i32 {
const h = vfs() orelse return -1;
const request = protocol.Request{ .operation = .mount, .node = 0, .offset = 0, .len = @intCast(target.len), .flags = 0 };
var message: [protocol.message_maximum]u8 = undefined;
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
const tlen = @min(target.len, protocol.maximum_payload);
@memcpy(message[protocol.request_size..][0..tlen], target[0..tlen]);
var rbuf: [protocol.message_maximum]u8 = undefined;
const result = ipc.callCap(h, message[0 .. protocol.request_size + tlen], &rbuf, backend) catch return -1;
if (result.len < protocol.reply_size) return -1;
return if (std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]).status == 0) 0 else -1;
}
/// A directory entry filled by `readdir`.
pub const DirEntry = struct {
kind: u32 = 0, // a protocol.NodeKind
size: u64 = 0,
name_buffer: [64]u8 = undefined,
name_len: usize = 0,
pub fn name(self: *const DirEntry) []const u8 {
return self.name_buffer[0..self.name_len];
}
};
/// Open a directory for reading with `readdir`. Returns an fd or -1.
pub fn opendir(path: []const u8) i32 {
return open(path, protocol.directory);
}
/// Read the next entry of a directory fd into `entry`; returns false at EOF or on
/// error. Advances the fd's cursor by one entry.
pub fn readdir(fd: i32, entry: *DirEntry) bool {
const f = fdPtr(fd) orelse return false;
const request = protocol.Request{ .operation = .readdir, .node = f.node, .offset = f.offset, .len = 0, .flags = 0 };
var buffer: [protocol.message_maximum]u8 = undefined;
const r = transact(request, &.{}, &buffer) orelse return false;
if (r.reply.status != 0 or r.reply.len == 0) return false; // error or EOF
if (r.payload.len < protocol.directory_entry_size) return false;
const header = std.mem.bytesToValue(protocol.DirectoryEntry, r.payload[0..protocol.directory_entry_size]);
entry.kind = header.kind;
entry.size = header.size;
const source = r.payload[protocol.directory_entry_size..];
const nlen = @min(@min(@as(usize, header.name_len), source.len), entry.name_buffer.len);
@memcpy(entry.name_buffer[0..nlen], source[0..nlen]);
entry.name_len = nlen;
f.offset += 1;
return true;
}
/// Close a directory fd (same as `close`).
pub fn closedir(fd: i32) void {
close(fd);
}
+39
View File
@@ -0,0 +1,39 @@
//! Pure path utilities for the VFS mount router — no IPC, no state, so they are
//! host-testable in isolation. The router uses these to decide whether an opened
//! path lies under a mount point and, if so, what it looks like relative to that
//! mount.
const std = @import("std");
/// If `path` lies under `mount_prefix` — equal to it, or the prefix followed by a
/// path separator — return the path relative to the mount ("/" for an exact
/// match, otherwise the tail beginning with '/'). Returns null when `path` is not
/// under the mount, so a prefix like "/mnt/usb" never captures "/mnt/usbextra".
pub fn underMount(path: []const u8, mount_prefix: []const u8) ?[]const u8 {
if (path.len < mount_prefix.len) return null;
if (!std.mem.eql(u8, path[0..mount_prefix.len], mount_prefix)) return null;
if (path.len == mount_prefix.len) return "/";
if (path[mount_prefix.len] != '/') return null;
return path[mount_prefix.len..];
}
/// Whether `path` is absolute (rooted at '/'). Bare names — what the flat ramfs
/// uses — are relative and never route through a mount.
pub fn isAbsolute(path: []const u8) bool {
return path.len > 0 and path[0] == '/';
}
test "underMount matches only at path boundaries" {
try std.testing.expectEqualStrings("/", underMount("/mnt/usb", "/mnt/usb").?);
try std.testing.expectEqualStrings("/system/kernel", underMount("/mnt/usb/system/kernel", "/mnt/usb").?);
try std.testing.expect(underMount("/mnt/usbextra", "/mnt/usb") == null); // not a boundary
try std.testing.expect(underMount("/mnt", "/mnt/usb") == null); // shorter than the prefix
try std.testing.expect(underMount("/other", "/mnt/usb") == null);
try std.testing.expect(underMount("greeting", "/mnt/usb") == null); // a bare name
}
test "isAbsolute distinguishes paths from bare names" {
try std.testing.expect(isAbsolute("/mnt/usb"));
try std.testing.expect(!isAbsolute("greeting"));
try std.testing.expect(!isAbsolute(""));
}
+42
View File
@@ -17,8 +17,36 @@ pub const Operation = enum(u32) {
read, // read(node, offset, len) -> bytes read, // read(node, offset, len) -> bytes
write, // write(node, offset, bytes) -> count write, // write(node, offset, bytes) -> count
status, // status(node) -> FileStatus 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)
}; };
/// The type of a filesystem node, aligned to the FSH file-type table
/// (docs/danos-file-system-hierarchy-FSH.md). Fills `FileStatus.kind` and
/// `DirectoryEntry.kind`; `regular = 0` keeps the historical hardcoded value.
pub const NodeKind = enum(u32) {
regular = 0,
directory = 1,
character_device = 2,
block_device = 3,
symbolic_link = 4,
fifo = 5,
socket = 6,
};
/// 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.
pub const DirectoryEntry = extern struct {
kind: u32, // a NodeKind
name_len: u32,
size: u64,
};
pub const directory_entry_size: usize = @sizeOf(DirectoryEntry);
/// Request header. `node` is the server-side open-file id (from a prior open); /// 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` /// for `open` the path is the payload and `len` is its length. `offset`/`len`
/// carry the read/write position and count. /// carry the read/write position and count.
@@ -57,3 +85,17 @@ pub const maximum_payload: usize = message_maximum - request_size;
/// Open flags (danos-native; the POSIX layer maps `O_CREAT` onto `create`). /// Open flags (danos-native; the POSIX layer maps `O_CREAT` onto `create`).
pub const create: u32 = 1; 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.
pub const directory: u32 = 2;
test "protocol struct sizes and node kinds" {
const std = @import("std");
try std.testing.expectEqual(@as(u32, 0), @intFromEnum(NodeKind.regular));
try std.testing.expectEqual(@as(u32, 1), @intFromEnum(NodeKind.directory));
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));
}
+181 -16
View File
@@ -3,14 +3,24 @@
//! file API marshals open/read/write/stat/close into calls to this server's //! file API marshals open/read/write/stat/close into calls to this server's
//! endpoint, published under the well-known `vfs` service id). //! endpoint, published under the well-known `vfs` service id).
//! //!
//! For now the namespace is a small in-memory ramfs (opening a name creates it): //! Two namespaces meet here (M5):
//! enough to prove the whole path — client file API -> IPC -> server dispatch -> //! - a small in-memory **ramfs** — opening a bare name creates it — enough to
//! reply. Device nodes backed by user-space drivers (/device) layer on top in M10, //! prove the round trip and to back the existing tests;
//! where `open` on a /device name forwards to the owning driver's endpoint. //! - **mounted filesystems**: a mount table maps an absolute path prefix (e.g.
//! `/mnt/usb`) to a backend server's endpoint. An open of a path under a mount
//! is *forwarded* to that backend (which speaks this same protocol), and every
//! later read/write/status/readdir/close on the resulting handle is relayed to
//! it. The VFS is the router; a filesystem (FAT) is the backend.
//!
//! A path routes through a mount only when it is absolute and lies under a mount
//! prefix; bare names always resolve in the flat ramfs — the backward-compat
//! contract the `vfs` / `vfs-client-death` tests rely on.
const std = @import("std"); const std = @import("std");
const runtime = @import("runtime"); const runtime = @import("runtime");
const protocol = runtime.vfs_protocol; const protocol = runtime.vfs_protocol;
const path = @import("path.zig");
const ipc = runtime.ipc;
const Node = struct { const Node = struct {
used: bool = false, used: bool = false,
@@ -22,15 +32,29 @@ const Node = struct {
const OpenFile = struct { const OpenFile = struct {
used: bool = false, used: bool = false,
// For a local handle: an index into `nodes`. For a forwarding handle: the
// node id the backend returned. (usize == u64 here, so it holds either.)
node: usize = 0, node: usize = 0,
// Non-null for a handle that forwards to a mounted backend.
backend: ?ipc.Handle = null,
// The client (task id — an IPC badge is one) that opened this handle. What // The client (task id — an IPC badge is one) that opened this handle. What
// release-on-death sweeps by: a service must never depend on its clients // release-on-death sweeps by: a service must never depend on its clients
// cleaning up after themselves (docs/process-lifecycle.md). // cleaning up after themselves (docs/process-lifecycle.md).
owner: u32 = 0, owner: u32 = 0,
}; };
// One mounted filesystem: an absolute path prefix and the backend endpoint that
// serves everything under it.
const Mount = struct {
used: bool = false,
prefix: [64]u8 = undefined,
prefix_len: usize = 0,
backend: ipc.Handle = 0,
};
var nodes = [_]Node{.{}} ** 8; var nodes = [_]Node{.{}} ** 8;
var opens = [_]OpenFile{.{}} ** 16; var opens = [_]OpenFile{.{}} ** 16;
var mounts = [_]Mount{.{}} ** 8;
fn findNode(name: []const u8) ?usize { fn findNode(name: []const u8) ?usize {
for (&nodes, 0..) |*n, i| { for (&nodes, 0..) |*n, i| {
@@ -57,6 +81,26 @@ fn openAt(id: u64) ?*OpenFile {
return if (o.used) o else null; return if (o.used) o else null;
} }
/// The mount whose prefix most specifically contains `name`, and the path
/// relative to it. Only absolute paths route; bare names never match.
const MountMatch = struct { backend: ipc.Handle, relative: []const u8 };
fn longestMount(name: []const u8) ?MountMatch {
if (!path.isAbsolute(name)) return null;
var best: ?MountMatch = null;
var best_len: usize = 0;
for (&mounts) |*m| {
if (!m.used) continue;
const prefix = m.prefix[0..m.prefix_len];
if (path.underMount(name, prefix)) |relative| {
if (best == null or prefix.len >= best_len) {
best_len = prefix.len;
best = .{ .backend = m.backend, .relative = relative };
}
}
}
return best;
}
/// Serialise a reply header + payload into `out`; returns the total length. /// Serialise a reply header + payload into `out`; returns the total length.
fn writeReply(out: []u8, reply: protocol.Reply, payload: []const u8) usize { fn writeReply(out: []u8, reply: protocol.Reply, payload: []const u8) usize {
@memcpy(out[0..protocol.reply_size], std.mem.asBytes(&reply)); @memcpy(out[0..protocol.reply_size], std.mem.asBytes(&reply));
@@ -76,13 +120,96 @@ fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return); _ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
} }
// --- mount routing ----------------------------------------------------------
/// Forward an open under a mount to its backend and, on success, allocate a local
/// forwarding handle that remembers the backend's node id.
fn forwardOpen(out: []u8, backend: ipc.Handle, relative: []const u8, flags: u32, sender: u32) usize {
const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = flags };
var message: [protocol.message_maximum]u8 = undefined;
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
const rel = relative[0..@min(relative.len, protocol.maximum_payload)];
@memcpy(message[protocol.request_size..][0..rel.len], rel);
var reply: [protocol.message_maximum]u8 = undefined;
const n = ipc.call(backend, message[0 .. protocol.request_size + rel.len], &reply) catch return fail(out);
if (n < protocol.reply_size) return fail(out);
const backend_reply = std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]);
if (backend_reply.status != 0) return writeReply(out, .{ .status = backend_reply.status }, &.{});
for (&opens, 0..) |*o, i| {
if (!o.used) {
o.* = .{ .used = true, .node = @intCast(backend_reply.node), .backend = backend, .owner = sender };
return writeReply(out, .{ .status = 0, .node = i }, &.{});
}
}
return fail(out);
}
/// Relay a read/write/status/readdir/close on a forwarding handle to the backend
/// (the node already rewritten to the backend's id) and copy its reply out.
fn forwardRequest(out: []u8, backend: ipc.Handle, request: protocol.Request, payload: []const u8) usize {
var message: [protocol.message_maximum]u8 = undefined;
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
const plen = @min(payload.len, protocol.maximum_payload);
@memcpy(message[protocol.request_size..][0..plen], payload[0..plen]);
var reply: [protocol.message_maximum]u8 = undefined;
const n = ipc.call(backend, message[0 .. protocol.request_size + plen], &reply) catch return fail(out);
const copy = @min(n, out.len);
@memcpy(out[0..copy], reply[0..copy]);
return copy;
}
/// Best-effort close of a backend node (used when a dead client's forwarding
/// handles are swept — the backend must not leak the vfs's opens).
fn forwardClose(backend: ipc.Handle, backend_node: u64) void {
const request = protocol.Request{ .operation = .close, .node = backend_node, .offset = 0, .len = 0, .flags = 0 };
var reply: [protocol.message_maximum]u8 = undefined;
_ = ipc.call(backend, std.mem.asBytes(&request), &reply) catch {};
}
fn doMount(out: []u8, prefix: []const u8, backend: ipc.Handle) usize {
for (&mounts) |*m| {
if (m.used and std.mem.eql(u8, m.prefix[0..m.prefix_len], prefix)) {
m.backend = backend;
writeLine("/system/services/vfs: remounted {s}\n", .{prefix});
return writeReply(out, .{ .status = 0 }, &.{});
}
}
for (&mounts) |*m| {
if (!m.used) {
const l = @min(prefix.len, m.prefix.len);
m.used = true;
@memcpy(m.prefix[0..l], prefix[0..l]);
m.prefix_len = l;
m.backend = backend;
writeLine("/system/services/vfs: mounted {s}\n", .{prefix[0..l]});
return writeReply(out, .{ .status = 0 }, &.{});
}
}
return fail(out);
}
fn doUnmount(out: []u8, prefix: []const u8) usize {
for (&mounts) |*m| {
if (m.used and std.mem.eql(u8, m.prefix[0..m.prefix_len], prefix)) {
m.used = false;
writeLine("/system/services/vfs: unmounted {s}\n", .{prefix});
return writeReply(out, .{ .status = 0 }, &.{});
}
}
return fail(out);
}
/// Release every open handle `client` held — called on that client's published /// Release every open handle `client` held — called on that client's published
/// exit event. The nodes (the files) stay: ramfs contents outlive their writers, /// exit event. Forwarding handles also tell their backend to release; local
/// only the dead client's handles go. /// nodes (the ramfs files) stay, since ramfs contents outlive their writers.
fn releaseClientHandles(client: u32) void { fn releaseClientHandles(client: u32) void {
var released: u32 = 0; var released: u32 = 0;
for (&opens) |*o| { for (&opens) |*o| {
if (o.used and o.owner == client) { if (o.used and o.owner == client) {
if (o.backend) |backend| forwardClose(backend, o.node);
o.used = false; o.used = false;
released += 1; released += 1;
} }
@@ -91,19 +218,28 @@ fn releaseClientHandles(client: u32) void {
} }
/// Handle one request from `sender`; write the reply into `out`, return its length. /// Handle one request from `sender`; write the reply into `out`, return its length.
fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize { fn handle(message: []const u8, out: []u8, sender: u32, capability: ?ipc.Handle) usize {
_ = capability;
if (message.len < protocol.request_size) return fail(out); if (message.len < protocol.request_size) return fail(out);
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]); const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
const payload = message[protocol.request_size..]; const payload = message[protocol.request_size..];
switch (request.operation) { switch (request.operation) {
.mount => {
const prefix = payload[0..@min(payload.len, request.len)];
const backend = capability orelse return fail(out);
return doMount(out, prefix, backend);
},
.unmount => {
const prefix = payload[0..@min(payload.len, request.len)];
return doUnmount(out, prefix);
},
.open => { .open => {
const name = payload[0..@min(payload.len, request.len)]; const name = payload[0..@min(payload.len, request.len)];
if (longestMount(name)) |m| return forwardOpen(out, m.backend, m.relative, request.flags, sender);
const ni = findNode(name) orelse createNode(name) orelse return fail(out); const ni = findNode(name) orelse createNode(name) orelse return fail(out);
for (&opens, 0..) |*o, i| { for (&opens, 0..) |*o, i| {
if (!o.used) { if (!o.used) {
o.* = .{ .used = true, .node = ni, .owner = sender }; o.* = .{ .used = true, .node = ni, .backend = null, .owner = sender };
return writeReply(out, .{ .status = 0, .node = i }, &.{}); return writeReply(out, .{ .status = 0, .node = i }, &.{});
} }
} }
@@ -111,7 +247,12 @@ fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.
}, },
.read => { .read => {
const of = openAt(request.node) orelse return fail(out); const of = openAt(request.node) orelse return fail(out);
const nd = &nodes[of.node]; if (of.backend) |backend| {
var forwarded = request;
forwarded.node = of.node;
return forwardRequest(out, backend, forwarded, payload);
}
const nd = &nodes[@intCast(of.node)];
const off: usize = @intCast(request.offset); const off: usize = @intCast(request.offset);
if (off >= nd.size) return writeReply(out, .{ .status = 0, .len = 0 }, &.{}); // EOF if (off >= nd.size) return writeReply(out, .{ .status = 0, .len = 0 }, &.{}); // EOF
const n = @min(@min(nd.size - off, request.len), protocol.maximum_payload); const n = @min(@min(nd.size - off, request.len), protocol.maximum_payload);
@@ -119,7 +260,12 @@ fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.
}, },
.write => { .write => {
const of = openAt(request.node) orelse return fail(out); const of = openAt(request.node) orelse return fail(out);
const nd = &nodes[of.node]; if (of.backend) |backend| {
var forwarded = request;
forwarded.node = of.node;
return forwardRequest(out, backend, forwarded, payload);
}
const nd = &nodes[@intCast(of.node)];
const off: usize = @intCast(request.offset); const off: usize = @intCast(request.offset);
if (off > nd.data.len) return fail(out); if (off > nd.data.len) return fail(out);
const n = @min(@min(payload.len, request.len), nd.data.len - off); const n = @min(@min(payload.len, request.len), nd.data.len - off);
@@ -129,11 +275,30 @@ fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.
}, },
.status => { .status => {
const of = openAt(request.node) orelse return fail(out); const of = openAt(request.node) orelse return fail(out);
const st = protocol.FileStatus{ .size = nodes[of.node].size, .kind = 0 }; if (of.backend) |backend| {
var forwarded = request;
forwarded.node = of.node;
return forwardRequest(out, backend, forwarded, payload);
}
const st = protocol.FileStatus{ .size = nodes[@intCast(of.node)].size, .kind = @intFromEnum(protocol.NodeKind.regular) };
return writeReply(out, .{ .status = 0, .len = @sizeOf(protocol.FileStatus) }, std.mem.asBytes(&st)); return writeReply(out, .{ .status = 0, .len = @sizeOf(protocol.FileStatus) }, std.mem.asBytes(&st));
}, },
.readdir => {
const of = openAt(request.node) orelse return fail(out);
if (of.backend) |backend| {
var forwarded = request;
forwarded.node = of.node;
return forwardRequest(out, backend, forwarded, payload);
}
// The flat ramfs has no directories: report EOF.
return writeReply(out, .{ .status = 0, .len = 0 }, &.{});
},
.close => { .close => {
if (request.node < opens.len) opens[@intCast(request.node)].used = false; const of = openAt(request.node);
if (of) |o| {
if (o.backend) |backend| forwardClose(backend, o.node);
o.used = false;
}
return writeReply(out, .{ .status = 0 }, &.{}); return writeReply(out, .{ .status = 0 }, &.{});
}, },
} }
@@ -142,7 +307,7 @@ fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.
/// Startup, under the harness: subscribe to the published exit events — when a /// Startup, under the harness: subscribe to the published exit events — when a
/// client dies holding open handles, the exit notification is how the VFS learns /// client dies holding open handles, the exit notification is how the VFS learns
/// to release them (docs/process-lifecycle.md). /// to release them (docs/process-lifecycle.md).
fn initialise(endpoint: runtime.ipc.Handle) bool { fn initialise(endpoint: ipc.Handle) bool {
if (!runtime.process.subscribeExits(endpoint)) { if (!runtime.process.subscribeExits(endpoint)) {
_ = runtime.system.write("/system/services/vfs: exit subscription failed\n"); _ = runtime.system.write("/system/services/vfs: exit subscription failed\n");
} }
@@ -152,8 +317,8 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
/// A non-signal notification: the only kind the VFS subscribes to is exit events. /// A non-signal notification: the only kind the VFS subscribes to is exit events.
fn onNotification(badge: u64) void { fn onNotification(badge: u64) void {
if (badge & runtime.ipc.notify_exit_bit != 0) { if (badge & ipc.notify_exit_bit != 0) {
releaseClientHandles(@intCast(badge & ~(runtime.ipc.notify_badge_bit | runtime.ipc.notify_exit_bit))); releaseClientHandles(@intCast(badge & ~(ipc.notify_badge_bit | ipc.notify_exit_bit)));
} }
} }