This commit is contained in:
CJ van den Berg 2026-02-26 12:50:22 +01:00
parent c56e713510
commit 15f471f390
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9
3 changed files with 182 additions and 2 deletions

View file

@ -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 },
},

View file

@ -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",

171
src/file_watcher.zig Normal file
View file

@ -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;
}
};