Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d53bfc5
chore(.gitignore): remove submodule path from `.gitignore`
ThatOneBro Mar 1, 2023
00a1d32
WIP(tty): setRawMode
ThatOneBro Feb 10, 2023
483291d
WIP(tty): add TTYModule.h changes
ThatOneBro Feb 10, 2023
40f5cfc
feat(tty): get `setRawMode` and `isRaw` working
ThatOneBro Feb 11, 2023
38ae1f8
wip(tty): cache if fd is tty, need to fix how header is shared
ThatOneBro Feb 12, 2023
b957928
fix(tty): fix isRaw, setRawMode, refactor cpp
ThatOneBro Feb 18, 2023
933626a
chore(tty): add missing newline
ThatOneBro Feb 18, 2023
b88debc
fix(readline): actually call setRawMode on stdin
ThatOneBro Feb 28, 2023
6eb4e8f
fix(node-test-helpers): stop trying to be so smart with types
ThatOneBro Feb 28, 2023
fe57824
chore(readline): add some types to tests
ThatOneBro Feb 28, 2023
23c9aac
test(node:process): add testing for `process.stdin.setRawMode`
ThatOneBro Mar 15, 2023
2e2a7a1
wip(tty): fix tests, start console asynciter test
ThatOneBro Mar 16, 2023
b9b53bb
wip(tty): make TTYHelper more general for non-stdin fds
ThatOneBro Mar 16, 2023
f210357
wip(tty): get `spawnInNewPty` to work
ThatOneBro Mar 17, 2023
46a3825
wip(tty): add `test/openpty.ts`
ThatOneBro Mar 17, 2023
48f7b0f
wip(tty): add `TTY_MODE_RAW_ASYNC_IO`
ThatOneBro Mar 17, 2023
4927e07
fix(tty): fix `line of console` async iter
ThatOneBro Mar 18, 2023
d2aa68b
chore(.gitignore): revert unintentional change to .gitignore
ThatOneBro Mar 18, 2023
c692ba2
chore(readline): cleanup
ThatOneBro Mar 18, 2023
102188f
chore: cleanup
ThatOneBro Mar 18, 2023
2859820
chore: update headers.h
ThatOneBro Mar 18, 2023
5101e58
run prettier
ThatOneBro Mar 18, 2023
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,7 @@ src/bun.js/debug-bindings-obj

failing-tests.txt
test.txt
myscript.sh
myscript.sh

test/helpers/*.so
test/helpers/*.dylib
81 changes: 81 additions & 0 deletions src/bun.js/bindings/JSTTYHelper.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#include "JSTTYHelper.h"

#include "../bindings/ZigGlobalObject.h"
#include "TTYHelper.h"

namespace WebCore {
using namespace Zig;

JSC_DEFINE_HOST_FUNCTION(jsFunctionInternalTty_isRaw,
(JSGlobalObject * globalObject,
CallFrame* callFrame))
{
VM& vm = globalObject->vm();
if (callFrame->argumentCount() < 1) {
return JSValue::encode(jsBoolean(false));
}

auto scope = DECLARE_CATCH_SCOPE(vm);
int fd = callFrame->argument(0).toInt32(globalObject);
RETURN_IF_EXCEPTION(scope, encodedJSValue());

auto isRaw = tty__is_raw(fd);
if (isRaw == -3) {
return JSValue::encode(jsUndefined());
}
return JSValue::encode(jsBoolean(isRaw));
}

JSC_DEFINE_HOST_FUNCTION(jsFunctionInternalTty_setRawMode,
(JSGlobalObject * globalObject,
CallFrame* callFrame))
{
VM& vm = globalObject->vm();
if (callFrame->argumentCount() < 2) {
return JSValue::encode(jsBoolean(false));
}

auto scope = DECLARE_CATCH_SCOPE(vm);
int fd = callFrame->argument(0).toInt32(globalObject);
RETURN_IF_EXCEPTION(scope, encodedJSValue());
bool shouldBeRaw = callFrame->argument(1).toBoolean(globalObject);
RETURN_IF_EXCEPTION(scope, encodedJSValue());

auto throwScope = DECLARE_THROW_SCOPE(vm);
auto result = tty__set_mode(fd, shouldBeRaw ? TTY_MODE_RAW : TTY_MODE_NORMAL);
if (result < 0) {
JSC::throwException(
globalObject, throwScope,
JSC::createError(globalObject, "Failed to set tty mode. Error code: "_s + WTFMove(std::to_string(result).c_str())));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use makeString instead of + operator

return JSValue::encode(jsUndefined());
}
return JSValue::encode(jsBoolean(true));
}

JSC_DEFINE_HOST_FUNCTION(jsFunctionInternalTty_setAsyncIoMode,
(JSGlobalObject * globalObject,
CallFrame* callFrame))
{
VM& vm = globalObject->vm();
if (callFrame->argumentCount() < 2) {
return JSValue::encode(jsBoolean(false));
}

auto scope = DECLARE_CATCH_SCOPE(vm);
int fd = callFrame->argument(0).toInt32(globalObject);
RETURN_IF_EXCEPTION(scope, encodedJSValue());
bool shouldBeIoMode = callFrame->argument(1).toBoolean(globalObject);
RETURN_IF_EXCEPTION(scope, encodedJSValue());

auto throwScope = DECLARE_THROW_SCOPE(vm);
auto result = tty__set_async_io_mode(fd, shouldBeIoMode);
if (result < 0) {
JSC::throwException(
globalObject, throwScope,
JSC::createError(globalObject, "Failed to set tty mode. Error code: "_s + WTFMove(std::to_string(result).c_str())));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use makeString instead of + operator

return JSValue::encode(jsUndefined());
}
return JSValue::encode(jsBoolean(true));
}

} // namespace WebCore
11 changes: 11 additions & 0 deletions src/bun.js/bindings/JSTTYHelper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#pragma once

#include "root.h"

namespace WebCore {

JSC_DECLARE_HOST_FUNCTION(jsFunctionInternalTty_isRaw);
JSC_DECLARE_HOST_FUNCTION(jsFunctionInternalTty_setRawMode);
JSC_DECLARE_HOST_FUNCTION(jsFunctionInternalTty_setAsyncIoMode);

} // namespace WebCore
222 changes: 222 additions & 0 deletions src/bun.js/bindings/TTYHelper.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
#include <termios.h>
#include <unistd.h>
#include <cstdio>

#include "TTYHelper.h"

namespace Zig {

static bool tty__orig_set = false;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will break when we have multiple instances of Bun's runtime

Sort of need this to be part of JSTTYHelper as an object with the relevant fields

static bool tty__raw_mode_set = false;
static tty_mode_t tty__cached_mode = TTY_MODE_UNSET;
static struct termios tty__orig_termios;
static struct termios tty__raw_mode_termios;
static struct termios tty__termios_tmp;
static int32_t tty__is_tty_val = -1;

int32_t tty__is_tty(int32_t fd)
{
// Only cache is_tty for stdin
// NOTE: This caching behavior assumes that stdin doesn't change for the duration of the program
// This may be a dangerous assumption but it's probably fine in 99% of cases as far as I can tell
if (UNLIKELY(fd != STDIN_FILENO)) {
return isatty(fd);
}

if (tty__is_tty_val == -1)
tty__is_tty_val = isatty(fd);

return tty__is_tty_val;
}

int32_t tty__get_termios(int32_t fd, termios* termios_p)
{
if (UNLIKELY(!tty__is_tty(fd)))
return -3;

if (termios_p == NULL && fd == STDIN_FILENO)
termios_p = &tty__orig_termios;

if (UNLIKELY(tcgetattr(fd, termios_p)))
return -1;

return 0;
}

int32_t tty__is_raw(int32_t fd)
{
if (UNLIKELY(!tty__is_tty(fd)))
return -3;

if (UNLIKELY(tty__get_termios(fd, &tty__termios_tmp)))
return -4;

return (tty__termios_tmp.c_lflag & (ECHO | ICANON)) == 0;
}

static inline tty_mode_t tty__internal_get_mode(termios* termios_p)
{
// If ICANON and ECHO are unset, we're in (one of the) raw mode(s)
if ((termios_p->c_lflag & (ECHO | ICANON)) == 0) {
// If OPOST is unset, we're in raw async I/O mode
if ((termios_p->c_oflag & OPOST) == 0)
return TTY_MODE_RAW_ASYNC_IO;
else // Otherwise, we're in normal raw mode
return TTY_MODE_RAW;
}

return TTY_MODE_NORMAL;
}

tty_mode_t tty__get_mode(int32_t fd)
{
if (UNLIKELY(!tty__is_tty(fd)))
return TTY_MODE_UNSET;

if (UNLIKELY(tty__get_termios(fd, &tty__termios_tmp)))
return TTY_MODE_UNSET;

return tty__internal_get_mode(&tty__termios_tmp);
}

int32_t tty__set_mode(int32_t fd, tty_mode_t mode)
{
if (UNLIKELY(!tty__is_tty(fd)))
return -3;

bool orig_just_set_now = false;

switch (mode) {
case TTY_MODE_NORMAL:
if (UNLIKELY(!tty__is_raw(fd)))
break;

if (UNLIKELY(tcsetattr(fd, TCSADRAIN, &tty__orig_termios)))
return -6;

break;

// NOTE: This is based on the code from libuv for TTY_MODE_RAW
case TTY_MODE_RAW:
if (!tty__orig_set) {
if (UNLIKELY(tty__get_termios(fd, nullptr)))
return -4;
tty__orig_set = true;
orig_just_set_now = true;
}

// If we set original this time, then we can make a copy of that for our new struct
if (orig_just_set_now) {
tty__termios_tmp = tty__orig_termios;
} else if (UNLIKELY(tcgetattr(fd, &tty__termios_tmp)))
return -5;

// Check if we are already in raw mode
// if so, then break
if (UNLIKELY(tty__internal_get_mode(&tty__termios_tmp) == TTY_MODE_RAW))
break;

// If we already have a raw mode struct, use that
if (tty__raw_mode_set) {
if (UNLIKELY(tcsetattr(fd, TCSADRAIN, &tty__raw_mode_termios)))
return -6;
break;
}

tty__termios_tmp.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
tty__termios_tmp.c_oflag |= (ONLCR);
tty__termios_tmp.c_cflag |= (CS8);
tty__termios_tmp.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
tty__termios_tmp.c_cc[VMIN] = 1;
tty__termios_tmp.c_cc[VTIME] = 0;

tty__raw_mode_termios = tty__termios_tmp;
tty__raw_mode_set = true;

if (UNLIKELY(tcsetattr(fd, TCSADRAIN, &tty__termios_tmp)))
return -6;

break;

case TTY_MODE_RAW_ASYNC_IO:

if (!tty__orig_set) {
if (UNLIKELY(tty__get_termios(fd, nullptr)))
return -4;
tty__orig_set = true;
orig_just_set_now = true;
}

// If we set original this time, then we can make a copy of that for our new struct
if (orig_just_set_now) {
tty__termios_tmp = tty__orig_termios;
} else if (UNLIKELY(tcgetattr(fd, &tty__termios_tmp)))
return -5;

// Check if we are already in raw mode
// if so, then break
if (UNLIKELY(tty__internal_get_mode(&tty__termios_tmp) == TTY_MODE_RAW_ASYNC_IO))
break;

// Here are the main difference between this (TTY_MODE_RAW_ASYNC_IO)
// and what Node and libuv consider "raw mode", as well as what we call "raw mode" (TTY_MODE_RAW):

// - We unset INLCR, which means we don't convert newlines to carriage returns
// - We unset IGNCR, which means we don't ignore carriage returns
// - We unset IGNBRK, which means we don't ignore break conditions
// - We unset PARMRK, which means we don't mark parity errors
// - We unset OPOST, which means we don't do any output processing... This means thinks like LF -> CRLF won't happen,
// but also that we won't do any other output processing
// - We unset ECHONL, which means we don't echo newlines
// - We unset CSIZE, which means we don't set the character size
// - We unset PARENB, which means we don't enable parity generation on output and parity checking for input

// - We don't unset INPCK, which means we *would* check parity... except actually we don't because we unset PARENB

// Some parts of this config may be redundant, but it's likely just so that things are more explicit semantically
// I basically pulled out the `cfmakeraw()` config to make it more explicit what we're doing here
// But the config below *should* be equivalent to `cfmakeraw()` (on most systems)
tty__termios_tmp.c_iflag &= ~(BRKINT | ICRNL | ISTRIP | IXON | INLCR | IGNCR | IGNBRK | PARMRK);
tty__termios_tmp.c_oflag &= ~OPOST;
tty__termios_tmp.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG | ECHONL);
tty__termios_tmp.c_cflag &= ~(CSIZE | PARENB);
tty__termios_tmp.c_cflag |= CS8;

if (UNLIKELY(tcsetattr(fd, TCSADRAIN, &tty__termios_tmp)))
return -6;

break;
case TTY_MODE_UNSET:
// User should really never call this with TTY_MODE_UNSET
return -10;
default:
return -1;
}

return 0;
}

int tty__set_async_io_mode(int fd, bool enabled)
{
tty_mode_t current_mode = tty__get_mode(fd);
bool is_async_io = current_mode == TTY_MODE_RAW_ASYNC_IO;

// If already in desired state, then return 0
if (is_async_io == enabled)
return 0;

if (enabled) {
// If enabling, cache the current mode so we can restore it later
tty__cached_mode = current_mode;
// Set the mode to raw async io
return tty__set_mode(fd, TTY_MODE_RAW_ASYNC_IO);
} else {
// If disabling, restore the cached mode and reset cached mode
// If unset, then default to normal. Though this should never happen under normal circumstances
auto result = tty__set_mode(fd, tty__cached_mode != TTY_MODE_UNSET ? tty__cached_mode : TTY_MODE_NORMAL);
tty__cached_mode = TTY_MODE_UNSET;
return result;
}
}

} // namespace Zig
34 changes: 34 additions & 0 deletions src/bun.js/bindings/TTYHelper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#pragma once

#include <termios.h>

#ifndef STDIN_FILENO
#define STDIN_FILENO 0
#endif

#ifndef UNLIKELY
#define UNLIKELY(x) __builtin_expect(!!(x), 0)
#endif

#ifndef LIKELY
#define LIKELY(x) __builtin_expect(!!(x), 1)
#endif

namespace Zig {

extern "C" typedef enum {
TTY_MODE_NORMAL,
TTY_MODE_RAW,
TTY_MODE_RAW_ASYNC_IO,
TTY_MODE_UNSET = -1,
} tty_mode_t;

extern "C" int32_t tty__is_tty(int32_t fd);
extern "C" int32_t tty__get_termios(int32_t fd, termios* termios_p);
extern "C" int32_t tty__is_raw(int32_t fd);
extern "C" int32_t tty__set_mode(int32_t fd, tty_mode_t mode);
extern "C" tty_mode_t tty__get_mode(int32_t fd);

int tty__set_async_io_mode(int fd, bool enabled);

} // namespace Zig
10 changes: 10 additions & 0 deletions src/bun.js/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
#include "JSStringDecoder.h"
#include "JSReadableState.h"
#include "JSReadableHelper.h"
#include "JSTTYHelper.h"
#include "Process.h"

#include "WebCoreJSBuiltinInternals.h"
Expand Down Expand Up @@ -1194,6 +1195,7 @@ static JSC_DEFINE_HOST_FUNCTION(functionLazyLoad,
static NeverDestroyed<const String> sqliteString(MAKE_STATIC_STRING_IMPL("sqlite"));
static NeverDestroyed<const String> bunJSCString(MAKE_STATIC_STRING_IMPL("bun:jsc"));
static NeverDestroyed<const String> bunStreamString(MAKE_STATIC_STRING_IMPL("bun:stream"));
static NeverDestroyed<const String> bunTtyString(MAKE_STATIC_STRING_IMPL("bun:tty"));
static NeverDestroyed<const String> noopString(MAKE_STATIC_STRING_IMPL("noop"));
static NeverDestroyed<const String> createImportMeta(MAKE_STATIC_STRING_IMPL("createImportMeta"));
static NeverDestroyed<const String> masqueradesAsUndefined(MAKE_STATIC_STRING_IMPL("masqueradesAsUndefined"));
Expand Down Expand Up @@ -1271,6 +1273,14 @@ static JSC_DEFINE_HOST_FUNCTION(functionLazyLoad,
return JSValue::encode(obj);
}

if (string == bunTtyString) {
auto* obj = constructEmptyObject(globalObject);
obj->putDirect(vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "isRaw"_s)), JSC::JSFunction::create(vm, globalObject, 0, "isRaw"_s, jsFunctionInternalTty_isRaw, ImplementationVisibility::Public), 0);
obj->putDirect(vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "setRawMode"_s)), JSC::JSFunction::create(vm, globalObject, 0, "setRawMode"_s, jsFunctionInternalTty_setRawMode, ImplementationVisibility::Public), 0);
obj->putDirect(vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "setAsyncIoMode"_s)), JSC::JSFunction::create(vm, globalObject, 0, "setAsyncIoMode"_s, jsFunctionInternalTty_setAsyncIoMode, ImplementationVisibility::Public), 0);
return JSValue::encode(obj);
}

if (string == createImportMeta) {
Zig::ImportMetaObject* obj = Zig::ImportMetaObject::create(globalObject, callFrame->argument(1));
return JSValue::encode(obj);
Expand Down
Loading