From 17e87b678fe114e461157cc585debf9802950627 Mon Sep 17 00:00:00 2001 From: Alex Kornitzer Date: Tue, 16 Apr 2024 20:50:35 +0100 Subject: [PATCH] fix(posix): use fcntl to check if a fd is nonblocking (#10080) The FileReader is responsible for deciding what sort of file descriptor has been passed to it. On MacOS, when stdin is provided to it the FileReader assumes that it is nonblocking even if the descriptor is not as it passes the pollable and not a tty check, this then results in hangs as the reader expects `EAGAIN`. To fix this we now check the file descriptor to see if it is nonblocking rather than using assumptions. --- src/bun.js/webcore/streams.zig | 2 +- src/bun.zig | 4 ++++ test/regression/issue/10080.test.ts | 33 +++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 test/regression/issue/10080.test.ts diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index a142b9919fb16e..aaf5d5a50a6513 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -3529,7 +3529,7 @@ pub const FileReader = struct { this.file_type = .nonblocking_pipe; } - this.nonblocking = is_nonblocking_tty or (this.pollable and !(file.is_atty orelse false)); + this.nonblocking = is_nonblocking_tty or bun.isNonBlocking(fd.int()); if (this.nonblocking and this.file_type == .pipe) { this.file_type = .nonblocking_pipe; diff --git a/src/bun.zig b/src/bun.zig index 536512da2a8e36..60a8ccfb7fc652 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -515,6 +515,10 @@ pub fn ensureNonBlocking(fd: anytype) void { _ = std.os.fcntl(fd, std.os.F.SETFL, current | std.os.O.NONBLOCK) catch 0; } +pub fn isNonBlocking(fd: anytype) bool { + return (std.os.fcntl(fd, std.os.F.GETFL, 0) catch 0 & std.os.O.NONBLOCK) == std.os.O.NONBLOCK; +} + const global_scope_log = sys.syslog; pub fn isReadable(fd: FileDescriptor) PollFlag { if (comptime Environment.isWindows) { diff --git a/test/regression/issue/10080.test.ts b/test/regression/issue/10080.test.ts new file mode 100644 index 00000000000000..7f92b68308d235 --- /dev/null +++ b/test/regression/issue/10080.test.ts @@ -0,0 +1,33 @@ +import { test, expect } from "bun:test"; +import { bunEnv, bunExe, isPosix } from "harness"; +import { tmpdir } from "os"; +import { join } from "path"; + +test.if(isPosix)( + "10080 - ensure blocking stdio is treated as such in FileReader", + async () => { + const expected = "foobar\n"; + const filename = join(tmpdir(), "bun.test.stream." + Date.now() + ".js"); + const contents = "for await (const line of console) {console.log(`foo${line}`)}"; + await Bun.write(filename, contents); + const shellCommand = `exec &> >(${bunExe()} ${filename}); echo "bar"; while read -r line; do echo $line; done`; + + const proc = Bun.spawn(["bash", "-c", shellCommand], { + stdin: "inherit", + stdout: "pipe", + stderr: "inherit", + env: bunEnv, + }); + const { value } = await proc.stdout.getReader().read(); + const output = new TextDecoder().decode(value); + if (output !== expected) { + expect(output).toEqual(expected); + throw new Error("Output didn't match!\n"); + } + + proc.kill(9); + await proc.exited; + expect(proc.killed).toBeTrue(); + }, + 1000, +);