[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