Files
llvm-project/lldb/source/Core/Statusline.cpp
jimingham fbb371affa Prevents a potential lock inversion in StatusLine when shutting down. (#179789)
We have gotten reports of an occasional deadlock on shutdown. The
Driver::MainLoop thread is shutting down, and gets stuck here:

  Thread 0x3c017c
  start 
    main 
       Driver::MainLoop() 
         lldb::SBDebugger::RunCommandInterpreter(bool, bool) 

lldb_private::CommandInterpreter::RunCommandInterpreter(lldb_private::CommandInterpreterRunOptions&)
            lldb_private::Debugger::RunIOHandlers()

lldb_private::Debugger::PopIOHandler(std::__1::shared_ptr<lldb_private::IOHandler>
const&)
                 lldb_private::Editline::Cancel()
                   lldb_private::LockableStreamFile::Lock()
                     std::__1::recursive_mutex::lock()

It has acquired the IO Handler list lock (in PopIOHandler) and is stuck
trying to get the debugger output stream lock.

Meanwhile, the event-handler thread is doing:

  Thread 0x3c01d3
  thread_start
    _pthread_start
       lldb_private::HostThreadMacOSX::ThreadCreateTrampoline(void*) 
lldb_private::HostNativeThreadBase::ThreadCreateTrampoline(void*)

std::__1::__function::__func<lldb_private::Debugger::StartEventHandlerThread()::$_0,
void* ()>::operator()()
            lldb_private::Debugger::DefaultEventHandler()
              lldb_private::Statusline::~Statusline()

lldb_private::Statusline::UpdateScrollWindow(lldb_private::Statusline::ScrollWindowMode)
                  lldb_private::Debugger::RefreshIOHandler() 
                    std::__1::recursive_mutex::lock()

UpdateScrollWindow gets the debugger output stream lock, and sends some
data to the output, then it calls RefreshIOHandler while still holding
the Debugger output stream lock. That's the problem, since if it gets
that lock before Editline::Cancel() completes, we'll get this deadlock.

The solution is simple, there's no reason why UpdateScrollWindow should
hold the debugger output lock when it calls RefreshIOHandler. If that
refresh ends up needing to write to the debugger output, it can take the
lock more narrowly at that point. This fixed the deadlock because after
yielding the output lock, the second thread waits on the first to get
the IO Handler lock. Meanwhile the first thread can now acquire the
debugger output lock and finish the Cancel, and exit PopIOHandler which
will allow the second thread to make progress in turn.

I couldn't figure out any way to test this...
2026-02-05 10:28:51 -08:00

158 lines
5.2 KiB
C++

//===-- Statusline.cpp ---------------------------------------------------===//
//
// 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 "lldb/Core/Statusline.h"
#include "lldb/Core/Debugger.h"
#include "lldb/Core/FormatEntity.h"
#include "lldb/Host/StreamFile.h"
#include "lldb/Interpreter/CommandInterpreter.h"
#include "lldb/Symbol/SymbolContext.h"
#include "lldb/Target/Process.h"
#include "lldb/Target/StackFrame.h"
#include "lldb/Utility/AnsiTerminal.h"
#include "lldb/Utility/StreamString.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Locale.h"
#define ESCAPE "\x1b"
#define ANSI_NORMAL ESCAPE "[0m"
#define ANSI_SAVE_CURSOR ESCAPE "7"
#define ANSI_RESTORE_CURSOR ESCAPE "8"
#define ANSI_CLEAR_BELOW ESCAPE "[J"
#define ANSI_CLEAR_SCREEN ESCAPE "[2J"
#define ANSI_SET_SCROLL_ROWS ESCAPE "[1;%ur"
#define ANSI_TO_START_OF_ROW ESCAPE "[%u;1f"
#define ANSI_REVERSE_VIDEO ESCAPE "[7m"
#define ANSI_UP_ROWS ESCAPE "[%dA"
using namespace lldb;
using namespace lldb_private;
Statusline::Statusline(Debugger &debugger)
: m_debugger(debugger), m_terminal_width(m_debugger.GetTerminalWidth()),
m_terminal_height(m_debugger.GetTerminalHeight()) {}
Statusline::~Statusline() { Disable(); }
void Statusline::TerminalSizeChanged() {
m_terminal_width = m_debugger.GetTerminalWidth();
m_terminal_height = m_debugger.GetTerminalHeight();
UpdateScrollWindow(ResizeStatusline);
// Redraw the old statusline.
Redraw(std::nullopt);
}
void Statusline::Enable(std::optional<ExecutionContextRef> exe_ctx_ref) {
// Reduce the scroll window to make space for the status bar below.
UpdateScrollWindow(EnableStatusline);
// Draw the statusline.
Redraw(exe_ctx_ref);
}
void Statusline::Disable() {
// Extend the scroll window to cover the status bar.
UpdateScrollWindow(DisableStatusline);
}
void Statusline::Draw(std::string str) {
lldb::LockableStreamFileSP stream_sp = m_debugger.GetOutputStreamSP();
if (!stream_sp)
return;
str = ansi::TrimAndPad(str, m_terminal_width);
LockedStreamFile locked_stream = stream_sp->Lock();
locked_stream << ANSI_SAVE_CURSOR;
locked_stream.Printf(ANSI_TO_START_OF_ROW,
static_cast<unsigned>(m_terminal_height));
// Use "reverse video" to make sure the statusline has a background. Only do
// this when colors are disabled, and rely on the statusline format otherwise.
if (!m_debugger.GetUseColor())
locked_stream << ANSI_REVERSE_VIDEO;
locked_stream << str;
locked_stream << ANSI_NORMAL;
locked_stream << ANSI_RESTORE_CURSOR;
}
void Statusline::UpdateScrollWindow(ScrollWindowMode mode) {
assert(m_terminal_width != 0 && m_terminal_height != 0);
lldb::LockableStreamFileSP stream_sp = m_debugger.GetOutputStreamSP();
if (!stream_sp)
return;
const unsigned reduced_scroll_rows = m_terminal_height - 1;
{ // Scope for locked_stream:
LockedStreamFile locked_stream = stream_sp->Lock();
switch (mode) {
case EnableStatusline:
// Move everything on the screen up.
locked_stream << '\n';
locked_stream.Printf(ANSI_UP_ROWS, 1);
// Reduce the scroll window.
locked_stream << ANSI_SAVE_CURSOR;
locked_stream.Printf(ANSI_SET_SCROLL_ROWS, reduced_scroll_rows);
locked_stream << ANSI_RESTORE_CURSOR;
break;
case DisableStatusline:
// Reset the scroll window.
locked_stream << ANSI_SAVE_CURSOR;
locked_stream.Printf(ANSI_SET_SCROLL_ROWS,
static_cast<unsigned>(m_terminal_height));
locked_stream << ANSI_RESTORE_CURSOR;
// Clear the screen below to hide the old statusline.
locked_stream << ANSI_CLEAR_BELOW;
break;
case ResizeStatusline:
// Clear the screen and update the scroll window.
// FIXME: Find a better solution (#146919).
locked_stream << ANSI_CLEAR_SCREEN;
locked_stream.Printf(ANSI_SET_SCROLL_ROWS, reduced_scroll_rows);
break;
}
}
m_debugger.RefreshIOHandler();
}
void Statusline::Redraw(std::optional<ExecutionContextRef> exe_ctx_ref) {
// Update the cached execution context.
if (exe_ctx_ref)
m_exe_ctx_ref = *exe_ctx_ref;
// Lock the execution context.
ExecutionContext exe_ctx =
m_exe_ctx_ref.Lock(/*thread_and_frame_only_if_stopped=*/false);
// Compute the symbol context if we're stopped.
SymbolContext sym_ctx;
llvm::Expected<StoppedExecutionContext> stopped_exe_ctx =
GetStoppedExecutionContext(&m_exe_ctx_ref);
if (stopped_exe_ctx) {
// The StoppedExecutionContext only ensures that we hold the run lock.
// The process could be in an exited or unloaded state and have no frame.
if (auto frame_sp = stopped_exe_ctx->GetFrameSP())
sym_ctx = frame_sp->GetSymbolContext(eSymbolContextEverything);
} else {
// We can draw the statusline without being stopped.
llvm::consumeError(stopped_exe_ctx.takeError());
}
StreamString stream;
FormatEntity::Entry format = m_debugger.GetStatuslineFormat();
FormatEntity::Formatter(&sym_ctx, &exe_ctx, nullptr, false, false)
.Format(format, stream);
Draw(stream.GetString().str());
}