https://github.com/charles-zablit created 
https://github.com/llvm/llvm-project/pull/201124

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

This patch wires stdio end-to-end 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`.

The STDIN path is different. It now uses `ConnectionConPTY::Write`.

>From d375ad41a77eb8eb91b62c70afb56e2c148115a4 Mon Sep 17 00:00:00 2001
From: Charles Zablit <[email protected]>
Date: Tue, 2 Jun 2026 14:40:32 +0100
Subject: [PATCH] [lldb][Windows] Forward inferior stdio through lldb-server
 via ConPTY

---
 .../lldb/Host/common/NativeProcessProtocol.h  | 13 ++++
 .../Host/windows/ConnectionConPTYWindows.cpp  | 27 ++++++-
 .../Windows/Common/NativeProcessWindows.cpp   | 71 ++++++++++++++++++-
 .../Windows/Common/NativeProcessWindows.h     | 25 +++++++
 .../GDBRemoteCommunicationServerLLGS.cpp      | 25 +++++--
 .../GDBRemoteCommunicationServerLLGS.h        |  5 ++
 6 files changed, 157 insertions(+), 9 deletions(-)

diff --git a/lldb/include/lldb/Host/common/NativeProcessProtocol.h 
b/lldb/include/lldb/Host/common/NativeProcessProtocol.h
index f3c0f16502fab..a27bf41661b1d 100644
--- a/lldb/include/lldb/Host/common/NativeProcessProtocol.h
+++ b/lldb/include/lldb/Host/common/NativeProcessProtocol.h
@@ -247,6 +247,14 @@ class NativeProcessProtocol {
   // Access to inferior stdio
   virtual int GetTerminalFileDescriptor() { return m_terminal_fd; }
 
+  /// Write up to \p len bytes from \p buf to the inferior's stdin. Used on
+  /// Windows where the platform owns a ConPTY for the inferior and stdin
+  /// can't be reached through a regular file descriptor exposed via
+  /// `GetTerminalFileDescriptor`.
+  virtual size_t WriteStdin(const void *buf, size_t len, Status &error) {
+    return 0;
+  }
+
   // Stop id interface
 
   uint32_t GetStopID() const;
@@ -266,6 +274,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/Host/windows/ConnectionConPTYWindows.cpp 
b/lldb/source/Host/windows/ConnectionConPTYWindows.cpp
index e9d026fc7dc9f..f7e90421653e3 100644
--- a/lldb/source/Host/windows/ConnectionConPTYWindows.cpp
+++ b/lldb/source/Host/windows/ConnectionConPTYWindows.cpp
@@ -61,5 +61,30 @@ size_t ConnectionConPTY::Read(void *dst, size_t dst_len,
 size_t ConnectionConPTY::Write(const void *src, size_t src_len,
                                lldb::ConnectionStatus &status,
                                Status *error_ptr) {
-  llvm_unreachable("not implemented");
+  if (!m_pty || !m_pty->IsConnected()) {
+    status = eConnectionStatusNoConnection;
+    if (error_ptr)
+      *error_ptr = Status::FromErrorString("ConPTY not connected");
+    return 0;
+  }
+  HANDLE stdin_handle = m_pty->GetSTDINHandle();
+  if (stdin_handle == INVALID_HANDLE_VALUE || stdin_handle == nullptr) {
+    status = eConnectionStatusNoConnection;
+    if (error_ptr)
+      *error_ptr = Status::FromErrorString("ConPTY STDIN handle is invalid");
+    return 0;
+  }
+  DWORD written = 0;
+  if (!::WriteFile(stdin_handle, src, static_cast<DWORD>(src_len), &written,
+                   nullptr)) {
+    DWORD err = ::GetLastError();
+    status = (err == ERROR_BROKEN_PIPE || err == ERROR_NO_DATA)
+                 ? eConnectionStatusEndOfFile
+                 : eConnectionStatusError;
+    if (error_ptr)
+      *error_ptr = Status(err, lldb::eErrorTypeWin32);
+    return written;
+  }
+  status = eConnectionStatusSuccess;
+  return written;
 }
diff --git 
a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp 
b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
index 73989f18173c9..9a8dd366b40e0 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"
@@ -52,7 +55,8 @@ NativeProcessWindows::NativeProcessWindows(ProcessLaunchInfo 
&launch_info,
           LLDB_INVALID_PROCESS_ID,
           PseudoTerminal::invalid_fd, // TODO: Implement on Windows
           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 inferior is reaped.
+  StopStdioForwarding();
+
   ProcessDebugger::OnExitProcess(exit_code);
 
   // No signal involved.  It is just an exit event.
@@ -686,4 +698,59 @@ 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));
+}
+
+size_t NativeProcessWindows::WriteStdin(const void *buf, size_t len,
+                                        Status &error) {
+  if (!m_stdio_communication.HasConnection()) {
+    error = Status::FromErrorString(
+        "no ConPTY connection on this NativeProcessWindows");
+    return 0;
+  }
+  ConnectionStatus status;
+  size_t written = m_stdio_communication.Write(buf, len, status, &error);
+  if (status != eConnectionStatusSuccess && error.Success())
+    error = Status::FromErrorStringWithFormatv(
+        "ConPTY stdin write returned status {0}", static_cast<int>(status));
+  return written;
+}
+
 } // namespace lldb_private
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h 
b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
index d2c37d202f242..c5a8e704af21c 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;
@@ -103,6 +107,10 @@ class NativeProcessWindows : public NativeProcessProtocol,
 
   bool HasPendingLibraryEvents() override;
 
+  /// Forward bytes from the gdb-remote `I` packet into the inferior's ConPTY's
+  /// stdin.
+  size_t WriteStdin(const void *buf, size_t len, Status &error) override;
+
   // ProcessDebugger Overrides
   void OnExitProcess(uint32_t exit_code) override;
   void OnDebuggerConnected(lldb::addr_t image_base) override;
@@ -155,6 +163,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 a84e965b04bee..bb5f85a6ca2dd 100644
--- 
a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
+++ 
b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
@@ -286,13 +286,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
   }
 
   {
@@ -1219,6 +1214,18 @@ void GDBRemoteCommunicationServerLLGS::NewSubprocess(
       DebuggedProcess{std::move(child_process), DebuggedProcess::Flag{}});
 }
 
+void GDBRemoteCommunicationServerLLGS::NewProcessOutput(NativeProcessProtocol 
*,
+                                                        llvm::StringRef data) {
+  if (data.empty())
+    return;
+
+  std::string owned(data);
+  m_mainloop.AddPendingCallback(
+      [this, owned = std::move(owned)](MainLoopBase &) {
+        SendONotification(owned.data(), owned.size());
+      });
+}
+
 void GDBRemoteCommunicationServerLLGS::DataAvailableCallback() {
   Log *log = GetLog(GDBRLog::Comm);
 
@@ -2532,12 +2539,18 @@ 
GDBRemoteCommunicationServerLLGS::Handle_I(StringExtractorGDBRemote &packet) {
     // write directly to stdin *this might block if stdin buffer is full*
     // TODO: enqueue this block in circular buffer and send window size to
     // remote host
-    ConnectionStatus status;
     Status error;
+#if defined(_WIN32)
+    // On Windows the inferior's stdio is owned by NativeProcessWindows.
+    if (m_current_process->WriteStdin(tmp, read, error) != read || 
error.Fail())
+      return SendErrorResponse(0x15);
+#else
+    ConnectionStatus status;
     m_stdio_communication.WriteAll(tmp, read, status, &error);
     if (error.Fail()) {
       return SendErrorResponse(0x15);
     }
+#endif
   }
 
   return SendOKResponse();
diff --git 
a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.h 
b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.h
index 5a088f8de2380..c8f0d0fc4f57e 100644
--- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.h
+++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.h
@@ -88,6 +88,11 @@ 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;
+
   Status InitializeConnection(std::unique_ptr<Connection> connection);
 
   GDBRemoteCommunication::PacketResult

_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits

Reply via email to