Files
bun.sh/src/bun.js/bindings/ProcessBindingTTYWrap.cpp
Jarred Sumner 69be630aea WebKit Upgrade (#19839)
Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com>
Co-authored-by: Ben Grant <ben@bun.sh>
Co-authored-by: 190n <7763597+190n@users.noreply.github.com>
2025-05-22 21:12:43 -07:00

521 lines
17 KiB
C++

#include "mimalloc.h"
#include "root.h"
#include "JavaScriptCore/JSDestructibleObject.h"
#include "JavaScriptCore/ExceptionScope.h"
#include "JavaScriptCore/Identifier.h"
#include <JavaScriptCore/ObjectConstructor.h>
#include "ProcessBindingTTYWrap.h"
#include "NodeTTYModule.h"
#include "WebCoreJSBuiltins.h"
#include <JavaScriptCore/FunctionPrototype.h>
#ifndef WIN32
#include <errno.h>
#include <dlfcn.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <netdb.h>
#include <unistd.h>
#include <sys/utsname.h>
#else
#include <uv.h>
#include <io.h>
#include <fcntl.h>
#endif
#if OS(WINDOWS)
extern "C" int Source__setRawModeStdin(bool raw);
namespace UV {
class TTY {
public:
uv_tty_t handle {};
uv_tty_t* tty() { return &handle; }
void close()
{
uv_close((uv_handle_t*)(tty()), [](uv_handle_t* handle) -> void {
uv_tty_t* ttyHandle = (uv_tty_t*)handle;
ptrdiff_t offset = offsetof(UV::TTY, handle);
UV::TTY* tty = (UV::TTY*)((char*)ttyHandle - offset);
delete tty;
});
}
};
}
#endif
namespace Bun {
using namespace JSC;
static bool getWindowSize(int fd, size_t* width, size_t* height)
{
#if OS(WINDOWS)
CONSOLE_SCREEN_BUFFER_INFO csbi;
HANDLE handle = INVALID_HANDLE_VALUE;
switch (fd) {
case 0:
handle = GetStdHandle(STD_INPUT_HANDLE);
break;
case 1:
handle = GetStdHandle(STD_OUTPUT_HANDLE);
break;
case 2:
handle = GetStdHandle(STD_ERROR_HANDLE);
break;
default:
break;
}
if (handle == INVALID_HANDLE_VALUE)
return false;
if (!GetConsoleScreenBufferInfo(handle, &csbi))
return false;
*width = csbi.srWindow.Right - csbi.srWindow.Left + 1;
*height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
return true;
#else
struct winsize ws;
int err;
do
err = ioctl(fd, TIOCGWINSZ, &ws);
while (err == -1 && errno == EINTR);
if (err == -1)
return false;
*width = ws.ws_col;
*height = ws.ws_row;
return true;
#endif
}
class TTYWrapObject final : public JSC::JSDestructibleObject {
public:
using Base = JSC::JSDestructibleObject;
static constexpr unsigned StructureFlags = Base::StructureFlags;
static TTYWrapObject* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, int fd)
{
TTYWrapObject* object = new (NotNull, JSC::allocateCell<TTYWrapObject>(vm)) TTYWrapObject(vm, structure, fd);
object->finishCreation(vm);
return object;
}
DECLARE_INFO;
template<typename, JSC::SubspaceAccess mode> static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
{
if constexpr (mode == JSC::SubspaceAccess::Concurrently)
return nullptr;
return WebCore::subspaceForImpl<TTYWrapObject, UseCustomHeapCellType::No>(
vm,
[](auto& spaces) { return spaces.m_clientSubspaceForTTYWrapObject.get(); },
[](auto& spaces, auto&& space) { spaces.m_clientSubspaceForTTYWrapObject = std::forward<decltype(space)>(space); },
[](auto& spaces) { return spaces.m_subspaceForTTYWrapObject.get(); },
[](auto& spaces, auto&& space) { spaces.m_subspaceForTTYWrapObject = std::forward<decltype(space)>(space); });
}
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSObject* prototype)
{
return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
}
static void destroy(JSC::JSCell* cell)
{
static_cast<TTYWrapObject*>(cell)->TTYWrapObject::~TTYWrapObject();
}
~TTYWrapObject()
{
#if OS(WINDOWS)
if (handle) {
handle->close();
}
#endif
}
int fd = -1;
#if OS(WINDOWS)
UV::TTY* handle;
#endif
private:
TTYWrapObject(JSC::VM& vm, JSC::Structure* structure, const int fd)
: Base(vm, structure)
, fd(fd)
{
#if OS(WINDOWS)
handle = nullptr;
#endif
}
void finishCreation(JSC::VM& vm)
{
Base::finishCreation(vm);
ASSERT(inherits(info()));
}
};
#if OS(WINDOWS)
extern "C" void Bun__setCTRLHandler(BOOL add);
#endif
const ClassInfo TTYWrapObject::s_info = {
"LibuvStreamWrap"_s,
&Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(TTYWrapObject)
};
JSC::EncodedJSValue Process_functionInternalGetWindowSize(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame);
extern "C" int Bun__ttySetMode(int fd, int mode);
JSC_DEFINE_HOST_FUNCTION(jsTTYSetMode, (JSC::JSGlobalObject * globalObject, CallFrame* callFrame))
{
#if OS(WINDOWS)
ASSERT(callFrame->argumentCount() == 1);
auto flag = callFrame->argument(0);
bool raw = flag.asBoolean();
Zig::GlobalObject* global = jsCast<Zig::GlobalObject*>(globalObject);
return JSValue::encode(jsNumber(Source__setRawModeStdin(raw)));
#else
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
if (callFrame->argumentCount() != 2) {
throwTypeError(globalObject, scope, "Expected 2 arguments"_s);
return {};
}
JSValue fd = callFrame->argument(0);
if (!fd.isNumber()) {
throwTypeError(globalObject, scope, "fd must be a number"_s);
return {};
}
JSValue mode = callFrame->argument(1);
auto fdToUse = fd.toInt32(globalObject);
RETURN_IF_EXCEPTION(scope, {});
// Nodejs does not throw when ttySetMode fails. An Error event is emitted instead.
int err = Bun__ttySetMode(fdToUse, mode.toInt32(globalObject));
return JSValue::encode(jsNumber(err));
#endif
}
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 = jsDynamicCast<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 {};
}
#if OS(WINDOWS)
if (mode.toInt32(globalObject) == 0) {
Bun__setCTRLHandler(1);
}
int err = uv_tty_set_mode(ttyWrap->handle->tty(), mode.toInt32(globalObject));
#else
// Nodejs does not throw when ttySetMode fails. An Error event is emitted instead.
int err = Bun__ttySetMode(fd, mode.toInt32(globalObject));
#endif
return JSValue::encode(jsNumber(err));
}
JSC_DEFINE_HOST_FUNCTION(TTYWrap_functionGetWindowSize,
(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, "getWindowSize requires 1 argument (an array)"_s);
return {};
}
TTYWrapObject* ttyWrap = jsDynamicCast<TTYWrapObject*>(callFrame->thisValue());
if (!ttyWrap) [[unlikely]] {
JSC::throwTypeError(globalObject, throwScope, "TTY.getWindowSize expects a TTYWrapObject as this"_s);
return {};
}
int fd = ttyWrap->fd;
JSC::JSArray* array = jsDynamicCast<JSC::JSArray*>(callFrame->uncheckedArgument(0));
if (!array || array->length() < 2) {
JSC::throwTypeError(globalObject, throwScope, "getWindowSize expects an array"_s);
return {};
}
size_t width, height;
if (!getWindowSize(fd, &width, &height)) {
return JSC::JSValue::encode(jsBoolean(false));
}
array->putDirectIndex(globalObject, 0, jsNumber(width));
array->putDirectIndex(globalObject, 1, jsNumber(height));
return JSC::JSValue::encode(jsBoolean(true));
}
JSC_DEFINE_HOST_FUNCTION(Process_functionInternalGetWindowSize,
(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, "getWindowSize requires 2 argument (a file descriptor)"_s);
return {};
}
int fd = callFrame->uncheckedArgument(0).toInt32(globalObject);
RETURN_IF_EXCEPTION(throwScope, {});
JSC::JSArray* array = jsDynamicCast<JSC::JSArray*>(callFrame->uncheckedArgument(1));
if (!array || array->length() < 2) {
JSC::throwTypeError(globalObject, throwScope, "getWindowSize requires 2 argument (an array)"_s);
return {};
}
size_t width, height;
if (!getWindowSize(fd, &width, &height)) {
return JSC::JSValue::encode(jsBoolean(false));
}
array->putDirectIndex(globalObject, 0, jsNumber(width));
array->putDirectIndex(globalObject, 1, jsNumber(height));
return JSC::JSValue::encode(jsBoolean(true));
}
static const HashTableValue TTYWrapPrototypeValues[] = {
{ "getWindowSize"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TTYWrap_functionGetWindowSize, 1 } },
{ "setRawMode"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TTYWrap_functionSetMode, 0 } },
};
class TTYWrapPrototype final : public JSC::JSNonFinalObject {
public:
DECLARE_INFO;
using Base = JSC::JSNonFinalObject;
static Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
{
return Structure::create(vm, globalObject, globalObject->objectPrototype(), TypeInfo(ObjectType, StructureFlags), info());
}
static TTYWrapPrototype* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure)
{
TTYWrapPrototype* prototype = new (NotNull, JSC::allocateCell<TTYWrapPrototype>(vm)) TTYWrapPrototype(vm, structure);
prototype->finishCreation(vm, globalObject);
return prototype;
}
template<typename CellType, JSC::SubspaceAccess>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
{
STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(TTYWrapPrototype, Base);
return &vm.plainObjectSpace();
}
void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
{
Base::finishCreation(vm);
reifyStaticProperties(vm, TTYWrapObject::info(), TTYWrapPrototypeValues, *this);
JSC_TO_STRING_TAG_WITHOUT_TRANSITION();
}
TTYWrapPrototype(JSC::VM& vm, JSC::Structure* structure)
: Base(vm, structure)
{
}
};
const ClassInfo TTYWrapPrototype::s_info = {
"LibuvStreamWrap"_s,
&Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(TTYWrapPrototype)
};
class TTYWrapConstructor final : public JSC::InternalFunction {
public:
using Base = JSC::InternalFunction;
static TTYWrapConstructor* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSObject* prototype)
{
TTYWrapConstructor* constructor = new (NotNull, JSC::allocateCell<TTYWrapConstructor>(vm)) TTYWrapConstructor(vm, structure);
constructor->finishCreation(vm, globalObject, prototype);
return constructor;
}
static constexpr unsigned StructureFlags = Base::StructureFlags;
static constexpr JSC::DestructionMode needsDestruction = DoesNotNeedDestruction;
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
{
return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, StructureFlags), info());
}
template<typename, JSC::SubspaceAccess mode> static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
{
if constexpr (mode == JSC::SubspaceAccess::Concurrently)
return nullptr;
return &vm.internalFunctionSpace();
}
static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES call(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe)
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
throwTypeError(globalObject, scope, "TTYWrapConstructor cannot be called as a function"_s);
return {};
}
// new TTY()
static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES construct(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe)
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
auto* constructor = jsDynamicCast<TTYWrapConstructor*>(callframe->jsCallee());
if (!constructor) {
throwTypeError(globalObject, scope, "TTYWrapConstructor::construct called with wrong 'this' value"_s);
return {};
}
if (callframe->argumentCount() < 1) {
throwTypeError(globalObject, scope, "Expected at least 1 argument"_s);
return {};
}
JSValue fd_value = callframe->argument(0);
int32_t fd = fd_value.toInt32(globalObject);
RETURN_IF_EXCEPTION(scope, {});
if (fd < 0) {
throwTypeError(globalObject, scope, "fd must be a positive number"_s);
return {};
}
auto prototypeValue = constructor->get(globalObject, vm.propertyNames->prototype);
RETURN_IF_EXCEPTION(scope, {});
if (!prototypeValue.isObject()) {
throwTypeError(globalObject, scope, "TTYWrapConstructor prototype is not an object"_s);
return {};
}
#if OS(WINDOWS)
auto* handle = new UV::TTY();
memset(handle, 0, sizeof(UV::TTY));
int rc = uv_tty_init(jsCast<Zig::GlobalObject*>(globalObject)->uvLoop(), handle->tty(), fd, 0);
if (rc < 0) {
delete handle;
throwTypeError(globalObject, scope, "Failed to initialize TTY handle"_s);
return {};
}
ASSERT(handle->tty()->loop);
#else
if (!isatty(fd)) {
throwTypeError(globalObject, scope, makeString("fd"_s, fd, " is not a tty"_s));
return {};
}
#endif
auto* structure = TTYWrapObject::createStructure(vm, globalObject, prototypeValue.getObject());
auto* object = TTYWrapObject::create(vm, globalObject, structure, fd);
#if OS(WINDOWS)
object->handle = handle;
#endif
return JSValue::encode(object);
}
DECLARE_EXPORT_INFO;
private:
TTYWrapConstructor(JSC::VM& vm, JSC::Structure* structure)
: Base(vm, structure, call, construct)
{
}
void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSObject* prototype)
{
Base::finishCreation(vm, 1, "TTYWrap"_s);
putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete);
}
};
const ClassInfo TTYWrapConstructor::s_info = { "TTY"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(TTYWrapConstructor) };
JSValue createBunTTYFunctions(Zig::GlobalObject* globalObject)
{
auto& vm = JSC::getVM(globalObject);
auto* obj = constructEmptyObject(globalObject);
obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "isatty"_s)), JSFunction::create(vm, globalObject, 0, "isatty"_s, Zig::jsFunctionTty_isatty, ImplementationVisibility::Public), 0);
obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "setRawMode"_s)), JSFunction::create(vm, globalObject, 0, "ttySetMode"_s, jsTTYSetMode, ImplementationVisibility::Public), 0);
obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "getWindowSize"_s)), JSFunction::create(vm, globalObject, 0, "getWindowSize"_s, Bun::Process_functionInternalGetWindowSize, ImplementationVisibility::Public), 0);
return obj;
}
JSValue createNodeTTYWrapObject(JSC::JSGlobalObject* globalObject)
{
auto& vm = JSC::getVM(globalObject);
auto* obj = constructEmptyObject(globalObject);
obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "isTTY"_s)), JSFunction::create(vm, globalObject, 0, "isatty"_s, Zig::jsFunctionTty_isatty, ImplementationVisibility::Public), 0);
TTYWrapPrototype* prototype = TTYWrapPrototype::create(vm, globalObject, TTYWrapPrototype::createStructure(vm, globalObject));
TTYWrapConstructor* constructor = TTYWrapConstructor::create(vm, globalObject, TTYWrapConstructor::createStructure(vm, globalObject, globalObject->functionPrototype()), prototype);
obj->putDirect(vm, Identifier::fromString(vm, "TTY"_s), constructor, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete);
return obj;
}
}