docs: fully document the public API and add examples
This commit is contained in:
parent
af29f5ea5e
commit
40deced0f2
3 changed files with 170 additions and 26 deletions
|
|
@ -8,6 +8,23 @@ pub const ObjectType = types.ObjectType;
|
||||||
pub const Error = types.Error;
|
pub const Error = types.Error;
|
||||||
pub const InterfaceType = types.InterfaceType;
|
pub const InterfaceType = types.InterfaceType;
|
||||||
|
|
||||||
|
/// The set of backend variants available on the current platform.
|
||||||
|
///
|
||||||
|
/// On Linux this is `InterfaceType` (`.polling` or `.threaded`), since the
|
||||||
|
/// only backend is inotify and the choice is how events are delivered.
|
||||||
|
/// On macOS, BSD, and Windows the variants select the OS-level mechanism.
|
||||||
|
/// Pass a value to `Create()` to get a watcher type for that variant.
|
||||||
|
///
|
||||||
|
/// On macOS the `.fsevents` variant is only present when the `macos_fsevents`
|
||||||
|
/// build option is enabled. To enable it when using nightwatch as a dependency,
|
||||||
|
/// pass the option in your `build.zig`:
|
||||||
|
///
|
||||||
|
/// ```zig
|
||||||
|
/// const nightwatch_dep = b.dependency("nightwatch", .{
|
||||||
|
/// .macos_fsevents = true,
|
||||||
|
/// });
|
||||||
|
/// exe.root_module.addImport("nightwatch", nightwatch_dep.module("nightwatch"));
|
||||||
|
/// ```
|
||||||
pub const Variant = switch (builtin.os.tag) {
|
pub const Variant = switch (builtin.os.tag) {
|
||||||
.linux => InterfaceType,
|
.linux => InterfaceType,
|
||||||
.macos => if (build_options.macos_fsevents) enum { fsevents, kqueue, kqueuedir } else enum { kqueue, kqueuedir },
|
.macos => if (build_options.macos_fsevents) enum { fsevents, kqueue, kqueuedir } else enum { kqueue, kqueuedir },
|
||||||
|
|
@ -16,6 +33,8 @@ pub const Variant = switch (builtin.os.tag) {
|
||||||
else => @compileError("unsupported OS"),
|
else => @compileError("unsupported OS"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The recommended variant for the current platform. `Default` is a
|
||||||
|
/// shorthand for `Create(default_variant)`.
|
||||||
pub const default_variant: Variant = switch (builtin.os.tag) {
|
pub const default_variant: Variant = switch (builtin.os.tag) {
|
||||||
.linux => .threaded,
|
.linux => .threaded,
|
||||||
.macos => if (build_options.macos_fsevents) .fsevents else .kqueue,
|
.macos => if (build_options.macos_fsevents) .fsevents else .kqueue,
|
||||||
|
|
@ -24,8 +43,27 @@ pub const default_variant: Variant = switch (builtin.os.tag) {
|
||||||
else => @compileError("unsupported OS"),
|
else => @compileError("unsupported OS"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// A ready-to-use watcher type using the recommended backend for the current
|
||||||
|
/// platform. Equivalent to `Create(default_variant)`.
|
||||||
pub const Default: type = Create(default_variant);
|
pub const Default: type = Create(default_variant);
|
||||||
|
|
||||||
|
/// Returns a `Watcher` type parameterized on the given backend variant.
|
||||||
|
///
|
||||||
|
/// Typical usage:
|
||||||
|
/// ```zig
|
||||||
|
/// const Watcher = nightwatch.Default; // or nightwatch.Create(.kqueue), etc.
|
||||||
|
/// var watcher = try Watcher.init(allocator, &my_handler.handler);
|
||||||
|
/// defer watcher.deinit();
|
||||||
|
/// try watcher.watch("/path/to/dir");
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// To iterate all available variants at comptime (e.g. in tests):
|
||||||
|
/// ```zig
|
||||||
|
/// inline for (comptime std.enums.values(nightwatch.Variant)) |v| {
|
||||||
|
/// const W = nightwatch.Create(v);
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
pub fn Create(comptime variant: Variant) type {
|
pub fn Create(comptime variant: Variant) type {
|
||||||
return struct {
|
return struct {
|
||||||
pub const Backend = switch (builtin.os.tag) {
|
pub const Backend = switch (builtin.os.tag) {
|
||||||
|
|
@ -47,10 +85,14 @@ pub fn Create(comptime variant: Variant) type {
|
||||||
},
|
},
|
||||||
else => @compileError("unsupported OS"),
|
else => @compileError("unsupported OS"),
|
||||||
};
|
};
|
||||||
|
/// Whether this watcher variant uses a background thread or requires
|
||||||
|
/// the caller to drive the event loop. See `InterfaceType`.
|
||||||
pub const interface_type: InterfaceType = switch (builtin.os.tag) {
|
pub const interface_type: InterfaceType = switch (builtin.os.tag) {
|
||||||
.linux => variant,
|
.linux => variant,
|
||||||
else => .threaded,
|
else => .threaded,
|
||||||
};
|
};
|
||||||
|
/// The handler type expected by `init`. `Handler` for threaded
|
||||||
|
/// variants, `PollingHandler` for the polling variant.
|
||||||
pub const Handler = switch (interface_type) {
|
pub const Handler = switch (interface_type) {
|
||||||
.threaded => types.Handler,
|
.threaded => types.Handler,
|
||||||
.polling => types.PollingHandler,
|
.polling => types.PollingHandler,
|
||||||
|
|
@ -63,11 +105,22 @@ pub fn Create(comptime variant: Variant) type {
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
interceptor: *InterceptorType,
|
interceptor: *InterceptorType,
|
||||||
|
|
||||||
/// True if the current backend detects file content modifications in real time.
|
/// Whether this backend detects file content modifications in real time.
|
||||||
/// False only when kqueue_dir_only=true, where directory-level watches are used
|
///
|
||||||
/// and file writes do not trigger a directory NOTE_WRITE event.
|
/// `false` only for the `kqueuedir` variant, which uses directory-level
|
||||||
|
/// kqueue watches. Because directory `NOTE_WRITE` events are not
|
||||||
|
/// triggered by writes to files inside the directory, file modifications
|
||||||
|
/// are not detected for unwatched files. Files added explicitly via
|
||||||
|
/// `watch()` do receive per-file `NOTE_WRITE` events and will report
|
||||||
|
/// modifications.
|
||||||
pub const detects_file_modifications = Backend.detects_file_modifications;
|
pub const detects_file_modifications = Backend.detects_file_modifications;
|
||||||
|
|
||||||
|
/// Create a new watcher.
|
||||||
|
///
|
||||||
|
/// `handler` must remain valid for the lifetime of the watcher. For
|
||||||
|
/// 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(allocator: std.mem.Allocator, handler: *Handler) !@This() {
|
||||||
const ic = try allocator.create(InterceptorType);
|
const ic = try allocator.create(InterceptorType);
|
||||||
errdefer allocator.destroy(ic);
|
errdefer allocator.destroy(ic);
|
||||||
|
|
@ -83,15 +136,27 @@ pub fn Create(comptime variant: Variant) type {
|
||||||
return .{ .allocator = allocator, .interceptor = ic };
|
return .{ .allocator = allocator, .interceptor = ic };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stop the watcher, release all watches, and free resources.
|
||||||
|
/// For threaded variants this joins the background thread.
|
||||||
pub fn deinit(self: *@This()) void {
|
pub fn deinit(self: *@This()) void {
|
||||||
self.interceptor.backend.deinit(self.allocator);
|
self.interceptor.backend.deinit(self.allocator);
|
||||||
self.allocator.destroy(self.interceptor);
|
self.allocator.destroy(self.interceptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Watch a path (file or directory) for changes. The handler will receive
|
/// Watch a path for changes.
|
||||||
/// `change` and (linux only) `rename` calls. When path is a directory,
|
///
|
||||||
/// all subdirectories are watched recursively and new directories created
|
/// `path` may be a file or a directory. Relative paths are resolved
|
||||||
/// inside are watched automatically.
|
/// against the current working directory at the time of the call.
|
||||||
|
/// Events are always delivered with absolute paths.
|
||||||
|
///
|
||||||
|
/// When `path` is a directory, all existing subdirectories are watched
|
||||||
|
/// recursively and any newly created subdirectory is automatically
|
||||||
|
/// added to the watch set.
|
||||||
|
///
|
||||||
|
/// The handler's `change` callback is called for every event. On
|
||||||
|
/// Linux (inotify), renames that can be paired atomically are delivered
|
||||||
|
/// via the `rename` callback instead; on all other platforms a rename
|
||||||
|
/// appears as a `deleted` event followed by a `created` event.
|
||||||
pub fn watch(self: *@This(), path: []const u8) Error!void {
|
pub fn watch(self: *@This(), path: []const u8) Error!void {
|
||||||
// Make the path absolute without resolving symlinks so that callers who
|
// Make the path absolute without resolving symlinks so that callers who
|
||||||
// pass "/tmp/foo" (where /tmp is a symlink) receive events with the same
|
// pass "/tmp/foo" (where /tmp is a symlink) receive events with the same
|
||||||
|
|
@ -110,21 +175,26 @@ pub fn Create(comptime variant: Variant) type {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop watching a previously watched path
|
/// Stop watching a previously watched path. Has no effect if `path`
|
||||||
|
/// was never watched. Does not unwatch subdirectories that were
|
||||||
|
/// added automatically as a result of watching `path`.
|
||||||
pub fn unwatch(self: *@This(), path: []const u8) void {
|
pub fn unwatch(self: *@This(), path: []const u8) void {
|
||||||
self.interceptor.backend.remove_watch(self.allocator, path);
|
self.interceptor.backend.remove_watch(self.allocator, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drive event delivery by reading from the inotify fd.
|
/// Read pending events from the backend fd and deliver them to the handler.
|
||||||
/// Only available in Linux poll mode (linux_poll_mode == true).
|
///
|
||||||
|
/// Only available for the `.polling` variant (Linux inotify). Call this
|
||||||
|
/// whenever `poll_fd()` is readable.
|
||||||
pub fn handle_read_ready(self: *@This()) !void {
|
pub fn handle_read_ready(self: *@This()) !void {
|
||||||
comptime if (@hasDecl(Backend, "polling") and Backend.polling) @compileError("handle_read_ready is only available in polling backends");
|
comptime if (@hasDecl(Backend, "polling") and Backend.polling) @compileError("handle_read_ready is only available in polling backends");
|
||||||
try self.interceptor.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
|
/// Returns the file descriptor to poll for `POLLIN` before calling
|
||||||
/// before calling handle_read_ready().
|
/// `handle_read_ready()`.
|
||||||
/// Only available in Linux poll mode (linux_poll_mode == true).
|
///
|
||||||
|
/// Only available for the `.polling` variant (Linux inotify).
|
||||||
pub fn poll_fd(self: *const @This()) std.posix.fd_t {
|
pub fn poll_fd(self: *const @This()) std.posix.fd_t {
|
||||||
comptime if (@hasDecl(Backend, "polling") and Backend.polling) @compileError("poll_fd is only available in polling backends");
|
comptime if (@hasDecl(Backend, "polling") and Backend.polling) @compileError("poll_fd is only available in polling backends");
|
||||||
return self.interceptor.backend.inotify_fd;
|
return self.interceptor.backend.inotify_fd;
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ const RecordedEvent = union(enum) {
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// MakeTestHandler — adapts to the Handler type required by the given Watcher.
|
// MakeTestHandler - adapts to the Handler type required by the given Watcher.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
fn MakeTestHandler(comptime Watcher: type) type {
|
fn MakeTestHandler(comptime Watcher: type) type {
|
||||||
|
|
@ -534,7 +534,7 @@ fn testRenameThenModify(comptime Watcher: type, allocator: std.mem.Allocator) !v
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Test blocks — each runs its case across all available variants.
|
// Test blocks - each runs its case across all available variants.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
test "creating a file emits a 'created' event" {
|
test "creating a file emits a 'created' event" {
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,98 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
/// The kind of filesystem change that occurred.
|
||||||
pub const EventType = enum {
|
pub const EventType = enum {
|
||||||
|
/// A new file or directory was created.
|
||||||
created,
|
created,
|
||||||
|
/// A file's contents were modified.
|
||||||
modified,
|
modified,
|
||||||
|
/// A file or directory was deleted.
|
||||||
deleted,
|
deleted,
|
||||||
/// kqueue, FSEvents and Windows emit deleted and then created events for renames.
|
/// A file or directory was renamed or moved.
|
||||||
/// INotfiy emits a rename event with both paths instead.
|
///
|
||||||
|
/// INotify delivers this as a single `rename` callback with both the
|
||||||
|
/// source and destination paths. All other backends (kqueue, FSEvents,
|
||||||
|
/// Windows) cannot pair the two sides atomically and emit a `deleted`
|
||||||
|
/// event for the old path followed by a `created` event for the new
|
||||||
|
/// path instead.
|
||||||
renamed,
|
renamed,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Whether the affected filesystem object is a file, directory, or unknown.
|
||||||
pub const ObjectType = enum {
|
pub const ObjectType = enum {
|
||||||
file,
|
file,
|
||||||
dir,
|
dir,
|
||||||
/// The object type is unknown on Windows when a file is deleted and no path exists to query.
|
/// The object type could not be determined. This happens on Windows
|
||||||
|
/// when an object is deleted and the path no longer exists to query.
|
||||||
unknown,
|
unknown,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Errors that may be returned by the public nightwatch API.
|
||||||
pub const Error = error{
|
pub const Error = error{
|
||||||
|
/// The user-supplied handler returned `error.HandlerFailed`.
|
||||||
HandlerFailed,
|
HandlerFailed,
|
||||||
OutOfMemory,
|
OutOfMemory,
|
||||||
|
/// The watch could not be registered (e.g. path does not exist, fd
|
||||||
|
/// limit reached, or the backend rejected the path).
|
||||||
WatchFailed,
|
WatchFailed,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const InterfaceType = enum {
|
/// Selects how the watcher delivers events to the caller.
|
||||||
polling,
|
///
|
||||||
threaded,
|
/// - `.threaded` — the backend spawns an internal thread that calls the
|
||||||
};
|
/// handler directly. The caller just needs to keep the `Watcher` alive.
|
||||||
|
/// - `.polling` — no internal thread is created. The caller must poll
|
||||||
|
/// `poll_fd()` for readability and call `handle_read_ready()` whenever
|
||||||
|
/// data is available. Currently only supported on Linux (inotify).
|
||||||
|
pub const InterfaceType = enum { polling, threaded };
|
||||||
|
|
||||||
|
/// Event handler interface used by threaded backends.
|
||||||
|
///
|
||||||
|
/// Implement this by embedding a `Handler` field in your context struct
|
||||||
|
/// and pointing `vtable` at a comptime-constant `VTable`:
|
||||||
|
///
|
||||||
|
/// ```zig
|
||||||
|
/// const MyHandler = struct {
|
||||||
|
/// handler: nightwatch.Handler,
|
||||||
|
/// // ... your fields ...
|
||||||
|
///
|
||||||
|
/// const vtable = nightwatch.Handler.VTable{
|
||||||
|
/// .change = changeCb,
|
||||||
|
/// .rename = renameCb,
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// fn changeCb(h: *nightwatch.Handler, path: []const u8,
|
||||||
|
/// ev: nightwatch.EventType, obj: nightwatch.ObjectType)
|
||||||
|
/// error{HandlerFailed}!void
|
||||||
|
/// {
|
||||||
|
/// const self: *MyHandler = @fieldParentPtr("handler", h);
|
||||||
|
/// _ = self; // use self...
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn renameCb(h: *nightwatch.Handler, src: []const u8, dst: []const u8,
|
||||||
|
/// obj: nightwatch.ObjectType) error{HandlerFailed}!void
|
||||||
|
/// {
|
||||||
|
/// const self: *MyHandler = @fieldParentPtr("handler", h);
|
||||||
|
/// _ = self;
|
||||||
|
/// }
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// var my_handler = MyHandler{ .handler = .{ .vtable = &MyHandler.vtable }, ... };
|
||||||
|
/// var watcher = try nightwatch.Default.init(allocator, &my_handler.handler);
|
||||||
|
/// ```
|
||||||
pub const Handler = struct {
|
pub const Handler = struct {
|
||||||
vtable: *const VTable,
|
vtable: *const VTable,
|
||||||
|
|
||||||
pub const VTable = struct {
|
pub const VTable = struct {
|
||||||
|
/// Called for every create / modify / delete / rename event.
|
||||||
|
/// `path` is the absolute path of the affected object.
|
||||||
|
/// The string is only valid for the duration of the call.
|
||||||
change: *const fn (handler: *Handler, path: []const u8, event_type: EventType, object_type: ObjectType) error{HandlerFailed}!void,
|
change: *const fn (handler: *Handler, path: []const u8, event_type: EventType, object_type: ObjectType) error{HandlerFailed}!void,
|
||||||
|
|
||||||
|
/// Called on INotify when a rename can be delivered as a single
|
||||||
|
/// (src -> dst) pair. `src` and `dst` are absolute paths valid only
|
||||||
|
/// for the duration of the call.
|
||||||
rename: *const fn (handler: *Handler, src_path: []const u8, dst_path: []const u8, object_type: ObjectType) error{HandlerFailed}!void,
|
rename: *const fn (handler: *Handler, src_path: []const u8, dst_path: []const u8, object_type: ObjectType) error{HandlerFailed}!void,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -45,19 +105,33 @@ pub const Handler = struct {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Used only by the inotify backend in poll mode (caller drives the event
|
/// Event handler interface used by polling backends (Linux inotify in poll mode).
|
||||||
/// loop via poll_fd / handle_read_ready)
|
///
|
||||||
|
/// Like `Handler` but with an additional `wait_readable` callback that the
|
||||||
|
/// backend calls to yield control back to the caller's event loop while
|
||||||
|
/// waiting for the inotify fd to become readable.
|
||||||
|
///
|
||||||
|
/// Usage is identical to `Handler`; use this type only when constructing a
|
||||||
|
/// `Create(.polling)` watcher on Linux.
|
||||||
pub const PollingHandler = struct {
|
pub const PollingHandler = struct {
|
||||||
vtable: *const VTable,
|
vtable: *const VTable,
|
||||||
|
|
||||||
|
/// Returned by `wait_readable` to describe what the backend should do next.
|
||||||
pub const ReadableStatus = enum {
|
pub const ReadableStatus = enum {
|
||||||
// TODO: is_readable, // backend may now read from fd (blocking mode)
|
/// The backend should wait for the next `handle_read_ready()` call
|
||||||
will_notify, // backend must wait for a handle_read_ready call
|
/// before reading from the fd. The caller is responsible for polling.
|
||||||
|
will_notify,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const VTable = struct {
|
pub const VTable = struct {
|
||||||
|
/// See `Handler.VTable.change`.
|
||||||
change: *const fn (handler: *PollingHandler, path: []const u8, event_type: EventType, object_type: ObjectType) error{HandlerFailed}!void,
|
change: *const fn (handler: *PollingHandler, path: []const u8, event_type: EventType, object_type: ObjectType) error{HandlerFailed}!void,
|
||||||
|
/// See `Handler.VTable.rename`.
|
||||||
rename: *const fn (handler: *PollingHandler, src_path: []const u8, dst_path: []const u8, object_type: ObjectType) error{HandlerFailed}!void,
|
rename: *const fn (handler: *PollingHandler, src_path: []const u8, dst_path: []const u8, object_type: ObjectType) error{HandlerFailed}!void,
|
||||||
|
/// Called by the backend when it needs the fd to be readable before
|
||||||
|
/// it can continue. The handler should arrange to call
|
||||||
|
/// `handle_read_ready()` when `poll_fd()` becomes readable and return
|
||||||
|
/// the appropriate `ReadableStatus`.
|
||||||
wait_readable: *const fn (handler: *PollingHandler) error{HandlerFailed}!ReadableStatus,
|
wait_readable: *const fn (handler: *PollingHandler) error{HandlerFailed}!ReadableStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue