Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
07aa2e7f63 fix(tty): avoid kqueue registration for TTY fds on macOS (#24158)
On macOS, kqueue cannot monitor /dev/tty, causing EINVAL when attempting
to register it. This fix ensures that when force_sync is true (which is
set for TTYs detected via isatty()), the FileSink uses non-pollable mode
by passing pollable=false to writer.start().

This also removes the Windows-specific branch that was redundant since
the same logic now applies on all platforms where force_sync is set.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 22:10:21 +00:00
2 changed files with 104 additions and 19 deletions

View File

@@ -320,27 +320,12 @@ pub fn setup(this: *FileSink, options: *const FileSink.Options) bun.sys.Maybe(vo
.result => |fd| fd,
};
if (comptime Environment.isWindows) {
if (this.force_sync) {
switch (this.writer.startSync(
fd,
this.pollable,
)) {
.err => |err| {
fd.close();
return .{ .err = err };
},
.result => {
this.writer.updateRef(this.eventLoop(), false);
},
}
return .success;
}
}
// When force_sync is true (e.g., for TTYs on macOS), don't register with kqueue.
// kqueue cannot monitor /dev/tty on macOS, which would cause EINVAL.
// See: https://github.com/oven-sh/bun/issues/24158
switch (this.writer.start(
fd,
this.pollable,
if (this.force_sync) false else this.pollable,
)) {
.err => |err| {
fd.close();

View File

@@ -0,0 +1,100 @@
import { describe, expect, it } from "bun:test";
import { bunEnv, bunExe, isMacOS } from "harness";
// https://github.com/oven-sh/bun/issues/24158
// tty.WriteStream fails with EINVAL: invalid argument, kqueue on macOS
// when opening /dev/tty because kqueue cannot monitor /dev/tty on macOS.
describe.if(isMacOS)("issue #24158", () => {
it("tty.WriteStream should work with /dev/tty", async () => {
// We can't test /dev/tty directly in unit tests as it requires an actual terminal.
// Instead, spawn a subprocess that attempts to create the WriteStream.
// If the bug is present, it will throw EINVAL from kqueue.
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const tty = require('node:tty');
const fs = require('node:fs');
try {
// Only run this test if /dev/tty exists and is accessible
const fd = fs.openSync("/dev/tty", "w");
const stream = new tty.WriteStream(fd);
stream.write("test");
stream.end();
fs.closeSync(fd);
console.log("success");
} catch (e) {
// If /dev/tty is not available (e.g., in CI without a TTY),
// that's expected - we're testing that it doesn't fail with EINVAL from kqueue
if (e.code === 'ENXIO' || e.code === 'ENOENT' || e.code === 'ENOTTY' || e.message.includes('not a tty')) {
console.log("no-tty");
} else {
console.error("error:", e.code || e.message);
process.exit(1);
}
}
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// The key assertion: if the bug is present, we'd see "error: EINVAL" or the process would crash
// with "EINVAL: invalid argument, kqueue"
if (stdout.trim() === "success" || stdout.trim() === "no-tty") {
expect(exitCode).toBe(0);
} else {
// If there's an error, it should not be EINVAL from kqueue
expect(stderr).not.toContain("kqueue");
expect(stderr).not.toContain("EINVAL");
expect(stdout).not.toContain("error: EINVAL");
}
});
it("Bun.file(fd).writer() should work with TTY fds", async () => {
// Test the direct code path that was causing the issue:
// Bun.file(fd).writer() -> FileSink.setup() -> writer.start()
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const fs = require('node:fs');
try {
const fd = fs.openSync("/dev/tty", "w");
const writer = Bun.file(fd).writer();
writer.write("test");
writer.end();
console.log("success");
} catch (e) {
if (e.code === 'ENXIO' || e.code === 'ENOENT' || e.code === 'ENOTTY' || e.message?.includes('not a tty')) {
console.log("no-tty");
} else {
console.error("error:", e.code || e.message);
process.exit(1);
}
}
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
if (stdout.trim() === "success" || stdout.trim() === "no-tty") {
expect(exitCode).toBe(0);
} else {
expect(stderr).not.toContain("kqueue");
expect(stderr).not.toContain("EINVAL");
expect(stdout).not.toContain("error: EINVAL");
}
});
});