Compare commits

...

2 Commits

Author SHA1 Message Date
autofix-ci[bot]
ce0b89c3d5 [autofix.ci] apply automated fixes 2025-11-24 08:20:59 +00:00
Claude
f53b4d378f fix(Bun.write): ensure synchronous write ordering on Windows
Fixes #11117

On Windows, when `Bun.write(Bun.stdout, s[i])` was called multiple times
in a loop, each call went through the async `WriteFileWindows` path which
used `uv_fs_write` asynchronously via libuv. Multiple async operations
issued in quick succession could complete out of order, causing the output
to be scrambled.

The fix enables the synchronous fast path for file descriptor-based writes
(like stdout/stderr) on Windows, while still using the async path for
path-based writes that need mkdirp support. On Windows, `bun.sys.write()`
uses `kernel32.WriteFile()` which is synchronous, so writes are guaranteed
to complete in order.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 23:49:01 -08:00
2 changed files with 128 additions and 65 deletions

View File

@@ -1244,84 +1244,94 @@ pub fn writeFileInternal(globalThis: *jsc.JSGlobalObject, path_or_blob_: *PathOr
// If you're doing Bun.write(), try to go fast by writing short input on the main thread.
// This is a heuristic, but it's a good one.
//
// except if you're on Windows. Windows I/O is slower. Let's not even try.
if (comptime !Environment.isWindows) {
if (path_or_blob == .path or
// On Windows, we only use the fast path for file descriptor writes (like stdout/stderr)
// because path-based writes need async I/O for mkdirp support, and async writes to
// the same fd can complete out of order causing scrambled output (issue #11117).
// For file descriptors, we use synchronous WriteFile which maintains ordering.
const can_use_fast_path = if (comptime Environment.isWindows)
// On Windows, only use fast path for fd-based writes (not path-based)
(path_or_blob == .blob and
path_or_blob.blob.store != null and
path_or_blob.blob.store.?.data == .file and
path_or_blob.blob.store.?.data.file.pathlike == .fd and
path_or_blob.blob.offset == 0 and !path_or_blob.blob.isS3())
else
(path_or_blob == .path or
// If they try to set an offset, its a little more complicated so let's avoid that
(path_or_blob.blob.offset == 0 and !path_or_blob.blob.isS3() and
// Is this a file that is known to be a pipe? Let's avoid blocking the main thread on it.
!(path_or_blob.blob.store != null and
path_or_blob.blob.store.?.data == .file and
path_or_blob.blob.store.?.data.file.mode != 0 and
bun.isRegularFile(path_or_blob.blob.store.?.data.file.mode))))
{
if (data.isString()) {
const len = try data.getLength(globalThis);
bun.isRegularFile(path_or_blob.blob.store.?.data.file.mode))));
if (len < 256 * 1024) {
const str = try data.toBunString(globalThis);
defer str.deref();
if (can_use_fast_path) {
if (data.isString()) {
const len = try data.getLength(globalThis);
const pathlike: jsc.Node.PathOrFileDescriptor = if (path_or_blob == .path)
path_or_blob.path
else
path_or_blob.blob.store.?.data.file.pathlike;
if (len < 256 * 1024) {
const str = try data.toBunString(globalThis);
defer str.deref();
if (pathlike == .path) {
const result = writeStringToFileFast(
globalThis,
pathlike,
str,
&needs_async,
true,
);
if (!needs_async) {
return result;
}
} else {
const result = writeStringToFileFast(
globalThis,
pathlike,
str,
&needs_async,
false,
);
if (!needs_async) {
return result;
}
const pathlike: jsc.Node.PathOrFileDescriptor = if (path_or_blob == .path)
path_or_blob.path
else
path_or_blob.blob.store.?.data.file.pathlike;
if (pathlike == .path) {
const result = writeStringToFileFast(
globalThis,
pathlike,
str,
&needs_async,
true,
);
if (!needs_async) {
return result;
}
} else {
const result = writeStringToFileFast(
globalThis,
pathlike,
str,
&needs_async,
false,
);
if (!needs_async) {
return result;
}
}
} else if (data.asArrayBuffer(globalThis)) |buffer_view| {
if (buffer_view.byte_len < 256 * 1024) {
const pathlike: jsc.Node.PathOrFileDescriptor = if (path_or_blob == .path)
path_or_blob.path
else
path_or_blob.blob.store.?.data.file.pathlike;
}
} else if (data.asArrayBuffer(globalThis)) |buffer_view| {
if (buffer_view.byte_len < 256 * 1024) {
const pathlike: jsc.Node.PathOrFileDescriptor = if (path_or_blob == .path)
path_or_blob.path
else
path_or_blob.blob.store.?.data.file.pathlike;
if (pathlike == .path) {
const result = writeBytesToFileFast(
globalThis,
pathlike,
buffer_view.byteSlice(),
&needs_async,
true,
);
if (pathlike == .path) {
const result = writeBytesToFileFast(
globalThis,
pathlike,
buffer_view.byteSlice(),
&needs_async,
true,
);
if (!needs_async) {
return result;
}
} else {
const result = writeBytesToFileFast(
globalThis,
pathlike,
buffer_view.byteSlice(),
&needs_async,
false,
);
if (!needs_async) {
return result;
}
} else {
const result = writeBytesToFileFast(
globalThis,
pathlike,
buffer_view.byteSlice(),
&needs_async,
false,
);
if (!needs_async) {
return result;
}
if (!needs_async) {
return result;
}
}
}
@@ -1705,7 +1715,7 @@ fn writeBytesToFileFast(
if (truncate) {
if (Environment.isWindows) {
_ = std.os.windows.kernel32.SetEndOfFile(fd.cast());
_ = bun.windows.SetEndOfFile(fd.cast());
} else {
_ = bun.sys.ftruncate(fd, @as(i64, @intCast(written)));
}

View File

@@ -0,0 +1,53 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// Regression test for https://github.com/oven-sh/bun/issues/11117
// On Windows, Bun.write() to stdout in a loop would produce scrambled output
// because async writes were not serialized properly.
test("Bun.write to stdout maintains write order", async () => {
const testString = "This is a test\n";
// Run the test multiple times to catch any race conditions
for (let run = 0; run < 10; run++) {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`const test = (s) => { const l = s.length; for (let i = 0; i < l; i++) { Bun.write(Bun.stdout, s[i]); } }; test(${JSON.stringify(testString)});`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout).toBe(testString);
expect(exitCode).toBe(0);
}
});
test("Bun.write to stderr maintains write order", async () => {
const testString = "Error message test\n";
// Run the test multiple times to catch any race conditions
for (let run = 0; run < 10; run++) {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`const test = (s) => { const l = s.length; for (let i = 0; i < l; i++) { Bun.write(Bun.stderr, s[i]); } }; test(${JSON.stringify(testString)});`,
],
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).toBe(testString);
expect(exitCode).toBe(0);
}
});