Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions src/js/node/tty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const {
isatty,
getWindowSize: _getWindowSize,
} = $cpp("ProcessBindingTTYWrap.cpp", "createBunTTYFunctions");
const { TTY } = process.platform === "win32" ? process.binding("tty_wrap") : { TTY: undefined };

const { validateInteger } = require("internal/validators");
const fs = require("internal/fs/streams");
Expand All @@ -20,6 +21,14 @@ function ReadStream(fd): void {
this.isRaw = false;
// Only set isTTY to true if the fd is actually a TTY
this.isTTY = isatty(fd);
if (process.platform === "win32" && fd !== 0 && this.isTTY) {
// Windows raw mode for non-stdin tty.ReadStream instances must go through
// uv_tty_set_mode, which needs a uv_tty_t wrapper for this fd. fd 0 uses
// Bun's shared stdin source and Source__setRawModeStdin instead, while
// non-TTY fds should keep the existing construction behavior and only fail
// if setRawMode() is later requested.
this.$bunNativePtr = new TTY(fd);
}
}
$toClass(ReadStream, "ReadStream", fs.ReadStream);

Expand Down Expand Up @@ -71,12 +80,17 @@ Object.defineProperty(ReadStream, "prototype", {
return this;
}

// If you call setRawMode before you call on('data'), the stream will
// not be constructed, leading to EBADF
// This corresponds to the `ensureConstructed` function in `native-readable.ts`
this.$start();

const err = handle.setRawMode(flag);
// If you call setRawMode before you call on('data'), native-readable
// streams need to be constructed before uv_tty_set_mode to avoid EBADF.
// This corresponds to the `ensureConstructed` function in
// `native-readable.ts`. Plain fd-backed tty.ReadStream instances, such
// as reopened CONIN$, do not have $start and only use the TTYWrap for
// console mode changes.
this.$start?.();

// Native-readable handles expose setRawMode(bool), but the TTYWrap
// binding mirrors libuv and expects numeric uv_tty_mode_t values.
const err = handle.setRawMode(handle instanceof TTY ? (flag ? 1 : 0) : flag);
if (err) {
this.emit("error", err);
return this;
Expand Down
71 changes: 71 additions & 0 deletions test/js/node/tty.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,77 @@ describe("ReadStream.prototype.setRawMode", () => {
returnsThis: true,
});
});

test.skipIf(!isWindows)("supports raw mode on reopened CONIN$ streams", async () => {
let output = "";
const decoder = new TextDecoder();
const done = Promise.withResolvers<void>();
const eof = Promise.withResolvers<void>();

const proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const fs = require("node:fs");
const tty = require("node:tty");
let err;
const fd = fs.openSync("\\\\\\\\.\\\\CONIN$", "r+");
const input = new tty.ReadStream(fd);
input.on("error", e => (err = e.message));
const before = input.isRaw;
const ret = input.setRawMode(true);
const afterTrue = input.isRaw;
input.setRawMode(false);
const afterFalse = input.isRaw;
input.destroy();
process.stdout.write(
"RESULT " +
JSON.stringify({
isTTY: input.isTTY,
before,
afterTrue,
afterFalse,
returnsThis: ret === input,
...(err ? { err } : {}),
}),
);
process.exit(0);
`,
],
env: bunEnv,
terminal: {
cols: 200,
rows: 24,
data(_t, chunk: Uint8Array) {
output += decoder.decode(chunk, { stream: true });
if (output.includes("RESULT ") && output.includes("}")) done.resolve();
},
exit() {
eof.resolve();
},
},
});

await Promise.race([done.promise, eof.promise]);
proc.kill();
await proc.exited;
proc.terminal?.close();
output += decoder.decode();

const stripped = Bun.stripANSI(output).replace(/[\r\n]/g, "");
const match = stripped.match(/RESULT (\{[^}]*\})/);
if (!match) {
throw new Error("child did not emit RESULT; terminal output was: " + JSON.stringify(output));
}
expect(JSON.parse(match[1])).toEqual({
isTTY: true,
before: false,
afterTrue: true,
afterFalse: false,
returnsThis: true,
});
});
});

describe("WriteStream.prototype.getColorDepth", () => {
Expand Down