refactor: add support for atomic rename events on linux
This commit is contained in:
parent
8ffcb0e732
commit
7672eb3c51
1 changed files with 70 additions and 20 deletions
|
|
@ -13,6 +13,8 @@ pub const EventType = enum {
|
||||||
created,
|
created,
|
||||||
modified,
|
modified,
|
||||||
deleted,
|
deleted,
|
||||||
|
/// 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.
|
||||||
renamed,
|
renamed,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -23,6 +25,7 @@ const SpawnError = error{ OutOfMemory, ThespianSpawnFailed };
|
||||||
/// Watch a path (file or directory) for changes. The caller will receive:
|
/// Watch a path (file or directory) for changes. The caller will receive:
|
||||||
/// .{ "FW", "change", path, event_type }
|
/// .{ "FW", "change", path, event_type }
|
||||||
/// where event_type is a file_watcher.EventType tag string: "created", "modified", "deleted", "renamed"
|
/// where event_type is a file_watcher.EventType tag string: "created", "modified", "deleted", "renamed"
|
||||||
|
/// On Linux, paired renames produce: .{ "FW", "rename", from_path, to_path }
|
||||||
pub fn watch(path: []const u8) FileWatcherError!void {
|
pub fn watch(path: []const u8) FileWatcherError!void {
|
||||||
return send(.{ "watch", path });
|
return send(.{ "watch", path });
|
||||||
}
|
}
|
||||||
|
|
@ -120,17 +123,34 @@ const LinuxBackend = struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drain(self: *LinuxBackend, parent: tp.pid_ref) !void {
|
fn drain(self: *LinuxBackend, allocator: std.mem.Allocator, parent: tp.pid_ref) !void {
|
||||||
const InotifyEvent = extern struct {
|
const InotifyEvent = extern struct {
|
||||||
wd: i32,
|
wd: i32,
|
||||||
mask: u32,
|
mask: u32,
|
||||||
cookie: u32,
|
cookie: u32,
|
||||||
len: u32,
|
len: u32,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A pending MOVED_FROM waiting to be paired with a MOVED_TO by cookie.
|
||||||
|
const PendingRename = struct {
|
||||||
|
cookie: u32,
|
||||||
|
path: []u8, // owned by drain's allocator
|
||||||
|
};
|
||||||
|
|
||||||
var buf: [4096]u8 align(@alignOf(InotifyEvent)) = undefined;
|
var buf: [4096]u8 align(@alignOf(InotifyEvent)) = undefined;
|
||||||
|
var pending_renames: std.ArrayListUnmanaged(PendingRename) = .empty;
|
||||||
|
defer {
|
||||||
|
// Any unpaired MOVED_FROM means the file was moved out of the watched tree.
|
||||||
|
for (pending_renames.items) |r| {
|
||||||
|
parent.send(.{ "FW", "change", r.path, EventType.deleted }) catch {}; // moved outside watched tree
|
||||||
|
allocator.free(r.path);
|
||||||
|
}
|
||||||
|
pending_renames.deinit(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const n = std.posix.read(self.inotify_fd, &buf) catch |e| switch (e) {
|
const n = std.posix.read(self.inotify_fd, &buf) catch |e| switch (e) {
|
||||||
error.WouldBlock => return,
|
error.WouldBlock => break,
|
||||||
else => return e,
|
else => return e,
|
||||||
};
|
};
|
||||||
var offset: usize = 0;
|
var offset: usize = 0;
|
||||||
|
|
@ -143,22 +163,50 @@ const LinuxBackend = struct {
|
||||||
std.mem.sliceTo(buf[name_offset..][0..ev.len], 0)
|
std.mem.sliceTo(buf[name_offset..][0..ev.len], 0)
|
||||||
else
|
else
|
||||||
"";
|
"";
|
||||||
const event_type: EventType = if (ev.mask & IN.CREATE != 0)
|
|
||||||
.created
|
var full_buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||||
else if (ev.mask & (IN.DELETE | IN.DELETE_SELF) != 0)
|
const full_path: []const u8 = if (name.len > 0)
|
||||||
.deleted
|
try std.fmt.bufPrint(&full_buf, "{s}/{s}", .{ watched_path, name })
|
||||||
else if (ev.mask & (IN.MODIFY | IN.CLOSE_WRITE) != 0)
|
|
||||||
.modified
|
|
||||||
else if (ev.mask & (IN.MOVED_FROM | IN.MOVED_TO | IN.MOVE_SELF) != 0)
|
|
||||||
.renamed
|
|
||||||
else
|
else
|
||||||
continue;
|
watched_path;
|
||||||
if (name.len > 0) {
|
|
||||||
var full_buf: [std.fs.max_path_bytes]u8 = undefined;
|
if (ev.mask & IN.MOVED_FROM != 0) {
|
||||||
const full_path = try std.fmt.bufPrint(&full_buf, "{s}/{s}", .{ watched_path, name });
|
// Park it, we may receive a paired MOVED_TO with the same cookie.
|
||||||
try parent.send(.{ "FW", "change", full_path, event_type });
|
try pending_renames.append(allocator, .{
|
||||||
|
.cookie = ev.cookie,
|
||||||
|
.path = try allocator.dupe(u8, full_path),
|
||||||
|
});
|
||||||
|
} else if (ev.mask & IN.MOVED_TO != 0) {
|
||||||
|
// Look for a paired MOVED_FROM.
|
||||||
|
var found: ?usize = null;
|
||||||
|
for (pending_renames.items, 0..) |r, i| {
|
||||||
|
if (r.cookie == ev.cookie) {
|
||||||
|
found = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found) |i| {
|
||||||
|
// Complete rename pair: emit a single atomic rename message.
|
||||||
|
const r = pending_renames.swapRemove(i);
|
||||||
|
defer allocator.free(r.path);
|
||||||
|
try parent.send(.{ "FW", "rename", r.path, full_path });
|
||||||
|
} else {
|
||||||
|
// No paired MOVED_FROM, file was moved in from outside the watched tree.
|
||||||
|
try parent.send(.{ "FW", "change", full_path, EventType.created });
|
||||||
|
}
|
||||||
|
} else if (ev.mask & IN.MOVE_SELF != 0) {
|
||||||
|
// The watched directory itself was renamed/moved away.
|
||||||
|
try parent.send(.{ "FW", "change", full_path, EventType.deleted });
|
||||||
} else {
|
} else {
|
||||||
try parent.send(.{ "FW", "change", watched_path, event_type });
|
const event_type: EventType = if (ev.mask & IN.CREATE != 0)
|
||||||
|
.created
|
||||||
|
else if (ev.mask & (IN.DELETE | IN.DELETE_SELF) != 0)
|
||||||
|
.deleted
|
||||||
|
else if (ev.mask & (IN.MODIFY | IN.CLOSE_WRITE) != 0)
|
||||||
|
.modified
|
||||||
|
else
|
||||||
|
continue;
|
||||||
|
try parent.send(.{ "FW", "change", full_path, event_type });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -227,7 +275,8 @@ const MacosBackend = struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drain(self: *MacosBackend, parent: tp.pid_ref) !void {
|
fn drain(self: *MacosBackend, allocator: std.mem.Allocator, parent: tp.pid_ref) !void {
|
||||||
|
_ = allocator;
|
||||||
var events: [64]std.posix.Kevent = undefined;
|
var events: [64]std.posix.Kevent = undefined;
|
||||||
const immediate: std.posix.timespec = .{ .sec = 0, .nsec = 0 };
|
const immediate: std.posix.timespec = .{ .sec = 0, .nsec = 0 };
|
||||||
const n = std.posix.kevent(self.kq, &.{}, &events, &immediate) catch return;
|
const n = std.posix.kevent(self.kq, &.{}, &events, &immediate) catch return;
|
||||||
|
|
@ -350,7 +399,8 @@ const WindowsBackend = struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drain(self: *WindowsBackend, parent: tp.pid_ref) !void {
|
fn drain(self: *WindowsBackend, allocator: std.mem.Allocator, parent: tp.pid_ref) !void {
|
||||||
|
_ = allocator;
|
||||||
var bytes: windows.DWORD = 0;
|
var bytes: windows.DWORD = 0;
|
||||||
var key: windows.ULONG_PTR = 0;
|
var key: windows.ULONG_PTR = 0;
|
||||||
var overlapped_ptr: ?*windows.OVERLAPPED = null;
|
var overlapped_ptr: ?*windows.OVERLAPPED = null;
|
||||||
|
|
@ -453,13 +503,13 @@ const Process = struct {
|
||||||
var err_msg: []const u8 = undefined;
|
var err_msg: []const u8 = undefined;
|
||||||
|
|
||||||
if (try cbor.match(m.buf, .{ "fd", tp.extract(&tag), "read_ready" })) {
|
if (try cbor.match(m.buf, .{ "fd", tp.extract(&tag), "read_ready" })) {
|
||||||
self.backend.drain(self.parent.ref()) catch |e| self.logger.err("drain", e);
|
self.backend.drain(self.allocator, self.parent.ref()) catch |e| self.logger.err("drain", e);
|
||||||
self.backend.arm();
|
self.backend.arm();
|
||||||
} else if (try cbor.match(m.buf, .{ "fd", tp.extract(&tag), "read_error", tp.extract(&err_code), tp.extract(&err_msg) })) {
|
} else if (try cbor.match(m.buf, .{ "fd", tp.extract(&tag), "read_error", tp.extract(&err_code), tp.extract(&err_msg) })) {
|
||||||
self.logger.print("fd read error on {s}: ({d}) {s}", .{ tag, err_code, err_msg });
|
self.logger.print("fd read error on {s}: ({d}) {s}", .{ tag, err_code, err_msg });
|
||||||
self.backend.arm();
|
self.backend.arm();
|
||||||
} else if (builtin.os.tag == .windows and try cbor.match(m.buf, .{"FW_poll"})) {
|
} else if (builtin.os.tag == .windows and try cbor.match(m.buf, .{"FW_poll"})) {
|
||||||
self.backend.drain(self.parent.ref()) catch |e| self.logger.err("drain", e);
|
self.backend.drain(self.allocator, self.parent.ref()) catch |e| self.logger.err("drain", e);
|
||||||
self.backend.arm();
|
self.backend.arm();
|
||||||
} else if (try cbor.match(m.buf, .{ "watch", tp.extract(&path) })) {
|
} else if (try cbor.match(m.buf, .{ "watch", tp.extract(&path) })) {
|
||||||
self.backend.add_watch(self.allocator, path) catch |e| self.logger.err("watch", e);
|
self.backend.add_watch(self.allocator, path) catch |e| self.logger.err("watch", e);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue