diff --git a/build.zig b/build.zig index b070768..c155850 100644 --- a/build.zig +++ b/build.zig @@ -349,6 +349,12 @@ pub fn build_exe( }); const syntax_mod = syntax_dep.module("syntax"); + const nightwatch_dep = b.dependency("nightwatch", .{ + .target = target, + .optimize = optimize, + }); + const nightwatch_mod = nightwatch_dep.module("nightwatch"); + const help_mod = b.createModule(.{ .root_source_file = b.path("help.md"), }); @@ -607,8 +613,7 @@ pub fn build_exe( const file_watcher_mod = b.createModule(.{ .root_source_file = b.path("src/file_watcher.zig"), .imports = &.{ - .{ .name = "soft_root", .module = soft_root_mod }, - .{ .name = "log", .module = log_mod }, + .{ .name = "nightwatch", .module = nightwatch_mod }, .{ .name = "cbor", .module = cbor_mod }, .{ .name = "thespian", .module = thespian_mod }, }, diff --git a/build.zig.zon b/build.zig.zon index ba03729..af33c62 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -50,6 +50,10 @@ .url = "git+https://github.com/hexops/xcode-frameworks?ref=main#8a1cfb373587ea4c9bb1468b7c986462d8d4e10e", .hash = "N-V-__8AALShqgXkvqYU6f__FrA22SMWmi2TXCJjNTO1m8XJ", }, + .nightwatch = .{ + .url = "git+https://git.flow-control.dev/neurocyte/nightwatch?ref=master#a1e5e3e9a5126d1ff7ce4959a823ea12f20ef0ae", + .hash = "nightwatch-0.1.0-uXzeH8WuAAC95apH6JQZQDzCGrXK2PRPq0rDPPxwUI3Z", + }, }, .paths = .{ "include", diff --git a/src/file_watcher.zig b/src/file_watcher.zig new file mode 100644 index 0000000..e94b59b --- /dev/null +++ b/src/file_watcher.zig @@ -0,0 +1,171 @@ +const std = @import("std"); +const tp = @import("thespian"); +const cbor = @import("cbor"); +const nightwatch = @import("nightwatch"); +const builtin = @import("builtin"); + +pid: tp.pid_ref, + +const Self = @This(); +const module_name = @typeName(Self); + +pub const EventType = nightwatch.EventType; + +pub const Error = error{ + FileWatcherSendFailed, + ThespianSpawnFailed, + OutOfMemory, +}; +const SpawnError = error{ OutOfMemory, ThespianSpawnFailed }; + +pub fn watch(path: []const u8) Error!void { + return send(.{ "watch", path }); +} + +pub fn unwatch(path: []const u8) Error!void { + return send(.{ "unwatch", path }); +} + +pub fn start() SpawnError!void { + _ = try get(); +} + +pub fn shutdown() void { + const pid = tp.env.get().proc(module_name); + if (pid.expired()) return; + pid.send(.{"shutdown"}) catch {}; +} + +fn get() SpawnError!Self { + const pid = tp.env.get().proc(module_name); + return if (pid.expired()) create() else .{ .pid = pid }; +} + +fn send(message: anytype) Error!void { + return (try get()).pid.send(message) catch error.FileWatcherSendFailed; +} + +fn create() SpawnError!Self { + const pid = try Process.create(); + defer pid.deinit(); + tp.env.get().proc_set(module_name, pid.ref()); + return .{ .pid = tp.env.get().proc(module_name) }; +} + +const Process = struct { + allocator: std.mem.Allocator, + parent: tp.pid, + receiver: Receiver, + nw: nightwatch, + fd_watcher: if (builtin.os.tag == .linux) tp.file_descriptor else void, + handler: nightwatch.Handler, + + const Receiver = tp.Receiver(*@This()); + + fn create() SpawnError!tp.pid { + const allocator = std.heap.c_allocator; + const self = try allocator.create(@This()); + errdefer allocator.destroy(self); + self.* = .{ + .allocator = allocator, + .parent = tp.self_pid().clone(), + .receiver = Receiver.init(@This().receive, self), + .nw = undefined, + .fd_watcher = if (builtin.os.tag == .linux) undefined else {}, + .handler = init_handler(), + }; + return tp.spawn_link(self.allocator, self, @This().start, module_name); + } + + fn deinit(self: *@This()) void { + if (builtin.os.tag == .linux) self.fd_watcher.deinit(); + self.nw.deinit(); + self.parent.deinit(); + self.allocator.destroy(self); + } + + pub fn init_handler() nightwatch.Handler { + return .{ + .vtable = &.{ + .change = handle_change, + .rename = handle_rename, + .wait_readable = if (builtin.os.tag == .linux) wait_readable else {}, + }, + }; + } + + fn start(self: *@This()) tp.result { + errdefer self.deinit(); + _ = tp.set_trap(true); + self.nw = nightwatch.init(self.allocator, &self.handler) catch |e| return tp.exit_error(e, @errorReturnTrace()); + if (builtin.os.tag == .linux) + self.fd_watcher = tp.file_descriptor.init(module_name, self.nw.backend.inotify_fd) catch |e| { + std.log.err("file_watcher.start: {}", .{e}); + return tp.exit_error(e, @errorReturnTrace()); + }; + tp.receive(&self.receiver); + } + + fn receive(self: *@This(), from: tp.pid_ref, m: tp.message) tp.result { + errdefer self.deinit(); + return self.receive_safe(from, m) catch |e| switch (e) { + error.ExitNormal => tp.exit_normal(), + else => { + const err = tp.exit_error(e, @errorReturnTrace()); + std.log.err("file_watcher.receive: {}", .{err}); + return err; + }, + }; + } + + fn receive_safe(self: *@This(), _: tp.pid_ref, m: tp.message) (error{ExitNormal} || cbor.Error)!void { + var path: []const u8 = undefined; + var tag: []const u8 = undefined; + var err_code: i64 = 0; + var err_msg: []const u8 = undefined; + + if (try cbor.match(m.buf, .{ "fd", tp.extract(&tag), "read_ready" })) { + // re-arm the file_discriptor + if (builtin.os.tag == .linux) { + self.fd_watcher.wait_read() catch |e| std.log.err("file_watcher wait_read: {}", .{e}); + self.nw.handle_read_ready() catch |e| std.log.err("file_watcher handle_read_ready: {}", .{e}); + } + } else if (try cbor.match(m.buf, .{ "fd", tp.extract(&tag), "read_error", tp.extract(&err_code), tp.extract(&err_msg) })) { + std.log.err("fd read error on {s}: ({d}) {s}", .{ tag, err_code, err_msg }); + } else if (try cbor.match(m.buf, .{ "watch", tp.extract(&path) })) { + self.nw.watch(path) catch |e| std.log.err("file_watcher watch: {s} -> {}", .{ path, e }); + } else if (try cbor.match(m.buf, .{ "unwatch", tp.extract(&path) })) { + self.nw.unwatch(path) catch |e| std.log.err("file_watcher unwatch: {s} -> {}", .{ path, e }); + } else if (try cbor.match(m.buf, .{"shutdown"})) { + return error.ExitNormal; + } else if (try cbor.match(m.buf, .{ "exit", tp.more })) { + return error.ExitNormal; + } else { + std.log.err("file_watcher.receive: {}", .{tp.unexpected(m)}); + } + } + + fn handle_change(handler: *nightwatch.Handler, path: []const u8, event_type: EventType) error{HandlerFailed}!void { + const self: *@This() = @alignCast(@fieldParentPtr("handler", handler)); + _ = self; + _ = path; + _ = event_type; + } + + fn handle_rename(handler: *nightwatch.Handler, src_path: []const u8, dst_path: []const u8) error{HandlerFailed}!void { + const self: *@This() = @alignCast(@fieldParentPtr("handler", handler)); + _ = self; + _ = src_path; + _ = dst_path; + } + + fn wait_readable(handler: *nightwatch.Handler) error{HandlerFailed}!nightwatch.ReadableStatus { + const self: *@This() = @alignCast(@fieldParentPtr("handler", handler)); + if (builtin.os.tag == .linux) + self.fd_watcher.wait_read() catch |e| { + std.log.err("file_watcher.wait_readable: {}", .{e}); + return error.HandlerFailed; + }; + return .will_notify; + } +};