Repository navigation
Bun.stdin.stream(), process.stdin: read an unsliced file from its current offset #41687
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d830968
39a41f9
06144ee
7ac7333
ae86c47
0447c23
bf1f735
6dd97e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -665,3 +665,48 @@ test("process.stdin over an anonymous pipe delivers each byte exactly once", asy | |
| expect(stdout).toBe(`${total} ${expected}`); | ||
| expect(err).toBeNull(); | ||
| }); | ||
|
|
||
| // A regular-file stdin is an open file description shared with the parent | ||
| // shell. The parent may already have consumed a prefix, and a sibling that | ||
| // runs after Bun continues from where Bun stopped. So the stream must read | ||
| // from the current offset with read(2), not pread from 0. | ||
| describe.skipIf(isWindows)("regular-file stdin inherited at an offset", () => { | ||
| const input = "HEADER-already-consumed\nbody1\nbody2\n"; | ||
|
|
||
| async function runAfterRead(js: string, tail = "") { | ||
| using dir = tempDir("stdin-file-offset", { "in.txt": input }); | ||
| await using proc = Bun.spawn({ | ||
| cmd: ["sh", "-c", `{ read hdr; "$BUN" -e ${JSON.stringify(js)}; ${tail} } < in.txt`], | ||
|
Comment on lines
+673
to
+679
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 The Windows half of this fix ships with no test: the whole describe is Why this was flaggedOn Windows an unsliced file Blob now takes the Verification: Trigger is any Windows CI run, so a Windows-only regression of this change can never turn a test red. src/runtime/webcore/ReadableStream.rs:548-549 changes
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed: the Windows path has no coverage in this PR, and I cannot run Windows. I wrote the shell-free version you describe. The parent opens It is not pushed. CI on this head is green (build 122398, 181 of 181 jobs), and a push starts a new run. If a maintainer wants it before the merge, I push it. If not, I open it as a follow-up PR after the merge, so that the Windows lanes run it. Patchdiff --git a/test/js/node/process/process-stdin.test.ts b/test/js/node/process/process-stdin.test.ts
index f6d52989c5..2ad6921570 100644
--- a/test/js/node/process/process-stdin.test.ts
+++ b/test/js/node/process/process-stdin.test.ts
@@ -1,6 +1,8 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isASAN, isDebug, isWindows, tempDir } from "harness";
import { exec } from "node:child_process";
+import { closeSync, openSync, readSync } from "node:fs";
+import { join } from "node:path";
test.concurrent("pipe does the right thing", async () => {
// Note: Bun.spawnSync uses memfd_create on Linux for pipe, which means we see
@@ -666,47 +668,46 @@ test("process.stdin over an anonymous pipe delivers each byte exactly once", asy
expect(err).toBeNull();
});
-// A regular-file stdin is an open file description shared with the parent
-// shell. The parent may already have consumed a prefix, and a sibling that
-// runs after Bun continues from where Bun stopped. So the stream must read
-// from the current offset with read(2), not pread from 0.
-describe.skipIf(isWindows)("regular-file stdin inherited at an offset", () => {
- const input = "HEADER-already-consumed\nbody1\nbody2\n";
-
- async function runAfterRead(js: string, tail = "") {
- using dir = tempDir("stdin-file-offset", { "in.txt": input });
- await using proc = Bun.spawn({
- cmd: ["sh", "-c", `{ read hdr; "$BUN" -e ${JSON.stringify(js)}; ${tail} } < in.txt`],
- cwd: String(dir),
- env: { ...bunEnv, BUN: bunExe() },
- stdout: "pipe",
- stderr: "pipe",
- });
- const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
- return { stdout, stderr, exitCode };
+// A regular-file stdin is an open file description shared with the parent,
+// like `{ read header; bun body.js; } < file`. The stream must read from the
+// current offset and advance it for whoever reads next, not pread from 0.
+describe("regular-file stdin inherited at an offset", () => {
+ const header = "HEADER-already-consumed\n";
+ const body = "body1\nbody2\n";
+
+ // `left` is what the next reader of the same description still gets.
+ async function runAfterHeader(js: string) {
+ using dir = tempDir("stdin-file-offset", { "in.txt": header + body });
+ const fd = openSync(join(String(dir), "in.txt"), "r");
+ try {
+ expect(readSync(fd, Buffer.alloc(header.length))).toBe(header.length);
+ await using proc = Bun.spawn({
+ cmd: [bunExe(), "-e", js],
+ env: bunEnv,
+ stdin: fd,
+ stdout: "pipe",
+ stderr: "pipe",
+ });
+ const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
+ return { stdout, stderr, exitCode, left: readSync(fd, Buffer.alloc(64)) };
+ } finally {
+ closeSync(fd);
+ }
}
- test.concurrent("process.stdin 'data' starts after the line the shell consumed", async () => {
+ test.concurrent("process.stdin reads the rest and leaves the offset at the end", async () => {
const js = `let s = ""; process.stdin.on("data", d => (s += d)).on("end", () => process.stdout.write(JSON.stringify(s)));`;
- expect(await runAfterRead(js)).toEqual({ stdout: JSON.stringify("body1\nbody2\n"), stderr: "", exitCode: 0 });
+ expect(await runAfterHeader(js)).toEqual({ stdout: JSON.stringify(body), stderr: "", exitCode: 0, left: 0 });
});
- test.concurrent("Bun.stdin.stream() starts after the line the shell consumed", async () => {
- const js = `process.stdout.write(JSON.stringify(await new Response(Bun.stdin.stream()).text()));`;
- expect(await runAfterRead(js)).toEqual({ stdout: JSON.stringify("body1\nbody2\n"), stderr: "", exitCode: 0 });
+ test.concurrent("Bun.stdin.stream() reads the rest and leaves the offset at the end", async () => {
+ const js = `let s = ""; for await (const chunk of Bun.stdin.stream()) s += Buffer.from(chunk).toString(); process.stdout.write(JSON.stringify(s));`;
+ expect(await runAfterHeader(js)).toEqual({ stdout: JSON.stringify(body), stderr: "", exitCode: 0, left: 0 });
});
- test.concurrent("for await (const line of console) starts after the line the shell consumed", async () => {
+ test.concurrent("for await (const line of console) starts after the consumed header", async () => {
const js = `for await (const line of console) { process.stdout.write(JSON.stringify(line)); break; }`;
- expect(await runAfterRead(js)).toEqual({ stdout: JSON.stringify("body1"), stderr: "", exitCode: 0 });
- });
-
- test.concurrent("process.stdin advances the shared offset for the next reader", async () => {
- const js = `let n = 0; process.stdin.on("data", d => (n += d.length)).on("end", () => process.stdout.write("bun:" + n + "\\n"));`;
- expect(await runAfterRead(js, `echo "cat:$(cat | wc -c | tr -d ' ')";`)).toEqual({
- stdout: "bun:12\ncat:0\n",
- stderr: "",
- exitCode: 0,
- });
+ const { stdout, stderr, exitCode } = await runAfterHeader(js);
+ expect({ stdout, stderr, exitCode }).toEqual({ stdout: JSON.stringify("body1"), stderr: "", exitCode: 0 });
});
}); |
||
| cwd: String(dir), | ||
| env: { ...bunEnv, BUN: bunExe() }, | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| }); | ||
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
| return { stdout, stderr, exitCode }; | ||
| } | ||
|
|
||
| test.concurrent("process.stdin 'data' starts after the line the shell consumed", async () => { | ||
| const js = `let s = ""; process.stdin.on("data", d => (s += d)).on("end", () => process.stdout.write(JSON.stringify(s)));`; | ||
| expect(await runAfterRead(js)).toEqual({ stdout: JSON.stringify("body1\nbody2\n"), stderr: "", exitCode: 0 }); | ||
| }); | ||
|
|
||
| test.concurrent("Bun.stdin.stream() starts after the line the shell consumed", async () => { | ||
| const js = `process.stdout.write(JSON.stringify(await new Response(Bun.stdin.stream()).text()));`; | ||
| expect(await runAfterRead(js)).toEqual({ stdout: JSON.stringify("body1\nbody2\n"), stderr: "", exitCode: 0 }); | ||
| }); | ||
|
|
||
| test.concurrent("for await (const line of console) starts after the line the shell consumed", async () => { | ||
| const js = `for await (const line of console) { process.stdout.write(JSON.stringify(line)); break; }`; | ||
| expect(await runAfterRead(js)).toEqual({ stdout: JSON.stringify("body1"), stderr: "", exitCode: 0 }); | ||
| }); | ||
|
|
||
| test.concurrent("process.stdin advances the shared offset for the next reader", async () => { | ||
| const js = `let n = 0; process.stdin.on("data", d => (n += d.length)).on("end", () => process.stdout.write("bun:" + n + "\\n"));`; | ||
| expect(await runAfterRead(js, `echo "cat:$(cat | wc -c | tr -d ' ')";`)).toEqual({ | ||
| stdout: "bun:12\ncat:0\n", | ||
| stderr: "", | ||
| exitCode: 0, | ||
| }); | ||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.