[Lldb-commits] [lldb] [lldb][Windows] Forward debuggee STDOUT through lldb-server via ConPTY (PR #201124)
Charles Zablit via lldb-commits
lldb-commits at lists.llvm.org
Fri Jun 5 07:14:04 PDT 2026
https://github.com/charles-zablit updated https://github.com/llvm/llvm-project/pull/201124
>From 17858ef458e52bc3795a4867342f7a6ce9ffe731 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Tue, 2 Jun 2026 16:10:27 +0100
Subject: [PATCH 1/4] [lldb][Windows] Forward debuggee stdout through
lldb-server via ConPTY
---
.../lldb/Host/common/NativeProcessProtocol.h | 5 ++
.../Windows/Common/NativeProcessWindows.cpp | 55 ++++++++++++++++++-
.../Windows/Common/NativeProcessWindows.h | 21 +++++++
.../GDBRemoteCommunicationServerLLGS.cpp | 17 ++++--
.../GDBRemoteCommunicationServerLLGS.h | 5 ++
5 files changed, 96 insertions(+), 7 deletions(-)
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..886515d86328b 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.
@@ -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..11fdf33f6b3d7 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,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);
diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.h b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.h
index d582d2d77b639..0de4897a7f3cf 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
>From 7b1d0977bb0bd66f58ae96a25252b764020e0e1f Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Tue, 2 Jun 2026 16:15:38 +0100
Subject: [PATCH 2/4] [lldb][gdb-remote] Don't emit \$O while the inferior is
stopped
---
.../GDBRemoteCommunicationServerLLGS.cpp | 33 ++++++++++++++++---
.../GDBRemoteCommunicationServerLLGS.h | 7 ++++
2 files changed, 36 insertions(+), 4 deletions(-)
diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
index 11fdf33f6b3d7..d56026e011564 100644
--- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
@@ -1224,11 +1224,26 @@ void GDBRemoteCommunicationServerLLGS::NewProcessOutput(NativeProcessProtocol *,
if (data.empty())
return;
- std::string owned(data);
+ {
+ std::lock_guard<std::mutex> lock(m_pending_output_mutex);
+ m_pending_output_buffer.append(data.begin(), data.end());
+ }
m_mainloop.AddPendingCallback(
- [this, owned = std::move(owned)](MainLoopBase &) {
- SendONotification(owned.data(), owned.size());
- });
+ [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() {
@@ -2038,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 0de4897a7f3cf..e5b4c9ec0bed0 100644
--- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.h
+++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.h
@@ -93,6 +93,10 @@ class GDBRemoteCommunicationServerLLGS
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
@@ -125,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;
>From 6db3d18a580eebc5898568eb4c8d9435dae95941 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Thu, 4 Jun 2026 15:29:33 +0100
Subject: [PATCH 3/4] remove todo
---
.../Plugins/Process/Windows/Common/NativeProcessWindows.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
index 886515d86328b..12a0302ace886 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
@@ -53,7 +53,7 @@ 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()),
m_stdio_communication("lldb.NativeProcessWindows.stdio") {
>From 022f7409ae481479d41333a947c419330eea0f04 Mon Sep 17 00:00:00 2001
From: Charles Zablit <zablitcharles at gmail.com>
Date: Fri, 5 Jun 2026 15:13:53 +0100
Subject: [PATCH 4/4] Update NativeProcessWindows.cpp
---
.../Plugins/Process/Windows/Common/NativeProcessWindows.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
index 12a0302ace886..b235ab281bad6 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
@@ -440,7 +440,7 @@ void NativeProcessWindows::OnExitProcess(uint32_t exit_code) {
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.
+ // read thread can exit. Tear it down before the debuggee is destroyed.
StopStdioForwarding();
ProcessDebugger::OnExitProcess(exit_code);
More information about the lldb-commits
mailing list