mirror of
https://github.com/oven-sh/bun
synced 2026-02-13 04:18:58 +00:00
Fixes #22785 The issue was that Bun was saving and restoring terminal settings for all TTY file descriptors at process initialization and exit, even when it never modified them. This could interfere with programs like 'less' that need to control the terminal when receiving piped input. The fix follows libuv's approach: - Only save terminal settings when we actually modify them via Bun__ttySetMode - Don't preemptively save terminal settings at initialization - Only restore settings for file descriptors we actually modified This prevents Bun from interfering with terminal control when stdout is piped to pagers like 'less'. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
297 lines
9.1 KiB
C++
297 lines
9.1 KiB
C++
#include "root.h"
|
|
#include "wtf-bindings.h"
|
|
#include <wtf/StackBounds.h>
|
|
#include <wtf/StackCheck.h>
|
|
#include <wtf/StackTrace.h>
|
|
#include <wtf/dtoa.h>
|
|
#include <atomic>
|
|
|
|
#include "wtf/SIMDUTF.h"
|
|
#if OS(WINDOWS)
|
|
#include <uv.h>
|
|
#endif
|
|
|
|
#if !OS(WINDOWS)
|
|
#include <stdatomic.h>
|
|
|
|
#include <termios.h>
|
|
static int orig_termios_fd = -1;
|
|
static struct termios orig_termios;
|
|
static std::atomic<int> orig_termios_spinlock;
|
|
static std::once_flag reset_once_flag;
|
|
|
|
static int current_tty_mode = 0;
|
|
static struct termios orig_tty_termios;
|
|
|
|
// External declarations for terminal restoration
|
|
extern "C" int32_t bun_stdio_tty[3];
|
|
extern struct termios termios_to_restore_later[3];
|
|
extern bool termios_was_saved[3];
|
|
|
|
#include <signal.h>
|
|
extern "C" void onExitSignal(int sig);
|
|
static std::once_flag signal_handler_once_flag;
|
|
|
|
int uv__tcsetattr(int fd, int how, const struct termios* term)
|
|
{
|
|
int rc;
|
|
|
|
do
|
|
rc = tcsetattr(fd, how, term);
|
|
while (rc == -1 && errno == EINTR);
|
|
|
|
if (rc == -1)
|
|
return errno;
|
|
|
|
return 0;
|
|
}
|
|
|
|
extern "C" int uv_tty_reset_mode(void)
|
|
{
|
|
int saved_errno;
|
|
int err;
|
|
|
|
saved_errno = errno;
|
|
|
|
if (atomic_exchange(&orig_termios_spinlock, 1))
|
|
return 16; // UV_EBUSY; /* In uv_tty_set_mode(). */
|
|
|
|
err = 0;
|
|
if (orig_termios_fd != -1)
|
|
err = uv__tcsetattr(orig_termios_fd, TCSANOW, &orig_termios);
|
|
|
|
atomic_store(&orig_termios_spinlock, 0);
|
|
errno = saved_errno;
|
|
|
|
return err;
|
|
}
|
|
|
|
static void uv__tty_make_raw(struct termios* tio)
|
|
{
|
|
assert(tio != NULL);
|
|
|
|
#if defined __sun || defined __MVS__
|
|
/*
|
|
* This implementation of cfmakeraw for Solaris and derivatives is taken from
|
|
* http://www.perkin.org.uk/posts/solaris-portability-cfmakeraw.html.
|
|
*/
|
|
tio->c_iflag &= ~(IMAXBEL | IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
|
|
tio->c_oflag &= ~OPOST;
|
|
tio->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
|
|
tio->c_cflag &= ~(CSIZE | PARENB);
|
|
tio->c_cflag |= CS8;
|
|
|
|
/*
|
|
* By default, most software expects a pending read to block until at
|
|
* least one byte becomes available. As per termio(7I), this requires
|
|
* setting the MIN and TIME parameters appropriately.
|
|
*
|
|
* As a somewhat unfortunate artifact of history, the MIN and TIME slots
|
|
* in the control character array overlap with the EOF and EOL slots used
|
|
* for canonical mode processing. Because the EOF character needs to be
|
|
* the ASCII EOT value (aka Control-D), it has the byte value 4. When
|
|
* switching to raw mode, this is interpreted as a MIN value of 4; i.e.,
|
|
* reads will block until at least four bytes have been input.
|
|
*
|
|
* Other platforms with a distinct MIN slot like Linux and FreeBSD appear
|
|
* to default to a MIN value of 1, so we'll force that value here:
|
|
*/
|
|
tio->c_cc[VMIN] = 1;
|
|
tio->c_cc[VTIME] = 0;
|
|
#else
|
|
cfmakeraw(tio);
|
|
#endif /* #ifdef __sun */
|
|
}
|
|
|
|
#endif
|
|
|
|
extern "C" void Bun__atexit(void (*func)(void));
|
|
|
|
extern "C" int Bun__ttySetMode(int fd, int mode)
|
|
{
|
|
#if !OS(WINDOWS)
|
|
struct termios tmp;
|
|
int expected;
|
|
int rc;
|
|
|
|
if (current_tty_mode == mode)
|
|
return 0;
|
|
|
|
if (current_tty_mode == 0 && mode != 0) {
|
|
do {
|
|
rc = tcgetattr(fd, &orig_tty_termios);
|
|
} while (rc == -1 && errno == EINTR);
|
|
|
|
if (rc == -1)
|
|
return errno;
|
|
|
|
/* This is used for uv_tty_reset_mode() */
|
|
do {
|
|
expected = 0;
|
|
} while (!atomic_compare_exchange_strong(&orig_termios_spinlock, &expected, 1));
|
|
|
|
if (orig_termios_fd == -1) {
|
|
orig_termios = orig_tty_termios;
|
|
orig_termios_fd = fd;
|
|
|
|
// Save the terminal settings for restoration later
|
|
// This follows libuv's approach - only save when we actually modify
|
|
if (fd >= 0 && fd <= 2 && bun_stdio_tty[fd]) {
|
|
termios_to_restore_later[fd] = orig_tty_termios;
|
|
termios_was_saved[fd] = true;
|
|
}
|
|
}
|
|
|
|
atomic_store(&orig_termios_spinlock, 0);
|
|
}
|
|
|
|
tmp = orig_tty_termios;
|
|
switch (mode) {
|
|
case 0: // normal
|
|
break;
|
|
case 1: // raw
|
|
tmp.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
|
|
tmp.c_oflag |= (ONLCR);
|
|
tmp.c_cflag |= (CS8);
|
|
tmp.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
|
|
tmp.c_cc[VMIN] = 1;
|
|
tmp.c_cc[VTIME] = 0;
|
|
|
|
std::call_once(reset_once_flag, [] {
|
|
Bun__atexit([] {
|
|
uv_tty_reset_mode();
|
|
});
|
|
});
|
|
|
|
// Set up signal handlers when we actually modify terminal settings
|
|
std::call_once(signal_handler_once_flag, [] {
|
|
struct sigaction sa;
|
|
memset(&sa, 0, sizeof(sa));
|
|
sigemptyset(&sa.sa_mask);
|
|
sa.sa_flags = SA_RESETHAND;
|
|
sa.sa_handler = onExitSignal;
|
|
sigaction(SIGTERM, &sa, nullptr);
|
|
sigaction(SIGINT, &sa, nullptr);
|
|
});
|
|
break;
|
|
case 2: // io
|
|
uv__tty_make_raw(&tmp);
|
|
|
|
std::call_once(reset_once_flag, [] {
|
|
Bun__atexit([] {
|
|
uv_tty_reset_mode();
|
|
});
|
|
});
|
|
|
|
// Set up signal handlers when we actually modify terminal settings
|
|
std::call_once(signal_handler_once_flag, [] {
|
|
struct sigaction sa;
|
|
memset(&sa, 0, sizeof(sa));
|
|
sigemptyset(&sa.sa_mask);
|
|
sa.sa_flags = SA_RESETHAND;
|
|
sa.sa_handler = onExitSignal;
|
|
sigaction(SIGTERM, &sa, nullptr);
|
|
sigaction(SIGINT, &sa, nullptr);
|
|
});
|
|
break;
|
|
}
|
|
|
|
/* Apply changes after draining */
|
|
rc = uv__tcsetattr(fd, TCSADRAIN, &tmp);
|
|
if (rc == 0)
|
|
current_tty_mode = mode;
|
|
|
|
return rc;
|
|
#else
|
|
return 0;
|
|
|
|
#endif
|
|
}
|
|
|
|
extern "C" double WTF__parseDouble(const LChar* string, size_t length, size_t* position)
|
|
{
|
|
return WTF::parseDouble({ string, length }, *position);
|
|
}
|
|
|
|
extern "C" size_t WTF__base64URLEncode(const char* __restrict inputDataBuffer, size_t inputDataBufferSize,
|
|
char* __restrict destinationDataBuffer,
|
|
size_t destinationDataBufferSize)
|
|
{
|
|
UNUSED_PARAM(destinationDataBufferSize);
|
|
return simdutf::binary_to_base64(inputDataBuffer, inputDataBufferSize, destinationDataBuffer, simdutf::base64_url);
|
|
}
|
|
|
|
namespace Bun {
|
|
String base64URLEncodeToString(Vector<uint8_t> data)
|
|
{
|
|
auto size = data.size();
|
|
size_t encodedLength = ((size * 4) + 2) / 3;
|
|
if (!encodedLength)
|
|
return String();
|
|
|
|
std::span<LChar> ptr;
|
|
auto result = String::createUninitialized(encodedLength, ptr);
|
|
|
|
encodedLength = WTF__base64URLEncode(reinterpret_cast<const char*>(data.begin()), data.size(), reinterpret_cast<char*>(ptr.data()), encodedLength);
|
|
if (result.length() != encodedLength) {
|
|
return result.substringSharingImpl(0, encodedLength);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// https://github.com/oven-sh/WebKit/blob/b7bc2ba65db9774d201018f2e1a0a891d6365c13/Source/JavaScriptCore/runtime/DatePrototype.cpp#L323-L345
|
|
size_t toISOString(JSC::VM& vm, double date, char in[64])
|
|
{
|
|
if (!std::isfinite(date))
|
|
return 0;
|
|
|
|
GregorianDateTime gregorianDateTime;
|
|
vm.dateCache.msToGregorianDateTime(date, WTF::TimeType::UTCTime, gregorianDateTime);
|
|
|
|
// Maximum amount of space we need in buffer: 8 (max. digits in year) + 2 * 5 (2 characters each for month, day, hour, minute, second) + 4 (. + 3 digits for milliseconds)
|
|
// 6 for formatting and one for null termination = 29.
|
|
char buffer[29];
|
|
// If the year is outside the bounds of 0 and 9999 inclusive we want to use the extended year format (ES 15.9.1.15.1).
|
|
int ms = static_cast<int>(fmod(date, msPerSecond));
|
|
if (ms < 0)
|
|
ms += msPerSecond;
|
|
|
|
int charactersWritten;
|
|
if (gregorianDateTime.year() > 9999 || gregorianDateTime.year() < 0)
|
|
charactersWritten = snprintf(buffer, sizeof(buffer), "%+07d-%02d-%02dT%02d:%02d:%02d.%03dZ", gregorianDateTime.year(), gregorianDateTime.month() + 1, gregorianDateTime.monthDay(), gregorianDateTime.hour(), gregorianDateTime.minute(), gregorianDateTime.second(), ms);
|
|
else
|
|
charactersWritten = snprintf(buffer, sizeof(buffer), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", gregorianDateTime.year(), gregorianDateTime.month() + 1, gregorianDateTime.monthDay(), gregorianDateTime.hour(), gregorianDateTime.minute(), gregorianDateTime.second(), ms);
|
|
|
|
ASSERT(charactersWritten > 0 && static_cast<unsigned>(charactersWritten) < sizeof(buffer));
|
|
|
|
memcpy(in, buffer, charactersWritten + 1);
|
|
if (static_cast<unsigned>(charactersWritten) >= sizeof(buffer))
|
|
return 0;
|
|
|
|
return charactersWritten;
|
|
}
|
|
|
|
static thread_local WTF::StackBounds stackBoundsForCurrentThread = WTF::StackBounds::emptyBounds();
|
|
|
|
extern "C" [[ZIG_EXPORT(nothrow)]] void Bun__StackCheck__initialize()
|
|
{
|
|
stackBoundsForCurrentThread = WTF::StackBounds::currentThreadStackBounds();
|
|
}
|
|
|
|
extern "C" [[ZIG_EXPORT(nothrow)]] void* Bun__StackCheck__getMaxStack()
|
|
{
|
|
return stackBoundsForCurrentThread.end();
|
|
}
|
|
|
|
extern "C" void WTF__DumpStackTrace(void** stack, size_t stack_count)
|
|
{
|
|
WTFPrintBacktrace({ stack, stack_count });
|
|
}
|
|
|
|
extern "C" void WTF__releaseFastMallocFreeMemoryForThisThread()
|
|
{
|
|
WTF::releaseFastMallocFreeMemoryForThisThread();
|
|
}
|
|
|
|
}
|