aboutsummaryrefslogtreecommitdiff
path: root/src/bun.js/node/fs_events.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/bun.js/node/fs_events.zig')
-rw-r--r--src/bun.js/node/fs_events.zig609
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);
+ }
+}