From 839e11ef1bffd33c494925f39372c0575e031373 Mon Sep 17 00:00:00 2001 From: chloe caruso Date: Wed, 5 Feb 2025 16:32:12 -0800 Subject: [PATCH] aaaa --- src/Watcher.zig | 670 ------------------------------------------------ src/watcher.zig | 18 +- 2 files changed, 9 insertions(+), 679 deletions(-) delete mode 100644 src/Watcher.zig diff --git a/src/Watcher.zig b/src/Watcher.zig deleted file mode 100644 index 3f710c64ceb354..00000000000000 --- a/src/Watcher.zig +++ /dev/null @@ -1,670 +0,0 @@ -//! Bun's cross-platform filesystem watcher. Runs on its own thread. -const Watcher = @This(); -pub const max_count = 128; - -pub const Event = WatchEvent; -pub const Item = WatchItem; -pub const ItemList = WatchList; -pub const WatchList = std.MultiArrayList(WatchItem); -pub const HashType = u32; -const no_watch_item: WatchItemIndex = std.math.maxInt(WatchItemIndex); - -// Consumer-facing -watch_events: [128]WatchEvent, -changed_filepaths: [128]?[:0]u8, - -/// The platform-specific implementation of the watcher -platform: Platform, - -watchlist: WatchList, -watched_count: usize, -mutex: Mutex, - -fs: *bun.fs.FileSystem, -allocator: std.mem.Allocator, -watchloop_handle: ?std.Thread.Id = null, -cwd: string, -thread: std.Thread = undefined, -running: bool = true, -close_descriptors: bool = false, - -evict_list: [max_eviction_count]WatchItemIndex = undefined, -evict_list_i: WatchItemIndex = 0, - -ctx: *anyopaque, -onFileUpdate: *const fn (this: *anyopaque, events: []WatchEvent, changed_files: []?[:0]u8, watchlist: WatchList) void, -onError: *const fn (this: *anyopaque, err: bun.sys.Error) void, - -thread_lock: bun.DebugThreadLock = bun.DebugThreadLock.unlocked, - -/// Initializes a watcher. Each watcher is tied to some context type, which -/// recieves watch callbacks on the watcher thread. This function does not -/// actually start the watcher thread. -/// -/// const watcher = try Watcher.init(T, instance_of_t, fs, bun.default_allocator) -/// errdefer watcher.deinit(false); -/// try watcher.start(); -/// -/// To integrate a started watcher into module resolution: -/// -/// transpiler.resolver.watcher = watcher.getResolveWatcher(); -/// -/// To integrate a started watcher into bundle_v2: -/// -/// bundle_v2.bun_watcher = watcher; -pub fn init(comptime T: type, ctx: *T, fs: *bun.fs.FileSystem, allocator: std.mem.Allocator) !*Watcher { - const wrapped = struct { - fn onFileUpdateWrapped(ctx_opaque: *anyopaque, events: []WatchEvent, changed_files: []?[:0]u8, watchlist: WatchList) void { - T.onFileUpdate(@alignCast(@ptrCast(ctx_opaque)), events, changed_files, watchlist); - } - fn onErrorWrapped(ctx_opaque: *anyopaque, err: bun.sys.Error) void { - if (@hasDecl(T, "onWatchError")) { - T.onWatchError(@alignCast(@ptrCast(ctx_opaque)), err); - } else { - T.onError(@alignCast(@ptrCast(ctx_opaque)), err); - } - } - }; - - const watcher = try allocator.create(Watcher); - errdefer allocator.destroy(watcher); - watcher.* = Watcher{ - .fs = fs, - .allocator = allocator, - .watched_count = 0, - .watchlist = WatchList{}, - .mutex = .{}, - .cwd = fs.top_level_dir, - .ctx = ctx, - .onFileUpdate = &wrapped.onFileUpdateWrapped, - .onError = &wrapped.onErrorWrapped, - .platform = .{}, - .watch_events = undefined, - .changed_filepaths = [_]?[:0]u8{null} ** 128, - }; - - try Platform.init(&watcher.platform, fs.top_level_dir); - - return watcher; -} - -pub fn start(this: *Watcher) !void { - bun.assert(this.watchloop_handle == null); - this.thread = try std.Thread.spawn(.{}, threadMain, .{this}); -} - -pub fn deinit(this: *Watcher, close_descriptors: bool) void { - if (this.watchloop_handle != null) { - this.mutex.lock(); - defer this.mutex.unlock(); - this.close_descriptors = close_descriptors; - this.running = false; - } else { - if (close_descriptors and this.running) { - const fds = this.watchlist.items(.fd); - for (fds) |fd| { - _ = bun.sys.close(fd); - } - } - this.watchlist.deinit(this.allocator); - const allocator = this.allocator; - allocator.destroy(this); - } -} - -pub fn getHash(filepath: string) HashType { - return @as(HashType, @truncate(bun.hash(filepath))); -} - -pub const WatchItemIndex = u16; -pub const max_eviction_count = 8096; - -const log = bun.Output.scoped(.watcher, false); - -const WindowsWatcher = @import("./watcher/WindowsWatcher.zig"); -// TODO: some platform-specific behavior is implemented in -// this file instead of the platform-specific file. -// ideally, the constants above can be inlined -const Platform = switch (Environment.os) { - .linux => @import("./watcher/INotifyWatcher.zig"), - .mac => @import("./watcher/KEventWatcher.zig"), - .windows => WindowsWatcher, - else => @compileError("Unsupported platform"), -}; - -pub const WatchEvent = struct { - index: WatchItemIndex, - op: Op, - name_off: u8 = 0, - name_len: u8 = 0, - - pub fn names(this: WatchEvent, buf: []?[:0]u8) []?[:0]u8 { - if (this.name_len == 0) return &[_]?[:0]u8{}; - return buf[this.name_off..][0..this.name_len]; - } - - pub const Sorter = void; - - pub fn sortByIndex(_: Sorter, event: WatchEvent, rhs: WatchEvent) bool { - return event.index < rhs.index; - } - - pub fn merge(this: *WatchEvent, other: WatchEvent) void { - this.name_len += other.name_len; - this.op = Op{ - .delete = this.op.delete or other.op.delete, - .metadata = this.op.metadata or other.op.metadata, - .rename = this.op.rename or other.op.rename, - .write = this.op.write or other.op.write, - }; - } - - pub const Op = packed struct { - delete: bool = false, - metadata: bool = false, - rename: bool = false, - write: bool = false, - move_to: bool = false, - - pub fn merge(before: Op, after: Op) Op { - return .{ - .delete = before.delete or after.delete, - .write = before.write or after.write, - .metadata = before.metadata or after.metadata, - .rename = before.rename or after.rename, - .move_to = before.move_to or after.move_to, - }; - } - - pub fn format(op: Op, comptime _: []const u8, _: std.fmt.FormatOptions, w: anytype) !void { - try w.writeAll("{"); - var first = true; - inline for (comptime std.meta.fieldNames(Op)) |name| { - if (@field(op, name)) { - if (!first) { - try w.writeAll(","); - } - first = false; - try w.writeAll(name); - } - } - try w.writeAll("}"); - } - }; -}; - -pub const WatchItem = struct { - file_path: string, - // filepath hash for quick comparison - hash: u32, - loader: options.Loader, - fd: bun.FileDescriptor, - count: u32, - parent_hash: u32, - kind: Kind, - package_json: ?*PackageJSON, - eventlist_index: if (Environment.isLinux) Platform.EventListIndex else u0 = 0, - - pub const Kind = enum { file, directory }; -}; - -fn threadMain(this: *Watcher) !void { - this.watchloop_handle = std.Thread.getCurrentId(); - this.thread_lock.lock(); - Output.Source.configureNamedThread("File Watcher"); - - defer Output.flush(); - if (FeatureFlags.verbose_watcher) Output.prettyln("Watcher started", .{}); - - switch (this.watchLoop()) { - .err => |err| { - this.watchloop_handle = null; - this.platform.stop(); - if (this.running) { - this.onError(this.ctx, err); - } - }, - .result => {}, - } - - // deinit and close descriptors if needed - if (this.close_descriptors) { - const fds = this.watchlist.items(.fd); - for (fds) |fd| { - _ = bun.sys.close(fd); - } - } - this.watchlist.deinit(this.allocator); - - const allocator = this.allocator; - allocator.destroy(this); -} - -pub fn flushEvictions(this: *Watcher) void { - if (this.evict_list_i == 0) return; - defer this.evict_list_i = 0; - - // swapRemove messes up the order - // But, it only messes up the order if any elements in the list appear after the item being removed - // So if we just sort the list by the biggest index first, that should be fine - std.sort.pdq( - WatchItemIndex, - this.evict_list[0..this.evict_list_i], - {}, - comptime std.sort.desc(WatchItemIndex), - ); - - var slice = this.watchlist.slice(); - const fds = slice.items(.fd); - var last_item = no_watch_item; - - for (this.evict_list[0..this.evict_list_i]) |item| { - // catch duplicates, since the list is sorted, duplicates will appear right after each other - if (item == last_item) continue; - - if (!Environment.isWindows) { - // on mac and linux we can just close the file descriptor - // TODO do we need to call inotify_rm_watch on linux? - _ = bun.sys.close(fds[item]); - } - last_item = item; - } - - last_item = no_watch_item; - // This is split into two passes because reading the slice while modified is potentially unsafe. - for (this.evict_list[0..this.evict_list_i]) |item| { - if (item == last_item) continue; - this.watchlist.swapRemove(item); - last_item = item; - } -} - -fn watchLoop(this: *Watcher) bun.JSC.Maybe(void) { - while (this.running) { - // individual platform implementation will call onFileUpdate - switch (Platform.watchLoopCycle(this)) { - .err => |err| return .{ .err = err }, - .result => |iter| iter, - } - } - return .{ .result = {} }; -} - -fn appendFileAssumeCapacity( - this: *Watcher, - fd: bun.FileDescriptor, - file_path: string, - hash: HashType, - loader: options.Loader, - parent_hash: HashType, - package_json: ?*PackageJSON, - comptime copy_file_path: bool, -) bun.JSC.Maybe(void) { - if (comptime Environment.isWindows) { - // on windows we can only watch items that are in the directory tree of the top level dir - const rel = bun.path.isParentOrEqual(this.fs.top_level_dir, file_path); - if (rel == .unrelated) { - Output.warn("File {s} is not in the project directory and will not be watched\n", .{file_path}); - return .{ .result = {} }; - } - } - - const watchlist_id = this.watchlist.len; - - const file_path_: string = if (comptime copy_file_path) - bun.asByteSlice(this.allocator.dupeZ(u8, file_path) catch bun.outOfMemory()) - else - file_path; - - var item = WatchItem{ - .file_path = file_path_, - .fd = fd, - .hash = hash, - .count = 0, - .loader = loader, - .parent_hash = parent_hash, - .package_json = package_json, - .kind = .file, - }; - - if (comptime Environment.isMac) { - const KEvent = std.c.Kevent; - - // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/kqueue.2.html - var event = std.mem.zeroes(KEvent); - - event.flags = std.c.EV.ADD | std.c.EV.CLEAR | std.c.EV.ENABLE; - // we want to know about the vnode - event.filter = std.c.EVFILT.VNODE; - - event.fflags = std.c.NOTE.WRITE | std.c.NOTE.RENAME | std.c.NOTE.DELETE; - - // id - event.ident = @intCast(fd.int()); - - // Store the hash for fast filtering later - event.udata = @as(usize, @intCast(watchlist_id)); - var events: [1]KEvent = .{event}; - - // This took a lot of work to figure out the right permutation - // Basically: - // - We register the event here. - // our while(true) loop above receives notification of changes to any of the events created here. - _ = std.posix.system.kevent( - this.platform.fd.cast(), - @as([]KEvent, events[0..1]).ptr, - 1, - @as([]KEvent, events[0..1]).ptr, - 0, - null, - ); - } else if (comptime Environment.isLinux) { - // var file_path_to_use_ = std.mem.trimRight(u8, file_path_, "/"); - // var buf: [bun.MAX_PATH_BYTES+1]u8 = undefined; - // bun.copy(u8, &buf, file_path_to_use_); - // buf[file_path_to_use_.len] = 0; - var buf = file_path_.ptr; - const slice: [:0]const u8 = buf[0..file_path_.len :0]; - item.eventlist_index = switch (this.platform.watchPath(slice)) { - .err => |err| return .{ .err = err }, - .result => |r| r, - }; - } - - this.watchlist.appendAssumeCapacity(item); - return .{ .result = {} }; -} - -fn appendDirectoryAssumeCapacity( - this: *Watcher, - stored_fd: bun.FileDescriptor, - file_path: string, - hash: HashType, - comptime copy_file_path: bool, -) bun.JSC.Maybe(WatchItemIndex) { - if (comptime Environment.isWindows) { - // on windows we can only watch items that are in the directory tree of the top level dir - const rel = bun.path.isParentOrEqual(this.fs.top_level_dir, file_path); - if (rel == .unrelated) { - Output.warn("Directory {s} is not in the project directory and will not be watched\n", .{file_path}); - return .{ .result = no_watch_item }; - } - } - - const fd = brk: { - if (stored_fd != .zero) break :brk stored_fd; - break :brk switch (bun.sys.openA(file_path, 0, 0)) { - .err => |err| return .{ .err = err }, - .result => |fd| fd, - }; - }; - - const parent_hash = getHash(bun.fs.PathName.init(file_path).dirWithTrailingSlash()); - - const file_path_: string = if (comptime copy_file_path) - bun.asByteSlice(this.allocator.dupeZ(u8, file_path) catch bun.outOfMemory()) - else - file_path; - - const watchlist_id = this.watchlist.len; - - var item = WatchItem{ - .file_path = file_path_, - .fd = fd, - .hash = hash, - .count = 0, - .loader = options.Loader.file, - .parent_hash = parent_hash, - .kind = .directory, - .package_json = null, - }; - - if (Environment.isMac) { - const KEvent = std.c.Kevent; - - // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/kqueue.2.html - var event = std.mem.zeroes(KEvent); - - event.flags = std.c.EV.ADD | std.c.EV.CLEAR | std.c.EV.ENABLE; - // we want to know about the vnode - event.filter = std.c.EVFILT.VNODE; - - // monitor: - // - Write - // - Rename - // - Delete - event.fflags = std.c.NOTE.WRITE | std.c.NOTE.RENAME | std.c.NOTE.DELETE; - - // id - event.ident = @intCast(fd.int()); - - // Store the hash for fast filtering later - event.udata = @as(usize, @intCast(watchlist_id)); - var events: [1]KEvent = .{event}; - - // This took a lot of work to figure out the right permutation - // Basically: - // - We register the event here. - // our while(true) loop above receives notification of changes to any of the events created here. - _ = std.posix.system.kevent( - this.platform.fd.cast(), - @as([]KEvent, events[0..1]).ptr, - 1, - @as([]KEvent, events[0..1]).ptr, - 0, - null, - ); - } else if (Environment.isLinux) { - const file_path_to_use_ = std.mem.trimRight(u8, file_path_, "/"); - var buf: bun.PathBuffer = undefined; - bun.copy(u8, &buf, file_path_to_use_); - buf[file_path_to_use_.len] = 0; - const slice: [:0]u8 = buf[0..file_path_to_use_.len :0]; - item.eventlist_index = switch (this.platform.watchDir(slice)) { - .err => |err| return .{ .err = err }, - .result => |r| r, - }; - } - - this.watchlist.appendAssumeCapacity(item); - return .{ - .result = @as(WatchItemIndex, @truncate(this.watchlist.len - 1)), - }; -} - -// Below is platform-independent - -pub fn appendFileMaybeLock( - this: *Watcher, - fd: bun.FileDescriptor, - file_path: string, - hash: HashType, - loader: options.Loader, - dir_fd: bun.FileDescriptor, - package_json: ?*PackageJSON, - comptime copy_file_path: bool, - comptime lock: bool, -) bun.JSC.Maybe(void) { - if (comptime lock) this.mutex.lock(); - defer if (comptime lock) this.mutex.unlock(); - bun.assert(file_path.len > 1); - const pathname = bun.fs.PathName.init(file_path); - - const parent_dir = pathname.dirWithTrailingSlash(); - const parent_dir_hash: HashType = getHash(parent_dir); - - var parent_watch_item: ?WatchItemIndex = null; - const autowatch_parent_dir = (comptime FeatureFlags.watch_directories) and this.isEligibleDirectory(parent_dir); - if (autowatch_parent_dir) { - var watchlist_slice = this.watchlist.slice(); - - if (dir_fd != .zero) { - const fds = watchlist_slice.items(.fd); - if (std.mem.indexOfScalar(bun.FileDescriptor, fds, dir_fd)) |i| { - parent_watch_item = @as(WatchItemIndex, @truncate(i)); - } - } - - if (parent_watch_item == null) { - const hashes = watchlist_slice.items(.hash); - if (std.mem.indexOfScalar(HashType, hashes, parent_dir_hash)) |i| { - parent_watch_item = @as(WatchItemIndex, @truncate(i)); - } - } - } - this.watchlist.ensureUnusedCapacity(this.allocator, 1 + @as(usize, @intCast(@intFromBool(parent_watch_item == null)))) catch bun.outOfMemory(); - - if (autowatch_parent_dir) { - parent_watch_item = parent_watch_item orelse switch (this.appendDirectoryAssumeCapacity(dir_fd, parent_dir, parent_dir_hash, copy_file_path)) { - .err => |err| return .{ .err = err }, - .result => |r| r, - }; - } - - switch (this.appendFileAssumeCapacity( - fd, - file_path, - hash, - loader, - parent_dir_hash, - package_json, - copy_file_path, - )) { - .err => |err| return .{ .err = err }, - .result => {}, - } - - if (comptime FeatureFlags.verbose_watcher) { - if (strings.indexOf(file_path, this.cwd)) |i| { - Output.prettyln("Added ./{s} to watch list.", .{file_path[i + this.cwd.len ..]}); - } else { - Output.prettyln("Added {s} to watch list.", .{file_path}); - } - } - - return .{ .result = {} }; -} - -inline fn isEligibleDirectory(this: *Watcher, dir: string) bool { - return strings.contains(dir, this.fs.top_level_dir) and !strings.contains(dir, "node_modules"); -} - -pub fn appendFile( - this: *Watcher, - fd: bun.FileDescriptor, - file_path: string, - hash: HashType, - loader: options.Loader, - dir_fd: bun.FileDescriptor, - package_json: ?*PackageJSON, - comptime copy_file_path: bool, -) bun.JSC.Maybe(void) { - return appendFileMaybeLock(this, fd, file_path, hash, loader, dir_fd, package_json, copy_file_path, true); -} - -pub fn addDirectory( - this: *Watcher, - fd: bun.FileDescriptor, - file_path: string, - hash: HashType, - comptime copy_file_path: bool, -) bun.JSC.Maybe(WatchItemIndex) { - this.mutex.lock(); - defer this.mutex.unlock(); - - if (this.indexOf(hash)) |idx| { - return .{ .result = @truncate(idx) }; - } - - this.watchlist.ensureUnusedCapacity(this.allocator, 1) catch bun.outOfMemory(); - - return this.appendDirectoryAssumeCapacity(fd, file_path, hash, copy_file_path); -} - -pub fn addFile( - this: *Watcher, - fd: bun.FileDescriptor, - file_path: string, - hash: HashType, - loader: options.Loader, - dir_fd: bun.FileDescriptor, - package_json: ?*PackageJSON, - comptime copy_file_path: bool, -) bun.JSC.Maybe(void) { - // This must lock due to concurrent transpiler - this.mutex.lock(); - defer this.mutex.unlock(); - - if (this.indexOf(hash)) |index| { - if (comptime FeatureFlags.atomic_file_watcher) { - // On Linux, the file descriptor might be out of date. - if (fd.int() > 0) { - var fds = this.watchlist.items(.fd); - fds[index] = fd; - } - } - return .{ .result = {} }; - } - - return this.appendFileMaybeLock(fd, file_path, hash, loader, dir_fd, package_json, copy_file_path, false); -} - -pub fn indexOf(this: *Watcher, hash: HashType) ?u32 { - for (this.watchlist.items(.hash), 0..) |other, i| { - if (hash == other) { - return @as(u32, @truncate(i)); - } - } - return null; -} - -pub fn remove(this: *Watcher, hash: HashType) void { - this.mutex.lock(); - defer this.mutex.unlock(); - if (this.indexOf(hash)) |index| { - this.removeAtIndex(@truncate(index), hash, &[_]HashType{}, .file); - } -} - -pub fn removeAtIndex(this: *Watcher, index: WatchItemIndex, hash: HashType, parents: []HashType, comptime kind: WatchItem.Kind) void { - bun.assert(index != no_watch_item); - - this.evict_list[this.evict_list_i] = index; - this.evict_list_i += 1; - - if (comptime kind == .directory) { - for (parents) |parent| { - if (parent == hash) { - this.evict_list[this.evict_list_i] = @as(WatchItemIndex, @truncate(parent)); - this.evict_list_i += 1; - } - } - } -} - -pub fn getResolveWatcher(watcher: *Watcher) bun.resolver.AnyResolveWatcher { - return bun.resolver.ResolveWatcher(*@This(), onMaybeWatchDirectory).init(watcher); -} - -pub fn onMaybeWatchDirectory(watch: *Watcher, file_path: string, dir_fd: bun.StoredFileDescriptorType) void { - // We don't want to watch: - // - Directories outside the root directory - // - Directories inside node_modules - if (std.mem.indexOf(u8, file_path, "node_modules") == null and std.mem.indexOf(u8, file_path, watch.fs.top_level_dir) != null) { - _ = watch.addDirectory(dir_fd, file_path, getHash(file_path), false); - } -} - -const std = @import("std"); -const bun = @import("root").bun; -const string = bun.string; -const Output = bun.Output; -const Global = bun.Global; -const Environment = bun.Environment; -const strings = bun.strings; -const stringZ = bun.stringZ; -const FeatureFlags = bun.FeatureFlags; -const options = @import("./options.zig"); -const Mutex = bun.Mutex; -const Futex = @import("./futex.zig"); -const PackageJSON = @import("./resolver/package_json.zig").PackageJSON; diff --git a/src/watcher.zig b/src/watcher.zig index f8cb05068169a7..3f710c64ceb354 100644 --- a/src/watcher.zig +++ b/src/watcher.zig @@ -35,7 +35,7 @@ ctx: *anyopaque, onFileUpdate: *const fn (this: *anyopaque, events: []WatchEvent, changed_files: []?[:0]u8, watchlist: WatchList) void, onError: *const fn (this: *anyopaque, err: bun.sys.Error) void, -thread_lock: bun.DebugThreadLock = .unlocked, +thread_lock: bun.DebugThreadLock = bun.DebugThreadLock.unlocked, /// Initializes a watcher. Each watcher is tied to some context type, which /// recieves watch callbacks on the watcher thread. This function does not @@ -119,8 +119,7 @@ pub fn getHash(filepath: string) HashType { pub const WatchItemIndex = u16; pub const max_eviction_count = 8096; -const DebugLogScope = bun.Output.Scoped(.watcher, false); -const log = DebugLogScope.log; +const log = bun.Output.scoped(.watcher, false); const WindowsWatcher = @import("./watcher/WindowsWatcher.zig"); // TODO: some platform-specific behavior is implemented in @@ -215,7 +214,7 @@ fn threadMain(this: *Watcher) !void { Output.Source.configureNamedThread("File Watcher"); defer Output.flush(); - log("Watcher started", .{}); + if (FeatureFlags.verbose_watcher) Output.prettyln("Watcher started", .{}); switch (this.watchLoop()) { .err => |err| { @@ -535,11 +534,12 @@ pub fn appendFileMaybeLock( .result => {}, } - if (DebugLogScope.isVisible()) { - log("Added {s} to watch list.", .{if (strings.indexOf(file_path, this.cwd)) |i| - file_path[i + this.cwd.len ..] - else - file_path}); + if (comptime FeatureFlags.verbose_watcher) { + if (strings.indexOf(file_path, this.cwd)) |i| { + Output.prettyln("Added ./{s} to watch list.", .{file_path[i + this.cwd.len ..]}); + } else { + Output.prettyln("Added {s} to watch list.", .{file_path}); + } } return .{ .result = {} };