mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
fix(windows): keep event loop alive during Bun.file() async reads
ReadFileUV was not calling refConcurrently()/unrefConcurrently() on the event loop, causing the process to exit prematurely before the async libuv file read operations completed. This resulted in Bun.file().text() silently failing (not throwing ENOENT) when reading non-existent files on Windows. Fixes #26632 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -138,7 +138,7 @@ pub fn doReadFile(this: *Blob, comptime Function: anytype, global: *JSGlobalObje
|
|||||||
promise_value.ensureStillAlive();
|
promise_value.ensureStillAlive();
|
||||||
handler.promise.strong.set(global, promise_value);
|
handler.promise.strong.set(global, promise_value);
|
||||||
|
|
||||||
read_file.ReadFileUV.start(handler.globalThis.bunVM().uvLoop(), this.store.?, this.offset, this.size, Handler, handler);
|
read_file.ReadFileUV.start(handler.globalThis.bunVM().eventLoop(), this.store.?, this.offset, this.size, Handler, handler);
|
||||||
|
|
||||||
return promise_value;
|
return promise_value;
|
||||||
}
|
}
|
||||||
@@ -180,7 +180,7 @@ pub fn NewInternalReadFileHandler(comptime Context: type, comptime Function: any
|
|||||||
pub fn doReadFileInternal(this: *Blob, comptime Handler: type, ctx: Handler, comptime Function: anytype, global: *JSGlobalObject) void {
|
pub fn doReadFileInternal(this: *Blob, comptime Handler: type, ctx: Handler, comptime Function: anytype, global: *JSGlobalObject) void {
|
||||||
if (Environment.isWindows) {
|
if (Environment.isWindows) {
|
||||||
const ReadFileHandler = NewInternalReadFileHandler(Handler, Function);
|
const ReadFileHandler = NewInternalReadFileHandler(Handler, Function);
|
||||||
return read_file.ReadFileUV.start(libuv.Loop.get(), this.store.?, this.offset, this.size, ReadFileHandler, ctx);
|
return read_file.ReadFileUV.start(global.bunVM().eventLoop(), this.store.?, this.offset, this.size, ReadFileHandler, ctx);
|
||||||
}
|
}
|
||||||
const file_read = read_file.ReadFile.createWithCtx(
|
const file_read = read_file.ReadFile.createWithCtx(
|
||||||
bun.default_allocator,
|
bun.default_allocator,
|
||||||
|
|||||||
@@ -523,6 +523,7 @@ pub const ReadFileUV = struct {
|
|||||||
pub const doClose = FileCloser(@This()).doClose;
|
pub const doClose = FileCloser(@This()).doClose;
|
||||||
|
|
||||||
loop: *libuv.Loop,
|
loop: *libuv.Loop,
|
||||||
|
event_loop: *jsc.EventLoop,
|
||||||
file_store: FileStore,
|
file_store: FileStore,
|
||||||
byte_store: ByteStore = ByteStore{ .allocator = bun.default_allocator },
|
byte_store: ByteStore = ByteStore{ .allocator = bun.default_allocator },
|
||||||
store: *Store,
|
store: *Store,
|
||||||
@@ -543,10 +544,11 @@ pub const ReadFileUV = struct {
|
|||||||
|
|
||||||
req: libuv.fs_t = std.mem.zeroes(libuv.fs_t),
|
req: libuv.fs_t = std.mem.zeroes(libuv.fs_t),
|
||||||
|
|
||||||
pub fn start(loop: *libuv.Loop, store: *Store, off: SizeType, max_len: SizeType, comptime Handler: type, handler: *anyopaque) void {
|
pub fn start(event_loop: *jsc.EventLoop, store: *Store, off: SizeType, max_len: SizeType, comptime Handler: type, handler: *anyopaque) void {
|
||||||
log("ReadFileUV.start", .{});
|
log("ReadFileUV.start", .{});
|
||||||
var this = bun.new(ReadFileUV, .{
|
var this = bun.new(ReadFileUV, .{
|
||||||
.loop = loop,
|
.loop = event_loop.virtual_machine.uvLoop(),
|
||||||
|
.event_loop = event_loop,
|
||||||
.file_store = store.data.file,
|
.file_store = store.data.file,
|
||||||
.store = store,
|
.store = store,
|
||||||
.offset = off,
|
.offset = off,
|
||||||
@@ -555,15 +557,20 @@ pub const ReadFileUV = struct {
|
|||||||
.on_complete_fn = @ptrCast(&Handler.run),
|
.on_complete_fn = @ptrCast(&Handler.run),
|
||||||
});
|
});
|
||||||
store.ref();
|
store.ref();
|
||||||
|
// Keep the event loop alive while the async operation is pending
|
||||||
|
event_loop.refConcurrently();
|
||||||
this.getFd(onFileOpen);
|
this.getFd(onFileOpen);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn finalize(this: *ReadFileUV) void {
|
pub fn finalize(this: *ReadFileUV) void {
|
||||||
log("ReadFileUV.finalize", .{});
|
log("ReadFileUV.finalize", .{});
|
||||||
|
const event_loop = this.event_loop;
|
||||||
defer {
|
defer {
|
||||||
this.store.deref();
|
this.store.deref();
|
||||||
this.req.deinit();
|
this.req.deinit();
|
||||||
bun.destroy(this);
|
bun.destroy(this);
|
||||||
|
// Release the event loop reference now that we're done
|
||||||
|
event_loop.unrefConcurrently();
|
||||||
log("ReadFileUV.finalize destroy", .{});
|
log("ReadFileUV.finalize destroy", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
58
test/regression/issue/26632.test.ts
Normal file
58
test/regression/issue/26632.test.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { bunEnv, bunExe, tempDir } from "harness";
|
||||||
|
|
||||||
|
// https://github.com/oven-sh/bun/issues/26632
|
||||||
|
// Bun.file().text() on a non-existent file should throw ENOENT error, not silently exit
|
||||||
|
test("Bun.file().text() on nonexistent file throws ENOENT", async () => {
|
||||||
|
using dir = tempDir("26632", {});
|
||||||
|
|
||||||
|
await using proc = Bun.spawn({
|
||||||
|
cmd: [bunExe(), "-e", `await Bun.file("nonexistent-file-that-does-not-exist.txt").text();`],
|
||||||
|
cwd: String(dir),
|
||||||
|
env: bunEnv,
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||||
|
|
||||||
|
expect(stdout).toBe("");
|
||||||
|
expect(stderr).toContain("ENOENT");
|
||||||
|
expect(exitCode).not.toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Bun.file().arrayBuffer() on nonexistent file throws ENOENT", async () => {
|
||||||
|
using dir = tempDir("26632", {});
|
||||||
|
|
||||||
|
await using proc = Bun.spawn({
|
||||||
|
cmd: [bunExe(), "-e", `await Bun.file("nonexistent-file-that-does-not-exist.txt").arrayBuffer();`],
|
||||||
|
cwd: String(dir),
|
||||||
|
env: bunEnv,
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||||
|
|
||||||
|
expect(stdout).toBe("");
|
||||||
|
expect(stderr).toContain("ENOENT");
|
||||||
|
expect(exitCode).not.toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Bun.file().bytes() on nonexistent file throws ENOENT", async () => {
|
||||||
|
using dir = tempDir("26632", {});
|
||||||
|
|
||||||
|
await using proc = Bun.spawn({
|
||||||
|
cmd: [bunExe(), "-e", `await Bun.file("nonexistent-file-that-does-not-exist.txt").bytes();`],
|
||||||
|
cwd: String(dir),
|
||||||
|
env: bunEnv,
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||||
|
|
||||||
|
expect(stdout).toBe("");
|
||||||
|
expect(stderr).toContain("ENOENT");
|
||||||
|
expect(exitCode).not.toBe(0);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user