diff options
Diffstat (limited to 'src/bun.js/node/path_watcher.zig')
| -rw-r--r-- | src/bun.js/node/path_watcher.zig | 560 |
1 files changed, 560 insertions, 0 deletions
diff --git a/src/bun.js/node/path_watcher.zig b/src/bun.js/node/path_watcher.zig new file mode 100644 index 000000000..5a22cb783 --- /dev/null +++ b/src/bun.js/node/path_watcher.zig @@ -0,0 +1,560 @@ +const std = @import("std"); + +const UnboundedQueue = @import("../unbounded_queue.zig").UnboundedQueue; +const Path = @import("../../resolver/resolve_path.zig"); +const Fs = @import("../../fs.zig"); +const Mutex = @import("../../lock.zig").Lock; + +const bun = @import("root").bun; +const Output = bun.Output; +const Environment = bun.Environment; +const StoredFileDescriptorType = bun.StoredFileDescriptorType; +const string = bun.string; +const JSC = bun.JSC; +const VirtualMachine = JSC.VirtualMachine; + +const sync = @import("../../sync.zig"); +const Semaphore = sync.Semaphore; + +var default_manager_mutex: Mutex = Mutex.init(); +var default_manager: ?*PathWatcherManager = null; + +pub const PathWatcherManager = struct { + const GenericWatcher = @import("../../watcher.zig"); + const options = @import("../../options.zig"); + pub const Watcher = GenericWatcher.NewWatcher(*PathWatcherManager); + const log = Output.scoped(.PathWatcherManager, false); + main_watcher: *Watcher, + + watchers: bun.BabyList(?*PathWatcher) = .{}, + watcher_count: u32 = 0, + vm: *JSC.VirtualMachine, + file_paths: bun.StringHashMap(PathInfo), + deinit_on_last_watcher: bool = false, + mutex: Mutex, + + const PathInfo = struct { + fd: StoredFileDescriptorType = 0, + is_file: bool = true, + path: [:0]const u8, + dirname: string, + refs: u32 = 0, + hash: Watcher.HashType, + }; + + fn _fdFromAbsolutePathZ( + this: *PathWatcherManager, + path: [:0]const u8, + ) !PathInfo { + if (this.file_paths.getEntry(path)) |entry| { + var info = entry.value_ptr; + info.refs += 1; + return info.*; + } + const cloned_path = try bun.default_allocator.dupeZ(u8, path); + errdefer bun.default_allocator.destroy(cloned_path); + + var stat = try bun.C.lstat_absolute(cloned_path); + var result = PathInfo{ + .path = cloned_path, + .dirname = cloned_path, + .hash = Watcher.getHash(cloned_path), + .refs = 1, + }; + + switch (stat.kind) { + .sym_link => { + var file = try std.fs.openFileAbsoluteZ(cloned_path, .{ .mode = .read_only }); + result.fd = file.handle; + const _stat = try file.stat(); + + result.is_file = _stat.kind != .directory; + if (result.is_file) { + result.dirname = std.fs.path.dirname(cloned_path) orelse cloned_path; + } + }, + .directory => { + const dir = (try std.fs.openIterableDirAbsoluteZ(cloned_path, .{ + .access_sub_paths = true, + })).dir; + result.fd = dir.fd; + result.is_file = false; + }, + else => { + const file = try std.fs.openFileAbsoluteZ(cloned_path, .{ .mode = .read_only }); + result.fd = file.handle; + result.is_file = true; + result.dirname = std.fs.path.dirname(cloned_path) orelse cloned_path; + }, + } + + _ = try this.file_paths.put(cloned_path, result); + return result; + } + + pub fn init(vm: *JSC.VirtualMachine) !*PathWatcherManager { + const this = try bun.default_allocator.create(PathWatcherManager); + errdefer bun.default_allocator.destroy(this); + var watchers = bun.BabyList(?*PathWatcher).initCapacity(bun.default_allocator, 1) catch |err| { + bun.default_allocator.destroy(this); + return err; + }; + errdefer watchers.deinitWithAllocator(bun.default_allocator); + var manager = PathWatcherManager{ + .file_paths = bun.StringHashMap(PathInfo).init(bun.default_allocator), + .watchers = watchers, + .main_watcher = try Watcher.init( + this, + vm.bundler.fs, + bun.default_allocator, + ), + .vm = vm, + .watcher_count = 0, + .mutex = Mutex.init(), + }; + + this.* = manager; + try this.main_watcher.start(); + return this; + } + + pub fn onFileUpdate( + this: *PathWatcherManager, + events: []GenericWatcher.WatchEvent, + changed_files: []?[:0]u8, + watchlist: GenericWatcher.Watchlist, + ) void { + var slice = watchlist.slice(); + const file_paths = slice.items(.file_path); + + var counts = slice.items(.count); + const kinds = slice.items(.kind); + var _on_file_update_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + + var ctx = this.main_watcher; + defer ctx.flushEvictions(); + + const timestamp = std.time.milliTimestamp(); + + this.mutex.lock(); + defer this.mutex.unlock(); + + const watchers = this.watchers.slice(); + + for (events) |event| { + const file_path = file_paths[event.index]; + const update_count = counts[event.index] + 1; + counts[event.index] = update_count; + const kind = kinds[event.index]; + + if (comptime Environment.isDebug) { + log("[watch] {s} ({s}, {})", .{ file_path, @tagName(kind), event.op }); + } + + switch (kind) { + .file => { + if (event.op.delete) { + ctx.removeAtIndex( + event.index, + 0, + &.{}, + .file, + ); + } + + if (event.op.write or event.op.delete or event.op.rename) { + const event_type: PathWatcher.EventType = if (event.op.delete or event.op.rename or event.op.move_to) .rename else .change; + const hash = Watcher.getHash(file_path); + + for (watchers) |w| { + if (w) |watcher| { + const entry_point = watcher.path.dirname; + var path = file_path; + + if (path.len < entry_point.len) { + continue; + } + if (watcher.path.is_file) { + if (watcher.path.hash != hash) { + continue; + } + } else { + if (!bun.strings.startsWith(path, entry_point)) { + continue; + } + } + // Remove common prefix, unless the watched folder is "/" + if (!(path.len == 1 and entry_point[0] == '/')) { + path = path[entry_point.len..]; + + // Ignore events with path equal to directory itself + if (path.len <= 1) { + continue; + } + if (path.len == 0) { + while (path.len > 0) { + if (bun.strings.startsWithChar(path, '/')) { + path = path[1..]; + break; + } else { + path = path[1..]; + } + } + } else { + // Skip forward slash + path = path[1..]; + } + } + + // Do not emit events from subdirectories (without option set) + if (path.len == 0 or (bun.strings.containsChar(path, '/') and !watcher.recursive)) { + continue; + } + watcher.emit(path, hash, timestamp, true, event_type); + } + } + } + }, + .directory => { + const affected = event.names(changed_files); + + for (affected) |changed_name_| { + const changed_name: []const u8 = bun.asByteSlice(changed_name_.?); + if (changed_name.len == 0 or changed_name[0] == '~' or changed_name[0] == '.') continue; + + var file_path_without_trailing_slash = std.mem.trimRight(u8, file_path, std.fs.path.sep_str); + + @memcpy(_on_file_update_path_buf[0..file_path_without_trailing_slash.len], file_path_without_trailing_slash); + + _on_file_update_path_buf[file_path_without_trailing_slash.len] = std.fs.path.sep; + + @memcpy(_on_file_update_path_buf[file_path_without_trailing_slash.len + 1 ..][0..changed_name.len], changed_name); + const len = file_path_without_trailing_slash.len + changed_name.len; + const path_slice = _on_file_update_path_buf[0 .. len + 1]; + + const hash = Watcher.getHash(path_slice); + + // skip consecutive duplicates + const event_type: PathWatcher.EventType = .rename; // renaming folders, creating folder or files will be always be rename + for (watchers) |w| { + if (w) |watcher| { + const entry_point = watcher.path.dirname; + var path = path_slice; + + if (watcher.path.is_file or path.len < entry_point.len or !bun.strings.startsWith(path, entry_point)) { + continue; + } + // Remove common prefix, unless the watched folder is "/" + if (!(path.len == 1 and entry_point[0] == '/')) { + path = path[entry_point.len..]; + + if (path.len == 0) { + while (path.len > 0) { + if (bun.strings.startsWithChar(path, '/')) { + path = path[1..]; + break; + } else { + path = path[1..]; + } + } + } else { + // Skip forward slash + path = path[1..]; + } + } + + // Do not emit events from subdirectories (without option set) + if (path.len == 0 or (bun.strings.containsChar(path, '/') and !watcher.recursive)) { + continue; + } + + watcher.emit(path, hash, timestamp, false, event_type); + } + } + } + }, + } + } + + if (comptime Environment.isDebug) { + Output.flush(); + } + for (watchers) |w| { + if (w) |watcher| { + if (watcher.needs_flush) watcher.flush(); + } + } + } + + pub fn onError( + this: *PathWatcherManager, + err: anyerror, + ) void { + this.mutex.lock(); + const watchers = this.watchers.slice(); + const timestamp = std.time.milliTimestamp(); + + // stop all watchers + for (watchers) |w| { + if (w) |watcher| { + watcher.emit(@errorName(err), 0, timestamp, false, .@"error"); + watcher.flush(); + } + } + + // we need a new manager at this point + default_manager_mutex.lock(); + defer default_manager_mutex.unlock(); + default_manager = null; + + // deinit manager when all watchers are closed + this.mutex.unlock(); + this.deinit(); + } + + fn addDirectory(this: *PathWatcherManager, watcher: *PathWatcher, path: PathInfo, buf: *[bun.MAX_PATH_BYTES + 1]u8) !void { + const fd = path.fd; + try this.main_watcher.addDirectory(fd, path.path, path.hash, false); + + var iter = (std.fs.IterableDir{ .dir = std.fs.Dir{ + .fd = fd, + } }).iterate(); + + while (try iter.next()) |entry| { + var parts = [2]string{ path.path, entry.name }; + var entry_path = Path.joinAbsStringBuf( + Fs.FileSystem.instance.topLevelDirWithoutTrailingSlash(), + buf, + &parts, + .auto, + ); + + buf[entry_path.len] = 0; + var entry_path_z = buf[0..entry_path.len :0]; + + var child_path = try this._fdFromAbsolutePathZ(entry_path_z); + errdefer this._decrementPathRef(entry_path_z); + try watcher.file_paths.push(bun.default_allocator, child_path.path); + + if (child_path.is_file) { + try this.main_watcher.addFile(child_path.fd, child_path.path, child_path.hash, options.Loader.file, 0, null, false); + } else { + if (watcher.recursive) { + try this.addDirectory(watcher, child_path, buf); + } + } + } + } + + fn registerWatcher(this: *PathWatcherManager, watcher: *PathWatcher) !void { + this.mutex.lock(); + defer this.mutex.unlock(); + + if (this.watcher_count == this.watchers.len) { + this.watcher_count += 1; + this.watchers.push(bun.default_allocator, watcher) catch unreachable; + } else { + var watchers = this.watchers.slice(); + for (watchers, 0..) |w, i| { + if (w == null) { + watchers[i] = watcher; + this.watcher_count += 1; + break; + } + } + } + const path = watcher.path; + if (path.is_file) { + try this.main_watcher.addFile(path.fd, path.path, path.hash, options.Loader.file, 0, null, false); + } else { + var buf: [bun.MAX_PATH_BYTES + 1]u8 = undefined; + try this.addDirectory(watcher, path, &buf); + } + } + + fn _decrementPathRef(this: *PathWatcherManager, file_path: [:0]const u8) void { + if (this.file_paths.getEntry(file_path)) |entry| { + var path = entry.value_ptr; + if (path.refs > 0) { + path.refs -= 1; + if (path.refs == 0) { + const path_ = path.path; + this.main_watcher.remove(path.hash); + _ = this.file_paths.remove(path_); + bun.default_allocator.free(path_); + } + } + } + } + + fn unregisterWatcher(this: *PathWatcherManager, watcher: *PathWatcher) void { + this.mutex.lock(); + defer this.mutex.unlock(); + + var watchers = this.watchers.slice(); + defer { + if (this.deinit_on_last_watcher and this.watcher_count == 0) { + this.deinit(); + } + } + + for (watchers, 0..) |w, i| { + if (w) |item| { + if (item == watcher) { + watchers[i] = null; + // if is the last one just pop + if (i == watchers.len - 1) { + this.watchers.len -= 1; + } + this.watcher_count -= 1; + + while (watcher.file_paths.popOrNull()) |file_path| { + this._decrementPathRef(file_path); + } + break; + } + } + } + } + + fn deinit(this: *PathWatcherManager) void { + // enable to create a new manager + default_manager_mutex.lock(); + defer default_manager_mutex.unlock(); + if (default_manager == this) { + default_manager = null; + } + + // only deinit if no watchers are registered + if (this.watcher_count > 0) { + // wait last watcher to close + this.deinit_on_last_watcher = true; + return; + } + + this.main_watcher.deinit(false); + + if (this.watcher_count > 0) { + while (this.watchers.popOrNull()) |watcher| { + if (watcher) |w| { + // unlink watcher + w.manager = null; + } + } + } + + // close all file descriptors and free paths + var it = this.file_paths.iterator(); + while (it.next()) |*entry| { + const path = entry.value_ptr.*; + std.os.close(path.fd); + bun.default_allocator.destroy(path.path); + } + + this.file_paths.deinit(); + + this.watchers.deinitWithAllocator(bun.default_allocator); + // this.mutex.deinit(); + + bun.default_allocator.destroy(this); + } +}; + +pub const PathWatcher = struct { + path: PathWatcherManager.PathInfo, + callback: Callback, + flushCallback: UpdateEndCallback, + manager: ?*PathWatcherManager, + recursive: bool, + needs_flush: bool = false, + ctx: ?*anyopaque, + // all watched file paths (including subpaths) except by path it self + file_paths: bun.BabyList([:0]const u8) = .{}, + last_change_event: ChangeEvent = .{}, + + pub const ChangeEvent = struct { + hash: PathWatcherManager.Watcher.HashType = 0, + event_type: EventType = .change, + time_stamp: i64 = 0, + }; + + pub const EventType = enum { + rename, + change, + @"error", + }; + const Callback = *const fn (ctx: ?*anyopaque, path: string, is_file: bool, event_type: EventType) void; + const UpdateEndCallback = *const fn (ctx: ?*anyopaque) void; + + pub fn init(manager: *PathWatcherManager, path: PathWatcherManager.PathInfo, recursive: bool, callback: Callback, updateEndCallback: UpdateEndCallback, ctx: ?*anyopaque) !*PathWatcher { + var this = try bun.default_allocator.create(PathWatcher); + this.* = PathWatcher{ + .path = path, + .callback = callback, + .manager = manager, + .recursive = recursive, + .flushCallback = updateEndCallback, + .ctx = ctx, + .file_paths = bun.BabyList([:0]const u8).initCapacity(bun.default_allocator, 1) catch |err| { + bun.default_allocator.destroy(this); + return err; + }, + }; + + errdefer this.deinit(); + + try manager.registerWatcher(this); + return this; + } + + pub fn emit(this: *PathWatcher, path: string, hash: PathWatcherManager.Watcher.HashType, time_stamp: i64, is_file: bool, event_type: EventType) void { + const time_diff = time_stamp - this.last_change_event.time_stamp; + // skip consecutive duplicates + if ((this.last_change_event.time_stamp == 0 or time_diff > 1) or this.last_change_event.event_type != event_type and this.last_change_event.hash != hash) { + this.last_change_event.time_stamp = time_stamp; + this.last_change_event.event_type = event_type; + this.last_change_event.hash = hash; + this.needs_flush = true; + this.callback(this.ctx, path, is_file, event_type); + } + } + + pub fn flush(this: *PathWatcher) void { + this.needs_flush = false; + this.flushCallback(this.ctx); + } + + pub fn deinit(this: *PathWatcher) void { + if (this.manager) |manager| { + manager.unregisterWatcher(this); + } + this.file_paths.deinitWithAllocator(bun.default_allocator); + + bun.default_allocator.destroy(this); + } +}; + +pub fn watch( + vm: *VirtualMachine, + path: [:0]const u8, + recursive: bool, + callback: PathWatcher.Callback, + updateEnd: PathWatcher.UpdateEndCallback, + ctx: ?*anyopaque, +) !*PathWatcher { + if (default_manager) |manager| { + const path_info = try manager._fdFromAbsolutePathZ(path); + errdefer manager._decrementPathRef(path); + return try PathWatcher.init(manager, path_info, recursive, callback, updateEnd, ctx); + } else { + default_manager_mutex.lock(); + defer default_manager_mutex.unlock(); + if (default_manager == null) { + default_manager = try PathWatcherManager.init(vm); + } + const manager = default_manager.?; + const path_info = try manager._fdFromAbsolutePathZ(path); + errdefer manager._decrementPathRef(path); + return try PathWatcher.init(manager, path_info, recursive, callback, updateEnd, ctx); + } +} |
