Compare commits
9 commits
8af70c05b6
...
ae4b56b62a
| Author | SHA1 | Date | |
|---|---|---|---|
| ae4b56b62a | |||
| 9b4d7c2121 | |||
| 9dda7efc25 | |||
| 4569a33382 | |||
| e4cc1b82fe | |||
| f3463dd0dc | |||
| 8dc759db61 | |||
| 9679b0cedf | |||
| 20c167b37d |
4 changed files with 400 additions and 88 deletions
|
|
@ -56,11 +56,13 @@ pub fn build(b: *std.Build) void {
|
|||
run_cmd.addArgs(args);
|
||||
|
||||
const mod_tests = b.addTest(.{
|
||||
.name = "mod_tests",
|
||||
.root_module = mod,
|
||||
});
|
||||
const run_mod_tests = b.addRunArtifact(mod_tests);
|
||||
|
||||
const exe_tests = b.addTest(.{
|
||||
.name = "exe_tests",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
|
|
@ -76,6 +78,7 @@ pub fn build(b: *std.Build) void {
|
|||
// Integration test suite: exercises the public API by performing real
|
||||
// filesystem operations and verifying Handler callbacks via TestHandler.
|
||||
const integration_tests = b.addTest(.{
|
||||
.name = "integration_tests",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/nightwatch_test.zig"),
|
||||
.target = target,
|
||||
|
|
@ -91,4 +94,8 @@ pub fn build(b: *std.Build) void {
|
|||
test_step.dependOn(&run_mod_tests.step);
|
||||
test_step.dependOn(&run_exe_tests.step);
|
||||
test_step.dependOn(&run_integration_tests.step);
|
||||
|
||||
b.installArtifact(mod_tests);
|
||||
b.installArtifact(exe_tests);
|
||||
b.installArtifact(integration_tests);
|
||||
}
|
||||
|
|
|
|||
172
src/main.zig
172
src/main.zig
|
|
@ -1,8 +1,178 @@
|
|||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const nightwatch = @import("nightwatch");
|
||||
|
||||
const is_posix = switch (builtin.os.tag) {
|
||||
.linux, .macos, .freebsd => true,
|
||||
else => false,
|
||||
};
|
||||
|
||||
// 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 {};
|
||||
}
|
||||
|
||||
const CliHandler = struct {
|
||||
handler: nightwatch.Handler,
|
||||
out: std.fs.File,
|
||||
|
||||
const vtable = nightwatch.Handler.VTable{
|
||||
.change = change_cb,
|
||||
.rename = rename_cb,
|
||||
.wait_readable = if (builtin.os.tag == .linux) wait_readable_cb else {},
|
||||
};
|
||||
|
||||
fn change_cb(h: *nightwatch.Handler, path: []const u8, event_type: nightwatch.EventType) error{HandlerFailed}!void {
|
||||
const self: *CliHandler = @fieldParentPtr("handler", h);
|
||||
var buf: [4096]u8 = undefined;
|
||||
var stdout = self.out.writer(&buf);
|
||||
defer stdout.interface.flush() catch {};
|
||||
const label = switch (event_type) {
|
||||
.created => "create ",
|
||||
.modified => "modify ",
|
||||
.deleted => "delete ",
|
||||
.dir_created => "mkdir ",
|
||||
.renamed => "rename ",
|
||||
};
|
||||
stdout.interface.print("{s} {s}\n", .{ label, path }) catch return error.HandlerFailed;
|
||||
}
|
||||
|
||||
fn rename_cb(h: *nightwatch.Handler, src: []const u8, dst: []const u8) error{HandlerFailed}!void {
|
||||
const self: *CliHandler = @fieldParentPtr("handler", h);
|
||||
var buf: [4096]u8 = undefined;
|
||||
var stdout = self.out.writer(&buf);
|
||||
defer stdout.interface.flush() catch {};
|
||||
stdout.interface.print("rename {s} -> {s}\n", .{ src, dst }) catch return error.HandlerFailed;
|
||||
}
|
||||
|
||||
fn wait_readable_cb(_: *nightwatch.Handler) error{HandlerFailed}!nightwatch.ReadableStatus {
|
||||
return .will_notify;
|
||||
}
|
||||
};
|
||||
|
||||
fn run_linux(watcher: *nightwatch) !void {
|
||||
var fds = [_]std.posix.pollfd{
|
||||
.{ .fd = watcher.poll_fd(), .events = std.posix.POLL.IN, .revents = 0 },
|
||||
.{ .fd = sig_pipe[0], .events = std.posix.POLL.IN, .revents = 0 },
|
||||
};
|
||||
while (true) {
|
||||
_ = try std.posix.poll(&fds, -1);
|
||||
if (fds[1].revents & std.posix.POLL.IN != 0) return; // signal
|
||||
if (fds[0].revents & std.posix.POLL.IN != 0) {
|
||||
watcher.handle_read_ready() catch return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_posix() void {
|
||||
// Backend (kqueue) drives its own thread; we just block until signal.
|
||||
var buf: [1]u8 = undefined;
|
||||
_ = std.posix.read(sig_pipe[0], &buf) catch {};
|
||||
}
|
||||
|
||||
var win_shutdown = std.atomic.Value(bool).init(false);
|
||||
|
||||
fn win_ctrl_handler(ctrl_type: std.os.windows.DWORD) callconv(.winapi) std.os.windows.BOOL {
|
||||
_ = ctrl_type;
|
||||
win_shutdown.store(true, .release);
|
||||
return std.os.windows.TRUE;
|
||||
}
|
||||
|
||||
fn run_windows() void {
|
||||
const SetConsoleCtrlHandler = struct {
|
||||
extern "kernel32" fn SetConsoleCtrlHandler(
|
||||
HandlerRoutine: ?*const fn (std.os.windows.DWORD) callconv(.winapi) std.os.windows.BOOL,
|
||||
Add: std.os.windows.BOOL,
|
||||
) callconv(.winapi) std.os.windows.BOOL;
|
||||
}.SetConsoleCtrlHandler;
|
||||
_ = SetConsoleCtrlHandler(win_ctrl_handler, std.os.windows.TRUE);
|
||||
while (!win_shutdown.load(.acquire)) {
|
||||
std.Thread.sleep(50 * std.time.ns_per_ms);
|
||||
}
|
||||
}
|
||||
|
||||
fn usage(out: std.fs.File) !void {
|
||||
var buf: [4096]u8 = undefined;
|
||||
var writer = out.writer(&buf);
|
||||
try writer.interface.print(
|
||||
\\Usage: nightwatch <path> [<path> ...]
|
||||
\\
|
||||
\\The Watch never sleeps.
|
||||
\\
|
||||
\\Events printed to stdout:
|
||||
\\ create a file was created
|
||||
\\ modify a file was modified
|
||||
\\ delete a file or directory was deleted
|
||||
\\ mkdir a directory was created
|
||||
\\ rename a file or directory was renamed
|
||||
\\
|
||||
\\Stand down with Ctrl-C.
|
||||
\\
|
||||
, .{});
|
||||
try writer.interface.flush();
|
||||
}
|
||||
|
||||
pub fn main() !void {
|
||||
std.debug.print("FABRICATI DIEM, PVNC\n", .{});
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const args = try std.process.argsAlloc(allocator);
|
||||
defer std.process.argsFree(allocator, args);
|
||||
|
||||
if (args.len < 2) {
|
||||
try usage(std.fs.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());
|
||||
return;
|
||||
}
|
||||
|
||||
var buf: [4096]u8 = undefined;
|
||||
var stderr = std.fs.File.stderr().writer(&buf);
|
||||
defer stderr.interface.flush() catch {};
|
||||
|
||||
if (is_posix) {
|
||||
sig_pipe = try std.posix.pipe();
|
||||
const sa = std.posix.Sigaction{
|
||||
.handler = .{ .handler = posix_sighandler },
|
||||
.mask = std.posix.sigemptyset(),
|
||||
.flags = 0,
|
||||
};
|
||||
std.posix.sigaction(std.posix.SIG.INT, &sa, null);
|
||||
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]);
|
||||
};
|
||||
|
||||
var cli_handler = CliHandler{
|
||||
.handler = .{ .vtable = &CliHandler.vtable },
|
||||
.out = std.fs.File.stdout(),
|
||||
};
|
||||
|
||||
var watcher = try nightwatch.init(allocator, &cli_handler.handler);
|
||||
defer watcher.deinit();
|
||||
|
||||
for (args[1..]) |path| {
|
||||
watcher.watch(path) catch |err| {
|
||||
try stderr.interface.print("nightwatch: {s}: {s}\n", .{ path, @errorName(err) });
|
||||
continue;
|
||||
};
|
||||
try stderr.interface.print("on watch: {s}\n", .{path});
|
||||
}
|
||||
|
||||
if (builtin.os.tag == .linux) {
|
||||
try run_linux(&watcher);
|
||||
} else if (builtin.os.tag == .windows) {
|
||||
run_windows();
|
||||
} else if (is_posix) {
|
||||
run_posix();
|
||||
}
|
||||
}
|
||||
|
||||
test "simple test" {}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,8 @@ pub const EventType = enum {
|
|||
created,
|
||||
modified,
|
||||
deleted,
|
||||
/// A new directory was created inside a watched directory. The
|
||||
/// receiver should call watch() on the path to get events for files
|
||||
/// created in it.
|
||||
/// A new directory was created inside a watched directory.
|
||||
/// The library automatically begins watching it; no action is required.
|
||||
dir_created,
|
||||
/// Only produced on macOS and Windows where the OS gives no pairing info.
|
||||
/// On Linux, paired renames are emitted as a { "FW", "rename", from, to } message instead.
|
||||
|
|
@ -49,34 +48,103 @@ pub const ReadableStatus = enum {
|
|||
};
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
backend: Backend,
|
||||
interceptor: *Interceptor,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, handler: *Handler) !@This() {
|
||||
var self: @This() = .{
|
||||
const ic = try allocator.create(Interceptor);
|
||||
errdefer allocator.destroy(ic);
|
||||
ic.* = .{
|
||||
.handler = .{ .vtable = &Interceptor.vtable },
|
||||
.user_handler = handler,
|
||||
.allocator = allocator,
|
||||
.backend = try Backend.init(handler),
|
||||
.backend = undefined,
|
||||
};
|
||||
try self.backend.arm(self.allocator);
|
||||
return self;
|
||||
ic.backend = try Backend.init(&ic.handler);
|
||||
errdefer ic.backend.deinit(allocator);
|
||||
try ic.backend.arm(allocator);
|
||||
return .{ .allocator = allocator, .interceptor = ic };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *@This()) void {
|
||||
self.backend.deinit(self.allocator);
|
||||
self.interceptor.backend.deinit(self.allocator);
|
||||
self.allocator.destroy(self.interceptor);
|
||||
}
|
||||
|
||||
/// Watch a path (file or directory) for changes. The handler will receive
|
||||
/// `change` and (linux only) `rename` calls
|
||||
/// `change` and (linux only) `rename` calls. When path is a directory,
|
||||
/// all subdirectories are watched recursively and new directories created
|
||||
/// inside are watched automatically.
|
||||
pub fn watch(self: *@This(), path: []const u8) Error!void {
|
||||
return self.backend.add_watch(self.allocator, path);
|
||||
try self.interceptor.backend.add_watch(self.allocator, path);
|
||||
if (!Backend.watches_recursively) {
|
||||
recurse_watch(&self.interceptor.backend, self.allocator, path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop watching a previously watched path
|
||||
pub fn unwatch(self: *@This(), path: []const u8) void {
|
||||
self.backend.remove_watch(self.allocator, path);
|
||||
self.interceptor.backend.remove_watch(self.allocator, path);
|
||||
}
|
||||
|
||||
pub fn handle_read_ready(self: *@This()) !void {
|
||||
try self.backend.handle_read_ready(self.allocator);
|
||||
try self.interceptor.backend.handle_read_ready(self.allocator);
|
||||
}
|
||||
|
||||
/// Returns the inotify file descriptor that should be polled for POLLIN
|
||||
/// before calling handle_read_ready(). Only available on Linux.
|
||||
pub fn poll_fd(self: *const @This()) std.posix.fd_t {
|
||||
comptime if (builtin.os.tag != .linux) @compileError("poll_fd is only available on Linux");
|
||||
return self.interceptor.backend.inotify_fd;
|
||||
}
|
||||
|
||||
// Wraps the user's handler to intercept dir_created events and auto-watch
|
||||
// new directories before forwarding to the user.
|
||||
// Heap-allocated so that &ic.handler stays valid regardless of how the
|
||||
// nightwatch struct is moved after init() returns.
|
||||
const Interceptor = struct {
|
||||
handler: Handler,
|
||||
user_handler: *Handler,
|
||||
allocator: std.mem.Allocator,
|
||||
backend: Backend,
|
||||
|
||||
const vtable = Handler.VTable{
|
||||
.change = change_cb,
|
||||
.rename = rename_cb,
|
||||
.wait_readable = if (builtin.os.tag == .linux) wait_readable_cb else {},
|
||||
};
|
||||
|
||||
fn change_cb(h: *Handler, path: []const u8, event_type: EventType) error{HandlerFailed}!void {
|
||||
const self: *Interceptor = @fieldParentPtr("handler", h);
|
||||
if (event_type == .dir_created and !Backend.watches_recursively) {
|
||||
self.backend.add_watch(self.allocator, path) catch {};
|
||||
recurse_watch(&self.backend, self.allocator, path);
|
||||
}
|
||||
return self.user_handler.change(path, event_type);
|
||||
}
|
||||
|
||||
fn rename_cb(h: *Handler, src: []const u8, dst: []const u8) error{HandlerFailed}!void {
|
||||
const self: *Interceptor = @fieldParentPtr("handler", h);
|
||||
return self.user_handler.rename(src, dst);
|
||||
}
|
||||
|
||||
fn wait_readable_cb(h: *Handler) error{HandlerFailed}!ReadableStatus {
|
||||
const self: *Interceptor = @fieldParentPtr("handler", h);
|
||||
return self.user_handler.wait_readable();
|
||||
}
|
||||
};
|
||||
|
||||
// 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();
|
||||
var it = dir.iterate();
|
||||
while (it.next() catch return) |entry| {
|
||||
if (entry.kind != .directory) continue;
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const sub = std.fmt.bufPrint(&buf, "{s}/{s}", .{ dir_path, entry.name }) catch continue;
|
||||
backend.add_watch(allocator, sub) catch {};
|
||||
recurse_watch(backend, allocator, sub);
|
||||
}
|
||||
}
|
||||
|
||||
const Backend = switch (builtin.os.tag) {
|
||||
|
|
@ -88,6 +156,8 @@ const Backend = switch (builtin.os.tag) {
|
|||
};
|
||||
|
||||
const INotifyBackend = struct {
|
||||
const watches_recursively = false;
|
||||
|
||||
handler: *Handler,
|
||||
inotify_fd: std.posix.fd_t,
|
||||
watches: std.AutoHashMapUnmanaged(i32, []u8), // wd -> owned path
|
||||
|
|
@ -247,6 +317,8 @@ const INotifyBackend = struct {
|
|||
};
|
||||
|
||||
const FSEventsBackend = struct {
|
||||
const watches_recursively = true; // FSEventStreamCreate watches the entire subtree
|
||||
|
||||
handler: *Handler,
|
||||
stream: ?*anyopaque, // FSEventStreamRef
|
||||
queue: ?*anyopaque, // dispatch_queue_t
|
||||
|
|
@ -435,11 +507,14 @@ const FSEventsBackend = struct {
|
|||
};
|
||||
|
||||
const KQueueBackend = struct {
|
||||
const watches_recursively = false;
|
||||
|
||||
handler: *Handler,
|
||||
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 path -> fd
|
||||
watches_mutex: std.Thread.Mutex,
|
||||
// Per-directory snapshots of filenames, used to diff on NOTE_WRITE.
|
||||
// Key: owned dir path (same as watches key), value: set of owned filenames.
|
||||
// Accessed from both the main thread (add_watch) and the background thread (scan_dir).
|
||||
|
|
@ -477,7 +552,16 @@ const KQueueBackend = struct {
|
|||
.udata = 0,
|
||||
};
|
||||
_ = try std.posix.kevent(kq, &.{shutdown_kev}, &.{}, null);
|
||||
return .{ .handler = handler, .kq = kq, .shutdown_pipe = pipe, .thread = null, .watches = .empty, .snapshots = .empty, .snapshots_mutex = .{} };
|
||||
return .{
|
||||
.handler = handler,
|
||||
.kq = kq,
|
||||
.shutdown_pipe = pipe,
|
||||
.thread = null,
|
||||
.watches = .empty,
|
||||
.watches_mutex = .{},
|
||||
.snapshots = .empty,
|
||||
.snapshots_mutex = .{},
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
|
||||
|
|
@ -494,7 +578,7 @@ const KQueueBackend = struct {
|
|||
self.watches.deinit(allocator);
|
||||
var sit = self.snapshots.iterator();
|
||||
while (sit.next()) |entry| {
|
||||
allocator.free(entry.key_ptr.*);
|
||||
// Keys are borrowed from self.watches and freed in the watches loop above.
|
||||
var names = entry.value_ptr.*;
|
||||
var nit = names.iterator();
|
||||
while (nit.next()) |ne| allocator.free(ne.key_ptr.*);
|
||||
|
|
@ -506,12 +590,21 @@ const KQueueBackend = struct {
|
|||
|
||||
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.kq, &self.watches, &self.snapshots, &self.snapshots_mutex, allocator, self.handler });
|
||||
self.thread = try std.Thread.spawn(.{}, thread_fn, .{
|
||||
self.kq,
|
||||
&self.watches,
|
||||
&self.watches_mutex,
|
||||
&self.snapshots,
|
||||
&self.snapshots_mutex,
|
||||
allocator,
|
||||
self.handler,
|
||||
});
|
||||
}
|
||||
|
||||
fn thread_fn(
|
||||
kq: std.posix.fd_t,
|
||||
watches: *const std.StringHashMapUnmanaged(std.posix.fd_t),
|
||||
watches_mutex: *std.Thread.Mutex,
|
||||
snapshots: *std.StringHashMapUnmanaged(std.StringHashMapUnmanaged(void)),
|
||||
snapshots_mutex: *std.Thread.Mutex,
|
||||
allocator: std.mem.Allocator,
|
||||
|
|
@ -525,18 +618,20 @@ const KQueueBackend = struct {
|
|||
if (ev.filter == EVFILT_READ) return; // shutdown pipe readable, exit
|
||||
if (ev.filter != EVFILT_VNODE) continue;
|
||||
// Find the directory path for this fd.
|
||||
watches_mutex.lock();
|
||||
var wit = watches.iterator();
|
||||
while (wit.next()) |entry| {
|
||||
if (entry.value_ptr.* != @as(std.posix.fd_t, @intCast(ev.ident))) continue;
|
||||
const dir_path = entry.key_ptr.*;
|
||||
if (ev.fflags & NOTE_DELETE != 0) {
|
||||
handler.change(dir_path, EventType.deleted) catch return;
|
||||
} else if (ev.fflags & NOTE_RENAME != 0) {
|
||||
handler.change(dir_path, EventType.renamed) catch return;
|
||||
} else if (ev.fflags & NOTE_WRITE != 0) {
|
||||
scan_dir(dir_path, snapshots, snapshots_mutex, allocator, handler) catch {};
|
||||
}
|
||||
break;
|
||||
const dir_path: ?[]const u8 = while (wit.next()) |entry| {
|
||||
if (entry.value_ptr.* == @as(std.posix.fd_t, @intCast(ev.ident)))
|
||||
break entry.key_ptr.*;
|
||||
} else null;
|
||||
watches_mutex.unlock();
|
||||
if (dir_path == null) continue;
|
||||
if (ev.fflags & NOTE_DELETE != 0) {
|
||||
handler.change(dir_path.?, EventType.deleted) catch return;
|
||||
} else if (ev.fflags & NOTE_RENAME != 0) {
|
||||
handler.change(dir_path.?, EventType.renamed) catch return;
|
||||
} else if (ev.fflags & NOTE_WRITE != 0) {
|
||||
scan_dir(dir_path.?, snapshots, snapshots_mutex, allocator, handler) catch {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -553,71 +648,102 @@ const KQueueBackend = struct {
|
|||
var dir = std.fs.openDirAbsolute(dir_path, .{ .iterate = true }) catch return;
|
||||
defer dir.close();
|
||||
|
||||
// Collect current filenames (no lock needed, reading filesystem only).
|
||||
var current: std.StringHashMapUnmanaged(void) = .empty;
|
||||
// Collect current files and subdirectories (no lock, reading filesystem only).
|
||||
var current_files: std.StringHashMapUnmanaged(void) = .empty;
|
||||
defer {
|
||||
var it = current.iterator();
|
||||
var it = current_files.iterator();
|
||||
while (it.next()) |e| allocator.free(e.key_ptr.*);
|
||||
current.deinit(allocator);
|
||||
current_files.deinit(allocator);
|
||||
}
|
||||
var current_dirs: std.ArrayListUnmanaged([]u8) = .empty;
|
||||
defer {
|
||||
for (current_dirs.items) |d| allocator.free(d);
|
||||
current_dirs.deinit(allocator);
|
||||
}
|
||||
var iter = dir.iterate();
|
||||
while (try iter.next()) |entry| {
|
||||
if (entry.kind != .file) continue;
|
||||
const name = try allocator.dupe(u8, entry.name);
|
||||
try current.put(allocator, name, {});
|
||||
switch (entry.kind) {
|
||||
.file => {
|
||||
const name = try allocator.dupe(u8, entry.name);
|
||||
try current_files.put(allocator, name, {});
|
||||
},
|
||||
.directory => {
|
||||
const name = try allocator.dupe(u8, entry.name);
|
||||
try current_dirs.append(allocator, name);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
// Emit dir_created for new subdirectories outside the lock (no snapshot involvement).
|
||||
var dir2 = std.fs.openDirAbsolute(dir_path, .{ .iterate = true }) catch return;
|
||||
defer dir2.close();
|
||||
var dir_iter = dir2.iterate();
|
||||
while (try dir_iter.next()) |entry| {
|
||||
if (entry.kind != .directory) continue;
|
||||
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;
|
||||
// Only emit if not already watched.
|
||||
if (!snapshots.contains(full_path))
|
||||
try handler.change(full_path, EventType.dir_created);
|
||||
}
|
||||
|
||||
snapshots_mutex.lock();
|
||||
defer snapshots_mutex.unlock();
|
||||
|
||||
// Get or create the snapshot for this directory.
|
||||
const gop = try snapshots.getOrPut(allocator, dir_path);
|
||||
if (!gop.found_existing) gop.value_ptr.* = .empty;
|
||||
const snapshot = gop.value_ptr;
|
||||
|
||||
// Emit created events for files in current but not in snapshot.
|
||||
var cit = current.iterator();
|
||||
while (cit.next()) |entry| {
|
||||
if (snapshot.contains(entry.key_ptr.*)) continue;
|
||||
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const full_path = std.fmt.bufPrint(&path_buf, "{s}/{s}", .{ dir_path, entry.key_ptr.* }) catch continue;
|
||||
try handler.change(full_path, EventType.created);
|
||||
const owned = try allocator.dupe(u8, entry.key_ptr.*);
|
||||
try snapshot.put(allocator, owned, {});
|
||||
}
|
||||
|
||||
// Emit deleted events for files in snapshot but not in current.
|
||||
// Diff against snapshot under the lock; collect events to emit after releasing it.
|
||||
var to_create: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
defer to_create.deinit(allocator);
|
||||
var to_delete: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
defer to_delete.deinit(allocator);
|
||||
var sit = snapshot.iterator();
|
||||
while (sit.next()) |entry| {
|
||||
if (current.contains(entry.key_ptr.*)) continue;
|
||||
try to_delete.append(allocator, entry.key_ptr.*);
|
||||
var new_dirs: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
defer new_dirs.deinit(allocator);
|
||||
|
||||
snapshots_mutex.lock();
|
||||
{
|
||||
for (current_dirs.items) |name| {
|
||||
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const full_path = std.fmt.bufPrint(&path_buf, "{s}/{s}", .{ dir_path, name }) catch continue;
|
||||
if (!snapshots.contains(full_path))
|
||||
try new_dirs.append(allocator, full_path);
|
||||
}
|
||||
|
||||
const gop = snapshots.getOrPut(allocator, dir_path) catch |e| {
|
||||
snapshots_mutex.unlock();
|
||||
return e;
|
||||
};
|
||||
if (!gop.found_existing) gop.value_ptr.* = .empty;
|
||||
const snapshot = gop.value_ptr;
|
||||
|
||||
var cit = current_files.iterator();
|
||||
while (cit.next()) |entry| {
|
||||
if (snapshot.contains(entry.key_ptr.*)) continue;
|
||||
const owned = allocator.dupe(u8, entry.key_ptr.*) catch |e| {
|
||||
snapshots_mutex.unlock();
|
||||
return e;
|
||||
};
|
||||
snapshot.put(allocator, owned, {}) catch |e| {
|
||||
allocator.free(owned);
|
||||
snapshots_mutex.unlock();
|
||||
return e;
|
||||
};
|
||||
try to_create.append(allocator, owned);
|
||||
}
|
||||
|
||||
var sit = snapshot.iterator();
|
||||
while (sit.next()) |entry| {
|
||||
if (current_files.contains(entry.key_ptr.*)) continue;
|
||||
try to_delete.append(allocator, entry.key_ptr.*);
|
||||
}
|
||||
for (to_delete.items) |name| _ = snapshot.fetchRemove(name);
|
||||
}
|
||||
snapshots_mutex.unlock();
|
||||
|
||||
// Emit all events outside the lock so handlers may safely call watch()/unwatch().
|
||||
for (new_dirs.items) |full_path|
|
||||
try handler.change(full_path, EventType.dir_created);
|
||||
for (to_create.items) |name| {
|
||||
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const full_path = std.fmt.bufPrint(&path_buf, "{s}/{s}", .{ dir_path, name }) catch continue;
|
||||
try handler.change(full_path, EventType.created);
|
||||
}
|
||||
for (to_delete.items) |name| {
|
||||
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const full_path = std.fmt.bufPrint(&path_buf, "{s}/{s}", .{ dir_path, name }) catch continue;
|
||||
try handler.change(full_path, EventType.deleted);
|
||||
_ = snapshot.fetchRemove(name);
|
||||
allocator.free(name);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) error{ WatchFailed, OutOfMemory }!void {
|
||||
if (self.watches.contains(path)) return;
|
||||
self.watches_mutex.lock();
|
||||
const already = self.watches.contains(path);
|
||||
self.watches_mutex.unlock();
|
||||
if (already) return;
|
||||
const path_fd = std.posix.open(path, .{ .ACCMODE = .RDONLY }, 0) catch |e| switch (e) {
|
||||
error.AccessDenied,
|
||||
error.PermissionDenied,
|
||||
|
|
@ -669,8 +795,13 @@ const KQueueBackend = struct {
|
|||
},
|
||||
};
|
||||
const owned_path = try allocator.dupe(u8, path);
|
||||
errdefer allocator.free(owned_path);
|
||||
try self.watches.put(allocator, owned_path, path_fd);
|
||||
self.watches_mutex.lock();
|
||||
self.watches.put(allocator, owned_path, path_fd) catch |e| {
|
||||
self.watches_mutex.unlock();
|
||||
allocator.free(owned_path);
|
||||
return e;
|
||||
};
|
||||
self.watches_mutex.unlock();
|
||||
// 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,
|
||||
|
|
@ -704,7 +835,10 @@ const KQueueBackend = struct {
|
|||
}
|
||||
|
||||
fn remove_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) void {
|
||||
if (self.watches.fetchRemove(path)) |entry| {
|
||||
self.watches_mutex.lock();
|
||||
const watches_entry = self.watches.fetchRemove(path);
|
||||
self.watches_mutex.unlock();
|
||||
if (watches_entry) |entry| {
|
||||
std.posix.close(entry.value);
|
||||
allocator.free(entry.key);
|
||||
}
|
||||
|
|
@ -718,6 +852,8 @@ const KQueueBackend = struct {
|
|||
};
|
||||
|
||||
const WindowsBackend = struct {
|
||||
const watches_recursively = true; // ReadDirectoryChangesW with bWatchSubtree=1
|
||||
|
||||
const windows = std.os.windows;
|
||||
|
||||
const win32 = struct {
|
||||
|
|
@ -904,16 +1040,11 @@ const WindowsBackend = struct {
|
|||
}
|
||||
}
|
||||
|
||||
fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) (windows.CreateIoCompletionPortError || error{
|
||||
InvalidUtf8,
|
||||
OutOfMemory,
|
||||
FileWatcherInvalidHandle,
|
||||
FileWatcherReadDirectoryChangesFailed,
|
||||
})!void {
|
||||
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();
|
||||
if (self.watches.contains(path)) return;
|
||||
const path_w = try std.unicode.utf8ToUtf16LeAllocZ(allocator, path);
|
||||
const path_w = std.unicode.utf8ToUtf16LeAllocZ(allocator, path) catch return error.WatchFailed;
|
||||
defer allocator.free(path_w);
|
||||
const handle = win32.CreateFileW(
|
||||
path_w,
|
||||
|
|
@ -924,16 +1055,16 @@ const WindowsBackend = struct {
|
|||
0x02000000 | 0x40000000, // FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED
|
||||
null,
|
||||
);
|
||||
if (handle == windows.INVALID_HANDLE_VALUE) return error.FileWatcherInvalidHandle;
|
||||
if (handle == windows.INVALID_HANDLE_VALUE) return error.WatchFailed;
|
||||
errdefer _ = win32.CloseHandle(handle);
|
||||
_ = try windows.CreateIoCompletionPort(handle, self.iocp, @intFromPtr(handle), 0);
|
||||
_ = windows.CreateIoCompletionPort(handle, self.iocp, @intFromPtr(handle), 0) catch return error.WatchFailed;
|
||||
const buf = try allocator.alignedAlloc(u8, .fromByteUnits(4), buf_size);
|
||||
errdefer allocator.free(buf);
|
||||
const owned_path = try allocator.dupe(u8, path);
|
||||
errdefer allocator.free(owned_path);
|
||||
var overlapped: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED);
|
||||
if (win32.ReadDirectoryChangesW(handle, buf.ptr, buf_size, 1, notify_filter, null, &overlapped, null) == 0)
|
||||
return error.FileWatcherReadDirectoryChangesFailed;
|
||||
return error.WatchFailed;
|
||||
try self.watches.put(allocator, owned_path, .{
|
||||
.handle = handle,
|
||||
.buf = buf,
|
||||
|
|
|
|||
|
|
@ -148,7 +148,11 @@ fn makeTempDir(allocator: std.mem.Allocator) ![]u8 {
|
|||
const name = try std.fmt.allocPrint(
|
||||
allocator,
|
||||
"/tmp/nightwatch_test_{d}_{d}",
|
||||
.{ std.os.linux.getpid(), n },
|
||||
.{ switch (builtin.os.tag) {
|
||||
.linux => std.os.linux.getpid(),
|
||||
.windows => std.os.windows.GetCurrentProcessId(),
|
||||
else => std.c.getpid(),
|
||||
}, n },
|
||||
);
|
||||
errdefer allocator.free(name);
|
||||
try std.fs.makeDirAbsolute(name);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue