mirror of
https://github.com/oven-sh/bun
synced 2026-02-13 04:18:58 +00:00
323 lines
11 KiB
Zig
323 lines
11 KiB
Zig
//! Bun's filesystem watcher implementation for windows using kernel32
|
|
|
|
const WindowsWatcher = @This();
|
|
|
|
mutex: Mutex = .{},
|
|
iocp: w.HANDLE = undefined,
|
|
watcher: DirWatcher = undefined,
|
|
buf: bun.PathBuffer = undefined,
|
|
base_idx: usize = 0,
|
|
|
|
pub const EventListIndex = c_int;
|
|
|
|
const Error = error{
|
|
IocpFailed,
|
|
ReadDirectoryChangesFailed,
|
|
CreateFileFailed,
|
|
InvalidPath,
|
|
};
|
|
|
|
const Action = enum(w.DWORD) {
|
|
Added = w.FILE_ACTION_ADDED,
|
|
Removed = w.FILE_ACTION_REMOVED,
|
|
Modified = w.FILE_ACTION_MODIFIED,
|
|
RenamedOld = w.FILE_ACTION_RENAMED_OLD_NAME,
|
|
RenamedNew = w.FILE_ACTION_RENAMED_NEW_NAME,
|
|
};
|
|
|
|
const FileEvent = struct {
|
|
action: Action,
|
|
filename: []u16 = undefined,
|
|
};
|
|
|
|
const DirWatcher = struct {
|
|
// must be initialized to zero (even though it's never read or written in our code),
|
|
// otherwise ReadDirectoryChangesW will fail with INVALID_HANDLE
|
|
overlapped: w.OVERLAPPED = std.mem.zeroes(w.OVERLAPPED),
|
|
buf: [64 * 1024]u8 align(@alignOf(w.FILE_NOTIFY_INFORMATION)) = undefined,
|
|
dirHandle: w.HANDLE,
|
|
|
|
// invalidates any EventIterators
|
|
fn prepare(this: *DirWatcher) bun.sys.Maybe(void) {
|
|
const filter: w.FileNotifyChangeFilter = .{ .file_name = true, .dir_name = true, .last_write = true, .creation = true };
|
|
if (w.kernel32.ReadDirectoryChangesW(this.dirHandle, &this.buf, this.buf.len, 1, filter, null, &this.overlapped, null) == 0) {
|
|
const err = w.kernel32.GetLastError();
|
|
log("failed to start watching directory: {s}", .{@tagName(err)});
|
|
return .{ .err = .{
|
|
.errno = @intFromEnum(bun.sys.SystemErrno.init(err) orelse bun.sys.SystemErrno.EINVAL),
|
|
.syscall = .watch,
|
|
} };
|
|
}
|
|
log("read directory changes!", .{});
|
|
return .success;
|
|
}
|
|
};
|
|
|
|
const EventIterator = struct {
|
|
watcher: *DirWatcher,
|
|
offset: usize = 0,
|
|
hasNext: bool = true,
|
|
|
|
pub fn next(this: *EventIterator) ?FileEvent {
|
|
if (!this.hasNext) return null;
|
|
const info_size = @sizeOf(w.FILE_NOTIFY_INFORMATION);
|
|
const info: *w.FILE_NOTIFY_INFORMATION = @alignCast(@ptrCast(this.watcher.buf[this.offset..].ptr));
|
|
const name_ptr: [*]u16 = @alignCast(@ptrCast(this.watcher.buf[this.offset + info_size ..]));
|
|
const filename: []u16 = name_ptr[0 .. info.FileNameLength / @sizeOf(u16)];
|
|
|
|
const action: Action = @enumFromInt(info.Action);
|
|
|
|
if (info.NextEntryOffset == 0) {
|
|
this.hasNext = false;
|
|
} else {
|
|
this.offset += @as(usize, info.NextEntryOffset);
|
|
}
|
|
|
|
return FileEvent{
|
|
.action = action,
|
|
.filename = filename,
|
|
};
|
|
}
|
|
};
|
|
|
|
pub fn init(this: *WindowsWatcher, root: []const u8) !void {
|
|
var pathbuf: bun.WPathBuffer = undefined;
|
|
const wpath = bun.strings.toNTPath(&pathbuf, root);
|
|
const path_len_bytes: u16 = @truncate(wpath.len * 2);
|
|
var nt_name = w.UNICODE_STRING{
|
|
.Length = path_len_bytes,
|
|
.MaximumLength = path_len_bytes,
|
|
.Buffer = @constCast(wpath.ptr),
|
|
};
|
|
var attr = w.OBJECT_ATTRIBUTES{
|
|
.Length = @sizeOf(w.OBJECT_ATTRIBUTES),
|
|
.RootDirectory = null,
|
|
.Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here.
|
|
.ObjectName = &nt_name,
|
|
.SecurityDescriptor = null,
|
|
.SecurityQualityOfService = null,
|
|
};
|
|
var handle: w.HANDLE = w.INVALID_HANDLE_VALUE;
|
|
var io: w.IO_STATUS_BLOCK = undefined;
|
|
const rc = w.ntdll.NtCreateFile(
|
|
&handle,
|
|
w.FILE_LIST_DIRECTORY,
|
|
&attr,
|
|
&io,
|
|
null,
|
|
0,
|
|
w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE,
|
|
w.FILE_OPEN,
|
|
w.FILE_DIRECTORY_FILE | w.FILE_OPEN_FOR_BACKUP_INTENT,
|
|
null,
|
|
0,
|
|
);
|
|
|
|
if (rc != .SUCCESS) {
|
|
const err = bun.windows.Win32Error.fromNTStatus(rc);
|
|
log("failed to open directory for watching: {s}", .{@tagName(err)});
|
|
return Error.CreateFileFailed;
|
|
}
|
|
errdefer _ = bun.windows.CloseHandle(handle);
|
|
|
|
this.iocp = try w.CreateIoCompletionPort(handle, null, 0, 1);
|
|
errdefer _ = bun.windows.CloseHandle(this.iocp);
|
|
|
|
this.watcher = .{ .dirHandle = handle };
|
|
|
|
@memcpy(this.buf[0..root.len], root);
|
|
const needs_slash = root.len == 0 or !bun.strings.charIsAnySlash(root[root.len - 1]);
|
|
if (needs_slash) {
|
|
this.buf[root.len] = '\\';
|
|
}
|
|
this.base_idx = if (needs_slash) root.len + 1 else root.len;
|
|
}
|
|
|
|
const Timeout = enum(w.DWORD) {
|
|
infinite = w.INFINITE,
|
|
minimal = 1,
|
|
none = 0,
|
|
};
|
|
|
|
// wait until new events are available
|
|
pub fn next(this: *WindowsWatcher, timeout: Timeout) bun.sys.Maybe(?EventIterator) {
|
|
switch (this.watcher.prepare()) {
|
|
.err => |err| {
|
|
log("prepare() returned error", .{});
|
|
return .{ .err = err };
|
|
},
|
|
.result => {},
|
|
}
|
|
|
|
var nbytes: w.DWORD = 0;
|
|
var key: w.ULONG_PTR = 0;
|
|
var overlapped: ?*w.OVERLAPPED = null;
|
|
while (true) {
|
|
const rc = w.kernel32.GetQueuedCompletionStatus(this.iocp, &nbytes, &key, &overlapped, @intFromEnum(timeout));
|
|
if (rc == 0) {
|
|
const err = w.kernel32.GetLastError();
|
|
if (err == .TIMEOUT or err == .WAIT_TIMEOUT) {
|
|
return .{ .result = null };
|
|
} else {
|
|
log("GetQueuedCompletionStatus failed: {s}", .{@tagName(err)});
|
|
return .{ .err = .{
|
|
.errno = @intFromEnum(bun.sys.SystemErrno.init(err) orelse bun.sys.SystemErrno.EINVAL),
|
|
.syscall = .watch,
|
|
} };
|
|
}
|
|
}
|
|
|
|
if (overlapped) |ptr| {
|
|
// ignore possible spurious events
|
|
if (ptr != &this.watcher.overlapped) {
|
|
continue;
|
|
}
|
|
if (nbytes == 0) {
|
|
// shutdown notification
|
|
// TODO close handles?
|
|
log("shutdown notification in WindowsWatcher.next", .{});
|
|
return .{ .err = .{
|
|
.errno = @intFromEnum(bun.sys.SystemErrno.ESHUTDOWN),
|
|
.syscall = .watch,
|
|
} };
|
|
}
|
|
return .{ .result = EventIterator{ .watcher = &this.watcher } };
|
|
} else {
|
|
log("GetQueuedCompletionStatus returned no overlapped event", .{});
|
|
return .{ .err = .{
|
|
.errno = @truncate(@intFromEnum(bun.sys.E.INVAL)),
|
|
.syscall = .watch,
|
|
} };
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn stop(this: *WindowsWatcher) void {
|
|
w.CloseHandle(this.watcher.dirHandle);
|
|
w.CloseHandle(this.iocp);
|
|
}
|
|
|
|
pub fn watchLoopCycle(this: *bun.Watcher) bun.sys.Maybe(void) {
|
|
const buf = &this.platform.buf;
|
|
const base_idx = this.platform.base_idx;
|
|
|
|
var event_id: usize = 0;
|
|
|
|
// first wait has infinite timeout - we're waiting for the next event and don't want to spin
|
|
var timeout = WindowsWatcher.Timeout.infinite;
|
|
while (true) {
|
|
var iter = switch (this.platform.next(timeout)) {
|
|
.err => |err| return .{ .err = err },
|
|
.result => |iter| iter orelse break,
|
|
};
|
|
// after the first wait, we want to coalesce further events but don't want to wait for them
|
|
// NOTE: using a 1ms timeout would be ideal, but that actually makes the thread wait for at least 10ms more than it should
|
|
// Instead we use a 0ms timeout, which may not do as much coalescing but is more responsive.
|
|
timeout = WindowsWatcher.Timeout.none;
|
|
const item_paths = this.watchlist.items(.file_path);
|
|
log("number of watched items: {d}", .{item_paths.len});
|
|
while (iter.next()) |event| {
|
|
const convert_res = bun.strings.copyUTF16IntoUTF8(buf[base_idx..], []const u16, event.filename);
|
|
const eventpath = buf[0 .. base_idx + convert_res.written];
|
|
|
|
log("watcher update event: (filename: {s}, action: {s}", .{ eventpath, @tagName(event.action) });
|
|
|
|
// TODO this probably needs a more sophisticated search algorithm in the future
|
|
// Possible approaches:
|
|
// - Keep a sorted list of the watched paths and perform a binary search. We could use a bool to keep
|
|
// track of whether the list is sorted and only sort it when we detect a change.
|
|
// - Use a prefix tree. Potentially more efficient for large numbers of watched paths, but complicated
|
|
// to implement and maintain.
|
|
// - others that i'm not thinking of
|
|
|
|
for (item_paths, 0..) |path, item_idx| {
|
|
// check if the current change applies to this item
|
|
// if so, add it to the eventlist
|
|
const rel = bun.path.isParentOrEqual(path, eventpath);
|
|
log("checking path: {s} = .{s}", .{ path, @tagName(rel) });
|
|
// skip unrelated items
|
|
if (rel == .unrelated) continue;
|
|
// if the event is for a parent dir of the item, only emit it if it's a delete or rename
|
|
|
|
// Check if we're about to exceed the watch_events array capacity
|
|
if (event_id >= this.watch_events.len) {
|
|
// Process current batch of events
|
|
switch (processWatchEventBatch(this, event_id)) {
|
|
.err => |err| return .{ .err = err },
|
|
.result => {},
|
|
}
|
|
// Reset event_id to start a new batch
|
|
event_id = 0;
|
|
}
|
|
|
|
this.watch_events[event_id] = createWatchEvent(event, @truncate(item_idx));
|
|
event_id += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process any remaining events in the final batch
|
|
if (event_id > 0) {
|
|
switch (processWatchEventBatch(this, event_id)) {
|
|
.err => |err| return .{ .err = err },
|
|
.result => {},
|
|
}
|
|
}
|
|
|
|
return .success;
|
|
}
|
|
|
|
fn processWatchEventBatch(this: *bun.Watcher, event_count: usize) bun.sys.Maybe(void) {
|
|
if (event_count == 0) {
|
|
return .success;
|
|
}
|
|
|
|
// log("event_count: {d}\n", .{event_count});
|
|
|
|
var all_events = this.watch_events[0..event_count];
|
|
std.sort.pdq(WatchEvent, all_events, {}, WatchEvent.sortByIndex);
|
|
|
|
var last_event_index: usize = 0;
|
|
var last_event_id: u32 = std.math.maxInt(u32);
|
|
|
|
for (all_events, 0..) |_, i| {
|
|
if (all_events[i].index == last_event_id) {
|
|
all_events[last_event_index].merge(all_events[i]);
|
|
continue;
|
|
}
|
|
last_event_index = i;
|
|
last_event_id = all_events[i].index;
|
|
}
|
|
if (all_events.len == 0) return .success;
|
|
all_events = all_events[0 .. last_event_index + 1];
|
|
|
|
log("calling onFileUpdate (all_events.len = {d})", .{all_events.len});
|
|
|
|
this.onFileUpdate(this.ctx, all_events, this.changed_filepaths[0 .. last_event_index + 1], this.watchlist);
|
|
|
|
return .success;
|
|
}
|
|
|
|
pub fn createWatchEvent(event: FileEvent, index: WatchItemIndex) WatchEvent {
|
|
return .{
|
|
.op = .{
|
|
.delete = event.action == .Removed,
|
|
.rename = event.action == .RenamedOld,
|
|
.write = event.action == .Modified,
|
|
},
|
|
.index = index,
|
|
};
|
|
}
|
|
|
|
const log = Output.scoped(.watcher, .visible);
|
|
|
|
const std = @import("std");
|
|
const w = std.os.windows;
|
|
|
|
const bun = @import("bun");
|
|
const Mutex = bun.Mutex;
|
|
const Output = bun.Output;
|
|
|
|
const WatchEvent = bun.Watcher.WatchEvent;
|
|
const WatchItemIndex = bun.Watcher.WatchItemIndex;
|