From 85ebcd2d3d5f3eb0ad58176e811e83aa3a5f08d3 Mon Sep 17 00:00:00 2001 From: ihsandeniz <132747884+ihsandeniz@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:09:38 +0300 Subject: [PATCH 1/2] IO (Unix): fixes blocking forever on incomplete terminal responses ffGetTerminalResponse() polls only before the first read(). If the terminal sends a response that is cut short (e.g. `\e[4;800` without the rest), the following read() in the loop blocks forever because the tty is in non-canonical mode with VMIN=1. Wait with the same timeout before every subsequent read() as well. --- src/common/impl/io_unix.c | 57 ++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/src/common/impl/io_unix.c b/src/common/impl/io_unix.c index 2bd8abac26..ca5b5dca37 100644 --- a/src/common/impl/io_unix.c +++ b/src/common/impl/io_unix.c @@ -149,6 +149,32 @@ void restoreTerm(void) { tcsetattr(ftty, TCSAFLUSH, &oldTerm); } +static const char* waitForTerminalResponse(void) { +#ifndef __APPLE__ + const int pollResult = poll(&(struct pollfd) { .fd = ftty, .events = POLLIN }, 1, FF_IO_TERM_RESP_WAIT_MS); + if (pollResult <= 0) { + FF_DEBUG("poll(/dev/tty) returned %d after %d ms: %s", + pollResult, FF_IO_TERM_RESP_WAIT_MS, pollResult == 0 ? "timed out" : strerror(errno)); + return "poll(/dev/tty) timeout or failed"; + } +#else + { + // On macOS, poll(/dev/tty) always returns immediately + // See also https://nathancraddock.com/blog/macos-dev-tty-polling/ + fd_set rd; + FD_ZERO(&rd); + FD_SET(ftty, &rd); + const int selectResult = select(ftty + 1, &rd, nullptr, nullptr, &(struct timeval) { .tv_sec = FF_IO_TERM_RESP_WAIT_MS / 1000, .tv_usec = (FF_IO_TERM_RESP_WAIT_MS % 1000) * 1000 }); + if (selectResult <= 0) { + FF_DEBUG("select(/dev/tty) returned %d after %d ms: %s", + selectResult, FF_IO_TERM_RESP_WAIT_MS, selectResult == 0 ? "timed out" : strerror(errno)); + return "select(/dev/tty) timeout or failed"; + } + } +#endif + return nullptr; +} + const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...) { if (ftty < 0) { ftty = open("/dev/tty", O_RDWR | O_NOCTTY | O_CLOEXEC); @@ -174,28 +200,10 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char* ffWriteFDData(ftty, strlen(request), request); // Give the terminal some time to respond -#ifndef __APPLE__ - const int pollResult = poll(&(struct pollfd) { .fd = ftty, .events = POLLIN }, 1, FF_IO_TERM_RESP_WAIT_MS); - if (pollResult <= 0) { - FF_DEBUG("poll(/dev/tty) returned %d after %d ms: %s", - pollResult, FF_IO_TERM_RESP_WAIT_MS, pollResult == 0 ? "timed out" : strerror(errno)); - return "poll(/dev/tty) timeout or failed"; - } -#else - { - // On macOS, poll(/dev/tty) always returns immediately - // See also https://nathancraddock.com/blog/macos-dev-tty-polling/ - fd_set rd; - FD_ZERO(&rd); - FD_SET(ftty, &rd); - const int selectResult = select(ftty + 1, &rd, nullptr, nullptr, &(struct timeval) { .tv_sec = FF_IO_TERM_RESP_WAIT_MS / 1000, .tv_usec = (FF_IO_TERM_RESP_WAIT_MS % 1000) * 1000 }); - if (selectResult <= 0) { - FF_DEBUG("select(/dev/tty) returned %d after %d ms: %s", - selectResult, FF_IO_TERM_RESP_WAIT_MS, selectResult == 0 ? "timed out" : strerror(errno)); - return "select(/dev/tty) timeout or failed"; - } + const char* error = waitForTerminalResponse(); + if (error) { + return error; } -#endif char buffer[1024]; size_t bytesRead = 0; @@ -204,6 +212,13 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char* va_start(args, format); while (true) { + // The response may arrive in several chunks; a terminal that sends an incomplete + // response would otherwise block `read` forever + if (bytesRead > 0 && (error = waitForTerminalResponse())) { + va_end(args); + return error; + } + ssize_t nRead = read(ftty, buffer + bytesRead, sizeof(buffer) - bytesRead - 1); if (nRead <= 0) { From ae2138bc6ca7bf3ede13a6b523c13d8f02da5dba Mon Sep 17 00:00:00 2001 From: ihsandeniz <132747884+ihsandeniz@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:04:39 +0300 Subject: [PATCH 2/2] IO (Windows): fixes blocking forever on incomplete terminal responses Same issue as on Unix: ffGetTerminalResponse() waits for input only before the first ReadFile(). If the terminal answers only part of the request (e.g. one of the two OSC 10/11 replies), the next ReadFile() blocks forever. Wait with the same timeout before every subsequent ReadFile() as well, and restore the console mode on that error path. While waiting, also skip key-up records: ReadFile() returns no data for them, so one left alone in the buffer would end the wait and the following ReadFile() would block. --- src/common/impl/io_windows.c | 66 +++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/src/common/impl/io_windows.c b/src/common/impl/io_windows.c index 91f58abceb..e787a83d07 100644 --- a/src/common/impl/io_windows.c +++ b/src/common/impl/io_windows.c @@ -414,6 +414,35 @@ void ffListFilesRecursively(const char* path, bool pretty) { listFilesRecursively(folder.length, &folder, 0, nullptr, pretty); } +static const char* waitForTerminalResponse(HANDLE hInput) { + while (true) { + NTSTATUS status = NtWaitForSingleObject(hInput, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) FF_IO_TERM_RESP_WAIT_MS * -10000 }); + if (status != STATUS_WAIT_0) { + FF_DEBUG("NtWaitForSingleObject() failed or timeout: %s", ffDebugNtStatus(status)); + return "NtWaitForSingleObject() failed or timeout"; + } + + // Ignore all unexpected input events + INPUT_RECORD record; + DWORD len = 0; + if (!PeekConsoleInputW(hInput, &record, 1, &len)) { + return nullptr; + } + + // Key-up events carry no input for `ReadFile`; leaving one in the buffer would make the + // wait return while the next `ReadFile` still blocks + if ( + record.EventType == KEY_EVENT && + record.Event.KeyEvent.bKeyDown && + record.Event.KeyEvent.uChar.UnicodeChar != L'\r' && + record.Event.KeyEvent.uChar.UnicodeChar != L'\n') { + return nullptr; + } + + ReadConsoleInputW(hInput, &record, 1, &len); + } +} + const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...) { HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE); FF_AUTO_CLOSE_FD HANDLE hConin = INVALID_HANDLE_VALUE; @@ -440,29 +469,10 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char* WriteFile(hOutput, request, (DWORD) strlen(request), &bytes, nullptr); } - while (true) { - NTSTATUS status = NtWaitForSingleObject(hInput, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) FF_IO_TERM_RESP_WAIT_MS * -10000 }); - if (status != STATUS_WAIT_0) { - FF_DEBUG("NtWaitForSingleObject() failed or timeout: %s", ffDebugNtStatus(status)); - SetConsoleMode(hInput, inputMode); - return "NtWaitForSingleObject() failed or timeout"; - } - - // Ignore all unexpected input events - INPUT_RECORD record; - DWORD len = 0; - if (!PeekConsoleInputW(hInput, &record, 1, &len)) { - break; - } - - if ( - record.EventType == KEY_EVENT && - record.Event.KeyEvent.uChar.UnicodeChar != L'\r' && - record.Event.KeyEvent.uChar.UnicodeChar != L'\n') { - break; - } else { - ReadConsoleInputW(hInput, &record, 1, &len); - } + const char* error = waitForTerminalResponse(hInput); + if (error) { + SetConsoleMode(hInput, inputMode); + return error; } va_list args; @@ -472,6 +482,16 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char* uint32_t bytesRead = 0; while (true) { + // The response may arrive in several chunks; a terminal that sends an incomplete + // response would otherwise block `ReadFile` forever + if (bytesRead > 0 && (error = waitForTerminalResponse(hInput))) { + va_end(args); + if (hasInputMode) { + SetConsoleMode(hInput, inputMode); + } + return error; + } + DWORD bytes = 0; BOOL readOk = ReadFile(hInput, buffer + bytesRead, (DWORD) (sizeof(buffer) - 1 - bytesRead), &bytes, nullptr); if (!readOk || bytes == 0) {