From 82b095bdb89e52cf46cad832e97fa7df4c8346b5 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 13 Apr 2026 11:23:30 +0200 Subject: [PATCH] build(zig-0.16): port to zig-0.16 --- README.md | 2 +- build.zig | 21 ++- build.zig.zon | 2 +- src/backend/FSEvents.zig | 21 ++- src/backend/INotify.zig | 55 +++--- src/backend/KQueue.zig | 130 +++++++------- src/backend/KQueueDir.zig | 98 +++++------ src/backend/Windows.zig | 70 ++++---- src/main.zig | 107 ++++++------ src/nightwatch.zig | 29 ++-- src/nightwatch_test.zig | 345 +++++++++++++++++++------------------- 11 files changed, 452 insertions(+), 428 deletions(-) diff --git a/README.md b/README.md index 8ee6bd6..9b5e233 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ in **Zig**. It provides: - A standalone CLI for tracking filesystem changes -- A module for embedding change-tracking into other Zig programs +- A module for embedding change-tracking into other Zig programs (zig-0.16 & zig-0.15 supported) - Minimal dependencies and consistent, predictable, cross-platform behavior It does not interfere. diff --git a/build.zig b/build.zig index 8ff5d56..cafffe2 100644 --- a/build.zig +++ b/build.zig @@ -35,16 +35,15 @@ pub fn build(b: *std.Build) void { mod.linkFramework("CoreFoundation", .{}); } - var version: std.ArrayList(u8) = .empty; - defer version.deinit(b.allocator); - gen_version(b, version.writer(b.allocator)) catch |e| { + var version: std.Io.Writer.Allocating = .init(b.allocator); + defer version.deinit(); + gen_version(b, &version.writer) catch |e| { if (b.release_mode != .off) std.debug.panic("gen_version failed: {any}", .{e}); - version.clearAndFree(b.allocator); - version.appendSlice(b.allocator, "unknown") catch {}; + version.writer.writeAll("unknown") catch {}; }; const write_file_step = b.addWriteFiles(); - const version_file = write_file_step.add("version", version.items); + const version_file = write_file_step.add("version", version.written()); const exe = b.addExecutable(.{ .name = "nightwatch", @@ -101,13 +100,13 @@ pub fn build(b: *std.Build) void { } } -fn gen_version(b: *std.Build, writer: anytype) !void { +fn gen_version(b: *std.Build, writer: *std.Io.Writer) !void { var code: u8 = 0; - const describe = try b.runAllowFail(&[_][]const u8{ "git", "describe", "--always", "--tags" }, &code, .Ignore); - const diff_ = try b.runAllowFail(&[_][]const u8{ "git", "diff", "--stat", "--patch", "HEAD" }, &code, .Ignore); - const diff = std.mem.trimRight(u8, diff_, "\r\n "); - const version = std.mem.trimRight(u8, describe, "\r\n "); + const describe = try b.runAllowFail(&[_][]const u8{ "git", "describe", "--always", "--tags" }, &code, .ignore); + const diff_ = try b.runAllowFail(&[_][]const u8{ "git", "diff", "--stat", "--patch", "HEAD" }, &code, .ignore); + const diff = std.mem.trimEnd(u8, diff_, "\r\n "); + const version = std.mem.trimEnd(u8, describe, "\r\n "); try writer.print("{s}{s}", .{ version, if (diff.len > 0) "-dirty" else "" }); } diff --git a/build.zig.zon b/build.zig.zon index 3b12074..d7638b3 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,7 +2,7 @@ .name = .nightwatch, .version = "1.0.0", .fingerprint = 0xb88932861fde7cb9, - .minimum_zig_version = "0.15.2", + .minimum_zig_version = "0.16.0-dev.3133+5ec8e45f3", .dependencies = .{ .@"xcode-frameworks" = .{ .url = "git+https://github.com/hexops/xcode-frameworks?ref=main#8a1cfb373587ea4c9bb1468b7c986462d8d4e10e", diff --git a/src/backend/FSEvents.zig b/src/backend/FSEvents.zig index f4b6e60..59f6abd 100644 --- a/src/backend/FSEvents.zig +++ b/src/backend/FSEvents.zig @@ -12,6 +12,7 @@ pub const emits_rename_for_dirs = false; pub const emits_subtree_created_on_movein = true; handler: *Handler, +io: std.Io, stream: ?*anyopaque, // FSEventStreamRef queue: ?*anyopaque, // dispatch_queue_t ctx: ?*CallbackContext, // heap-allocated, freed after stream is stopped @@ -79,6 +80,7 @@ const cf = struct { const CallbackContext = struct { handler: *Handler, + io: std.Io, // Snapshot of the watched root paths at arm() time, used to filter out // spurious events for the root directories themselves that FSEvents // sometimes delivers as historical events at stream start. @@ -86,9 +88,10 @@ const CallbackContext = struct { last_event_id: *?u64, // points to FSEvents.last_seen_event_id }; -pub fn init(handler: *Handler) error{}!@This() { +pub fn init(io: std.Io, handler: *Handler) error{}!@This() { return .{ .handler = handler, + .io = io, .stream = null, .queue = null, .ctx = null, @@ -168,7 +171,7 @@ pub fn arm(self: *@This(), allocator: std.mem.Allocator) error{ OutOfMemory, Arm const ctx = try allocator.create(CallbackContext); errdefer allocator.destroy(ctx); - ctx.* = .{ .handler = self.handler, .watched_roots = roots, .last_event_id = &self.last_seen_event_id }; + ctx.* = .{ .handler = self.handler, .io = self.io, .watched_roots = roots, .last_event_id = &self.last_seen_event_id }; // FSEventStreamCreate copies the context struct; stack allocation is fine. const stream_ctx = FSEventStreamContext{ .version = 0, .info = ctx }; @@ -249,12 +252,12 @@ fn callback( // FSEvents fires ItemRenamed for both sides of a rename unpaired. // Normalize to created/deleted based on whether the path still exists, // so move-in appears as created and move-out as deleted on all platforms. - const exists = if (std.fs.accessAbsolute(path, .{})) |_| true else |_| false; + const exists = if (std.Io.Dir.accessAbsolute(ctx.io, path, .{})) |_| true else |_| false; ctx.handler.change(path, if (exists) .created else .deleted, ot) catch {}; // If a directory was moved in from outside the watched tree, FSEvents // only fires for the directory itself - not for its pre-existing contents. // Walk the subtree and emit individual created events for each entry. - if (exists and ot == .dir) emit_subtree_created(ctx.handler, path); + if (exists and ot == .dir) emit_subtree_created(ctx.handler, ctx.io, path); // If a write was coalesced with a move-in, also emit the modify. if (exists and flags & kFSEventStreamEventFlagItemModified != 0) { ctx.handler.change(path, .modified, ot) catch {}; @@ -271,11 +274,11 @@ fn callback( // Called when a directory is moved into the watched tree: FSEvents fires // ItemRenamed only for the directory itself, not for its pre-existing contents. // Uses only stack storage so it can be called from the GCD callback thread. -fn emit_subtree_created(handler: *Handler, dir_path: []const u8) void { - var dir = std.fs.openDirAbsolute(dir_path, .{ .iterate = true }) catch return; - defer dir.close(); +fn emit_subtree_created(handler: *Handler, io: std.Io, dir_path: []const u8) void { + var dir = std.Io.Dir.openDirAbsolute(io, dir_path, .{ .iterate = true }) catch return; + defer dir.close(io); var iter = dir.iterate(); - while (iter.next() catch return) |entry| { + while (iter.next(io) catch return) |entry| { const ot: ObjectType = switch (entry.kind) { .file => .file, .directory => .dir, @@ -284,7 +287,7 @@ fn emit_subtree_created(handler: *Handler, dir_path: []const u8) void { var path_buf: [std.fs.max_path_bytes]u8 = undefined; const full_path = std.fmt.bufPrint(&path_buf, "{s}/{s}", .{ dir_path, entry.name }) catch continue; handler.change(full_path, .created, ot) catch {}; - if (ot == .dir) emit_subtree_created(handler, full_path); + if (ot == .dir) emit_subtree_created(handler, io, full_path); } } diff --git a/src/backend/INotify.zig b/src/backend/INotify.zig index e08bdf3..e13ef48 100644 --- a/src/backend/INotify.zig +++ b/src/backend/INotify.zig @@ -13,6 +13,7 @@ const PendingRename = struct { pub fn Create(comptime variant: InterfaceType) type { return struct { handler: *Handler, + io: std.Io, inotify_fd: std.posix.fd_t, watches: std.AutoHashMapUnmanaged(i32, WatchEntry), // wd -> {owned path, is dir} // Protects `watches` against concurrent access by the background thread @@ -20,7 +21,7 @@ pub fn Create(comptime variant: InterfaceType) type { // (add_watch / remove_watch). Void for the polling variant, which is // single-threaded. watches_mutex: switch (variant) { - .threaded => std.Thread.Mutex, + .threaded => std.Io.Mutex, .polling => void, }, pending_renames: std.ArrayListUnmanaged(PendingRename), @@ -56,17 +57,20 @@ pub fn Create(comptime variant: InterfaceType) type { const in_flags: std.os.linux.O = .{ .NONBLOCK = true }; - pub fn init(handler: *Handler) !@This() { - const inotify_fd = try std.posix.inotify_init1(@bitCast(in_flags)); - errdefer std.posix.close(inotify_fd); + pub fn init(io: std.Io, handler: *Handler) !@This() { + const rc = std.os.linux.inotify_init1(@bitCast(in_flags)); + if (std.posix.errno(rc) != .SUCCESS) return error.WatchFailed; + const inotify_fd: std.posix.fd_t = @intCast(rc); + errdefer std.Io.Threaded.closeFd(inotify_fd); switch (variant) { .threaded => { - const stop_pipe = try std.posix.pipe(); + const stop_pipe = try std.Io.Threaded.pipe2(.{}); return .{ .handler = handler, + .io = io, .inotify_fd = inotify_fd, .watches = .empty, - .watches_mutex = .{}, + .watches_mutex = std.Io.Mutex.init, .pending_renames = .empty, .stop_pipe = stop_pipe, .thread = null, @@ -75,6 +79,7 @@ pub fn Create(comptime variant: InterfaceType) type { .polling => { return .{ .handler = handler, + .io = io, .inotify_fd = inotify_fd, .watches = .empty, .watches_mutex = {}, @@ -89,17 +94,17 @@ pub fn Create(comptime variant: InterfaceType) type { pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { if (comptime variant == .threaded) { // Signal thread to stop and wait for it to exit. - _ = std.posix.write(self.stop_pipe[1], "x") catch {}; + _ = std.posix.system.write(self.stop_pipe[1], "x", 1); if (self.thread) |t| t.join(); - std.posix.close(self.stop_pipe[0]); - std.posix.close(self.stop_pipe[1]); + std.Io.Threaded.closeFd(self.stop_pipe[0]); + std.Io.Threaded.closeFd(self.stop_pipe[1]); } var it = self.watches.iterator(); while (it.next()) |entry| allocator.free(entry.value_ptr.*.path); self.watches.deinit(allocator); for (self.pending_renames.items) |r| allocator.free(r.path); self.pending_renames.deinit(allocator); - std.posix.close(self.inotify_fd); + std.Io.Threaded.closeFd(self.inotify_fd); } pub fn arm(self: *@This(), allocator: std.mem.Allocator) error{HandlerFailed}!void { @@ -150,22 +155,22 @@ pub fn Create(comptime variant: InterfaceType) type { }, } const is_dir = blk: { - var d = std.fs.openDirAbsolute(path, .{}) catch break :blk false; - defer d.close(); + var d = std.Io.Dir.openDirAbsolute(self.io, path, .{}) catch break :blk false; + defer d.close(self.io); break :blk true; }; const owned_path = try allocator.dupe(u8, path); errdefer allocator.free(owned_path); - if (comptime variant == .threaded) self.watches_mutex.lock(); - defer if (comptime variant == .threaded) self.watches_mutex.unlock(); + if (comptime variant == .threaded) self.watches_mutex.lockUncancelable(self.io); + defer if (comptime variant == .threaded) self.watches_mutex.unlock(self.io); const result = try self.watches.getOrPut(allocator, @intCast(wd)); if (result.found_existing) allocator.free(result.value_ptr.*.path); result.value_ptr.* = .{ .path = owned_path, .is_dir = is_dir }; } pub fn remove_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) void { - if (comptime variant == .threaded) self.watches_mutex.lock(); - defer if (comptime variant == .threaded) self.watches_mutex.unlock(); + if (comptime variant == .threaded) self.watches_mutex.lockUncancelable(self.io); + defer if (comptime variant == .threaded) self.watches_mutex.unlock(self.io); var it = self.watches.iterator(); while (it.next()) |entry| { if (!std.mem.eql(u8, entry.value_ptr.*.path, path)) continue; @@ -177,8 +182,8 @@ pub fn Create(comptime variant: InterfaceType) type { } fn has_watch_for_path(self: *@This(), path: []const u8) bool { - if (comptime variant == .threaded) self.watches_mutex.lock(); - defer if (comptime variant == .threaded) self.watches_mutex.unlock(); + if (comptime variant == .threaded) self.watches_mutex.lockUncancelable(self.io); + defer if (comptime variant == .threaded) self.watches_mutex.unlock(self.io); var it = self.watches.iterator(); while (it.next()) |entry| { if (std.mem.eql(u8, entry.value_ptr.*.path, path)) return true; @@ -190,8 +195,8 @@ pub fn Create(comptime variant: InterfaceType) type { // Any entry equal to old_path is updated to new_path; any entry whose // path begins with old_path + sep has its prefix replaced with new_path. fn rename_watch_paths(self: *@This(), allocator: std.mem.Allocator, old_path: []const u8, new_path: []const u8) void { - if (comptime variant == .threaded) self.watches_mutex.lock(); - defer if (comptime variant == .threaded) self.watches_mutex.unlock(); + if (comptime variant == .threaded) self.watches_mutex.lockUncancelable(self.io); + defer if (comptime variant == .threaded) self.watches_mutex.unlock(self.io); var it = self.watches.valueIterator(); while (it.next()) |entry| { if (std.mem.eql(u8, entry.path, old_path)) { @@ -216,10 +221,10 @@ pub fn Create(comptime variant: InterfaceType) type { // subdirectory. Called after an unpaired IN_MOVED_TO for a directory so that // the caller sees individual 'created' events for the moved-in tree contents. fn emit_subtree_created(self: *@This(), dir_path: []const u8) error{HandlerFailed}!void { - var dir = std.fs.openDirAbsolute(dir_path, .{ .iterate = true }) catch return; - defer dir.close(); + var dir = std.Io.Dir.openDirAbsolute(self.io, dir_path, .{ .iterate = true }) catch return; + defer dir.close(self.io); var iter = dir.iterate(); - while (iter.next() catch return) |entry| { + while (iter.next(self.io) catch return) |entry| { const ot: ObjectType = switch (entry.kind) { .file => .file, .directory => .dir, @@ -277,13 +282,13 @@ pub fn Create(comptime variant: InterfaceType) type { var watched_buf: [std.fs.max_path_bytes]u8 = undefined; var watched_len: usize = 0; var watched_is_dir = false; - if (comptime variant == .threaded) self.watches_mutex.lock(); + if (comptime variant == .threaded) self.watches_mutex.lockUncancelable(self.io); if (self.watches.get(ev.wd)) |e| { @memcpy(watched_buf[0..e.path.len], e.path); watched_len = e.path.len; watched_is_dir = e.is_dir; } - if (comptime variant == .threaded) self.watches_mutex.unlock(); + if (comptime variant == .threaded) self.watches_mutex.unlock(self.io); if (watched_len == 0) continue; const watched_path = watched_buf[0..watched_len]; diff --git a/src/backend/KQueue.zig b/src/backend/KQueue.zig index 104154f..ba79f9d 100644 --- a/src/backend/KQueue.zig +++ b/src/backend/KQueue.zig @@ -16,14 +16,15 @@ kq: std.posix.fd_t, shutdown_pipe: [2]std.posix.fd_t, // [0]=read [1]=write; write a byte to wake the thread thread: ?std.Thread, watches: std.StringHashMapUnmanaged(std.posix.fd_t), // owned dir path -> fd -watches_mutex: std.Thread.Mutex, +watches_mutex: std.Io.Mutex, file_watches: std.StringHashMapUnmanaged(std.posix.fd_t), // owned file path -> fd -file_watches_mutex: std.Thread.Mutex, +file_watches_mutex: std.Io.Mutex, // Per-directory snapshots of filenames, used to diff on NOTE_WRITE. // Key: independently owned dir path, value: set of owned filenames. // Accessed from both the main thread (add_watch) and the background thread (scan_dir). snapshots: std.StringHashMapUnmanaged(std.StringHashMapUnmanaged(void)), -snapshots_mutex: std.Thread.Mutex, +snapshots_mutex: std.Io.Mutex, +io: std.Io, const EVFILT_VNODE: i16 = -4; const EVFILT_READ: i16 = -1; @@ -37,7 +38,7 @@ const NOTE_EXTEND: u32 = 0x00000004; const NOTE_ATTRIB: u32 = 0x00000008; const NOTE_RENAME: u32 = 0x00000020; -pub fn init(handler: *Handler) (std.posix.KQueueError || std.posix.KEventError)!@This() { +pub fn init(io: std.Io, handler: *Handler) (std.posix.KQueueError || std.posix.KEventError)!@This() { // Per-file kqueue watches require one open fd per watched file. Bump // the soft NOFILE limit to the hard limit so large directory trees don't // exhaust the default quota (256 on macOS, 1024 on many FreeBSD installs). @@ -46,11 +47,11 @@ pub fn init(handler: *Handler) (std.posix.KQueueError || std.posix.KEventError)! std.posix.setrlimit(.NOFILE, .{ .cur = rl.max, .max = rl.max }) catch {}; } else |_| {} const kq = try std.posix.kqueue(); - errdefer std.posix.close(kq); - const pipe = try std.posix.pipe(); + errdefer std.Io.Threaded.closeFd(kq); + const pipe = try std.Io.Threaded.pipe2(.{}); errdefer { - std.posix.close(pipe[0]); - std.posix.close(pipe[1]); + std.Io.Threaded.closeFd(pipe[0]); + std.Io.Threaded.closeFd(pipe[1]); } // Register the read end of the shutdown pipe with kqueue so the thread // wakes up when we want to shut down. @@ -69,29 +70,30 @@ pub fn init(handler: *Handler) (std.posix.KQueueError || std.posix.KEventError)! .shutdown_pipe = pipe, .thread = null, .watches = .empty, - .watches_mutex = .{}, + .watches_mutex = std.Io.Mutex.init, .file_watches = .empty, - .file_watches_mutex = .{}, + .file_watches_mutex = std.Io.Mutex.init, .snapshots = .empty, - .snapshots_mutex = .{}, + .snapshots_mutex = std.Io.Mutex.init, + .io = io, }; } pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { // Signal the thread to exit by writing to the shutdown pipe. - _ = std.posix.write(self.shutdown_pipe[1], &[_]u8{0}) catch {}; + _ = std.posix.system.write(self.shutdown_pipe[1], &[_]u8{0}, 1); if (self.thread) |t| t.join(); - std.posix.close(self.shutdown_pipe[0]); - std.posix.close(self.shutdown_pipe[1]); + std.Io.Threaded.closeFd(self.shutdown_pipe[0]); + std.Io.Threaded.closeFd(self.shutdown_pipe[1]); var it = self.watches.iterator(); while (it.next()) |entry| { - std.posix.close(entry.value_ptr.*); + std.Io.Threaded.closeFd(entry.value_ptr.*); allocator.free(entry.key_ptr.*); } self.watches.deinit(allocator); var fit = self.file_watches.iterator(); while (fit.next()) |entry| { - std.posix.close(entry.value_ptr.*); + std.Io.Threaded.closeFd(entry.value_ptr.*); allocator.free(entry.key_ptr.*); } self.file_watches.deinit(allocator); @@ -104,15 +106,15 @@ pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { names.deinit(allocator); } self.snapshots.deinit(allocator); - std.posix.close(self.kq); + std.Io.Threaded.closeFd(self.kq); } pub fn arm(self: *@This(), allocator: std.mem.Allocator) (error{AlreadyArmed} || std.Thread.SpawnError)!void { if (self.thread != null) return error.AlreadyArmed; - self.thread = try std.Thread.spawn(.{}, thread_fn, .{ self, allocator }); + self.thread = try std.Thread.spawn(.{}, thread_fn, .{ self, self.io, allocator }); } -fn thread_fn(self: *@This(), allocator: std.mem.Allocator) void { +fn thread_fn(self: *@This(), io: std.Io, allocator: std.mem.Allocator) void { var events: [64]std.posix.Kevent = undefined; while (true) { // Block indefinitely until kqueue has events. @@ -130,7 +132,7 @@ fn thread_fn(self: *@This(), allocator: std.mem.Allocator) void { // free it before we finish using it. var file_path_buf: [std.fs.max_path_bytes]u8 = undefined; var file_path_len: usize = 0; - self.file_watches_mutex.lock(); + self.file_watches_mutex.lockUncancelable(io); var fwit = self.file_watches.iterator(); while (fwit.next()) |entry| { if (entry.value_ptr.* == fd) { @@ -139,7 +141,7 @@ fn thread_fn(self: *@This(), allocator: std.mem.Allocator) void { break; } } - self.file_watches_mutex.unlock(); + self.file_watches_mutex.unlock(io); if (file_path_len > 0) { const fp = file_path_buf[0..file_path_len]; if (ev.fflags & (NOTE_WRITE | NOTE_EXTEND) != 0) @@ -154,7 +156,7 @@ fn thread_fn(self: *@This(), allocator: std.mem.Allocator) void { // Same copy-under-lock pattern. var dir_path_buf: [std.fs.max_path_bytes]u8 = undefined; var dir_path_len: usize = 0; - self.watches_mutex.lock(); + self.watches_mutex.lockUncancelable(io); var wit = self.watches.iterator(); while (wit.next()) |entry| { if (entry.value_ptr.* == fd) { @@ -163,7 +165,7 @@ fn thread_fn(self: *@This(), allocator: std.mem.Allocator) void { break; } } - self.watches_mutex.unlock(); + self.watches_mutex.unlock(io); if (dir_path_len == 0) continue; const dir_path = dir_path_buf[0..dir_path_len]; if (ev.fflags & NOTE_DELETE != 0) { @@ -190,8 +192,8 @@ fn thread_fn(self: *@This(), allocator: std.mem.Allocator) void { // Scan a directory and diff against the snapshot, emitting created/deleted events. fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) !void { - var dir = std.fs.openDirAbsolute(dir_path, .{ .iterate = true }) catch return; - defer dir.close(); + var dir = std.Io.Dir.openDirAbsolute(self.io, dir_path, .{ .iterate = true }) catch return; + defer dir.close(self.io); // Arena for all temporaries - freed in one shot at the end. var arena = std.heap.ArenaAllocator.init(allocator); @@ -202,7 +204,7 @@ fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) var current_files: std.StringHashMapUnmanaged(void) = .empty; var current_dirs: std.ArrayListUnmanaged([]u8) = .empty; var iter = dir.iterate(); - while (try iter.next()) |entry| { + while (try iter.next(self.io)) |entry| { switch (entry.kind) { .file => { const name = try tmp.dupe(u8, entry.name); @@ -231,9 +233,9 @@ fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) // Use a boolean flag rather than errdefer to unlock the mutex. An errdefer // scoped to the whole function would fire again after the explicit unlock below, // double-unlocking the mutex (UB) if handler.change() later returns an error. - self.snapshots_mutex.lock(); + self.snapshots_mutex.lockUncancelable(self.io); var snapshots_locked = true; - defer if (snapshots_locked) self.snapshots_mutex.unlock(); + defer if (snapshots_locked) self.snapshots_mutex.unlock(self.io); { for (current_dirs.items) |name| { var path_buf: [std.fs.max_path_bytes]u8 = undefined; @@ -281,7 +283,7 @@ fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) to_delete_owned = true; // ownership transferred; defer will free all items } snapshots_locked = false; - self.snapshots_mutex.unlock(); + self.snapshots_mutex.unlock(self.io); // Emit all events outside the lock so handlers may safely call watch()/unwatch(). // Emit created dirs, then deletions, then creations. Deletions first ensures that @@ -314,10 +316,10 @@ fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) // scan_dir (e.g. a directory moved into the watched tree) so callers see // individual 'created' events for all pre-existing contents. fn emit_subtree_created(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) !void { - var dir = std.fs.openDirAbsolute(dir_path, .{ .iterate = true }) catch return; - defer dir.close(); + var dir = std.Io.Dir.openDirAbsolute(self.io, dir_path, .{ .iterate = true }) catch return; + defer dir.close(self.io); var iter = dir.iterate(); - while (iter.next() catch return) |entry| { + while (iter.next(self.io) catch return) |entry| { const ot: ObjectType = switch (entry.kind) { .file => .file, .directory => .dir, @@ -338,9 +340,9 @@ fn emit_subtree_created(self: *@This(), allocator: std.mem.Allocator, dir_path: } fn register_file_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) void { - self.file_watches_mutex.lock(); + self.file_watches_mutex.lockUncancelable(self.io); const already = self.file_watches.contains(path); - self.file_watches_mutex.unlock(); + self.file_watches_mutex.unlock(self.io); if (already) return; const fd = std.posix.open(path, .{ .ACCMODE = .RDONLY }, 0) catch return; const kev = std.posix.Kevent{ @@ -352,43 +354,43 @@ fn register_file_watch(self: *@This(), allocator: std.mem.Allocator, path: []con .udata = 0, }; _ = std.posix.kevent(self.kq, &.{kev}, &.{}, null) catch { - std.posix.close(fd); + std.Io.Threaded.closeFd(fd); return; }; const owned = allocator.dupe(u8, path) catch { - std.posix.close(fd); + std.Io.Threaded.closeFd(fd); return; }; - self.file_watches_mutex.lock(); + self.file_watches_mutex.lockUncancelable(self.io); if (self.file_watches.contains(path)) { - self.file_watches_mutex.unlock(); - std.posix.close(fd); + self.file_watches_mutex.unlock(self.io); + std.Io.Threaded.closeFd(fd); allocator.free(owned); return; } self.file_watches.put(allocator, owned, fd) catch { - self.file_watches_mutex.unlock(); - std.posix.close(fd); + self.file_watches_mutex.unlock(self.io); + std.Io.Threaded.closeFd(fd); allocator.free(owned); return; }; - self.file_watches_mutex.unlock(); + self.file_watches_mutex.unlock(self.io); } fn deregister_file_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) void { - self.file_watches_mutex.lock(); + self.file_watches_mutex.lockUncancelable(self.io); const kv = self.file_watches.fetchRemove(path); - self.file_watches_mutex.unlock(); + self.file_watches_mutex.unlock(self.io); if (kv) |entry| { - std.posix.close(entry.value); + std.Io.Threaded.closeFd(entry.value); allocator.free(entry.key); } } pub fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) error{ WatchFailed, OutOfMemory }!void { - self.watches_mutex.lock(); + self.watches_mutex.lockUncancelable(self.io); const already = self.watches.contains(path); - self.watches_mutex.unlock(); + self.watches_mutex.unlock(self.io); if (already) return; const path_fd = std.posix.open(path, .{ .ACCMODE = .RDONLY }, 0) catch |e| switch (e) { error.AccessDenied, @@ -420,7 +422,7 @@ pub fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) return error.WatchFailed; }, }; - errdefer std.posix.close(path_fd); + errdefer std.Io.Threaded.closeFd(path_fd); const kev = std.posix.Kevent{ .ident = @intCast(path_fd), .filter = EVFILT_VNODE, @@ -441,19 +443,19 @@ pub fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) }, }; const owned_path = try allocator.dupe(u8, path); - self.watches_mutex.lock(); + self.watches_mutex.lockUncancelable(self.io); if (self.watches.contains(path)) { - self.watches_mutex.unlock(); - std.posix.close(path_fd); + self.watches_mutex.unlock(self.io); + std.Io.Threaded.closeFd(path_fd); allocator.free(owned_path); return; } self.watches.put(allocator, owned_path, path_fd) catch |e| { - self.watches_mutex.unlock(); + self.watches_mutex.unlock(self.io); allocator.free(owned_path); return e; }; - self.watches_mutex.unlock(); + self.watches_mutex.unlock(self.io); // Take initial snapshot so first NOTE_WRITE has a baseline to diff against. self.take_snapshot(allocator, owned_path) catch |e| switch (e) { error.AccessDenied, @@ -470,8 +472,8 @@ pub fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) } fn take_snapshot(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) !void { - var dir = std.fs.openDirAbsolute(dir_path, .{ .iterate = true }) catch return; - defer dir.close(); + var dir = std.Io.Dir.openDirAbsolute(self.io, dir_path, .{ .iterate = true }) catch return; + defer dir.close(self.io); // Collect file names first so we can register file watches without holding the lock. var names: std.ArrayListUnmanaged([]u8) = .empty; defer { @@ -479,12 +481,12 @@ fn take_snapshot(self: *@This(), allocator: std.mem.Allocator, dir_path: []const names.deinit(allocator); } var iter = dir.iterate(); - while (try iter.next()) |entry| { + while (try iter.next(self.io)) |entry| { if (entry.kind != .file) continue; try names.append(allocator, try allocator.dupe(u8, entry.name)); } - self.snapshots_mutex.lock(); - errdefer self.snapshots_mutex.unlock(); + self.snapshots_mutex.lockUncancelable(self.io); + errdefer self.snapshots_mutex.unlock(self.io); const gop = try self.snapshots.getOrPut(allocator, dir_path); if (!gop.found_existing) { // Snapshot outer keys are independently owned so they can be safely @@ -504,7 +506,7 @@ fn take_snapshot(self: *@This(), allocator: std.mem.Allocator, dir_path: []const return e; }; } - self.snapshots_mutex.unlock(); + self.snapshots_mutex.unlock(self.io); // Register a kqueue watch for each existing file so writes are detected. for (names.items) |name| { var path_buf: [std.fs.max_path_bytes]u8 = undefined; @@ -514,16 +516,16 @@ fn take_snapshot(self: *@This(), allocator: std.mem.Allocator, dir_path: []const } pub fn remove_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) void { - self.watches_mutex.lock(); + self.watches_mutex.lockUncancelable(self.io); const watches_entry = self.watches.fetchRemove(path); - self.watches_mutex.unlock(); + self.watches_mutex.unlock(self.io); if (watches_entry) |entry| { - std.posix.close(entry.value); + std.Io.Threaded.closeFd(entry.value); allocator.free(entry.key); } - self.snapshots_mutex.lock(); + self.snapshots_mutex.lockUncancelable(self.io); const snap_entry = self.snapshots.fetchRemove(path); - self.snapshots_mutex.unlock(); + self.snapshots_mutex.unlock(self.io); if (snap_entry) |entry| { allocator.free(entry.key); // independently owned; see take_snapshot/scan_dir var names = entry.value; diff --git a/src/backend/KQueueDir.zig b/src/backend/KQueueDir.zig index fbd08d4..aeab174 100644 --- a/src/backend/KQueueDir.zig +++ b/src/backend/KQueueDir.zig @@ -17,13 +17,14 @@ kq: std.posix.fd_t, shutdown_pipe: [2]std.posix.fd_t, // [0]=read [1]=write; write a byte to wake the thread thread: ?std.Thread, watches: std.StringHashMapUnmanaged(WatchEntry), // owned path -> {fd, is_file} -watches_mutex: std.Thread.Mutex, +watches_mutex: std.Io.Mutex, // Per-directory snapshots: owned filename -> mtime_ns. // Used to diff on NOTE_WRITE: detects creates, deletes, and (opportunistically) // modifications when the same directory fires another event later. // Key: independently owned dir path, value: map of owned filename -> mtime_ns. snapshots: std.StringHashMapUnmanaged(std.StringHashMapUnmanaged(i128)), -snapshots_mutex: std.Thread.Mutex, +snapshots_mutex: std.Io.Mutex, +io: std.Io, const EVFILT_VNODE: i16 = -4; const EVFILT_READ: i16 = -1; @@ -37,13 +38,13 @@ const NOTE_EXTEND: u32 = 0x00000004; const NOTE_ATTRIB: u32 = 0x00000008; const NOTE_RENAME: u32 = 0x00000020; -pub fn init(handler: *Handler) (std.posix.KQueueError || std.posix.KEventError)!@This() { +pub fn init(io: std.Io, handler: *Handler) (std.posix.KQueueError || std.posix.KEventError)!@This() { const kq = try std.posix.kqueue(); - errdefer std.posix.close(kq); - const pipe = try std.posix.pipe(); + errdefer std.Io.Threaded.closeFd(kq); + const pipe = try std.Io.Threaded.pipe2(.{}); errdefer { - std.posix.close(pipe[0]); - std.posix.close(pipe[1]); + std.Io.Threaded.closeFd(pipe[0]); + std.Io.Threaded.closeFd(pipe[1]); } // Register the read end of the shutdown pipe with kqueue so the thread // wakes up when we want to shut down. @@ -62,21 +63,22 @@ pub fn init(handler: *Handler) (std.posix.KQueueError || std.posix.KEventError)! .shutdown_pipe = pipe, .thread = null, .watches = .empty, - .watches_mutex = .{}, + .watches_mutex = std.Io.Mutex.init, .snapshots = .empty, - .snapshots_mutex = .{}, + .snapshots_mutex = std.Io.Mutex.init, + .io = io, }; } pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { // Signal the thread to exit by writing to the shutdown pipe. - _ = std.posix.write(self.shutdown_pipe[1], &[_]u8{0}) catch {}; + _ = std.posix.system.write(self.shutdown_pipe[1], &[_]u8{0}, 1); if (self.thread) |t| t.join(); - std.posix.close(self.shutdown_pipe[0]); - std.posix.close(self.shutdown_pipe[1]); + std.Io.Threaded.closeFd(self.shutdown_pipe[0]); + std.Io.Threaded.closeFd(self.shutdown_pipe[1]); var it = self.watches.iterator(); while (it.next()) |entry| { - std.posix.close(entry.value_ptr.*.fd); + std.Io.Threaded.closeFd(entry.value_ptr.*.fd); allocator.free(entry.key_ptr.*); } self.watches.deinit(allocator); @@ -89,15 +91,15 @@ pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { snap.deinit(allocator); } self.snapshots.deinit(allocator); - std.posix.close(self.kq); + std.Io.Threaded.closeFd(self.kq); } pub fn arm(self: *@This(), allocator: std.mem.Allocator) (error{AlreadyArmed} || std.Thread.SpawnError)!void { if (self.thread != null) return error.AlreadyArmed; - self.thread = try std.Thread.spawn(.{}, thread_fn, .{ self, allocator }); + self.thread = try std.Thread.spawn(.{}, thread_fn, .{ self, self.io, allocator }); } -fn thread_fn(self: *@This(), allocator: std.mem.Allocator) void { +fn thread_fn(self: *@This(), io: std.Io, allocator: std.mem.Allocator) void { var events: [64]std.posix.Kevent = undefined; while (true) { // Block indefinitely until kqueue has events. @@ -115,7 +117,7 @@ fn thread_fn(self: *@This(), allocator: std.mem.Allocator) void { var watch_path_buf: [std.fs.max_path_bytes]u8 = undefined; var watch_path_len: usize = 0; var is_file: bool = false; - self.watches_mutex.lock(); + self.watches_mutex.lockUncancelable(io); var wit = self.watches.iterator(); while (wit.next()) |entry| { if (entry.value_ptr.*.fd == fd) { @@ -125,7 +127,7 @@ fn thread_fn(self: *@This(), allocator: std.mem.Allocator) void { break; } } - self.watches_mutex.unlock(); + self.watches_mutex.unlock(io); if (watch_path_len == 0) continue; const watch_path = watch_path_buf[0..watch_path_len]; if (is_file) { @@ -174,8 +176,8 @@ fn thread_fn(self: *@This(), allocator: std.mem.Allocator) void { // written before a NOTE_WRITE fires for another reason (create/delete/rename of a sibling), // the mtime diff will catch it. fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) !void { - var dir = std.fs.openDirAbsolute(dir_path, .{ .iterate = true }) catch return; - defer dir.close(); + var dir = std.Io.Dir.openDirAbsolute(self.io, dir_path, .{ .iterate = true }) catch return; + defer dir.close(self.io); // Arena for all temporaries - freed in one shot at the end. var arena = std.heap.ArenaAllocator.init(allocator); @@ -187,10 +189,10 @@ fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) var current_files: std.StringHashMapUnmanaged(i128) = .empty; var current_dirs: std.ArrayListUnmanaged([]u8) = .empty; var iter = dir.iterate(); - while (try iter.next()) |entry| { + while (try iter.next(self.io)) |entry| { switch (entry.kind) { .file => { - const mtime = (dir.statFile(entry.name) catch continue).mtime; + const mtime = (dir.statFile(self.io, entry.name, .{}) catch continue).mtime; const name = try tmp.dupe(u8, entry.name); try current_files.put(tmp, name, mtime); }, @@ -218,9 +220,9 @@ fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) // Use a boolean flag rather than errdefer to unlock the mutex. An errdefer // scoped to the whole function would fire again after the explicit unlock below, // double-unlocking the mutex (UB) if handler.change() later returns an error. - self.snapshots_mutex.lock(); + self.snapshots_mutex.lockUncancelable(self.io); var snapshots_locked = true; - defer if (snapshots_locked) self.snapshots_mutex.unlock(); + defer if (snapshots_locked) self.snapshots_mutex.unlock(self.io); { for (current_dirs.items) |name| { var path_buf: [std.fs.max_path_bytes]u8 = undefined; @@ -276,7 +278,7 @@ fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) to_delete_owned = true; // ownership transferred; defer will free all items } snapshots_locked = false; - self.snapshots_mutex.unlock(); + self.snapshots_mutex.unlock(self.io); // Emit all events outside the lock so handlers may safely call watch()/unwatch(). // Order: new dirs, deletions (source before dest for renames), creations, modifications. @@ -310,10 +312,10 @@ fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) // Called after a new dir appears in scan_dir so callers see individual // 'created' events for the pre-existing tree of a moved-in directory. fn emit_subtree_created(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) !void { - var dir = std.fs.openDirAbsolute(dir_path, .{ .iterate = true }) catch return; - defer dir.close(); + var dir = std.Io.Dir.openDirAbsolute(self.io, dir_path, .{ .iterate = true }) catch return; + defer dir.close(self.io); var iter = dir.iterate(); - while (iter.next() catch return) |entry| { + while (iter.next(self.io) catch return) |entry| { const ot: ObjectType = switch (entry.kind) { .file => .file, .directory => .dir, @@ -332,9 +334,9 @@ fn emit_subtree_created(self: *@This(), allocator: std.mem.Allocator, dir_path: } pub fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) error{ WatchFailed, OutOfMemory }!void { - self.watches_mutex.lock(); + self.watches_mutex.lockUncancelable(self.io); const already = self.watches.contains(path); - self.watches_mutex.unlock(); + self.watches_mutex.unlock(self.io); if (already) return; const path_fd = std.posix.open(path, .{ .ACCMODE = .RDONLY }, 0) catch |e| switch (e) { error.AccessDenied, @@ -366,7 +368,7 @@ pub fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) return error.WatchFailed; }, }; - errdefer std.posix.close(path_fd); + errdefer std.Io.Threaded.closeFd(path_fd); const kev = std.posix.Kevent{ .ident = @intCast(path_fd), .filter = EVFILT_VNODE, @@ -390,19 +392,19 @@ pub fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) const stat = std.posix.fstat(path_fd) catch null; const is_file = if (stat) |s| std.posix.S.ISREG(s.mode) else false; const owned_path = try allocator.dupe(u8, path); - self.watches_mutex.lock(); + self.watches_mutex.lockUncancelable(self.io); if (self.watches.contains(path)) { - self.watches_mutex.unlock(); - std.posix.close(path_fd); + self.watches_mutex.unlock(self.io); + std.Io.Threaded.closeFd(path_fd); allocator.free(owned_path); return; } self.watches.put(allocator, owned_path, .{ .fd = path_fd, .is_file = is_file }) catch |e| { - self.watches_mutex.unlock(); + self.watches_mutex.unlock(self.io); allocator.free(owned_path); return e; }; - self.watches_mutex.unlock(); + self.watches_mutex.unlock(self.io); // For directory watches only: take initial snapshot so first NOTE_WRITE has a baseline. if (!is_file) { self.take_snapshot(allocator, owned_path) catch return error.OutOfMemory; @@ -410,8 +412,8 @@ pub fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) } fn take_snapshot(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) !void { - var dir = std.fs.openDirAbsolute(dir_path, .{ .iterate = true }) catch return; - defer dir.close(); + var dir = std.Io.Dir.openDirAbsolute(self.io, dir_path, .{ .iterate = true }) catch return; + defer dir.close(self.io); // Collect (name, mtime) pairs without holding the lock so that statFile // syscalls don't stall the background thread waiting for snapshots_mutex. @@ -421,15 +423,15 @@ fn take_snapshot(self: *@This(), allocator: std.mem.Allocator, dir_path: []const const Entry = struct { name: []u8, mtime: i128 }; var entries: std.ArrayListUnmanaged(Entry) = .empty; var iter = dir.iterate(); - while (iter.next() catch null) |e| { + while (iter.next(self.io) catch null) |e| { if (e.kind != .file) continue; - const mtime = (dir.statFile(e.name) catch continue).mtime; + const mtime = (dir.statFile(self.io, e.name, .{}) catch continue).mtime; const name = tmp.dupe(u8, e.name) catch continue; entries.append(tmp, .{ .name = name, .mtime = mtime }) catch continue; } - self.snapshots_mutex.lock(); - errdefer self.snapshots_mutex.unlock(); + self.snapshots_mutex.lockUncancelable(self.io); + errdefer self.snapshots_mutex.unlock(self.io); const gop = try self.snapshots.getOrPut(allocator, dir_path); if (!gop.found_existing) { // Snapshot outer keys are independently owned so they can be safely @@ -446,20 +448,20 @@ fn take_snapshot(self: *@This(), allocator: std.mem.Allocator, dir_path: []const const owned = allocator.dupe(u8, e.name) catch continue; snapshot.put(allocator, owned, e.mtime) catch allocator.free(owned); } - self.snapshots_mutex.unlock(); + self.snapshots_mutex.unlock(self.io); } pub fn remove_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) void { - self.watches_mutex.lock(); + self.watches_mutex.lockUncancelable(self.io); const watches_entry = self.watches.fetchRemove(path); - self.watches_mutex.unlock(); + self.watches_mutex.unlock(self.io); if (watches_entry) |entry| { - std.posix.close(entry.value.fd); + std.Io.Threaded.closeFd(entry.value.fd); allocator.free(entry.key); } - self.snapshots_mutex.lock(); + self.snapshots_mutex.lockUncancelable(self.io); const snap_entry = self.snapshots.fetchRemove(path); - self.snapshots_mutex.unlock(); + self.snapshots_mutex.unlock(self.io); if (snap_entry) |entry| { allocator.free(entry.key); // independently owned; see take_snapshot/scan_dir var snap = entry.value; diff --git a/src/backend/Windows.zig b/src/backend/Windows.zig index f2ff9e7..b94e904 100644 --- a/src/backend/Windows.zig +++ b/src/backend/Windows.zig @@ -51,10 +51,11 @@ const win32 = struct { }; handler: *Handler, +io: std.Io, iocp: windows.HANDLE, thread: ?std.Thread, watches: std.StringHashMapUnmanaged(*Watch), -watches_mutex: std.Thread.Mutex, +watches_mutex: std.Io.Mutex, path_types: std.StringHashMapUnmanaged(ObjectType), // A completion key of zero is used to signal the background thread to exit. @@ -90,9 +91,9 @@ const notify_filter: windows.DWORD = 0x00000010 | // FILE_NOTIFY_CHANGE_LAST_WRITE 0x00000040; // FILE_NOTIFY_CHANGE_CREATION -pub fn init(handler: *Handler) windows.CreateIoCompletionPortError!@This() { +pub fn init(io: std.Io, handler: *Handler) windows.CreateIoCompletionPortError!@This() { const iocp = try windows.CreateIoCompletionPort(windows.INVALID_HANDLE_VALUE, null, 0, 1); - return .{ .handler = handler, .iocp = iocp, .thread = null, .watches = .empty, .watches_mutex = .{}, .path_types = .empty }; + return .{ .handler = handler, .io = io, .iocp = iocp, .thread = null, .watches = .empty, .watches_mutex = std.Io.Mutex.init, .path_types = .empty }; } pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { @@ -116,14 +117,15 @@ pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { pub fn arm(self: *@This(), allocator: std.mem.Allocator) (error{AlreadyArmed} || std.Thread.SpawnError)!void { if (self.thread != null) return error.AlreadyArmed; - self.thread = try std.Thread.spawn(.{}, thread_fn, .{ allocator, self.iocp, &self.watches, &self.watches_mutex, &self.path_types, self.handler }); + self.thread = try std.Thread.spawn(.{}, thread_fn, .{ allocator, self.io, self.iocp, &self.watches, &self.watches_mutex, &self.path_types, self.handler }); } fn thread_fn( allocator: std.mem.Allocator, + io: std.Io, iocp: windows.HANDLE, watches: *std.StringHashMapUnmanaged(*Watch), - watches_mutex: *std.Thread.Mutex, + watches_mutex: *std.Io.Mutex, path_types: *std.StringHashMapUnmanaged(ObjectType), handler: *Handler, ) void { @@ -135,7 +137,7 @@ fn thread_fn( const ok = win32.GetQueuedCompletionStatus(iocp, &bytes, &key, &overlapped_ptr, windows.INFINITE); if (ok == 0 or key == SHUTDOWN_KEY) return; const triggered_handle: windows.HANDLE = @ptrFromInt(key); - watches_mutex.lock(); + watches_mutex.lockUncancelable(io); var it = watches.iterator(); while (it.next()) |entry| { const w = entry.value_ptr.*; @@ -198,16 +200,16 @@ fn thread_fn( } // For dirs, also scan children into cache. if (pr.object_type == .dir) - scan_path_types_into(allocator, path_types, full_path); + scan_path_types_into(allocator, io, path_types, full_path); const next_entry_offset = info.NextEntryOffset; - watches_mutex.unlock(); + watches_mutex.unlock(io); handler.rename(src, full_path, pr.object_type) catch |e| { std.log.err("nightwatch: handler returned {s}, stopping watch thread", .{@errorName(e)}); - watches_mutex.lock(); + watches_mutex.lockUncancelable(io); pending_rename = null; break; }; - watches_mutex.lock(); + watches_mutex.lockUncancelable(io); pending_rename = null; if (next_entry_offset == 0) break; offset += next_entry_offset; @@ -259,7 +261,7 @@ fn thread_fn( // contents into the cache so subsequent deletes resolve their // type. Scanning a genuinely new (empty) directory is a no-op. if (ot == .dir and event_type == .created) - scan_path_types_into(allocator, path_types, full_path); + scan_path_types_into(allocator, io, path_types, full_path); break :blk ot; }; // Suppress FILE_ACTION_MODIFIED on directories: these are @@ -274,20 +276,20 @@ fn thread_fn( // the main thread may call remove_watch() which frees w.buf, making // the `info` pointer (which points into w.buf) a dangling reference. const next_entry_offset = info.NextEntryOffset; - watches_mutex.unlock(); + watches_mutex.unlock(io); handler.change(full_path, event_type, object_type) catch |e| { std.log.err("nightwatch: handler returned {s}, stopping watch thread", .{@errorName(e)}); - watches_mutex.lock(); + watches_mutex.lockUncancelable(io); break; }; if (event_type == .created and object_type == .dir) { - emit_subtree_created(handler, full_path) catch |e| { + emit_subtree_created(handler, io, full_path) catch |e| { std.log.err("nightwatch: handler returned {s}, stopping watch thread", .{@errorName(e)}); - watches_mutex.lock(); + watches_mutex.lockUncancelable(io); break; }; } - watches_mutex.lock(); + watches_mutex.lockUncancelable(io); if (next_entry_offset == 0) break; offset += next_entry_offset; } @@ -295,9 +297,9 @@ fn thread_fn( // Emit as .deleted, matching INotify behaviour. if (pending_rename) |pr| { const src = pr.path_buf[0..pr.path_len]; - watches_mutex.unlock(); + watches_mutex.unlock(io); handler.change(src, .deleted, pr.object_type) catch {}; - watches_mutex.lock(); + watches_mutex.lockUncancelable(io); } } // Re-arm ReadDirectoryChangesW for the next batch. @@ -306,13 +308,13 @@ fn thread_fn( std.log.err("nightwatch: ReadDirectoryChangesW re-arm failed for {s}, future events lost", .{entry.key_ptr.*}); break; } - watches_mutex.unlock(); + watches_mutex.unlock(io); } } pub fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) error{ OutOfMemory, WatchFailed }!void { - self.watches_mutex.lock(); - defer self.watches_mutex.unlock(); + self.watches_mutex.lockUncancelable(self.io); + defer self.watches_mutex.unlock(self.io); if (self.watches.contains(path)) return; const path_w = std.unicode.utf8ToUtf16LeAllocZ(allocator, path) catch return error.WatchFailed; defer allocator.free(path_w); @@ -353,11 +355,11 @@ pub fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) // Called after a directory is created or moved into the watched tree so callers // see individual 'created' events for all pre-existing contents. // Must be called with watches_mutex NOT held (handler calls are outside the lock). -fn emit_subtree_created(handler: *Handler, dir_path: []const u8) error{HandlerFailed}!void { - var dir = std.fs.openDirAbsolute(dir_path, .{ .iterate = true }) catch return; - defer dir.close(); +fn emit_subtree_created(handler: *Handler, io: std.Io, dir_path: []const u8) error{HandlerFailed}!void { + var dir = std.Io.Dir.openDirAbsolute(io, dir_path, .{ .iterate = true }) catch return; + defer dir.close(io); var iter = dir.iterate(); - while (iter.next() catch return) |entry| { + while (iter.next(io) catch return) |entry| { const ot: ObjectType = switch (entry.kind) { .file => .file, .directory => .dir, @@ -366,19 +368,19 @@ fn emit_subtree_created(handler: *Handler, dir_path: []const u8) error{HandlerFa var path_buf: [std.fs.max_path_bytes]u8 = undefined; const full_path = std.fmt.bufPrint(&path_buf, "{s}\\{s}", .{ dir_path, entry.name }) catch continue; try handler.change(full_path, EventType.created, ot); - if (ot == .dir) try emit_subtree_created(handler, full_path); + if (ot == .dir) try emit_subtree_created(handler, io, full_path); } } fn scan_path_types(self: *@This(), allocator: std.mem.Allocator, root: []const u8) void { - scan_path_types_into(allocator, &self.path_types, root); + scan_path_types_into(allocator, self.io, &self.path_types, root); } -fn scan_path_types_into(allocator: std.mem.Allocator, path_types: *std.StringHashMapUnmanaged(ObjectType), root: []const u8) void { - var dir = std.fs.openDirAbsolute(root, .{ .iterate = true }) catch return; - defer dir.close(); +fn scan_path_types_into(allocator: std.mem.Allocator, io: std.Io, path_types: *std.StringHashMapUnmanaged(ObjectType), root: []const u8) void { + var dir = std.Io.Dir.openDirAbsolute(io, root, .{ .iterate = true }) catch return; + defer dir.close(io); var iter = dir.iterate(); - while (iter.next() catch return) |entry| { + while (iter.next(io) catch return) |entry| { const ot: ObjectType = switch (entry.kind) { .directory => .dir, .file => .file, @@ -394,13 +396,13 @@ fn scan_path_types_into(allocator: std.mem.Allocator, path_types: *std.StringHas }; } gop.value_ptr.* = ot; - if (ot == .dir) scan_path_types_into(allocator, path_types, gop.key_ptr.*); + if (ot == .dir) scan_path_types_into(allocator, io, path_types, gop.key_ptr.*); } } pub fn remove_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) void { - self.watches_mutex.lock(); - defer self.watches_mutex.unlock(); + self.watches_mutex.lockUncancelable(self.io); + defer self.watches_mutex.unlock(self.io); if (self.watches.fetchRemove(path)) |entry| { const w = entry.value; _ = win32.CloseHandle(w.handle); diff --git a/src/main.zig b/src/main.zig index 1d8a096..120eb0d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -17,14 +17,15 @@ const is_posix = switch (builtin.os.tag) { // Self-pipe: signal handler writes a byte so poll() / read() unblocks cleanly. var sig_pipe: if (is_posix) [2]std.posix.fd_t else void = undefined; -fn posix_sighandler(_: c_int) callconv(.c) void { - _ = std.posix.write(sig_pipe[1], &[_]u8{0}) catch {}; +fn posix_sighandler(_: std.posix.SIG) callconv(.c) void { + _ = std.posix.system.write(sig_pipe[1], &[_]u8{0}, 1); } const CliHandler = struct { handler: Watcher.Handler, - out: std.fs.File, - tty: std.io.tty.Config, + io: std.Io, + out: std.Io.File, + tty_mode: std.Io.Terminal.Mode, ignore: []const []const u8, const vtable: Watcher.Handler.VTable = switch (Watcher.interface_type) { @@ -45,9 +46,9 @@ const CliHandler = struct { if (std.mem.eql(u8, path, ignored)) return; } var buf: [4096]u8 = undefined; - var w = self.out.writer(&buf); - defer w.interface.flush() catch {}; - const color: std.io.tty.Color = switch (event_type) { + var w = std.Io.File.writer(self.out, self.io, &buf); + defer w.flush() catch {}; + const color: std.Io.Terminal.Color = switch (event_type) { .created => .green, .modified => .blue, .closed => .bright_black, @@ -59,9 +60,10 @@ const CliHandler = struct { .closed => "close ", .deleted => "delete ", }; - self.tty.setColor(&w.interface, color) catch return error.HandlerFailed; + const tty = std.Io.Terminal{ .writer = &w.interface, .mode = self.tty_mode }; + tty.setColor(color) catch return error.HandlerFailed; w.interface.writeAll(event_label) catch return error.HandlerFailed; - self.tty.setColor(&w.interface, .reset) catch return error.HandlerFailed; + tty.setColor(.reset) catch return error.HandlerFailed; w.interface.writeAll(" ") catch return error.HandlerFailed; self.writeTypeLabel(&w.interface, object_type) catch return error.HandlerFailed; w.interface.print(" {s}\n", .{path}) catch return error.HandlerFailed; @@ -73,27 +75,29 @@ const CliHandler = struct { if (std.mem.eql(u8, src, ignored) or std.mem.eql(u8, dst, ignored)) return; } var buf: [4096]u8 = undefined; - var w = self.out.writer(&buf); - defer w.interface.flush() catch {}; - self.tty.setColor(&w.interface, .magenta) catch return error.HandlerFailed; + var w = std.Io.File.writer(self.out, self.io, &buf); + defer w.flush() catch {}; + const tty = std.Io.Terminal{ .writer = &w.interface, .mode = self.tty_mode }; + tty.setColor(.magenta) catch return error.HandlerFailed; w.interface.writeAll("rename ") catch return error.HandlerFailed; - self.tty.setColor(&w.interface, .reset) catch return error.HandlerFailed; + tty.setColor(.reset) catch return error.HandlerFailed; w.interface.writeAll(" ") catch return error.HandlerFailed; self.writeTypeLabel(&w.interface, object_type) catch return error.HandlerFailed; w.interface.print(" {s} -> {s}\n", .{ src, dst }) catch return error.HandlerFailed; } - fn writeTypeLabel(self: *CliHandler, w: *std.io.Writer, object_type: nightwatch.ObjectType) !void { + fn writeTypeLabel(self: *CliHandler, w: *std.Io.Writer, object_type: nightwatch.ObjectType) !void { + const tty = std.Io.Terminal{ .writer = w, .mode = self.tty_mode }; switch (object_type) { .file => { - try self.tty.setColor(w, .cyan); + try tty.setColor(.cyan); try w.writeAll("file"); - try self.tty.setColor(w, .reset); + try tty.setColor(.reset); }, .dir => { - try self.tty.setColor(w, .yellow); + try tty.setColor(.yellow); try w.writeAll("dir "); - try self.tty.setColor(w, .reset); + try tty.setColor(.reset); }, .unknown => try w.writeAll("? "), } @@ -145,9 +149,9 @@ fn run_windows() void { } } -fn usage(out: std.fs.File) !void { +fn usage(io: std.Io, out: std.Io.File) !void { var buf: [4096]u8 = undefined; - var writer = out.writer(&buf); + var writer = std.Io.File.writer(out, io, &buf); try writer.interface.print( \\Usage: nightwatch [--ignore ]... [ ...] \\ @@ -169,12 +173,12 @@ fn usage(out: std.fs.File) !void { \\Stand down with Ctrl-C. \\ , .{}); - try writer.interface.flush(); + try writer.flush(); } -fn version(out: std.fs.File) !void { +fn version(io: std.Io, out: std.Io.File) !void { var buf: [4096]u8 = undefined; - var writer = out.writer(&buf); + var writer = std.Io.File.writer(out, io, &buf); try writer.interface.print( \\nightwatch version {s} \\using: {s} @@ -183,7 +187,7 @@ fn version(out: std.fs.File) !void { @embedFile("version"), @typeName(get_nightwatch().Backend), }); - try writer.interface.flush(); + try writer.flush(); } fn get_nightwatch() type { @@ -193,49 +197,47 @@ fn get_nightwatch() type { }; } -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); +pub fn main(init: std.process.Init) !void { + const allocator = init.gpa; - const args = try std.process.argsAlloc(allocator); - defer std.process.argsFree(allocator, args); + const args = try init.minimal.args.toSlice(init.arena.allocator()); if (args.len < 2) { - try usage(std.fs.File.stderr()); + try usage(init.io, std.Io.File.stderr()); std.process.exit(1); } if (std.mem.eql(u8, args[1], "-h") or std.mem.eql(u8, args[1], "--help")) { - try usage(std.fs.File.stdout()); + try usage(init.io, std.Io.File.stdout()); return; } if (std.mem.eql(u8, args[1], "--version")) { - try version(std.fs.File.stdout()); + try version(init.io, std.Io.File.stdout()); return; } var buf: [4096]u8 = undefined; - var stderr = std.fs.File.stderr().writer(&buf); - defer stderr.interface.flush() catch {}; + var stderr = std.Io.File.writer(std.Io.File.stderr(), init.io, &buf); + defer stderr.flush() catch {}; var out_buf: [4096]u8 = undefined; - var stdout = std.fs.File.stdout().writer(&out_buf); - defer stdout.interface.flush() catch {}; + var stdout = std.Io.File.writer(std.Io.File.stdout(), init.io, &out_buf); + defer stdout.flush() catch {}; // Parse --ignore options and watch paths. // Ignored paths are made absolute (without resolving symlinks) so they // match the absolute paths the backend emits in event callbacks. - var ignore_list = std.ArrayListUnmanaged([]const u8){}; + var ignore_list = std.ArrayListUnmanaged([]const u8).empty; defer { for (ignore_list.items) |p| allocator.free(p); ignore_list.deinit(allocator); } - var watch_paths = std.ArrayListUnmanaged([]const u8){}; + var watch_paths = std.ArrayListUnmanaged([]const u8).empty; defer watch_paths.deinit(allocator); var cwd_buf: [std.fs.max_path_bytes]u8 = undefined; - const cwd = try std.fs.cwd().realpath(".", &cwd_buf); + const cwd_len = try std.Io.Dir.cwd().realPath(init.io, &cwd_buf); + const cwd = cwd_buf[0..cwd_len]; var i: usize = 1; while (i < args.len) : (i += 1) { @@ -257,12 +259,12 @@ pub fn main() !void { } if (watch_paths.items.len == 0) { - try usage(std.fs.File.stderr()); + try usage(init.io, std.Io.File.stderr()); std.process.exit(1); } if (is_posix) { - sig_pipe = try std.posix.pipe(); + sig_pipe = try std.Io.Threaded.pipe2(.{}); const sa = std.posix.Sigaction{ .handler = .{ .handler = posix_sighandler }, .mask = std.posix.sigemptyset(), @@ -272,18 +274,23 @@ pub fn main() !void { std.posix.sigaction(std.posix.SIG.TERM, &sa, null); } defer if (is_posix) { - std.posix.close(sig_pipe[0]); - std.posix.close(sig_pipe[1]); + std.Io.Threaded.closeFd(sig_pipe[0]); + std.Io.Threaded.closeFd(sig_pipe[1]); }; + const NO_COLOR = if (init.environ_map.get("NO_COLOR")) |v| v.len > 0 else false; + const CLICOLOR_FORCE = if (init.environ_map.get("CLICOLOR_FORCE")) |v| v.len > 0 else false; + const tty_mode = std.Io.Terminal.Mode.detect(init.io, std.Io.File.stdout(), NO_COLOR, CLICOLOR_FORCE) catch .no_color; + var cli_handler = CliHandler{ .handler = .{ .vtable = &CliHandler.vtable }, - .out = std.fs.File.stdout(), - .tty = std.io.tty.detectConfig(std.fs.File.stdout()), + .out = std.Io.File.stdout(), + .tty_mode = tty_mode, + .io = init.io, .ignore = ignore_list.items, }; - var watcher = try get_nightwatch().init(allocator, &cli_handler.handler); + var watcher = try get_nightwatch().init(init.io, allocator, &cli_handler.handler); defer watcher.deinit(); @@ -292,11 +299,11 @@ pub fn main() !void { try stderr.interface.print("nightwatch: {s}: {s}\n", .{ path, @errorName(err) }); continue; }; - try cli_handler.tty.setColor(&stdout.interface, .dim); + (std.Io.Terminal{ .writer = &stdout.interface, .mode = tty_mode }).setColor(.dim) catch {}; try stdout.interface.print("# on watch: {s}", .{path}); - try cli_handler.tty.setColor(&stdout.interface, .reset); + (std.Io.Terminal{ .writer = &stdout.interface, .mode = tty_mode }).setColor(.reset) catch {}; try stdout.interface.print("\n", .{}); - try stdout.interface.flush(); + try stdout.flush(); } if (Watcher.interface_type == .polling) { diff --git a/src/nightwatch.zig b/src/nightwatch.zig index 5a3e117..af2147e 100644 --- a/src/nightwatch.zig +++ b/src/nightwatch.zig @@ -103,6 +103,7 @@ pub fn Create(comptime variant: Variant) type { }; allocator: std.mem.Allocator, + io: std.Io, interceptor: *InterceptorType, /// Whether this backend detects file content modifications in real time. @@ -125,19 +126,20 @@ pub fn Create(comptime variant: Variant) type { /// threaded variants the backend's internal thread will call into it /// concurrently; for the polling variant calls happen synchronously /// inside `handle_read_ready()`. - pub fn init(allocator: std.mem.Allocator, handler: *Handler) !@This() { + pub fn init(io: std.Io, allocator: std.mem.Allocator, handler: *Handler) !@This() { const ic = try allocator.create(InterceptorType); errdefer allocator.destroy(ic); ic.* = .{ .handler = .{ .vtable = &InterceptorType.vtable }, .user_handler = handler, + .io = io, .allocator = allocator, .backend = undefined, }; - ic.backend = try Backend.init(&ic.handler); + ic.backend = try Backend.init(io, &ic.handler); errdefer ic.backend.deinit(allocator); try ic.backend.arm(allocator); - return .{ .allocator = allocator, .interceptor = ic }; + return .{ .io = io, .allocator = allocator, .interceptor = ic }; } /// Stop the watcher, release all watches, and free resources. @@ -170,7 +172,8 @@ pub fn Create(comptime variant: Variant) type { path else blk: { var cwd_buf: [std.fs.max_path_bytes]u8 = undefined; - const cwd = std.fs.cwd().realpath(".", &cwd_buf) catch return error.WatchFailed; + const cwd_len = std.Io.Dir.cwd().realPath(self.io, &cwd_buf) catch return error.WatchFailed; + const cwd = cwd_buf[0..cwd_len]; break :blk std.fmt.bufPrint(&buf, "{s}{c}{s}", .{ cwd, std.fs.path.sep, path }) catch return error.WatchFailed; }; // Collapse any . and .. segments without touching the filesystem so that @@ -180,7 +183,7 @@ pub fn Create(comptime variant: Variant) type { defer self.allocator.free(norm); try self.interceptor.backend.add_watch(self.allocator, norm); if (!Backend.watches_recursively) { - recurse_watch(&self.interceptor.backend, self.allocator, norm); + recurse_watch(&self.interceptor.backend, self.io, self.allocator, norm); } } @@ -229,6 +232,7 @@ pub fn Create(comptime variant: Variant) type { const Interceptor = struct { handler: Handler, user_handler: *Handler, + io: std.Io, allocator: std.mem.Allocator, backend: Backend, @@ -242,7 +246,7 @@ pub fn Create(comptime variant: Variant) type { if (event_type == .created and object_type == .dir and !Backend.watches_recursively) { self.backend.add_watch(self.allocator, path) catch |e| std.log.err("nightwatch: add_watch failed for {s}: {s}", .{ path, @errorName(e) }); - recurse_watch(&self.backend, self.allocator, path); + recurse_watch(&self.backend, self.io, self.allocator, path); } return self.user_handler.change(path, event_type, object_type); } @@ -256,6 +260,7 @@ pub fn Create(comptime variant: Variant) type { const PollingInterceptor = struct { handler: PollingHandler, user_handler: *PollingHandler, + io: std.Io, allocator: std.mem.Allocator, backend: Backend, @@ -272,7 +277,7 @@ pub fn Create(comptime variant: Variant) type { if (event_type == .created and object_type == .dir and !Backend.watches_recursively) { self.backend.add_watch(self.allocator, path) catch |e| std.log.err("nightwatch: add_watch failed for {s}: {s}", .{ path, @errorName(e) }); - recurse_watch(&self.backend, self.allocator, path); + recurse_watch(&self.backend, self.io, self.allocator, path); } return self.user_handler.change(path, event_type, object_type); } @@ -289,17 +294,17 @@ pub fn Create(comptime variant: Variant) type { }; // Scans subdirectories of dir_path and adds a watch for each one, recursively. - fn recurse_watch(backend: *Backend, allocator: std.mem.Allocator, dir_path: []const u8) void { - var dir = std.fs.openDirAbsolute(dir_path, .{ .iterate = true }) catch return; - defer dir.close(); + fn recurse_watch(backend: *Backend, io: std.Io, allocator: std.mem.Allocator, dir_path: []const u8) void { + var dir = std.Io.Dir.openDirAbsolute(io, dir_path, .{ .iterate = true }) catch return; + defer dir.close(io); var it = dir.iterate(); - while (it.next() catch return) |entry| { + while (it.next(io) catch return) |entry| { if (entry.kind != .directory) continue; var buf: [std.fs.max_path_bytes]u8 = undefined; const sub = std.fmt.bufPrint(&buf, "{s}{c}{s}", .{ dir_path, std.fs.path.sep, entry.name }) catch continue; backend.add_watch(allocator, sub) catch |e| std.log.err("nightwatch: add_watch failed for {s}: {s}", .{ sub, @errorName(e) }); - recurse_watch(backend, allocator, sub); + recurse_watch(backend, io, allocator, sub); } } }; diff --git a/src/nightwatch_test.zig b/src/nightwatch_test.zig index 79e64c9..7acc307 100644 --- a/src/nightwatch_test.zig +++ b/src/nightwatch_test.zig @@ -137,7 +137,7 @@ fn MakeTestHandler(comptime Watcher: type) type { var temp_dir_counter = std.atomic.Value(u32).init(0); -fn makeTempDir(allocator: std.mem.Allocator) ![]u8 { +fn makeTempDir(io: std.Io, allocator: std.mem.Allocator) ![]u8 { const n = temp_dir_counter.fetchAdd(1, .monotonic); const pid = switch (builtin.os.tag) { .linux => std.os.linux.getpid(), @@ -151,12 +151,13 @@ fn makeTempDir(allocator: std.mem.Allocator) ![]u8 { break :blk try std.fmt.allocPrint(allocator, "{s}\\nightwatch_test_{d}_{d}", .{ tmp_dir, pid, n }); } else try std.fmt.allocPrint(allocator, "/tmp/nightwatch_test_{d}_{d}", .{ pid, n }); errdefer allocator.free(name); - try std.fs.makeDirAbsolute(name); + try std.Io.Dir.createDirAbsolute(io, name, .default_dir); // On macOS /tmp is a symlink to /private/tmp; FSEvents always delivers // canonical paths, so resolve now so all test-constructed paths match. if (builtin.os.tag == .macos) { var real_buf: [std.fs.max_path_bytes]u8 = undefined; - const real = std.fs.realpath(name, &real_buf) catch return name; + const real_len = std.Io.Dir.realPathFileAbsolute(io, name, &real_buf) catch return name; + const real = real_buf[0..real_len]; if (!std.mem.eql(u8, name, real)) { const canon = try allocator.dupe(u8, real); allocator.free(name); @@ -166,16 +167,16 @@ fn makeTempDir(allocator: std.mem.Allocator) ![]u8 { return name; } -fn removeTempDir(path: []const u8) void { - std.fs.deleteTreeAbsolute(path) catch {}; +fn removeTempDir(io: std.Io, path: []const u8) void { + std.Io.Dir.cwd().deleteTree(io, path) catch {}; } /// Drive event delivery for any Watcher variant. -fn drainEvents(comptime Watcher: type, watcher: *Watcher) !void { +fn drainEvents(comptime Watcher: type, io: std.Io, watcher: *Watcher) !void { if (comptime Watcher.interface_type == .polling) { try watcher.handle_read_ready(); } else { - std.Thread.sleep(300 * std.time.ns_per_ms); + try std.Io.sleep(io, std.Io.Duration.fromMilliseconds(300), .awake); } } @@ -183,41 +184,41 @@ fn drainEvents(comptime Watcher: type, watcher: *Watcher) !void { // Individual test case functions, each parametrized on the Watcher type. // --------------------------------------------------------------------------- -fn testCreateFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { +fn testCreateFile(comptime Watcher: type, io: std.Io, allocator: std.mem.Allocator) !void { const TH = MakeTestHandler(Watcher); - const tmp = try makeTempDir(allocator); + const tmp = try makeTempDir(io, allocator); defer { - removeTempDir(tmp); + removeTempDir(io, tmp); allocator.free(tmp); } const th = try TH.init(allocator); defer th.deinit(); - var watcher = try Watcher.init(allocator, &th.handler); + var watcher = try Watcher.init(allocator, io, &th.handler); defer watcher.deinit(); try watcher.watch(tmp); const file_path = try std.fs.path.join(allocator, &.{ tmp, "hello.txt" }); defer allocator.free(file_path); { - const f = try std.fs.createFileAbsolute(file_path, .{}); - f.close(); + const f = try std.Io.Dir.createFileAbsolute(io, file_path, .{}); + f.close(io); } - try drainEvents(Watcher, &watcher); + try drainEvents(Watcher, io, &watcher); try std.testing.expect(th.hasChange(file_path, .created, .file)); } -fn testModifyFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { +fn testModifyFile(comptime Watcher: type, io: std.Io, allocator: std.mem.Allocator) !void { if (comptime !Watcher.detects_file_modifications) return error.SkipZigTest; const TH = MakeTestHandler(Watcher); - const tmp = try makeTempDir(allocator); + const tmp = try makeTempDir(io, allocator); defer { - removeTempDir(tmp); + removeTempDir(io, tmp); allocator.free(tmp); } @@ -227,37 +228,37 @@ fn testModifyFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { const file_path = try std.fs.path.join(allocator, &.{ tmp, "data.txt" }); defer allocator.free(file_path); { - const f = try std.fs.createFileAbsolute(file_path, .{}); - f.close(); + const f = try std.Io.Dir.createFileAbsolute(io, file_path, .{}); + f.close(io); } - var watcher = try Watcher.init(allocator, &th.handler); + var watcher = try Watcher.init(allocator, io, &th.handler); defer watcher.deinit(); try watcher.watch(tmp); // Drain before writing: FSEvents may deliver a coalesced create+modify if the // file was created just before the stream started. A drain here separates any // stale creation event from the upcoming write, so the write arrives in its // own callback with only ItemModified set. - try drainEvents(Watcher, &watcher); + try drainEvents(Watcher, io, &watcher); { - const f = try std.fs.openFileAbsolute(file_path, .{ .mode = .write_only }); - defer f.close(); - try f.writeAll("hello nightwatch\n"); + const f = try std.Io.Dir.openFileAbsolute(io, file_path, .{ .mode = .write_only }); + defer f.close(io); + try f.writeStreamingAll(io, "hello nightwatch\n"); } - try drainEvents(Watcher, &watcher); + try drainEvents(Watcher, io, &watcher); try std.testing.expect(th.hasChange(file_path, .modified, .file)); } -fn testCloseFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { +fn testCloseFile(comptime Watcher: type, io: std.Io, allocator: std.mem.Allocator) !void { if (comptime !Watcher.emits_close_events) return error.SkipZigTest; const TH = MakeTestHandler(Watcher); - const tmp = try makeTempDir(allocator); + const tmp = try makeTempDir(io, allocator); defer { - removeTempDir(tmp); + removeTempDir(io, tmp); allocator.free(tmp); } @@ -267,31 +268,31 @@ fn testCloseFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { const file_path = try std.fs.path.join(allocator, &.{ tmp, "data.txt" }); defer allocator.free(file_path); { - const f = try std.fs.createFileAbsolute(file_path, .{}); - f.close(); + const f = try std.Io.Dir.createFileAbsolute(io, file_path, .{}); + f.close(io); } - var watcher = try Watcher.init(allocator, &th.handler); + var watcher = try Watcher.init(allocator, io, &th.handler); defer watcher.deinit(); try watcher.watch(tmp); { - const f = try std.fs.openFileAbsolute(file_path, .{ .mode = .write_only }); - defer f.close(); - try f.writeAll("hello nightwatch\n"); + const f = try std.Io.Dir.openFileAbsolute(io, file_path, .{ .mode = .write_only }); + defer f.close(io); + try f.writeStreamingAll(io, "hello nightwatch\n"); } - try drainEvents(Watcher, &watcher); + try drainEvents(Watcher, io, &watcher); try std.testing.expect(th.hasChange(file_path, .modified, .file)); try std.testing.expect(th.hasChange(file_path, .closed, .file)); } -fn testDeleteFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { +fn testDeleteFile(comptime Watcher: type, io: std.Io, allocator: std.mem.Allocator) !void { const TH = MakeTestHandler(Watcher); - const tmp = try makeTempDir(allocator); + const tmp = try makeTempDir(io, allocator); defer { - removeTempDir(tmp); + removeTempDir(io, tmp); allocator.free(tmp); } @@ -301,52 +302,52 @@ fn testDeleteFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { const file_path = try std.fs.path.join(allocator, &.{ tmp, "gone.txt" }); defer allocator.free(file_path); - var watcher = try Watcher.init(allocator, &th.handler); + var watcher = try Watcher.init(allocator, io, &th.handler); defer watcher.deinit(); try watcher.watch(tmp); { - const f = try std.fs.createFileAbsolute(file_path, .{}); - f.close(); + const f = try std.Io.Dir.createFileAbsolute(io, file_path, .{}); + f.close(io); } - try drainEvents(Watcher, &watcher); + try drainEvents(Watcher, io, &watcher); - try std.fs.deleteFileAbsolute(file_path); - try drainEvents(Watcher, &watcher); + try std.Io.Dir.deleteFileAbsolute(io, file_path); + try drainEvents(Watcher, io, &watcher); try std.testing.expect(th.hasChange(file_path, .deleted, .file)); } -fn testCreateSubdir(comptime Watcher: type, allocator: std.mem.Allocator) !void { +fn testCreateSubdir(comptime Watcher: type, io: std.Io, allocator: std.mem.Allocator) !void { const TH = MakeTestHandler(Watcher); - const tmp = try makeTempDir(allocator); + const tmp = try makeTempDir(io, allocator); defer { - removeTempDir(tmp); + removeTempDir(io, tmp); allocator.free(tmp); } const th = try TH.init(allocator); defer th.deinit(); - var watcher = try Watcher.init(allocator, &th.handler); + var watcher = try Watcher.init(allocator, io, &th.handler); defer watcher.deinit(); try watcher.watch(tmp); const dir_path = try std.fs.path.join(allocator, &.{ tmp, "subdir" }); defer allocator.free(dir_path); - try std.fs.makeDirAbsolute(dir_path); + try std.Io.Dir.createDirAbsolute(io, dir_path, .default_dir); - try drainEvents(Watcher, &watcher); + try drainEvents(Watcher, io, &watcher); try std.testing.expect(th.hasChange(dir_path, .created, .dir)); } -fn testDeleteSubdir(comptime Watcher: type, allocator: std.mem.Allocator) !void { +fn testDeleteSubdir(comptime Watcher: type, io: std.Io, allocator: std.mem.Allocator) !void { const TH = MakeTestHandler(Watcher); - const tmp = try makeTempDir(allocator); + const tmp = try makeTempDir(io, allocator); defer { - removeTempDir(tmp); + removeTempDir(io, tmp); allocator.free(tmp); } @@ -355,27 +356,26 @@ fn testDeleteSubdir(comptime Watcher: type, allocator: std.mem.Allocator) !void const dir_path = try std.fs.path.join(allocator, &.{ tmp, "subdir" }); defer allocator.free(dir_path); - try std.fs.makeDirAbsolute(dir_path); + try std.Io.Dir.createDirAbsolute(io, dir_path, .default_dir); - var watcher = try Watcher.init(allocator, &th.handler); + var watcher = try Watcher.init(allocator, io, &th.handler); defer watcher.deinit(); try watcher.watch(tmp); - try drainEvents(Watcher, &watcher); + try drainEvents(Watcher, io, &watcher); - try std.fs.deleteDirAbsolute(dir_path); - try drainEvents(Watcher, &watcher); + try std.Io.Dir.deleteDirAbsolute(io, dir_path); + try drainEvents(Watcher, io, &watcher); try std.testing.expect(th.hasChange(dir_path, .deleted, .dir)); } - -fn testRenameFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { +fn testRenameFile(comptime Watcher: type, io: std.Io, allocator: std.mem.Allocator) !void { const TH = MakeTestHandler(Watcher); - const tmp = try makeTempDir(allocator); + const tmp = try makeTempDir(io, allocator); defer { - removeTempDir(tmp); + removeTempDir(io, tmp); allocator.free(tmp); } @@ -388,16 +388,16 @@ fn testRenameFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { defer allocator.free(dst_path); { - const f = try std.fs.createFileAbsolute(src_path, .{}); - f.close(); + const f = try std.Io.Dir.createFileAbsolute(io, src_path, .{}); + f.close(io); } - var watcher = try Watcher.init(allocator, &th.handler); + var watcher = try Watcher.init(allocator, io, &th.handler); defer watcher.deinit(); try watcher.watch(tmp); - try std.fs.renameAbsolute(src_path, dst_path); - try drainEvents(Watcher, &watcher); + try std.Io.Dir.renameAbsolute(src_path, dst_path, io); + try drainEvents(Watcher, io, &watcher); if (comptime Watcher.emits_rename_for_files) { // INotify/Windows: paired atomic rename callback. @@ -409,12 +409,12 @@ fn testRenameFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { } } -fn testRenameDir(comptime Watcher: type, allocator: std.mem.Allocator) !void { +fn testRenameDir(comptime Watcher: type, io: std.Io, allocator: std.mem.Allocator) !void { const TH = MakeTestHandler(Watcher); - const tmp = try makeTempDir(allocator); + const tmp = try makeTempDir(io, allocator); defer { - removeTempDir(tmp); + removeTempDir(io, tmp); allocator.free(tmp); } @@ -426,14 +426,14 @@ fn testRenameDir(comptime Watcher: type, allocator: std.mem.Allocator) !void { const dst_path = try std.fs.path.join(allocator, &.{ tmp, "after" }); defer allocator.free(dst_path); - try std.fs.makeDirAbsolute(src_path); + try std.Io.Dir.createDirAbsolute(io, src_path, .default_dir); - var watcher = try Watcher.init(allocator, &th.handler); + var watcher = try Watcher.init(allocator, io, &th.handler); defer watcher.deinit(); try watcher.watch(tmp); - try std.fs.renameAbsolute(src_path, dst_path); - try drainEvents(Watcher, &watcher); + try std.Io.Dir.renameAbsolute(src_path, dst_path, io); + try drainEvents(Watcher, io, &watcher); if (comptime Watcher.emits_rename_for_dirs) { // INotify/Windows: paired rename callback. @@ -444,61 +444,61 @@ fn testRenameDir(comptime Watcher: type, allocator: std.mem.Allocator) !void { } } -fn testUnwatchedDir(comptime Watcher: type, allocator: std.mem.Allocator) !void { +fn testUnwatchedDir(comptime Watcher: type, io: std.Io, allocator: std.mem.Allocator) !void { const TH = MakeTestHandler(Watcher); - const watched = try makeTempDir(allocator); + const watched = try makeTempDir(io, allocator); defer { - removeTempDir(watched); + removeTempDir(io, watched); allocator.free(watched); } - const unwatched = try makeTempDir(allocator); + const unwatched = try makeTempDir(io, allocator); defer { - removeTempDir(unwatched); + removeTempDir(io, unwatched); allocator.free(unwatched); } const th = try TH.init(allocator); defer th.deinit(); - var watcher = try Watcher.init(allocator, &th.handler); + var watcher = try Watcher.init(allocator, io, &th.handler); defer watcher.deinit(); try watcher.watch(watched); const file_path = try std.fs.path.join(allocator, &.{ unwatched, "silent.txt" }); defer allocator.free(file_path); { - const f = try std.fs.createFileAbsolute(file_path, .{}); - f.close(); + const f = try std.Io.Dir.createFileAbsolute(io, file_path, .{}); + f.close(io); } - try drainEvents(Watcher, &watcher); + try drainEvents(Watcher, io, &watcher); try std.testing.expectEqual(@as(usize, 0), th.events.items.len); } -fn testUnwatch(comptime Watcher: type, allocator: std.mem.Allocator) !void { +fn testUnwatch(comptime Watcher: type, io: std.Io, allocator: std.mem.Allocator) !void { const TH = MakeTestHandler(Watcher); - const tmp = try makeTempDir(allocator); + const tmp = try makeTempDir(io, allocator); defer { - removeTempDir(tmp); + removeTempDir(io, tmp); allocator.free(tmp); } const th = try TH.init(allocator); defer th.deinit(); - var watcher = try Watcher.init(allocator, &th.handler); + var watcher = try Watcher.init(allocator, io, &th.handler); defer watcher.deinit(); try watcher.watch(tmp); const file1 = try std.fs.path.join(allocator, &.{ tmp, "watched.txt" }); defer allocator.free(file1); { - const f = try std.fs.createFileAbsolute(file1, .{}); - f.close(); + const f = try std.Io.Dir.createFileAbsolute(io, file1, .{}); + f.close(io); } - try drainEvents(Watcher, &watcher); + try drainEvents(Watcher, io, &watcher); try std.testing.expect(th.hasChange(file1, .created, .file)); watcher.unwatch(tmp) catch return error.TestUnexpectedResult; @@ -507,27 +507,27 @@ fn testUnwatch(comptime Watcher: type, allocator: std.mem.Allocator) !void { const file2 = try std.fs.path.join(allocator, &.{ tmp, "after_unwatch.txt" }); defer allocator.free(file2); { - const f = try std.fs.createFileAbsolute(file2, .{}); - f.close(); + const f = try std.Io.Dir.createFileAbsolute(io, file2, .{}); + f.close(io); } - try drainEvents(Watcher, &watcher); + try drainEvents(Watcher, io, &watcher); try std.testing.expectEqual(count_before, th.events.items.len); } -fn testMultipleFiles(comptime Watcher: type, allocator: std.mem.Allocator) !void { +fn testMultipleFiles(comptime Watcher: type, io: std.Io, allocator: std.mem.Allocator) !void { const TH = MakeTestHandler(Watcher); - const tmp = try makeTempDir(allocator); + const tmp = try makeTempDir(io, allocator); defer { - removeTempDir(tmp); + removeTempDir(io, tmp); allocator.free(tmp); } const th = try TH.init(allocator); defer th.deinit(); - var watcher = try Watcher.init(allocator, &th.handler); + var watcher = try Watcher.init(allocator, io, &th.handler); defer watcher.deinit(); try watcher.watch(tmp); @@ -537,25 +537,24 @@ fn testMultipleFiles(comptime Watcher: type, allocator: std.mem.Allocator) !void const name = try std.fmt.allocPrint(allocator, "file{d}.txt", .{i}); defer allocator.free(name); p.* = try std.fs.path.join(allocator, &.{ tmp, name }); - const f = try std.fs.createFileAbsolute(p.*, .{}); - f.close(); + const f = try std.Io.Dir.createFileAbsolute(io, p.*, .{}); + f.close(io); } defer for (paths) |p| allocator.free(p); - try drainEvents(Watcher, &watcher); + try drainEvents(Watcher, io, &watcher); for (paths) |p| { try std.testing.expect(th.hasChange(p, .created, .file)); } } -fn testRenameOrder(comptime Watcher: type, allocator: std.mem.Allocator) !void { - +fn testRenameOrder(comptime Watcher: type, io: std.Io, allocator: std.mem.Allocator) !void { const TH = MakeTestHandler(Watcher); - const tmp = try makeTempDir(allocator); + const tmp = try makeTempDir(io, allocator); defer { - removeTempDir(tmp); + removeTempDir(io, tmp); allocator.free(tmp); } @@ -568,16 +567,16 @@ fn testRenameOrder(comptime Watcher: type, allocator: std.mem.Allocator) !void { defer allocator.free(dst_path); { - const f = try std.fs.createFileAbsolute(src_path, .{}); - f.close(); + const f = try std.Io.Dir.createFileAbsolute(io, src_path, .{}); + f.close(io); } - var watcher = try Watcher.init(allocator, &th.handler); + var watcher = try Watcher.init(allocator, io, &th.handler); defer watcher.deinit(); try watcher.watch(tmp); - try std.fs.renameAbsolute(src_path, dst_path); - try drainEvents(Watcher, &watcher); + try std.Io.Dir.renameAbsolute(src_path, dst_path, io); + try drainEvents(Watcher, io, &watcher); // Backends that deliver a paired rename() callback (INotify, Windows) have // no two-event ordering to verify - the pair is a single atomic event. @@ -591,14 +590,14 @@ fn testRenameOrder(comptime Watcher: type, allocator: std.mem.Allocator) !void { try std.testing.expect(src_idx < dst_idx); } -fn testRenameThenModify(comptime Watcher: type, allocator: std.mem.Allocator) !void { +fn testRenameThenModify(comptime Watcher: type, io: std.Io, allocator: std.mem.Allocator) !void { if (comptime !Watcher.detects_file_modifications) return error.SkipZigTest; const TH = MakeTestHandler(Watcher); - const tmp = try makeTempDir(allocator); + const tmp = try makeTempDir(io, allocator); defer { - removeTempDir(tmp); + removeTempDir(io, tmp); allocator.free(tmp); } @@ -611,23 +610,23 @@ fn testRenameThenModify(comptime Watcher: type, allocator: std.mem.Allocator) !v defer allocator.free(dst_path); { - const f = try std.fs.createFileAbsolute(src_path, .{}); - f.close(); + const f = try std.Io.Dir.createFileAbsolute(io, src_path, .{}); + f.close(io); } - var watcher = try Watcher.init(allocator, &th.handler); + var watcher = try Watcher.init(allocator, io, &th.handler); defer watcher.deinit(); try watcher.watch(tmp); - try std.fs.renameAbsolute(src_path, dst_path); - try drainEvents(Watcher, &watcher); + try std.Io.Dir.renameAbsolute(src_path, dst_path, io); + try drainEvents(Watcher, io, &watcher); { - const f = try std.fs.openFileAbsolute(dst_path, .{ .mode = .write_only }); - defer f.close(); - try f.writeAll("post-rename content\n"); + const f = try std.Io.Dir.openFileAbsolute(io, dst_path, .{ .mode = .write_only }); + defer f.close(io); + try f.writeStreamingAll(io, "post-rename content\n"); } - try drainEvents(Watcher, &watcher); + try drainEvents(Watcher, io, &watcher); // Prefer the paired rename event (INotify, Windows); fall back to any // event touching src_path for backends that emit separate events. @@ -642,24 +641,24 @@ fn testRenameThenModify(comptime Watcher: type, allocator: std.mem.Allocator) !v try std.testing.expect(rename_idx < modify_idx); } -fn testMoveOutFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { +fn testMoveOutFile(comptime Watcher: type, io: std.Io, allocator: std.mem.Allocator) !void { const TH = MakeTestHandler(Watcher); - const watched = try makeTempDir(allocator); + const watched = try makeTempDir(io, allocator); defer { - removeTempDir(watched); + removeTempDir(io, watched); allocator.free(watched); } - const other = try makeTempDir(allocator); + const other = try makeTempDir(io, allocator); defer { - removeTempDir(other); + removeTempDir(io, other); allocator.free(other); } const th = try TH.init(allocator); defer th.deinit(); - var watcher = try Watcher.init(allocator, &th.handler); + var watcher = try Watcher.init(allocator, io, &th.handler); defer watcher.deinit(); try watcher.watch(watched); @@ -669,13 +668,13 @@ fn testMoveOutFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { defer allocator.free(dst_path); { - const f = try std.fs.createFileAbsolute(src_path, .{}); - f.close(); + const f = try std.Io.Dir.createFileAbsolute(io, src_path, .{}); + f.close(io); } - try drainEvents(Watcher, &watcher); + try drainEvents(Watcher, io, &watcher); - try std.fs.renameAbsolute(src_path, dst_path); - try drainEvents(Watcher, &watcher); + try std.Io.Dir.renameAbsolute(src_path, dst_path, io); + try drainEvents(Watcher, io, &watcher); // File moved out of the watched tree: appears as deleted on all platforms. try std.testing.expect(th.hasChange(src_path, .deleted, .file)); @@ -683,24 +682,24 @@ fn testMoveOutFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { try std.testing.expect(!th.hasChange(dst_path, .created, .file)); } -fn testMoveInFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { +fn testMoveInFile(comptime Watcher: type, io: std.Io, allocator: std.mem.Allocator) !void { const TH = MakeTestHandler(Watcher); - const watched = try makeTempDir(allocator); + const watched = try makeTempDir(io, allocator); defer { - removeTempDir(watched); + removeTempDir(io, watched); allocator.free(watched); } - const other = try makeTempDir(allocator); + const other = try makeTempDir(io, allocator); defer { - removeTempDir(other); + removeTempDir(io, other); allocator.free(other); } const th = try TH.init(allocator); defer th.deinit(); - var watcher = try Watcher.init(allocator, &th.handler); + var watcher = try Watcher.init(allocator, io, &th.handler); defer watcher.deinit(); try watcher.watch(watched); @@ -710,12 +709,12 @@ fn testMoveInFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { defer allocator.free(dst_path); { - const f = try std.fs.createFileAbsolute(src_path, .{}); - f.close(); + const f = try std.Io.Dir.createFileAbsolute(io, src_path, .{}); + f.close(io); } - try std.fs.renameAbsolute(src_path, dst_path); - try drainEvents(Watcher, &watcher); + try std.Io.Dir.renameAbsolute(src_path, dst_path, io); + try drainEvents(Watcher, io, &watcher); // File moved into the watched tree: appears as created. try std.testing.expect(th.hasChange(dst_path, .created, .file)); @@ -723,24 +722,24 @@ fn testMoveInFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { try std.testing.expect(!th.hasChange(src_path, .deleted, .file)); } -fn testMoveInSubdir(comptime Watcher: type, allocator: std.mem.Allocator) !void { +fn testMoveInSubdir(comptime Watcher: type, io: std.Io, allocator: std.mem.Allocator) !void { const TH = MakeTestHandler(Watcher); - const watched = try makeTempDir(allocator); + const watched = try makeTempDir(io, allocator); defer { - removeTempDir(watched); + removeTempDir(io, watched); allocator.free(watched); } - const other = try makeTempDir(allocator); + const other = try makeTempDir(io, allocator); defer { - removeTempDir(other); + removeTempDir(io, other); allocator.free(other); } const th = try TH.init(allocator); defer th.deinit(); - var watcher = try Watcher.init(allocator, &th.handler); + var watcher = try Watcher.init(allocator, io, &th.handler); defer watcher.deinit(); try watcher.watch(watched); @@ -754,29 +753,29 @@ fn testMoveInSubdir(comptime Watcher: type, allocator: std.mem.Allocator) !void defer allocator.free(dst_file); // Create subdir with a file in the unwatched root, then move it in. - try std.fs.makeDirAbsolute(src_sub); + try std.Io.Dir.createDirAbsolute(io, src_sub, .default_dir); { - const f = try std.fs.createFileAbsolute(src_file, .{}); - f.close(); + const f = try std.Io.Dir.createFileAbsolute(io, src_file, .{}); + f.close(io); } - try std.fs.renameAbsolute(src_sub, dst_sub); - try drainEvents(Watcher, &watcher); + try std.Io.Dir.renameAbsolute(src_sub, dst_sub, io); + try drainEvents(Watcher, io, &watcher); try std.testing.expect(th.hasChange(dst_sub, .created, .dir)); if (comptime Watcher.emits_subtree_created_on_movein) try std.testing.expect(th.hasChange(dst_file, .created, .file)); // Delete the file inside the moved-in subdir. - try std.fs.deleteFileAbsolute(dst_file); - try drainEvents(Watcher, &watcher); + try std.Io.Dir.deleteFileAbsolute(io, dst_file); + try drainEvents(Watcher, io, &watcher); // Object type must be .file, not .unknown - backend must have seeded // the path_types cache when the subdir was moved in. try std.testing.expect(th.hasChange(dst_file, .deleted, .file)); // Delete the now-empty subdir. - try std.fs.deleteDirAbsolute(dst_sub); - try drainEvents(Watcher, &watcher); + try std.Io.Dir.deleteDirAbsolute(io, dst_sub); + try drainEvents(Watcher, io, &watcher); try std.testing.expect(th.hasChange(dst_sub, .deleted, .dir)); } @@ -787,13 +786,13 @@ fn testMoveInSubdir(comptime Watcher: type, allocator: std.mem.Allocator) !void test "creating a file emits a 'created' event" { inline for (comptime std.enums.values(nw.Variant)) |variant| { - try testCreateFile(nw.Create(variant), std.testing.allocator); + try testCreateFile(nw.Create(variant), std.testing.io, std.testing.allocator); } } test "writing to a file emits a 'modified' event" { inline for (comptime std.enums.values(nw.Variant)) |variant| { - testModifyFile(nw.Create(variant), std.testing.allocator) catch |e| { + testModifyFile(nw.Create(variant), std.testing.io, std.testing.allocator) catch |e| { if (e != error.SkipZigTest) return e; }; } @@ -801,67 +800,67 @@ test "writing to a file emits a 'modified' event" { test "closing a file after writing emits a 'closed' event (inotify only)" { inline for (comptime std.enums.values(nw.Variant)) |variant| { - try testCloseFile(nw.Create(variant), std.testing.allocator); + try testCloseFile(nw.Create(variant), std.testing.io, std.testing.allocator); } } test "deleting a file emits a 'deleted' event" { inline for (comptime std.enums.values(nw.Variant)) |variant| { - try testDeleteFile(nw.Create(variant), std.testing.allocator); + try testDeleteFile(nw.Create(variant), std.testing.io, std.testing.allocator); } } test "deleting a dir emits a 'deleted' event" { inline for (comptime std.enums.values(nw.Variant)) |variant| { - try testDeleteSubdir(nw.Create(variant), std.testing.allocator); + try testDeleteSubdir(nw.Create(variant), std.testing.io, std.testing.allocator); } } test "creating a sub-directory emits a 'created' event with object_type dir" { inline for (comptime std.enums.values(nw.Variant)) |variant| { - try testCreateSubdir(nw.Create(variant), std.testing.allocator); + try testCreateSubdir(nw.Create(variant), std.testing.io, std.testing.allocator); } } test "renaming a file is reported correctly per-platform" { inline for (comptime std.enums.values(nw.Variant)) |variant| { - try testRenameFile(nw.Create(variant), std.testing.allocator); + try testRenameFile(nw.Create(variant), std.testing.io, std.testing.allocator); } } test "renaming a directory emits a deleted event for the old path" { inline for (comptime std.enums.values(nw.Variant)) |variant| { - try testRenameDir(nw.Create(variant), std.testing.allocator); + try testRenameDir(nw.Create(variant), std.testing.io, std.testing.allocator); } } test "an unwatched directory produces no events" { inline for (comptime std.enums.values(nw.Variant)) |variant| { - try testUnwatchedDir(nw.Create(variant), std.testing.allocator); + try testUnwatchedDir(nw.Create(variant), std.testing.io, std.testing.allocator); } } test "unwatch stops delivering events for that directory" { inline for (comptime std.enums.values(nw.Variant)) |variant| { - try testUnwatch(nw.Create(variant), std.testing.allocator); + try testUnwatch(nw.Create(variant), std.testing.io, std.testing.allocator); } } test "multiple files created sequentially all appear in the event list" { inline for (comptime std.enums.values(nw.Variant)) |variant| { - try testMultipleFiles(nw.Create(variant), std.testing.allocator); + try testMultipleFiles(nw.Create(variant), std.testing.io, std.testing.allocator); } } test "rename: old-name event precedes new-name event" { inline for (comptime std.enums.values(nw.Variant)) |variant| { - try testRenameOrder(nw.Create(variant), std.testing.allocator); + try testRenameOrder(nw.Create(variant), std.testing.io, std.testing.allocator); } } test "rename-then-modify: rename event precedes the subsequent modify event" { inline for (comptime std.enums.values(nw.Variant)) |variant| { - testRenameThenModify(nw.Create(variant), std.testing.allocator) catch |e| { + testRenameThenModify(nw.Create(variant), std.testing.io, std.testing.allocator) catch |e| { if (e != error.SkipZigTest) return e; }; } @@ -869,18 +868,18 @@ test "rename-then-modify: rename event precedes the subsequent modify event" { test "moving a file out of the watched tree appears as deleted or renamed" { inline for (comptime std.enums.values(nw.Variant)) |variant| { - try testMoveOutFile(nw.Create(variant), std.testing.allocator); + try testMoveOutFile(nw.Create(variant), std.testing.io, std.testing.allocator); } } test "moving a file into the watched tree appears as created" { inline for (comptime std.enums.values(nw.Variant)) |variant| { - try testMoveInFile(nw.Create(variant), std.testing.allocator); + try testMoveInFile(nw.Create(variant), std.testing.io, std.testing.allocator); } } test "moving a subdir into the watched tree: contents can be deleted with correct types" { inline for (comptime std.enums.values(nw.Variant)) |variant| { - try testMoveInSubdir(nw.Create(variant), std.testing.allocator); + try testMoveInSubdir(nw.Create(variant), std.testing.io, std.testing.allocator); } }