aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
m---------src/bun.js/WebKit0
-rw-r--r--src/bun.js/bindings/JSSink.cpp2
-rw-r--r--src/bun.js/bindings/JSSink.h2
-rw-r--r--src/bun.js/bindings/ZigGeneratedClasses.cpp10
-rw-r--r--src/bun.js/bindings/ZigGeneratedClasses.h27
-rw-r--r--src/bun.js/bindings/generated_classes.zig1
-rw-r--r--src/bun.js/bindings/generated_classes_list.zig2
-rw-r--r--src/bun.js/node/node.classes.ts1
-rw-r--r--src/bun.js/node/node_fs_watcher.zig500
-rw-r--r--src/watcher.zig20
-rw-r--r--test/js/node/watch/fs.watch.test.js45
11 files changed, 351 insertions, 259 deletions
diff --git a/src/bun.js/WebKit b/src/bun.js/WebKit
-Subproject 4c8ab8fdfb102522fdd8e55d4eea53e8ce2755c
+Subproject b2f1006a06f81bc860c89dd4c7cec3e7117c4c4
diff --git a/src/bun.js/bindings/JSSink.cpp b/src/bun.js/bindings/JSSink.cpp
index 4acf01ff7..19bf05599 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-14T21:38:04.394Z
+// Generated by 'make generate-sink' at 2023-06-25T17:34:54.187Z
// 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 37c458e9b..9bf5554c4 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-14T21:38:04.394Z
+// Generated by 'make generate-sink' at 2023-06-25T17:34:54.186Z
//
#pragma once
diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp
index e0a3f33d6..387580d54 100644
--- a/src/bun.js/bindings/ZigGeneratedClasses.cpp
+++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp
@@ -5565,6 +5565,12 @@ void JSFSWatcherPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* glob
JSC_TO_STRING_TAG_WITHOUT_TRANSITION();
}
+extern "C" bool FSWatcher__hasPendingActivity(void* ptr);
+bool JSFSWatcher::hasPendingActivity(void* ctx)
+{
+ return FSWatcher__hasPendingActivity(ctx);
+}
+
JSFSWatcher::~JSFSWatcher()
{
if (m_ctx) {
@@ -5649,6 +5655,8 @@ void JSFSWatcher::visitChildrenImpl(JSCell* cell, Visitor& visitor)
ASSERT_GC_OBJECT_INHERITS(thisObject, info());
Base::visitChildren(thisObject, visitor);
visitor.append(thisObject->m_listener);
+
+ visitor.addOpaqueRoot(thisObject->wrapped());
}
DEFINE_VISIT_CHILDREN(JSFSWatcher);
@@ -5659,6 +5667,8 @@ void JSFSWatcher::visitAdditionalChildren(Visitor& visitor)
JSFSWatcher* thisObject = this;
ASSERT_GC_OBJECT_INHERITS(thisObject, info());
visitor.append(thisObject->m_listener);
+
+ visitor.addOpaqueRoot(this->wrapped());
}
DEFINE_VISIT_ADDITIONAL_CHILDREN(JSFSWatcher);
diff --git a/src/bun.js/bindings/ZigGeneratedClasses.h b/src/bun.js/bindings/ZigGeneratedClasses.h
index 3fa0e26d2..1631f960e 100644
--- a/src/bun.js/bindings/ZigGeneratedClasses.h
+++ b/src/bun.js/bindings/ZigGeneratedClasses.h
@@ -623,10 +623,37 @@ public:
: Base(vm, structure)
{
m_ctx = sinkPtr;
+ m_weakThis = JSC::Weak<JSFSWatcher>(this, getOwner());
}
void finishCreation(JSC::VM&);
+ JSC::Weak<JSFSWatcher> m_weakThis;
+
+ static bool hasPendingActivity(void* ctx);
+
+ class Owner final : public JSC::WeakHandleOwner {
+ public:
+ bool isReachableFromOpaqueRoots(JSC::Handle<JSC::Unknown> handle, void* context, JSC::AbstractSlotVisitor& visitor, const char** reason) final
+ {
+ auto* controller = JSC::jsCast<JSFSWatcher*>(handle.slot()->asCell());
+ if (JSFSWatcher::hasPendingActivity(controller->wrapped())) {
+ if (UNLIKELY(reason))
+ *reason = "has pending activity";
+ return true;
+ }
+
+ return visitor.containsOpaqueRoot(context);
+ }
+ void finalize(JSC::Handle<JSC::Unknown>, void* context) final {}
+ };
+
+ static JSC::WeakHandleOwner* getOwner()
+ {
+ static NeverDestroyed<Owner> m_owner;
+ return &m_owner.get();
+ }
+
DECLARE_VISIT_CHILDREN;
template<typename Visitor> void visitAdditionalChildren(Visitor&);
DECLARE_VISIT_OUTPUT_CONSTRAINTS;
diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig
index 74e30cd83..bdde69c1a 100644
--- a/src/bun.js/bindings/generated_classes.zig
+++ b/src/bun.js/bindings/generated_classes.zig
@@ -1492,6 +1492,7 @@ pub const JSFSWatcher = struct {
@export(FSWatcher.doRef, .{ .name = "FSWatcherPrototype__doRef" });
@export(FSWatcher.doUnref, .{ .name = "FSWatcherPrototype__doUnref" });
@export(FSWatcher.finalize, .{ .name = "FSWatcherClass__finalize" });
+ @export(FSWatcher.hasPendingActivity, .{ .name = "FSWatcher__hasPendingActivity" });
@export(FSWatcher.hasRef, .{ .name = "FSWatcherPrototype__hasRef" });
}
}
diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig
index d90267337..543d492b5 100644
--- a/src/bun.js/bindings/generated_classes_list.zig
+++ b/src/bun.js/bindings/generated_classes_list.zig
@@ -37,5 +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;
+ pub const FSWatcher = JSC.Node.FSWatcher;
};
diff --git a/src/bun.js/node/node.classes.ts b/src/bun.js/node/node.classes.ts
index ce35c940a..2efad5245 100644
--- a/src/bun.js/node/node.classes.ts
+++ b/src/bun.js/node/node.classes.ts
@@ -7,6 +7,7 @@ export default [
noConstructor: true,
finalize: true,
configurable: false,
+ hasPendingActivity: true,
klass: {},
JSType: "0b11101110",
proto: {
diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig
index 397d51916..b1f4ec8a9 100644
--- a/src/bun.js/node/node_fs_watcher.zig
+++ b/src/bun.js/node/node_fs_watcher.zig
@@ -4,6 +4,7 @@ const bun = @import("root").bun;
const Fs = @import("../../fs.zig");
const Path = @import("../../resolver/resolve_path.zig");
const Encoder = JSC.WebCore.Encoder;
+const Mutex = @import("../../lock.zig").Lock;
const FSEvents = @import("./fs_events.zig");
@@ -30,17 +31,28 @@ pub const FSWatcher = struct {
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();
- }
+ // JSObject
+ mutex: Mutex,
+ signal: ?*JSC.AbortSignal,
+ persistent: bool,
+ default_watcher: ?*FSWatcher.Watcher,
+ fsevents_watcher: ?*FSEvents.FSEventsWatcher,
+ poll_ref: JSC.PollRef = .{},
+ globalThis: *JSC.JSGlobalObject,
+ js_this: JSC.JSValue,
+ encoding: JSC.Node.Encoding,
+ // user can call close and pre-detach so we need to track this
+ closed: bool,
+ // counts pending tasks so we only deinit after all tasks are done
+ task_count: u32,
+ has_pending_activity: std.atomic.Atomic(bool),
+ pub usingnamespace JSC.Codegen.JSFSWatcher;
pub fn eventLoop(this: FSWatcher) *EventLoop {
return this.ctx.eventLoop();
@@ -51,6 +63,9 @@ pub const FSWatcher = struct {
}
pub fn deinit(this: *FSWatcher) void {
+ // stop all managers and signals
+ this.detach();
+
while (this.file_paths.popOrNull()) |file_path| {
bun.default_allocator.destroy(file_path);
}
@@ -107,41 +122,47 @@ pub const FSWatcher = struct {
}
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);
- },
- .abort => {
- js_watcher.emitIfAborted();
- },
- }
+ // this runs on JS Context Thread
+
+ for (this.entries[0..this.count]) |entry| {
+ switch (entry.event_type) {
+ .rename => {
+ this.ctx.emit(entry.file_path, "rename");
+ },
+ .change => {
+ this.ctx.emit(entry.file_path, "change");
+ },
+ .@"error" => {
+ // file_path is the error message in this case
+ this.ctx.emitError(entry.file_path);
+ },
+ .abort => {
+ this.ctx.emitIfAborted();
+ },
}
}
+
+ this.ctx.unrefTask();
}
pub fn enqueue(this: *FSWatchTask) void {
if (this.count == 0)
return;
- var that = bun.default_allocator.create(FSWatchTask) catch unreachable;
+ // if false is closed or detached (can still contain valid refs but will not create a new one)
+ if (this.ctx.refTask()) {
+ 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);
+ that.* = this.*;
+ this.count = 0;
+ that.concurrent_task.task = JSC.Task.init(that);
+ this.ctx.enqueueTaskConcurrent(&that.concurrent_task);
+ return;
+ }
+ // closed or detached so just cleanEntries
+ this.cleanEntries();
}
-
- pub fn deinit(this: *FSWatchTask) void {
+ pub fn cleanEntries(this: *FSWatchTask) void {
while (this.count > 0) {
this.count -= 1;
switch (this.entries[this.count].free_type) {
@@ -150,6 +171,10 @@ pub const FSWatcher = struct {
else => {},
}
}
+ }
+
+ pub fn deinit(this: *FSWatchTask) void {
+ this.cleanEntries();
bun.default_allocator.destroy(this);
}
};
@@ -275,7 +300,7 @@ pub const FSWatcher = struct {
const kinds = slice.items(.kind);
var _on_file_update_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
- var ctx = this.watcher_instance.?;
+ var ctx = this.default_watcher.?;
defer ctx.flushEvictions();
defer Output.flush();
@@ -540,241 +565,225 @@ pub const FSWatcher = struct {
pub fn createFSWatcher(this: Arguments) !JSC.JSValue {
const obj = try FSWatcher.init(this);
- return obj.toJS();
+ if (obj.js_this != .zero) {
+ return obj.js_this;
+ }
+ return JSC.JSValue.jsUndefined();
}
};
- pub const JSObject = struct {
- signal: ?*JSC.AbortSignal,
- persistent: bool,
- manager: ?*FSWatcher.Watcher,
- fsevents_watcher: ?*FSEvents.FSEventsWatcher,
- poll_ref: JSC.PollRef = .{},
- globalThis: ?*JSC.JSGlobalObject,
- js_this: JSC.JSValue,
- encoding: JSC.Node.Encoding,
- closed: bool,
-
- pub usingnamespace JSC.Codegen.JSFSWatcher;
-
- 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 initJS(this: *FSWatcher, listener: JSC.JSValue) void {
+ if (this.persistent) {
+ this.poll_ref.ref(this.ctx);
}
- 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(instance.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()) {
- obj.signal = s.ref();
- // abort next tick
- var current_task: FSWatchTask = .{
- .ctx = instance,
- };
- current_task.append("", .abort, .none);
- current_task.enqueue();
- } else {
- // watch for abortion
- obj.signal = s.ref().listen(JSObject, obj, JSObject.emitAbort);
- }
+ const js_this = FSWatcher.toJS(this, this.globalThis);
+ js_this.ensureStillAlive();
+ this.js_this = js_this;
+ FSWatcher.listenerSetCached(js_this, this.globalThis, listener);
+
+ if (this.signal) |s| {
+ // already aborted?
+ if (s.aborted()) {
+ // safely abort next tick
+ var current_task: FSWatchTask = .{
+ .ctx = this,
+ };
+ current_task.append("", .abort, .none);
+ current_task.enqueue();
+ } else {
+ // watch for abortion
+ this.signal = s.listen(FSWatcher, this, FSWatcher.emitAbort);
}
- return obj;
}
+ }
- pub fn emitIfAborted(this: *JSObject) void {
- if (this.signal) |s| {
- if (s.aborted()) {
- const err = s.abortReason();
- this.emitAbort(err);
- }
+ pub fn emitIfAborted(this: *FSWatcher) void {
+ if (this.signal) |s| {
+ if (s.aborted()) {
+ const err = s.abortReason();
+ this.emitAbort(err);
}
}
+ }
- pub fn emitAbort(this: *JSObject, err: JSC.JSValue) void {
- if (this.closed) return;
- defer this.close(true);
-
- 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 emitAbort(this: *FSWatcher, err: JSC.JSValue) void {
+ if (this.closed) return;
+ defer this.close();
+
+ err.ensureStillAlive();
+ if (this.js_this != .zero) {
+ const js_this = this.js_this;
+ js_this.ensureStillAlive();
+ if (FSWatcher.listenerGetCached(js_this)) |listener| {
+ listener.ensureStillAlive();
+ var args = [_]JSC.JSValue{
+ JSC.ZigString.static("error").toValue(this.globalThis),
+ if (err.isEmptyOrUndefinedOrNull()) JSC.WebCore.AbortSignal.createAbortError(JSC.ZigString.static("The user aborted a request"), &JSC.ZigString.Empty, this.globalThis) else err,
+ };
+ _ = listener.callWithGlobalThis(
+ this.globalThis,
+ &args,
+ );
}
}
- pub fn emitError(this: *JSObject, err: string) void {
- if (this.closed) return;
- defer this.close(true);
-
- 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 emitError(this: *FSWatcher, err: string) void {
+ if (this.closed) return;
+ defer this.close();
+
+ if (this.js_this != .zero) {
+ const js_this = this.js_this;
+ js_this.ensureStillAlive();
+ if (FSWatcher.listenerGetCached(js_this)) |listener| {
+ listener.ensureStillAlive();
+ var args = [_]JSC.JSValue{
+ JSC.ZigString.static("error").toValue(this.globalThis),
+ JSC.ZigString.fromUTF8(err).toErrorInstance(this.globalThis),
+ };
+ _ = listener.callWithGlobalThis(
+ this.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 {
- // convert to desired encoding
- filename = Encoder.toStringAtRuntime(file_name.ptr, file_name.len, globalThis, this.encoding);
- }
- }
- var args = [_]JSC.JSValue{
- JSC.ZigString.static(eventType).toValue(globalThis),
- filename,
- };
- _ = listener.callWithGlobalThis(
- globalThis,
- &args,
- );
+ pub fn emit(this: *FSWatcher, file_name: string, comptime eventType: string) void {
+ if (this.js_this != .zero) {
+ const js_this = this.js_this;
+ js_this.ensureStillAlive();
+ if (FSWatcher.listenerGetCached(js_this)) |listener| {
+ listener.ensureStillAlive();
+ var filename: JSC.JSValue = JSC.JSValue.jsUndefined();
+ if (file_name.len > 0) {
+ if (this.encoding == .buffer)
+ filename = JSC.ArrayBuffer.createBuffer(this.globalThis, file_name)
+ else if (this.encoding == .utf8) {
+ filename = JSC.ZigString.fromUTF8(file_name).toValueGC(this.globalThis);
+ } else {
+ // convert to desired encoding
+ filename = Encoder.toStringAtRuntime(file_name.ptr, file_name.len, this.globalThis, this.encoding);
}
}
+ var args = [_]JSC.JSValue{
+ JSC.ZigString.static(eventType).toValue(this.globalThis),
+ filename,
+ };
+ _ = listener.callWithGlobalThis(
+ this.globalThis,
+ &args,
+ );
}
}
+ }
- pub fn ref(this: *JSObject) void {
- if (this.closed) return;
-
- if (!this.persistent) {
- this.persistent = true;
- this.poll_ref.ref(this.getFSWatcher().ctx);
- }
- }
-
- pub fn doRef(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue {
- this.ref();
- return JSC.JSValue.jsUndefined();
+ pub fn doRef(this: *FSWatcher, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue {
+ if (!this.closed and !this.persistent) {
+ this.persistent = true;
+ this.poll_ref.ref(this.ctx);
}
+ return JSC.JSValue.jsUndefined();
+ }
- pub fn unref(this: *JSObject) void {
- if (this.persistent) {
- this.persistent = false;
- this.poll_ref.unref(this.getFSWatcher().ctx);
- }
+ pub fn doUnref(this: *FSWatcher, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue {
+ if (this.persistent) {
+ this.persistent = false;
+ this.poll_ref.unref(this.ctx);
}
+ return JSC.JSValue.jsUndefined();
+ }
- pub fn doUnref(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue {
- this.unref();
- return JSC.JSValue.jsUndefined();
- }
+ pub fn hasRef(this: *FSWatcher, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue {
+ return JSC.JSValue.jsBoolean(this.persistent);
+ }
- pub fn hasRef(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue {
- return JSC.JSValue.jsBoolean(this.persistent);
- }
+ // this can be called from Watcher Thread or JS Context Thread
+ pub fn refTask(this: *FSWatcher) bool {
+ this.mutex.lock();
+ defer this.mutex.unlock();
+ // stop new references
+ if (this.closed) return false;
+ this.task_count += 1;
+ return true;
+ }
- pub fn close(
- this: *JSObject,
- emitEvent: bool,
- ) void {
- if (!this.closed) {
- if (this.signal) |signal| {
- this.signal = null;
- signal.detach(this);
- }
- this.closed = true;
- if (emitEvent) {
- this.emit("", "close");
- }
+ pub fn hasPendingActivity(this: *FSWatcher) callconv(.C) bool {
+ @fence(.Acquire);
+ return this.has_pending_activity.load(.Acquire);
+ }
+ // only called from Main Thread
+ pub fn updateHasPendingActivity(this: *FSWatcher) void {
+ @fence(.Release);
+ this.has_pending_activity.store(false, .Release);
+ }
- this.detach();
- }
+ // unref is always called on main JS Context Thread
+ pub fn unrefTask(this: *FSWatcher) void {
+ this.mutex.lock();
+ defer this.mutex.unlock();
+ this.task_count -= 1;
+ if (this.closed and this.task_count == 0) {
+ this.updateHasPendingActivity();
}
+ }
- pub fn detach(this: *JSObject) void {
- this.unref();
+ pub fn close(
+ this: *FSWatcher,
+ ) void {
+ this.mutex.lock();
+ if (!this.closed) {
+ this.closed = true;
- if (this.js_this != .zero) {
- this.js_this.unprotect();
- this.js_this = .zero;
- }
+ // emit should only be called unlocked
+ this.mutex.unlock();
- this.globalThis = null;
+ this.emit("", "close");
+ // we immediately detach here
+ this.detach();
- if (this.signal) |signal| {
- 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);
+ // no need to lock again, because ref checks closed and unref is only called on main thread
+ if (this.task_count == 0) {
+ this.updateHasPendingActivity();
}
+ } else {
+ this.mutex.unlock();
+ }
+ }
- if (this.fsevents_watcher) |manager| {
- var ctx = bun.cast(*FSWatcher, manager.ctx.?);
- ctx.js_watcher = null;
- ctx.deinit();
- manager.deinit();
- }
+ // this can be called multiple times
+ pub fn detach(this: *FSWatcher) void {
+ if (this.persistent) {
+ this.persistent = false;
+ this.poll_ref.unref(this.ctx);
}
- pub fn doClose(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue {
- this.close(true);
- return JSC.JSValue.jsUndefined();
+ if (this.signal) |signal| {
+ this.signal = null;
+ signal.detach(this);
}
- pub fn finalize(this: *JSObject) callconv(.C) void {
- if (!this.closed) {
- this.detach();
- }
+ if (this.default_watcher) |default_watcher| {
+ this.default_watcher = null;
+ default_watcher.deinit(true);
+ }
- bun.default_allocator.destroy(this);
+ if (this.fsevents_watcher) |fsevents_watcher| {
+ this.fsevents_watcher = null;
+ fsevents_watcher.deinit();
}
- };
+
+ this.js_this = .zero;
+ }
+
+ pub fn doClose(this: *FSWatcher, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue {
+ this.close();
+ return JSC.JSValue.jsUndefined();
+ }
+
+ pub fn finalize(this: *FSWatcher) callconv(.C) void {
+ this.deinit();
+ }
const PathResult = struct {
fd: StoredFileDescriptorType = 0,
@@ -837,6 +846,17 @@ pub const FSWatcher = struct {
const vm = args.global_this.bunVM();
ctx.* = .{
.ctx = vm,
+ .mutex = Mutex.init(),
+ .signal = if (args.signal) |s| s.ref() else null,
+ .persistent = args.persistent,
+ .default_watcher = null,
+ .fsevents_watcher = null,
+ .globalThis = args.global_this,
+ .js_this = .zero,
+ .encoding = args.encoding,
+ .closed = false,
+ .task_count = 0,
+ .has_pending_activity = std.atomic.Atomic(bool).init(true),
.verbose = args.verbose,
.file_paths = bun.BabyList(string).initCapacity(bun.default_allocator, 1) catch |err| {
ctx.deinit();
@@ -850,22 +870,17 @@ pub const FSWatcher = struct {
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.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;
- };
-
+ ctx.initJS(args.listener);
return ctx;
}
}
- var fs_watcher = FSWatcher.Watcher.init(
+ var default_watcher = FSWatcher.Watcher.init(
ctx,
vm.bundler.fs,
bun.default_allocator,
@@ -874,7 +889,7 @@ pub const FSWatcher = struct {
return err;
};
- ctx.watcher_instance = fs_watcher;
+ ctx.default_watcher = default_watcher;
if (fs_type.is_file) {
var file_path_clone = bun.default_allocator.dupeZ(u8, file_path) catch unreachable;
@@ -882,32 +897,23 @@ pub const FSWatcher = struct {
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| {
+ default_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, true) catch |err| {
+ addDirectory(ctx, default_watcher, fs_type.fd, file_path, args.recursive, &buf, true) catch |err| {
ctx.deinit();
- fs_watcher.deinit(true);
return err;
};
}
- fs_watcher.start() catch |err| {
+ default_watcher.start() catch |err| {
ctx.deinit();
-
- fs_watcher.deinit(true);
- return 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;
};
+ ctx.initJS(args.listener);
return ctx;
}
};
diff --git a/src/watcher.zig b/src/watcher.zig
index 044770dc4..e3b3600ad 100644
--- a/src/watcher.zig
+++ b/src/watcher.zig
@@ -519,7 +519,7 @@ pub fn NewWatcher(comptime ContextType: type) type {
var changelist_array: [128]KEvent = std.mem.zeroes([128]KEvent);
var changelist = &changelist_array;
- while (this.running) {
+ while (true) {
defer Output.flush();
var count_ = std.os.system.kevent(
@@ -576,10 +576,12 @@ pub fn NewWatcher(comptime ContextType: type) type {
defer this.mutex.unlock();
if (this.running) {
this.ctx.onFileUpdate(watchevents, this.changed_filepaths[0..watchevents.len], this.watchlist);
+ } else {
+ break;
}
}
} else if (Environment.isLinux) {
- restart: while (this.running) {
+ restart: while (true) {
defer Output.flush();
var events = try INotify.read();
@@ -588,14 +590,14 @@ pub fn NewWatcher(comptime ContextType: type) type {
// TODO: is this thread safe?
var remaining_events = events.len;
- var name_off: u8 = 0;
- var temp_name_list: [128]?[:0]u8 = undefined;
- var temp_name_off: u8 = 0;
-
const eventlist_index = this.watchlist.items(.eventlist_index);
while (remaining_events > 0) {
- const slice = events[0..@min(remaining_events, this.watch_events.len)];
+ var name_off: u8 = 0;
+ var temp_name_list: [128]?[:0]u8 = undefined;
+ var temp_name_off: u8 = 0;
+
+ const slice = events[0..@min(128, remaining_events, this.watch_events.len)];
var watchevents = this.watch_events[0..slice.len];
var watch_event_id: u32 = 0;
for (slice) |event| {
@@ -647,8 +649,10 @@ pub fn NewWatcher(comptime ContextType: type) type {
defer this.mutex.unlock();
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;
+ } else {
+ break;
}
+ remaining_events -= slice.len;
}
}
}
diff --git a/test/js/node/watch/fs.watch.test.js b/test/js/node/watch/fs.watch.test.js
index 56e1798f1..33d05df29 100644
--- a/test/js/node/watch/fs.watch.test.js
+++ b/test/js/node/watch/fs.watch.test.js
@@ -17,6 +17,8 @@ const testDir = tempDirWithFiles("watch", {
"relative.txt": "hello",
"abort.txt": "hello",
"url.txt": "hello",
+ "close.txt": "hello",
+ "close-close.txt": "hello",
[encodingFileName]: "hello",
});
@@ -105,6 +107,7 @@ describe("fs.watch", () => {
let err = undefined;
watcher.on("change", (event, filename) => {
const basename = path.basename(filename);
+
if (basename === "subfolder") return;
count++;
try {
@@ -274,6 +277,46 @@ describe("fs.watch", () => {
}
});
+ test("calling close from error event should not throw", done => {
+ const filepath = path.join(testDir, "close.txt");
+ try {
+ const ac = new AbortController();
+ const watcher = fs.watch(pathToFileURL(filepath), { signal: ac.signal });
+ watcher.once("error", () => {
+ try {
+ watcher.close();
+ done();
+ } catch (e) {
+ done("Should not error when calling close from error event");
+ }
+ });
+ ac.abort();
+ } catch (e) {
+ done(e);
+ }
+ });
+
+ test("calling close from close event should not throw", done => {
+ const filepath = path.join(testDir, "close-close.txt");
+ try {
+ const ac = new AbortController();
+ const watcher = fs.watch(pathToFileURL(filepath), { signal: ac.signal });
+
+ watcher.once("close", () => {
+ try {
+ watcher.close();
+ done();
+ } catch (e) {
+ done("Should not error when calling close from close event");
+ }
+ });
+
+ ac.abort();
+ } catch (e) {
+ done(e);
+ }
+ });
+
test("Signal aborted after creating the watcher", async () => {
const filepath = path.join(testDir, "abort.txt");
@@ -300,7 +343,7 @@ describe("fs.watch", () => {
});
});
-describe("fs.promises.watchFile", () => {
+describe("fs.promises.watch", () => {
test("add file/folder to folder", async () => {
let count = 0;
const root = path.join(testDir, "add-promise-directory");