Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 32 additions & 4 deletions src/watcher/WindowsWatcher.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ iocp: w.HANDLE = undefined,
watcher: DirWatcher = undefined,
buf: bun.PathBuffer = undefined,
base_idx: usize = 0,
/// Track if handles have been closed to prevent double-close
handles_closed: bool = false,
Comment on lines +10 to +11
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Reset handles_closed in init to avoid leaks on reuse

The flag is great for preventing double-closes, but if a WindowsWatcher instance is ever re‑initialized (calling init again after stop/shutdown), handles_closed will still be true, so closeHandles() becomes a no‑op and new handles will leak.

Consider explicitly clearing it at the start of init:

 pub fn init(this: *WindowsWatcher, root: []const u8) !void {
+    this.handles_closed = false;
+
     var pathbuf: bun.WPathBuffer = undefined;
     const wpath = bun.strings.toNTPath(&pathbuf, root);

This is a cheap change and makes the lifecycle more robust even if reuse is rare.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/watcher/WindowsWatcher.zig around lines 10-11, the boolean flag
handles_closed is never reset when an instance is re-initialized, so after
stop/shutdown handles_closed remains true and subsequent init calls will skip
closing old handles causing leaks; modify the init function to explicitly set
self.handles_closed = false at the start of initialization (before allocating or
opening any handles) so the lifecycle can be reused safely, and ensure any
concurrent access is still correctly synchronized if applicable.


pub const EventListIndex = c_int;

Expand Down Expand Up @@ -173,9 +175,9 @@ pub fn next(this: *WindowsWatcher, timeout: Timeout) bun.sys.Maybe(?EventIterato
continue;
}
if (nbytes == 0) {
// shutdown notification
// TODO close handles?
// shutdown notification - close handles immediately to prevent resource leaks
log("shutdown notification in WindowsWatcher.next", .{});
this.closeHandles();
return .{ .err = .{
Comment on lines 177 to 181
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Good: close on shutdown; optionally short‑circuit next after ESHUTDOWN

Closing handles on the nbytes == 0 “shutdown” completion is exactly what was missing and matches the intent.

One small hardening you might consider: if next is accidentally called again after this path, it will call GetQueuedCompletionStatus on an iocp that closeHandles() has already set to w.INVALID_HANDLE_VALUE. Windows will fail that call, but you could avoid making the syscall at all by bailing out early when the watcher is already shut down:

 pub fn next(this: *WindowsWatcher, timeout: Timeout) bun.sys.Maybe(?EventIterator) {
+    if (this.handles_closed) {
+        return .{ .err = .{
+            .errno = @intFromEnum(bun.sys.SystemErrno.ESHUTDOWN),
+            .syscall = .watch,
+        } };
+    }
+
     switch (this.watcher.prepare()) {

Not required for correctness given current callers, but it makes the API more self‑defensive.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/watcher/WindowsWatcher.zig around lines 177 to 181, add a defensive
early-return in next so it does not call GetQueuedCompletionStatus after
closeHandles has invalidated the iocp; detect the shutdown state (e.g. check if
this.iocp == w.INVALID_HANDLE_VALUE or a dedicated closed flag) at the top of
next and immediately return the same shutdown error/result used for the nbytes
== 0 path so subsequent calls short-circuit instead of making a syscall on an
invalid handle.

.errno = @intFromEnum(bun.sys.SystemErrno.ESHUTDOWN),
.syscall = .watch,
Expand All @@ -192,9 +194,35 @@ pub fn next(this: *WindowsWatcher, timeout: Timeout) bun.sys.Maybe(?EventIterato
}
}

/// Close handles if they haven't been closed already.
/// This function is idempotent and safe to call multiple times.
///
/// When a shutdown notification is received from the IOCP, we need to close
/// handles immediately to prevent resource leaks. This function ensures handles
/// are properly closed and marked as invalid to prevent double-close errors.
fn closeHandles(this: *WindowsWatcher) void {
if (this.handles_closed) {
return;
}
this.handles_closed = true;

// Close directory handle if it's still valid
if (this.watcher.dirHandle != w.INVALID_HANDLE_VALUE) {
_ = bun.windows.CloseHandle(this.watcher.dirHandle);
this.watcher.dirHandle = w.INVALID_HANDLE_VALUE;
log("closed directory handle", .{});
}

// Close IOCP handle if it's still valid
if (this.iocp != w.INVALID_HANDLE_VALUE) {
_ = bun.windows.CloseHandle(this.iocp);
this.iocp = w.INVALID_HANDLE_VALUE;
log("closed IOCP handle", .{});
}
}

pub fn stop(this: *WindowsWatcher) void {
w.CloseHandle(this.watcher.dirHandle);
w.CloseHandle(this.iocp);
this.closeHandles();
}

pub fn watchLoopCycle(this: *bun.Watcher) bun.sys.Maybe(void) {
Expand Down