[Lldb-commits] [lldb] [lldb][Windows] Forward debuggee stdio through lldb-server via ConPTY (PR #201124)
Charles Zablit via lldb-commits
lldb-commits at lists.llvm.org
Tue Jun 2 06:55:30 PDT 2026
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 <c_zablit at apple.com>
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
More information about the lldb-commits
mailing list