[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