Skip to content

std.Build.Watch: key fanotify file descriptors by mount id #24113

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 23, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 56 additions & 47 deletions lib/std/Build/Watch.zig
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ const Os = switch (builtin.os.tag) {

/// Keyed differently but indexes correspond 1:1 with `dir_table`.
handle_table: HandleTable,
poll_fds: [1]posix.pollfd,
/// fanotify file descriptors are keyed by mount id since marks
/// are limited to a single filesystem.
poll_fds: std.AutoArrayHashMapUnmanaged(MountId, posix.pollfd),

const HandleTable = std.ArrayHashMapUnmanaged(FileHandle, ReactionSet, FileHandle.Adapter, false);
const MountId = i32;
const HandleTable = std.ArrayHashMapUnmanaged(FileHandle, struct { mount_id: MountId, reaction_set: ReactionSet }, FileHandle.Adapter, false);

const fan_mask: std.os.linux.fanotify.MarkMask = .{
.CLOSE_WRITE = true,
Expand Down Expand Up @@ -92,53 +95,33 @@ const Os = switch (builtin.os.tag) {
};

fn init() !Watch {
const fan_fd = std.posix.fanotify_init(.{
.CLASS = .NOTIF,
.CLOEXEC = true,
.NONBLOCK = true,
.REPORT_NAME = true,
.REPORT_DIR_FID = true,
.REPORT_FID = true,
.REPORT_TARGET_FID = true,
}, 0) catch |err| switch (err) {
error.UnsupportedFlags => fatal("fanotify_init failed due to old kernel; requires 5.17+", .{}),
else => |e| return e,
};
return .{
.dir_table = .{},
.os = switch (builtin.os.tag) {
.linux => .{
.handle_table = .{},
.poll_fds = .{
.{
.fd = fan_fd,
.events = std.posix.POLL.IN,
.revents = undefined,
},
},
.poll_fds = .{},
},
else => {},
},
.generation = 0,
};
}

fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path) !FileHandle {
fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path, mount_id: *MountId) !FileHandle {
var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined;
var mount_id: i32 = undefined;
var buf: [std.fs.max_path_bytes]u8 = undefined;
const adjusted_path = if (path.sub_path.len == 0) "./" else std.fmt.bufPrint(&buf, "{s}/", .{
path.sub_path,
}) catch return error.NameTooLong;
const stack_ptr: *std.os.linux.file_handle = @ptrCast(&file_handle_buffer);
stack_ptr.handle_bytes = file_handle_buffer.len - @sizeOf(std.os.linux.file_handle);
try posix.name_to_handle_at(path.root_dir.handle.fd, adjusted_path, stack_ptr, &mount_id, std.os.linux.AT.HANDLE_FID);
try posix.name_to_handle_at(path.root_dir.handle.fd, adjusted_path, stack_ptr, mount_id, std.os.linux.AT.HANDLE_FID);
const stack_lfh: FileHandle = .{ .handle = stack_ptr };
return stack_lfh.clone(gpa);
}

fn markDirtySteps(w: *Watch, gpa: Allocator) !bool {
const fan_fd = w.os.getFanFd();
fn markDirtySteps(w: *Watch, gpa: Allocator, fan_fd: posix.fd_t) !bool {
const fanotify = std.os.linux.fanotify;
const M = fanotify.event_metadata;
var events_buf: [256 + 4096]u8 = undefined;
Expand Down Expand Up @@ -167,10 +150,10 @@ const Os = switch (builtin.os.tag) {
const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes);
const file_name = std.mem.span(file_name_z);
const lfh: FileHandle = .{ .handle = file_handle };
if (w.os.handle_table.getPtr(lfh)) |reaction_set| {
if (reaction_set.getPtr(".")) |glob_set|
if (w.os.handle_table.getPtr(lfh)) |value| {
if (value.reaction_set.getPtr(".")) |glob_set|
any_dirty = markStepSetDirty(gpa, glob_set, any_dirty);
if (reaction_set.getPtr(file_name)) |step_set|
if (value.reaction_set.getPtr(file_name)) |step_set|
any_dirty = markStepSetDirty(gpa, step_set, any_dirty);
}
},
Expand All @@ -180,19 +163,38 @@ const Os = switch (builtin.os.tag) {
}
}

fn getFanFd(os: *const @This()) posix.fd_t {
return os.poll_fds[0].fd;
}

fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
const fan_fd = w.os.getFanFd();
// Add missing marks and note persisted ones.
for (steps) |step| {
for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
const reaction_set = rs: {
const gop = try w.dir_table.getOrPut(gpa, path);
if (!gop.found_existing) {
const dir_handle = try Os.getDirHandle(gpa, path);
var mount_id: MountId = undefined;
const dir_handle = try Os.getDirHandle(gpa, path, &mount_id);
const fan_fd = blk: {
const fd_gop = try w.os.poll_fds.getOrPut(gpa, mount_id);
if (!fd_gop.found_existing) {
const fan_fd = std.posix.fanotify_init(.{
.CLASS = .NOTIF,
.CLOEXEC = true,
.NONBLOCK = true,
.REPORT_NAME = true,
.REPORT_DIR_FID = true,
.REPORT_FID = true,
.REPORT_TARGET_FID = true,
}, 0) catch |err| switch (err) {
error.UnsupportedFlags => fatal("fanotify_init failed due to old kernel; requires 5.17+", .{}),
else => |e| return e,
};
fd_gop.value_ptr.* = .{
.fd = fan_fd,
.events = std.posix.POLL.IN,
.revents = undefined,
};
}
break :blk fd_gop.value_ptr.*.fd;
};
// `dir_handle` may already be present in the table in
// the case that we have multiple Cache.Path instances
// that compare inequal but ultimately point to the same
Expand All @@ -204,17 +206,17 @@ const Os = switch (builtin.os.tag) {
_ = w.dir_table.pop();
} else {
assert(dh_gop.index == gop.index);
dh_gop.value_ptr.* = .{};
dh_gop.value_ptr.* = .{ .mount_id = mount_id, .reaction_set = .{} };
posix.fanotify_mark(fan_fd, .{
.ADD = true,
.ONLYDIR = true,
}, fan_mask, path.root_dir.handle.fd, path.subPathOrDot()) catch |err| {
fatal("unable to watch {}: {s}", .{ path, @errorName(err) });
};
}
break :rs dh_gop.value_ptr;
break :rs &dh_gop.value_ptr.reaction_set;
}
break :rs &w.os.handle_table.values()[gop.index];
break :rs &w.os.handle_table.values()[gop.index].reaction_set;
};
for (files.items) |basename| {
const gop = try reaction_set.getOrPut(gpa, basename);
Expand All @@ -229,7 +231,7 @@ const Os = switch (builtin.os.tag) {
var i: usize = 0;
while (i < w.os.handle_table.entries.len) {
{
const reaction_set = &w.os.handle_table.values()[i];
const reaction_set = &w.os.handle_table.values()[i].reaction_set;
var step_set_i: usize = 0;
while (step_set_i < reaction_set.entries.len) {
const step_set = &reaction_set.values()[step_set_i];
Expand All @@ -256,6 +258,8 @@ const Os = switch (builtin.os.tag) {

const path = w.dir_table.keys()[i];

const mount_id = w.os.handle_table.values()[i].mount_id;
const fan_fd = w.os.poll_fds.getEntry(mount_id).?.value_ptr.fd;
posix.fanotify_mark(fan_fd, .{
.REMOVE = true,
.ONLYDIR = true,
Expand All @@ -272,13 +276,14 @@ const Os = switch (builtin.os.tag) {
}

fn wait(w: *Watch, gpa: Allocator, timeout: Timeout) !WaitResult {
const events_len = try std.posix.poll(&w.os.poll_fds, timeout.to_i32_ms());
return if (events_len == 0)
.timeout
else if (try Os.markDirtySteps(w, gpa))
.dirty
else
.clean;
const events_len = try std.posix.poll(w.os.poll_fds.values(), timeout.to_i32_ms());
if (events_len == 0)
return .timeout;
for (w.os.poll_fds.values()) |poll_fd| {
if (poll_fd.revents & std.posix.POLL.IN == std.posix.POLL.IN and try Os.markDirtySteps(w, gpa, poll_fd.fd))
return .dirty;
}
return .clean;
}
},
.windows => struct {
Expand Down Expand Up @@ -838,7 +843,11 @@ pub const Match = struct {
};

fn markAllFilesDirty(w: *Watch, gpa: Allocator) void {
for (w.os.handle_table.values()) |reaction_set| {
for (w.os.handle_table.values()) |value| {
const reaction_set = switch (builtin.os.tag) {
.linux => value.reaction_set,
else => value,
};
for (reaction_set.values()) |step_set| {
for (step_set.keys()) |step| {
step.recursiveReset(gpa);
Expand Down
Loading