[Lldb-commits] [lldb] [lldb][windows] inherit cursor's position when creating a ConPTY (PR #193818)
Charles Zablit via lldb-commits
lldb-commits at lists.llvm.org
Tue Apr 28 03:26:59 PDT 2026
https://github.com/charles-zablit updated https://github.com/llvm/llvm-project/pull/193818
>From 2fff3a18dc14d5967f7503fb335f21580dffaf21 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Thu, 23 Apr 2026 19:16:19 +0100
Subject: [PATCH 1/5] [lldb][windows] inherit cursor's position when creating a
ConPTY
---
.../Host/windows/ConnectionConPTYWindows.h | 1 -
.../include/lldb/Host/windows/PseudoConsole.h | 14 +--
.../Host/windows/ConnectionConPTYWindows.cpp | 41 +------
lldb/source/Host/windows/PseudoConsole.cpp | 101 +++++-------------
lldb/test/API/windows/conpty/TestConPTY.py | 42 +++++---
5 files changed, 53 insertions(+), 146 deletions(-)
diff --git a/lldb/include/lldb/Host/windows/ConnectionConPTYWindows.h b/lldb/include/lldb/Host/windows/ConnectionConPTYWindows.h
index 7f1524445ae4c..ce3ae737ad61f 100644
--- a/lldb/include/lldb/Host/windows/ConnectionConPTYWindows.h
+++ b/lldb/include/lldb/Host/windows/ConnectionConPTYWindows.h
@@ -43,7 +43,6 @@ class ConnectionConPTY : public ConnectionGenericFile {
protected:
std::shared_ptr<PseudoConsole> m_pty;
- bool m_pty_vt_sequence_was_stripped = false;
};
} // namespace lldb_private
diff --git a/lldb/include/lldb/Host/windows/PseudoConsole.h b/lldb/include/lldb/Host/windows/PseudoConsole.h
index 7cef792fd9c84..5bcd08e30ac90 100644
--- a/lldb/include/lldb/Host/windows/PseudoConsole.h
+++ b/lldb/include/lldb/Host/windows/PseudoConsole.h
@@ -42,8 +42,8 @@ class PseudoConsole {
bool inheritable);
/// Creates and opens a new ConPTY instance with a default console size of
- /// 80x25. Also sets up the associated STDIN/STDOUT pipes and drains any
- /// initialization sequences emitted by Windows.
+ /// 80x25. Also sets up the associated STDIN/STDOUT pipes and responds to
+ /// the cursor-position query that ConPTY emits at startup.
///
/// \return
/// An llvm::Error if the ConPTY could not be created, or if ConPTY is
@@ -108,16 +108,6 @@ class PseudoConsole {
Mode GetMode() const { return m_mode; };
- /// Drains initialization sequences from the ConPTY output pipe.
- ///
- /// When a process first attaches to a ConPTY, Windows emits VT100/ANSI escape
- /// sequences (ESC[2J for clear screen, ESC[H for cursor home and more) as
- /// part of the PseudoConsole initialization. To prevent these sequences from
- /// appearing in the debugger output (and flushing lldb's shell for instance)
- /// we launch a short-lived dummy process that triggers the initialization,
- /// then drain all output before launching the actual debuggee.
- llvm::Error DrainInitSequences();
-
/// Returns a reference to the mutex used to synchronize access to the
/// ConPTY state.
std::mutex &GetMutex() { return m_mutex; };
diff --git a/lldb/source/Host/windows/ConnectionConPTYWindows.cpp b/lldb/source/Host/windows/ConnectionConPTYWindows.cpp
index 39ca3522cb947..b0ce239bb8cb6 100644
--- a/lldb/source/Host/windows/ConnectionConPTYWindows.cpp
+++ b/lldb/source/Host/windows/ConnectionConPTYWindows.cpp
@@ -12,37 +12,6 @@
using namespace lldb;
using namespace lldb_private;
-/// Strips the ConPTY initialization sequences that Windows unconditionally
-/// emits when a process is first attached to a pseudo console.
-///
-/// These are emitted by ConPTY's host process (conhost.exe) at process attach
-/// time, not by the debuggee. They are always the first bytes on the output
-/// pipe and are always present as a contiguous prefix.
-///
-/// \param dst Buffer containing the data read from the ConPTY output pipe.
-/// Modified in place: if the initialization sequences are present
-/// as a prefix, they are removed by shifting the remaining bytes
-/// to the front of the buffer.
-/// \param dst_len The size of \p dst.
-/// \param len On input, the number of valid bytes in \p dst. On output,
-/// reduced by the number of bytes stripped.
-/// \return
-/// \p true if the sequence was found and stripped.
-static bool StripConPTYInitSequences(void *dst, size_t dst_len, size_t &len) {
- static const char sequences[] = "\x1b[?9001l\x1b[?1004l";
- static const size_t sequences_len = sizeof(sequences) - 1;
- char *buf = static_cast<char *>(dst);
- if (len >= sequences_len) {
- assert(dst_len >= len - sequences_len);
- if (memcmp(buf, sequences, sequences_len) == 0) {
- memmove(buf, buf + sequences_len, len - sequences_len);
- len -= sequences_len;
- return true;
- }
- }
- return false;
-}
-
ConnectionConPTY::ConnectionConPTY(std::shared_ptr<PseudoConsole> pty)
: ConnectionGenericFile(pty->GetSTDOUTHandle(), false), m_pty(pty) {}
@@ -69,15 +38,7 @@ size_t ConnectionConPTY::Read(void *dst, size_t dst_len,
m_pty->GetCV().wait(guard, [this] { return !m_pty->IsStopping(); });
}
- size_t bytes_read =
- ConnectionGenericFile::Read(dst, dst_len, timeout, status, error_ptr);
-
- if (bytes_read > 0 && !m_pty_vt_sequence_was_stripped) {
- if (StripConPTYInitSequences(dst, dst_len, bytes_read))
- m_pty_vt_sequence_was_stripped = true;
- }
-
- return bytes_read;
+ return ConnectionGenericFile::Read(dst, dst_len, timeout, status, error_ptr);
}
size_t ConnectionConPTY::Write(const void *src, size_t src_len,
diff --git a/lldb/source/Host/windows/PseudoConsole.cpp b/lldb/source/Host/windows/PseudoConsole.cpp
index ef6f17771fc34..1c5ce43355cd0 100644
--- a/lldb/source/Host/windows/PseudoConsole.cpp
+++ b/lldb/source/Host/windows/PseudoConsole.cpp
@@ -8,15 +8,13 @@
#include "lldb/Host/windows/PseudoConsole.h"
+#include <cstdio>
#include <mutex>
-#include "lldb/Host/windows/PipeWindows.h"
-#include "lldb/Host/windows/ProcessLauncherWindows.h"
#include "lldb/Host/windows/windows.h"
#include "lldb/Utility/LLDBLog.h"
#include "llvm/Support/Errc.h"
-#include "llvm/Support/Errno.h"
using namespace lldb_private;
@@ -26,6 +24,8 @@ typedef HRESULT(WINAPI *CreatePseudoConsole_t)(COORD size, HANDLE hInput,
typedef VOID(WINAPI *ClosePseudoConsole_t)(HPCON hPC);
+static constexpr DWORD PSEUDOCONSOLE_INHERIT_CURSOR = 0x1;
+
struct Kernel32 {
Kernel32() {
hModule = LoadLibraryW(L"kernel32.dll");
@@ -130,14 +130,22 @@ llvm::Error PseudoConsole::OpenPseudoConsole() {
}
COORD consoleSize{80, 25};
+ // Cursor position within the visible window, 1-indexed for VT sequences.
+ // Defaults to the last row so ConPTY won't scroll back over existing output
+ // if we can't query the real console.
+ int cursorRow = consoleSize.Y;
+ int cursorCol = 1;
CONSOLE_SCREEN_BUFFER_INFO csbi;
- if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi))
+ if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi)) {
consoleSize = {
static_cast<SHORT>(csbi.srWindow.Right - csbi.srWindow.Left + 1),
static_cast<SHORT>(csbi.srWindow.Bottom - csbi.srWindow.Top + 1)};
+ cursorRow = csbi.dwCursorPosition.Y - csbi.srWindow.Top + 1;
+ cursorCol = csbi.dwCursorPosition.X + 1;
+ }
HPCON hPC = INVALID_HANDLE_VALUE;
- HRESULT hr = kernel32.CreatePseudoConsole(consoleSize, hInputRead,
- hOutputWrite, 0, &hPC);
+ HRESULT hr = kernel32.CreatePseudoConsole(
+ consoleSize, hInputRead, hOutputWrite, PSEUDOCONSOLE_INHERIT_CURSOR, &hPC);
CloseHandle(hInputRead);
CloseHandle(hOutputWrite);
@@ -154,10 +162,16 @@ llvm::Error PseudoConsole::OpenPseudoConsole() {
m_conpty_input = hInputWrite;
m_mode = Mode::ConPTY;
- if (auto error = DrainInitSequences()) {
- Log *log = GetLog(LLDBLog::Host);
- LLDB_LOG_ERROR(log, std::move(error),
- "failed to finalize ConPTY's setup: {0}");
+ // PSEUDOCONSOLE_INHERIT_CURSOR causes ConPTY to emit ESC[6n on the output
+ // pipe to query the current cursor position before it finishes initializing.
+ // Write the cursor position response to the input pipe so ConPTY can read it
+ // and initialize without clearing the screen or overwriting LLDB's prompt.
+ {
+ char response[32];
+ int len = snprintf(response, sizeof(response), "\x1b[%d;%dR",
+ cursorRow, cursorCol);
+ DWORD nwritten = 0;
+ WriteFile(m_conpty_input, response, len, &nwritten, NULL);
}
return llvm::Error::success();
@@ -230,70 +244,3 @@ llvm::Error PseudoConsole::OpenAnonymousPipes() {
m_mode = Mode::Pipe;
return llvm::Error::success();
}
-
-llvm::Error PseudoConsole::DrainInitSequences() {
- STARTUPINFOEXW startupinfoex = {};
- startupinfoex.StartupInfo.cb = sizeof(STARTUPINFOEXW);
- startupinfoex.StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
-
- auto attributelist_or_err = ProcThreadAttributeList::Create(startupinfoex);
- if (!attributelist_or_err)
- return llvm::errorCodeToError(attributelist_or_err.getError());
- ProcThreadAttributeList attributelist = std::move(*attributelist_or_err);
- if (auto error = attributelist.SetupPseudoConsole(m_conpty_handle))
- return error;
-
- PROCESS_INFORMATION pi = {};
-
- wchar_t comspec[MAX_PATH];
- DWORD comspecLen = GetEnvironmentVariableW(L"COMSPEC", comspec, MAX_PATH);
- if (comspecLen == 0 || comspecLen >= MAX_PATH)
- return llvm::createStringError(
- std::error_code(GetLastError(), std::system_category()),
- "Failed to get the 'COMSPEC' environment variable");
-
- static constexpr char s_drain_marker[] = "LLDB_CONPTY_DRAIN_DONE";
- static constexpr wchar_t s_drain_marker_w[] = L"LLDB_CONPTY_DRAIN_DONE";
- std::wstring cmdline_str =
- std::wstring(comspec) + L" /c echo " + s_drain_marker_w;
- std::vector<wchar_t> cmdline(cmdline_str.begin(), cmdline_str.end());
- cmdline.push_back(L'\0');
-
- if (!CreateProcessW(/*lpApplicationName=*/comspec, cmdline.data(),
- /*lpProcessAttributes=*/NULL, /*lpThreadAttributes=*/NULL,
- /*bInheritHandles=*/TRUE,
- /*dwCreationFlags=*/EXTENDED_STARTUPINFO_PRESENT |
- CREATE_UNICODE_ENVIRONMENT,
- /*lpEnvironment=*/NULL, /*lpCurrentDirectory=*/NULL,
- /*lpStartupInfo=*/
- reinterpret_cast<STARTUPINFOW *>(&startupinfoex),
- /*lpProcessInformation=*/&pi))
- return llvm::errorCodeToError(
- std::error_code(GetLastError(), std::system_category()));
-
- char buf[4096];
- OVERLAPPED ov = {};
- ov.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
-
- DWORD read = 0;
- ReadFile(m_conpty_output, buf, sizeof(buf), &read, &ov);
-
- WaitForSingleObject(pi.hProcess, INFINITE);
-
- std::string accumulated;
- while (GetOverlappedResult(m_conpty_output, &ov, &read, /*bWait=*/TRUE) &&
- read > 0) {
- accumulated.append(buf, read);
- if (accumulated.find(s_drain_marker) != std::string::npos)
- break;
- ResetEvent(ov.hEvent);
- ReadFile(m_conpty_output, buf, sizeof(buf), &read, &ov);
- }
-
- CancelIo(m_conpty_output);
- CloseHandle(ov.hEvent);
- CloseHandle(pi.hProcess);
- CloseHandle(pi.hThread);
-
- return llvm::Error::success();
-}
diff --git a/lldb/test/API/windows/conpty/TestConPTY.py b/lldb/test/API/windows/conpty/TestConPTY.py
index 40b550a185af7..f48792deab199 100644
--- a/lldb/test/API/windows/conpty/TestConPTY.py
+++ b/lldb/test/API/windows/conpty/TestConPTY.py
@@ -53,27 +53,20 @@ def _run_to_exit(self, mode):
@skipUnlessWindowsConPTY2022
def test_stdout_delivery(self):
"""ConPTY delivers the inferior's stdout to LLDB."""
- output = self._run_to_exit("basic")
- self.assertEqual("Hello from ConPTY\r\n", output)
-
- @skipUnlessWindows
- @skipUnlessWindowsConPTY2022
- def test_vt_init_stripped(self):
- """ConPTY VT initialization sequences are stripped from GetSTDOUT."""
- # Sequences emitted by conhost.exe at attach time, defined in
- # ConnectionConPTYWindows.cpp :: StripConPTYInitSequences.
- VT_INIT = "\x1b[?9001l\x1b[?1004l"
+ import re
output = self._run_to_exit("basic")
-
+ output = re.sub(r"\x1b(?:\[[0-9;?]*[A-Za-z]|\][^\x07]*\x07)", "", output)
self.assertIn("Hello from ConPTY\r\n", output)
- self.assertNotIn(VT_INIT, output)
@skipUnlessWindows
@skipUnlessWindowsConPTY2022
def test_large_output(self):
"""ConPTY delivers all output lines when output spans multiple reads."""
+ import re
+
output = self._run_to_exit("large")
+ output = re.sub(r"\x1b(?:\[[0-9;?]*[A-Za-z]|\][^\x07]*\x07)", "", output)
output_lines = output.split("\r\n")[:-1]
self.assertEqual(
@@ -88,10 +81,9 @@ def test_large_output(self):
def test_basic_output_without_vt_check(self):
"""ConPTY delivers the inferior's stdout on all supported Windows versions.
- Unlike test_stdout_delivery and test_vt_init_stripped, this test strips
- VT escape sequences before asserting, so it passes on older Windows
- versions (e.g. Windows Server 2019) where ConPTY emits different init
- sequences that LLDB does not strip.
+ Unlike test_stdout_delivery, this test strips VT escape sequences before
+ asserting, so it passes on older Windows versions (e.g. Windows Server
+ 2019) where ConPTY emits different sequences.
"""
import re
@@ -99,3 +91,21 @@ def test_basic_output_without_vt_check(self):
output = self._run_to_exit("basic")
stripped = re.sub(r"\x1b\[[0-9;?]*[A-Za-z]", "", output)
self.assertIn("Hello from ConPTY", stripped)
+
+ @skipUnlessWindows
+ @skipUnlessWindowsConPTY2022
+ def test_no_screen_clear_on_init(self):
+ """PSEUDOCONSOLE_INHERIT_CURSOR prevents ConPTY from emitting
+ screen-clearing sequences that would overwrite existing terminal output.
+
+ With PSEUDOCONSOLE_INHERIT_CURSOR, ConPTY queries the current cursor
+ position (ESC[6n) and skips the full-screen reset it would otherwise
+ emit. Verify that none of those reset sequences appear in the process
+ output.
+ """
+ output = self._run_to_exit("basic")
+
+ # Emitted by ConPTY during a full-screen init (no cursor inheritance).
+ self.assertNotIn("\x1b[2J", output) # clear screen
+ self.assertNotIn("\x1b[3J", output) # erase scrollback
+ self.assertNotIn("\x1b[H", output) # cursor home
>From 2d3522e9b20da9a476579b666ac9b242eeb5c237 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Fri, 24 Apr 2026 15:16:30 +0100
Subject: [PATCH 2/5] strip specific vt sequences
---
.../Host/windows/ConnectionConPTYWindows.h | 1 +
.../Host/windows/ConnectionConPTYWindows.cpp | 97 ++++++++++++++++++-
2 files changed, 97 insertions(+), 1 deletion(-)
diff --git a/lldb/include/lldb/Host/windows/ConnectionConPTYWindows.h b/lldb/include/lldb/Host/windows/ConnectionConPTYWindows.h
index ce3ae737ad61f..7f1524445ae4c 100644
--- a/lldb/include/lldb/Host/windows/ConnectionConPTYWindows.h
+++ b/lldb/include/lldb/Host/windows/ConnectionConPTYWindows.h
@@ -43,6 +43,7 @@ class ConnectionConPTY : public ConnectionGenericFile {
protected:
std::shared_ptr<PseudoConsole> m_pty;
+ bool m_pty_vt_sequence_was_stripped = false;
};
} // namespace lldb_private
diff --git a/lldb/source/Host/windows/ConnectionConPTYWindows.cpp b/lldb/source/Host/windows/ConnectionConPTYWindows.cpp
index b0ce239bb8cb6..1a722e54e9f74 100644
--- a/lldb/source/Host/windows/ConnectionConPTYWindows.cpp
+++ b/lldb/source/Host/windows/ConnectionConPTYWindows.cpp
@@ -9,9 +9,97 @@
#include "lldb/Host/windows/ConnectionConPTYWindows.h"
#include "lldb/Utility/Status.h"
+#include <cstring>
+
using namespace lldb;
using namespace lldb_private;
+/// Remove ConPTY management sequences from a buffer in-place.
+///
+/// ConPTY injects several VT sequences into its output pipe that are not part
+/// of the inferior's output: a cursor-position query (\x1b[6n) emitted during
+/// PSEUDOCONSOLE_INHERIT_CURSOR initialisation, Win32 Input Mode toggles
+/// (\x1b[?9001h/l), focus-event toggles (\x1b[?1004h/l), and a window-title
+/// OSC sequence (\x1b]0;...\x07). These sequences must not reach the outer
+/// terminal.
+///
+/// \param[in,out] data Buffer containing raw ConPTY output.
+/// \param[in,out] len On entry, the number of valid bytes in \p data.
+/// Updated to the number of bytes after stripping.
+/// \return true if at least one sequence was stripped (caller should stop
+/// calling this function on future reads).
+static bool StripConPTYInitSequences(void *data, size_t &len) {
+ auto *buf = static_cast<char *>(data);
+ char *out = buf;
+ const char *in = buf;
+ const char *end = buf + len;
+ bool stripped = false;
+
+ while (in < end) {
+ if (*in != '\x1b') {
+ *out++ = *in++;
+ continue;
+ }
+
+ size_t remaining = end - in;
+
+ // \x1b[6n - cursor-position query (PSEUDOCONSOLE_INHERIT_CURSOR init)
+ if (remaining >= 4 && memcmp(in, "\x1b[6n", 4) == 0) {
+ in += 4;
+ stripped = true;
+ continue;
+ }
+
+ // \x1b[?9001h / \x1b[?9001l - Win32 Input Mode enable/disable
+ if (remaining >= 8 && memcmp(in, "\x1b[?9001", 7) == 0 &&
+ (in[7] == 'h' || in[7] == 'l')) {
+ in += 8;
+ stripped = true;
+ continue;
+ }
+
+ // \x1b[?1004h / \x1b[?1004l - focus-event reporting enable/disable
+ if (remaining >= 8 && memcmp(in, "\x1b[?1004", 7) == 0 &&
+ (in[7] == 'h' || in[7] == 'l')) {
+ in += 8;
+ stripped = true;
+ continue;
+ }
+
+ // \x1b[m - SGR reset emitted after cursor-position init
+ if (remaining >= 3 && memcmp(in, "\x1b[m", 3) == 0) {
+ in += 3;
+ stripped = true;
+ continue;
+ }
+
+ // \x1b[?25h - show cursor emitted after cursor-position init
+ if (remaining >= 6 && memcmp(in, "\x1b[?25h", 6) == 0) {
+ in += 6;
+ stripped = true;
+ continue;
+ }
+
+ // \x1b]0;...\x07 - ConPTY window-title OSC sequence
+ if (remaining >= 4 && in[1] == ']' && in[2] == '0' && in[3] == ';') {
+ const char *bel =
+ static_cast<const char *>(memchr(in + 4, '\x07', end - in - 4));
+ if (bel) {
+ in = bel + 1;
+ } else {
+ in = end;
+ }
+ stripped = true;
+ continue;
+ }
+
+ *out++ = *in++;
+ }
+
+ len = static_cast<size_t>(out - buf);
+ return stripped;
+}
+
ConnectionConPTY::ConnectionConPTY(std::shared_ptr<PseudoConsole> pty)
: ConnectionGenericFile(pty->GetSTDOUTHandle(), false), m_pty(pty) {}
@@ -38,7 +126,14 @@ size_t ConnectionConPTY::Read(void *dst, size_t dst_len,
m_pty->GetCV().wait(guard, [this] { return !m_pty->IsStopping(); });
}
- return ConnectionGenericFile::Read(dst, dst_len, timeout, status, error_ptr);
+ size_t bytes_read =
+ ConnectionGenericFile::Read(dst, dst_len, timeout, status, error_ptr);
+
+ if (bytes_read > 0 && !m_pty_vt_sequence_was_stripped) {
+ if (StripConPTYInitSequences(dst, bytes_read))
+ m_pty_vt_sequence_was_stripped = true;
+ }
+ return bytes_read;
}
size_t ConnectionConPTY::Write(const void *src, size_t src_len,
>From 45fdd053fe58613820470737f4152bbcaca5fb0e Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Fri, 24 Apr 2026 16:44:21 +0100
Subject: [PATCH 3/5] fix conpty CRLF interleaving
---
.../Host/windows/ConnectionConPTYWindows.h | 9 ++-
.../include/lldb/Host/windows/PseudoConsole.h | 3 +-
.../Host/windows/ConnectionConPTYWindows.cpp | 58 ++++++++++++-------
lldb/source/Host/windows/PseudoConsole.cpp | 11 ++--
lldb/test/API/windows/conpty/TestConPTY.py | 2 +-
5 files changed, 51 insertions(+), 32 deletions(-)
diff --git a/lldb/include/lldb/Host/windows/ConnectionConPTYWindows.h b/lldb/include/lldb/Host/windows/ConnectionConPTYWindows.h
index 7f1524445ae4c..518dcf2275582 100644
--- a/lldb/include/lldb/Host/windows/ConnectionConPTYWindows.h
+++ b/lldb/include/lldb/Host/windows/ConnectionConPTYWindows.h
@@ -33,8 +33,11 @@ class ConnectionConPTY : public ConnectionGenericFile {
///
/// Before reading, check if the ConPTY is closing and wait for it to close
/// before reading. This prevents race conditions when closing the ConPTY
- /// during a read. After reading, remove the ConPTY VT init sequence if
- /// present.
+ /// during a read.
+ /// After reading, remove the ConPTY VT init sequence if present. On the first
+ /// read that contains ConPTY management sequences (cursor query, Win32 Input
+ /// Mode, focus events, window title), strips them in-place and sets
+ /// m_conpty_sequences_stripped to skip scanning on all subsequent reads.
size_t Read(void *dst, size_t dst_len, const Timeout<std::micro> &timeout,
lldb::ConnectionStatus &status, Status *error_ptr) override;
@@ -43,7 +46,7 @@ class ConnectionConPTY : public ConnectionGenericFile {
protected:
std::shared_ptr<PseudoConsole> m_pty;
- bool m_pty_vt_sequence_was_stripped = false;
+ bool m_conpty_sequences_stripped = false;
};
} // namespace lldb_private
diff --git a/lldb/include/lldb/Host/windows/PseudoConsole.h b/lldb/include/lldb/Host/windows/PseudoConsole.h
index 5bcd08e30ac90..0e927e3a433ec 100644
--- a/lldb/include/lldb/Host/windows/PseudoConsole.h
+++ b/lldb/include/lldb/Host/windows/PseudoConsole.h
@@ -34,8 +34,7 @@ class PseudoConsole {
PseudoConsole &operator=(const PseudoConsole &) = delete;
PseudoConsole &operator=(PseudoConsole &&) = delete;
- /// Creates a named pipe pair for overlapped I/O. The read end is set to
- /// non-blocking (PIPE_NOWAIT).
+ /// Creates a named pipe pair for overlapped I/O.
/// On failure any handles that were successfully opened are closed and an
/// error is returned.
llvm::Error CreateOverlappedPipePair(HANDLE &out_read, HANDLE &out_write,
diff --git a/lldb/source/Host/windows/ConnectionConPTYWindows.cpp b/lldb/source/Host/windows/ConnectionConPTYWindows.cpp
index 1a722e54e9f74..e28c2f5b14dde 100644
--- a/lldb/source/Host/windows/ConnectionConPTYWindows.cpp
+++ b/lldb/source/Host/windows/ConnectionConPTYWindows.cpp
@@ -8,6 +8,7 @@
#include "lldb/Host/windows/ConnectionConPTYWindows.h"
#include "lldb/Utility/Status.h"
+#include "lldb/Utility/Timeout.h"
#include <cstring>
@@ -28,7 +29,7 @@ using namespace lldb_private;
/// Updated to the number of bytes after stripping.
/// \return true if at least one sequence was stripped (caller should stop
/// calling this function on future reads).
-static bool StripConPTYInitSequences(void *data, size_t &len) {
+static bool StripConPTYSequences(void *data, size_t &len) {
auto *buf = static_cast<char *>(data);
char *out = buf;
const char *in = buf;
@@ -66,20 +67,6 @@ static bool StripConPTYInitSequences(void *data, size_t &len) {
continue;
}
- // \x1b[m - SGR reset emitted after cursor-position init
- if (remaining >= 3 && memcmp(in, "\x1b[m", 3) == 0) {
- in += 3;
- stripped = true;
- continue;
- }
-
- // \x1b[?25h - show cursor emitted after cursor-position init
- if (remaining >= 6 && memcmp(in, "\x1b[?25h", 6) == 0) {
- in += 6;
- stripped = true;
- continue;
- }
-
// \x1b]0;...\x07 - ConPTY window-title OSC sequence
if (remaining >= 4 && in[1] == ']' && in[2] == '0' && in[3] == ';') {
const char *bel =
@@ -126,13 +113,44 @@ size_t ConnectionConPTY::Read(void *dst, size_t dst_len,
m_pty->GetCV().wait(guard, [this] { return !m_pty->IsStopping(); });
}
+ char *out = static_cast<char *>(dst);
size_t bytes_read =
- ConnectionGenericFile::Read(dst, dst_len, timeout, status, error_ptr);
-
- if (bytes_read > 0 && !m_pty_vt_sequence_was_stripped) {
- if (StripConPTYInitSequences(dst, bytes_read))
- m_pty_vt_sequence_was_stripped = true;
+ ConnectionGenericFile::Read(out, dst_len, timeout, status, error_ptr);
+
+ if (bytes_read > 0 && !m_conpty_sequences_stripped)
+ if (StripConPTYSequences(out, bytes_read))
+ m_conpty_sequences_stripped = true;
+
+ // ConPTY translates LF -> CRLF via two separate pipe writes.
+ if (bytes_read > 0 && bytes_read < dst_len && out[bytes_read - 1] == '\r' &&
+ status == eConnectionStatusSuccess) {
+ OVERLAPPED lf_ov = {};
+ lf_ov.hEvent = ::CreateEvent(nullptr, /*bManualReset=*/TRUE,
+ /*bInitialState=*/FALSE, nullptr);
+ if (lf_ov.hEvent) {
+ BOOL ok = ::ReadFile(m_file, out + bytes_read, 1, nullptr, &lf_ov);
+ DWORD err = ok ? ERROR_SUCCESS : ::GetLastError();
+ if (err == ERROR_IO_PENDING) {
+ if (::WaitForSingleObject(lf_ov.hEvent, 20) == WAIT_OBJECT_0)
+ err = ERROR_SUCCESS;
+ else {
+ ::CancelIoEx(m_file, &lf_ov);
+ err = ERROR_OPERATION_ABORTED;
+ }
+ }
+ if (err == ERROR_SUCCESS) {
+ DWORD lf_read = 0;
+ if (::GetOverlappedResult(m_file, &lf_ov, &lf_read, FALSE))
+ bytes_read += lf_read;
+ } else if (err == ERROR_OPERATION_ABORTED) {
+ // Wait for the cancel to complete so lf_ov is safe to destroy.
+ DWORD dummy = 0;
+ ::GetOverlappedResult(m_file, &lf_ov, &dummy, TRUE);
+ }
+ ::CloseHandle(lf_ov.hEvent);
+ }
}
+
return bytes_read;
}
diff --git a/lldb/source/Host/windows/PseudoConsole.cpp b/lldb/source/Host/windows/PseudoConsole.cpp
index 1c5ce43355cd0..aef6da4fbf97c 100644
--- a/lldb/source/Host/windows/PseudoConsole.cpp
+++ b/lldb/source/Host/windows/PseudoConsole.cpp
@@ -88,8 +88,6 @@ llvm::Error PseudoConsole::CreateOverlappedPipePair(HANDLE &out_read,
std::error_code(GetLastError(), std::system_category()));
}
- DWORD mode = PIPE_NOWAIT;
- SetNamedPipeHandleState(out_read, &mode, NULL, NULL);
return llvm::Error::success();
}
@@ -144,8 +142,9 @@ llvm::Error PseudoConsole::OpenPseudoConsole() {
cursorCol = csbi.dwCursorPosition.X + 1;
}
HPCON hPC = INVALID_HANDLE_VALUE;
- HRESULT hr = kernel32.CreatePseudoConsole(
- consoleSize, hInputRead, hOutputWrite, PSEUDOCONSOLE_INHERIT_CURSOR, &hPC);
+ HRESULT hr =
+ kernel32.CreatePseudoConsole(consoleSize, hInputRead, hOutputWrite,
+ PSEUDOCONSOLE_INHERIT_CURSOR, &hPC);
CloseHandle(hInputRead);
CloseHandle(hOutputWrite);
@@ -168,8 +167,8 @@ llvm::Error PseudoConsole::OpenPseudoConsole() {
// and initialize without clearing the screen or overwriting LLDB's prompt.
{
char response[32];
- int len = snprintf(response, sizeof(response), "\x1b[%d;%dR",
- cursorRow, cursorCol);
+ int len = snprintf(response, sizeof(response), "\x1b[%d;%dR", cursorRow,
+ cursorCol);
DWORD nwritten = 0;
WriteFile(m_conpty_input, response, len, &nwritten, NULL);
}
diff --git a/lldb/test/API/windows/conpty/TestConPTY.py b/lldb/test/API/windows/conpty/TestConPTY.py
index f48792deab199..a3e7b5081c704 100644
--- a/lldb/test/API/windows/conpty/TestConPTY.py
+++ b/lldb/test/API/windows/conpty/TestConPTY.py
@@ -108,4 +108,4 @@ def test_no_screen_clear_on_init(self):
# Emitted by ConPTY during a full-screen init (no cursor inheritance).
self.assertNotIn("\x1b[2J", output) # clear screen
self.assertNotIn("\x1b[3J", output) # erase scrollback
- self.assertNotIn("\x1b[H", output) # cursor home
+ self.assertNotIn("\x1b[H", output) # cursor home
>From 2ec5c73a149da3df55f5cc5bdb9106c8c5d5ca61 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Mon, 27 Apr 2026 19:08:53 +0100
Subject: [PATCH 4/5] remove CLRF normalization
---
.../Host/windows/ConnectionConPTYWindows.cpp | 68 +++++++------------
1 file changed, 24 insertions(+), 44 deletions(-)
diff --git a/lldb/source/Host/windows/ConnectionConPTYWindows.cpp b/lldb/source/Host/windows/ConnectionConPTYWindows.cpp
index e28c2f5b14dde..46e1f68744ba3 100644
--- a/lldb/source/Host/windows/ConnectionConPTYWindows.cpp
+++ b/lldb/source/Host/windows/ConnectionConPTYWindows.cpp
@@ -27,14 +27,13 @@ using namespace lldb_private;
/// \param[in,out] data Buffer containing raw ConPTY output.
/// \param[in,out] len On entry, the number of valid bytes in \p data.
/// Updated to the number of bytes after stripping.
-/// \return true if at least one sequence was stripped (caller should stop
-/// calling this function on future reads).
-static bool StripConPTYSequences(void *data, size_t &len) {
+/// \param[in] strip_init If true, also strip init-only sequences (\x1b[m,
+/// \x1b[?25h) that ConPTY emits at startup.
+static void StripConPTYSequences(void *data, size_t &len, bool strip_init) {
auto *buf = static_cast<char *>(data);
char *out = buf;
const char *in = buf;
const char *end = buf + len;
- bool stripped = false;
while (in < end) {
if (*in != '\x1b') {
@@ -45,17 +44,30 @@ static bool StripConPTYSequences(void *data, size_t &len) {
size_t remaining = end - in;
// \x1b[6n - cursor-position query (PSEUDOCONSOLE_INHERIT_CURSOR init)
+ // This query is always replied to in OpenPseudoConsole.
if (remaining >= 4 && memcmp(in, "\x1b[6n", 4) == 0) {
in += 4;
- stripped = true;
continue;
}
+ if (strip_init) {
+ // \x1b[m - SGR reset (ConPTY init)
+ if (remaining >= 3 && memcmp(in, "\x1b[m", 3) == 0) {
+ in += 3;
+ continue;
+ }
+
+ // \x1b[?25h - show cursor (ConPTY init)
+ if (remaining >= 6 && memcmp(in, "\x1b[?25h", 6) == 0) {
+ in += 6;
+ continue;
+ }
+ }
+
// \x1b[?9001h / \x1b[?9001l - Win32 Input Mode enable/disable
if (remaining >= 8 && memcmp(in, "\x1b[?9001", 7) == 0 &&
(in[7] == 'h' || in[7] == 'l')) {
in += 8;
- stripped = true;
continue;
}
@@ -63,7 +75,6 @@ static bool StripConPTYSequences(void *data, size_t &len) {
if (remaining >= 8 && memcmp(in, "\x1b[?1004", 7) == 0 &&
(in[7] == 'h' || in[7] == 'l')) {
in += 8;
- stripped = true;
continue;
}
@@ -71,12 +82,11 @@ static bool StripConPTYSequences(void *data, size_t &len) {
if (remaining >= 4 && in[1] == ']' && in[2] == '0' && in[3] == ';') {
const char *bel =
static_cast<const char *>(memchr(in + 4, '\x07', end - in - 4));
- if (bel) {
+ // We assume a sequence is not split accross multiple chunks.
+ if (bel)
in = bel + 1;
- } else {
+ else
in = end;
- }
- stripped = true;
continue;
}
@@ -84,7 +94,6 @@ static bool StripConPTYSequences(void *data, size_t &len) {
}
len = static_cast<size_t>(out - buf);
- return stripped;
}
ConnectionConPTY::ConnectionConPTY(std::shared_ptr<PseudoConsole> pty)
@@ -117,38 +126,9 @@ size_t ConnectionConPTY::Read(void *dst, size_t dst_len,
size_t bytes_read =
ConnectionGenericFile::Read(out, dst_len, timeout, status, error_ptr);
- if (bytes_read > 0 && !m_conpty_sequences_stripped)
- if (StripConPTYSequences(out, bytes_read))
- m_conpty_sequences_stripped = true;
-
- // ConPTY translates LF -> CRLF via two separate pipe writes.
- if (bytes_read > 0 && bytes_read < dst_len && out[bytes_read - 1] == '\r' &&
- status == eConnectionStatusSuccess) {
- OVERLAPPED lf_ov = {};
- lf_ov.hEvent = ::CreateEvent(nullptr, /*bManualReset=*/TRUE,
- /*bInitialState=*/FALSE, nullptr);
- if (lf_ov.hEvent) {
- BOOL ok = ::ReadFile(m_file, out + bytes_read, 1, nullptr, &lf_ov);
- DWORD err = ok ? ERROR_SUCCESS : ::GetLastError();
- if (err == ERROR_IO_PENDING) {
- if (::WaitForSingleObject(lf_ov.hEvent, 20) == WAIT_OBJECT_0)
- err = ERROR_SUCCESS;
- else {
- ::CancelIoEx(m_file, &lf_ov);
- err = ERROR_OPERATION_ABORTED;
- }
- }
- if (err == ERROR_SUCCESS) {
- DWORD lf_read = 0;
- if (::GetOverlappedResult(m_file, &lf_ov, &lf_read, FALSE))
- bytes_read += lf_read;
- } else if (err == ERROR_OPERATION_ABORTED) {
- // Wait for the cancel to complete so lf_ov is safe to destroy.
- DWORD dummy = 0;
- ::GetOverlappedResult(m_file, &lf_ov, &dummy, TRUE);
- }
- ::CloseHandle(lf_ov.hEvent);
- }
+ if (bytes_read > 0) {
+ StripConPTYSequences(out, bytes_read, !m_conpty_sequences_stripped);
+ m_conpty_sequences_stripped = true;
}
return bytes_read;
>From b5a265800e1e4e86fddaa5166bde1ae785964d94 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Tue, 28 Apr 2026 11:26:46 +0100
Subject: [PATCH 5/5] address comments
---
lldb/source/Host/windows/PseudoConsole.cpp | 8 ++++----
lldb/test/API/windows/conpty/TestConPTY.py | 16 ++++++++++++++--
2 files changed, 18 insertions(+), 6 deletions(-)
diff --git a/lldb/source/Host/windows/PseudoConsole.cpp b/lldb/source/Host/windows/PseudoConsole.cpp
index aef6da4fbf97c..89065ed4085ef 100644
--- a/lldb/source/Host/windows/PseudoConsole.cpp
+++ b/lldb/source/Host/windows/PseudoConsole.cpp
@@ -166,11 +166,11 @@ llvm::Error PseudoConsole::OpenPseudoConsole() {
// Write the cursor position response to the input pipe so ConPTY can read it
// and initialize without clearing the screen or overwriting LLDB's prompt.
{
- char response[32];
- int len = snprintf(response, sizeof(response), "\x1b[%d;%dR", cursorRow,
- cursorCol);
+ llvm::SmallString<32> response =
+ llvm::formatv("\x1b[{0};{1}R", cursorRow, cursorCol).sstr<32>();
DWORD nwritten = 0;
- WriteFile(m_conpty_input, response, len, &nwritten, NULL);
+ WriteFile(m_conpty_input, response.data(), response.size(), &nwritten,
+ NULL);
}
return llvm::Error::success();
diff --git a/lldb/test/API/windows/conpty/TestConPTY.py b/lldb/test/API/windows/conpty/TestConPTY.py
index a3e7b5081c704..46a194c71d50b 100644
--- a/lldb/test/API/windows/conpty/TestConPTY.py
+++ b/lldb/test/API/windows/conpty/TestConPTY.py
@@ -32,6 +32,18 @@ def tearDown(self):
os.environ["LLDB_LAUNCH_FLAG_USE_PIPES"] = self._saved_pipes_flag
TestBase.tearDown(self)
+ @staticmethod
+ def _strip_output(text: str) -> str:
+ """
+ Strip VT sequences that ConPTY injects around the inferior's output
+ (CSI sequences like SGR resets, mode switches, cursor queries; and
+ OSC sequences like window-title sets) so the assertion only checks
+ the inferior's actual stdout content.
+ """
+ import re
+
+ return re.sub(r"\x1b(?:\[[0-9;?]*[A-Za-z]|\][^\x07]*\x07)", "", text)
+
def _run_to_exit(self, mode):
"""Build, launch with *mode* as argv[1], run to exit, return stdout."""
self.build()
@@ -56,7 +68,7 @@ def test_stdout_delivery(self):
import re
output = self._run_to_exit("basic")
- output = re.sub(r"\x1b(?:\[[0-9;?]*[A-Za-z]|\][^\x07]*\x07)", "", output)
+ output = self._strip_output(output)
self.assertIn("Hello from ConPTY\r\n", output)
@skipUnlessWindows
@@ -66,7 +78,7 @@ def test_large_output(self):
import re
output = self._run_to_exit("large")
- output = re.sub(r"\x1b(?:\[[0-9;?]*[A-Za-z]|\][^\x07]*\x07)", "", output)
+ output = self._strip_output(output)
output_lines = output.split("\r\n")[:-1]
self.assertEqual(
More information about the lldb-commits
mailing list