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
51 changes: 42 additions & 9 deletions src/js/node/tty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ const {
getWindowSize: _getWindowSize,
rawModeStateSize,
}: {
/** The errno, or 0. On Windows, for stdin: `(flag)`. */
setRawMode(fdOrFlag: number | boolean | null, flag?: boolean, state?: Uint8Array): number;
/** The errno, or 0. `mode` is a uv_tty_mode_t. On Windows, for stdin: `(flag)`. */
setRawMode(fdOrFlag: number | boolean | null, mode?: number, state?: Uint8Array): number;
isatty(fd: unknown): boolean;
/** Fills `size` with [columns, rows]. */
getWindowSize(fd: number | null, size: number[]): boolean;
Expand All @@ -25,21 +25,29 @@ const fs = require("internal/fs/streams");
// that state per ReadStream rather than per process.
const kRawModeState = Symbol("rawModeState");

// libuv's uv_tty_mode_t. RAW_VT is the raw mode Node asks for.
const enum TTYMode {
NORMAL = 0,
IO = 2,
RAW_VT = 3,
}

type Environment = Record<string, string | undefined>;
type DoneCallback = (error?: Error | null) => void;

declare global {
namespace $Node {
interface TTYReadStream extends ReadStream {
isRaw: boolean;
rawMode: false | "raw" | "io";
isTTY: boolean;
/** On `process.stdin`, which is a native readable. */
$bunNativePtr?: $Bindgen.FileInternalReadableStreamSource;
$start(): void;
[kRawModeState]?: Uint8Array;
ref(): this;
unref(): this;
setRawMode(flag: boolean): this;
setRawMode(mode: boolean | "raw" | "io"): this;
}

interface TTYWriteStream extends WriteStream {
Expand All @@ -64,6 +72,7 @@ function ReadStream(this: $Node.TTYReadStream, fd: number | null): void {
}
fs.ReadStream.$apply(this, ["", { fd }]);
this.isRaw = false;
this.rawMode = false;
// Only set isTTY to true if the fd is actually a TTY
this.isTTY = isatty(fd);
}
Expand Down Expand Up @@ -93,8 +102,25 @@ Object.defineProperty(ReadStream, "prototype", {
return this;
};

Prototype.setRawMode = function (this: $Node.TTYReadStream, flag: boolean) {
flag = !!flag;
// https://github.com/nodejs/node/blob/v26.12.1/lib/tty.js#L85-L109
Prototype.setRawMode = function (this: $Node.TTYReadStream, mode: boolean | string) {
let rawMode: false | "raw" | "io";
if (mode === "io" || mode === "raw") {
rawMode = mode;
} else if (typeof mode === "string") {
throw $ERR_INVALID_ARG_VALUE("mode", mode, "must be true, false, 'raw', or 'io'");
} else {
rawMode = mode ? "raw" : false;
}

if (process.platform === "win32" && rawMode === "io") {
// No UV_TTY_MODE_IO on Windows: https://github.com/nodejs/node/blob/v26.12.1/deps/uv/src/win/tty.c#L395-L396
const UV_ENOTSUP = -4049;
const { ErrnoException } = require("internal/shared");
this.emit("error", new ErrnoException(UV_ENOTSUP, "setRawMode"));
return this;
}
Comment thread
robobun marked this conversation as resolved.
Resolved
const isRaw = rawMode !== false;

// On windows, this goes through the stream handle itself, as it must call
// uv_tty_set_mode on the uv_tty_t.
Expand All @@ -105,7 +131,7 @@ Object.defineProperty(ReadStream, "prototype", {
// Special case for stdin, as it has a shared uv_tty handle
// and it's stream is constructed differently
if (this.fd === 0) {
const err = ttySetMode(flag);
const err = ttySetMode(isRaw);
if (err) {
this.emit("error", new Error("setRawMode failed with errno: " + err));
return this;
Expand All @@ -122,22 +148,29 @@ Object.defineProperty(ReadStream, "prototype", {
// This corresponds to the `ensureConstructed` function in `native-readable.ts`
this.$start();

const err = handle.setRawMode(flag);
const err = handle.setRawMode(isRaw);
if (err) {
this.emit("error", err);
return this;
}
}
} else {
let ttyMode = TTYMode.NORMAL;
if (rawMode === "io") {
ttyMode = TTYMode.IO;
} else if (rawMode === "raw") {
ttyMode = TTYMode.RAW_VT;
}
const state = (this[kRawModeState] ??= new Uint8Array(rawModeStateSize));
const err = ttySetMode(this.fd, flag, state);
const err = ttySetMode(this.fd, ttyMode, state);
if (err) {
this.emit("error", new Error("setRawMode failed with errno: " + err));
Comment thread
robobun marked this conversation as resolved.
Resolved
return this;
}
}

this.isRaw = flag;
this.isRaw = isRaw;
this.rawMode = rawMode;

return this;
};
Expand Down
23 changes: 10 additions & 13 deletions src/jsc/bindings/ProcessBindingTTYWrap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -236,27 +236,18 @@ JSC_DEFINE_HOST_FUNCTION(TTYWrap_functionSetMode,
(JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(globalObject);
auto argCount = callFrame->argumentCount();
auto throwScope = DECLARE_THROW_SCOPE(vm);
if (argCount == 0) {
JSC::throwTypeError(globalObject, throwScope, "setRawMode requires 1 argument (a number)"_s);
return {};
}

TTYWrapObject* ttyWrap = dynamicDowncast<TTYWrapObject>(callFrame->thisValue());
if (!ttyWrap) [[unlikely]] {
JSC::throwTypeError(globalObject, throwScope, "TTY.setRawMode expects a TTYWrapObject as this"_s);
return {};
}

int fd = ttyWrap->fd;
JSValue mode = callFrame->argument(0);
if (!mode.isNumber()) {
throwTypeError(globalObject, throwScope, "mode must be a number"_s);
return {};
}
// https://github.com/nodejs/node/blob/v26.12.1/src/tty_wrap.cc#L135-L136
int modeInt = callFrame->argument(0).toInt32(globalObject);
RETURN_IF_EXCEPTION(throwScope, {});

int modeInt = JSC::toInt32(mode.asNumber());
#if OS(WINDOWS)
if (modeInt == 0) {
Bun__setCTRLHandler(1);
Expand All @@ -265,7 +256,7 @@ JSC_DEFINE_HOST_FUNCTION(TTYWrap_functionSetMode,
int err = uv_tty_set_mode(ttyWrap->handle->tty(), modeInt);
#else
// Nodejs does not throw when ttySetMode fails. An Error event is emitted instead.
int err = Bun__ttySetMode(fd, modeInt, &ttyWrap->ttyState, 1);
int err = Bun__ttySetMode(ttyWrap->fd, modeInt, &ttyWrap->ttyState, 1);
#endif
return JSValue::encode(jsNumber(err));
}
Expand Down Expand Up @@ -523,6 +514,12 @@ JSValue createNodeTTYWrapObject(JSC::JSGlobalObject* globalObject)

obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "isTTY"_s)), JSFunction::create(vm, globalObject, 0, "isatty"_s, Zig::jsFunctionTty_isatty, ImplementationVisibility::Public), 0);

// https://github.com/nodejs/node/blob/v26.12.1/src/tty_wrap.cc#L75-L77
const unsigned constant = JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete;
obj->putDirect(vm, Identifier::fromString(vm, "UV_TTY_MODE_NORMAL"_s), jsNumber(0), constant);
obj->putDirect(vm, Identifier::fromString(vm, "UV_TTY_MODE_IO"_s), jsNumber(2), constant);
obj->putDirect(vm, Identifier::fromString(vm, "UV_TTY_MODE_RAW_VT"_s), jsNumber(3), constant);

TTYWrapPrototype* prototype = TTYWrapPrototype::create(vm, globalObject, TTYWrapPrototype::createStructure(vm, globalObject));
TTYWrapConstructor* constructor = TTYWrapConstructor::create(vm, globalObject, TTYWrapConstructor::createStructure(vm, globalObject, globalObject->functionPrototype()), prototype);

Expand Down
13 changes: 13 additions & 0 deletions src/jsc/bindings/wtf-bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,19 @@ static int ttySetMode(int fd, int mode, BunTTYState& state, int action)
int expected;
int rc;

// libuv 1.52.1, in Node v26.12.1, aborts on an unknown mode: https://github.com/libuv/libuv/blob/v1.53.0/src/unix/tty.c#L287-L298
switch (mode) {
case 3: // raw-vt: there is only a single raw TTY mode on UNIX
mode = 1;
break;
case 0:
case 1:
case 2:
break;
default:
return EINVAL;
Comment thread
robobun marked this conversation as resolved.
Resolved
}

if (state.mode == mode)
return 0;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5932,9 +5932,7 @@ node:tty.ReadStream.prototype setRawMode()
throws TypeError: Attempted to assign to readonly property. <- a number, a string
throws TypeError: Cannot access property of undefined or null <- undefined, null
node:tty.ReadStream.prototype setRawMode("a", 1)
throws TypeError: fd must be a number, stores [rawModeState] <- every object
throws TypeError: Attempted to assign to readonly property. <- a number, a string
throws TypeError: Cannot access property of undefined or null <- undefined, null
throws TypeError ERR_INVALID_ARG_VALUE: The argument 'mode' must be true, false, 'raw', or 'io'. Received 'a' <- every receiver
"
`;

Expand All @@ -5961,10 +5959,7 @@ node:tty.ReadStream.prototype setRawMode()
throws TypeError: undefined is not an object <- undefined
throws TypeError: null is not an object <- null
node:tty.ReadStream.prototype setRawMode("a", 1)
throws TypeError: what is called is not a function <- {}, an object without a prototype, an instance of another class, a proxy of {}, a number, a string
throws Error: setRawMode failed because it was called on something that is not a TTY <- an heir of the prototype object, a proxy of such an heir, the prototype object
throws TypeError: undefined is not an object <- undefined
throws TypeError: null is not an object <- null
throws TypeError ERR_INVALID_ARG_VALUE: The argument 'mode' must be true, false, 'raw', or 'io'. Received 'a' <- every receiver
"
`;

Expand Down
23 changes: 23 additions & 0 deletions test/js/node/nodettywrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,26 @@ test("process.binding('tty_wrap')", () => {
console.warn("warn: Skipping tty tests because stdin is not a tty");
}
});

// https://github.com/nodejs/node/blob/v26.12.1/src/tty_wrap.cc#L75-L77
test("process.binding('tty_wrap') has libuv's tty modes", () => {
const tty_wrap: any = process.binding("tty_wrap");
const constant = (value: number) => ({ value, writable: false, enumerable: true, configurable: false });

expect(Object.getOwnPropertyNames(tty_wrap)).toEqual([
"isTTY",
"UV_TTY_MODE_NORMAL",
"UV_TTY_MODE_IO",
"UV_TTY_MODE_RAW_VT",
"TTY",
]);
expect({
UV_TTY_MODE_NORMAL: Object.getOwnPropertyDescriptor(tty_wrap, "UV_TTY_MODE_NORMAL"),
UV_TTY_MODE_IO: Object.getOwnPropertyDescriptor(tty_wrap, "UV_TTY_MODE_IO"),
UV_TTY_MODE_RAW_VT: Object.getOwnPropertyDescriptor(tty_wrap, "UV_TTY_MODE_RAW_VT"),
}).toEqual({
UV_TTY_MODE_NORMAL: constant(0),
UV_TTY_MODE_IO: constant(2),
UV_TTY_MODE_RAW_VT: constant(3),
});
});
42 changes: 42 additions & 0 deletions test/js/node/test/pseudo-tty/test-set-raw-mode-modes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';
require('../common');
const assert = require('assert');
const { spawnSyncAndExitWithoutError } = require('../common/child_process');

function isOnlcrEnabled() {
const { child: { stdout } } = spawnSyncAndExitWithoutError('stty', ['-a'], {
encoding: 'utf8',
stdio: ['inherit', 'pipe', 'pipe'],
});

return /(?:^|[\s;])onlcr(?:[\s;]|$)/.test(stdout);
}

process.stdin.setRawMode(true);
console.log(`raw=${isOnlcrEnabled()}`);
assert.strictEqual(process.stdin.isRaw, true);
assert.strictEqual(process.stdin.rawMode, 'raw');

process.stdin.setRawMode(false);
console.log(`normal=${process.stdin.isRaw}`);
assert.strictEqual(process.stdin.rawMode, false);
assert.throws(
() => process.stdin.setRawMode('raw-vt'),
{
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
assert.strictEqual(process.stdin.rawMode, false);

process.stdin.setRawMode('raw');
console.log(`raw-string=${isOnlcrEnabled()}`);
assert.strictEqual(process.stdin.isRaw, true);
assert.strictEqual(process.stdin.rawMode, 'raw');

process.stdin.setRawMode(false);
process.stdin.setRawMode('io');
console.log(`io=${isOnlcrEnabled()}`);
assert.strictEqual(process.stdin.isRaw, true);
assert.strictEqual(process.stdin.rawMode, 'io');

process.stdin.setRawMode(false);
4 changes: 4 additions & 0 deletions test/js/node/test/pseudo-tty/test-set-raw-mode-modes.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
raw=true
normal=false
raw-string=true
io=false
Loading
Loading