[Lldb-commits] [lldb] [lldb][windows] add STDIN and STDOUT forwarding support (PR #180561)
Charles Zablit via lldb-commits
lldb-commits at lists.llvm.org
Wed Feb 11 08:09:01 PST 2026
https://github.com/charles-zablit updated https://github.com/llvm/llvm-project/pull/180561
>From a04da056223bc2221186a21a638c642b4686aba9 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Mon, 9 Feb 2026 17:15:06 +0000
Subject: [PATCH 1/2] [lldb][windows] add STDIN and STDOUT forwarding support
---
.../Host/windows/ProcessLauncherWindows.cpp | 3 +-
.../Process/Windows/Common/ProcessWindows.cpp | 124 +++++++++++++-----
.../Shell/Settings/TestFrameFormatColor.test | 2 +-
.../Settings/TestFrameFormatNoColor.test | 2 +-
4 files changed, 96 insertions(+), 35 deletions(-)
diff --git a/lldb/source/Host/windows/ProcessLauncherWindows.cpp b/lldb/source/Host/windows/ProcessLauncherWindows.cpp
index 6483e668d73b3..cfd84731f0eb6 100644
--- a/lldb/source/Host/windows/ProcessLauncherWindows.cpp
+++ b/lldb/source/Host/windows/ProcessLauncherWindows.cpp
@@ -203,7 +203,8 @@ ProcessLauncherWindows::LaunchProcess(const ProcessLaunchInfo &launch_info,
BOOL result = ::CreateProcessW(
wexecutable.c_str(), pwcommandLine, NULL, NULL,
- /*bInheritHandles=*/!inherited_handles.empty(), flags, environment.data(),
+ /*bInheritHandles=*/!inherited_handles.empty() || use_pty, flags,
+ environment.data(),
wworkingDirectory.size() == 0 ? NULL : wworkingDirectory.c_str(),
reinterpret_cast<STARTUPINFOW *>(&startupinfoex), &pi);
diff --git a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
index 226cc147aadae..373729c952071 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
@@ -958,21 +958,60 @@ class IOHandlerProcessSTDIOWindows : public IOHandler {
IOHandler::Type::ProcessIO),
m_process(process),
m_read_file(GetInputFD(), File::eOpenOptionReadOnly, false),
- m_write_file(conpty_input) {
- m_pipe.CreateNew();
+ m_write_file(conpty_input),
+ m_interrupt_event(
+ CreateEvent(/*lpEventAttributes=*/NULL, /*bManualReset=*/FALSE,
+ /*bInitialState=*/FALSE, /*lpName=*/NULL)) {}
+
+ ~IOHandlerProcessSTDIOWindows() override {
+ if (m_interrupt_event != INVALID_HANDLE_VALUE)
+ ::CloseHandle(m_interrupt_event);
}
- ~IOHandlerProcessSTDIOWindows() override = default;
-
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, NULL, 0, NULL, &bytesAvailable, NULL)) {
+ 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 ||
- !m_pipe.CanRead() || !m_pipe.CanWrite()) {
+ if (!m_read_file.IsValid() || m_write_file == INVALID_HANDLE_VALUE) {
SetIsDone(true);
return;
}
@@ -980,9 +1019,18 @@ class IOHandlerProcessSTDIOWindows : public IOHandler {
SetIsDone(false);
SetIsRunning(true);
- HANDLE hStdin = (HANDLE)_get_osfhandle(m_read_file.GetDescriptor());
- HANDLE hInterrupt = (HANDLE)_get_osfhandle(m_pipe.GetReadFileDescriptor());
- HANDLE waitHandles[2] = {hStdin, hInterrupt};
+ 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) {
{
@@ -996,6 +1044,20 @@ class IOHandlerProcessSTDIOWindows : public IOHandler {
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)
@@ -1007,14 +1069,10 @@ class IOHandlerProcessSTDIOWindows : public IOHandler {
break;
}
case WAIT_OBJECT_0 + 1: {
- char ch = 0;
- DWORD read = 0;
- if (!ReadFile(hInterrupt, &ch, 1, &read, nullptr) || read != 1)
- goto exit_loop;
-
- if (ch == eControlOpQuit)
+ ControlOp op = m_pending_op.exchange(eControlOpNone);
+ if (op == eControlOpQuit)
goto exit_loop;
- if (ch == eControlOpInterrupt &&
+ if (op == eControlOpInterrupt &&
StateIsRunningState(m_process->GetState()))
m_process->SendAsyncInterrupt();
break;
@@ -1026,24 +1084,24 @@ class IOHandlerProcessSTDIOWindows : public IOHandler {
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) {
- char ch = eControlOpQuit;
- if (llvm::Error err = m_pipe.Write(&ch, 1).takeError()) {
- LLDB_LOG_ERROR(GetLog(LLDBLog::Process), std::move(err),
- "Pipe write failed: {0}");
- }
+ m_pending_op.store(eControlOpQuit);
+ ::SetEvent(m_interrupt_event);
}
}
bool Interrupt() override {
if (m_active) {
- char ch = eControlOpInterrupt;
- return !errorToBool(m_pipe.Write(&ch, 1).takeError());
+ m_pending_op.store(eControlOpInterrupt);
+ ::SetEvent(m_interrupt_event);
+ return true;
}
if (StateIsRunningState(m_process->GetState())) {
m_process->SendAsyncInterrupt();
@@ -1055,19 +1113,21 @@ class IOHandlerProcessSTDIOWindows : public IOHandler {
void GotEOF() override {}
private:
- Process *m_process;
- NativeFile m_read_file; // Read from this file (usually actual STDIN for LLDB
- HANDLE m_write_file =
- INVALID_HANDLE_VALUE; // Write to this file (usually the primary pty for
- // getting io to debuggee)
- Pipe m_pipe;
- std::mutex m_mutex;
- bool m_is_running = false;
-
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;
};
void ProcessWindows::SetPseudoConsoleHandle(
diff --git a/lldb/test/Shell/Settings/TestFrameFormatColor.test b/lldb/test/Shell/Settings/TestFrameFormatColor.test
index 970d7238e7512..f30dafadf5919 100644
--- a/lldb/test/Shell/Settings/TestFrameFormatColor.test
+++ b/lldb/test/Shell/Settings/TestFrameFormatColor.test
@@ -9,4 +9,4 @@ c
q
# Check the ASCII escape code
-# CHECK:
+# CHECK: {{\[[0-9;]+m}}
diff --git a/lldb/test/Shell/Settings/TestFrameFormatNoColor.test b/lldb/test/Shell/Settings/TestFrameFormatNoColor.test
index 2bcdb8e82bd9d..37906311c4f69 100644
--- a/lldb/test/Shell/Settings/TestFrameFormatNoColor.test
+++ b/lldb/test/Shell/Settings/TestFrameFormatNoColor.test
@@ -9,4 +9,4 @@ c
q
# Check the ASCII escape code
-# CHECK-NOT:
+# CHECK-NOT: {{\[[0-9;]+m}}
>From 504d9196f1aff2fccd14c866f307a4bf03cff1e4 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Wed, 11 Feb 2026 17:08:46 +0100
Subject: [PATCH 2/2] remove test change
---
lldb/test/Shell/Settings/TestFrameFormatColor.test | 2 +-
lldb/test/Shell/Settings/TestFrameFormatNoColor.test | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/lldb/test/Shell/Settings/TestFrameFormatColor.test b/lldb/test/Shell/Settings/TestFrameFormatColor.test
index f30dafadf5919..970d7238e7512 100644
--- a/lldb/test/Shell/Settings/TestFrameFormatColor.test
+++ b/lldb/test/Shell/Settings/TestFrameFormatColor.test
@@ -9,4 +9,4 @@ c
q
# Check the ASCII escape code
-# CHECK: {{\[[0-9;]+m}}
+# CHECK:
diff --git a/lldb/test/Shell/Settings/TestFrameFormatNoColor.test b/lldb/test/Shell/Settings/TestFrameFormatNoColor.test
index 37906311c4f69..2bcdb8e82bd9d 100644
--- a/lldb/test/Shell/Settings/TestFrameFormatNoColor.test
+++ b/lldb/test/Shell/Settings/TestFrameFormatNoColor.test
@@ -9,4 +9,4 @@ c
q
# Check the ASCII escape code
-# CHECK-NOT: {{\[[0-9;]+m}}
+# CHECK-NOT:
More information about the lldb-commits
mailing list