diff options
Diffstat (limited to 'src/bun.js/node/fs_events.zig')
-rw-r--r-- | src/bun.js/node/fs_events.zig | 609 |
1 files changed, 609 insertions, 0 deletions
diff --git a/src/bun.js/node/fs_events.zig b/src/bun.js/node/fs_events.zig new file mode 100644 index 000000000..a3fba5441 --- /dev/null +++ b/src/bun.js/node/fs_events.zig @@ -0,0 +1,609 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Environment = bun.Environment; +const Mutex = @import("../../lock.zig").Lock; +const sync = @import("../../sync.zig"); +const Semaphore = sync.Semaphore; +const UnboundedQueue = @import("../unbounded_queue.zig").UnboundedQueue; +const TaggedPointerUnion = @import("../../tagged_pointer.zig").TaggedPointerUnion; +const string = bun.string; + +pub const CFAbsoluteTime = f64; +pub const CFTimeInterval = f64; +pub const CFArrayCallBacks = anyopaque; + +pub const FSEventStreamEventFlags = c_int; +pub const OSStatus = c_int; +pub const CFIndex = c_long; + +pub const FSEventStreamCreateFlags = u32; +pub const FSEventStreamEventId = u64; + +pub const CFStringEncoding = c_uint; + +pub const CFArrayRef = ?*anyopaque; +pub const CFAllocatorRef = ?*anyopaque; +pub const CFBundleRef = ?*anyopaque; +pub const CFDictionaryRef = ?*anyopaque; +pub const CFRunLoopRef = ?*anyopaque; +pub const CFRunLoopSourceRef = ?*anyopaque; +pub const CFStringRef = ?*anyopaque; +pub const CFTypeRef = ?*anyopaque; +pub const FSEventStreamRef = ?*anyopaque; +pub const FSEventStreamCallback = *const fn (FSEventStreamRef, ?*anyopaque, usize, ?*anyopaque, *FSEventStreamEventFlags, *FSEventStreamEventId) callconv(.C) void; + +// we only care about info and perform +pub const CFRunLoopSourceContext = extern struct { + version: CFIndex = 0, + info: *anyopaque, + retain: ?*anyopaque = null, + release: ?*anyopaque = null, + copyDescription: ?*anyopaque = null, + equal: ?*anyopaque = null, + hash: ?*anyopaque = null, + schedule: ?*anyopaque = null, + cancel: ?*anyopaque = null, + perform: *const fn (?*anyopaque) callconv(.C) void, +}; + +pub const FSEventStreamContext = extern struct { + version: CFIndex = 0, + info: ?*anyopaque = null, + pad: [3]?*anyopaque = .{ null, null, null }, +}; + +pub const kCFStringEncodingUTF8: CFStringEncoding = 0x8000100; +pub const noErr: OSStatus = 0; + +pub const kFSEventStreamCreateFlagNoDefer: c_int = 2; +pub const kFSEventStreamCreateFlagFileEvents: c_int = 16; + +pub const kFSEventStreamEventFlagEventIdsWrapped: c_int = 8; +pub const kFSEventStreamEventFlagHistoryDone: c_int = 16; +pub const kFSEventStreamEventFlagItemChangeOwner: c_int = 0x4000; +pub const kFSEventStreamEventFlagItemCreated: c_int = 0x100; +pub const kFSEventStreamEventFlagItemFinderInfoMod: c_int = 0x2000; +pub const kFSEventStreamEventFlagItemInodeMetaMod: c_int = 0x400; +pub const kFSEventStreamEventFlagItemIsDir: c_int = 0x20000; +pub const kFSEventStreamEventFlagItemModified: c_int = 0x1000; +pub const kFSEventStreamEventFlagItemRemoved: c_int = 0x200; +pub const kFSEventStreamEventFlagItemRenamed: c_int = 0x800; +pub const kFSEventStreamEventFlagItemXattrMod: c_int = 0x8000; +pub const kFSEventStreamEventFlagKernelDropped: c_int = 4; +pub const kFSEventStreamEventFlagMount: c_int = 64; +pub const kFSEventStreamEventFlagRootChanged: c_int = 32; +pub const kFSEventStreamEventFlagUnmount: c_int = 128; +pub const kFSEventStreamEventFlagUserDropped: c_int = 2; + +// Lazy function call binding. +const RTLD_LAZY = 0x1; +// Symbols exported from this image (dynamic library or bundle) +// are generally hidden and only availble to dlsym() when +// directly using the handle returned by this call to dlopen(). +const RTLD_LOCAL = 0x4; + +pub const kFSEventsModified: c_int = + kFSEventStreamEventFlagItemChangeOwner | + kFSEventStreamEventFlagItemFinderInfoMod | + kFSEventStreamEventFlagItemInodeMetaMod | + kFSEventStreamEventFlagItemModified | + kFSEventStreamEventFlagItemXattrMod; + +pub const kFSEventsRenamed: c_int = + kFSEventStreamEventFlagItemCreated | + kFSEventStreamEventFlagItemRemoved | + kFSEventStreamEventFlagItemRenamed; + +pub const kFSEventsSystem: c_int = + kFSEventStreamEventFlagUserDropped | + kFSEventStreamEventFlagKernelDropped | + kFSEventStreamEventFlagEventIdsWrapped | + kFSEventStreamEventFlagHistoryDone | + kFSEventStreamEventFlagMount | + kFSEventStreamEventFlagUnmount | + kFSEventStreamEventFlagRootChanged; + +var fsevents_mutex: Mutex = Mutex.init(); +var fsevents_default_loop_mutex: Mutex = Mutex.init(); +var fsevents_default_loop: ?*FSEventsLoop = null; + +fn dlsym(handle: ?*anyopaque, comptime Type: type, comptime symbol: [:0]const u8) ?Type { + if (std.c.dlsym(handle, symbol)) |ptr| { + return bun.cast(Type, ptr); + } + return null; +} + +pub const CoreFoundation = struct { + handle: ?*anyopaque, + ArrayCreate: *fn (CFAllocatorRef, [*]?*anyopaque, CFIndex, ?*CFArrayCallBacks) callconv(.C) CFArrayRef, + Release: *fn (CFTypeRef) callconv(.C) void, + + RunLoopAddSource: *fn (CFRunLoopRef, CFRunLoopSourceRef, CFStringRef) callconv(.C) void, + RunLoopGetCurrent: *fn () callconv(.C) CFRunLoopRef, + RunLoopRemoveSource: *fn (CFRunLoopRef, CFRunLoopSourceRef, CFStringRef) callconv(.C) void, + RunLoopRun: *fn () callconv(.C) void, + RunLoopSourceCreate: *fn (CFAllocatorRef, CFIndex, *CFRunLoopSourceContext) callconv(.C) CFRunLoopSourceRef, + RunLoopSourceSignal: *fn (CFRunLoopSourceRef) callconv(.C) void, + RunLoopStop: *fn (CFRunLoopRef) callconv(.C) void, + RunLoopWakeUp: *fn (CFRunLoopRef) callconv(.C) void, + StringCreateWithFileSystemRepresentation: *fn (CFAllocatorRef, [*]const u8) callconv(.C) CFStringRef, + RunLoopDefaultMode: *CFStringRef, + + pub fn get() CoreFoundation { + if (fsevents_cf) |cf| return cf; + fsevents_mutex.lock(); + defer fsevents_mutex.unlock(); + if (fsevents_cf) |cf| return cf; + + InitLibrary(); + + return fsevents_cf.?; + } + + // We Actually never deinit it + // pub fn deinit(this: *CoreFoundation) void { + // if(this.handle) | ptr| { + // this.handle = null; + // _ = std.c.dlclose(this.handle); + // } + // } + +}; + +pub const CoreServices = struct { + handle: ?*anyopaque, + FSEventStreamCreate: *fn (CFAllocatorRef, FSEventStreamCallback, *FSEventStreamContext, CFArrayRef, FSEventStreamEventId, CFTimeInterval, FSEventStreamCreateFlags) callconv(.C) FSEventStreamRef, + FSEventStreamInvalidate: *fn (FSEventStreamRef) callconv(.C) void, + FSEventStreamRelease: *fn (FSEventStreamRef) callconv(.C) void, + FSEventStreamScheduleWithRunLoop: *fn (FSEventStreamRef, CFRunLoopRef, CFStringRef) callconv(.C) void, + FSEventStreamStart: *fn (FSEventStreamRef) callconv(.C) c_int, + FSEventStreamStop: *fn (FSEventStreamRef) callconv(.C) void, + // libuv set it to -1 so the actual value is this + kFSEventStreamEventIdSinceNow: FSEventStreamEventId = 18446744073709551615, + + pub fn get() CoreServices { + if (fsevents_cs) |cs| return cs; + fsevents_mutex.lock(); + defer fsevents_mutex.unlock(); + if (fsevents_cs) |cs| return cs; + + InitLibrary(); + + return fsevents_cs.?; + } + + // We Actually never deinit it + // pub fn deinit(this: *CoreServices) void { + // if(this.handle) | ptr| { + // this.handle = null; + // _ = std.c.dlclose(this.handle); + // } + // } + +}; + +var fsevents_cf: ?CoreFoundation = null; +var fsevents_cs: ?CoreServices = null; + +fn InitLibrary() void { + const fsevents_cf_handle = std.c.dlopen("/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation", RTLD_LAZY | RTLD_LOCAL); + if (fsevents_cf_handle == null) @panic("Cannot Load CoreFoundation"); + + fsevents_cf = CoreFoundation{ + .handle = fsevents_cf_handle, + .ArrayCreate = dlsym(fsevents_cf_handle, *fn (CFAllocatorRef, [*]?*anyopaque, CFIndex, ?*CFArrayCallBacks) callconv(.C) CFArrayRef, "CFArrayCreate") orelse @panic("Cannot Load CoreFoundation"), + .Release = dlsym(fsevents_cf_handle, *fn (CFTypeRef) callconv(.C) void, "CFRelease") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopAddSource = dlsym(fsevents_cf_handle, *fn (CFRunLoopRef, CFRunLoopSourceRef, CFStringRef) callconv(.C) void, "CFRunLoopAddSource") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopGetCurrent = dlsym(fsevents_cf_handle, *fn () callconv(.C) CFRunLoopRef, "CFRunLoopGetCurrent") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopRemoveSource = dlsym(fsevents_cf_handle, *fn (CFRunLoopRef, CFRunLoopSourceRef, CFStringRef) callconv(.C) void, "CFRunLoopRemoveSource") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopRun = dlsym(fsevents_cf_handle, *fn () callconv(.C) void, "CFRunLoopRun") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopSourceCreate = dlsym(fsevents_cf_handle, *fn (CFAllocatorRef, CFIndex, *CFRunLoopSourceContext) callconv(.C) CFRunLoopSourceRef, "CFRunLoopSourceCreate") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopSourceSignal = dlsym(fsevents_cf_handle, *fn (CFRunLoopSourceRef) callconv(.C) void, "CFRunLoopSourceSignal") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopStop = dlsym(fsevents_cf_handle, *fn (CFRunLoopRef) callconv(.C) void, "CFRunLoopStop") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopWakeUp = dlsym(fsevents_cf_handle, *fn (CFRunLoopRef) callconv(.C) void, "CFRunLoopWakeUp") orelse @panic("Cannot Load CoreFoundation"), + .StringCreateWithFileSystemRepresentation = dlsym(fsevents_cf_handle, *fn (CFAllocatorRef, [*]const u8) callconv(.C) CFStringRef, "CFStringCreateWithFileSystemRepresentation") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopDefaultMode = dlsym(fsevents_cf_handle, *CFStringRef, "kCFRunLoopDefaultMode") orelse @panic("Cannot Load CoreFoundation"), + }; + + const fsevents_cs_handle = std.c.dlopen("/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices", RTLD_LAZY | RTLD_LOCAL); + if (fsevents_cs_handle == null) @panic("Cannot Load CoreServices"); + + fsevents_cs = CoreServices{ + .handle = fsevents_cs_handle, + .FSEventStreamCreate = dlsym(fsevents_cs_handle, *fn (CFAllocatorRef, FSEventStreamCallback, *FSEventStreamContext, CFArrayRef, FSEventStreamEventId, CFTimeInterval, FSEventStreamCreateFlags) callconv(.C) FSEventStreamRef, "FSEventStreamCreate") orelse @panic("Cannot Load CoreServices"), + .FSEventStreamInvalidate = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef) callconv(.C) void, "FSEventStreamInvalidate") orelse @panic("Cannot Load CoreServices"), + .FSEventStreamRelease = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef) callconv(.C) void, "FSEventStreamRelease") orelse @panic("Cannot Load CoreServices"), + .FSEventStreamScheduleWithRunLoop = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef, CFRunLoopRef, CFStringRef) callconv(.C) void, "FSEventStreamScheduleWithRunLoop") orelse @panic("Cannot Load CoreServices"), + .FSEventStreamStart = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef) callconv(.C) c_int, "FSEventStreamStart") orelse @panic("Cannot Load CoreServices"), + .FSEventStreamStop = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef) callconv(.C) void, "FSEventStreamStop") orelse @panic("Cannot Load CoreServices"), + }; +} + +pub const FSEventsLoop = struct { + signal_source: CFRunLoopSourceRef, + mutex: Mutex, + loop: CFRunLoopRef = null, + sem: Semaphore, + thread: std.Thread = undefined, + tasks: ConcurrentTask.Queue = ConcurrentTask.Queue{}, + watchers: bun.BabyList(?*FSEventsWatcher) = .{}, + watcher_count: u32 = 0, + fsevent_stream: FSEventStreamRef = null, + paths: ?[]?*anyopaque = null, + cf_paths: CFArrayRef = null, + has_scheduled_watchers: bool = false, + + pub const Task = struct { + ctx: ?*anyopaque, + callback: *const (fn (*anyopaque) void), + + pub fn run(this: *Task) void { + var callback = this.callback; + var ctx = this.ctx; + callback(ctx.?); + } + + pub fn New(comptime Type: type, comptime Callback: anytype) type { + return struct { + pub fn init(ctx: *Type) Task { + return Task{ + .callback = wrap, + .ctx = ctx, + }; + } + + pub fn wrap(this: ?*anyopaque) void { + @call(.always_inline, Callback, .{@ptrCast(*Type, @alignCast(@alignOf(Type), this.?))}); + } + }; + } + }; + + pub const ConcurrentTask = struct { + task: Task = undefined, + next: ?*ConcurrentTask = null, + auto_delete: bool = false, + + pub const Queue = UnboundedQueue(ConcurrentTask, .next); + + pub fn from(this: *ConcurrentTask, task: Task) *ConcurrentTask { + this.* = .{ + .task = task, + .next = null, + }; + return this; + } + }; + + pub fn CFThreadLoop(this: *FSEventsLoop) void { + bun.Output.Source.configureNamedThread("CFThreadLoop"); + + const CF = CoreFoundation.get(); + + this.loop = CF.RunLoopGetCurrent(); + + CF.RunLoopAddSource(this.loop, this.signal_source, CF.RunLoopDefaultMode.*); + + this.sem.post(); + + CF.RunLoopRun(); + CF.RunLoopRemoveSource(this.loop, this.signal_source, CF.RunLoopDefaultMode.*); + + this.loop = null; + } + + // Runs in CF thread, executed after `enqueueTaskConcurrent()` + fn CFLoopCallback(arg: ?*anyopaque) callconv(.C) void { + if (arg) |self| { + const this = bun.cast(*FSEventsLoop, self); + + var concurrent = this.tasks.popBatch(); + const count = concurrent.count; + if (count == 0) + return; + + var iter = concurrent.iterator(); + while (iter.next()) |task| { + task.task.run(); + if (task.auto_delete) bun.default_allocator.destroy(task); + } + } + } + + pub fn init() !*FSEventsLoop { + const this = bun.default_allocator.create(FSEventsLoop) catch unreachable; + + const CF = CoreFoundation.get(); + + var ctx = CFRunLoopSourceContext{ + .info = this, + .perform = CFLoopCallback, + }; + + const signal_source = CF.RunLoopSourceCreate(null, 0, &ctx); + if (signal_source == null) { + return error.FailedToCreateCoreFoudationSourceLoop; + } + + var fs_loop = FSEventsLoop{ .sem = Semaphore.init(0), .mutex = Mutex.init(), .signal_source = signal_source }; + + this.* = fs_loop; + this.thread = try std.Thread.spawn(.{}, FSEventsLoop.CFThreadLoop, .{this}); + + // sync threads + this.sem.wait(); + return this; + } + + fn enqueueTaskConcurrent(this: *FSEventsLoop, task: Task) void { + const CF = CoreFoundation.get(); + var concurrent = bun.default_allocator.create(ConcurrentTask) catch unreachable; + concurrent.auto_delete = true; + this.tasks.push(concurrent.from(task)); + CF.RunLoopSourceSignal(this.signal_source); + CF.RunLoopWakeUp(this.loop); + } + + // Runs in CF thread, when there're events in FSEventStream + fn _events_cb(_: FSEventStreamRef, info: ?*anyopaque, numEvents: usize, eventPaths: ?*anyopaque, eventFlags: *FSEventStreamEventFlags, _: *FSEventStreamEventId) callconv(.C) void { + const paths_ptr = bun.cast([*][*:0]const u8, eventPaths); + const paths = paths_ptr[0..numEvents]; + var loop = bun.cast(*FSEventsLoop, info); + const event_flags = bun.cast([*]FSEventStreamEventFlags, eventFlags); + + for (loop.watchers.slice()) |watcher| { + if (watcher) |handle| { + for (paths, 0..) |path_ptr, i| { + var flags = event_flags[i]; + var path = path_ptr[0..bun.len(path_ptr)]; + // Filter out paths that are outside handle's request + if (path.len < handle.path.len or !bun.strings.startsWith(path, handle.path)) { + continue; + } + const is_file = (flags & kFSEventStreamEventFlagItemIsDir) == 0; + + // Remove common prefix, unless the watched folder is "/" + if (!(handle.path.len == 1 and handle.path[0] == '/')) { + path = path[handle.path.len..]; + + // Ignore events with path equal to directory itself + if (path.len <= 1 and is_file) { + continue; + } + if (path.len == 0) { + // Since we're using fsevents to watch the file itself, path == handle.path, and we now need to get the basename of the file back + while (path.len > 0) { + if (bun.strings.startsWithChar(path, '/')) { + path = path[1..]; + break; + } else { + path = path[1..]; + } + } + + // Created and Removed seem to be always set, but don't make sense + flags &= ~kFSEventsRenamed; + } 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 !handle.recursive)) { + continue; + } + + var is_rename = true; + + if ((flags & kFSEventsRenamed) == 0) { + if ((flags & kFSEventsModified) != 0 or is_file) { + is_rename = false; + } + } + + handle.callback(handle.ctx, path, is_file, is_rename); + } + } + } + } + + // Runs on CF Thread + pub fn _schedule(this: *FSEventsLoop) void { + this.mutex.lock(); + defer this.mutex.unlock(); + this.has_scheduled_watchers = false; + + var watchers = this.watchers.slice(); + + const CF = CoreFoundation.get(); + const CS = CoreServices.get(); + + if (this.fsevent_stream) |stream| { + // Stop emitting events + CS.FSEventStreamStop(stream); + + // Release stream + CS.FSEventStreamInvalidate(stream); + CS.FSEventStreamRelease(stream); + this.fsevent_stream = null; + } + // clean old paths + if (this.paths) |p| { + this.paths = null; + bun.default_allocator.destroy(p); + } + if (this.cf_paths) |cf| { + this.cf_paths = null; + CF.Release(cf); + } + + const paths = bun.default_allocator.alloc(?*anyopaque, this.watcher_count) catch unreachable; + var count: u32 = 0; + for (watchers) |w| { + if (w) |watcher| { + const path = CF.StringCreateWithFileSystemRepresentation(null, watcher.path.ptr); + paths[count] = path; + count += 1; + } + } + + const cf_paths = CF.ArrayCreate(null, paths.ptr, count, null); + var ctx: FSEventStreamContext = .{ + .info = this, + }; + + const latency: CFAbsoluteTime = 0.05; + // Explanation of selected flags: + // 1. NoDefer - without this flag, events that are happening continuously + // (i.e. each event is happening after time interval less than `latency`, + // counted from previous event), will be deferred and passed to callback + // once they'll either fill whole OS buffer, or when this continuous stream + // will stop (i.e. there'll be delay between events, bigger than + // `latency`). + // Specifying this flag will invoke callback after `latency` time passed + // since event. + // 2. FileEvents - fire callback for file changes too (by default it is firing + // it only for directory changes). + // + const flags: FSEventStreamCreateFlags = kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagFileEvents; + + // + // NOTE: It might sound like a good idea to remember last seen StreamEventId, + // but in reality one dir might have last StreamEventId less than, the other, + // that is being watched now. Which will cause FSEventStream API to report + // changes to files from the past. + // + const ref = CS.FSEventStreamCreate(null, _events_cb, &ctx, cf_paths, CS.kFSEventStreamEventIdSinceNow, latency, flags); + + CS.FSEventStreamScheduleWithRunLoop(ref, this.loop, CF.RunLoopDefaultMode.*); + if (CS.FSEventStreamStart(ref) == 0) { + //clean in case of failure + bun.default_allocator.destroy(paths); + CF.Release(cf_paths); + CS.FSEventStreamInvalidate(ref); + CS.FSEventStreamRelease(ref); + return; + } + this.fsevent_stream = ref; + this.paths = paths; + this.cf_paths = cf_paths; + } + + fn registerWatcher(this: *FSEventsLoop, watcher: *FSEventsWatcher) 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; + } + } + } + + if (this.has_scheduled_watchers == false) { + this.has_scheduled_watchers = true; + this.enqueueTaskConcurrent(Task.New(FSEventsLoop, _schedule).init(this)); + } + } + + fn unregisterWatcher(this: *FSEventsLoop, watcher: *FSEventsWatcher) void { + this.mutex.lock(); + defer this.mutex.unlock(); + var watchers = this.watchers.slice(); + 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; + break; + } + } + } + } + + // Runs on CF loop to close the loop + fn _stop(this: *FSEventsLoop) void { + const CF = CoreFoundation.get(); + CF.RunLoopStop(this.loop); + } + fn deinit(this: *FSEventsLoop) void { + // signal close and wait + this.enqueueTaskConcurrent(Task.New(FSEventsLoop, FSEventsLoop._stop).init(this)); + this.thread.join(); + const CF = CoreFoundation.get(); + + CF.Release(this.signal_source); + this.signal_source = null; + + this.sem.deinit(); + this.mutex.deinit(); + if (this.watcher_count > 0) { + while (this.watchers.popOrNull()) |watcher| { + if (watcher) |w| { + // unlink watcher + w.loop = null; + } + } + } + + this.watchers.deinitWithAllocator(bun.default_allocator); + + bun.default_allocator.destroy(this); + } +}; + +pub const FSEventsWatcher = struct { + path: string, + callback: Callback, + loop: ?*FSEventsLoop, + recursive: bool, + ctx: ?*anyopaque, + + const Callback = *const fn (ctx: ?*anyopaque, path: string, is_file: bool, is_rename: bool) void; + + pub fn init(loop: *FSEventsLoop, path: string, recursive: bool, callback: Callback, ctx: ?*anyopaque) *FSEventsWatcher { + var this = bun.default_allocator.create(FSEventsWatcher) catch unreachable; + this.* = FSEventsWatcher{ + .path = path, + .callback = callback, + .loop = loop, + .recursive = recursive, + .ctx = ctx, + }; + + loop.registerWatcher(this); + return this; + } + + pub fn deinit(this: *FSEventsWatcher) void { + if (this.loop) |loop| { + loop.unregisterWatcher(this); + } + bun.default_allocator.destroy(this); + } +}; + +pub fn watch(path: string, recursive: bool, callback: FSEventsWatcher.Callback, ctx: ?*anyopaque) !*FSEventsWatcher { + if (fsevents_default_loop) |loop| { + return FSEventsWatcher.init(loop, path, recursive, callback, ctx); + } else { + fsevents_default_loop_mutex.lock(); + defer fsevents_default_loop_mutex.unlock(); + if (fsevents_default_loop == null) { + fsevents_default_loop = try FSEventsLoop.init(); + } + return FSEventsWatcher.init(fsevents_default_loop.?, path, recursive, callback, ctx); + } +} |