[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 08:09:21 PDT 2026


https://github.com/charles-zablit updated https://github.com/llvm/llvm-project/pull/201638

>From 35d4db93b51d6eeb4c1f0c8c6c5bb27afc6adba4 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 ++-
 .../Plugins/Process/Utility/CMakeLists.txt    |   1 +
 .../Utility/IOHandlerProcessSTDIOWindows.cpp  | 172 ++++++++++++++++
 .../Utility/IOHandlerProcessSTDIOWindows.h    |  63 ++++++
 .../Windows/Common/NativeProcessWindows.cpp   |  15 ++
 .../Windows/Common/NativeProcessWindows.h     |   7 +
 .../Process/Windows/Common/ProcessWindows.cpp | 190 +-----------------
 .../Process/Windows/Common/ProcessWindows.h   |   2 +
 .../GDBRemoteCommunicationServerLLGS.cpp      |   9 +-
 .../Process/gdb-remote/ProcessGDBRemote.cpp   |  16 +-
 11 files changed, 325 insertions(+), 183 deletions(-)
 create mode 100644 lldb/source/Plugins/Process/Utility/IOHandlerProcessSTDIOWindows.cpp
 create mode 100644 lldb/source/Plugins/Process/Utility/IOHandlerProcessSTDIOWindows.h

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/Utility/CMakeLists.txt b/lldb/source/Plugins/Process/Utility/CMakeLists.txt
index 3652a0cfb530f..4206b398d982f 100644
--- a/lldb/source/Plugins/Process/Utility/CMakeLists.txt
+++ b/lldb/source/Plugins/Process/Utility/CMakeLists.txt
@@ -4,6 +4,7 @@ set_property(DIRECTORY PROPERTY LLDB_PLUGIN_KIND ProcessUtility)
 add_lldb_library(lldbPluginProcessUtility
   AuxVector.cpp
   FreeBSDSignals.cpp
+  IOHandlerProcessSTDIOWindows.cpp
   GDBRemoteSignals.cpp
   HistoryThread.cpp
   HistoryUnwind.cpp
diff --git a/lldb/source/Plugins/Process/Utility/IOHandlerProcessSTDIOWindows.cpp b/lldb/source/Plugins/Process/Utility/IOHandlerProcessSTDIOWindows.cpp
new file mode 100644
index 0000000000000..f8f5435fdbe07
--- /dev/null
+++ b/lldb/source/Plugins/Process/Utility/IOHandlerProcessSTDIOWindows.cpp
@@ -0,0 +1,172 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "IOHandlerProcessSTDIOWindows.h"
+
+#include "lldb/Host/windows/windows.h"
+#include "lldb/Utility/LLDBLog.h"
+#include "lldb/Utility/Log.h"
+#include "lldb/Utility/State.h"
+
+using namespace lldb_private;
+
+IOHandlerProcessSTDIOWindows::IOHandlerProcessSTDIOWindows(Process *process)
+    : IOHandler(process->GetTarget().GetDebugger(), IOHandler::Type::ProcessIO),
+      m_process(process),
+      m_read_file(GetInputFD(), File::eOpenOptionReadOnly, false),
+      m_interrupt_event(
+          CreateEvent(/*lpEventAttributes=*/nullptr, /*bManualReset=*/FALSE,
+                      /*bInitialState=*/FALSE, /*lpName=*/nullptr)) {}
+
+IOHandlerProcessSTDIOWindows::~IOHandlerProcessSTDIOWindows() {
+  if (m_interrupt_event != INVALID_HANDLE_VALUE)
+    ::CloseHandle(m_interrupt_event);
+}
+
+void IOHandlerProcessSTDIOWindows::SetIsRunning(bool running) {
+  std::lock_guard<std::mutex> guard(m_mutex);
+  SetIsDone(!running);
+  m_is_running = running;
+}
+
+/// Peek the console for input. If it has any, drain the pipe until text input
+/// is found or the pipe is empty.
+///
+/// \param hStdin
+///     The handle to the standard input's pipe.
+///
+/// \return
+///     true if the pipe has text input.
+llvm::Expected<bool>
+IOHandlerProcessSTDIOWindows::ConsoleHasTextInput(const HANDLE hStdin) {
+  // Check if there are already characters buffered. Pressing enter counts as
+  // 2 characters '\r\n' and only one of them is a keyDown event.
+  DWORD bytesAvailable = 0;
+  if (PeekNamedPipe(hStdin, nullptr, 0, nullptr, &bytesAvailable, nullptr)) {
+    if (bytesAvailable > 0)
+      return true;
+  }
+
+  while (true) {
+    INPUT_RECORD inputRecord;
+    DWORD numRead = 0;
+    if (!PeekConsoleInput(hStdin, &inputRecord, 1, &numRead))
+      return llvm::createStringError("failed to peek standard input");
+
+    if (numRead == 0)
+      return false;
+
+    if (inputRecord.EventType == KEY_EVENT &&
+        inputRecord.Event.KeyEvent.bKeyDown &&
+        inputRecord.Event.KeyEvent.uChar.AsciiChar != 0)
+      return true;
+
+    if (!ReadConsoleInput(hStdin, &inputRecord, 1, &numRead))
+      return llvm::createStringError("failed to read standard input");
+  }
+}
+
+void IOHandlerProcessSTDIOWindows::Run() {
+  if (!m_read_file.IsValid()) {
+    SetIsDone(true);
+    return;
+  }
+
+  SetIsDone(false);
+  SetIsRunning(true);
+
+  HANDLE hStdin = m_read_file.GetWaitableHandle();
+  HANDLE waitHandles[2] = {hStdin, m_interrupt_event};
+
+  DWORD consoleMode;
+  bool isConsole = GetConsoleMode(hStdin, &consoleMode) != 0;
+  // With ENABLE_LINE_INPUT, ReadFile returns only when a carriage return is
+  // read. This will block lldb in ReadFile until the user hits enter. Save
+  // the previous console mode to restore it later and remove
+  // ENABLE_LINE_INPUT.
+  DWORD oldConsoleMode = consoleMode;
+  SetConsoleMode(hStdin, consoleMode & ~ENABLE_LINE_INPUT & ~ENABLE_ECHO_INPUT);
+
+  while (true) {
+    {
+      std::lock_guard<std::mutex> guard(m_mutex);
+      if (GetIsDone())
+        goto exit_loop;
+    }
+
+    DWORD result = WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE);
+    switch (result) {
+    case WAIT_FAILED:
+      goto exit_loop;
+    case WAIT_OBJECT_0: {
+      if (isConsole) {
+        auto hasInputOrErr = ConsoleHasTextInput(hStdin);
+        if (!hasInputOrErr) {
+          Log *log = GetLog(LLDBLog::Process);
+          LLDB_LOG_ERROR(log, hasInputOrErr.takeError(),
+                         "failed to process debuggee's IO: {0}");
+          goto exit_loop;
+        }
+
+        // If no text input is ready, go back to waiting.
+        if (!*hasInputOrErr)
+          continue;
+      }
+
+      char ch = 0;
+      DWORD read = 0;
+      if (!ReadFile(hStdin, &ch, 1, &read, nullptr) || read != 1)
+        goto exit_loop;
+
+      Status err;
+      m_process->PutSTDIN(&ch, 1, err);
+      if (err.Fail())
+        goto exit_loop;
+      break;
+    }
+    case WAIT_OBJECT_0 + 1: {
+      ControlOp op = m_pending_op.exchange(eControlOpNone);
+      if (op == eControlOpQuit)
+        goto exit_loop;
+      if (op == eControlOpInterrupt &&
+          StateIsRunningState(m_process->GetState()))
+        m_process->SendAsyncInterrupt();
+      break;
+    }
+    default:
+      goto exit_loop;
+    }
+  }
+
+exit_loop:;
+  SetIsRunning(false);
+  SetIsDone(true);
+  SetConsoleMode(hStdin, oldConsoleMode);
+}
+
+void IOHandlerProcessSTDIOWindows::Cancel() {
+  std::lock_guard<std::mutex> guard(m_mutex);
+  SetIsDone(true);
+  if (m_is_running) {
+    m_pending_op.store(eControlOpQuit);
+    ::SetEvent(m_interrupt_event);
+  }
+}
+
+bool IOHandlerProcessSTDIOWindows::Interrupt() {
+  if (m_active) {
+    m_pending_op.store(eControlOpInterrupt);
+    ::SetEvent(m_interrupt_event);
+    return true;
+  }
+  if (StateIsRunningState(m_process->GetState())) {
+    m_process->SendAsyncInterrupt();
+    return true;
+  }
+  return false;
+}
\ No newline at end of file
diff --git a/lldb/source/Plugins/Process/Utility/IOHandlerProcessSTDIOWindows.h b/lldb/source/Plugins/Process/Utility/IOHandlerProcessSTDIOWindows.h
new file mode 100644
index 0000000000000..fcbd587b40143
--- /dev/null
+++ b/lldb/source/Plugins/Process/Utility/IOHandlerProcessSTDIOWindows.h
@@ -0,0 +1,63 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LIBLLDB_PLUGINS_PROCESS_WINDOWS_COMMON_IO_HANDLER_PROCESS_STDIO_WINDOWS_H_
+#define LIBLLDB_PLUGINS_PROCESS_WINDOWS_COMMON_IO_HANDLER_PROCESS_STDIO_WINDOWS_H_
+
+#include "lldb/Core/IOHandler.h"
+#include "lldb/Host/File.h"
+#include "lldb/Target/Process.h"
+
+typedef void *HANDLE;
+
+using namespace lldb_private;
+
+class IOHandlerProcessSTDIOWindows : public IOHandler {
+public:
+  IOHandlerProcessSTDIOWindows(Process *process);
+
+  ~IOHandlerProcessSTDIOWindows() override;
+
+  void SetIsRunning(bool running);
+
+  /// Peek the console for input. If it has any, drain the pipe until text input
+  /// is found or the pipe is empty.
+  ///
+  /// \param hStdin
+  ///     The handle to the standard input's pipe.
+  ///
+  /// \return
+  ///     true if the pipe has text input.
+  llvm::Expected<bool> ConsoleHasTextInput(const HANDLE hStdin);
+
+  void Run() override;
+
+  void Cancel() override;
+
+  bool Interrupt() override;
+
+  void GotEOF() override {}
+
+private:
+  enum ControlOp : char {
+    eControlOpQuit = 'q',
+    eControlOpInterrupt = 'i',
+    eControlOpNone = 0,
+  };
+
+  Process *m_process;
+  /// Read from this file (usually actual STDIN for LLDB)
+  NativeFile m_read_file;
+  HANDLE m_interrupt_event =
+      reinterpret_cast<HANDLE>(static_cast<intptr_t>(-1));
+  std::atomic<ControlOp> m_pending_op{eControlOpNone};
+  std::mutex m_mutex;
+  bool m_is_running = false;
+};
+
+#endif // LIBLLDB_PLUGINS_PROCESS_WINDOWS_COMMON_IO_HANDLER_PROCESS_STDIO_WINDOWS_H_
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 9a273463792ce..83d9e784f75a0 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
@@ -45,6 +45,7 @@
 #include "ExceptionRecord.h"
 #include "ForwardDecl.h"
 #include "LocalDebugDelegate.h"
+#include "Plugins/Process/Utility/IOHandlerProcessSTDIOWindows.h"
 #include "ProcessWindowsLog.h"
 #include "TargetThreadWindows.h"
 
@@ -1084,184 +1085,15 @@ Status ProcessWindows::DisableWatchpoint(WatchpointSP wp_sp, bool notify) {
   return error;
 }
 
-class IOHandlerProcessSTDIOWindows : public IOHandler {
-public:
-  IOHandlerProcessSTDIOWindows(Process *process, HANDLE conpty_input)
-      : IOHandler(process->GetTarget().GetDebugger(),
-                  IOHandler::Type::ProcessIO),
-        m_process(process),
-        m_read_file(GetInputFD(), File::eOpenOptionReadOnly, false),
-        m_write_file(conpty_input),
-        m_interrupt_event(
-            CreateEvent(/*lpEventAttributes=*/nullptr, /*bManualReset=*/FALSE,
-                        /*bInitialState=*/FALSE, /*lpName=*/nullptr)) {}
-
-  ~IOHandlerProcessSTDIOWindows() override {
-    if (m_interrupt_event != INVALID_HANDLE_VALUE)
-      ::CloseHandle(m_interrupt_event);
+size_t ProcessWindows::PutSTDIN(const char *src, size_t src_len,
+                                Status &error) {
+  if (!m_stdio_communication.IsConnected()) {
+    error = Status::FromErrorString("stdin not connected");
+    return 0;
   }
-
-  void SetIsRunning(bool running) {
-    std::lock_guard<std::mutex> guard(m_mutex);
-    SetIsDone(!running);
-    m_is_running = running;
-  }
-
-  /// Peek the console for input. If it has any, drain the pipe until text input
-  /// is found or the pipe is empty.
-  ///
-  /// \param hStdin
-  ///     The handle to the standard input's pipe.
-  ///
-  /// \return
-  ///     true if the pipe has text input.
-  llvm::Expected<bool> ConsoleHasTextInput(const HANDLE hStdin) {
-    // Check if there are already characters buffered. Pressing enter counts as
-    // 2 characters '\r\n' and only one of them is a keyDown event.
-    DWORD bytesAvailable = 0;
-    if (PeekNamedPipe(hStdin, nullptr, 0, nullptr, &bytesAvailable, nullptr)) {
-      if (bytesAvailable > 0)
-        return true;
-    }
-
-    while (true) {
-      INPUT_RECORD inputRecord;
-      DWORD numRead = 0;
-      if (!PeekConsoleInput(hStdin, &inputRecord, 1, &numRead))
-        return llvm::createStringError("failed to peek standard input");
-
-      if (numRead == 0)
-        return false;
-
-      if (inputRecord.EventType == KEY_EVENT &&
-          inputRecord.Event.KeyEvent.bKeyDown &&
-          inputRecord.Event.KeyEvent.uChar.AsciiChar != 0)
-        return true;
-
-      if (!ReadConsoleInput(hStdin, &inputRecord, 1, &numRead))
-        return llvm::createStringError("failed to read standard input");
-    }
-  }
-
-  void Run() override {
-    if (!m_read_file.IsValid() || m_write_file == INVALID_HANDLE_VALUE) {
-      SetIsDone(true);
-      return;
-    }
-
-    SetIsDone(false);
-    SetIsRunning(true);
-
-    HANDLE hStdin = m_read_file.GetWaitableHandle();
-    HANDLE waitHandles[2] = {hStdin, m_interrupt_event};
-
-    DWORD consoleMode;
-    bool isConsole = GetConsoleMode(hStdin, &consoleMode) != 0;
-    // With ENABLE_LINE_INPUT, ReadFile returns only when a carriage return is
-    // read. This will block lldb in ReadFile until the user hits enter. Save
-    // the previous console mode to restore it later and remove
-    // ENABLE_LINE_INPUT.
-    DWORD oldConsoleMode = consoleMode;
-    SetConsoleMode(hStdin,
-                   consoleMode & ~ENABLE_LINE_INPUT & ~ENABLE_ECHO_INPUT);
-
-    while (true) {
-      {
-        std::lock_guard<std::mutex> guard(m_mutex);
-        if (GetIsDone())
-          goto exit_loop;
-      }
-
-      DWORD result = WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE);
-      switch (result) {
-      case WAIT_FAILED:
-        goto exit_loop;
-      case WAIT_OBJECT_0: {
-        if (isConsole) {
-          auto hasInputOrErr = ConsoleHasTextInput(hStdin);
-          if (!hasInputOrErr) {
-            Log *log = GetLog(WindowsLog::Process);
-            LLDB_LOG_ERROR(log, hasInputOrErr.takeError(),
-                           "failed to process debuggee's IO: {0}");
-            goto exit_loop;
-          }
-
-          // If no text input is ready, go back to waiting.
-          if (!*hasInputOrErr)
-            continue;
-        }
-
-        char ch = 0;
-        DWORD read = 0;
-        if (!ReadFile(hStdin, &ch, 1, &read, nullptr) || read != 1)
-          goto exit_loop;
-
-        DWORD written = 0;
-        if (!WriteFile(m_write_file, &ch, 1, &written, nullptr) || written != 1)
-          goto exit_loop;
-        break;
-      }
-      case WAIT_OBJECT_0 + 1: {
-        ControlOp op = m_pending_op.exchange(eControlOpNone);
-        if (op == eControlOpQuit)
-          goto exit_loop;
-        if (op == eControlOpInterrupt &&
-            StateIsRunningState(m_process->GetState()))
-          m_process->SendAsyncInterrupt();
-        break;
-      }
-      default:
-        goto exit_loop;
-      }
-    }
-
-  exit_loop:;
-    SetIsRunning(false);
-    SetIsDone(true);
-    SetConsoleMode(hStdin, oldConsoleMode);
-  }
-
-  void Cancel() override {
-    std::lock_guard<std::mutex> guard(m_mutex);
-    SetIsDone(true);
-    if (m_is_running) {
-      m_pending_op.store(eControlOpQuit);
-      ::SetEvent(m_interrupt_event);
-    }
-  }
-
-  bool Interrupt() override {
-    if (m_active) {
-      m_pending_op.store(eControlOpInterrupt);
-      ::SetEvent(m_interrupt_event);
-      return true;
-    }
-    if (StateIsRunningState(m_process->GetState())) {
-      m_process->SendAsyncInterrupt();
-      return true;
-    }
-    return false;
-  }
-
-  void GotEOF() override {}
-
-private:
-  enum ControlOp : char {
-    eControlOpQuit = 'q',
-    eControlOpInterrupt = 'i',
-    eControlOpNone = 0,
-  };
-
-  Process *m_process;
-  /// Read from this file (usually actual STDIN for LLDB)
-  NativeFile m_read_file;
-  /// Write to this file (usually the primary pty for getting io to debuggee)
-  HANDLE m_write_file = INVALID_HANDLE_VALUE;
-  HANDLE m_interrupt_event = INVALID_HANDLE_VALUE;
-  std::atomic<ControlOp> m_pending_op{eControlOpNone};
-  std::mutex m_mutex;
-  bool m_is_running = false;
-};
+  ConnectionStatus status;
+  return m_stdio_communication.WriteAll(src, src_len, status, &error);
+}
 
 void ProcessWindows::SetPseudoConsoleHandle() {
   if (m_pty == nullptr)
@@ -1277,8 +1109,8 @@ void ProcessWindows::SetPseudoConsoleHandle() {
     {
       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, m_pty->GetSTDINHandle());
+        m_process_input_reader =
+            std::make_shared<IOHandlerProcessSTDIOWindows>(this);
     }
   }
 }
diff --git a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h
index da35ae923dcee..38c47a95e6cbf 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h
@@ -113,6 +113,8 @@ class ProcessWindows : public Process, public ProcessDebugger {
   /// buffered in the ConPTY/pipe to the process's STDOUT cache.
   void DrainProcessStdout();
 
+  size_t PutSTDIN(const char *src, size_t src_len, Status &error);
+
   ProcessWindows(lldb::TargetSP target_sp, lldb::ListenerSP listener_sp);
 
   Status DoGetMemoryRegionInfo(lldb::addr_t vm_addr,
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..c6573499ff3d6 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/Utility/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 24cdb004703724e62e4960937eca0bd4c7e3dda8 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 c6573499ff3d6..0319c3606362a 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 540a72bc597eca7827c48522e8e67cec3b0b46a5 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