mirror of
https://github.com/oven-sh/bun
synced 2026-02-15 21:32:05 +00:00
636 lines
25 KiB
Zig
636 lines
25 KiB
Zig
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 = bun.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 = bun.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 {
|
|
const callback = this.callback;
|
|
const 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(bun.callmod_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;
|
|
}
|
|
|
|
const 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;
|
|
|
|
const 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 {
|
|
const 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);
|
|
}
|
|
}
|