[Lldb-commits] [lldb] 8f6d2cf - [lldb][windows] inherit cursor's position when creating a ConPTY (#193818)
via lldb-commits
lldb-commits at lists.llvm.org
Tue Apr 28 03:42:34 PDT 2026
Author: Charles Zablit
Date: 2026-04-28T11:42:28+01:00
New Revision: 8f6d2cffd5a05e75c007f72026ccaf5972162972
URL: https://github.com/llvm/llvm-project/commit/8f6d2cffd5a05e75c007f72026ccaf5972162972
DIFF: https://github.com/llvm/llvm-project/commit/8f6d2cffd5a05e75c007f72026ccaf5972162972.diff
LOG: [lldb][windows] inherit cursor's position when creating a ConPTY (#193818)
Added:
Modified:
lldb/include/lldb/Host/windows/ConnectionConPTYWindows.h
lldb/include/lldb/Host/windows/PseudoConsole.h
lldb/source/Host/windows/ConnectionConPTYWindows.cpp
lldb/source/Host/windows/PseudoConsole.cpp
lldb/test/API/windows/conpty/TestConPTY.py
Removed:
################################################################################
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 7cef792fd9c84..0e927e3a433ec 100644
--- a/lldb/include/lldb/Host/windows/PseudoConsole.h
+++ b/lldb/include/lldb/Host/windows/PseudoConsole.h
@@ -34,16 +34,15 @@ 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,
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 +107,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..46e1f68744ba3 100644
--- a/lldb/source/Host/windows/ConnectionConPTYWindows.cpp
+++ b/lldb/source/Host/windows/ConnectionConPTYWindows.cpp
@@ -8,39 +8,92 @@
#include "lldb/Host/windows/ConnectionConPTYWindows.h"
#include "lldb/Utility/Status.h"
+#include "lldb/Utility/Timeout.h"
+
+#include <cstring>
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.
+/// Remove ConPTY management sequences from a buffer in-place.
///
-/// 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.
+/// 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 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;
+/// \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.
+/// \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;
+
+ while (in < end) {
+ if (*in != '\x1b') {
+ *out++ = *in++;
+ continue;
+ }
+
+ 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;
+ 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;
+ 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;
+ 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));
+ // We assume a sequence is not split accross multiple chunks.
+ if (bel)
+ in = bel + 1;
+ else
+ in = end;
+ continue;
+ }
+
+ *out++ = *in++;
}
- return false;
+
+ len = static_cast<size_t>(out - buf);
}
ConnectionConPTY::ConnectionConPTY(std::shared_ptr<PseudoConsole> pty)
@@ -69,12 +122,13 @@ 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);
+ ConnectionGenericFile::Read(out, 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;
+ if (bytes_read > 0) {
+ StripConPTYSequences(out, bytes_read, !m_conpty_sequences_stripped);
+ m_conpty_sequences_stripped = true;
}
return bytes_read;
diff --git a/lldb/source/Host/windows/PseudoConsole.cpp b/lldb/source/Host/windows/PseudoConsole.cpp
index ef6f17771fc34..89065ed4085ef 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");
@@ -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();
}
@@ -130,14 +128,23 @@ 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 +161,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.
+ {
+ llvm::SmallString<32> response =
+ llvm::formatv("\x1b[{0};{1}R", cursorRow, cursorCol).sstr<32>();
+ DWORD nwritten = 0;
+ WriteFile(m_conpty_input, response.data(), response.size(), &nwritten,
+ NULL);
}
return llvm::Error::success();
@@ -230,70 +243,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..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()
@@ -53,27 +65,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 = self._strip_output(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 = self._strip_output(output)
output_lines = output.split("\r\n")[:-1]
self.assertEqual(
@@ -88,10 +93,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
diff erent 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
diff erent sequences.
"""
import re
@@ -99,3 +103,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
More information about the lldb-commits
mailing list