[Lldb-commits] [lldb] [lldb-dap][windows] don't use the ConPTY in internalConsole mode (PR #186472)

via lldb-commits lldb-commits at lists.llvm.org
Fri Mar 13 10:53:22 PDT 2026


llvmbot wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-lldb

Author: Charles Zablit (charles-zablit)

<details>
<summary>Changes</summary>

In `internalConsole` mode (especially in VSCode), lldb-dap should not use the ConPTY to read the process' output. This is because the internalConsole is not a real terminal, there is no reason to use terminal emulation, which will add arbitrary line returns to the output.

Instead, this patch introduces the `eLaunchFlagUsePipes` flag in ProcessLaunchInfo which tells ProcessLaunchWindows to use regular pipes instead of a ConPTY to get the stdin and stdout of the debuggee.

The result is that output which is supposed to be on a single line is properly rendered:

# Before
<img width="2214" height="672" alt="Screenshot 2026-03-13 at 17 07 35" src="https://github.com/user-attachments/assets/26292d11-2288-46ee-a6d2-0b66bfa41288" />

# After
<img width="2215" height="689" alt="Screenshot 2026-03-13 at 17 12 39" src="https://github.com/user-attachments/assets/c9cad9af-b1ce-4c7b-91d5-f684e48e64ca" />


---
Full diff: https://github.com/llvm/llvm-project/pull/186472.diff


8 Files Affected:

- (modified) lldb/include/lldb/Host/ProcessLaunchInfo.h (+9-1) 
- (modified) lldb/include/lldb/Host/windows/PseudoConsole.h (+26-1) 
- (modified) lldb/include/lldb/lldb-enumerations.h (+4) 
- (modified) lldb/source/Host/common/ProcessLaunchInfo.cpp (+8) 
- (modified) lldb/source/Host/windows/ProcessLauncherWindows.cpp (+32-6) 
- (modified) lldb/source/Host/windows/PseudoConsole.cpp (+62) 
- (modified) lldb/source/Target/Target.cpp (+13-2) 
- (modified) lldb/tools/lldb-dap/Handler/RequestHandler.cpp (+4) 


``````````diff
diff --git a/lldb/include/lldb/Host/ProcessLaunchInfo.h b/lldb/include/lldb/Host/ProcessLaunchInfo.h
index 7801662b244ad..365d1952d173f 100644
--- a/lldb/include/lldb/Host/ProcessLaunchInfo.h
+++ b/lldb/include/lldb/Host/ProcessLaunchInfo.h
@@ -64,6 +64,14 @@ class ProcessLaunchInfo : public ProcessInfo {
   // but stderr doesn't, then only stderr will be redirected to a pty.)
   llvm::Error SetUpPtyRedirection();
 
+#ifdef _WIN32
+  // Redirect stdin/stdout/stderr to anonymous pipes instead of a ConPTY.
+  // Used when terminal emulation is not needed (e.g. lldb-dap internalConsole).
+  llvm::Error SetUpPipeRedirection();
+#endif
+
+  bool HasPTY() const { return m_pty != nullptr; }
+
   size_t GetNumFileActions() const { return m_file_actions.size(); }
 
   const FileAction *GetFileActionAtIndex(size_t idx) const;
@@ -138,7 +146,7 @@ class ProcessLaunchInfo : public ProcessInfo {
 #ifdef _WIN32
     if (!m_pty)
       return false;
-    return GetPTY().GetPseudoTerminalHandle() != ((HANDLE)(long long)-1) &&
+    return GetPTY().GetMode() != PseudoConsole::Mode::None &&
            GetNumFileActions() == 0;
 #else
     return true;
diff --git a/lldb/include/lldb/Host/windows/PseudoConsole.h b/lldb/include/lldb/Host/windows/PseudoConsole.h
index a1967fc85188c..7d7e00cbb1d7a 100644
--- a/lldb/include/lldb/Host/windows/PseudoConsole.h
+++ b/lldb/include/lldb/Host/windows/PseudoConsole.h
@@ -22,6 +22,8 @@ namespace lldb_private {
 class PseudoConsole {
 
 public:
+  enum Mode { ConPTY, Pipe, None };
+
   PseudoConsole() = default;
   ~PseudoConsole();
 
@@ -40,14 +42,25 @@ class PseudoConsole {
   ///     otherwise.
   llvm::Error OpenPseudoConsole();
 
+  /// Creates a pair of anonymous pipes to use for stdio instead of a ConPTY.
+  ///
+  /// \return
+  ///     An llvm::Error if the pipes could not be created.
+  llvm::Error OpenAnonymousPipes();
+
   /// Closes the ConPTY and invalidates its handle, without closing the STDIN
   /// and STDOUT pipes. Closing the ConPTY signals EOF to any process currently
   /// attached to it.
   void Close();
 
-  /// Closes the STDIN and STDOUT pipe handles and invalidates them
+  /// Closes the STDIN and STDOUT pipe handles and invalidates them.
   void ClosePipes();
 
+  /// Closes the child-side pipe handles (stdin read end and stdout/stderr write
+  /// end) that were passed to CreateProcessW. Must be called after a successful
+  /// CreateProcessW to avoid keeping the pipes alive indefinitely.
+  void CloseChildHandles();
+
   /// Returns whether the ConPTY and its pipes are currently open and valid.
   bool IsConnected() const;
 
@@ -78,6 +91,14 @@ class PseudoConsole {
   ///     invalid.
   HANDLE GetSTDINHandle() const { return m_conpty_input; };
 
+  /// The child-side stdin read HANDLE (pipe mode only).
+  HANDLE GetChildStdinHandle() const { return m_pipe_child_stdin; };
+
+  /// The child-side stdout/stderr write HANDLE (pipe mode only).
+  HANDLE GetChildStdoutHandle() const { return m_pipe_child_stdout; };
+
+  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
@@ -112,6 +133,10 @@ class PseudoConsole {
   HANDLE m_conpty_handle = ((HANDLE)(long long)-1);
   HANDLE m_conpty_output = ((HANDLE)(long long)-1);
   HANDLE m_conpty_input = ((HANDLE)(long long)-1);
+  // Pipe mode: child-side handles passed to CreateProcessW, closed after launch
+  HANDLE m_pipe_child_stdin = ((HANDLE)(long long)-1);
+  HANDLE m_pipe_child_stdout = ((HANDLE)(long long)-1);
+  Mode m_mode = Mode::None;
   std::mutex m_mutex{};
   std::condition_variable m_cv{};
   std::atomic<bool> m_stopping = false;
diff --git a/lldb/include/lldb/lldb-enumerations.h b/lldb/include/lldb/lldb-enumerations.h
index d2600d0a6ce44..11b7808bb1b61 100644
--- a/lldb/include/lldb/lldb-enumerations.h
+++ b/lldb/include/lldb/lldb-enumerations.h
@@ -132,6 +132,10 @@ FLAGS_ENUM(LaunchFlags){
                     ///< permissions but instead inherit them from its parent.
     eLaunchFlagMemoryTagging =
         (1u << 13), ///< Launch process with memory tagging explicitly enabled.
+    eLaunchFlagUsePipes =
+        (1u << 14), ///< Use anonymous pipes for stdio instead of a ConPTY on
+                    ///< Windows. Useful when terminal emulation is not needed
+                    ///< (e.g. lldb-dap internalConsole mode).
 };
 
 /// Thread Run Modes.
diff --git a/lldb/source/Host/common/ProcessLaunchInfo.cpp b/lldb/source/Host/common/ProcessLaunchInfo.cpp
index 2f67a417996ac..9f87e6a783adc 100644
--- a/lldb/source/Host/common/ProcessLaunchInfo.cpp
+++ b/lldb/source/Host/common/ProcessLaunchInfo.cpp
@@ -242,6 +242,14 @@ llvm::Error ProcessLaunchInfo::SetUpPtyRedirection() {
 #endif
 }
 
+#ifdef _WIN32
+llvm::Error ProcessLaunchInfo::SetUpPipeRedirection() {
+  if (!m_pty)
+    m_pty = std::make_shared<PTY>();
+  return m_pty->OpenAnonymousPipes();
+}
+#endif
+
 bool ProcessLaunchInfo::ConvertArgumentsForLaunchingInShell(
     Status &error, bool will_debug, bool first_arg_is_full_shell_command,
     uint32_t num_resumes) {
diff --git a/lldb/source/Host/windows/ProcessLauncherWindows.cpp b/lldb/source/Host/windows/ProcessLauncherWindows.cpp
index fb091eb75d9b8..11c9d6ec30925 100644
--- a/lldb/source/Host/windows/ProcessLauncherWindows.cpp
+++ b/lldb/source/Host/windows/ProcessLauncherWindows.cpp
@@ -130,7 +130,9 @@ ProcessLauncherWindows::LaunchProcess(const ProcessLaunchInfo &launch_info,
   startupinfoex.StartupInfo.cb = sizeof(STARTUPINFOEXW);
   startupinfoex.StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
 
-  bool use_pty = launch_info.ShouldUsePTY();
+  PseudoConsole::Mode pty_mode = launch_info.ShouldUsePTY()
+                                     ? launch_info.GetPTY().GetMode()
+                                     : PseudoConsole::Mode::None;
 
   HANDLE stdin_handle = GetStdioHandle(launch_info, STDIN_FILENO);
   HANDLE stdout_handle = GetStdioHandle(launch_info, STDOUT_FILENO);
@@ -152,13 +154,31 @@ ProcessLauncherWindows::LaunchProcess(const ProcessLaunchInfo &launch_info,
   ProcThreadAttributeList attributelist = std::move(*attributelist_or_err);
 
   std::vector<HANDLE> inherited_handles;
-  if (use_pty) {
+  switch (pty_mode) {
+  case PseudoConsole::Mode::ConPTY: {
     HPCON hPC = launch_info.GetPTY().GetPseudoTerminalHandle();
     if (auto err = attributelist.SetupPseudoConsole(hPC)) {
       error = Status::FromError(std::move(err));
       return HostProcess();
     }
-  } else {
+    break;
+  }
+  case PseudoConsole::Mode::Pipe: {
+    PseudoConsole &pty = launch_info.GetPTY();
+    startupinfoex.StartupInfo.hStdInput = pty.GetChildStdinHandle();
+    startupinfoex.StartupInfo.hStdOutput = pty.GetChildStdoutHandle();
+    startupinfoex.StartupInfo.hStdError = pty.GetChildStdoutHandle();
+    inherited_handles = {pty.GetChildStdinHandle(), pty.GetChildStdoutHandle()};
+    if (!UpdateProcThreadAttribute(
+            startupinfoex.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
+            inherited_handles.data(), inherited_handles.size() * sizeof(HANDLE),
+            nullptr, nullptr)) {
+      error = Status(::GetLastError(), eErrorTypeWin32);
+      return HostProcess();
+    }
+    break;
+  }
+  case PseudoConsole::Mode::None: {
     auto inherited_handles_or_err =
         GetInheritedHandles(startupinfoex, &launch_info, stdout_handle,
                             stderr_handle, stdin_handle);
@@ -167,6 +187,8 @@ ProcessLauncherWindows::LaunchProcess(const ProcessLaunchInfo &launch_info,
       return HostProcess();
     }
     inherited_handles = std::move(*inherited_handles_or_err);
+    break;
+  }
   }
 
   const char *hide_console_var =
@@ -182,7 +204,8 @@ ProcessLauncherWindows::LaunchProcess(const ProcessLaunchInfo &launch_info,
   if (launch_info.GetFlags().Test(eLaunchFlagDebug))
     flags |= DEBUG_ONLY_THIS_PROCESS;
 
-  if (launch_info.GetFlags().Test(eLaunchFlagDisableSTDIO) || use_pty)
+  if (launch_info.GetFlags().Test(eLaunchFlagDisableSTDIO) ||
+      pty_mode != PseudoConsole::Mode::None)
     flags &= ~CREATE_NEW_CONSOLE;
 
   std::vector<wchar_t> environment =
@@ -210,8 +233,9 @@ ProcessLauncherWindows::LaunchProcess(const ProcessLaunchInfo &launch_info,
 
   BOOL result = ::CreateProcessW(
       wexecutable.c_str(), pwcommandLine, NULL, NULL,
-      /*bInheritHandles=*/!inherited_handles.empty() || use_pty, flags,
-      environment.data(),
+      /*bInheritHandles=*/!inherited_handles.empty() ||
+          pty_mode != PseudoConsole::Mode::None,
+      flags, environment.data(),
       wworkingDirectory.size() == 0 ? NULL : wworkingDirectory.c_str(),
       reinterpret_cast<STARTUPINFOW *>(&startupinfoex), &pi);
 
@@ -226,6 +250,8 @@ ProcessLauncherWindows::LaunchProcess(const ProcessLaunchInfo &launch_info,
     // Do not call CloseHandle on pi.hProcess, since we want to pass that back
     // through the HostProcess.
     ::CloseHandle(pi.hThread);
+    if (pty_mode == PseudoConsole::Mode::Pipe)
+      launch_info.GetPTY().CloseChildHandles();
   }
 
   if (!result)
diff --git a/lldb/source/Host/windows/PseudoConsole.cpp b/lldb/source/Host/windows/PseudoConsole.cpp
index b8b74091fe474..fed2336a191f8 100644
--- a/lldb/source/Host/windows/PseudoConsole.cpp
+++ b/lldb/source/Host/windows/PseudoConsole.cpp
@@ -126,6 +126,7 @@ llvm::Error PseudoConsole::OpenPseudoConsole() {
   m_conpty_handle = hPC;
   m_conpty_output = hOutputRead;
   m_conpty_input = hInputWrite;
+  m_mode = Mode::ConPTY;
 
   if (auto error = DrainInitSequences()) {
     Log *log = GetLog(LLDBLog::Host);
@@ -137,6 +138,9 @@ llvm::Error PseudoConsole::OpenPseudoConsole() {
 }
 
 bool PseudoConsole::IsConnected() const {
+  if (m_mode == Mode::Pipe)
+    return m_conpty_input != INVALID_HANDLE_VALUE &&
+           m_conpty_output != INVALID_HANDLE_VALUE;
   return m_conpty_handle != INVALID_HANDLE_VALUE &&
          m_conpty_input != INVALID_HANDLE_VALUE &&
          m_conpty_output != INVALID_HANDLE_VALUE;
@@ -162,6 +166,64 @@ void PseudoConsole::ClosePipes() {
   m_conpty_output = INVALID_HANDLE_VALUE;
 }
 
+void PseudoConsole::CloseChildHandles() {
+  if (m_pipe_child_stdin != INVALID_HANDLE_VALUE)
+    CloseHandle(m_pipe_child_stdin);
+  if (m_pipe_child_stdout != INVALID_HANDLE_VALUE)
+    CloseHandle(m_pipe_child_stdout);
+
+  m_pipe_child_stdin = INVALID_HANDLE_VALUE;
+  m_pipe_child_stdout = INVALID_HANDLE_VALUE;
+}
+
+llvm::Error PseudoConsole::OpenAnonymousPipes() {
+  SECURITY_ATTRIBUTES sa = {sizeof(SECURITY_ATTRIBUTES), NULL, TRUE};
+  HANDLE hStdinRead = INVALID_HANDLE_VALUE;
+  HANDLE hStdinWrite = INVALID_HANDLE_VALUE;
+  if (!CreatePipe(&hStdinRead, &hStdinWrite, &sa, 0))
+    return llvm::errorCodeToError(
+        std::error_code(GetLastError(), std::system_category()));
+  // Parent write end must not be inherited by the child.
+  SetHandleInformation(hStdinWrite, HANDLE_FLAG_INHERIT, 0);
+
+  wchar_t pipe_name[MAX_PATH];
+  swprintf(pipe_name, MAX_PATH, L"\\\\.\\pipe\\pipes-lldb-%d-%p",
+           GetCurrentProcessId(), this);
+
+  HANDLE hStdoutRead =
+      CreateNamedPipeW(pipe_name, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED,
+                       PIPE_TYPE_BYTE | PIPE_WAIT, 1, 4096, 4096, 0, NULL);
+  if (hStdoutRead == INVALID_HANDLE_VALUE) {
+    CloseHandle(hStdinRead);
+    CloseHandle(hStdinWrite);
+    return llvm::errorCodeToError(
+        std::error_code(GetLastError(), std::system_category()));
+  }
+
+  SECURITY_ATTRIBUTES child_security_attributes = {sizeof(SECURITY_ATTRIBUTES),
+                                                   NULL, TRUE};
+  HANDLE hStdoutWrite =
+      CreateFileW(pipe_name, GENERIC_WRITE, 0, &child_security_attributes,
+                  OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
+  if (hStdoutWrite == INVALID_HANDLE_VALUE) {
+    CloseHandle(hStdinRead);
+    CloseHandle(hStdinWrite);
+    CloseHandle(hStdoutRead);
+    return llvm::errorCodeToError(
+        std::error_code(GetLastError(), std::system_category()));
+  }
+
+  DWORD mode = PIPE_NOWAIT;
+  SetNamedPipeHandleState(hStdoutRead, &mode, NULL, NULL);
+
+  m_conpty_input = hStdinWrite;
+  m_conpty_output = hStdoutRead;
+  m_pipe_child_stdin = hStdinRead;
+  m_pipe_child_stdout = hStdoutWrite;
+  m_mode = Mode::Pipe;
+  return llvm::Error::success();
+}
+
 llvm::Error PseudoConsole::DrainInitSequences() {
   STARTUPINFOEXW startupinfoex = {};
   startupinfoex.StartupInfo.cb = sizeof(STARTUPINFOEXW);
diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index 9c8124a15333b..939c848395730 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -3859,8 +3859,19 @@ void Target::FinalizeFileActions(ProcessLaunchInfo &info) {
       }
 
       if (default_to_use_pty) {
-        llvm::Error Err = info.SetUpPtyRedirection();
-        LLDB_LOG_ERROR(log, std::move(Err), "SetUpPtyRedirection failed: {0}");
+#ifdef _WIN32
+        if (info.GetFlags().Test(eLaunchFlagUsePipes)) {
+          llvm::Error Err = info.SetUpPipeRedirection();
+          LLDB_LOG_ERROR(log, std::move(Err),
+                         "SetUpPipeRedirection failed: {0}");
+        } else {
+#endif
+          llvm::Error Err = info.SetUpPtyRedirection();
+          LLDB_LOG_ERROR(log, std::move(Err),
+                         "SetUpPtyRedirection failed: {0}");
+#ifdef _WIN32
+        }
+#endif
       }
     }
   }
diff --git a/lldb/tools/lldb-dap/Handler/RequestHandler.cpp b/lldb/tools/lldb-dap/Handler/RequestHandler.cpp
index 5e8c2163c838f..f7aad21c0be41 100644
--- a/lldb/tools/lldb-dap/Handler/RequestHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/RequestHandler.cpp
@@ -223,6 +223,10 @@ llvm::Error BaseRequestHandler::LaunchProcess(
       SetLaunchFlag(flags, arguments.disableASLR, lldb::eLaunchFlagDisableASLR);
   flags = SetLaunchFlag(flags, arguments.disableSTDIO,
                         lldb::eLaunchFlagDisableSTDIO);
+#ifdef _WIN32
+  flags = SetLaunchFlag(flags, arguments.console == protocol::eConsoleInternal,
+                        lldb::eLaunchFlagUsePipes);
+#endif
   launch_info.SetLaunchFlags(flags | lldb::eLaunchFlagDebug |
                              lldb::eLaunchFlagStopAtEntry);
 

``````````

</details>


https://github.com/llvm/llvm-project/pull/186472


More information about the lldb-commits mailing list