[Lldb-commits] [lldb] [lldb][Windows] add stdin support to lldb-server (PR #201638)
Charles Zablit via lldb-commits
lldb-commits at lists.llvm.org
Fri Jun 5 09:14:23 PDT 2026
https://github.com/charles-zablit updated https://github.com/llvm/llvm-project/pull/201638
>From 488a6ce990aa489d73306795d3e40112a499bc9e Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Thu, 4 Jun 2026 18:09:30 +0100
Subject: [PATCH 1/3] [lldb][Windows] add stdin support to lldb-server
---
.../lldb/Host/common/NativeProcessProtocol.h | 6 ++++-
.../Host/windows/ConnectionConPTYWindows.cpp | 27 ++++++++++++++++++-
.../Windows/Common/NativeProcessWindows.cpp | 15 +++++++++++
.../Windows/Common/NativeProcessWindows.h | 7 +++++
.../Process/Windows/Common/ProcessWindows.cpp | 1 +
.../GDBRemoteCommunicationServerLLGS.cpp | 9 ++++++-
.../Process/gdb-remote/ProcessGDBRemote.cpp | 16 ++++++++++-
7 files changed, 77 insertions(+), 4 deletions(-)
diff --git a/lldb/include/lldb/Host/common/NativeProcessProtocol.h b/lldb/include/lldb/Host/common/NativeProcessProtocol.h
index 4e4804623b1d8..435185a38f3f9 100644
--- a/lldb/include/lldb/Host/common/NativeProcessProtocol.h
+++ b/lldb/include/lldb/Host/common/NativeProcessProtocol.h
@@ -247,8 +247,12 @@ class NativeProcessProtocol {
// Access to inferior stdio
virtual int GetTerminalFileDescriptor() { return m_terminal_fd; }
- // Stop id interface
+ /// Write up to \p len bytes from \p buf to the inferior's stdin.
+ virtual size_t WriteStdin(const void *buf, size_t len, Status &error) {
+ return 0;
+ }
+ // Stop id interface
uint32_t GetStopID() const;
// Callbacks for low-level process state changes
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 b235ab281bad6..bf447c9ea81f5 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
@@ -749,4 +749,19 @@ void NativeProcessWindows::STDIOReadThreadBytesReceived(void *baton,
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 95b85754ebdeb..cac3920d82aa9 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
@@ -107,6 +107,13 @@ class NativeProcessWindows : public NativeProcessProtocol,
bool HasPendingLibraryEvents() override;
+ /// Forward bytes from the gdb-remote `I` packet into the inferior's
+ /// ConPTY-backed stdin via `m_stdio_communication.Write` →
+ /// `ConnectionConPTY::Write` → `WriteFile` on the parent-side STDIN
+ /// HANDLE. Returns the number of bytes written (0 if the PTY is
+ /// disconnected or write fails).
+ 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;
diff --git a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
index 941c25e9ecb77..d6776260806f3 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
@@ -46,6 +46,7 @@
#include "ForwardDecl.h"
#include "IOHandlerProcessSTDIOWindows.h"
#include "LocalDebugDelegate.h"
+#include "Plugins/Process/Utility/IOHandlerProcessSTDIOWindows.h"
#include "ProcessWindowsLog.h"
#include "TargetThreadWindows.h"
diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
index d56026e011564..077144f94227d 100644
--- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
@@ -2569,12 +2569,19 @@ 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/ProcessGDBRemote.cpp b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
index f6eaf5851338b..1cded3f428e0b 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
@@ -95,6 +95,10 @@
#include "llvm/Support/Threading.h"
#include "llvm/Support/raw_ostream.h"
+#ifdef _WIN32
+#include "Plugins/Process/Windows/Common/IOHandlerProcessSTDIOWindows.h"
+#endif
+
#if defined(__APPLE__)
#define DEBUGSERVER_BASENAME "debugserver"
#elif defined(_WIN32)
@@ -873,8 +877,18 @@ Status ProcessGDBRemote::DoLaunch(lldb_private::Module *exe_module,
SetPrivateState(SetThreadStopInfo(response));
if (!disable_stdio) {
- if (pty.GetPrimaryFileDescriptor() != PseudoTerminal::invalid_fd)
+ if (pty.GetPrimaryFileDescriptor() != PseudoTerminal::invalid_fd) {
SetSTDIOFileDescriptor(pty.ReleasePrimaryFileDescriptor());
+ }
+#ifdef _WIN32
+ else if (m_stdin_forward) {
+ // No client-side PTY FD on Windows.
+ std::lock_guard<std::mutex> guard(m_process_input_reader_mutex);
+ if (!m_process_input_reader)
+ m_process_input_reader =
+ std::make_shared<IOHandlerProcessSTDIOWindows>(this);
+ }
+#endif
}
}
} else {
>From 8ef86c815abf2be99d845bf47a9061c1837120c1 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Thu, 4 Jun 2026 18:10:57 +0100
Subject: [PATCH 2/3] [lldb][gdb-remote] Plumb interrupt_timeout through
SendStdinNotification
---
.../Process/gdb-remote/GDBRemoteCommunicationClient.cpp | 7 ++++---
.../Process/gdb-remote/GDBRemoteCommunicationClient.h | 9 ++++++++-
.../Plugins/Process/gdb-remote/ProcessGDBRemote.cpp | 2 +-
3 files changed, 13 insertions(+), 5 deletions(-)
diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp
index 8df7936786b04..3a69a1d21f906 100644
--- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp
@@ -1442,13 +1442,14 @@ bool GDBRemoteCommunicationClient::GetHostInfo(bool force) {
return m_qHostInfo_is_valid == eLazyBoolYes;
}
-int GDBRemoteCommunicationClient::SendStdinNotification(const char *data,
- size_t data_len) {
+int GDBRemoteCommunicationClient::SendStdinNotification(
+ const char *data, size_t data_len, std::chrono::seconds interrupt_timeout) {
StreamString packet;
packet.PutCString("I");
packet.PutBytesAsRawHex8(data, data_len);
StringExtractorGDBRemote response;
- if (SendPacketAndWaitForResponse(packet.GetString(), response) ==
+ if (SendPacketAndWaitForResponse(packet.GetString(), response,
+ interrupt_timeout) ==
PacketResult::Success) {
return 0;
}
diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h
index 79ca0bcd3ed22..384a7da4d06e4 100644
--- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h
+++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h
@@ -127,10 +127,17 @@ class GDBRemoteCommunicationClient : public GDBRemoteClientBase {
/// \param[in] data_len
/// The number of bytes available at \a data.
///
+ /// \param[in] interrupt_timeout
+ /// If the inferior is running, how long to wait for a `\x03` BREAK
+ /// to interrupt it before giving up. Pass zero (the default) only
+ /// when the caller knows the inferior is stopped.
+ ///
/// \return
/// Zero if the attach was successful, or an error indicating
/// an error code.
- int SendStdinNotification(const char *data, size_t data_len);
+ int SendStdinNotification(
+ const char *data, size_t data_len,
+ std::chrono::seconds interrupt_timeout = std::chrono::seconds(0));
/// Sets the path to use for stdin/out/err for a process
/// that will be launched with the 'A' packet.
diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
index 1cded3f428e0b..0114a8004d149 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
@@ -3352,7 +3352,7 @@ size_t ProcessGDBRemote::PutSTDIN(const char *src, size_t src_len,
ConnectionStatus status;
m_stdio_communication.WriteAll(src, src_len, status, nullptr);
} else if (m_stdin_forward) {
- m_gdb_comm.SendStdinNotification(src, src_len);
+ m_gdb_comm.SendStdinNotification(src, src_len, GetInterruptTimeout());
}
return 0;
}
>From 0dba3573a4c66c919df3ac5e93c9c241aee600b9 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Thu, 4 Jun 2026 18:12:21 +0100
Subject: [PATCH 3/3] [lldb][Windows] Surface DebugBreakProcess Halt() as a
SIGSTOP signal stop
---
.../Windows/Common/NativeProcessWindows.cpp | 36 +++++++++++++++++--
.../Windows/Common/NativeProcessWindows.h | 3 ++
2 files changed, 37 insertions(+), 2 deletions(-)
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
index bf447c9ea81f5..f0dc855457ac9 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
@@ -173,8 +173,13 @@ NativeProcessWindows::GetThreadByID(lldb::tid_t thread_id) {
Status NativeProcessWindows::Halt() {
bool caused_stop = false;
StateType state = GetState();
- if (state != eStateStopped)
- return HaltProcess(caused_stop);
+ if (state != eStateStopped) {
+ m_pending_halt = true;
+ Status err = HaltProcess(caused_stop);
+ if (err.Fail() || !caused_stop)
+ m_pending_halt = false;
+ return err;
+ }
return Status();
}
@@ -581,6 +586,33 @@ NativeProcessWindows::HandleBreakpointException(const ExceptionRecord &record) {
// Any remaining STATUS_BREAKPOINT is a breakpoint instruction in the
// program's own code (e.g. `__debugbreak()` or `__builtin_debugtrap()`).
// Stop the debugger and let the user decide what to do.
+ if (m_pending_halt) {
+ LLDB_LOG(log,
+ "DebugBreakProcess injection treated as Halt SIGSTOP for tid "
+ "{0:x}",
+ thread_id);
+ m_pending_halt = false;
+ ThreadStopInfo signal_info;
+ signal_info.reason = StopReason::eStopReasonSignal;
+ signal_info.signo = 19; // SIGSTOP on POSIX
+
+ // Halt all threads at the kernel level.
+ for (uint32_t i = 0; i < m_threads.size(); ++i) {
+ auto t = static_cast<NativeThreadWindows *>(m_threads[i].get());
+ if (t->DoStop().Fail())
+ exit(1);
+ }
+ if (!m_threads.empty()) {
+ auto first = static_cast<NativeThreadWindows *>(m_threads[0].get());
+ first->SetStopReason(signal_info, "interrupt");
+ }
+ SetCurrentThreadID(thread_id);
+ if (NativeThreadWindows *injected = GetThreadByID(thread_id))
+ injected->SetStopReason(signal_info, "interrupt");
+ SetState(eStateStopped, true);
+ return ExceptionResult::BreakInDebugger;
+ }
+
std::string desc = formatv("Exception {0:x8} encountered at address {1:x8}",
record.GetExceptionCode(), exception_addr)
.str();
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
index cac3920d82aa9..d1c57fc01e06b 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
@@ -167,6 +167,9 @@ class NativeProcessWindows : public NativeProcessProtocol,
/// launch / attach.
bool m_initial_stop_seen = false;
+ /// Set when Halt() / Interrupt() schedules a DebugBreakProcess injection.
+ bool m_pending_halt = false;
+
/// PseudoConsole for the lldb-server stdio-forwarding path.
std::shared_ptr<PseudoConsole> m_pty;
More information about the lldb-commits
mailing list