From c595275b57f5661e90ef45c629d2a77dab2ede73 Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Thu, 8 Jun 2023 16:46:34 -0300 Subject: [PATCH 01/18] initial support --- src/bun.js/bindings/JSSink.cpp | 2 +- src/bun.js/bindings/JSSink.h | 2 +- src/bun.js/bindings/JSSinkLookupTable.h | 2 +- src/bun.js/bindings/ZigGeneratedClasses.cpp | 31 + src/bun.js/bindings/generated_classes.zig | 3 + .../bindings/generated_classes_list.zig | 1 + src/bun.js/event_loop.zig | 7 + src/bun.js/javascript.zig | 7 + src/bun.js/node/node.classes.ts | 30 +- src/bun.js/node/node_fs.zig | 13 +- src/bun.js/node/node_fs_binding.zig | 2 + src/bun.js/node/node_fs_watcher.zig | 884 ++++++++++++++++++ src/bun.js/node/types.zig | 4 + src/fs.zig | 54 ++ src/http.zig | 7 +- src/js/node/fs.js | 59 +- src/js/node/fs.promises.ts | 48 + src/jsc.zig | 1 + src/watcher.zig | 25 +- 19 files changed, 1166 insertions(+), 16 deletions(-) create mode 100644 src/bun.js/node/node_fs_watcher.zig diff --git a/src/bun.js/bindings/JSSink.cpp b/src/bun.js/bindings/JSSink.cpp index 36be334ddf7b..e1ebb6db8492 100644 --- a/src/bun.js/bindings/JSSink.cpp +++ b/src/bun.js/bindings/JSSink.cpp @@ -1,6 +1,6 @@ // AUTO-GENERATED FILE. DO NOT EDIT. -// Generated by 'make generate-sink' at 2023-05-18T01:04:00.447Z +// Generated by 'make generate-sink' at 2023-06-07T22:11:33.115Z // To regenerate this file, run: // // make generate-sink diff --git a/src/bun.js/bindings/JSSink.h b/src/bun.js/bindings/JSSink.h index 5bbfab77789b..ebda36e99838 100644 --- a/src/bun.js/bindings/JSSink.h +++ b/src/bun.js/bindings/JSSink.h @@ -1,6 +1,6 @@ // AUTO-GENERATED FILE. DO NOT EDIT. -// Generated by 'make generate-sink' at 2023-05-18T01:04:00.446Z +// Generated by 'make generate-sink' at 2023-06-07T22:11:33.114Z // #pragma once diff --git a/src/bun.js/bindings/JSSinkLookupTable.h b/src/bun.js/bindings/JSSinkLookupTable.h index a4ace6dc3860..e4ed8162951f 100644 --- a/src/bun.js/bindings/JSSinkLookupTable.h +++ b/src/bun.js/bindings/JSSinkLookupTable.h @@ -1,4 +1,4 @@ -// Automatically generated from src/bun.js/bindings/JSSink.cpp using /Users/jarred/Code/bun/src/bun.js/WebKit/Source/JavaScriptCore/create_hash_table. DO NOT EDIT! +// Automatically generated from src/bun.js/bindings/JSSink.cpp using /home/cirospaciari/Repos/bun/src/bun.js/WebKit/Source/JavaScriptCore/create_hash_table. DO NOT EDIT! diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp index d51a1959aaab..f7cf1e1f7801 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.cpp +++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp @@ -7654,6 +7654,9 @@ JSC_DECLARE_HOST_FUNCTION(NodeJSFSPrototype__utimesCallback); extern "C" EncodedJSValue NodeJSFSPrototype__utimesSync(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(NodeJSFSPrototype__utimesSyncCallback); +extern "C" EncodedJSValue NodeJSFSPrototype__watch(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(NodeJSFSPrototype__watchCallback); + extern "C" EncodedJSValue NodeJSFSPrototype__write(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(NodeJSFSPrototype__writeCallback); @@ -7751,6 +7754,7 @@ static const HashTableValue JSNodeJSFSPrototypeTableValues[] = { { "unlinkSync"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__unlinkSyncCallback, 1 } }, { "utimes"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__utimesCallback, 4 } }, { "utimesSync"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__utimesSyncCallback, 3 } }, + { "watch"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__watchCallback, 3 } }, { "write"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__writeCallback, 6 } }, { "writeFile"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__writeFileCallback, 4 } }, { "writeFileSync"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__writeFileSyncCallback, 3 } }, @@ -9795,6 +9799,33 @@ JSC_DEFINE_HOST_FUNCTION(NodeJSFSPrototype__utimesSyncCallback, (JSGlobalObject return NodeJSFSPrototype__utimesSync(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(NodeJSFSPrototype__watchCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSNodeJSFS* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return NodeJSFSPrototype__watch(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(NodeJSFSPrototype__writeCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig index 0ec65a469cee..d626cac60411 100644 --- a/src/bun.js/bindings/generated_classes.zig +++ b/src/bun.js/bindings/generated_classes.zig @@ -2312,6 +2312,8 @@ pub const JSNodeJSFS = struct { @compileLog("Expected NodeJSFS.utimes to be a callback but received " ++ @typeName(@TypeOf(NodeJSFS.utimes))); if (@TypeOf(NodeJSFS.utimesSync) != CallbackType) @compileLog("Expected NodeJSFS.utimesSync to be a callback but received " ++ @typeName(@TypeOf(NodeJSFS.utimesSync))); + if (@TypeOf(NodeJSFS.watch) != CallbackType) + @compileLog("Expected NodeJSFS.watch to be a callback but received " ++ @typeName(@TypeOf(NodeJSFS.watch))); if (@TypeOf(NodeJSFS.write) != CallbackType) @compileLog("Expected NodeJSFS.write to be a callback but received " ++ @typeName(@TypeOf(NodeJSFS.write))); if (@TypeOf(NodeJSFS.writeFile) != CallbackType) @@ -2402,6 +2404,7 @@ pub const JSNodeJSFS = struct { @export(NodeJSFS.unlinkSync, .{ .name = "NodeJSFSPrototype__unlinkSync" }); @export(NodeJSFS.utimes, .{ .name = "NodeJSFSPrototype__utimes" }); @export(NodeJSFS.utimesSync, .{ .name = "NodeJSFSPrototype__utimesSync" }); + @export(NodeJSFS.watch, .{ .name = "NodeJSFSPrototype__watch" }); @export(NodeJSFS.write, .{ .name = "NodeJSFSPrototype__write" }); @export(NodeJSFS.writeFile, .{ .name = "NodeJSFSPrototype__writeFile" }); @export(NodeJSFS.writeFileSync, .{ .name = "NodeJSFSPrototype__writeFileSync" }); diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index c54965093d42..d90267337bde 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -37,4 +37,5 @@ pub const Classes = struct { pub const BuildArtifact = JSC.API.BuildArtifact; pub const BuildMessage = JSC.BuildMessage; pub const ResolveMessage = JSC.ResolveMessage; + pub const FSWatcher = JSC.Node.FSWatcher.JSObject; }; diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index 0a3459d6471c..a3ccd16adf90 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -224,6 +224,7 @@ pub const CppTask = opaque { const ThreadSafeFunction = JSC.napi.ThreadSafeFunction; const MicrotaskForDefaultGlobalObject = JSC.MicrotaskForDefaultGlobalObject; const HotReloadTask = JSC.HotReloader.HotReloadTask; +const FSWatchTask = JSC.Node.FSWatcher.FSWatchTask; const PollPendingModulesTask = JSC.ModuleLoader.AsyncModule.Queue; // const PromiseTask = JSInternalPromise.Completion.PromiseTask; const GetAddrInfoRequestTask = JSC.DNS.GetAddrInfoRequest.Task; @@ -242,6 +243,7 @@ pub const Task = TaggedPointerUnion(.{ HotReloadTask, PollPendingModulesTask, GetAddrInfoRequestTask, + FSWatchTask, // PromiseTask, // TimeoutTasklet, }); @@ -467,6 +469,11 @@ pub const EventLoop = struct { // special case: we return return 0; }, + .FSWatchTask => { + var transform_task: *FSWatchTask = task.get(FSWatchTask).?; + transform_task.*.run(); + transform_task.deinit(); + }, @field(Task.Tag, typeBaseName(@typeName(AnyTask))) => { var any: *AnyTask = task.get(AnyTask).?; any.run(); diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index bebfbeb18a00..3baa25e229e8 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -2609,6 +2609,13 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime return this.tombstones.get(key); } + pub fn onError( + _: *@This(), + err: anyerror, + ) void { + Output.prettyErrorln("Watcher crashed: {s}", .{@errorName(err)}); + } + pub fn onFileUpdate( this: *@This(), events: []watcher.WatchEvent, diff --git a/src/bun.js/node/node.classes.ts b/src/bun.js/node/node.classes.ts index f984077e4401..ce35c940af55 100644 --- a/src/bun.js/node/node.classes.ts +++ b/src/bun.js/node/node.classes.ts @@ -1,6 +1,34 @@ import { define } from "../scripts/class-definitions"; export default [ + define({ + name: "FSWatcher", + construct: false, + noConstructor: true, + finalize: true, + configurable: false, + klass: {}, + JSType: "0b11101110", + proto: { + ref: { + fn: "doRef", + length: 0, + }, + unref: { + fn: "doUnref", + length: 0, + }, + hasRef: { + fn: "hasRef", + length: 0, + }, + close: { + fn: "doClose", + length: 0, + }, + }, + values: ["listener"], + }), define({ name: "Timeout", construct: false, @@ -300,7 +328,7 @@ export default [ utimes: { fn: "utimes", length: 4 }, utimesSync: { fn: "utimesSync", length: 3 }, // TODO: - // watch: { fn: "watch", length: 3 }, + watch: { fn: "watch", length: 3 }, // watchFile: { fn: "watchFile", length: 3 }, writeFile: { fn: "writeFile", length: 4 }, writeFileSync: { fn: "writeFileSync", length: 3 }, diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 3ea0822e6da3..21a65251a6db 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -34,7 +34,6 @@ const Mode = JSC.Node.Mode; const uid_t = std.os.uid_t; const gid_t = std.os.gid_t; - /// u63 to allow one null bit const ReadPosition = u63; @@ -2313,7 +2312,7 @@ pub const Arguments = struct { }; pub const UnwatchFile = void; - pub const Watch = void; + pub const Watch = JSC.Node.FSWatcher.Arguments; pub const WatchFile = void; pub const Fsync = struct { fd: FileDescriptor, @@ -2475,7 +2474,7 @@ const Return = struct { pub const Truncate = void; pub const Unlink = void; pub const UnwatchFile = void; - pub const Watch = void; + pub const Watch = JSC.JSValue; pub const WatchFile = void; pub const Utimes = void; @@ -4181,8 +4180,12 @@ pub const NodeFS = struct { return Maybe(Return.Lutimes).todo; } - pub fn watch(_: *NodeFS, _: Arguments.Watch, comptime _: Flavor) Maybe(Return.Watch) { - return Maybe(Return.Watch).todo; + pub fn watch(_: *NodeFS, args: Arguments.Watch, comptime _: Flavor) Maybe(Return.Watch) { + const watcher = args.createFSWatcher() catch |err| { + args.global_this.throwError(err, "Failed to watch filename"); + return Maybe(Return.Watch){ .result = JSC.JSValue.jsUndefined() }; + }; + return Maybe(Return.Watch){ .result = watcher }; } pub fn createReadStream(_: *NodeFS, _: Arguments.CreateReadStream, comptime _: Flavor) Maybe(Return.CreateReadStream) { return Maybe(Return.CreateReadStream).todo; diff --git a/src/bun.js/node/node_fs_binding.zig b/src/bun.js/node/node_fs_binding.zig index 74b769bf6b2e..f178f03554c8 100644 --- a/src/bun.js/node/node_fs_binding.zig +++ b/src/bun.js/node/node_fs_binding.zig @@ -241,6 +241,8 @@ pub const NodeJSFS = struct { return JSC.Node.Stats.getConstructor(globalThis); } + pub const watch = callSync(.watch); + // Not implemented yet: const notimpl = fdatasync; pub const opendir = notimpl; diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig new file mode 100644 index 000000000000..5e911fce09be --- /dev/null +++ b/src/bun.js/node/node_fs_watcher.zig @@ -0,0 +1,884 @@ +const std = @import("std"); +const JSC = @import("root").bun.JSC; +const bun = @import("root").bun; +const Fs = @import("../../fs.zig"); +const Path = @import("../../resolver/resolve_path.zig"); +const VirtualMachine = JSC.VirtualMachine; +const EventLoop = JSC.EventLoop; +const PathLike = JSC.Node.PathLike; +const ArgumentsSlice = JSC.Node.ArgumentsSlice; +const Output = bun.Output; +const string = bun.string; +const StoredFileDescriptorType = bun.StoredFileDescriptorType; +const Environment = bun.Environment; + +pub const FSWatcher = struct { + const watcher = @import("../../watcher.zig"); + const options = @import("../../options.zig"); + pub const Watcher = watcher.NewWatcher(*FSWatcher); + + onAccept: std.ArrayHashMapUnmanaged(FSWatcher.Watcher.HashType, bun.BabyList(OnAcceptCallback), bun.ArrayIdentityContext, false) = .{}, + ctx: *VirtualMachine, + js_watcher: ?*JSObject = null, + watcher_instance: ?*FSWatcher.Watcher = null, + verbose: bool = false, + tombstones: std.StringHashMapUnmanaged(*bun.fs.FileSystem.RealFS.EntriesOption) = .{}, + file_paths: bun.BabyList(string) = .{}, + + pub fn toJS(this: *FSWatcher) JSC.JSValue { + return if (this.js_watcher) |js| js.js_this else JSC.JSValue.jsUndefined(); + } + + pub fn eventLoop(this: FSWatcher) *EventLoop { + return this.ctx.eventLoop(); + } + + pub fn enqueueTaskConcurrent(this: FSWatcher, task: *JSC.ConcurrentTask) void { + this.eventLoop().enqueueTaskConcurrent(task); + } + + pub fn deinit(this: *FSWatcher) void { + while (this.file_paths.popOrNull()) |file_path| { + bun.default_allocator.destroy(file_path); + } + this.file_paths.deinitWithAllocator(bun.default_allocator); + bun.default_allocator.destroy(this); + } + + pub const FSWatchTask = struct { + ctx: *FSWatcher, + count: u8 = 0, + + entries: [8]Entry = undefined, + concurrent_task: JSC.ConcurrentTask = undefined, + + pub const EventType = enum { + rename, + change, + @"error", + }; + + pub const Entry = struct { + file_path: string, + event_type: EventType, + needs_free: bool, + }; + + pub fn append(this: *FSWatchTask, file_path: string, event_type: EventType, needs_free: bool) void { + if (this.count == 8) { + this.enqueue(); + var ctx = this.ctx; + this.* = .{ + .ctx = ctx, + .count = 0, + }; + } + + this.entries[this.count] = .{ + .file_path = file_path, + .event_type = event_type, + .needs_free = needs_free, + }; + this.count += 1; + } + + pub fn run(this: *FSWatchTask) void { + // this runs on JS Context + if (this.ctx.js_watcher) |js_watcher| { + for (this.entries[0..this.count]) |entry| { + switch (entry.event_type) { + .rename => { + js_watcher.emit(entry.file_path, "rename"); + }, + .change => { + js_watcher.emit(entry.file_path, "change"); + }, + .@"error" => { + // file_path is the error message in this case + js_watcher.emitError(entry.file_path); + }, + } + } + } + } + + pub fn enqueue(this: *FSWatchTask) void { + if (this.count == 0) + return; + + var that = bun.default_allocator.create(FSWatchTask) catch unreachable; + + that.* = this.*; + this.count = 0; + that.concurrent_task.task = JSC.Task.init(that); + this.ctx.enqueueTaskConcurrent(&that.concurrent_task); + } + + pub fn deinit(this: *FSWatchTask) void { + while (this.count > 0) { + this.count -= 1; + if (this.entries[this.count].needs_free) { + bun.default_allocator.destroy(this.entries[this.count].file_path); + } + } + bun.default_allocator.destroy(this); + } + }; + + fn NewCallback(comptime FunctionSignature: type) type { + return union(enum) { + javascript_callback: JSC.Strong, + zig_callback: struct { + ptr: *anyopaque, + function: *const FunctionSignature, + }, + }; + } + + pub const OnAcceptCallback = NewCallback(fn ( + vm: *JSC.VirtualMachine, + specifier: []const u8, + ) void); + + fn addDirectory(ctx: *FSWatcher, fs_watcher: *FSWatcher.Watcher, fd: StoredFileDescriptorType, file_path: string, recursive: bool, buf: *[bun.MAX_PATH_BYTES]u8) !void { + var file_path_clone = bun.default_allocator.alloc(u8, file_path.len) catch unreachable; + bun.copy(u8, file_path_clone, file_path); + ctx.file_paths.push(bun.default_allocator, file_path_clone) catch unreachable; + + const entries_option = Fs.FileSystem.instance.fs.readDirectory( + file_path_clone, + null, + 0, + true, + ) catch |err| { + ctx.deinit(); + bun.default_allocator.destroy(fs_watcher); + return err; + }; + + fs_watcher.addDirectory(fd, file_path_clone, FSWatcher.Watcher.getHash(file_path), false) catch |err| { + ctx.deinit(); + bun.default_allocator.destroy(fs_watcher); + return err; + }; + if (entries_option.* == .entries) { + var entries = entries_option.entries.data.iterator(); + while (entries.next()) |entry_iter| { + var entry: *Fs.FileSystem.Entry = entry_iter.value_ptr.*; + + var parts = [2]string{ entry.dir, entry.base() }; + var entry_path = Path.joinAbsStringBuf( + Fs.FileSystem.instance.topLevelDirWithoutTrailingSlash(), + buf, + &parts, + .auto, + ); + + buf[file_path.len + 1] = 0; + buf[file_path.len] = 0; + var entry_path_z = buf[0..file_path.len :0]; + + var kind = entry.kind(&Fs.FileSystem.instance.fs, true); + + if (kind == .dir) { + if (recursive) { + if (entry.cache.fd == 0) { + const dir = bun.openDirForPath(entry_path_z) catch |err| { + ctx.deinit(); + bun.default_allocator.destroy(fs_watcher); + return err; + }; + entry.cache.fd = dir.fd; + } + addDirectory(ctx, fs_watcher, entry.cache.fd, entry_path, recursive, buf) catch |err| { + ctx.deinit(); + bun.default_allocator.destroy(fs_watcher); + return err; + }; + } + } else { + if (entry.cache.fd == 0) { + const file = bun.openFileForPath(entry_path_z) catch |err| { + ctx.deinit(); + bun.default_allocator.destroy(fs_watcher); + return err; + }; + entry.cache.fd = file.handle; + } + file_path_clone = bun.default_allocator.alloc(u8, entry_path.len) catch unreachable; + bun.copy(u8, file_path_clone, entry_path); + ctx.file_paths.push(bun.default_allocator, file_path_clone) catch unreachable; + + fs_watcher.addFile(entry.cache.fd, file_path_clone, FSWatcher.Watcher.getHash(entry_path), options.Loader.file, 0, null, false) catch |err| { + ctx.deinit(); + bun.default_allocator.destroy(fs_watcher); + return err; + }; + } + } + } + } + + fn putTombstone(this: *FSWatcher, key: []const u8, value: *bun.fs.FileSystem.RealFS.EntriesOption) void { + this.tombstones.put(bun.default_allocator, key, value) catch unreachable; + } + + fn getTombstone(this: *FSWatcher, key: []const u8) ?*bun.fs.FileSystem.RealFS.EntriesOption { + return this.tombstones.get(key); + } + + pub fn onError( + this: *FSWatcher, + err: anyerror, + ) void { + var current_task: FSWatchTask = .{ + .ctx = this, + }; + current_task.append(@errorName(err), .@"error", false); + current_task.enqueue(); + } + + pub fn onFileUpdate( + this: *FSWatcher, + events: []watcher.WatchEvent, + changed_files: []?[:0]u8, + watchlist: watcher.Watchlist, + ) void { + var slice = watchlist.slice(); + const file_paths = slice.items(.file_path); + var counts = slice.items(.count); + const kinds = slice.items(.kind); + const hashes = slice.items(.hash); + const parents = slice.items(.parent_hash); + var file_descriptors = slice.items(.fd); + var ctx = this.watcher_instance.?; + defer ctx.flushEvictions(); + defer Output.flush(); + + var bundler = if (@TypeOf(this.ctx.bundler) == *bun.Bundler) + this.ctx.bundler + else + &this.ctx.bundler; + + var fs: *Fs.FileSystem = bundler.fs; + var rfs: *Fs.FileSystem.RealFS = &fs.fs; + + var current_task: FSWatchTask = .{ + .ctx = this, + }; + defer current_task.enqueue(); + + 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]; + + // so it's consistent with the rest + // if we use .extname we might run into an issue with whether or not the "." is included. + // const path = Fs.PathName.init(file_path); + const id = hashes[event.index]; + + if (comptime Environment.isDebug) { + if (this.verbose) { + Output.prettyErrorln("[watch] {s} ({s}, {})", .{ file_path, @tagName(kind), event.op }); + } + } + + switch (kind) { + .file => { + if (event.op.delete) { + ctx.removeAtIndex( + event.index, + 0, + &.{}, + .file, + ); + } + + if (this.verbose) + Output.prettyErrorln("File changed: {s}", .{fs.relativeTo(file_path)}); + + if (event.op.write or event.op.delete or event.op.rename) { + // if is deleted we actually emit as rename like node.js + if (event.op.delete or event.op.rename) { + current_task.append(file_path, .rename, false); + } else { + current_task.append(file_path, .change, false); + } + } + }, + .directory => { + var affected_buf: [128][]const u8 = undefined; + var entries_option: ?*Fs.FileSystem.RealFS.EntriesOption = null; + + const affected = brk: { + if (comptime Environment.isMac) { + if (rfs.entries.get(file_path)) |existing| { + this.putTombstone(file_path, existing); + entries_option = existing; + } else if (this.getTombstone(file_path)) |existing| { + entries_option = existing; + } + + var affected_i: usize = 0; + + // if a file descriptor is stale, we need to close it + if (event.op.delete and entries_option != null) { + for (parents, 0..) |parent_hash, entry_id| { + if (parent_hash == id) { + const affected_path = file_paths[entry_id]; + const was_deleted = check: { + std.os.access(affected_path, std.os.F_OK) catch break :check true; + break :check false; + }; + if (!was_deleted) continue; + + affected_buf[affected_i] = affected_path[file_path.len..]; + affected_i += 1; + if (affected_i >= affected_buf.len) break; + } + } + } + + break :brk affected_buf[0..affected_i]; + } + + break :brk event.names(changed_files); + }; + + if (affected.len > 0 and !Environment.isMac) { + if (rfs.entries.get(file_path)) |existing| { + this.putTombstone(file_path, existing); + entries_option = existing; + } else if (this.getTombstone(file_path)) |existing| { + entries_option = existing; + } + } + + if (entries_option) |dir_ent| { + var last_file_hash: FSWatcher.Watcher.HashType = std.math.maxInt(FSWatcher.Watcher.HashType); + + for (affected) |changed_name_| { + const changed_name: []const u8 = if (comptime Environment.isMac) + changed_name_ + else + bun.asByteSlice(changed_name_.?); + if (changed_name.len == 0 or changed_name[0] == '~' or changed_name[0] == '.') continue; + + const loader = (bundler.options.loaders.get(Fs.PathName.init(changed_name).ext) orelse .file); + var prev_entry_id: usize = std.math.maxInt(usize); + if (loader != .file) { + var path_string: bun.PathString = undefined; + var file_hash: FSWatcher.Watcher.HashType = last_file_hash; + const abs_path: string = brk: { + if (dir_ent.entries.get(@ptrCast([]const u8, changed_name))) |file_ent| { + // reset the file descriptor + file_ent.entry.cache.fd = 0; + file_ent.entry.need_stat = true; + path_string = file_ent.entry.abs_path; + file_hash = FSWatcher.Watcher.getHash(path_string.slice()); + for (hashes, 0..) |hash, entry_id| { + if (hash == file_hash) { + if (file_descriptors[entry_id] != 0) { + if (prev_entry_id != entry_id) { + ctx.removeAtIndex( + @truncate(u16, entry_id), + 0, + &.{}, + .file, + ); + } + } + + prev_entry_id = entry_id; + break; + } + } + const path_slice = path_string.slice(); + if (path_slice.len > 0) { + if (event.op.rename or event.op.move_to) { + current_task.append(path_slice, .rename, false); + } else { + current_task.append(path_slice, .change, false); + } + break :brk path_slice; + } + } + + var file_path_without_trailing_slash = std.mem.trimRight(u8, file_path, std.fs.path.sep_str); + const file_path_without_trailing_slash_buf = bun.default_allocator.alloc(u8, file_path_without_trailing_slash.len + changed_name.len + 1) catch unreachable; + + @memcpy(file_path_without_trailing_slash_buf.ptr, file_path_without_trailing_slash.ptr, file_path_without_trailing_slash.len); + file_path_without_trailing_slash_buf[file_path_without_trailing_slash.len] = std.fs.path.sep; + + @memcpy(file_path_without_trailing_slash_buf[file_path_without_trailing_slash.len + 1 ..].ptr, changed_name.ptr, changed_name.len); + const path_slice = file_path_without_trailing_slash_buf[0 .. file_path_without_trailing_slash.len + changed_name.len + 1]; + file_hash = FSWatcher.Watcher.getHash(path_slice); + + if (event.op.rename or event.op.move_to) { + current_task.append(file_path_without_trailing_slash_buf, .rename, true); + } else { + current_task.append(file_path_without_trailing_slash_buf, .change, true); + } + + break :brk path_slice; + }; + + // skip consecutive duplicates + if (last_file_hash == file_hash) continue; + last_file_hash = file_hash; + + if (this.verbose) + Output.prettyErrorln(" File change: {s}", .{fs.relativeTo(abs_path)}); + } + } + } + + if (this.verbose) { + Output.prettyErrorln(" Dir change: {s}", .{fs.relativeTo(file_path)}); + } + }, + } + } + } + + pub const Arguments = struct { + path: PathLike, + listener: JSC.JSValue, + global_this: JSC.C.JSContextRef, + signal: ?*JSC.AbortSignal, + persistent: bool, + recursive: bool, + encoding: JSC.Node.Encoding, + verbose: bool, + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Arguments { + const vm = ctx.vm(); + const path = PathLike.fromJS(ctx, arguments, exception) orelse { + if (exception.* == null) { + JSC.throwInvalidArguments( + "filename must be a string or TypedArray", + .{}, + ctx, + exception, + ); + } + return null; + }; + + if (exception.* != null) return null; + var listener: JSC.JSValue = .zero; + var signal: ?*JSC.AbortSignal = null; + var persistent: bool = true; + var recursive: bool = false; + var encoding: JSC.Node.Encoding = .utf8; + var verbose = false; + if (arguments.nextEat()) |options_or_callable| { + + // options + if (options_or_callable.isObject()) { + if (options_or_callable.get(ctx, "persistent")) |persistent_| { + if (!persistent_.isBoolean()) { + JSC.throwInvalidArguments( + "persistent must be a boolean.", + .{}, + ctx, + exception, + ); + return null; + } + persistent = persistent_.toBoolean(); + } + + if (options_or_callable.get(ctx, "verbose")) |verbose_| { + if (!verbose_.isBoolean()) { + JSC.throwInvalidArguments( + "verbose must be a boolean.", + .{}, + ctx, + exception, + ); + return null; + } + verbose = verbose_.toBoolean(); + } + + if (options_or_callable.get(ctx, "encoding")) |encoding_| { + if (!encoding_.isString()) { + JSC.throwInvalidArguments( + "encoding must be a string.", + .{}, + ctx, + exception, + ); + return null; + } + if (JSC.Node.Encoding.fromStringValue(encoding_, ctx)) |node_encoding| { + encoding = node_encoding; + } else { + JSC.throwInvalidArguments( + "invalid encoding.", + .{}, + ctx, + exception, + ); + return null; + } + } + + if (options_or_callable.get(ctx, "recursive")) |recursive_| { + if (!recursive_.isBoolean()) { + JSC.throwInvalidArguments( + "recursive must be a boolean.", + .{}, + ctx, + exception, + ); + return null; + } + recursive = recursive_.toBoolean(); + } + + // abort signal + if (options_or_callable.get(ctx, "signal")) |signal_| { + if (JSC.AbortSignal.fromJS(signal_)) |signal_obj| { + //Keep it alive + signal_.ensureStillAlive(); + signal = signal_obj; + } else { + JSC.throwInvalidArguments( + "signal is not of type AbortSignal.", + .{}, + ctx, + exception, + ); + + return null; + } + } + + // listener + if (arguments.nextEat()) |callable| { + if (!callable.isCell() or !callable.isCallable(vm)) { + exception.* = JSC.toInvalidArguments("Expected \"listener\" callback to be a function", .{}, ctx).asObjectRef(); + return null; + } + listener = callable; + } + } else { + if (!options_or_callable.isCell() or !options_or_callable.isCallable(vm)) { + exception.* = JSC.toInvalidArguments("Expected \"listener\" callback to be a function", .{}, ctx).asObjectRef(); + return null; + } + listener = options_or_callable; + } + } + if (listener == .zero) { + exception.* = JSC.toInvalidArguments("Expected \"listener\" callback", .{}, ctx).asObjectRef(); + return null; + } + + return Arguments{ + .path = path, + .listener = listener, + .global_this = ctx, + .signal = signal, + .persistent = persistent, + .recursive = recursive, + .encoding = encoding, + .verbose = verbose, + }; + } + + pub fn createFSWatcher(this: Arguments) !JSC.JSValue { + const obj = try FSWatcher.init(this); + return obj.toJS(); + } + }; + + pub const JSObject = struct { + signal: ?*JSC.AbortSignal, + persistent: bool, + manager: *FSWatcher.Watcher, + poll_ref: JSC.PollRef = .{}, + globalThis: ?*JSC.JSGlobalObject, + js_this: JSC.JSValue, + encoding: JSC.Node.Encoding, + closed: bool, + + pub usingnamespace JSC.Codegen.JSFSWatcher; + + pub fn init(globalThis: *JSC.JSGlobalObject, manager: *FSWatcher.Watcher, signal: ?*JSC.AbortSignal, listener: JSC.JSValue, persistent: bool, encoding: JSC.Node.Encoding) !*JSObject { + var obj = try globalThis.allocator().create(JSObject); + obj.* = .{ + .signal = null, + .persistent = persistent, + .manager = manager, + .globalThis = globalThis, + .js_this = .zero, + .encoding = encoding, + .closed = false, + }; + + if (persistent) { + obj.poll_ref.ref(obj.manager.ctx.ctx); + } + + var js_this = JSObject.toJS(obj, globalThis); + JSObject.listenerSetCached(js_this, globalThis, listener); + obj.js_this = js_this; + obj.js_this.protect(); + + if (signal) |s| { + // already aborted? + if (s.aborted()) { + const err = s.abortReason(); + obj.emitAbort(err); + } else { + // watch for abortion + obj.signal = s.ref().listen(JSObject, obj, JSObject.emitAbort); + } + } + return obj; + } + + pub fn emitAbort(this: *JSObject, err: JSC.JSValue) void { + if (this.closed) return; + defer this.close(false); + + err.ensureStillAlive(); + + if (this.globalThis) |globalThis| { + if (this.js_this != .zero) { + if (JSObject.listenerGetCached(this.js_this)) |listener| { + var args = [_]JSC.JSValue{ + JSC.ZigString.static("error").toValue(globalThis), + if (err.isEmptyOrUndefinedOrNull()) JSC.WebCore.AbortSignal.createAbortError(JSC.ZigString.static("The user aborted a request"), &JSC.ZigString.Empty, globalThis) else err, + }; + _ = listener.callWithGlobalThis( + globalThis, + &args, + ); + } + } + } + } + pub fn emitError(this: *JSObject, err: string) void { + if (this.closed) return; + defer this.close(false); + + if (this.globalThis) |globalThis| { + if (this.js_this != .zero) { + if (JSObject.listenerGetCached(this.js_this)) |listener| { + var args = [_]JSC.JSValue{ + JSC.ZigString.static("error").toValue(globalThis), + JSC.ZigString.fromUTF8(err).toErrorInstance(globalThis), + }; + _ = listener.callWithGlobalThis( + globalThis, + &args, + ); + } + } + } + } + + pub fn emit(this: *JSObject, file_name: string, comptime eventType: string) void { + if (this.globalThis) |globalThis| { + if (this.js_this != .zero) { + if (JSObject.listenerGetCached(this.js_this)) |listener| { + var filename: JSC.JSValue = JSC.JSValue.jsUndefined(); + if (file_name.len > 0) { + if (this.encoding == .buffer) + filename = JSC.ArrayBuffer.createBuffer(globalThis, file_name) + else if (this.encoding == .utf8) { + filename = JSC.ZigString.fromUTF8(file_name).toValueGC(globalThis); + } else { + // TODO: this is correct? + filename = JSC.ZigString.fromUTF8(JSC.ZigString.fromUTF8(file_name).encode(this.encoding)).toValue(globalThis); + } + } + var args = [_]JSC.JSValue{ + JSC.ZigString.static(eventType).toValue(globalThis), + filename, + }; + _ = listener.callWithGlobalThis( + globalThis, + &args, + ); + } + } + } + } + + pub fn ref(this: *JSObject) void { + if (this.closed) return; + + if (!this.persistent) { + this.persistent = true; + this.poll_ref.ref(this.manager.ctx.ctx); + } + } + + pub fn doRef(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + this.ref(); + return JSC.JSValue.jsUndefined(); + } + + pub fn unref(this: *JSObject) void { + if (this.persistent) { + this.persistent = false; + this.poll_ref.unref(this.manager.ctx.ctx); + } + } + + pub fn doUnref(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + this.unref(); + return JSC.JSValue.jsUndefined(); + } + + pub fn hasRef(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + return JSC.JSValue.jsBoolean(this.persistent); + } + + pub fn close( + this: *JSObject, + emitEvent: bool, + ) void { + if (!this.closed) { + if (this.signal) |signal| { + this.signal = null; + signal.detach(this); + } + this.closed = true; + this.manager.stop(); + this.js_this.unprotect(); + this.unref(); + if (emitEvent) { + this.emit("", "close"); + } + } + } + + pub fn doClose(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + this.close(true); + return JSC.JSValue.jsUndefined(); + } + + pub fn finalize(this: *JSObject) callconv(.C) void { + this.manager.ctx.js_watcher = null; + this.js_this = .zero; + this.globalThis = null; + this.manager.stop(); + this.manager.thread.join(); + + if (this.signal) |signal| { + this.signal = null; + signal.detach(this); + } + + if (this.persistent) { + this.persistent = false; + this.poll_ref.unref(this.manager.ctx.ctx); + } + bun.default_allocator.destroy(this.manager.ctx); + bun.default_allocator.destroy(this.manager); + bun.default_allocator.destroy(this); + } + }; + + pub fn init(args: Arguments) !*FSWatcher { + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + + var parts = [_]string{ + args.path.slice(), + }; + + var file_path = Path.joinAbsStringBuf( + Fs.FileSystem.instance.top_level_dir, + &buf, + &parts, + .auto, + ); + + buf[file_path.len + 1] = 0; + buf[file_path.len] = 0; + var file_path_z = buf[0..file_path.len :0]; + + var fs_type = try Fs.FileSystem.instance.fs.kindFromAbsolute(file_path_z, 0, true); + + var ctx = try bun.default_allocator.create(FSWatcher); + const vm = args.global_this.bunVM(); + ctx.* = .{ + .ctx = vm, + .verbose = args.verbose, + .file_paths = bun.BabyList(string).initCapacity(bun.default_allocator, 1) catch |err| { + ctx.deinit(); + return err; + }, + }; + + var fs_watcher = FSWatcher.Watcher.init( + ctx, + vm.bundler.fs, + bun.default_allocator, + ) catch |err| { + ctx.deinit(); + return err; + }; + + ctx.watcher_instance = fs_watcher; + + if (fs_type.kind == .file) { + if (fs_type.fd == 0) { + const file = bun.openFileForPath(file_path_z) catch |err| { + ctx.deinit(); + bun.default_allocator.destroy(fs_watcher); + return err; + }; + fs_type.fd = file.handle; + } + + var file_path_clone = bun.default_allocator.alloc(u8, file_path.len) catch unreachable; + bun.copy(u8, file_path_clone, file_path); + ctx.file_paths.push(bun.default_allocator, file_path_clone) catch unreachable; + fs_watcher.addFile(fs_type.fd, file_path_clone, FSWatcher.Watcher.getHash(file_path), options.Loader.file, 0, null, false) catch |err| { + ctx.deinit(); + bun.default_allocator.destroy(fs_watcher); + return err; + }; + } else if (fs_type.kind == .dir) { + if (fs_type.fd == 0) { + const dir = bun.openDirForPath(file_path_z) catch |err| { + ctx.deinit(); + bun.default_allocator.destroy(fs_watcher); + return err; + }; + fs_type.fd = dir.fd; + } + addDirectory(ctx, fs_watcher, fs_type.fd, file_path, args.recursive, &buf) catch |err| { + ctx.deinit(); + bun.default_allocator.destroy(fs_watcher); + return err; + }; + } else { + ctx.deinit(); + bun.default_allocator.destroy(fs_watcher); + return error.UnableToFindDirectory; + } + + fs_watcher.start() catch |err| { + ctx.deinit(); + bun.default_allocator.destroy(fs_watcher); + return err; + }; + + ctx.js_watcher = JSObject.init(args.global_this, fs_watcher, args.signal, args.listener, args.persistent, args.encoding) catch |err| { + ctx.deinit(); + bun.default_allocator.destroy(fs_watcher); + return err; + }; + + return ctx; + } +}; diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index e2de357060fd..659ac31bbc47 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -93,6 +93,10 @@ pub fn Maybe(comptime ResultType: type) type { return JSC.JSValue.jsUndefined(); } + if (comptime ReturnType == JSC.JSValue) { + return r; + } + if (comptime ReturnType == JSC.ArrayBuffer) { return r.toJS(globalThis, null); } diff --git a/src/fs.zig b/src/fs.zig index e87d931df0a7..98174fac3a15 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -1109,6 +1109,60 @@ pub const FileSystem = struct { return File{ .path = Path.init(path), .contents = file_contents }; } + pub fn kindFromAbsolute( + fs: *RealFS, + absolute_path: [:0]const u8, + existing_fd: StoredFileDescriptorType, + store_fd: bool, + ) !Entry.Cache { + var outpath: [bun.MAX_PATH_BYTES]u8 = undefined; + + var stat = try C.lstat_absolute(absolute_path); + const is_symlink = stat.kind == std.fs.File.Kind.SymLink; + var _kind = stat.kind; + var cache = Entry.Cache{ + .kind = Entry.Kind.file, + .symlink = PathString.empty, + }; + var symlink: []const u8 = ""; + + if (is_symlink) { + var file = try if (existing_fd != 0) + std.fs.File{ .handle = existing_fd } + else if (store_fd) + std.fs.openFileAbsoluteZ(absolute_path, .{ .mode = .read_only }) + else + bun.openFileForPath(absolute_path); + setMaxFd(file.handle); + + defer { + if ((!store_fd or fs.needToCloseFiles()) and existing_fd == 0) { + file.close(); + } else if (comptime FeatureFlags.store_file_descriptors) { + cache.fd = file.handle; + } + } + const _stat = try file.stat(); + + symlink = try bun.getFdPath(file.handle, &outpath); + + _kind = _stat.kind; + } + + std.debug.assert(_kind != .SymLink); + + if (_kind == .Directory) { + cache.kind = .dir; + } else { + cache.kind = .file; + } + if (symlink.len > 0) { + cache.symlink = PathString.init(try FilenameStore.instance.append([]const u8, symlink)); + } + + return cache; + } + pub fn kind( fs: *RealFS, _dir: string, diff --git a/src/http.zig b/src/http.zig index 827bfa6de0fa..80718db2fa42 100644 --- a/src/http.zig +++ b/src/http.zig @@ -3238,7 +3238,12 @@ pub const Server = struct { threadlocal var filechange_buf: [32]u8 = undefined; threadlocal var filechange_buf_hinted: [32]u8 = undefined; - + pub fn onError( + _: *@This(), + err: anyerror, + ) void { + Output.prettyErrorln("Watcher crashed: {s}", .{@errorName(err)}); + } pub fn onFileUpdate( ctx: *Server, events: []watcher.WatchEvent, diff --git a/src/js/node/fs.js b/src/js/node/fs.js index f117020dd47f..8631c8bc2da4 100644 --- a/src/js/node/fs.js +++ b/src/js/node/fs.js @@ -1,3 +1,5 @@ +import { EventEmitter } from "stream"; + // Hardcoded module "node:fs" var { direct, isPromise, isCallable } = import.meta.primordials; var promises = import.meta.require("node:fs/promises"); @@ -7,6 +9,57 @@ var NativeReadable = _getNativeReadableStreamPrototype(2, Readable); // 2 means var fs = Bun.fs(); var debug = process.env.DEBUG ? console.log : () => {}; + +class FSWatcher extends EventEmitter { + #watcher; + #listener; + constructor(path, options, listener) { + super(); + if (path instanceof URL) { + throw new TypeError("Watch URLs are not supported yet"); + } else if (Buffer.isBuffer(path)) { + path = path.toString(); + } else if (typeof path !== "string") { + throw new TypeError("Expected path to be a string or Buffer"); + } + + if (typeof options === "function") { + listener = options; + options = {}; + } else if (typeof options === "string") { + options = { encoding: options }; + } + + if (typeof listener !== "function") { + listener = () => {}; + } + + this.#listener = listener; + this.#watcher = fs.watch(path, options, this.#onEvent.bind(this)); + } + + #onEvent(eventType, filenameOrError) { + if (eventType === "error" || eventType === "close") { + this.emit(eventType, filenameOrError); + } else { + this.emit("change", eventType, filenameOrError); + this.#listener(eventType, filenameOrError); + } + } + + close() { + this.#watcher?.close(); + this.#watcher = null; + } + + ref() { + this.#watcher?.ref(); + } + + unref() { + this.#watcher?.unref(); + } +} export var access = function access(...args) { callbackify(fs.accessSync, args); }, @@ -153,6 +206,9 @@ export var access = function access(...args) { rmdirSync = fs.rmdirSync.bind(fs), Dirent = fs.Dirent, Stats = fs.Stats, + watch = function watch(path, options, listener) { + return new FSWatcher(path, options, listener); + }, promises = import.meta.require("node:fs/promises"); function callbackify(fsFunction, args) { @@ -1002,7 +1058,8 @@ export default { writeSync, WriteStream, ReadStream, - + watch, + FSWatcher, [Symbol.for("::bunternal::")]: { ReadStreamClass, WriteStreamClass, diff --git a/src/js/node/fs.promises.ts b/src/js/node/fs.promises.ts index de802928b8da..444c9bd6f9fb 100644 --- a/src/js/node/fs.promises.ts +++ b/src/js/node/fs.promises.ts @@ -1,4 +1,5 @@ // Hardcoded module "node:fs/promises" + // Note: `constants` is injected into the top of this file declare var constants: typeof import("node:fs/promises").constants; @@ -38,6 +39,52 @@ var promisify = { }, }[notrace]; +export function watch( + filename: string | Buffer | URL, + options: { encoding?: BufferEncoding; persistent?: boolean; recursive?: boolean; signal?: AbortSignal } = {}, +) { + type Event = { + eventType: string; + filename: string | Buffer | undefined; + }; + const events: Array = []; + if (filename instanceof URL) { + throw new TypeError("Watch URLs are not supported yet"); + } else if (Buffer.isBuffer(filename)) { + filename = filename.toString(); + } else if (typeof filename !== "string") { + throw new TypeError("Expected path to be a string or Buffer"); + } + let nextEventResolve: Function | null = null; + fs.watch(filename, options || {}, (eventType: string, filename: string | Buffer | undefined) => { + events.push({ eventType, filename }); + if (nextEventResolve) { + const resolve = nextEventResolve; + nextEventResolve = null; + resolve(); + } + }); + return { + async *[Symbol.asyncIterator]() { + let closed = false; + while (!closed) { + while (events.length) { + let event = events.shift() as Event; + if (event.eventType === "close") { + closed = true; + break; + } + if (event.eventType === "error") { + closed = true; + throw event.filename; + } + yield event; + } + await new Promise((resolve: Function) => (nextEventResolve = resolve)); + } + }, + }; +} export var access = promisify(fs.accessSync), appendFile = promisify(fs.appendFileSync), close = promisify(fs.closeSync), @@ -112,6 +159,7 @@ export default { lutimes, rm, rmdir, + watch, constants, [Symbol.for("CommonJS")]: 0, }; diff --git a/src/jsc.zig b/src/jsc.zig index 67cf3f05c7b1..ca31d5f1a1d2 100644 --- a/src/jsc.zig +++ b/src/jsc.zig @@ -50,6 +50,7 @@ pub const FFI = @import("./bun.js/api/ffi.zig").FFI; pub const Node = struct { pub usingnamespace @import("./bun.js/node/types.zig"); pub usingnamespace @import("./bun.js/node/node_fs.zig"); + pub usingnamespace @import("./bun.js/node/node_fs_watcher.zig"); pub usingnamespace @import("./bun.js/node/node_fs_binding.zig"); pub usingnamespace @import("./bun.js/node/node_os.zig"); pub const Syscall = @import("./bun.js/node/syscall.zig"); diff --git a/src/watcher.zig b/src/watcher.zig index 155c0b4733ea..07ceccecfec0 100644 --- a/src/watcher.zig +++ b/src/watcher.zig @@ -108,6 +108,10 @@ pub const INotify = struct { std.os.inotify_rm_watch(inotify_fd, wd); } + pub fn isRunning() bool { + return loaded_inotify; + } + var coalesce_interval: isize = 100_000; pub fn init() !void { std.debug.assert(!loaded_inotify); @@ -229,6 +233,10 @@ const DarwinWatcher = struct { if (fd == 0) return error.KQueueError; } + pub fn isRunning() bool { + return fd != 0; + } + pub fn stop() void { if (fd != 0) { std.os.close(fd); @@ -361,6 +369,7 @@ pub fn NewWatcher(comptime ContextType: type) type { watchloop_handle: ?std.Thread.Id = null, cwd: string, thread: std.Thread = undefined, + running: bool = true, pub const HashType = u32; @@ -372,7 +381,9 @@ pub fn NewWatcher(comptime ContextType: type) type { pub fn init(ctx: ContextType, fs: *Fs.FileSystem, allocator: std.mem.Allocator) !*Watcher { var watcher = try allocator.create(Watcher); - try PlatformWatcher.init(); + if (!PlatformWatcher.isRunning()) { + try PlatformWatcher.init(); + } watcher.* = Watcher{ .fs = fs, @@ -393,6 +404,10 @@ pub fn NewWatcher(comptime ContextType: type) type { this.thread = try std.Thread.spawn(.{}, Watcher.watchLoop, .{this}); } + pub fn stop(this: *Watcher) void { + this.running = false; + } + // This must only be called from the watcher thread pub fn watchLoop(this: *Watcher) !void { this.watchloop_handle = std.Thread.getCurrentId(); @@ -402,10 +417,10 @@ pub fn NewWatcher(comptime ContextType: type) type { if (FeatureFlags.verbose_watcher) Output.prettyln("Watcher started", .{}); this._watchLoop() catch |err| { - Output.prettyErrorln("Watcher crashed: {s}", .{@errorName(err)}); - this.watchloop_handle = null; PlatformWatcher.stop(); + + this.ctx.onError(err); return; }; } @@ -475,7 +490,7 @@ pub fn NewWatcher(comptime ContextType: type) type { var changelist_array: [128]KEvent = std.mem.zeroes([128]KEvent); var changelist = &changelist_array; - while (true) { + while (this.running) { defer Output.flush(); var count_ = std.os.system.kevent( @@ -534,7 +549,7 @@ pub fn NewWatcher(comptime ContextType: type) type { this.ctx.onFileUpdate(watchevents, this.changed_filepaths[0..watchevents.len], this.watchlist); } } else if (Environment.isLinux) { - restart: while (true) { + restart: while (this.running) { defer Output.flush(); var events = try INotify.read(); From a8aa9c1ec1d7dff01f8a043a44819e89726051f1 Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Thu, 8 Jun 2023 17:03:24 -0300 Subject: [PATCH 02/18] add types --- packages/bun-types/fs.d.ts | 63 +++++++++++++++++++++++++++++ packages/bun-types/fs/promises.d.ts | 58 ++++++++++++++++++++++++++ src/js/node/fs.promises.ts | 3 ++ 3 files changed, 124 insertions(+) diff --git a/packages/bun-types/fs.d.ts b/packages/bun-types/fs.d.ts index 14c5c1d1d6c8..ce410a5cdcdb 100644 --- a/packages/bun-types/fs.d.ts +++ b/packages/bun-types/fs.d.ts @@ -19,6 +19,7 @@ */ declare module "fs" { import * as stream from "stream"; + import type EventEmitter from "events"; import type { SystemError, ArrayBufferView } from "bun"; interface ObjectEncodingOptions { encoding?: BufferEncoding | null | undefined; @@ -3929,6 +3930,68 @@ declare module "fs" { */ recursive?: boolean; } + + export interface FSWatcher extends EventEmitter { + /** + * Stop watching for changes on the given `fs.FSWatcher`. Once stopped, the `fs.FSWatcher` object is no longer usable. + * @since v0.6.8 + */ + close(): void; + /** + * events.EventEmitter + * 1. change + * 2. error + */ + addListener(event: string, listener: (...args: any[]) => void): this; + addListener(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this; + addListener(event: 'error', listener: (error: Error) => void): this; + addListener(event: 'close', listener: () => void): this; + on(event: string, listener: (...args: any[]) => void): this; + on(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this; + on(event: 'error', listener: (error: Error) => void): this; + on(event: 'close', listener: () => void): this; + once(event: string, listener: (...args: any[]) => void): this; + once(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this; + once(event: 'error', listener: (error: Error) => void): this; + once(event: 'close', listener: () => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this; + prependListener(event: 'error', listener: (error: Error) => void): this; + prependListener(event: 'close', listener: () => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this; + prependOnceListener(event: 'error', listener: (error: Error) => void): this; + prependOnceListener(event: 'close', listener: () => void): this; + } + /** + * Watch for changes on `filename`, where `filename` is either a file or a + * directory. + * + * The second argument is optional. If `options` is provided as a string, it + * specifies the `encoding`. Otherwise `options` should be passed as an object. + * + * The listener callback gets two arguments `(eventType, filename)`. `eventType`is either `'rename'` or `'change'`, and `filename` is the name of the file + * which triggered the event. + * + * On most platforms, `'rename'` is emitted whenever a filename appears or + * disappears in the directory. + * + * The listener callback is attached to the `'change'` event fired by `fs.FSWatcher`, but it is not the same thing as the `'change'` value of`eventType`. + * + * If a `signal` is passed, aborting the corresponding AbortController will close + * the returned `fs.FSWatcher`. + * @since v0.5.10 + * @param listener + */ + export function watch( + filename: PathLike, + options: + | (WatchOptions & { + encoding: 'buffer'; + }) + | 'buffer', + listener?: WatchListener + ): FSWatcher; } declare module "node:fs" { diff --git a/packages/bun-types/fs/promises.d.ts b/packages/bun-types/fs/promises.d.ts index 0d71464b9e28..2b908fceb298 100644 --- a/packages/bun-types/fs/promises.d.ts +++ b/packages/bun-types/fs/promises.d.ts @@ -26,6 +26,7 @@ declare module "fs/promises" { Abortable, RmOptions, RmDirOptions, + WatchOptions, } from "node:fs"; const constants: typeof import("node:fs")["constants"]; @@ -709,6 +710,63 @@ declare module "fs/promises" { * To remove a directory recursively, use `fs.promises.rm()` instead, with the `recursive` option set to `true`. */ function rmdir(path: PathLike, options?: RmDirOptions): Promise; + + /** + * Returns an async iterator that watches for changes on `filename`, where `filename`is either a file or a directory. + * + * ```js + * const { watch } = require('node:fs/promises'); + * + * const ac = new AbortController(); + * const { signal } = ac; + * setTimeout(() => ac.abort(), 10000); + * + * (async () => { + * try { + * const watcher = watch(__filename, { signal }); + * for await (const event of watcher) + * console.log(event); + * } catch (err) { + * if (err.name === 'AbortError') + * return; + * throw err; + * } + * })(); + * ``` + * + * On most platforms, `'rename'` is emitted whenever a filename appears or + * disappears in the directory. + * + * All the `caveats` for `fs.watch()` also apply to `fsPromises.watch()`. + * @since v0.6.8 + * @return of objects with the properties: + */ + function watch( + filename: PathLike, + options: + | (WatchOptions & { + encoding: 'buffer'; + }) + | 'buffer' + ): AsyncIterable>; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `persistent` is not supplied, the default of `true` is used. + * If `recursive` is not supplied, the default of `false` is used. + */ + function watch(filename: PathLike, options?: WatchOptions | BufferEncoding): AsyncIterable>; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `persistent` is not supplied, the default of `true` is used. + * If `recursive` is not supplied, the default of `false` is used. + */ + function watch(filename: PathLike, options: WatchOptions | string): AsyncIterable> | AsyncIterable>; } declare module "node:fs/promises" { diff --git a/src/js/node/fs.promises.ts b/src/js/node/fs.promises.ts index 444c9bd6f9fb..7df446ccb508 100644 --- a/src/js/node/fs.promises.ts +++ b/src/js/node/fs.promises.ts @@ -56,6 +56,9 @@ export function watch( throw new TypeError("Expected path to be a string or Buffer"); } let nextEventResolve: Function | null = null; + if (typeof options === "string") { + options = { encoding: options }; + } fs.watch(filename, options || {}, (eventType: string, filename: string | Buffer | undefined) => { events.push({ eventType, filename }); if (nextEventResolve) { From ffbdafdeaeb7b6f6ddcaa6e632158271df206b2e Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Thu, 8 Jun 2023 17:11:55 -0300 Subject: [PATCH 03/18] fix comment --- packages/bun-types/fs.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bun-types/fs.d.ts b/packages/bun-types/fs.d.ts index ce410a5cdcdb..3102556d29ad 100644 --- a/packages/bun-types/fs.d.ts +++ b/packages/bun-types/fs.d.ts @@ -3980,7 +3980,7 @@ declare module "fs" { * * If a `signal` is passed, aborting the corresponding AbortController will close * the returned `fs.FSWatcher`. - * @since v0.5.10 + * @since v0.6.8 * @param listener */ export function watch( From f45548c307d16249353b753374a71712f9a50aba Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Thu, 8 Jun 2023 18:05:44 -0300 Subject: [PATCH 04/18] fix types --- packages/bun-types/fs.d.ts | 50 ++++++++++++++++++---- src/js/private.d.ts | 86 +++++++++++++++++++++++++++++++++++++- 2 files changed, 127 insertions(+), 9 deletions(-) diff --git a/packages/bun-types/fs.d.ts b/packages/bun-types/fs.d.ts index 3102556d29ad..5dfb2c7f25c2 100644 --- a/packages/bun-types/fs.d.ts +++ b/packages/bun-types/fs.d.ts @@ -3937,6 +3937,17 @@ declare module "fs" { * @since v0.6.8 */ close(): void; + + /** + * When called, requests that the Node.js event loop not exit so long as the is active. Calling watcher.ref() multiple times will have no effect. + */ + ref(): void; + + /** + * When called, the active object will not require the Node.js event loop to remain active. If there is no other activity keeping the event loop running, the process may exit before the object's callback is invoked. Calling watcher.unref() multiple times will have no effect. + */ + unref(): void; + /** * events.EventEmitter * 1. change @@ -3983,15 +3994,38 @@ declare module "fs" { * @since v0.6.8 * @param listener */ - export function watch( - filename: PathLike, - options: - | (WatchOptions & { - encoding: 'buffer'; - }) - | 'buffer', - listener?: WatchListener + export function watch( + filename: PathLike, + options: + | (WatchOptions & { + encoding: 'buffer'; + }) + | 'buffer', + listener?: WatchListener ): FSWatcher; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `persistent` is not supplied, the default of `true` is used. + * If `recursive` is not supplied, the default of `false` is used. + */ + export function watch(filename: PathLike, options?: WatchOptions | BufferEncoding | null, listener?: WatchListener): FSWatcher; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `persistent` is not supplied, the default of `true` is used. + * If `recursive` is not supplied, the default of `false` is used. + */ + export function watch(filename: PathLike, options: WatchOptions | string, listener?: WatchListener): FSWatcher; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + */ + export function watch(filename: PathLike, listener?: WatchListener): FSWatcher; } declare module "node:fs" { diff --git a/src/js/private.d.ts b/src/js/private.d.ts index b6ed6480144c..b689c208ef23 100644 --- a/src/js/private.d.ts +++ b/src/js/private.d.ts @@ -6,11 +6,95 @@ */ declare function $bundleError(error: string); +type BunFSWatchOptions = { encoding?: BufferEncoding; persistent?: boolean; recursive?: boolean; signal?: AbortSignal }; + +type BunWatchEventType = "rename" | "change" | "error" | "close"; +type BunWatchListener = (event: WatchEventType, filename: T | Error | undefined) => void; + +interface BunFSWatcher { + /** + * Stop watching for changes on the given `BunFSWatcher`. Once stopped, the `BunFSWatcher` object is no longer usable. + * @since v0.6.8 + */ + close(): void; + + /** + * When called, requests that the Node.js event loop not exit so long as the is active. Calling watcher.ref() multiple times will have no effect. + */ + ref(): void; + + /** + * When called, the active object will not require the Node.js event loop to remain active. If there is no other activity keeping the event loop running, the process may exit before the object's callback is invoked. Calling watcher.unref() multiple times will have no effect. + */ + unref(): void; +} +type BunFS = Omit & { + /** + * Watch for changes on `filename`, where `filename` is either a file or a + * directory. + * + * The second argument is optional. If `options` is provided as a string, it + * specifies the `encoding`. Otherwise `options` should be passed as an object. + * + * The listener callback gets two arguments `(eventType, filename)`. `eventType`is either `'rename'`, `'change', 'error' or 'close'`, and `filename` is the name of the file + * which triggered the event, the error when `eventType` is 'error' or undefined when eventType is 'close'. + * + * On most platforms, `'rename'` is emitted whenever a filename appears or + * disappears in the directory. + * + * + * If a `signal` is passed, aborting the corresponding AbortController will close + * the returned `BunFSWatcher`. + * @since v0.6.8 + * @param listener + */ + watch( + filename: string, + options: + | (WatchOptions & { + encoding: "buffer"; + }) + | "buffer", + listener?: BunWatchListener, + ): BunFSWatcher; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `BunFSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `persistent` is not supplied, the default of `true` is used. + * If `recursive` is not supplied, the default of `false` is used. + */ + watch( + filename: string, + options?: WatchOptions | BufferEncoding | null, + listener?: BunWatchListener, + ): BunFSWatcher; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `BunFSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `persistent` is not supplied, the default of `true` is used. + * If `recursive` is not supplied, the default of `false` is used. + */ + watch( + filename: string, + options: BunWatchListener | string, + listener?: BunWatchListener, + ): BunFSWatcher; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `BunFSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + */ + watch(filename: string, listener?: BunWatchListener): BunFSWatcher; +}; + declare module "bun" { var TOML: { parse(contents: string): any; }; - function fs(): typeof import("node:fs"); + function fs(): BunFS; function _Os(): typeof import("node:os"); function jest(): typeof import("bun:test"); var main: string; From 10e5375d0bdb351b8deaa1c265cc5f3a3996cebd Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Thu, 15 Jun 2023 16:50:43 -0300 Subject: [PATCH 05/18] bigfix up --- src/bun.js/bindings/JSSink.cpp | 2 +- src/bun.js/bindings/JSSink.h | 2 +- ...igGeneratedClasses+DOMClientIsoSubspaces.h | 1 + .../ZigGeneratedClasses+DOMIsoSubspaces.h | 1 + .../ZigGeneratedClasses+lazyStructureHeader.h | 6 + .../ZigGeneratedClasses+lazyStructureImpl.h | 7 + src/bun.js/bindings/ZigGeneratedClasses.cpp | 291 +++++++++++++++++ src/bun.js/bindings/ZigGeneratedClasses.h | 56 ++++ src/bun.js/bindings/generated_classes.zig | 91 ++++++ src/bun.js/node/node_fs_watcher.zig | 308 +++++++----------- src/watcher.zig | 32 +- 11 files changed, 603 insertions(+), 194 deletions(-) diff --git a/src/bun.js/bindings/JSSink.cpp b/src/bun.js/bindings/JSSink.cpp index e1ebb6db8492..4acf01ff7833 100644 --- a/src/bun.js/bindings/JSSink.cpp +++ b/src/bun.js/bindings/JSSink.cpp @@ -1,6 +1,6 @@ // AUTO-GENERATED FILE. DO NOT EDIT. -// Generated by 'make generate-sink' at 2023-06-07T22:11:33.115Z +// Generated by 'make generate-sink' at 2023-06-14T21:38:04.394Z // To regenerate this file, run: // // make generate-sink diff --git a/src/bun.js/bindings/JSSink.h b/src/bun.js/bindings/JSSink.h index ebda36e99838..37c458e9b7b3 100644 --- a/src/bun.js/bindings/JSSink.h +++ b/src/bun.js/bindings/JSSink.h @@ -1,6 +1,6 @@ // AUTO-GENERATED FILE. DO NOT EDIT. -// Generated by 'make generate-sink' at 2023-06-07T22:11:33.114Z +// Generated by 'make generate-sink' at 2023-06-14T21:38:04.394Z // #pragma once diff --git a/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h b/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h index b16febcdbea5..f0d491c0b69f 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h @@ -8,6 +8,7 @@ std::unique_ptr m_clientSubspaceForExpectConstructor;std: std::unique_ptr m_clientSubspaceForExpectAnything; std::unique_ptr m_clientSubspaceForExpectStringContaining; std::unique_ptr m_clientSubspaceForExpectStringMatching; +std::unique_ptr m_clientSubspaceForFSWatcher; std::unique_ptr m_clientSubspaceForFileSystemRouter; std::unique_ptr m_clientSubspaceForFileSystemRouterConstructor;std::unique_ptr m_clientSubspaceForListener; std::unique_ptr m_clientSubspaceForMD4; diff --git a/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h b/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h index 59263e62c29c..02a9adbca296 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h @@ -8,6 +8,7 @@ std::unique_ptr m_subspaceForExpectConstructor;std::unique_ptr m_subspaceForExpectAnything; std::unique_ptr m_subspaceForExpectStringContaining; std::unique_ptr m_subspaceForExpectStringMatching; +std::unique_ptr m_subspaceForFSWatcher; std::unique_ptr m_subspaceForFileSystemRouter; std::unique_ptr m_subspaceForFileSystemRouterConstructor;std::unique_ptr m_subspaceForListener; std::unique_ptr m_subspaceForMD4; diff --git a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h index 4471fbab35a6..ac03032e6a0d 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h @@ -58,6 +58,12 @@ JSC::Structure* JSExpectStringMatchingStructure() { return m_JSExpectStringMatch JSC::LazyClassStructure m_JSExpectStringMatching; bool hasJSExpectStringMatchingSetterValue { false }; mutable JSC::WriteBarrier m_JSExpectStringMatchingSetterValue; +JSC::Structure* JSFSWatcherStructure() { return m_JSFSWatcher.getInitializedOnMainThread(this); } + JSC::JSObject* JSFSWatcherConstructor() { return m_JSFSWatcher.constructorInitializedOnMainThread(this); } + JSC::JSValue JSFSWatcherPrototype() { return m_JSFSWatcher.prototypeInitializedOnMainThread(this); } + JSC::LazyClassStructure m_JSFSWatcher; + bool hasJSFSWatcherSetterValue { false }; + mutable JSC::WriteBarrier m_JSFSWatcherSetterValue; JSC::Structure* JSFileSystemRouterStructure() { return m_JSFileSystemRouter.getInitializedOnMainThread(this); } JSC::JSObject* JSFileSystemRouterConstructor() { return m_JSFileSystemRouter.constructorInitializedOnMainThread(this); } JSC::JSValue JSFileSystemRouterPrototype() { return m_JSFileSystemRouter.prototypeInitializedOnMainThread(this); } diff --git a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h index 4e5a2c1fabd4..b3b5327a4dec 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h @@ -58,6 +58,12 @@ void GlobalObject::initGeneratedLazyClasses() { init.setPrototype(WebCore::JSExpectStringMatching::createPrototype(init.vm, reinterpret_cast(init.global))); init.setStructure(WebCore::JSExpectStringMatching::createStructure(init.vm, init.global, init.prototype)); + }); + m_JSFSWatcher.initLater( + [](LazyClassStructure::Initializer& init) { + init.setPrototype(WebCore::JSFSWatcher::createPrototype(init.vm, reinterpret_cast(init.global))); + init.setStructure(WebCore::JSFSWatcher::createStructure(init.vm, init.global, init.prototype)); + }); m_JSFileSystemRouter.initLater( [](LazyClassStructure::Initializer& init) { @@ -211,6 +217,7 @@ void GlobalObject::visitGeneratedLazyClasses(GlobalObject *thisObject, Visitor& thisObject->m_JSExpectAnything.visit(visitor); visitor.append(thisObject->m_JSExpectAnythingSetterValue); thisObject->m_JSExpectStringContaining.visit(visitor); visitor.append(thisObject->m_JSExpectStringContainingSetterValue); thisObject->m_JSExpectStringMatching.visit(visitor); visitor.append(thisObject->m_JSExpectStringMatchingSetterValue); + thisObject->m_JSFSWatcher.visit(visitor); visitor.append(thisObject->m_JSFSWatcherSetterValue); thisObject->m_JSFileSystemRouter.visit(visitor); visitor.append(thisObject->m_JSFileSystemRouterSetterValue); thisObject->m_JSListener.visit(visitor); visitor.append(thisObject->m_JSListenerSetterValue); thisObject->m_JSMD4.visit(visitor); visitor.append(thisObject->m_JSMD4SetterValue); diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp index f7cf1e1f7801..e0a3f33d6269 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.cpp +++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp @@ -5381,6 +5381,297 @@ void JSExpectStringMatching::visitOutputConstraintsImpl(JSCell* cell, Visitor& v } DEFINE_VISIT_OUTPUT_CONSTRAINTS(JSExpectStringMatching); +class JSFSWatcherPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + + static JSFSWatcherPrototype* create(JSC::VM& vm, JSGlobalObject* globalObject, JSC::Structure* structure) + { + JSFSWatcherPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSFSWatcherPrototype(vm, globalObject, structure); + ptr->finishCreation(vm, globalObject); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSFSWatcherPrototype(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) + : Base(vm, structure) + { + } + + void finishCreation(JSC::VM&, JSC::JSGlobalObject*); +}; + +extern "C" void FSWatcherClass__finalize(void*); + +extern "C" EncodedJSValue FSWatcherPrototype__doClose(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(FSWatcherPrototype__closeCallback); + +extern "C" EncodedJSValue FSWatcherPrototype__hasRef(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(FSWatcherPrototype__hasRefCallback); + +extern "C" EncodedJSValue FSWatcherPrototype__doRef(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(FSWatcherPrototype__refCallback); + +extern "C" EncodedJSValue FSWatcherPrototype__doUnref(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(FSWatcherPrototype__unrefCallback); + +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSFSWatcherPrototype, JSFSWatcherPrototype::Base); + +static const HashTableValue JSFSWatcherPrototypeTableValues[] = { + { "close"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, FSWatcherPrototype__closeCallback, 0 } }, + { "hasRef"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, FSWatcherPrototype__hasRefCallback, 0 } }, + { "ref"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, FSWatcherPrototype__refCallback, 0 } }, + { "unref"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, FSWatcherPrototype__unrefCallback, 0 } } +}; + +const ClassInfo JSFSWatcherPrototype::s_info = { "FSWatcher"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSFSWatcherPrototype) }; + +JSC_DEFINE_HOST_FUNCTION(FSWatcherPrototype__closeCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSFSWatcher* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return FSWatcherPrototype__doClose(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(FSWatcherPrototype__hasRefCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSFSWatcher* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return FSWatcherPrototype__hasRef(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(FSWatcherPrototype__refCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSFSWatcher* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return FSWatcherPrototype__doRef(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(FSWatcherPrototype__unrefCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSFSWatcher* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return FSWatcherPrototype__doUnref(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +extern "C" void FSWatcherPrototype__listenerSetCachedValue(JSC::EncodedJSValue thisValue, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue value) +{ + auto& vm = globalObject->vm(); + auto* thisObject = jsCast(JSValue::decode(thisValue)); + thisObject->m_listener.set(vm, thisObject, JSValue::decode(value)); +} + +extern "C" EncodedJSValue FSWatcherPrototype__listenerGetCachedValue(JSC::EncodedJSValue thisValue) +{ + auto* thisObject = jsCast(JSValue::decode(thisValue)); + return JSValue::encode(thisObject->m_listener.get()); +} + +void JSFSWatcherPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSFSWatcher::info(), JSFSWatcherPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +JSFSWatcher::~JSFSWatcher() +{ + if (m_ctx) { + FSWatcherClass__finalize(m_ctx); + } +} +void JSFSWatcher::destroy(JSCell* cell) +{ + static_cast(cell)->JSFSWatcher::~JSFSWatcher(); +} + +const ClassInfo JSFSWatcher::s_info = { "FSWatcher"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSFSWatcher) }; + +void JSFSWatcher::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSFSWatcher* JSFSWatcher::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ctx) +{ + JSFSWatcher* ptr = new (NotNull, JSC::allocateCell(vm)) JSFSWatcher(vm, structure, ctx); + ptr->finishCreation(vm); + return ptr; +} + +extern "C" void* FSWatcher__fromJS(JSC::EncodedJSValue value) +{ + JSC::JSValue decodedValue = JSC::JSValue::decode(value); + if (decodedValue.isEmpty() || !decodedValue.isCell()) + return nullptr; + + JSC::JSCell* cell = decodedValue.asCell(); + JSFSWatcher* object = JSC::jsDynamicCast(cell); + + if (!object) + return nullptr; + + return object->wrapped(); +} + +extern "C" bool FSWatcher__dangerouslySetPtr(JSC::EncodedJSValue value, void* ptr) +{ + JSFSWatcher* object = JSC::jsDynamicCast(JSValue::decode(value)); + if (!object) + return false; + + object->m_ctx = ptr; + return true; +} + +extern "C" const size_t FSWatcher__ptrOffset = JSFSWatcher::offsetOfWrapped(); + +void JSFSWatcher::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) +{ + auto* thisObject = jsCast(cell); + if (void* wrapped = thisObject->wrapped()) { + // if (thisObject->scriptExecutionContext()) + // analyzer.setLabelForCell(cell, "url " + thisObject->scriptExecutionContext()->url().string()); + } + Base::analyzeHeap(cell, analyzer); +} + +JSObject* JSFSWatcher::createPrototype(VM& vm, JSDOMGlobalObject* globalObject) +{ + return JSFSWatcherPrototype::create(vm, globalObject, JSFSWatcherPrototype::createStructure(vm, globalObject, globalObject->objectPrototype())); +} + +extern "C" EncodedJSValue FSWatcher__create(Zig::GlobalObject* globalObject, void* ptr) +{ + auto& vm = globalObject->vm(); + JSC::Structure* structure = globalObject->JSFSWatcherStructure(); + JSFSWatcher* instance = JSFSWatcher::create(vm, globalObject, structure, ptr); + + return JSValue::encode(instance); +} + +template +void JSFSWatcher::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + JSFSWatcher* thisObject = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_listener); +} + +DEFINE_VISIT_CHILDREN(JSFSWatcher); + +template +void JSFSWatcher::visitAdditionalChildren(Visitor& visitor) +{ + JSFSWatcher* thisObject = this; + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + visitor.append(thisObject->m_listener); +} + +DEFINE_VISIT_ADDITIONAL_CHILDREN(JSFSWatcher); + +template +void JSFSWatcher::visitOutputConstraintsImpl(JSCell* cell, Visitor& visitor) +{ + JSFSWatcher* thisObject = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + thisObject->visitAdditionalChildren(visitor); +} + +DEFINE_VISIT_OUTPUT_CONSTRAINTS(JSFSWatcher); class JSFileSystemRouterPrototype final : public JSC::JSNonFinalObject { public: using Base = JSC::JSNonFinalObject; diff --git a/src/bun.js/bindings/ZigGeneratedClasses.h b/src/bun.js/bindings/ZigGeneratedClasses.h index 668cd3f6b17c..3fa0e26d2ff9 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.h +++ b/src/bun.js/bindings/ZigGeneratedClasses.h @@ -578,6 +578,62 @@ class JSExpectStringMatching final : public JSC::JSDestructibleObject { mutable JSC::WriteBarrier m_testValue; }; +class JSFSWatcher final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static JSFSWatcher* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ctx); + + DECLARE_EXPORT_INFO; + template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForFSWatcher.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForFSWatcher = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForFSWatcher.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForFSWatcher = std::forward(space); }); + } + + static void destroy(JSC::JSCell*); + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(static_cast(0b11101110), StructureFlags), info()); + } + + static JSObject* createPrototype(VM& vm, JSDOMGlobalObject* globalObject); + ; + + ~JSFSWatcher(); + + void* wrapped() const { return m_ctx; } + + void detach() + { + m_ctx = nullptr; + } + + static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + static ptrdiff_t offsetOfWrapped() { return OBJECT_OFFSETOF(JSFSWatcher, m_ctx); } + + void* m_ctx { nullptr }; + + JSFSWatcher(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr) + : Base(vm, structure) + { + m_ctx = sinkPtr; + } + + void finishCreation(JSC::VM&); + + DECLARE_VISIT_CHILDREN; + template void visitAdditionalChildren(Visitor&); + DECLARE_VISIT_OUTPUT_CONSTRAINTS; + + mutable JSC::WriteBarrier m_listener; +}; + class JSFileSystemRouter final : public JSC::JSDestructibleObject { public: using Base = JSC::JSDestructibleObject; diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig index d626cac60411..74e30cd835bf 100644 --- a/src/bun.js/bindings/generated_classes.zig +++ b/src/bun.js/bindings/generated_classes.zig @@ -1406,6 +1406,96 @@ pub const JSExpectStringMatching = struct { } } }; +pub const JSFSWatcher = struct { + const FSWatcher = Classes.FSWatcher; + const GetterType = fn (*FSWatcher, *JSC.JSGlobalObject) callconv(.C) JSC.JSValue; + const GetterTypeWithThisValue = fn (*FSWatcher, JSC.JSValue, *JSC.JSGlobalObject) callconv(.C) JSC.JSValue; + const SetterType = fn (*FSWatcher, *JSC.JSGlobalObject, JSC.JSValue) callconv(.C) bool; + const SetterTypeWithThisValue = fn (*FSWatcher, JSC.JSValue, *JSC.JSGlobalObject, JSC.JSValue) callconv(.C) bool; + const CallbackType = fn (*FSWatcher, *JSC.JSGlobalObject, *JSC.CallFrame) callconv(.C) JSC.JSValue; + + /// Return the pointer to the wrapped object. + /// If the object does not match the type, return null. + pub fn fromJS(value: JSC.JSValue) ?*FSWatcher { + JSC.markBinding(@src()); + return FSWatcher__fromJS(value); + } + + extern fn FSWatcherPrototype__listenerSetCachedValue(JSC.JSValue, *JSC.JSGlobalObject, JSC.JSValue) void; + + extern fn FSWatcherPrototype__listenerGetCachedValue(JSC.JSValue) JSC.JSValue; + + /// `FSWatcher.listener` setter + /// This value will be visited by the garbage collector. + pub fn listenerSetCached(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void { + JSC.markBinding(@src()); + FSWatcherPrototype__listenerSetCachedValue(thisValue, globalObject, value); + } + + /// `FSWatcher.listener` getter + /// This value will be visited by the garbage collector. + pub fn listenerGetCached(thisValue: JSC.JSValue) ?JSC.JSValue { + JSC.markBinding(@src()); + const result = FSWatcherPrototype__listenerGetCachedValue(thisValue); + if (result == .zero) + return null; + + return result; + } + + /// Create a new instance of FSWatcher + pub fn toJS(this: *FSWatcher, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + JSC.markBinding(@src()); + if (comptime Environment.allow_assert) { + const value__ = FSWatcher__create(globalObject, this); + std.debug.assert(value__.as(FSWatcher).? == this); // If this fails, likely a C ABI issue. + return value__; + } else { + return FSWatcher__create(globalObject, this); + } + } + + /// Modify the internal ptr to point to a new instance of FSWatcher. + pub fn dangerouslySetPtr(value: JSC.JSValue, ptr: ?*FSWatcher) bool { + JSC.markBinding(@src()); + return FSWatcher__dangerouslySetPtr(value, ptr); + } + + /// Detach the ptr from the thisValue + pub fn detachPtr(_: *FSWatcher, value: JSC.JSValue) void { + JSC.markBinding(@src()); + std.debug.assert(FSWatcher__dangerouslySetPtr(value, null)); + } + + extern fn FSWatcher__fromJS(JSC.JSValue) ?*FSWatcher; + extern fn FSWatcher__getConstructor(*JSC.JSGlobalObject) JSC.JSValue; + + extern fn FSWatcher__create(globalObject: *JSC.JSGlobalObject, ptr: ?*FSWatcher) JSC.JSValue; + + extern fn FSWatcher__dangerouslySetPtr(JSC.JSValue, ?*FSWatcher) bool; + + comptime { + if (@TypeOf(FSWatcher.finalize) != (fn (*FSWatcher) callconv(.C) void)) { + @compileLog("FSWatcher.finalize is not a finalizer"); + } + + if (@TypeOf(FSWatcher.doClose) != CallbackType) + @compileLog("Expected FSWatcher.doClose to be a callback but received " ++ @typeName(@TypeOf(FSWatcher.doClose))); + if (@TypeOf(FSWatcher.hasRef) != CallbackType) + @compileLog("Expected FSWatcher.hasRef to be a callback but received " ++ @typeName(@TypeOf(FSWatcher.hasRef))); + if (@TypeOf(FSWatcher.doRef) != CallbackType) + @compileLog("Expected FSWatcher.doRef to be a callback but received " ++ @typeName(@TypeOf(FSWatcher.doRef))); + if (@TypeOf(FSWatcher.doUnref) != CallbackType) + @compileLog("Expected FSWatcher.doUnref to be a callback but received " ++ @typeName(@TypeOf(FSWatcher.doUnref))); + if (!JSC.is_bindgen) { + @export(FSWatcher.doClose, .{ .name = "FSWatcherPrototype__doClose" }); + @export(FSWatcher.doRef, .{ .name = "FSWatcherPrototype__doRef" }); + @export(FSWatcher.doUnref, .{ .name = "FSWatcherPrototype__doUnref" }); + @export(FSWatcher.finalize, .{ .name = "FSWatcherClass__finalize" }); + @export(FSWatcher.hasRef, .{ .name = "FSWatcherPrototype__hasRef" }); + } + } +}; pub const JSFileSystemRouter = struct { const FileSystemRouter = Classes.FileSystemRouter; const GetterType = fn (*FileSystemRouter, *JSC.JSGlobalObject) callconv(.C) JSC.JSValue; @@ -4858,6 +4948,7 @@ comptime { _ = JSExpectAnything; _ = JSExpectStringContaining; _ = JSExpectStringMatching; + _ = JSFSWatcher; _ = JSFileSystemRouter; _ = JSListener; _ = JSMD4; diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig index 5e911fce09be..8e7e5684f152 100644 --- a/src/bun.js/node/node_fs_watcher.zig +++ b/src/bun.js/node/node_fs_watcher.zig @@ -16,13 +16,13 @@ pub const FSWatcher = struct { const watcher = @import("../../watcher.zig"); const options = @import("../../options.zig"); pub const Watcher = watcher.NewWatcher(*FSWatcher); + const log = Output.scoped(.FSWatcher, false); onAccept: std.ArrayHashMapUnmanaged(FSWatcher.Watcher.HashType, bun.BabyList(OnAcceptCallback), bun.ArrayIdentityContext, false) = .{}, ctx: *VirtualMachine, js_watcher: ?*JSObject = null, watcher_instance: ?*FSWatcher.Watcher = null, verbose: bool = false, - tombstones: std.StringHashMapUnmanaged(*bun.fs.FileSystem.RealFS.EntriesOption) = .{}, file_paths: bun.BabyList(string) = .{}, pub fn toJS(this: *FSWatcher) JSC.JSValue { @@ -145,73 +145,54 @@ pub const FSWatcher = struct { bun.copy(u8, file_path_clone, file_path); ctx.file_paths.push(bun.default_allocator, file_path_clone) catch unreachable; - const entries_option = Fs.FileSystem.instance.fs.readDirectory( - file_path_clone, - null, - 0, - true, - ) catch |err| { + fs_watcher.addDirectory(fd, file_path_clone, FSWatcher.Watcher.getHash(file_path), false) catch |err| { ctx.deinit(); - bun.default_allocator.destroy(fs_watcher); + fs_watcher.deinit(true); return err; }; - fs_watcher.addDirectory(fd, file_path_clone, FSWatcher.Watcher.getHash(file_path), false) catch |err| { + var iter = (std.fs.IterableDir{ .dir = std.fs.Dir{ + .fd = fd, + } }).iterate(); + + while (iter.next() catch |err| { ctx.deinit(); - bun.default_allocator.destroy(fs_watcher); + fs_watcher.deinit(true); return err; - }; - if (entries_option.* == .entries) { - var entries = entries_option.entries.data.iterator(); - while (entries.next()) |entry_iter| { - var entry: *Fs.FileSystem.Entry = entry_iter.value_ptr.*; - - var parts = [2]string{ entry.dir, entry.base() }; - var entry_path = Path.joinAbsStringBuf( - Fs.FileSystem.instance.topLevelDirWithoutTrailingSlash(), - buf, - &parts, - .auto, - ); - - buf[file_path.len + 1] = 0; - buf[file_path.len] = 0; - var entry_path_z = buf[0..file_path.len :0]; - - var kind = entry.kind(&Fs.FileSystem.instance.fs, true); - - if (kind == .dir) { - if (recursive) { - if (entry.cache.fd == 0) { - const dir = bun.openDirForPath(entry_path_z) catch |err| { - ctx.deinit(); - bun.default_allocator.destroy(fs_watcher); - return err; - }; - entry.cache.fd = dir.fd; - } - addDirectory(ctx, fs_watcher, entry.cache.fd, entry_path, recursive, buf) catch |err| { - ctx.deinit(); - bun.default_allocator.destroy(fs_watcher); - return err; - }; - } - } else { - if (entry.cache.fd == 0) { - const file = bun.openFileForPath(entry_path_z) catch |err| { - ctx.deinit(); - bun.default_allocator.destroy(fs_watcher); - return err; - }; - entry.cache.fd = file.handle; - } - file_path_clone = bun.default_allocator.alloc(u8, entry_path.len) catch unreachable; - bun.copy(u8, file_path_clone, entry_path); - ctx.file_paths.push(bun.default_allocator, file_path_clone) catch unreachable; + }) |entry| { + var parts = [2]string{ file_path_clone, entry.name }; + var entry_path = Path.joinAbsStringBuf( + Fs.FileSystem.instance.topLevelDirWithoutTrailingSlash(), + buf, + &parts, + .auto, + ); + + buf[entry_path.len + 1] = 0; + buf[entry_path.len] = 0; + var entry_path_z = buf[0..entry_path.len :0]; + + var fs_info = fdFromAbsolutePathZ(entry_path_z) catch |err| { + ctx.deinit(); + fs_watcher.deinit(true); + return err; + }; + + if (fs_info.is_file) { + file_path_clone = bun.default_allocator.alloc(u8, entry_path.len) catch unreachable; + bun.copy(u8, file_path_clone, entry_path); + ctx.file_paths.push(bun.default_allocator, file_path_clone) catch unreachable; - fs_watcher.addFile(entry.cache.fd, file_path_clone, FSWatcher.Watcher.getHash(entry_path), options.Loader.file, 0, null, false) catch |err| { + fs_watcher.addFile(fs_info.fd, file_path_clone, FSWatcher.Watcher.getHash(entry_path), options.Loader.file, 0, null, false) catch |err| { + ctx.deinit(); + fs_watcher.deinit(true); + return err; + }; + } else { + if (recursive) { + addDirectory(ctx, fs_watcher, fs_info.fd, entry_path, recursive, buf) catch |err| { ctx.deinit(); - bun.default_allocator.destroy(fs_watcher); + fs_watcher.deinit(true); return err; }; } @@ -219,14 +200,6 @@ pub const FSWatcher = struct { } } - fn putTombstone(this: *FSWatcher, key: []const u8, value: *bun.fs.FileSystem.RealFS.EntriesOption) void { - this.tombstones.put(bun.default_allocator, key, value) catch unreachable; - } - - fn getTombstone(this: *FSWatcher, key: []const u8) ?*bun.fs.FileSystem.RealFS.EntriesOption { - return this.tombstones.get(key); - } - pub fn onError( this: *FSWatcher, err: anyerror, @@ -250,7 +223,7 @@ pub const FSWatcher = struct { const kinds = slice.items(.kind); const hashes = slice.items(.hash); const parents = slice.items(.parent_hash); - var file_descriptors = slice.items(.fd); + var ctx = this.watcher_instance.?; defer ctx.flushEvictions(); defer Output.flush(); @@ -261,7 +234,6 @@ pub const FSWatcher = struct { &this.ctx.bundler; var fs: *Fs.FileSystem = bundler.fs; - var rfs: *Fs.FileSystem.RealFS = &fs.fs; var current_task: FSWatchTask = .{ .ctx = this, @@ -310,21 +282,13 @@ pub const FSWatcher = struct { }, .directory => { var affected_buf: [128][]const u8 = undefined; - var entries_option: ?*Fs.FileSystem.RealFS.EntriesOption = null; const affected = brk: { if (comptime Environment.isMac) { - if (rfs.entries.get(file_path)) |existing| { - this.putTombstone(file_path, existing); - entries_option = existing; - } else if (this.getTombstone(file_path)) |existing| { - entries_option = existing; - } - var affected_i: usize = 0; // if a file descriptor is stale, we need to close it - if (event.op.delete and entries_option != null) { + if (event.op.delete) { for (parents, 0..) |parent_hash, entry_id| { if (parent_hash == id) { const affected_path = file_paths[entry_id]; @@ -347,92 +311,42 @@ pub const FSWatcher = struct { break :brk event.names(changed_files); }; - if (affected.len > 0 and !Environment.isMac) { - if (rfs.entries.get(file_path)) |existing| { - this.putTombstone(file_path, existing); - entries_option = existing; - } else if (this.getTombstone(file_path)) |existing| { - entries_option = existing; - } - } + var last_file_hash: FSWatcher.Watcher.HashType = std.math.maxInt(FSWatcher.Watcher.HashType); - if (entries_option) |dir_ent| { - var last_file_hash: FSWatcher.Watcher.HashType = std.math.maxInt(FSWatcher.Watcher.HashType); - - for (affected) |changed_name_| { - const changed_name: []const u8 = if (comptime Environment.isMac) - changed_name_ - else - bun.asByteSlice(changed_name_.?); - if (changed_name.len == 0 or changed_name[0] == '~' or changed_name[0] == '.') continue; - - const loader = (bundler.options.loaders.get(Fs.PathName.init(changed_name).ext) orelse .file); - var prev_entry_id: usize = std.math.maxInt(usize); - if (loader != .file) { - var path_string: bun.PathString = undefined; - var file_hash: FSWatcher.Watcher.HashType = last_file_hash; - const abs_path: string = brk: { - if (dir_ent.entries.get(@ptrCast([]const u8, changed_name))) |file_ent| { - // reset the file descriptor - file_ent.entry.cache.fd = 0; - file_ent.entry.need_stat = true; - path_string = file_ent.entry.abs_path; - file_hash = FSWatcher.Watcher.getHash(path_string.slice()); - for (hashes, 0..) |hash, entry_id| { - if (hash == file_hash) { - if (file_descriptors[entry_id] != 0) { - if (prev_entry_id != entry_id) { - ctx.removeAtIndex( - @truncate(u16, entry_id), - 0, - &.{}, - .file, - ); - } - } - - prev_entry_id = entry_id; - break; - } - } - const path_slice = path_string.slice(); - if (path_slice.len > 0) { - if (event.op.rename or event.op.move_to) { - current_task.append(path_slice, .rename, false); - } else { - current_task.append(path_slice, .change, false); - } - break :brk path_slice; - } - } + for (affected) |changed_name_| { + const changed_name: []const u8 = if (comptime Environment.isMac) + changed_name_ + else + 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); - const file_path_without_trailing_slash_buf = bun.default_allocator.alloc(u8, file_path_without_trailing_slash.len + changed_name.len + 1) catch unreachable; + var file_hash: FSWatcher.Watcher.HashType = last_file_hash; + const abs_path: string = brk: { + var file_path_without_trailing_slash = std.mem.trimRight(u8, file_path, std.fs.path.sep_str); + const file_path_without_trailing_slash_buf = bun.default_allocator.alloc(u8, file_path_without_trailing_slash.len + changed_name.len + 1) catch unreachable; - @memcpy(file_path_without_trailing_slash_buf.ptr, file_path_without_trailing_slash.ptr, file_path_without_trailing_slash.len); - file_path_without_trailing_slash_buf[file_path_without_trailing_slash.len] = std.fs.path.sep; + @memcpy(file_path_without_trailing_slash_buf.ptr, file_path_without_trailing_slash.ptr, file_path_without_trailing_slash.len); + file_path_without_trailing_slash_buf[file_path_without_trailing_slash.len] = std.fs.path.sep; - @memcpy(file_path_without_trailing_slash_buf[file_path_without_trailing_slash.len + 1 ..].ptr, changed_name.ptr, changed_name.len); - const path_slice = file_path_without_trailing_slash_buf[0 .. file_path_without_trailing_slash.len + changed_name.len + 1]; - file_hash = FSWatcher.Watcher.getHash(path_slice); + @memcpy(file_path_without_trailing_slash_buf[file_path_without_trailing_slash.len + 1 ..].ptr, changed_name.ptr, changed_name.len); + const path_slice = file_path_without_trailing_slash_buf[0 .. file_path_without_trailing_slash.len + changed_name.len + 1]; + file_hash = FSWatcher.Watcher.getHash(path_slice); - if (event.op.rename or event.op.move_to) { - current_task.append(file_path_without_trailing_slash_buf, .rename, true); - } else { - current_task.append(file_path_without_trailing_slash_buf, .change, true); - } + if (event.op.rename or event.op.move_to) { + current_task.append(file_path_without_trailing_slash_buf, .rename, true); + } else { + current_task.append(file_path_without_trailing_slash_buf, .change, true); + } - break :brk path_slice; - }; + break :brk path_slice; + }; - // skip consecutive duplicates - if (last_file_hash == file_hash) continue; - last_file_hash = file_hash; + // skip consecutive duplicates + if (last_file_hash == file_hash) continue; + last_file_hash = file_hash; - if (this.verbose) - Output.prettyErrorln(" File change: {s}", .{fs.relativeTo(abs_path)}); - } - } + if (this.verbose) + Output.prettyErrorln(" Dir change: {s}", .{fs.relativeTo(abs_path)}); } if (this.verbose) { @@ -751,7 +665,7 @@ pub const FSWatcher = struct { signal.detach(this); } this.closed = true; - this.manager.stop(); + this.manager.stop(true); this.js_this.unprotect(); this.unref(); if (emitEvent) { @@ -769,8 +683,7 @@ pub const FSWatcher = struct { this.manager.ctx.js_watcher = null; this.js_this = .zero; this.globalThis = null; - this.manager.stop(); - this.manager.thread.join(); + this.manager.stop(true); if (this.signal) |signal| { this.signal = null; @@ -781,12 +694,47 @@ pub const FSWatcher = struct { this.persistent = false; this.poll_ref.unref(this.manager.ctx.ctx); } - bun.default_allocator.destroy(this.manager.ctx); - bun.default_allocator.destroy(this.manager); + this.manager.ctx.deinit(); + this.manager.deinit(true); bun.default_allocator.destroy(this); } }; + const PathResult = struct { + fd: StoredFileDescriptorType = 0, + is_file: bool = true, + }; + + fn fdFromAbsolutePathZ( + absolute_path_z: [:0]const u8, + ) !PathResult { + var stat = try bun.C.lstat_absolute(absolute_path_z); + var result = PathResult{}; + + switch (stat.kind) { + .SymLink => { + var file = try std.fs.openFileAbsoluteZ(absolute_path_z, .{ .mode = .read_only }); + result.fd = file.handle; + const _stat = try file.stat(); + + result.is_file = _stat.kind == .Directory; + }, + .Directory => { + const dir = (try std.fs.openIterableDirAbsoluteZ(absolute_path_z, .{ + .access_sub_paths = true, + })).dir; + result.fd = dir.fd; + result.is_file = false; + }, + else => { + const file = try bun.openFileForPath(absolute_path_z); + result.fd = file.handle; + result.is_file = true; + }, + } + return result; + } + pub fn init(args: Arguments) !*FSWatcher { var buf: [bun.MAX_PATH_BYTES]u8 = undefined; @@ -805,7 +753,7 @@ pub const FSWatcher = struct { buf[file_path.len] = 0; var file_path_z = buf[0..file_path.len :0]; - var fs_type = try Fs.FileSystem.instance.fs.kindFromAbsolute(file_path_z, 0, true); + var fs_type = try fdFromAbsolutePathZ(file_path_z); var ctx = try bun.default_allocator.create(FSWatcher); const vm = args.global_this.bunVM(); @@ -829,53 +777,33 @@ pub const FSWatcher = struct { ctx.watcher_instance = fs_watcher; - if (fs_type.kind == .file) { - if (fs_type.fd == 0) { - const file = bun.openFileForPath(file_path_z) catch |err| { - ctx.deinit(); - bun.default_allocator.destroy(fs_watcher); - return err; - }; - fs_type.fd = file.handle; - } - + if (fs_type.is_file) { var file_path_clone = bun.default_allocator.alloc(u8, file_path.len) catch unreachable; bun.copy(u8, file_path_clone, file_path); ctx.file_paths.push(bun.default_allocator, file_path_clone) catch unreachable; fs_watcher.addFile(fs_type.fd, file_path_clone, FSWatcher.Watcher.getHash(file_path), options.Loader.file, 0, null, false) catch |err| { ctx.deinit(); - bun.default_allocator.destroy(fs_watcher); + fs_watcher.deinit(true); return err; }; - } else if (fs_type.kind == .dir) { - if (fs_type.fd == 0) { - const dir = bun.openDirForPath(file_path_z) catch |err| { - ctx.deinit(); - bun.default_allocator.destroy(fs_watcher); - return err; - }; - fs_type.fd = dir.fd; - } + } else { addDirectory(ctx, fs_watcher, fs_type.fd, file_path, args.recursive, &buf) catch |err| { ctx.deinit(); - bun.default_allocator.destroy(fs_watcher); + fs_watcher.deinit(true); return err; }; - } else { - ctx.deinit(); - bun.default_allocator.destroy(fs_watcher); - return error.UnableToFindDirectory; } fs_watcher.start() catch |err| { ctx.deinit(); - bun.default_allocator.destroy(fs_watcher); + + fs_watcher.deinit(true); return err; }; ctx.js_watcher = JSObject.init(args.global_this, fs_watcher, args.signal, args.listener, args.persistent, args.encoding) catch |err| { ctx.deinit(); - bun.default_allocator.destroy(fs_watcher); + fs_watcher.deinit(true); return err; }; diff --git a/src/watcher.zig b/src/watcher.zig index 07ceccecfec0..64a89bbe698e 100644 --- a/src/watcher.zig +++ b/src/watcher.zig @@ -370,6 +370,7 @@ pub fn NewWatcher(comptime ContextType: type) type { cwd: string, thread: std.Thread = undefined, running: bool = true, + close_descriptors: bool = false, pub const HashType = u32; @@ -404,8 +405,27 @@ pub fn NewWatcher(comptime ContextType: type) type { this.thread = try std.Thread.spawn(.{}, Watcher.watchLoop, .{this}); } - pub fn stop(this: *Watcher) void { + pub fn stop(this: *Watcher, close_descriptors: bool) void { this.running = false; + this.close_descriptors = close_descriptors; + } + + pub fn deinit(this: *Watcher, close_descriptors: bool) void { + this.running = false; + this.close_descriptors = close_descriptors; + if (this.watchloop_handle != null) { + std.Thread.join(this.thread); + } else { + if (this.close_descriptors) { + const fds = this.watchlist.items(.fd); + for (fds) |fd| { + std.os.close(fd); + } + } + this.watchlist.deinit(this.allocator); + } + const allocator = this.allocator; + allocator.destroy(this); } // This must only be called from the watcher thread @@ -421,8 +441,16 @@ pub fn NewWatcher(comptime ContextType: type) type { PlatformWatcher.stop(); this.ctx.onError(err); - return; }; + + // deinit and close descriptors if needed + if (this.close_descriptors) { + const fds = this.watchlist.items(.fd); + for (fds) |fd| { + std.os.close(fd); + } + } + this.watchlist.deinit(this.allocator); } var evict_list_i: WatchItemIndex = 0; From e4c81ba95be687d572e97407b920b8485508fe22 Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Fri, 16 Jun 2023 00:39:54 -0300 Subject: [PATCH 06/18] more fixes --- src/bun.js/node/node_fs_watcher.zig | 161 +++++++++++++++------- src/js/node/fs.js | 15 +- src/watcher.zig | 4 +- test/js/node/watch/fixtures/persistent.js | 5 + test/js/node/watch/fixtures/relative.js | 23 ++++ test/js/node/watch/fs.watch.test.js | 124 +++++++++++++++++ 6 files changed, 276 insertions(+), 56 deletions(-) create mode 100644 test/js/node/watch/fixtures/persistent.js create mode 100644 test/js/node/watch/fixtures/relative.js create mode 100644 test/js/node/watch/fs.watch.test.js diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig index 8e7e5684f152..cf6834ea9f8a 100644 --- a/src/bun.js/node/node_fs_watcher.zig +++ b/src/bun.js/node/node_fs_watcher.zig @@ -18,12 +18,21 @@ pub const FSWatcher = struct { pub const Watcher = watcher.NewWatcher(*FSWatcher); const log = Output.scoped(.FSWatcher, false); + pub const ChangeEvent = struct { + hash: Watcher.HashType = 0, + event_type: FSWatchTask.EventType = .change, + time_stamp: i64 = 0, + }; + onAccept: std.ArrayHashMapUnmanaged(FSWatcher.Watcher.HashType, bun.BabyList(OnAcceptCallback), bun.ArrayIdentityContext, false) = .{}, ctx: *VirtualMachine, js_watcher: ?*JSObject = null, watcher_instance: ?*FSWatcher.Watcher = null, verbose: bool = false, file_paths: bun.BabyList(string) = .{}, + entry_path: ?string = null, + entry_dir: string = "", + last_change_event: ChangeEvent = .{}, pub fn toJS(this: *FSWatcher) JSC.JSValue { return if (this.js_watcher) |js| js.js_this else JSC.JSValue.jsUndefined(); @@ -42,6 +51,10 @@ pub const FSWatcher = struct { bun.default_allocator.destroy(file_path); } this.file_paths.deinitWithAllocator(bun.default_allocator); + if (this.entry_path) |path| { + this.entry_path = null; + bun.default_allocator.destroy(path); + } bun.default_allocator.destroy(this); } @@ -140,11 +153,15 @@ pub const FSWatcher = struct { specifier: []const u8, ) void); - fn addDirectory(ctx: *FSWatcher, fs_watcher: *FSWatcher.Watcher, fd: StoredFileDescriptorType, file_path: string, recursive: bool, buf: *[bun.MAX_PATH_BYTES]u8) !void { - var file_path_clone = bun.default_allocator.alloc(u8, file_path.len) catch unreachable; - bun.copy(u8, file_path_clone, file_path); - ctx.file_paths.push(bun.default_allocator, file_path_clone) catch unreachable; + fn addDirectory(ctx: *FSWatcher, fs_watcher: *FSWatcher.Watcher, fd: StoredFileDescriptorType, file_path: string, recursive: bool, buf: *[bun.MAX_PATH_BYTES]u8, is_entry_path: bool) !void { + var file_path_clone = bun.default_allocator.dupe(u8, file_path) catch unreachable; + if (is_entry_path) { + ctx.entry_path = file_path_clone; + ctx.entry_dir = file_path_clone; + } else { + ctx.file_paths.push(bun.default_allocator, file_path_clone) catch unreachable; + } fs_watcher.addDirectory(fd, file_path_clone, FSWatcher.Watcher.getHash(file_path), false) catch |err| { ctx.deinit(); fs_watcher.deinit(true); @@ -179,8 +196,8 @@ pub const FSWatcher = struct { }; if (fs_info.is_file) { - file_path_clone = bun.default_allocator.alloc(u8, entry_path.len) catch unreachable; - bun.copy(u8, file_path_clone, entry_path); + file_path_clone = bun.default_allocator.dupe(u8, entry_path) catch unreachable; + ctx.file_paths.push(bun.default_allocator, file_path_clone) catch unreachable; fs_watcher.addFile(fs_info.fd, file_path_clone, FSWatcher.Watcher.getHash(entry_path), options.Loader.file, 0, null, false) catch |err| { @@ -190,7 +207,7 @@ pub const FSWatcher = struct { }; } else { if (recursive) { - addDirectory(ctx, fs_watcher, fs_info.fd, entry_path, recursive, buf) catch |err| { + addDirectory(ctx, fs_watcher, fs_info.fd, entry_path, recursive, buf, false) catch |err| { ctx.deinit(); fs_watcher.deinit(true); return err; @@ -219,10 +236,12 @@ pub const FSWatcher = struct { ) void { var slice = watchlist.slice(); const file_paths = slice.items(.file_path); + var counts = slice.items(.count); const kinds = slice.items(.kind); const hashes = slice.items(.hash); const parents = slice.items(.parent_hash); + var _on_file_update_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; var ctx = this.watcher_instance.?; defer ctx.flushEvictions(); @@ -240,6 +259,9 @@ pub const FSWatcher = struct { }; defer current_task.enqueue(); + const time_stamp = std.time.milliTimestamp(); + const time_diff = time_stamp - this.last_change_event.time_stamp; + for (events) |event| { const file_path = file_paths[event.index]; const update_count = counts[event.index] + 1; @@ -268,15 +290,25 @@ pub const FSWatcher = struct { ); } - if (this.verbose) - Output.prettyErrorln("File changed: {s}", .{fs.relativeTo(file_path)}); + var file_hash: FSWatcher.Watcher.HashType = FSWatcher.Watcher.getHash(file_path); if (event.op.write or event.op.delete or event.op.rename) { - // if is deleted we actually emit as rename like node.js - if (event.op.delete or event.op.rename) { - current_task.append(file_path, .rename, false); - } else { - current_task.append(file_path, .change, false); + const event_type: FSWatchTask.EventType = if (event.op.delete or event.op.rename) .rename else .change; + // 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 != file_hash) { + this.last_change_event.time_stamp = time_stamp; + this.last_change_event.event_type = event_type; + this.last_change_event.hash = file_hash; + + const relative_slice = fs.relative(this.entry_dir, file_path); + + if (this.verbose) + Output.prettyErrorln("File changed: {s}", .{relative_slice}); + + const relative_path = bun.default_allocator.dupe(u8, relative_slice) catch unreachable; + + // if is deleted we actually emit as rename like node.js + current_task.append(relative_path, event_type, true); } } }, @@ -311,8 +343,6 @@ pub const FSWatcher = struct { break :brk event.names(changed_files); }; - var last_file_hash: FSWatcher.Watcher.HashType = std.math.maxInt(FSWatcher.Watcher.HashType); - for (affected) |changed_name_| { const changed_name: []const u8 = if (comptime Environment.isMac) changed_name_ @@ -320,37 +350,40 @@ pub const FSWatcher = struct { bun.asByteSlice(changed_name_.?); if (changed_name.len == 0 or changed_name[0] == '~' or changed_name[0] == '.') continue; - var file_hash: FSWatcher.Watcher.HashType = last_file_hash; - const abs_path: string = brk: { + var file_hash: FSWatcher.Watcher.HashType = 0; + const relative_slice: string = brk: { var file_path_without_trailing_slash = std.mem.trimRight(u8, file_path, std.fs.path.sep_str); - const file_path_without_trailing_slash_buf = bun.default_allocator.alloc(u8, file_path_without_trailing_slash.len + changed_name.len + 1) catch unreachable; - @memcpy(file_path_without_trailing_slash_buf.ptr, file_path_without_trailing_slash.ptr, file_path_without_trailing_slash.len); - file_path_without_trailing_slash_buf[file_path_without_trailing_slash.len] = std.fs.path.sep; + @memcpy(&_on_file_update_path_buf, file_path_without_trailing_slash.ptr, file_path_without_trailing_slash.len); + _on_file_update_path_buf[file_path_without_trailing_slash.len] = std.fs.path.sep; - @memcpy(file_path_without_trailing_slash_buf[file_path_without_trailing_slash.len + 1 ..].ptr, changed_name.ptr, changed_name.len); - const path_slice = file_path_without_trailing_slash_buf[0 .. file_path_without_trailing_slash.len + changed_name.len + 1]; + @memcpy(_on_file_update_path_buf[file_path_without_trailing_slash.len + 1 ..].ptr, changed_name.ptr, changed_name.len); + const path_slice = _on_file_update_path_buf[0 .. file_path_without_trailing_slash.len + changed_name.len + 1]; file_hash = FSWatcher.Watcher.getHash(path_slice); - if (event.op.rename or event.op.move_to) { - current_task.append(file_path_without_trailing_slash_buf, .rename, true); - } else { - current_task.append(file_path_without_trailing_slash_buf, .change, true); - } + const relative = fs.relative(this.entry_dir, path_slice); - break :brk path_slice; + break :brk relative; }; // skip consecutive duplicates - if (last_file_hash == file_hash) continue; - last_file_hash = file_hash; + const event_type: FSWatchTask.EventType = if (event.op.delete or event.op.rename or event.op.move_to) .rename else .change; + 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 != file_hash) { + const relative_path = bun.default_allocator.dupe(u8, relative_slice) catch unreachable; - if (this.verbose) - Output.prettyErrorln(" Dir change: {s}", .{fs.relativeTo(abs_path)}); + this.last_change_event.time_stamp = time_stamp; + this.last_change_event.event_type = event_type; + this.last_change_event.hash = file_hash; + + current_task.append(relative_path, event_type, true); + + if (this.verbose) + Output.prettyErrorln(" Dir change: {s}", .{relative_path}); + } } - if (this.verbose) { - Output.prettyErrorln(" Dir change: {s}", .{fs.relativeTo(file_path)}); + if (this.verbose and affected.len == 0) { + Output.prettyErrorln(" Dir change: {s}", .{fs.relative(this.entry_dir, file_path)}); } }, } @@ -544,21 +577,30 @@ pub const FSWatcher = struct { obj.js_this.protect(); if (signal) |s| { + obj.signal = s.ref(); // already aborted? if (s.aborted()) { - const err = s.abortReason(); - obj.emitAbort(err); + if (bun.uws.Loop.get()) |loop| { + loop.nextTick(*JSObject, obj, JSObject._abortNextTick); + } } else { // watch for abortion - obj.signal = s.ref().listen(JSObject, obj, JSObject.emitAbort); + _ = s.listen(JSObject, obj, JSObject.emitAbort); } } return obj; } + fn _abortNextTick(this: *JSObject) void { + if (this.signal) |s| { + const err = s.abortReason(); + this.emitAbort(err); + } + } + pub fn emitAbort(this: *JSObject, err: JSC.JSValue) void { if (this.closed) return; - defer this.close(false); + defer this.close(true); err.ensureStillAlive(); @@ -579,7 +621,7 @@ pub const FSWatcher = struct { } pub fn emitError(this: *JSObject, err: string) void { if (this.closed) return; - defer this.close(false); + defer this.close(true); if (this.globalThis) |globalThis| { if (this.js_this != .zero) { @@ -665,35 +707,46 @@ pub const FSWatcher = struct { signal.detach(this); } this.closed = true; - this.manager.stop(true); - this.js_this.unprotect(); - this.unref(); if (emitEvent) { this.emit("", "close"); } + + this.manager.stop(true); + + // will actually unref the watcher only nextTick so we receive close message + if (bun.uws.Loop.get()) |loop| { + loop.nextTick(*JSObject, this, JSObject._detachNextTick); + } } } + fn _detachNextTick(this: *JSObject) void { + this.js_this.unprotect(); + this.js_this = .zero; + this.unref(); + } + pub fn doClose(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { this.close(true); return JSC.JSValue.jsUndefined(); } pub fn finalize(this: *JSObject) callconv(.C) void { + this.unref(); + + if (this.js_this != .zero) { + this.js_this.unprotect(); + this.js_this = .zero; + } + this.manager.ctx.js_watcher = null; - this.js_this = .zero; this.globalThis = null; - this.manager.stop(true); if (this.signal) |signal| { this.signal = null; signal.detach(this); } - if (this.persistent) { - this.persistent = false; - this.poll_ref.unref(this.manager.ctx.ctx); - } this.manager.ctx.deinit(); this.manager.deinit(true); bun.default_allocator.destroy(this); @@ -778,16 +831,18 @@ pub const FSWatcher = struct { ctx.watcher_instance = fs_watcher; if (fs_type.is_file) { - var file_path_clone = bun.default_allocator.alloc(u8, file_path.len) catch unreachable; - bun.copy(u8, file_path_clone, file_path); - ctx.file_paths.push(bun.default_allocator, file_path_clone) catch unreachable; + var file_path_clone = bun.default_allocator.dupe(u8, file_path) catch unreachable; + + ctx.entry_path = file_path_clone; + ctx.entry_dir = std.fs.path.dirname(file_path_clone) orelse file_path_clone; + fs_watcher.addFile(fs_type.fd, file_path_clone, FSWatcher.Watcher.getHash(file_path), options.Loader.file, 0, null, false) catch |err| { ctx.deinit(); fs_watcher.deinit(true); return err; }; } else { - addDirectory(ctx, fs_watcher, fs_type.fd, file_path, args.recursive, &buf) catch |err| { + addDirectory(ctx, fs_watcher, fs_type.fd, file_path, args.recursive, &buf, true) catch |err| { ctx.deinit(); fs_watcher.deinit(true); return err; diff --git a/src/js/node/fs.js b/src/js/node/fs.js index 8631c8bc2da4..04cd352df751 100644 --- a/src/js/node/fs.js +++ b/src/js/node/fs.js @@ -35,7 +35,20 @@ class FSWatcher extends EventEmitter { } this.#listener = listener; - this.#watcher = fs.watch(path, options, this.#onEvent.bind(this)); + try { + this.#watcher = fs.watch(path, options || {}, this.#onEvent.bind(this)); + } catch (e) { + if (!e.message?.startsWith("FileNotFound")) { + throw e; + } + const notFound = new Error(`ENOENT: no such file or directory, watch '${path}'`); + notFound.code = "ENOENT"; + notFound.errno = -2; + notFound.path = path; + notFound.syscall = "watch"; + notFound.filename = path; + throw notFound; + } } #onEvent(eventType, filenameOrError) { diff --git a/src/watcher.zig b/src/watcher.zig index 64a89bbe698e..258fde1c5eef 100644 --- a/src/watcher.zig +++ b/src/watcher.zig @@ -411,12 +411,12 @@ pub fn NewWatcher(comptime ContextType: type) type { } pub fn deinit(this: *Watcher, close_descriptors: bool) void { - this.running = false; this.close_descriptors = close_descriptors; if (this.watchloop_handle != null) { + this.running = false; std.Thread.join(this.thread); } else { - if (this.close_descriptors) { + if (this.close_descriptors and this.running) { const fds = this.watchlist.items(.fd); for (fds) |fd| { std.os.close(fd); diff --git a/test/js/node/watch/fixtures/persistent.js b/test/js/node/watch/fixtures/persistent.js new file mode 100644 index 000000000000..72a2b6564e0e --- /dev/null +++ b/test/js/node/watch/fixtures/persistent.js @@ -0,0 +1,5 @@ +import fs from "fs"; +fs.watch(import.meta.path, { persistent: false, signal: AbortSignal.timeout(4000) }).on("error", err => { + console.error(err.message); + process.exit(1); +}); diff --git a/test/js/node/watch/fixtures/relative.js b/test/js/node/watch/fixtures/relative.js new file mode 100644 index 000000000000..4abad56854c6 --- /dev/null +++ b/test/js/node/watch/fixtures/relative.js @@ -0,0 +1,23 @@ +import fs from "fs"; +const watcher = fs.watch("relative.txt", { signal: AbortSignal.timeout(2000) }); + +watcher.on("change", function (event, filename) { + if (filename !== "relative.txt" && event !== "change") { + console.error("fail"); + clearInterval(interval); + watcher.close(); + process.exit(1); + } else { + clearInterval(interval); + watcher.close(); + } +}); +watcher.on("error", err => { + clearInterval(interval); + console.error(err.message); + process.exit(1); +}); + +const interval = setInterval(() => { + fs.writeFileSync("relative.txt", "world"); +}, 1000); diff --git a/test/js/node/watch/fs.watch.test.js b/test/js/node/watch/fs.watch.test.js new file mode 100644 index 000000000000..cd7ac916856e --- /dev/null +++ b/test/js/node/watch/fs.watch.test.js @@ -0,0 +1,124 @@ +import fs from "fs"; +import path from "path"; +import { tempDirWithFiles, bunRun, bunRunAsScript } from "harness"; +import { describe, expect, test } from "bun:test"; +import exp from "constants"; + +// Because macOS (and possibly other operating systems) can return a watcher +// before it is actually watching, we need to repeat the operation to avoid +// a race condition. +function repeat(fn) { + setImmediate(fn); + const interval = setInterval(fn, 2000); + return interval; +} +const testDir = tempDirWithFiles("watch", { + "watch.txt": "hello", + "relative.txt": "hello", + "abort.txt": "hello", +}); + +describe("fs.watch", () => { + // TODO: sometimes Subprocess never ends and test hangs + test.skip("non-persistent watcher should not block the event loop", done => { + try { + // https://github.com/joyent/node/issues/2293 - non-persistent watcher should not block the event loop + bunRun(path.join(import.meta.dir, "fixtures", "persistent.js")); + done(); + } catch (e) { + done(e); + } + }); + // TODO: sometimes Subprocess never ends and test hangs + test.skip("should work with relative files", done => { + try { + bunRunAsScript(testDir, path.join(import.meta.dir, "fixtures", "relative.js")); + done(); + } catch (e) { + done(e); + } + }); + + test("should emit event when file is deleted", done => { + const testsubdir = tempDirWithFiles("subdir", { + "deleted.txt": "hello", + }); + const filepath = path.join(testsubdir, "deleted.txt"); + const watcher = fs.watch(testsubdir, function (event, filename) { + expect(event).toBe("rename"); + expect(filename).toBe("deleted.txt"); + + clearInterval(interval); + watcher.close(); + done(); + }); + + const interval = repeat(() => { + fs.rmSync(filepath, { force: true }); + const fd = fs.openSync(filepath, "w"); + fs.closeSync(fd); + }); + }, 4000); + + test("should emit 'change' event when file is modified", done => { + const filepath = path.join(testDir, "watch.txt"); + + const watcher = fs.watch(filepath); + watcher.on("change", function (event, filename) { + expect(event).toBe("change"); + expect(filename).toBe("watch.txt"); + clearInterval(interval); + watcher.close(); + done(); + }); + + const interval = repeat(() => { + fs.writeFileSync(filepath, "world"); + }); + }, 4000); + + test("Signal aborted after creating the watcher", done => { + const filepath = path.join(testDir, "abort.txt"); + + const ac = new AbortController(); + const watcher = fs.watch(filepath, { signal: ac.signal }); + watcher.once("close", () => done()); + setImmediate(() => ac.abort()); + }, 1000); + + test("Signal aborted before creating the watcher", done => { + const filepath = path.join(testDir, "abort.txt"); + + const signal = AbortSignal.abort(); + const watcher = fs.watch(filepath, { signal }); + watcher.once("close", () => done()); + }, 1000); + + test("should error on invalid path", done => { + try { + fs.watch(path.join(testDir, "404.txt")); + } catch (err) { + expect(err).toBeInstanceOf(Error); + expect(err.code).toBe("ENOENT"); + done(); + } + }); + + test.todo( + "should close when root is deleted", + done => { + const root = path.join(testDir, "watched-directory"); + try { + fs.mkdirSync(root); + } catch {} + const watcher = fs.watch(root, { signal: AbortSignal.timeout(3000) }); + watcher.on("close", () => done()); + watcher.on("error", () => done(err)); + + setTimeout(() => { + fs.mkdirSync(root); + }, 1000); + }, + 4000, + ); +}); From 5c8983f0702a7a3a1bb27dd2b7ed014a4e243f9f Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Fri, 16 Jun 2023 02:50:31 -0300 Subject: [PATCH 07/18] fix some encoding support for watch --- src/bun.js/node/node_fs_watcher.zig | 6 ++- src/bun.js/webcore/encoding.zig | 13 +++++ test/js/node/watch/fs.watch.test.js | 82 ++++++++++++++++++++++++----- 3 files changed, 85 insertions(+), 16 deletions(-) diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig index cf6834ea9f8a..bdd511f6857e 100644 --- a/src/bun.js/node/node_fs_watcher.zig +++ b/src/bun.js/node/node_fs_watcher.zig @@ -3,6 +3,8 @@ const JSC = @import("root").bun.JSC; const bun = @import("root").bun; const Fs = @import("../../fs.zig"); const Path = @import("../../resolver/resolve_path.zig"); +const Encoder = JSC.WebCore.Encoder; + const VirtualMachine = JSC.VirtualMachine; const EventLoop = JSC.EventLoop; const PathLike = JSC.Node.PathLike; @@ -650,8 +652,8 @@ pub const FSWatcher = struct { else if (this.encoding == .utf8) { filename = JSC.ZigString.fromUTF8(file_name).toValueGC(globalThis); } else { - // TODO: this is correct? - filename = JSC.ZigString.fromUTF8(JSC.ZigString.fromUTF8(file_name).encode(this.encoding)).toValue(globalThis); + // convert to desired encoding + filename = Encoder.toStringAtRuntime(file_name.ptr, file_name.len, globalThis, this.encoding); } } var args = [_]JSC.JSValue{ diff --git a/src/bun.js/webcore/encoding.zig b/src/bun.js/webcore/encoding.zig index 5c8221128ceb..df2aaf45605c 100644 --- a/src/bun.js/webcore/encoding.zig +++ b/src/bun.js/webcore/encoding.zig @@ -802,7 +802,20 @@ pub const Encoder = struct { // pub fn writeUTF16AsUTF8(utf16: [*]const u16, len: usize, to: [*]u8, to_len: usize) callconv(.C) i32 { // return @intCast(i32, strings.copyUTF16IntoUTF8(to[0..to_len], []const u16, utf16[0..len], true).written); // } + pub fn toStringAtRuntime(input: [*]const u8, len: usize, globalObject: *JSGlobalObject, encoding: JSC.Node.Encoding) JSValue { + return switch (encoding) { + .ucs2 => toString(input, len, globalObject, .utf16le), + .utf16le => toString(input, len, globalObject, .utf16le), + .utf8 => toString(input, len, globalObject, .utf8), + .ascii => toString(input, len, globalObject, .ascii), + .hex => toString(input, len, globalObject, .hex), + .base64 => toString(input, len, globalObject, .base64), + .base64url => toString(input, len, globalObject, .base64url), + // treat everything else as utf8 + else => toString(input, len, globalObject, .utf8), + }; + } pub fn toString(input_ptr: [*]const u8, len: usize, global: *JSGlobalObject, comptime encoding: JSC.Node.Encoding) JSValue { if (len == 0) return ZigString.Empty.toValue(global); diff --git a/test/js/node/watch/fs.watch.test.js b/test/js/node/watch/fs.watch.test.js index cd7ac916856e..5283142e07fe 100644 --- a/test/js/node/watch/fs.watch.test.js +++ b/test/js/node/watch/fs.watch.test.js @@ -1,9 +1,8 @@ import fs from "fs"; import path from "path"; +import { createTest } from "node-harness"; import { tempDirWithFiles, bunRun, bunRunAsScript } from "harness"; -import { describe, expect, test } from "bun:test"; -import exp from "constants"; - +const { describe, expect, test, createDoneDotAll } = createTest(import.meta.path); // Because macOS (and possibly other operating systems) can return a watcher // before it is actually watching, we need to repeat the operation to avoid // a race condition. @@ -12,10 +11,12 @@ function repeat(fn) { const interval = setInterval(fn, 2000); return interval; } +const encodingFileName = `新建文夹件.txt`; const testDir = tempDirWithFiles("watch", { "watch.txt": "hello", "relative.txt": "hello", "abort.txt": "hello", + [encodingFileName]: "hello", }); describe("fs.watch", () => { @@ -45,12 +46,16 @@ describe("fs.watch", () => { }); const filepath = path.join(testsubdir, "deleted.txt"); const watcher = fs.watch(testsubdir, function (event, filename) { - expect(event).toBe("rename"); - expect(filename).toBe("deleted.txt"); - - clearInterval(interval); - watcher.close(); - done(); + try { + expect(event).toBe("rename"); + expect(filename).toBe("deleted.txt"); + done(); + } catch (e) { + done(e); + } finally { + clearInterval(interval); + watcher.close(); + } }); const interval = repeat(() => { @@ -65,11 +70,16 @@ describe("fs.watch", () => { const watcher = fs.watch(filepath); watcher.on("change", function (event, filename) { - expect(event).toBe("change"); - expect(filename).toBe("watch.txt"); - clearInterval(interval); - watcher.close(); - done(); + try { + expect(event).toBe("change"); + expect(filename).toBe("watch.txt"); + done(); + } catch (e) { + done(e); + } finally { + clearInterval(interval); + watcher.close(); + } }); const interval = repeat(() => { @@ -97,13 +107,57 @@ describe("fs.watch", () => { test("should error on invalid path", done => { try { fs.watch(path.join(testDir, "404.txt")); + done(new Error("should not reach here")); } catch (err) { expect(err).toBeInstanceOf(Error); expect(err.code).toBe("ENOENT"); + expect(err.syscall).toBe("watch"); done(); } }); + // TODO: these encodings are broken + // "utf16le", "ucs2", "ucs-2", "latin1", "binary", + + test("should work with different encodings", done => { + const createDone = createDoneDotAll(err => { + watchers.forEach(w => w.close()); + clearInterval(interval); + done(err); + }); + + const watchers = []; + const filepath = path.join(testDir, encodingFileName); + + ["utf8", "buffer", "hex", "ascii", "utf-8", "base64"].forEach(name => { + const encodeDone = createDone(); + const encoded_filename = + name !== "buffer" ? Buffer.from(encodingFileName, "utf8").toString(name) : Buffer.from(encodingFileName); + watchers.push( + fs.watch(filepath, { encoding: name }, (event, filename) => { + try { + expect(event).toBe("change"); + + if (name !== "buffer") { + expect(filename).toBe(encoded_filename); + } else { + expect(filename).toBeInstanceOf(Buffer); + expect(filename.toString("utf8")).toBe(encodingFileName); + } + + encodeDone(); + } catch (e) { + encodeDone(e); + } + }), + ); + }); + + const interval = repeat(() => { + fs.writeFileSync(filepath, "world"); + }); + }, 30000); + test.todo( "should close when root is deleted", done => { From 8d18f47f4b3ec9e152823178aad54ddab0dacddd Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Fri, 16 Jun 2023 04:16:06 -0300 Subject: [PATCH 08/18] fix rename event --- src/bun.js/node/node_fs_watcher.zig | 18 ++--- test/js/node/watch/fs.watch.test.js | 107 ++++++++++++++++++++++++++-- 2 files changed, 110 insertions(+), 15 deletions(-) diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig index bdd511f6857e..e996b07d9dff 100644 --- a/src/bun.js/node/node_fs_watcher.zig +++ b/src/bun.js/node/node_fs_watcher.zig @@ -156,15 +156,15 @@ pub const FSWatcher = struct { ) void); fn addDirectory(ctx: *FSWatcher, fs_watcher: *FSWatcher.Watcher, fd: StoredFileDescriptorType, file_path: string, recursive: bool, buf: *[bun.MAX_PATH_BYTES]u8, is_entry_path: bool) !void { - var file_path_clone = bun.default_allocator.dupe(u8, file_path) catch unreachable; + var dir_path_clone = bun.default_allocator.dupe(u8, file_path) catch unreachable; if (is_entry_path) { - ctx.entry_path = file_path_clone; - ctx.entry_dir = file_path_clone; + ctx.entry_path = dir_path_clone; + ctx.entry_dir = dir_path_clone; } else { - ctx.file_paths.push(bun.default_allocator, file_path_clone) catch unreachable; + ctx.file_paths.push(bun.default_allocator, dir_path_clone) catch unreachable; } - fs_watcher.addDirectory(fd, file_path_clone, FSWatcher.Watcher.getHash(file_path), false) catch |err| { + fs_watcher.addDirectory(fd, dir_path_clone, FSWatcher.Watcher.getHash(file_path), false) catch |err| { ctx.deinit(); fs_watcher.deinit(true); return err; @@ -179,7 +179,7 @@ pub const FSWatcher = struct { fs_watcher.deinit(true); return err; }) |entry| { - var parts = [2]string{ file_path_clone, entry.name }; + var parts = [2]string{ dir_path_clone, entry.name }; var entry_path = Path.joinAbsStringBuf( Fs.FileSystem.instance.topLevelDirWithoutTrailingSlash(), buf, @@ -198,7 +198,7 @@ pub const FSWatcher = struct { }; if (fs_info.is_file) { - file_path_clone = bun.default_allocator.dupe(u8, entry_path) catch unreachable; + const file_path_clone = bun.default_allocator.dupe(u8, entry_path) catch unreachable; ctx.file_paths.push(bun.default_allocator, file_path_clone) catch unreachable; @@ -295,7 +295,7 @@ pub const FSWatcher = struct { var file_hash: FSWatcher.Watcher.HashType = FSWatcher.Watcher.getHash(file_path); if (event.op.write or event.op.delete or event.op.rename) { - const event_type: FSWatchTask.EventType = if (event.op.delete or event.op.rename) .rename else .change; + const event_type: FSWatchTask.EventType = if (event.op.delete or event.op.rename or event.op.move_to) .rename else .change; // 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 != file_hash) { this.last_change_event.time_stamp = time_stamp; @@ -369,7 +369,7 @@ pub const FSWatcher = struct { }; // skip consecutive duplicates - const event_type: FSWatchTask.EventType = if (event.op.delete or event.op.rename or event.op.move_to) .rename else .change; + const event_type: FSWatchTask.EventType = .rename; // renaming folders, creating folder or files will be always be rename 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 != file_hash) { const relative_path = bun.default_allocator.dupe(u8, relative_slice) catch unreachable; diff --git a/test/js/node/watch/fs.watch.test.js b/test/js/node/watch/fs.watch.test.js index 5283142e07fe..bf7e894dc302 100644 --- a/test/js/node/watch/fs.watch.test.js +++ b/test/js/node/watch/fs.watch.test.js @@ -2,13 +2,15 @@ import fs from "fs"; import path from "path"; import { createTest } from "node-harness"; import { tempDirWithFiles, bunRun, bunRunAsScript } from "harness"; +import { pathToFileURL } from "bun"; + const { describe, expect, test, createDoneDotAll } = createTest(import.meta.path); // Because macOS (and possibly other operating systems) can return a watcher // before it is actually watching, we need to repeat the operation to avoid // a race condition. function repeat(fn) { - setImmediate(fn); - const interval = setInterval(fn, 2000); + setTimeout(fn, 100); + const interval = setInterval(fn, 500); return interval; } const encodingFileName = `新建文夹件.txt`; @@ -16,6 +18,7 @@ const testDir = tempDirWithFiles("watch", { "watch.txt": "hello", "relative.txt": "hello", "abort.txt": "hello", + "url.txt": "hello", [encodingFileName]: "hello", }); @@ -40,6 +43,67 @@ describe("fs.watch", () => { } }); + test("add file/folder to folder", done => { + let count = 0; + const root = path.join(testDir, "add-directory"); + try { + fs.mkdirSync(root); + } catch {} + const watcher = fs.watch(root, { signal: AbortSignal.timeout(3000) }); + watcher.on("change", (event, filename) => { + count++; + try { + expect(event).toBe("rename"); + expect(["new-file.txt", "new-folder.txt"]).toContain(filename); + if (count >= 2) done(); + } catch (e) { + done(e); + } + watcher.close(); + }); + watcher.on("error", () => done(err)); + watcher.on("close", () => { + clearInterval(interval); + }); + + const interval = repeat(() => { + fs.writeFileSync(path.join(root, "new-file.txt"), "hello"); + fs.mkdirSync(path.join(root, "new-folder.txt")); + fs.rmdirSync(path.join(root, "new-folder.txt")); + }); + }, 4000); + + test("add file/folder to subfolder", done => { + let count = 0; + const root = path.join(testDir, "add-directory"); + try { + fs.mkdirSync(root); + } catch {} + const subfolder = path.join(root, "subfolder"); + fs.mkdirSync(subfolder); + const watcher = fs.watch(root, { recursive: true, signal: AbortSignal.timeout(3000) }); + watcher.on("change", (event, filename) => { + count++; + try { + expect(event).toBe("rename"); + expect(["new-file.txt", "new-folder.txt"]).toContain(path.basename(filename)); + if (count >= 2) done(); + } catch (e) { + done(e); + } + watcher.close(); + }); + watcher.on("error", () => done(err)); + watcher.on("close", () => { + clearInterval(interval); + }); + + const interval = repeat(() => { + fs.writeFileSync(path.join(subfolder, "new-file.txt"), "hello"); + fs.mkdirSync(path.join(subfolder, "new-folder.txt")); + fs.rmdirSync(path.join(subfolder, "new-folder.txt")); + }); + }, 4000); test("should emit event when file is deleted", done => { const testsubdir = tempDirWithFiles("subdir", { "deleted.txt": "hello", @@ -116,10 +180,13 @@ describe("fs.watch", () => { } }); - // TODO: these encodings are broken - // "utf16le", "ucs2", "ucs-2", "latin1", "binary", + const encodings = ["utf8", "buffer", "hex", "ascii", "utf-8", "base64"]; + const brokenEncodings = ["utf16le", "ucs2", "ucs-2", "latin1", "binary"]; - test("should work with different encodings", done => { + test.todo(`should work with encodings ${brokenEncodings.join(", ")}`, done => { + done(new Error("TODO: implement")); + }); + test(`should work with encodings ${encodings.join(", ")}`, done => { const createDone = createDoneDotAll(err => { watchers.forEach(w => w.close()); clearInterval(interval); @@ -129,7 +196,7 @@ describe("fs.watch", () => { const watchers = []; const filepath = path.join(testDir, encodingFileName); - ["utf8", "buffer", "hex", "ascii", "utf-8", "base64"].forEach(name => { + encodings.forEach(name => { const encodeDone = createDone(); const encoded_filename = name !== "buffer" ? Buffer.from(encodingFileName, "utf8").toString(name) : Buffer.from(encodingFileName); @@ -158,6 +225,34 @@ describe("fs.watch", () => { }); }, 30000); + test.todo( + "should work with url", + done => { + const filepath = path.join(testDir, "url.txt"); + try { + const watcher = fs.watch(pathToFileURL(filepath)); + watcher.on("change", function (event, filename) { + try { + expect(event).toBe("change"); + expect(filename).toBe("watch.txt"); + done(); + } catch (e) { + done(e); + } finally { + clearInterval(interval); + watcher.close(); + } + }); + + const interval = repeat(() => { + fs.writeFileSync(filepath, "world"); + }); + } catch (e) { + done(e); + } + }, + 4000, + ); test.todo( "should close when root is deleted", done => { From ff262e1fc96f45225957b0de638ff0bdfb995de0 Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Fri, 16 Jun 2023 08:20:31 -0300 Subject: [PATCH 09/18] fixup --- src/bun.js/node/node_fs_watcher.zig | 75 ++--- src/js/out/modules/node/fs.js | 56 +++- src/js/out/modules/node/fs.promises.js | 2 +- src/watcher.zig | 33 ++- test/js/node/watch/fixtures/close.js | 7 + test/js/node/watch/fixtures/relative.js | 2 +- test/js/node/watch/fixtures/unref.js | 7 + test/js/node/watch/fs.watch.test.js | 365 +++++++++++++++++------- 8 files changed, 385 insertions(+), 162 deletions(-) create mode 100644 test/js/node/watch/fixtures/close.js create mode 100644 test/js/node/watch/fixtures/unref.js diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig index e996b07d9dff..6bf5e4cd701e 100644 --- a/src/bun.js/node/node_fs_watcher.zig +++ b/src/bun.js/node/node_fs_watcher.zig @@ -71,6 +71,7 @@ pub const FSWatcher = struct { rename, change, @"error", + abort, }; pub const Entry = struct { @@ -112,6 +113,9 @@ pub const FSWatcher = struct { // file_path is the error message in this case js_watcher.emitError(entry.file_path); }, + .abort => { + js_watcher.emitIfAborted(); + }, } } } @@ -155,8 +159,8 @@ pub const FSWatcher = struct { specifier: []const u8, ) void); - fn addDirectory(ctx: *FSWatcher, fs_watcher: *FSWatcher.Watcher, fd: StoredFileDescriptorType, file_path: string, recursive: bool, buf: *[bun.MAX_PATH_BYTES]u8, is_entry_path: bool) !void { - var dir_path_clone = bun.default_allocator.dupe(u8, file_path) catch unreachable; + fn addDirectory(ctx: *FSWatcher, fs_watcher: *FSWatcher.Watcher, fd: StoredFileDescriptorType, file_path: string, recursive: bool, buf: *[bun.MAX_PATH_BYTES + 1]u8, is_entry_path: bool) !void { + var dir_path_clone = bun.default_allocator.dupeZ(u8, file_path) catch unreachable; if (is_entry_path) { ctx.entry_path = dir_path_clone; @@ -187,7 +191,6 @@ pub const FSWatcher = struct { .auto, ); - buf[entry_path.len + 1] = 0; buf[entry_path.len] = 0; var entry_path_z = buf[0..entry_path.len :0]; @@ -198,7 +201,7 @@ pub const FSWatcher = struct { }; if (fs_info.is_file) { - const file_path_clone = bun.default_allocator.dupe(u8, entry_path) catch unreachable; + const file_path_clone = bun.default_allocator.dupeZ(u8, entry_path) catch unreachable; ctx.file_paths.push(bun.default_allocator, file_path_clone) catch unreachable; @@ -309,7 +312,6 @@ pub const FSWatcher = struct { const relative_path = bun.default_allocator.dupe(u8, relative_slice) catch unreachable; - // if is deleted we actually emit as rename like node.js current_task.append(relative_path, event_type, true); } } @@ -579,24 +581,30 @@ pub const FSWatcher = struct { obj.js_this.protect(); if (signal) |s| { - obj.signal = s.ref(); + // already aborted? if (s.aborted()) { - if (bun.uws.Loop.get()) |loop| { - loop.nextTick(*JSObject, obj, JSObject._abortNextTick); - } + obj.signal = s.ref(); + // abort next tick + var current_task: FSWatchTask = .{ + .ctx = manager.ctx, + }; + current_task.append("", .abort, false); + current_task.enqueue(); } else { // watch for abortion - _ = s.listen(JSObject, obj, JSObject.emitAbort); + obj.signal = s.ref().listen(JSObject, obj, JSObject.emitAbort); } } return obj; } - fn _abortNextTick(this: *JSObject) void { + pub fn emitIfAborted(this: *JSObject) void { if (this.signal) |s| { - const err = s.abortReason(); - this.emitAbort(err); + if (s.aborted()) { + const err = s.abortReason(); + this.emitAbort(err); + } } } @@ -713,27 +721,11 @@ pub const FSWatcher = struct { this.emit("", "close"); } - this.manager.stop(true); - - // will actually unref the watcher only nextTick so we receive close message - if (bun.uws.Loop.get()) |loop| { - loop.nextTick(*JSObject, this, JSObject._detachNextTick); - } + this.detach(); } } - fn _detachNextTick(this: *JSObject) void { - this.js_this.unprotect(); - this.js_this = .zero; - this.unref(); - } - - pub fn doClose(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { - this.close(true); - return JSC.JSValue.jsUndefined(); - } - - pub fn finalize(this: *JSObject) callconv(.C) void { + pub fn detach(this: *JSObject) void { this.unref(); if (this.js_this != .zero) { @@ -741,7 +733,6 @@ pub const FSWatcher = struct { this.js_this = .zero; } - this.manager.ctx.js_watcher = null; this.globalThis = null; if (this.signal) |signal| { @@ -749,8 +740,21 @@ pub const FSWatcher = struct { signal.detach(this); } + this.manager.ctx.js_watcher = null; this.manager.ctx.deinit(); this.manager.deinit(true); + } + + pub fn doClose(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + this.close(true); + return JSC.JSValue.jsUndefined(); + } + + pub fn finalize(this: *JSObject) callconv(.C) void { + if (!this.closed) { + this.detach(); + } + bun.default_allocator.destroy(this); } }; @@ -782,7 +786,7 @@ pub const FSWatcher = struct { result.is_file = false; }, else => { - const file = try bun.openFileForPath(absolute_path_z); + const file = try std.fs.openFileAbsoluteZ(absolute_path_z, .{ .mode = .read_only }); result.fd = file.handle; result.is_file = true; }, @@ -791,7 +795,7 @@ pub const FSWatcher = struct { } pub fn init(args: Arguments) !*FSWatcher { - var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + var buf: [bun.MAX_PATH_BYTES + 1]u8 = undefined; var parts = [_]string{ args.path.slice(), @@ -804,7 +808,6 @@ pub const FSWatcher = struct { .auto, ); - buf[file_path.len + 1] = 0; buf[file_path.len] = 0; var file_path_z = buf[0..file_path.len :0]; @@ -833,7 +836,7 @@ pub const FSWatcher = struct { ctx.watcher_instance = fs_watcher; if (fs_type.is_file) { - var file_path_clone = bun.default_allocator.dupe(u8, file_path) catch unreachable; + var file_path_clone = bun.default_allocator.dupeZ(u8, file_path) catch unreachable; ctx.entry_path = file_path_clone; ctx.entry_dir = std.fs.path.dirname(file_path_clone) orelse file_path_clone; diff --git a/src/js/out/modules/node/fs.js b/src/js/out/modules/node/fs.js index cc1e14d2b718..605eda83d79b 100644 --- a/src/js/out/modules/node/fs.js +++ b/src/js/out/modules/node/fs.js @@ -1,3 +1,4 @@ +var {EventEmitter } = import.meta.require("node:stream"); var callbackify = function(fsFunction, args) { try { const result = fsFunction.apply(fs, args.slice(0, args.length - 1)), callback = args[args.length - 1]; @@ -16,7 +17,53 @@ function createWriteStream(path, options) { return new WriteStream(path, options); } var { direct, isPromise, isCallable } = import.meta.primordials, promises = import.meta.require("node:fs/promises"), { Readable, NativeWritable, _getNativeReadableStreamPrototype, eos: eos_ } = import.meta.require("node:stream"), NativeReadable = _getNativeReadableStreamPrototype(2, Readable), fs = Bun.fs(), debug = process.env.DEBUG ? console.log : () => { -}, access = function access2(...args) { +}; + +class FSWatcher extends EventEmitter { + #watcher; + #listener; + constructor(path, options, listener) { + super(); + if (path instanceof URL) + throw new TypeError("Watch URLs are not supported yet"); + else if (Buffer.isBuffer(path)) + path = path.toString(); + else if (typeof path !== "string") + throw new TypeError("Expected path to be a string or Buffer"); + if (typeof options === "function") + listener = options, options = {}; + else if (typeof options === "string") + options = { encoding: options }; + if (typeof listener !== "function") + listener = () => { + }; + this.#listener = listener; + try { + this.#watcher = fs.watch(path, options || {}, this.#onEvent.bind(this)); + } catch (e) { + if (!e.message?.startsWith("FileNotFound")) + throw e; + const notFound = new Error(`ENOENT: no such file or directory, watch '${path}'`); + throw notFound.code = "ENOENT", notFound.errno = -2, notFound.path = path, notFound.syscall = "watch", notFound.filename = path, notFound; + } + } + #onEvent(eventType, filenameOrError) { + if (eventType === "error" || eventType === "close") + this.emit(eventType, filenameOrError); + else + this.emit("change", eventType, filenameOrError), this.#listener(eventType, filenameOrError); + } + close() { + this.#watcher?.close(), this.#watcher = null; + } + ref() { + this.#watcher?.ref(); + } + unref() { + this.#watcher?.unref(); + } +} +var access = function access2(...args) { callbackify(fs.accessSync, args); }, appendFile = function appendFile2(...args) { callbackify(fs.appendFileSync, args); @@ -88,7 +135,9 @@ var { direct, isPromise, isCallable } = import.meta.primordials, promises = impo callbackify(fs.utimesSync, args); }, lutimes = function lutimes2(...args) { callbackify(fs.lutimesSync, args); -}, accessSync = fs.accessSync.bind(fs), appendFileSync = fs.appendFileSync.bind(fs), closeSync = fs.closeSync.bind(fs), copyFileSync = fs.copyFileSync.bind(fs), existsSync = fs.existsSync.bind(fs), chownSync = fs.chownSync.bind(fs), chmodSync = fs.chmodSync.bind(fs), fchmodSync = fs.fchmodSync.bind(fs), fchownSync = fs.fchownSync.bind(fs), fstatSync = fs.fstatSync.bind(fs), fsyncSync = fs.fsyncSync.bind(fs), ftruncateSync = fs.ftruncateSync.bind(fs), futimesSync = fs.futimesSync.bind(fs), lchmodSync = fs.lchmodSync.bind(fs), lchownSync = fs.lchownSync.bind(fs), linkSync = fs.linkSync.bind(fs), lstatSync = fs.lstatSync.bind(fs), mkdirSync = fs.mkdirSync.bind(fs), mkdtempSync = fs.mkdtempSync.bind(fs), openSync = fs.openSync.bind(fs), readSync = fs.readSync.bind(fs), writeSync = fs.writeSync.bind(fs), readdirSync = fs.readdirSync.bind(fs), readFileSync = fs.readFileSync.bind(fs), writeFileSync = fs.writeFileSync.bind(fs), readlinkSync = fs.readlinkSync.bind(fs), realpathSync = fs.realpathSync.bind(fs), renameSync = fs.renameSync.bind(fs), statSync = fs.statSync.bind(fs), symlinkSync = fs.symlinkSync.bind(fs), truncateSync = fs.truncateSync.bind(fs), unlinkSync = fs.unlinkSync.bind(fs), utimesSync = fs.utimesSync.bind(fs), lutimesSync = fs.lutimesSync.bind(fs), rmSync = fs.rmSync.bind(fs), rmdirSync = fs.rmdirSync.bind(fs), Dirent = fs.Dirent, Stats = fs.Stats, promises = import.meta.require("node:fs/promises"), readStreamPathFastPathSymbol = Symbol.for("Bun.Node.readStreamPathFastPath"), readStreamSymbol = Symbol.for("Bun.NodeReadStream"), readStreamPathOrFdSymbol = Symbol.for("Bun.NodeReadStreamPathOrFd"), writeStreamSymbol = Symbol.for("Bun.NodeWriteStream"), writeStreamPathFastPathSymbol = Symbol.for("Bun.NodeWriteStreamFastPath"), writeStreamPathFastPathCallSymbol = Symbol.for("Bun.NodeWriteStreamFastPathCall"), kIoDone = Symbol.for("kIoDone"), defaultReadStreamOptions = { +}, accessSync = fs.accessSync.bind(fs), appendFileSync = fs.appendFileSync.bind(fs), closeSync = fs.closeSync.bind(fs), copyFileSync = fs.copyFileSync.bind(fs), existsSync = fs.existsSync.bind(fs), chownSync = fs.chownSync.bind(fs), chmodSync = fs.chmodSync.bind(fs), fchmodSync = fs.fchmodSync.bind(fs), fchownSync = fs.fchownSync.bind(fs), fstatSync = fs.fstatSync.bind(fs), fsyncSync = fs.fsyncSync.bind(fs), ftruncateSync = fs.ftruncateSync.bind(fs), futimesSync = fs.futimesSync.bind(fs), lchmodSync = fs.lchmodSync.bind(fs), lchownSync = fs.lchownSync.bind(fs), linkSync = fs.linkSync.bind(fs), lstatSync = fs.lstatSync.bind(fs), mkdirSync = fs.mkdirSync.bind(fs), mkdtempSync = fs.mkdtempSync.bind(fs), openSync = fs.openSync.bind(fs), readSync = fs.readSync.bind(fs), writeSync = fs.writeSync.bind(fs), readdirSync = fs.readdirSync.bind(fs), readFileSync = fs.readFileSync.bind(fs), writeFileSync = fs.writeFileSync.bind(fs), readlinkSync = fs.readlinkSync.bind(fs), realpathSync = fs.realpathSync.bind(fs), renameSync = fs.renameSync.bind(fs), statSync = fs.statSync.bind(fs), symlinkSync = fs.symlinkSync.bind(fs), truncateSync = fs.truncateSync.bind(fs), unlinkSync = fs.unlinkSync.bind(fs), utimesSync = fs.utimesSync.bind(fs), lutimesSync = fs.lutimesSync.bind(fs), rmSync = fs.rmSync.bind(fs), rmdirSync = fs.rmdirSync.bind(fs), Dirent = fs.Dirent, Stats = fs.Stats, watch = function watch2(path, options, listener) { + return new FSWatcher(path, options, listener); +}, promises = import.meta.require("node:fs/promises"), readStreamPathFastPathSymbol = Symbol.for("Bun.Node.readStreamPathFastPath"), readStreamSymbol = Symbol.for("Bun.NodeReadStream"), readStreamPathOrFdSymbol = Symbol.for("Bun.NodeReadStreamPathOrFd"), writeStreamSymbol = Symbol.for("Bun.NodeWriteStream"), writeStreamPathFastPathSymbol = Symbol.for("Bun.NodeWriteStreamFastPath"), writeStreamPathFastPathCallSymbol = Symbol.for("Bun.NodeWriteStreamFastPathCall"), kIoDone = Symbol.for("kIoDone"), defaultReadStreamOptions = { file: void 0, fd: void 0, flags: "r", @@ -590,6 +639,8 @@ var fs_default = { writeSync, WriteStream, ReadStream, + watch, + FSWatcher, [Symbol.for("::bunternal::")]: { ReadStreamClass, WriteStreamClass @@ -600,6 +651,7 @@ export { writeFileSync, writeFile, write, + watch, utimesSync, utimes, unlinkSync, diff --git a/src/js/out/modules/node/fs.promises.js b/src/js/out/modules/node/fs.promises.js index 2780ff16621f..ef33307713c5 100644 --- a/src/js/out/modules/node/fs.promises.js +++ b/src/js/out/modules/node/fs.promises.js @@ -1 +1 @@ -var D=Bun.fs(),B="::bunternal::",E={[B]:(S)=>{var b={[B]:function(C,J,q){var z;try{z=S.apply(D,q),q=void 0}catch(A){q=void 0,J(A);return}C(z)}}[B];return async function(...C){return await new Promise((J,q)=>{process.nextTick(b,J,q,C)})}}}[B],G=E(D.accessSync),H=E(D.appendFileSync),I=E(D.closeSync),K=E(D.copyFileSync),L=E(D.existsSync),M=E(D.chownSync),N=E(D.chmodSync),O=E(D.fchmodSync),P=E(D.fchownSync),Q=E(D.fstatSync),R=E(D.fsyncSync),T=E(D.ftruncateSync),U=E(D.futimesSync),V=E(D.lchmodSync),W=E(D.lchownSync),X=E(D.linkSync),Y=E(D.lstatSync),Z=E(D.mkdirSync),_=E(D.mkdtempSync),$=E(D.openSync),x=E(D.readSync),j=E(D.writeSync),v=E(D.readdirSync),w=E(D.readFileSync),k=E(D.writeFileSync),F=E(D.readlinkSync),h=E(D.realpathSync),g=E(D.renameSync),u=E(D.statSync),d=E(D.symlinkSync),n=E(D.truncateSync),l=E(D.unlinkSync),a=E(D.utimesSync),c=E(D.lutimesSync),t=E(D.rmSync),y=E(D.rmdirSync),p={access:G,appendFile:H,close:I,copyFile:K,exists:L,chown:M,chmod:N,fchmod:O,fchown:P,fstat:Q,fsync:R,ftruncate:T,futimes:U,lchmod:V,lchown:W,link:X,lstat:Y,mkdir:Z,mkdtemp:_,open:$,read:x,write:j,readdir:v,readFile:w,writeFile:k,readlink:F,realpath:h,rename:g,stat:u,symlink:d,truncate:n,unlink:l,utimes:a,lutimes:c,rm:t,rmdir:y,constants,[Symbol.for("CommonJS")]:0};export{k as writeFile,j as write,a as utimes,l as unlink,n as truncate,d as symlink,u as stat,y as rmdir,t as rm,g as rename,h as realpath,F as readlink,v as readdir,w as readFile,x as read,$ as open,_ as mkdtemp,Z as mkdir,c as lutimes,Y as lstat,X as link,W as lchown,V as lchmod,U as futimes,T as ftruncate,R as fsync,Q as fstat,P as fchown,O as fchmod,L as exists,p as default,K as copyFile,I as close,M as chown,N as chmod,H as appendFile,G as access}; +function H(S,C={}){const J=[];if(S instanceof URL)throw new TypeError("Watch URLs are not supported yet");else if(Buffer.isBuffer(S))S=S.toString();else if(typeof S!=="string")throw new TypeError("Expected path to be a string or Buffer");let b=null;if(typeof C==="string")C={encoding:C};return D.watch(S,C||{},(q,z)=>{if(J.push({eventType:q,filename:z}),b){const A=b;b=null,A()}}),{async*[Symbol.asyncIterator](){let q=!1;while(!q){while(J.length){let z=J.shift();if(z.eventType==="close"){q=!0;break}if(z.eventType==="error")throw q=!0,z.filename;yield z}await new Promise((z)=>b=z)}}}}var D=Bun.fs(),B="::bunternal::",G={[B]:(S)=>{var C={[B]:function(J,b,q){var z;try{z=S.apply(D,q),q=void 0}catch(A){q=void 0,b(A);return}J(z)}}[B];return async function(...J){return await new Promise((b,q)=>{process.nextTick(C,b,q,J)})}}}[B],I=G(D.accessSync),K=G(D.appendFileSync),L=G(D.closeSync),M=G(D.copyFileSync),N=G(D.existsSync),O=G(D.chownSync),P=G(D.chmodSync),Q=G(D.fchmodSync),U=G(D.fchownSync),V=G(D.fstatSync),W=G(D.fsyncSync),X=G(D.ftruncateSync),Y=G(D.futimesSync),Z=G(D.lchmodSync),_=G(D.lchownSync),$=G(D.linkSync),T=G(D.lstatSync),E=G(D.mkdirSync),j=G(D.mkdtempSync),R=G(D.openSync),k=G(D.readSync),x=G(D.writeSync),F=G(D.readdirSync),u=G(D.readFileSync),w=G(D.writeFileSync),g=G(D.readlinkSync),h=G(D.realpathSync),d=G(D.renameSync),c=G(D.statSync),v=G(D.symlinkSync),a=G(D.truncateSync),y=G(D.unlinkSync),l=G(D.utimesSync),t=G(D.lutimesSync),p=G(D.rmSync),n=G(D.rmdirSync),m={access:I,appendFile:K,close:L,copyFile:M,exists:N,chown:O,chmod:P,fchmod:Q,fchown:U,fstat:V,fsync:W,ftruncate:X,futimes:Y,lchmod:Z,lchown:_,link:$,lstat:T,mkdir:E,mkdtemp:j,open:R,read:k,write:x,readdir:F,readFile:u,writeFile:w,readlink:g,realpath:h,rename:d,stat:c,symlink:v,truncate:a,unlink:y,utimes:l,lutimes:t,rm:p,rmdir:n,watch:H,constants,[Symbol.for("CommonJS")]:0};export{w as writeFile,x as write,H as watch,l as utimes,y as unlink,a as truncate,v as symlink,c as stat,n as rmdir,p as rm,d as rename,h as realpath,g as readlink,F as readdir,u as readFile,k as read,R as open,j as mkdtemp,E as mkdir,t as lutimes,T as lstat,$ as link,_ as lchown,Z as lchmod,Y as futimes,X as ftruncate,W as fsync,V as fstat,U as fchown,Q as fchmod,N as exists,m as default,M as copyFile,L as close,O as chown,P as chmod,K as appendFile,I as access}; diff --git a/src/watcher.zig b/src/watcher.zig index 258fde1c5eef..044770dc4450 100644 --- a/src/watcher.zig +++ b/src/watcher.zig @@ -405,16 +405,13 @@ pub fn NewWatcher(comptime ContextType: type) type { this.thread = try std.Thread.spawn(.{}, Watcher.watchLoop, .{this}); } - pub fn stop(this: *Watcher, close_descriptors: bool) void { - this.running = false; - this.close_descriptors = close_descriptors; - } - pub fn deinit(this: *Watcher, close_descriptors: bool) void { + this.mutex.lock(); + defer this.mutex.unlock(); + this.close_descriptors = close_descriptors; if (this.watchloop_handle != null) { this.running = false; - std.Thread.join(this.thread); } else { if (this.close_descriptors and this.running) { const fds = this.watchlist.items(.fd); @@ -423,9 +420,9 @@ pub fn NewWatcher(comptime ContextType: type) type { } } this.watchlist.deinit(this.allocator); + const allocator = this.allocator; + allocator.destroy(this); } - const allocator = this.allocator; - allocator.destroy(this); } // This must only be called from the watcher thread @@ -439,8 +436,9 @@ pub fn NewWatcher(comptime ContextType: type) type { this._watchLoop() catch |err| { this.watchloop_handle = null; PlatformWatcher.stop(); - - this.ctx.onError(err); + if (this.running) { + this.ctx.onError(err); + } }; // deinit and close descriptors if needed @@ -451,6 +449,9 @@ pub fn NewWatcher(comptime ContextType: type) type { } } this.watchlist.deinit(this.allocator); + + const allocator = this.allocator; + allocator.destroy(this); } var evict_list_i: WatchItemIndex = 0; @@ -573,8 +574,9 @@ pub fn NewWatcher(comptime ContextType: type) type { this.mutex.lock(); defer this.mutex.unlock(); - - this.ctx.onFileUpdate(watchevents, this.changed_filepaths[0..watchevents.len], this.watchlist); + if (this.running) { + this.ctx.onFileUpdate(watchevents, this.changed_filepaths[0..watchevents.len], this.watchlist); + } } } else if (Environment.isLinux) { restart: while (this.running) { @@ -643,9 +645,10 @@ pub fn NewWatcher(comptime ContextType: type) type { this.mutex.lock(); defer this.mutex.unlock(); - - this.ctx.onFileUpdate(all_events[0 .. last_event_index + 1], this.changed_filepaths[0 .. name_off + 1], this.watchlist); - remaining_events -= slice.len; + if (this.running) { + this.ctx.onFileUpdate(all_events[0 .. last_event_index + 1], this.changed_filepaths[0 .. name_off + 1], this.watchlist); + remaining_events -= slice.len; + } } } } diff --git a/test/js/node/watch/fixtures/close.js b/test/js/node/watch/fixtures/close.js new file mode 100644 index 000000000000..8eeeb79a3eaf --- /dev/null +++ b/test/js/node/watch/fixtures/close.js @@ -0,0 +1,7 @@ +import fs from "fs"; +fs.watch(import.meta.path, { signal: AbortSignal.timeout(4000) }) + .on("error", err => { + console.error(err.message); + process.exit(1); + }) + .close(); diff --git a/test/js/node/watch/fixtures/relative.js b/test/js/node/watch/fixtures/relative.js index 4abad56854c6..26e09da1a036 100644 --- a/test/js/node/watch/fixtures/relative.js +++ b/test/js/node/watch/fixtures/relative.js @@ -20,4 +20,4 @@ watcher.on("error", err => { const interval = setInterval(() => { fs.writeFileSync("relative.txt", "world"); -}, 1000); +}, 10); diff --git a/test/js/node/watch/fixtures/unref.js b/test/js/node/watch/fixtures/unref.js new file mode 100644 index 000000000000..a0c506a04317 --- /dev/null +++ b/test/js/node/watch/fixtures/unref.js @@ -0,0 +1,7 @@ +import fs from "fs"; +fs.watch(import.meta.path, { signal: AbortSignal.timeout(4000) }) + .on("error", err => { + console.error(err.message); + process.exit(1); + }) + .unref(); diff --git a/test/js/node/watch/fs.watch.test.js b/test/js/node/watch/fs.watch.test.js index bf7e894dc302..7e1d9dcbedf4 100644 --- a/test/js/node/watch/fs.watch.test.js +++ b/test/js/node/watch/fs.watch.test.js @@ -1,16 +1,14 @@ import fs from "fs"; import path from "path"; -import { createTest } from "node-harness"; import { tempDirWithFiles, bunRun, bunRunAsScript } from "harness"; import { pathToFileURL } from "bun"; -const { describe, expect, test, createDoneDotAll } = createTest(import.meta.path); +import { describe, expect, test } from "bun:test"; // Because macOS (and possibly other operating systems) can return a watcher // before it is actually watching, we need to repeat the operation to avoid // a race condition. function repeat(fn) { - setTimeout(fn, 100); - const interval = setInterval(fn, 500); + const interval = setInterval(fn, 20); return interval; } const encodingFileName = `新建文夹件.txt`; @@ -23,8 +21,7 @@ const testDir = tempDirWithFiles("watch", { }); describe("fs.watch", () => { - // TODO: sometimes Subprocess never ends and test hangs - test.skip("non-persistent watcher should not block the event loop", done => { + test("non-persistent watcher should not block the event loop", done => { try { // https://github.com/joyent/node/issues/2293 - non-persistent watcher should not block the event loop bunRun(path.join(import.meta.dir, "fixtures", "persistent.js")); @@ -33,8 +30,26 @@ describe("fs.watch", () => { done(e); } }); - // TODO: sometimes Subprocess never ends and test hangs - test.skip("should work with relative files", done => { + + test("watcher should close and not block the event loop", done => { + try { + bunRun(path.join(import.meta.dir, "fixtures", "close.js")); + done(); + } catch (e) { + done(e); + } + }); + + test("unref watcher should not block the event loop", done => { + try { + bunRun(path.join(import.meta.dir, "fixtures", "unref.js")); + done(); + } catch (e) { + done(e); + } + }); + + test("should work with relative files", done => { try { bunRunAsScript(testDir, path.join(import.meta.dir, "fixtures", "relative.js")); done(); @@ -49,21 +64,26 @@ describe("fs.watch", () => { try { fs.mkdirSync(root); } catch {} + let err = undefined; const watcher = fs.watch(root, { signal: AbortSignal.timeout(3000) }); watcher.on("change", (event, filename) => { count++; try { expect(event).toBe("rename"); expect(["new-file.txt", "new-folder.txt"]).toContain(filename); - if (count >= 2) done(); + if (count >= 2) { + watcher.close(); + } } catch (e) { - done(e); + err = e; + watcher.close(); } - watcher.close(); }); - watcher.on("error", () => done(err)); + + watcher.on("error", e => (err = e)); watcher.on("close", () => { clearInterval(interval); + done(err); }); const interval = repeat(() => { @@ -71,31 +91,35 @@ describe("fs.watch", () => { fs.mkdirSync(path.join(root, "new-folder.txt")); fs.rmdirSync(path.join(root, "new-folder.txt")); }); - }, 4000); + }); test("add file/folder to subfolder", done => { let count = 0; - const root = path.join(testDir, "add-directory"); + const root = path.join(testDir, "add-subdirectory"); try { fs.mkdirSync(root); } catch {} const subfolder = path.join(root, "subfolder"); fs.mkdirSync(subfolder); const watcher = fs.watch(root, { recursive: true, signal: AbortSignal.timeout(3000) }); + let err = undefined; watcher.on("change", (event, filename) => { count++; try { expect(event).toBe("rename"); expect(["new-file.txt", "new-folder.txt"]).toContain(path.basename(filename)); - if (count >= 2) done(); + if (count >= 2) { + watcher.close(); + } } catch (e) { - done(e); + err = e; + watcher.close(); } - watcher.close(); }); - watcher.on("error", () => done(err)); + watcher.on("error", () => (err = e)); watcher.on("close", () => { clearInterval(interval); + done(err); }); const interval = repeat(() => { @@ -103,70 +127,62 @@ describe("fs.watch", () => { fs.mkdirSync(path.join(subfolder, "new-folder.txt")); fs.rmdirSync(path.join(subfolder, "new-folder.txt")); }); - }, 4000); + }); + test("should emit event when file is deleted", done => { const testsubdir = tempDirWithFiles("subdir", { "deleted.txt": "hello", }); const filepath = path.join(testsubdir, "deleted.txt"); + let err = undefined; const watcher = fs.watch(testsubdir, function (event, filename) { try { expect(event).toBe("rename"); expect(filename).toBe("deleted.txt"); - done(); } catch (e) { - done(e); + err = e; } finally { clearInterval(interval); watcher.close(); } }); + watcher.once("close", () => { + done(err); + }); + const interval = repeat(() => { fs.rmSync(filepath, { force: true }); const fd = fs.openSync(filepath, "w"); fs.closeSync(fd); }); - }, 4000); + }); test("should emit 'change' event when file is modified", done => { const filepath = path.join(testDir, "watch.txt"); const watcher = fs.watch(filepath); + let err = undefined; watcher.on("change", function (event, filename) { try { expect(event).toBe("change"); expect(filename).toBe("watch.txt"); - done(); } catch (e) { - done(e); + err = e; } finally { clearInterval(interval); watcher.close(); } }); + watcher.once("close", () => { + done(err); + }); + const interval = repeat(() => { fs.writeFileSync(filepath, "world"); }); - }, 4000); - - test("Signal aborted after creating the watcher", done => { - const filepath = path.join(testDir, "abort.txt"); - - const ac = new AbortController(); - const watcher = fs.watch(filepath, { signal: ac.signal }); - watcher.once("close", () => done()); - setImmediate(() => ac.abort()); - }, 1000); - - test("Signal aborted before creating the watcher", done => { - const filepath = path.join(testDir, "abort.txt"); - - const signal = AbortSignal.abort(); - const watcher = fs.watch(filepath, { signal }); - watcher.once("close", () => done()); - }, 1000); + }); test("should error on invalid path", done => { try { @@ -180,42 +196,42 @@ describe("fs.watch", () => { } }); - const encodings = ["utf8", "buffer", "hex", "ascii", "utf-8", "base64"]; - const brokenEncodings = ["utf16le", "ucs2", "ucs-2", "latin1", "binary"]; + const encodings = ["utf8", "buffer", "hex", "ascii", "base64", "utf16le", "ucs2"]; + const brokenEncodings = ["latin1", "binary"]; test.todo(`should work with encodings ${brokenEncodings.join(", ")}`, done => { done(new Error("TODO: implement")); }); - test(`should work with encodings ${encodings.join(", ")}`, done => { - const createDone = createDoneDotAll(err => { - watchers.forEach(w => w.close()); - clearInterval(interval); - done(err); - }); + test(`should work with encodings ${encodings.join(", ")}`, async () => { const watchers = []; const filepath = path.join(testDir, encodingFileName); + const promises = []; encodings.forEach(name => { - const encodeDone = createDone(); const encoded_filename = name !== "buffer" ? Buffer.from(encodingFileName, "utf8").toString(name) : Buffer.from(encodingFileName); - watchers.push( - fs.watch(filepath, { encoding: name }, (event, filename) => { - try { - expect(event).toBe("change"); - - if (name !== "buffer") { - expect(filename).toBe(encoded_filename); - } else { - expect(filename).toBeInstanceOf(Buffer); - expect(filename.toString("utf8")).toBe(encodingFileName); - } - - encodeDone(); - } catch (e) { - encodeDone(e); - } + + promises.push( + new Promise((resolve, reject) => { + watchers.push( + fs.watch(filepath, { encoding: name }, (event, filename) => { + try { + expect(event).toBe("change"); + + if (name !== "buffer") { + expect(filename).toBe(encoded_filename); + } else { + expect(filename).toBeInstanceOf(Buffer); + expect(filename.toString("utf8")).toBe(encodingFileName); + } + + resolve(); + } catch (e) { + reject(e); + } + }), + ); }), ); }); @@ -223,51 +239,186 @@ describe("fs.watch", () => { const interval = repeat(() => { fs.writeFileSync(filepath, "world"); }); - }, 30000); - test.todo( - "should work with url", - done => { - const filepath = path.join(testDir, "url.txt"); - try { - const watcher = fs.watch(pathToFileURL(filepath)); - watcher.on("change", function (event, filename) { - try { - expect(event).toBe("change"); - expect(filename).toBe("watch.txt"); - done(); - } catch (e) { - done(e); - } finally { + try { + await Promise.all(promises); + } finally { + clearInterval(interval); + watchers.forEach(watcher => watcher.close()); + } + }); + + test.todo("should work with url", done => { + const filepath = path.join(testDir, "url.txt"); + try { + const watcher = fs.watch(pathToFileURL(filepath)); + let err = undefined; + watcher.on("change", function (event, filename) { + try { + expect(event).toBe("change"); + expect(filename).toBe("watch.txt"); + } catch (e) { + err = e; + } finally { + clearInterval(interval); + watcher.close(); + } + }); + + watcher.once("close", () => { + done(err); + }); + + const interval = repeat(() => { + fs.writeFileSync(filepath, "world"); + }); + } catch (e) { + done(e); + } + }); + + test("Signal aborted after creating the watcher", async () => { + const filepath = path.join(testDir, "abort.txt"); + + const ac = new AbortController(); + const promise = new Promise((resolve, reject) => { + const watcher = fs.watch(filepath, { signal: ac.signal }); + watcher.once("error", err => (err.message === "The operation was aborted." ? resolve() : reject(err))); + watcher.once("close", () => reject()); + }); + await Bun.sleep(10); + ac.abort(); + await promise; + }); + + test("Signal aborted before creating the watcher", async () => { + const filepath = path.join(testDir, "abort.txt"); + + const signal = AbortSignal.abort(); + await new Promise((resolve, reject) => { + const watcher = fs.watch(filepath, { signal }); + watcher.once("error", err => (err.message === "The operation was aborted." ? resolve() : reject(err))); + watcher.once("close", () => reject()); + }); + }); +}); + +describe("fs.promises.watchFile", () => { + test("add file/folder to folder", async () => { + let count = 0; + const root = path.join(testDir, "add-promise-directory"); + try { + fs.mkdirSync(root); + } catch {} + let success = false; + let err = undefined; + try { + const ac = new AbortController(); + const watcher = fs.promises.watch(root, { signal: ac.signal }); + + const interval = repeat(() => { + fs.writeFileSync(path.join(root, "new-file.txt"), "hello"); + fs.mkdirSync(path.join(root, "new-folder.txt")); + fs.rmdirSync(path.join(root, "new-folder.txt")); + }); + + for await (const event of watcher) { + count++; + try { + expect(event.eventType).toBe("rename"); + expect(["new-file.txt", "new-folder.txt"]).toContain(event.filename); + + if (count >= 2) { + success = true; clearInterval(interval); - watcher.close(); + ac.abort(); } - }); + } catch (e) { + err = e; + clearInterval(interval); + ac.abort(); + } + } + } catch (e) { + if (!success) { + throw err || e; + } + } + }); - const interval = repeat(() => { - fs.writeFileSync(filepath, "world"); - }); + test("add file/folder to subfolder", async () => { + let count = 0; + const root = path.join(testDir, "add-promise-subdirectory"); + try { + fs.mkdirSync(root); + } catch {} + const subfolder = path.join(root, "subfolder"); + fs.mkdirSync(subfolder); + let success = false; + let err = undefined; + + try { + const ac = new AbortController(); + const watcher = fs.promises.watch(root, { recursive: true, signal: ac.signal }); + + const interval = repeat(() => { + fs.writeFileSync(path.join(subfolder, "new-file.txt"), "hello"); + fs.mkdirSync(path.join(subfolder, "new-folder.txt")); + fs.rmdirSync(path.join(subfolder, "new-folder.txt")); + }); + for await (const event of watcher) { + count++; + try { + expect(event.eventType).toBe("rename"); + expect(["new-file.txt", "new-folder.txt"]).toContain(path.basename(event.filename)); + + if (count >= 2) { + success = true; + clearInterval(interval); + ac.abort(); + } + } catch (e) { + err = e; + clearInterval(interval); + ac.abort(); + } + } + } catch (e) { + if (!success) { + throw err || e; + } + } + }); + + test("Signal aborted after creating the watcher", async () => { + const filepath = path.join(testDir, "abort.txt"); + + const ac = new AbortController(); + const watcher = fs.promises.watch(filepath, { signal: ac.signal }); + + const promise = (async () => { + try { + for await (const _ of watcher); } catch (e) { - done(e); + expect(e.message).toBe("The operation was aborted."); } - }, - 4000, - ); - test.todo( - "should close when root is deleted", - done => { - const root = path.join(testDir, "watched-directory"); + })(); + await Bun.sleep(10); + ac.abort(); + await promise; + }); + + test("Signal aborted before creating the watcher", async () => { + const filepath = path.join(testDir, "abort.txt"); + + const signal = AbortSignal.abort(); + const watcher = fs.promises.watch(filepath, { signal }); + await (async () => { try { - fs.mkdirSync(root); - } catch {} - const watcher = fs.watch(root, { signal: AbortSignal.timeout(3000) }); - watcher.on("close", () => done()); - watcher.on("error", () => done(err)); - - setTimeout(() => { - fs.mkdirSync(root); - }, 1000); - }, - 4000, - ); + for await (const _ of watcher); + } catch (e) { + expect(e.message).toBe("The operation was aborted."); + } + })(); + }); }); From 3179a766e5c9d4984bbdda937d3683a81e757379 Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Fri, 16 Jun 2023 08:33:05 -0300 Subject: [PATCH 10/18] fix latin1 --- src/bun.js/webcore/encoding.zig | 2 +- test/js/node/watch/fs.watch.test.js | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/bun.js/webcore/encoding.zig b/src/bun.js/webcore/encoding.zig index df2aaf45605c..061a25eed06e 100644 --- a/src/bun.js/webcore/encoding.zig +++ b/src/bun.js/webcore/encoding.zig @@ -811,7 +811,7 @@ pub const Encoder = struct { .hex => toString(input, len, globalObject, .hex), .base64 => toString(input, len, globalObject, .base64), .base64url => toString(input, len, globalObject, .base64url), - + .latin1 => toString(input, len, globalObject, .latin1), // treat everything else as utf8 else => toString(input, len, globalObject, .utf8), }; diff --git a/test/js/node/watch/fs.watch.test.js b/test/js/node/watch/fs.watch.test.js index 7e1d9dcbedf4..5b20b2594548 100644 --- a/test/js/node/watch/fs.watch.test.js +++ b/test/js/node/watch/fs.watch.test.js @@ -196,12 +196,7 @@ describe("fs.watch", () => { } }); - const encodings = ["utf8", "buffer", "hex", "ascii", "base64", "utf16le", "ucs2"]; - const brokenEncodings = ["latin1", "binary"]; - - test.todo(`should work with encodings ${brokenEncodings.join(", ")}`, done => { - done(new Error("TODO: implement")); - }); + const encodings = ["utf8", "buffer", "hex", "ascii", "base64", "utf16le", "ucs2", "latin1", "binary"]; test(`should work with encodings ${encodings.join(", ")}`, async () => { const watchers = []; From 421a447b3bb8610984c3b6a240b026622d07196d Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Wed, 21 Jun 2023 23:29:43 -0400 Subject: [PATCH 11/18] add fs_events, still failing some tests --- src/bun.js/node/fs_events.zig | 614 ++++++++++++++++++++++++++++ src/bun.js/node/node_fs_watcher.zig | 114 +++++- test/js/node/watch/fs.watch.test.js | 71 ++-- 3 files changed, 744 insertions(+), 55 deletions(-) create mode 100644 src/bun.js/node/fs_events.zig diff --git a/src/bun.js/node/fs_events.zig b/src/bun.js/node/fs_events.zig new file mode 100644 index 000000000000..b1a56a92b5d8 --- /dev/null +++ b/src/bun.js/node/fs_events.zig @@ -0,0 +1,614 @@ +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 (bun.strings.containsChar(path, '/') and !handle.recursive) { + break; + } + + 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, this.watcher_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 == 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); + } +}; + +fn test_callback(ctx: ?*anyopaque, path: string, is_file: bool, is_rename: bool) void { + _ = ctx; + _ = path; + _ = is_file; + _ = is_rename; +} + +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); + } +} diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig index 6bf5e4cd701e..63a26a1605c5 100644 --- a/src/bun.js/node/node_fs_watcher.zig +++ b/src/bun.js/node/node_fs_watcher.zig @@ -5,6 +5,8 @@ const Fs = @import("../../fs.zig"); const Path = @import("../../resolver/resolve_path.zig"); const Encoder = JSC.WebCore.Encoder; +const FSEvents = @import("./fs_events.zig"); + const VirtualMachine = JSC.VirtualMachine; const EventLoop = JSC.EventLoop; const PathLike = JSC.Node.PathLike; @@ -74,13 +76,19 @@ pub const FSWatcher = struct { abort, }; + pub const EventFreeType = enum { + destroy, + free, + none, + }; + pub const Entry = struct { file_path: string, event_type: EventType, - needs_free: bool, + free_type: EventFreeType, }; - pub fn append(this: *FSWatchTask, file_path: string, event_type: EventType, needs_free: bool) void { + pub fn append(this: *FSWatchTask, file_path: string, event_type: EventType, free_type: EventFreeType) void { if (this.count == 8) { this.enqueue(); var ctx = this.ctx; @@ -93,7 +101,7 @@ pub const FSWatcher = struct { this.entries[this.count] = .{ .file_path = file_path, .event_type = event_type, - .needs_free = needs_free, + .free_type = free_type, }; this.count += 1; } @@ -136,8 +144,10 @@ pub const FSWatcher = struct { pub fn deinit(this: *FSWatchTask) void { while (this.count > 0) { this.count -= 1; - if (this.entries[this.count].needs_free) { - bun.default_allocator.destroy(this.entries[this.count].file_path); + switch (this.entries[this.count].free_type) { + .destroy => bun.default_allocator.destroy(this.entries[this.count].file_path), + .free => bun.default_allocator.free(this.entries[this.count].file_path), + else => {}, } } bun.default_allocator.destroy(this); @@ -229,10 +239,30 @@ pub const FSWatcher = struct { var current_task: FSWatchTask = .{ .ctx = this, }; - current_task.append(@errorName(err), .@"error", false); + current_task.append(@errorName(err), .@"error", .none); current_task.enqueue(); } + pub fn onFSEventUpdate( + ctx: ?*anyopaque, + path: string, + _: bool, + is_rename: bool, + ) void { + const this = bun.cast(*FSWatcher, ctx.?); + + var current_task: FSWatchTask = .{ + .ctx = this, + }; + defer current_task.enqueue(); + + const relative_path = bun.default_allocator.alloc(u8, path.len) catch unreachable; + bun.copy(u8, relative_path, path); + const event_type: FSWatchTask.EventType = if (is_rename) .rename else .change; + + current_task.append(relative_path, event_type, .free); + } + pub fn onFileUpdate( this: *FSWatcher, events: []watcher.WatchEvent, @@ -312,11 +342,14 @@ pub const FSWatcher = struct { const relative_path = bun.default_allocator.dupe(u8, relative_slice) catch unreachable; - current_task.append(relative_path, event_type, true); + current_task.append(relative_path, event_type, .destroy); } } }, .directory => { + // if (comptime Environment.isMac) { + // unreachable; + // } var affected_buf: [128][]const u8 = undefined; const affected = brk: { @@ -379,7 +412,7 @@ pub const FSWatcher = struct { this.last_change_event.event_type = event_type; this.last_change_event.hash = file_hash; - current_task.append(relative_path, event_type, true); + current_task.append(relative_path, event_type, .destroy); if (this.verbose) Output.prettyErrorln(" Dir change: {s}", .{relative_path}); @@ -464,7 +497,7 @@ pub const FSWatcher = struct { ); return null; } - if (JSC.Node.Encoding.fromStringValue(encoding_, ctx)) |node_encoding| { + if (JSC.Node.Encoding.fromJS(encoding_, ctx.ptr())) |node_encoding| { encoding = node_encoding; } else { JSC.throwInvalidArguments( @@ -550,7 +583,8 @@ pub const FSWatcher = struct { pub const JSObject = struct { signal: ?*JSC.AbortSignal, persistent: bool, - manager: *FSWatcher.Watcher, + manager: ?*FSWatcher.Watcher, + fsevents_watcher: ?*FSEvents.FSEventsWatcher, poll_ref: JSC.PollRef = .{}, globalThis: ?*JSC.JSGlobalObject, js_this: JSC.JSValue, @@ -559,20 +593,29 @@ pub const FSWatcher = struct { pub usingnamespace JSC.Codegen.JSFSWatcher; - pub fn init(globalThis: *JSC.JSGlobalObject, manager: *FSWatcher.Watcher, signal: ?*JSC.AbortSignal, listener: JSC.JSValue, persistent: bool, encoding: JSC.Node.Encoding) !*JSObject { + pub fn getFSWatcher(this: *JSObject) *FSWatcher { + if (this.manager) |manager| return manager.ctx; + if (this.fsevents_watcher) |manager| return bun.cast(*FSWatcher, manager.ctx.?); + + @panic("No context attached to JSFSWatcher"); + } + + pub fn init(globalThis: *JSC.JSGlobalObject, manager: ?*FSWatcher.Watcher, fsevents_watcher: ?*FSEvents.FSEventsWatcher, signal: ?*JSC.AbortSignal, listener: JSC.JSValue, persistent: bool, encoding: JSC.Node.Encoding) !*JSObject { var obj = try globalThis.allocator().create(JSObject); obj.* = .{ .signal = null, .persistent = persistent, .manager = manager, + .fsevents_watcher = fsevents_watcher, .globalThis = globalThis, .js_this = .zero, .encoding = encoding, .closed = false, }; + const instance = obj.getFSWatcher(); if (persistent) { - obj.poll_ref.ref(obj.manager.ctx.ctx); + obj.poll_ref.ref(instance.ctx); } var js_this = JSObject.toJS(obj, globalThis); @@ -587,9 +630,9 @@ pub const FSWatcher = struct { obj.signal = s.ref(); // abort next tick var current_task: FSWatchTask = .{ - .ctx = manager.ctx, + .ctx = instance, }; - current_task.append("", .abort, false); + current_task.append("", .abort, .none); current_task.enqueue(); } else { // watch for abortion @@ -682,7 +725,7 @@ pub const FSWatcher = struct { if (!this.persistent) { this.persistent = true; - this.poll_ref.ref(this.manager.ctx.ctx); + this.poll_ref.ref(this.getFSWatcher().ctx); } } @@ -694,7 +737,7 @@ pub const FSWatcher = struct { pub fn unref(this: *JSObject) void { if (this.persistent) { this.persistent = false; - this.poll_ref.unref(this.manager.ctx.ctx); + this.poll_ref.unref(this.getFSWatcher().ctx); } } @@ -739,10 +782,20 @@ pub const FSWatcher = struct { this.signal = null; signal.detach(this); } + if (this.manager) |manager| { + var ctx = manager.ctx; + this.manager = null; + ctx.js_watcher = null; + ctx.deinit(); + manager.deinit(true); + } - this.manager.ctx.js_watcher = null; - this.manager.ctx.deinit(); - this.manager.deinit(true); + if (this.fsevents_watcher) |manager| { + var ctx = bun.cast(*FSWatcher, manager.ctx.?); + ctx.js_watcher = null; + ctx.deinit(); + manager.deinit(); + } } pub fn doClose(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { @@ -824,6 +877,27 @@ pub const FSWatcher = struct { }, }; + if (comptime Environment.isMac) { + if (!fs_type.is_file) { + var dir_path_clone = bun.default_allocator.dupeZ(u8, file_path) catch unreachable; + ctx.entry_path = dir_path_clone; + ctx.entry_dir = dir_path_clone; + + var fsevents_watcher = FSEvents.watch(dir_path_clone, args.recursive, onFSEventUpdate, bun.cast(*anyopaque, ctx)) catch |err| { + ctx.deinit(); + return err; + }; + + ctx.js_watcher = JSObject.init(args.global_this, null, fsevents_watcher, args.signal, args.listener, args.persistent, args.encoding) catch |err| { + ctx.deinit(); + fsevents_watcher.deinit(); + return err; + }; + + return ctx; + } + } + var fs_watcher = FSWatcher.Watcher.init( ctx, vm.bundler.fs, @@ -861,7 +935,7 @@ pub const FSWatcher = struct { return err; }; - ctx.js_watcher = JSObject.init(args.global_this, fs_watcher, args.signal, args.listener, args.persistent, args.encoding) catch |err| { + ctx.js_watcher = JSObject.init(args.global_this, fs_watcher, null, args.signal, args.listener, args.persistent, args.encoding) catch |err| { ctx.deinit(); fs_watcher.deinit(true); return err; diff --git a/test/js/node/watch/fs.watch.test.js b/test/js/node/watch/fs.watch.test.js index 5b20b2594548..ff32d21ea872 100644 --- a/test/js/node/watch/fs.watch.test.js +++ b/test/js/node/watch/fs.watch.test.js @@ -93,41 +93,41 @@ describe("fs.watch", () => { }); }); - test("add file/folder to subfolder", done => { - let count = 0; - const root = path.join(testDir, "add-subdirectory"); - try { - fs.mkdirSync(root); - } catch {} - const subfolder = path.join(root, "subfolder"); - fs.mkdirSync(subfolder); - const watcher = fs.watch(root, { recursive: true, signal: AbortSignal.timeout(3000) }); - let err = undefined; - watcher.on("change", (event, filename) => { - count++; - try { - expect(event).toBe("rename"); - expect(["new-file.txt", "new-folder.txt"]).toContain(path.basename(filename)); - if (count >= 2) { - watcher.close(); - } - } catch (e) { - err = e; - watcher.close(); - } - }); - watcher.on("error", () => (err = e)); - watcher.on("close", () => { - clearInterval(interval); - done(err); - }); - - const interval = repeat(() => { - fs.writeFileSync(path.join(subfolder, "new-file.txt"), "hello"); - fs.mkdirSync(path.join(subfolder, "new-folder.txt")); - fs.rmdirSync(path.join(subfolder, "new-folder.txt")); - }); - }); + // test("add file/folder to subfolder", done => { + // let count = 0; + // const root = path.join(testDir, "add-subdirectory"); + // try { + // fs.mkdirSync(root); + // } catch {} + // const subfolder = path.join(root, "subfolder"); + // fs.mkdirSync(subfolder); + // const watcher = fs.watch(root, { recursive: true, signal: AbortSignal.timeout(3000) }); + // let err = undefined; + // watcher.on("change", (event, filename) => { + // count++; + // try { + // expect(event).toBe("rename"); + // expect(["new-file.txt", "new-folder.txt"]).toContain(path.basename(filename)); + // if (count >= 2) { + // watcher.close(); + // } + // } catch (e) { + // err = e; + // watcher.close(); + // } + // }); + // watcher.on("error", (e) => (err = e)); + // watcher.on("close", () => { + // clearInterval(interval); + // done(err); + // }); + + // const interval = repeat(() => { + // fs.writeFileSync(path.join(subfolder, "new-file.txt"), "hello"); + // fs.mkdirSync(path.join(subfolder, "new-folder.txt")); + // fs.rmdirSync(path.join(subfolder, "new-folder.txt")); + // }); + // }); test("should emit event when file is deleted", done => { const testsubdir = tempDirWithFiles("subdir", { @@ -364,6 +364,7 @@ describe("fs.promises.watchFile", () => { for await (const event of watcher) { count++; try { + console.log(event.filename); expect(event.eventType).toBe("rename"); expect(["new-file.txt", "new-folder.txt"]).toContain(path.basename(event.filename)); From e62dd9713edc877e4d759e7e66285bb2532141ff Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Thu, 22 Jun 2023 00:38:47 -0400 Subject: [PATCH 12/18] fixuup --- src/bun.js/node/fs_events.zig | 29 ++--- src/bun.js/node/node_fs_watcher.zig | 7 +- src/js/node/fs.js | 6 +- test/js/node/watch/fs.watch.test.js | 166 ++++++++++++++-------------- 4 files changed, 103 insertions(+), 105 deletions(-) diff --git a/src/bun.js/node/fs_events.zig b/src/bun.js/node/fs_events.zig index b1a56a92b5d8..4cff50520e40 100644 --- a/src/bun.js/node/fs_events.zig +++ b/src/bun.js/node/fs_events.zig @@ -391,8 +391,8 @@ pub const FSEventsLoop = struct { } // Do not emit events from subdirectories (without option set) - if (bun.strings.containsChar(path, '/') and !handle.recursive) { - break; + if (path.len == 0 or (bun.strings.containsChar(path, '/') and !handle.recursive)) { + continue; } var is_rename = true; @@ -449,7 +449,7 @@ pub const FSEventsLoop = struct { } } - const cf_paths = CF.ArrayCreate(null, paths.ptr, this.watcher_count, null); + const cf_paths = CF.ArrayCreate(null, paths.ptr, count, null); var ctx: FSEventStreamContext = .{ .info = this, }; @@ -519,14 +519,16 @@ pub const FSEventsLoop = struct { defer this.mutex.unlock(); var watchers = this.watchers.slice(); for (watchers, 0..) |w, i| { - if (w == watcher) { - watchers[i] = null; - // if is the last one just pop - if (i == watchers.len - 1) { - this.watchers.len -= 1; + 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; } - this.watcher_count -= 1; - break; } } } @@ -593,13 +595,6 @@ pub const FSEventsWatcher = struct { } }; -fn test_callback(ctx: ?*anyopaque, path: string, is_file: bool, is_rename: bool) void { - _ = ctx; - _ = path; - _ = is_file; - _ = is_rename; -} - 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); diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig index 63a26a1605c5..ac18f91af0d0 100644 --- a/src/bun.js/node/node_fs_watcher.zig +++ b/src/bun.js/node/node_fs_watcher.zig @@ -849,9 +849,12 @@ pub const FSWatcher = struct { pub fn init(args: Arguments) !*FSWatcher { var buf: [bun.MAX_PATH_BYTES + 1]u8 = undefined; - + var slice = args.path.slice(); + if (bun.strings.startsWith(slice, "file://")) { + slice = slice[6..]; + } var parts = [_]string{ - args.path.slice(), + slice, }; var file_path = Path.joinAbsStringBuf( diff --git a/src/js/node/fs.js b/src/js/node/fs.js index 04cd352df751..6b3998edc849 100644 --- a/src/js/node/fs.js +++ b/src/js/node/fs.js @@ -15,12 +15,8 @@ class FSWatcher extends EventEmitter { #listener; constructor(path, options, listener) { super(); - if (path instanceof URL) { - throw new TypeError("Watch URLs are not supported yet"); - } else if (Buffer.isBuffer(path)) { + if (Buffer.isBuffer(path)) { path = path.toString(); - } else if (typeof path !== "string") { - throw new TypeError("Expected path to be a string or Buffer"); } if (typeof options === "function") { diff --git a/test/js/node/watch/fs.watch.test.js b/test/js/node/watch/fs.watch.test.js index ff32d21ea872..f22289f9e7aa 100644 --- a/test/js/node/watch/fs.watch.test.js +++ b/test/js/node/watch/fs.watch.test.js @@ -21,56 +21,95 @@ const testDir = tempDirWithFiles("watch", { }); describe("fs.watch", () => { - test("non-persistent watcher should not block the event loop", done => { - try { - // https://github.com/joyent/node/issues/2293 - non-persistent watcher should not block the event loop - bunRun(path.join(import.meta.dir, "fixtures", "persistent.js")); - done(); - } catch (e) { - done(e); - } - }); + // test("non-persistent watcher should not block the event loop", done => { + // try { + // // https://github.com/joyent/node/issues/2293 - non-persistent watcher should not block the event loop + // bunRun(path.join(import.meta.dir, "fixtures", "persistent.js")); + // done(); + // } catch (e) { + // done(e); + // } + // }); - test("watcher should close and not block the event loop", done => { - try { - bunRun(path.join(import.meta.dir, "fixtures", "close.js")); - done(); - } catch (e) { - done(e); - } - }); + // test("watcher should close and not block the event loop", done => { + // try { + // bunRun(path.join(import.meta.dir, "fixtures", "close.js")); + // done(); + // } catch (e) { + // done(e); + // } + // }); - test("unref watcher should not block the event loop", done => { - try { - bunRun(path.join(import.meta.dir, "fixtures", "unref.js")); - done(); - } catch (e) { - done(e); - } - }); + // test("unref watcher should not block the event loop", done => { + // try { + // bunRun(path.join(import.meta.dir, "fixtures", "unref.js")); + // done(); + // } catch (e) { + // done(e); + // } + // }); - test("should work with relative files", done => { - try { - bunRunAsScript(testDir, path.join(import.meta.dir, "fixtures", "relative.js")); - done(); - } catch (e) { - done(e); - } - }); + // test("should work with relative files", done => { + // try { + // bunRunAsScript(testDir, path.join(import.meta.dir, "fixtures", "relative.js")); + // done(); + // } catch (e) { + // done(e); + // } + // }); - test("add file/folder to folder", done => { + // test("add file/folder to folder", done => { + // let count = 0; + // const root = path.join(testDir, "add-directory"); + // try { + // fs.mkdirSync(root); + // } catch {} + // let err = undefined; + // const watcher = fs.watch(root, { signal: AbortSignal.timeout(3000) }); + // watcher.on("change", (event, filename) => { + // count++; + // try { + // expect(event).toBe("rename"); + // expect(["new-file.txt", "new-folder.txt"]).toContain(filename); + // if (count >= 2) { + // watcher.close(); + // } + // } catch (e) { + // err = e; + // watcher.close(); + // } + // }); + + // watcher.on("error", e => (err = e)); + // watcher.on("close", () => { + // clearInterval(interval); + // done(err); + // }); + + // const interval = repeat(() => { + // fs.writeFileSync(path.join(root, "new-file.txt"), "hello"); + // fs.mkdirSync(path.join(root, "new-folder.txt")); + // fs.rmdirSync(path.join(root, "new-folder.txt")); + // }); + // }); + + test("add file/folder to subfolder", done => { let count = 0; - const root = path.join(testDir, "add-directory"); + const root = path.join(testDir, "add-subdirectory"); try { fs.mkdirSync(root); } catch {} + const subfolder = path.join(root, "subfolder"); + fs.mkdirSync(subfolder); + const watcher = fs.watch(root, { recursive: true, signal: AbortSignal.timeout(3000) }); let err = undefined; - const watcher = fs.watch(root, { signal: AbortSignal.timeout(3000) }); watcher.on("change", (event, filename) => { + const basename = path.basename(filename); + if (basename === "subfolder") return; count++; try { expect(event).toBe("rename"); - expect(["new-file.txt", "new-folder.txt"]).toContain(filename); + expect(["new-file.txt", "new-folder.txt"]).toContain(basename); if (count >= 2) { watcher.close(); } @@ -79,7 +118,6 @@ describe("fs.watch", () => { watcher.close(); } }); - watcher.on("error", e => (err = e)); watcher.on("close", () => { clearInterval(interval); @@ -87,48 +125,12 @@ describe("fs.watch", () => { }); const interval = repeat(() => { - fs.writeFileSync(path.join(root, "new-file.txt"), "hello"); - fs.mkdirSync(path.join(root, "new-folder.txt")); - fs.rmdirSync(path.join(root, "new-folder.txt")); + fs.writeFileSync(path.join(subfolder, "new-file.txt"), "hello"); + fs.mkdirSync(path.join(subfolder, "new-folder.txt")); + fs.rmdirSync(path.join(subfolder, "new-folder.txt")); }); }); - // test("add file/folder to subfolder", done => { - // let count = 0; - // const root = path.join(testDir, "add-subdirectory"); - // try { - // fs.mkdirSync(root); - // } catch {} - // const subfolder = path.join(root, "subfolder"); - // fs.mkdirSync(subfolder); - // const watcher = fs.watch(root, { recursive: true, signal: AbortSignal.timeout(3000) }); - // let err = undefined; - // watcher.on("change", (event, filename) => { - // count++; - // try { - // expect(event).toBe("rename"); - // expect(["new-file.txt", "new-folder.txt"]).toContain(path.basename(filename)); - // if (count >= 2) { - // watcher.close(); - // } - // } catch (e) { - // err = e; - // watcher.close(); - // } - // }); - // watcher.on("error", (e) => (err = e)); - // watcher.on("close", () => { - // clearInterval(interval); - // done(err); - // }); - - // const interval = repeat(() => { - // fs.writeFileSync(path.join(subfolder, "new-file.txt"), "hello"); - // fs.mkdirSync(path.join(subfolder, "new-folder.txt")); - // fs.rmdirSync(path.join(subfolder, "new-folder.txt")); - // }); - // }); - test("should emit event when file is deleted", done => { const testsubdir = tempDirWithFiles("subdir", { "deleted.txt": "hello", @@ -243,7 +245,7 @@ describe("fs.watch", () => { } }); - test.todo("should work with url", done => { + test("should work with url", done => { const filepath = path.join(testDir, "url.txt"); try { const watcher = fs.watch(pathToFileURL(filepath)); @@ -251,7 +253,7 @@ describe("fs.watch", () => { watcher.on("change", function (event, filename) { try { expect(event).toBe("change"); - expect(filename).toBe("watch.txt"); + expect(filename).toBe("url.txt"); } catch (e) { err = e; } finally { @@ -362,11 +364,13 @@ describe("fs.promises.watchFile", () => { fs.rmdirSync(path.join(subfolder, "new-folder.txt")); }); for await (const event of watcher) { + const basename = path.basename(event.filename); + if (basename === "subfolder") continue; + count++; try { - console.log(event.filename); expect(event.eventType).toBe("rename"); - expect(["new-file.txt", "new-folder.txt"]).toContain(path.basename(event.filename)); + expect(["new-file.txt", "new-folder.txt"]).toContain(basename); if (count >= 2) { success = true; From e1e3b1c9ec1378932474de0551b74cbdf1abb18a Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Thu, 22 Jun 2023 00:47:32 -0400 Subject: [PATCH 13/18] remove unecesary check --- src/js/node/fs.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/js/node/fs.js b/src/js/node/fs.js index 6b3998edc849..6b0e3954e861 100644 --- a/src/js/node/fs.js +++ b/src/js/node/fs.js @@ -15,9 +15,6 @@ class FSWatcher extends EventEmitter { #listener; constructor(path, options, listener) { super(); - if (Buffer.isBuffer(path)) { - path = path.toString(); - } if (typeof options === "function") { listener = options; From 63c50b98a965316e582cb63aa585a8e65c4ae1de Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Thu, 22 Jun 2023 00:51:12 -0400 Subject: [PATCH 14/18] readd tests ops --- test/js/node/watch/fs.watch.test.js | 142 ++++++++++++++-------------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/test/js/node/watch/fs.watch.test.js b/test/js/node/watch/fs.watch.test.js index f22289f9e7aa..56e1798f1ef8 100644 --- a/test/js/node/watch/fs.watch.test.js +++ b/test/js/node/watch/fs.watch.test.js @@ -21,77 +21,77 @@ const testDir = tempDirWithFiles("watch", { }); describe("fs.watch", () => { - // test("non-persistent watcher should not block the event loop", done => { - // try { - // // https://github.com/joyent/node/issues/2293 - non-persistent watcher should not block the event loop - // bunRun(path.join(import.meta.dir, "fixtures", "persistent.js")); - // done(); - // } catch (e) { - // done(e); - // } - // }); - - // test("watcher should close and not block the event loop", done => { - // try { - // bunRun(path.join(import.meta.dir, "fixtures", "close.js")); - // done(); - // } catch (e) { - // done(e); - // } - // }); - - // test("unref watcher should not block the event loop", done => { - // try { - // bunRun(path.join(import.meta.dir, "fixtures", "unref.js")); - // done(); - // } catch (e) { - // done(e); - // } - // }); - - // test("should work with relative files", done => { - // try { - // bunRunAsScript(testDir, path.join(import.meta.dir, "fixtures", "relative.js")); - // done(); - // } catch (e) { - // done(e); - // } - // }); - - // test("add file/folder to folder", done => { - // let count = 0; - // const root = path.join(testDir, "add-directory"); - // try { - // fs.mkdirSync(root); - // } catch {} - // let err = undefined; - // const watcher = fs.watch(root, { signal: AbortSignal.timeout(3000) }); - // watcher.on("change", (event, filename) => { - // count++; - // try { - // expect(event).toBe("rename"); - // expect(["new-file.txt", "new-folder.txt"]).toContain(filename); - // if (count >= 2) { - // watcher.close(); - // } - // } catch (e) { - // err = e; - // watcher.close(); - // } - // }); - - // watcher.on("error", e => (err = e)); - // watcher.on("close", () => { - // clearInterval(interval); - // done(err); - // }); - - // const interval = repeat(() => { - // fs.writeFileSync(path.join(root, "new-file.txt"), "hello"); - // fs.mkdirSync(path.join(root, "new-folder.txt")); - // fs.rmdirSync(path.join(root, "new-folder.txt")); - // }); - // }); + test("non-persistent watcher should not block the event loop", done => { + try { + // https://github.com/joyent/node/issues/2293 - non-persistent watcher should not block the event loop + bunRun(path.join(import.meta.dir, "fixtures", "persistent.js")); + done(); + } catch (e) { + done(e); + } + }); + + test("watcher should close and not block the event loop", done => { + try { + bunRun(path.join(import.meta.dir, "fixtures", "close.js")); + done(); + } catch (e) { + done(e); + } + }); + + test("unref watcher should not block the event loop", done => { + try { + bunRun(path.join(import.meta.dir, "fixtures", "unref.js")); + done(); + } catch (e) { + done(e); + } + }); + + test("should work with relative files", done => { + try { + bunRunAsScript(testDir, path.join(import.meta.dir, "fixtures", "relative.js")); + done(); + } catch (e) { + done(e); + } + }); + + test("add file/folder to folder", done => { + let count = 0; + const root = path.join(testDir, "add-directory"); + try { + fs.mkdirSync(root); + } catch {} + let err = undefined; + const watcher = fs.watch(root, { signal: AbortSignal.timeout(3000) }); + watcher.on("change", (event, filename) => { + count++; + try { + expect(event).toBe("rename"); + expect(["new-file.txt", "new-folder.txt"]).toContain(filename); + if (count >= 2) { + watcher.close(); + } + } catch (e) { + err = e; + watcher.close(); + } + }); + + watcher.on("error", e => (err = e)); + watcher.on("close", () => { + clearInterval(interval); + done(err); + }); + + const interval = repeat(() => { + fs.writeFileSync(path.join(root, "new-file.txt"), "hello"); + fs.mkdirSync(path.join(root, "new-folder.txt")); + fs.rmdirSync(path.join(root, "new-folder.txt")); + }); + }); test("add file/folder to subfolder", done => { let count = 0; From 1e58f1b998db60145c7d5f41e9997e54e3470d5a Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Thu, 22 Jun 2023 08:36:44 -0400 Subject: [PATCH 15/18] this is necessary? just testing CI/CD weird errors --- src/js/out/modules/node/fs.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/js/out/modules/node/fs.js b/src/js/out/modules/node/fs.js index 605eda83d79b..cc3763cfcac0 100644 --- a/src/js/out/modules/node/fs.js +++ b/src/js/out/modules/node/fs.js @@ -24,12 +24,6 @@ class FSWatcher extends EventEmitter { #listener; constructor(path, options, listener) { super(); - if (path instanceof URL) - throw new TypeError("Watch URLs are not supported yet"); - else if (Buffer.isBuffer(path)) - path = path.toString(); - else if (typeof path !== "string") - throw new TypeError("Expected path to be a string or Buffer"); if (typeof options === "function") listener = options, options = {}; else if (typeof options === "string") From d17e607f3d61759529bd90010e439ec80f451f3b Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Thu, 22 Jun 2023 08:58:36 -0400 Subject: [PATCH 16/18] just use dupe here --- src/bun.js/node/node_fs_watcher.zig | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig index ac18f91af0d0..2087850557df 100644 --- a/src/bun.js/node/node_fs_watcher.zig +++ b/src/bun.js/node/node_fs_watcher.zig @@ -256,11 +256,10 @@ pub const FSWatcher = struct { }; defer current_task.enqueue(); - const relative_path = bun.default_allocator.alloc(u8, path.len) catch unreachable; - bun.copy(u8, relative_path, path); + const relative_path = bun.default_allocator.dupe(u8, path) catch unreachable; const event_type: FSWatchTask.EventType = if (is_rename) .rename else .change; - current_task.append(relative_path, event_type, .free); + current_task.append(relative_path, event_type, .destroy); } pub fn onFileUpdate( From ad5cb0ad75e5cf4eb75927ba0ca360b8b8492d40 Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Thu, 22 Jun 2023 10:46:42 -0400 Subject: [PATCH 17/18] cleanup and fix deinit --- src/bun.js/node/fs_events.zig | 4 +-- src/bun.js/node/node_fs_watcher.zig | 48 ++++------------------------- 2 files changed, 8 insertions(+), 44 deletions(-) diff --git a/src/bun.js/node/fs_events.zig b/src/bun.js/node/fs_events.zig index 4cff50520e40..a3fba5441bdc 100644 --- a/src/bun.js/node/fs_events.zig +++ b/src/bun.js/node/fs_events.zig @@ -547,8 +547,8 @@ pub const FSEventsLoop = struct { CF.Release(this.signal_source); this.signal_source = null; - // this.sem.deinit(); - // this.mutex.deinit(); + this.sem.deinit(); + this.mutex.deinit(); if (this.watcher_count > 0) { while (this.watchers.popOrNull()) |watcher| { if (watcher) |w| { diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig index 2087850557df..80a01801d206 100644 --- a/src/bun.js/node/node_fs_watcher.zig +++ b/src/bun.js/node/node_fs_watcher.zig @@ -273,8 +273,6 @@ pub const FSWatcher = struct { var counts = slice.items(.count); const kinds = slice.items(.kind); - const hashes = slice.items(.hash); - const parents = slice.items(.parent_hash); var _on_file_update_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; var ctx = this.watcher_instance.?; @@ -302,11 +300,6 @@ pub const FSWatcher = struct { counts[event.index] = update_count; const kind = kinds[event.index]; - // so it's consistent with the rest - // if we use .extname we might run into an issue with whether or not the "." is included. - // const path = Fs.PathName.init(file_path); - const id = hashes[event.index]; - if (comptime Environment.isDebug) { if (this.verbose) { Output.prettyErrorln("[watch] {s} ({s}, {})", .{ file_path, @tagName(kind), event.op }); @@ -346,44 +339,15 @@ pub const FSWatcher = struct { } }, .directory => { - // if (comptime Environment.isMac) { - // unreachable; - // } - var affected_buf: [128][]const u8 = undefined; - - const affected = brk: { - if (comptime Environment.isMac) { - var affected_i: usize = 0; - - // if a file descriptor is stale, we need to close it - if (event.op.delete) { - for (parents, 0..) |parent_hash, entry_id| { - if (parent_hash == id) { - const affected_path = file_paths[entry_id]; - const was_deleted = check: { - std.os.access(affected_path, std.os.F_OK) catch break :check true; - break :check false; - }; - if (!was_deleted) continue; - - affected_buf[affected_i] = affected_path[file_path.len..]; - affected_i += 1; - if (affected_i >= affected_buf.len) break; - } - } - } - - break :brk affected_buf[0..affected_i]; - } + // macOS should use FSEvents for directories + if (comptime Environment.isMac) { + @panic("Unexpected directory watch"); + } - break :brk event.names(changed_files); - }; + const affected = event.names(changed_files); for (affected) |changed_name_| { - const changed_name: []const u8 = if (comptime Environment.isMac) - changed_name_ - else - bun.asByteSlice(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_hash: FSWatcher.Watcher.HashType = 0; From 6def69a63078a252534a9f979c4915195eba9aae Mon Sep 17 00:00:00 2001 From: cirospaciari Date: Fri, 23 Jun 2023 15:34:09 -0300 Subject: [PATCH 18/18] fix zig upgrade --- src/bun.js/node/node_fs_watcher.zig | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig index 80a01801d206..397d519161c1 100644 --- a/src/bun.js/node/node_fs_watcher.zig +++ b/src/bun.js/node/node_fs_watcher.zig @@ -354,10 +354,11 @@ pub const FSWatcher = struct { const relative_slice: string = brk: { var file_path_without_trailing_slash = std.mem.trimRight(u8, file_path, std.fs.path.sep_str); - @memcpy(&_on_file_update_path_buf, file_path_without_trailing_slash.ptr, file_path_without_trailing_slash.len); + @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 ..].ptr, changed_name.ptr, changed_name.len); + @memcpy(_on_file_update_path_buf[file_path_without_trailing_slash.len + 1 ..][0..changed_name.len], changed_name); const path_slice = _on_file_update_path_buf[0 .. file_path_without_trailing_slash.len + changed_name.len + 1]; file_hash = FSWatcher.Watcher.getHash(path_slice); @@ -787,14 +788,14 @@ pub const FSWatcher = struct { var result = PathResult{}; switch (stat.kind) { - .SymLink => { + .sym_link => { var file = try std.fs.openFileAbsoluteZ(absolute_path_z, .{ .mode = .read_only }); result.fd = file.handle; const _stat = try file.stat(); - result.is_file = _stat.kind == .Directory; + result.is_file = _stat.kind == .directory; }, - .Directory => { + .directory => { const dir = (try std.fs.openIterableDirAbsoluteZ(absolute_path_z, .{ .access_sub_paths = true, })).dir;