[Lldb-commits] [lldb] 3734a92 - [lldb][Windows] Forward debuggee STDOUT through lldb-server via ConPTY (#201124)

via lldb-commits lldb-commits at lists.llvm.org
Fri Jun 5 07:30:10 PDT 2026


Author: Charles Zablit
Date: 2026-06-05T15:30:04+01:00
New Revision: 3734a928541890a8f2b5d91587ff0bd6ebc90b46

URL: https://github.com/llvm/llvm-project/commit/3734a928541890a8f2b5d91587ff0bd6ebc90b46
DIFF: https://github.com/llvm/llvm-project/commit/3734a928541890a8f2b5d91587ff0bd6ebc90b46.diff

LOG: [lldb][Windows] Forward debuggee STDOUT through lldb-server via ConPTY (#201124)

`lldb-server.exe` currently does not forward the debuggee's STDIO to the
client.

This patch wires STDOUT using a ConPTY, mirroring the existing
`ProcessWindows` path. The two implementations share the same
infrastructure (PseudoConsole, ConnectionConPTY, ThreadedCommunication):

- The ConPTY is setup in
`GDBRemoteCommunicationServerLLGS::LaunchProcess`.
- `NativeProcessWindows` owns the ConPTY STDOUT read thread. The
read-thread callback forwards each chunk into a new
`NativeProcessProtocol::NewProcessOutput` delegate hook.
- `GDBRemoteCommunicationServerLLGS::NewProcessOutput` copies the data
and posts `SendONotification` to the main loop.
- The ConPTY is closed in `OnExitProcess`.

This patch uses a default terminal size to create the ConPTY, which
causes issues. This will be fixed in
https://github.com/llvm/llvm-project/pull/201141.

The STDIN path will be done in a follow up PR.

rdar://178725650

Added: 
    

Modified: 
    lldb/include/lldb/Host/common/NativeProcessProtocol.h
    lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
    lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
    lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
    lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.h

Removed: 
    


################################################################################
diff  --git a/lldb/include/lldb/Host/common/NativeProcessProtocol.h b/lldb/include/lldb/Host/common/NativeProcessProtocol.h
index f3c0f16502fab..4e4804623b1d8 100644
--- a/lldb/include/lldb/Host/common/NativeProcessProtocol.h
+++ b/lldb/include/lldb/Host/common/NativeProcessProtocol.h
@@ -266,6 +266,11 @@ class NativeProcessProtocol {
     virtual void
     NewSubprocess(NativeProcessProtocol *parent_process,
                   std::unique_ptr<NativeProcessProtocol> child_process) = 0;
+
+    /// Called by the platform when the inferior writes to stdout/stderr
+    /// through a redirected pseudoconsole that the platform owns.
+    virtual void NewProcessOutput(NativeProcessProtocol *process,
+                                  llvm::StringRef data) {}
   };
 
   virtual Status GetLoadedModuleFileSpec(const char *module_path,

diff  --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
index 60cd4106d9db4..b235ab281bad6 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
@@ -17,11 +17,14 @@
 #include "lldb/Host/ProcessLaunchInfo.h"
 #include "lldb/Host/PseudoTerminal.h"
 #include "lldb/Host/windows/AutoHandle.h"
+#include "lldb/Host/windows/ConnectionConPTYWindows.h"
 #include "lldb/Host/windows/HostThreadWindows.h"
 #include "lldb/Host/windows/ProcessLauncherWindows.h"
+#include "lldb/Host/windows/PseudoConsole.h"
 #include "lldb/Target/MemoryRegionInfo.h"
 #include "lldb/Target/Process.h"
 #include "lldb/Utility/State.h"
+#include "llvm/ADT/StringRef.h"
 #include "llvm/Support/ConvertUTF.h"
 #include "llvm/Support/Errc.h"
 #include "llvm/Support/Error.h"
@@ -50,9 +53,10 @@ NativeProcessWindows::NativeProcessWindows(ProcessLaunchInfo &launch_info,
                                            llvm::Error &E)
     : NativeProcessProtocol(
           LLDB_INVALID_PROCESS_ID,
-          PseudoTerminal::invalid_fd, // TODO: Implement on Windows
+          PseudoTerminal::invalid_fd, // NativeProcessWindows owns the ConPTY.
           delegate),
-      ProcessDebugger(), m_arch(launch_info.GetArchitecture()) {
+      ProcessDebugger(), m_arch(launch_info.GetArchitecture()),
+      m_stdio_communication("lldb.NativeProcessWindows.stdio") {
   ErrorAsOutParameter EOut(&E);
   DebugDelegateSP delegate_sp(new NativeDebugDelegate(*this));
   E = LaunchProcess(launch_info, delegate_sp).ToError();
@@ -60,12 +64,16 @@ NativeProcessWindows::NativeProcessWindows(ProcessLaunchInfo &launch_info,
     return;
 
   SetID(GetDebuggedProcessId());
+
+  m_pty = launch_info.TakePTY();
+  StartStdioForwarding();
 }
 
 NativeProcessWindows::NativeProcessWindows(lldb::pid_t pid, int terminal_fd,
                                            NativeDelegate &delegate,
                                            llvm::Error &E)
-    : NativeProcessProtocol(pid, terminal_fd, delegate), ProcessDebugger() {
+    : NativeProcessProtocol(pid, terminal_fd, delegate), ProcessDebugger(),
+      m_stdio_communication("lldb.NativeProcessWindows.stdio") {
   ErrorAsOutParameter EOut(&E);
   DebugDelegateSP delegate_sp(new NativeDebugDelegate(*this));
   ProcessAttachInfo attach_info;
@@ -431,6 +439,10 @@ void NativeProcessWindows::OnExitProcess(uint32_t exit_code) {
   Log *log = GetLog(WindowsLog::Process);
   LLDB_LOG(log, "Process {0} exited with code {1}", GetID(), exit_code);
 
+  // Closing the ConPTY signals EOF on the parent-side STDOUT pipe so the
+  // read thread can exit. Tear it down before the debuggee is destroyed.
+  StopStdioForwarding();
+
   ProcessDebugger::OnExitProcess(exit_code);
 
   // No signal involved.  It is just an exit event.
@@ -698,4 +710,43 @@ NativeProcessWindows::Manager::Attach(
     return std::move(E);
   return std::move(process_up);
 }
+
+NativeProcessWindows::~NativeProcessWindows() { StopStdioForwarding(); }
+
+void NativeProcessWindows::StartStdioForwarding() {
+  if (!m_pty || !m_pty->IsConnected())
+    return;
+
+  m_stdio_communication.SetConnection(
+      std::make_unique<ConnectionConPTY>(m_pty));
+  if (!m_stdio_communication.IsConnected())
+    return;
+  m_stdio_communication.SetReadThreadBytesReceivedCallback(
+      &NativeProcessWindows::STDIOReadThreadBytesReceived, this);
+  m_stdio_communication.StartReadThread();
+}
+
+void NativeProcessWindows::StopStdioForwarding() {
+  if (!m_stdio_communication.HasConnection())
+    return;
+
+  if (m_pty)
+    m_pty->Close();
+
+  if (m_stdio_communication.ReadThreadIsRunning())
+    m_stdio_communication.JoinReadThread();
+
+  if (m_stdio_communication.HasConnection())
+    m_stdio_communication.Disconnect();
+}
+
+void NativeProcessWindows::STDIOReadThreadBytesReceived(void *baton,
+                                                        const void *src,
+                                                        size_t src_len) {
+  auto *self = static_cast<NativeProcessWindows *>(baton);
+  if (src_len == 0)
+    return;
+  self->m_delegate.NewProcessOutput(
+      self, llvm::StringRef(static_cast<const char *>(src), src_len));
+}
 } // namespace lldb_private

diff  --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
index a15b51c7c66ab..95b85754ebdeb 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
@@ -9,6 +9,7 @@
 #ifndef liblldb_NativeProcessWindows_h_
 #define liblldb_NativeProcessWindows_h_
 
+#include "lldb/Core/ThreadedCommunication.h"
 #include "lldb/Host/common/NativeProcessProtocol.h"
 #include "lldb/lldb-forward.h"
 
@@ -21,6 +22,7 @@ class HostProcess;
 class NativeProcessWindows;
 class NativeThreadWindows;
 class NativeDebugDelegate;
+class PseudoConsole;
 
 using NativeDebugDelegateSP = std::shared_ptr<NativeDebugDelegate>;
 
@@ -47,6 +49,8 @@ class NativeProcessWindows : public NativeProcessProtocol,
     }
   };
 
+  ~NativeProcessWindows() override;
+
   Status Resume(const ResumeActionList &resume_actions) override;
 
   Status Halt() override;
@@ -155,6 +159,23 @@ class NativeProcessWindows : public NativeProcessProtocol,
   /// Whether we've seen the loader breakpoint that fires once per process at
   /// launch / attach.
   bool m_initial_stop_seen = false;
+
+  /// PseudoConsole for the lldb-server stdio-forwarding path.
+  std::shared_ptr<PseudoConsole> m_pty;
+
+  /// Wraps a ConnectionConPTY around the PTY's parent-side STDOUT HANDLE.
+  ThreadedCommunication m_stdio_communication;
+
+  /// Bridge between m_stdio_communication's read thread and
+  /// NativeDelegate::NewProcessOutput.
+  static void STDIOReadThreadBytesReceived(void *baton, const void *src,
+                                           size_t src_len);
+
+  /// Wire up m_stdio_communication on m_pty's STDOUT HANDLE.
+  void StartStdioForwarding();
+
+  /// Tear down the read thread and disconnect m_stdio_communication.
+  void StopStdioForwarding();
 };
 
 //------------------------------------------------------------------

diff  --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
index ec80a3e960949..d56026e011564 100644
--- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
@@ -291,13 +291,8 @@ Status GDBRemoteCommunicationServerLLGS::LaunchProcess() {
   m_process_launch_info.GetFlags().Set(eLaunchFlagDebug);
 
   if (should_forward_stdio) {
-    // Temporarily relax the following for Windows until we can take advantage
-    // of the recently added pty support. This doesn't really affect the use of
-    // lldb-server on Windows.
-#if !defined(_WIN32)
     if (llvm::Error Err = m_process_launch_info.SetUpPtyRedirection())
       return Status::FromError(std::move(Err));
-#endif
   }
 
   {
@@ -1224,6 +1219,33 @@ void GDBRemoteCommunicationServerLLGS::NewSubprocess(
       DebuggedProcess{std::move(child_process), DebuggedProcess::Flag{}});
 }
 
+void GDBRemoteCommunicationServerLLGS::NewProcessOutput(NativeProcessProtocol *,
+                                                        llvm::StringRef data) {
+  if (data.empty())
+    return;
+
+  {
+    std::lock_guard<std::mutex> lock(m_pending_output_mutex);
+    m_pending_output_buffer.append(data.begin(), data.end());
+  }
+  m_mainloop.AddPendingCallback(
+      [this](MainLoopBase &) { FlushPendingProcessOutput(); });
+}
+
+void GDBRemoteCommunicationServerLLGS::FlushPendingProcessOutput() {
+  if (!m_current_process || !StateIsRunningState(m_current_process->GetState()))
+    return;
+
+  std::string out;
+  {
+    std::lock_guard<std::mutex> lock(m_pending_output_mutex);
+    if (m_pending_output_buffer.empty())
+      return;
+    out.swap(m_pending_output_buffer);
+  }
+  SendONotification(out.data(), out.size());
+}
+
 void GDBRemoteCommunicationServerLLGS::DataAvailableCallback() {
   Log *log = GetLog(GDBRLog::Comm);
 
@@ -2031,6 +2053,16 @@ GDBRemoteCommunicationServerLLGS::SendStopReasonForState(
     bool force_synchronous) {
   Log *log = GetLog(LLDBLog::Process);
 
+  {
+    std::string out;
+    {
+      std::lock_guard<std::mutex> lock(m_pending_output_mutex);
+      out.swap(m_pending_output_buffer);
+    }
+    if (!out.empty())
+      SendONotification(out.data(), out.size());
+  }
+
   if (m_disabling_non_stop) {
     // Check if we are waiting for any more processes to stop.  If we are,
     // do not send the OK response yet.

diff  --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.h b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.h
index d582d2d77b639..e5b4c9ec0bed0 100644
--- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.h
+++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.h
@@ -88,6 +88,15 @@ class GDBRemoteCommunicationServerLLGS
   NewSubprocess(NativeProcessProtocol *parent_process,
                 std::unique_ptr<NativeProcessProtocol> child_process) override;
 
+  /// Forward a chunk of inferior stdout/stderr produced by the platform's
+  /// own reader. This is used on Windows.
+  void NewProcessOutput(NativeProcessProtocol *process,
+                        llvm::StringRef data) override;
+
+  /// Drain m_pending_output_buffer and emit a `$O` packet if the debuggee
+  /// is currently in a running state. No-op otherwise.
+  void FlushPendingProcessOutput();
+
   Status InitializeConnection(std::unique_ptr<Connection> connection);
 
   GDBRemoteCommunication::PacketResult
@@ -120,6 +129,9 @@ class GDBRemoteCommunicationServerLLGS
   Communication m_stdio_communication;
   MainLoop::ReadHandleUP m_stdio_handle_up;
 
+  std::string m_pending_output_buffer;
+  std::mutex m_pending_output_mutex;
+
   llvm::StringMap<std::unique_ptr<llvm::MemoryBuffer>> m_xfer_buffer_map;
   std::mutex m_saved_registers_mutex;
   std::unordered_map<uint32_t, lldb::DataBufferSP> m_saved_registers_map;


        


More information about the lldb-commits mailing list