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, .{@as(*Type, @ptrCast(@alignCast(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, auto_delete: bool) *ConcurrentTask { this.* = .{ .task = task, .next = null, .auto_delete = auto_delete, }; 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; this.tasks.push(concurrent.from(task, true)); 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| { const handle_path = handle.path; 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.emit(path, is_file, if (is_rename) .rename else .change); } handle.flush(); } } } // Runs on CF Thread pub fn _schedule(this: *FSEventsLoop) void { this.mutex.lock(); defer this.mutex.unlock(); this.has_scheduled_watchers = false; const watcher_count = this.watcher_count; 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.free(p); } if (this.cf_paths) |cf| { this.cf_paths = null; CF.Release(cf); } if (watcher_count == 0) { return; } const paths = bun.default_allocator.alloc(?*anyopaque, 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.free(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(); 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, flushCallback: UpdateEndCallback, loop: ?*FSEventsLoop, recursive: bool, ctx: ?*anyopaque, pub const EventType = enum { rename, change, @"error", }; pub const Callback = *const fn (ctx: ?*anyopaque, path: string, is_file: bool, event_type: EventType) void; pub const UpdateEndCallback = *const fn (ctx: ?*anyopaque) void; pub fn init(loop: *FSEventsLoop, path: string, recursive: bool, callback: Callback, updateEnd: UpdateEndCallback, ctx: ?*anyopaque) *FSEventsWatcher { var this = bun.default_allocator.create(FSEventsWatcher) catch unreachable; this.* = FSEventsWatcher{ .path = path, .callback = callback, .flushCallback = updateEnd, .loop = loop, .recursive = recursive, .ctx = ctx, }; loop.registerWatcher(this); return this; } pub fn emit(this: *FSEventsWatcher, path: string, is_file: bool, event_type: EventType) void { this.callback(this.ctx, path, is_file, event_type); } pub fn flush(this: *FSEventsWatcher) void { this.flushCallback(this.ctx); } 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, updateEnd: FSEventsWatcher.UpdateEndCallback, ctx: ?*anyopaque) !*FSEventsWatcher { if (fsevents_default_loop) |loop| { return FSEventsWatcher.init(loop, path, recursive, callback, updateEnd, 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, updateEnd, ctx); } }