Publish exit events to subscribers; the VFS releases dead clients' handles (M17.3)

process_subscribe adds an endpoint to a bounded, ref-counted subscriber
table; every death posts the same badge encoding a supervisor's exit
notification uses, equally late, so subscribers observe a fully-released
child. A dying subscriber's own subscriptions are removed first — it never
hears about itself. The VFS is the first subscriber: open handles now
record their owner and are swept when the owner dies, because a service
must never depend on clients cleaning up after themselves
(docs/process-lifecycle.md). Proven by the vfs-client-death scenario.
This commit is contained in:
Daniel Samson
2026-07-12 23:41:44 +01:00
parent 2ebfb0c3b0
commit d8c55c6f2f
8 changed files with 202 additions and 7 deletions
+45 -5
View File
@@ -23,6 +23,10 @@ const Node = struct {
const OpenFile = struct {
used: bool = false,
node: usize = 0,
// 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
// cleaning up after themselves (docs/process-lifecycle.md).
owner: u32 = 0,
};
var nodes = [_]Node{.{}} ** 8;
@@ -65,8 +69,29 @@ fn fail(out: []u8) usize {
return writeReply(out, .{ .status = -1 }, &.{});
}
/// Handle one request; write the reply into `out`, return its length.
fn handle(message: []const u8, out: []u8) usize {
/// Format one whole log line and emit it in a single `debug_write`, so lines from
/// concurrent processes can never land in the middle of it.
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
var line: [96]u8 = undefined;
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
}
/// Release every open handle `client` held — called on that client's published
/// exit event. The nodes (the files) stay: ramfs contents outlive their writers,
/// only the dead client's handles go.
fn releaseClientHandles(client: u32) void {
var released: u32 = 0;
for (&opens) |*o| {
if (o.used and o.owner == client) {
o.used = false;
released += 1;
}
}
if (released != 0) writeLine("vfs: released {d} handle(s) for dead client {d}\n", .{ released, client });
}
/// Handle one request from `sender`; write the reply into `out`, return its length.
fn handle(message: []const u8, out: []u8, sender: u32) usize {
if (message.len < protocol.request_size) return fail(out);
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
const payload = message[protocol.request_size..];
@@ -77,7 +102,7 @@ fn handle(message: []const u8, out: []u8) usize {
const ni = findNode(name) orelse createNode(name) orelse return fail(out);
for (&opens, 0..) |*o, i| {
if (!o.used) {
o.* = .{ .used = true, .node = ni };
o.* = .{ .used = true, .node = ni, .owner = sender };
return writeReply(out, .{ .status = 0, .node = i }, &.{});
}
}
@@ -122,6 +147,12 @@ pub fn main() void {
_ = runtime.system.write("vfs: register failed\n");
return;
}
// First subscriber of the published exit events (docs/process-lifecycle.md):
// when a client dies holding open handles, the notification below is how the
// VFS learns to release them.
if (!runtime.process.subscribeExits(endpoint)) {
_ = runtime.system.write("vfs: exit subscription failed\n");
}
_ = runtime.system.write("vfs: ready\n");
var reply_buffer: [protocol.message_maximum]u8 = undefined;
@@ -129,8 +160,17 @@ pub fn main() void {
var receive: [protocol.message_maximum]u8 = undefined;
while (true) {
const got = runtime.ipc.replyWait(endpoint, reply_buffer[0..reply_len], &receive, null);
// Ignore notifications (none expected here); handle a request.
reply_len = handle(receive[0..got.len], &reply_buffer);
if (got.isChildExit()) {
// A published exit event: a process died; drop whatever it held.
releaseClientHandles(got.childProcessId());
reply_len = 0;
continue;
}
if (got.isNotification()) {
reply_len = 0; // not a request; nothing owed
continue;
}
reply_len = handle(receive[0..got.len], &reply_buffer, got.senderTaskId());
}
}