Skip to content

[lldb-dap] Fix raciness in launch and attach tests #137920

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def verify_breakpoint_hit(self, breakpoint_ids):
match_desc = "breakpoint %s." % (breakpoint_id)
if match_desc in description:
return
self.assertTrue(False, "breakpoint not hit")
self.assertTrue(False, f"breakpoint not hit: stopped_events={stopped_events}")

def verify_stop_exception_info(self, expected_description, timeout=timeoutval):
"""Wait for the process we are debugging to stop, and verify the stop
Expand Down
1 change: 0 additions & 1 deletion lldb/test/API/tools/lldb-dap/attach/TestDAP_attach.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ def spawn_and_wait(program, delay):
process.wait()


@skipIf
class TestDAP_attach(lldbdap_testcase.DAPTestCaseBase):
def set_and_hit_breakpoint(self, continueToExit=True):
source = "main.c"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import socket


@skip
class TestDAP_attachByPortNum(lldbdap_testcase.DAPTestCaseBase):
default_timeout = 20

Expand Down
4 changes: 3 additions & 1 deletion lldb/tools/lldb-dap/DAP.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ DAP::DAP(Log *log, const ReplMode default_repl_mode,
exception_breakpoints(), focus_tid(LLDB_INVALID_THREAD_ID),
stop_at_entry(false), is_attach(false),
restarting_process_id(LLDB_INVALID_PROCESS_ID),
configuration_done_sent(false), waiting_for_run_in_terminal(false),
configuration_done_sent(false),
first_stop_state(FirstStopState::NoStopEvent),
waiting_for_run_in_terminal(false),
progress_event_reporter(
[&](const ProgressEvent &event) { SendJSON(event.ToJSON()); }),
reverse_request_seq(0), repl_mode(default_repl_mode) {
Expand Down
11 changes: 11 additions & 0 deletions lldb/tools/lldb-dap/DAP.h
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,17 @@ struct DAP {
// the old process here so we can detect this case and keep running.
lldb::pid_t restarting_process_id;
bool configuration_done_sent;

enum class FirstStopState {
NoStopEvent,
PendingStopEvent,
IgnoredStopEvent,
};
std::mutex first_stop_mutex;
std::condition_variable first_stop_cv;
FirstStopState first_stop_state;

bool ignore_next_stop;
llvm::StringMap<std::unique_ptr<BaseRequestHandler>> request_handlers;
bool waiting_for_run_in_terminal;
ProgressEventReporter progress_event_reporter;
Expand Down
33 changes: 23 additions & 10 deletions lldb/tools/lldb-dap/Handler/AttachRequestHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,15 @@ void AttachRequestHandler::operator()(const llvm::json::Object &request) const {
std::string connect_url =
llvm::formatv("connect://{0}:", gdb_remote_hostname);
connect_url += std::to_string(gdb_remote_port);
dap.target.ConnectRemote(listener, connect_url.c_str(), "gdb-remote",
error);
// Connect remote will generate a stopped event even in synchronous
// mode.
{
std::lock_guard<std::mutex> guard(dap.first_stop_mutex);
dap.first_stop_state = DAP::FirstStopState::PendingStopEvent;
dap.target.ConnectRemote(listener, connect_url.c_str(), "gdb-remote",
error);
}
dap.first_stop_cv.notify_one();
} else {
// Attach by process name or id.
dap.target.Attach(attach_info, error);
Expand All @@ -168,15 +175,21 @@ void AttachRequestHandler::operator()(const llvm::json::Object &request) const {
dap.target.LoadCore(core_file.data(), error);
}
} else {
// We have "attachCommands" that are a set of commands that are expected
// to execute the commands after which a process should be created. If there
// is no valid process after running these commands, we have failed.
if (llvm::Error err = dap.RunAttachCommands(attachCommands)) {
response["success"] = false;
EmplaceSafeString(response, "message", llvm::toString(std::move(err)));
dap.SendJSON(llvm::json::Value(std::move(response)));
return;
{
std::lock_guard<std::mutex> guard(dap.first_stop_mutex);
dap.first_stop_state = DAP::FirstStopState::PendingStopEvent;
// We have "attachCommands" that are a set of commands that are expected
// to execute the commands after which a process should be created. If
// there is no valid process after running these commands, we have failed.
if (llvm::Error err = dap.RunAttachCommands(attachCommands)) {
response["success"] = false;
EmplaceSafeString(response, "message", llvm::toString(std::move(err)));
dap.SendJSON(llvm::json::Value(std::move(response)));
return;
}
}
dap.first_stop_cv.notify_one();

// The custom commands might have created a new target so we should use the
// selected target after these commands are run.
dap.target = dap.debugger.GetSelectedTarget();
Expand Down
18 changes: 18 additions & 0 deletions lldb/tools/lldb-dap/Handler/ConfigurationDoneRequestHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,24 @@ void ConfigurationDoneRequestHandler::operator()(
FillResponse(request, response);
dap.SendJSON(llvm::json::Value(std::move(response)));
dap.configuration_done_sent = true;

{
// Temporary unlock the API mutex to avoid a deadlock between the API mutex
// and the first stop mutex.
lock.unlock();

// Block until we have either ignored the fist stop event or we didn't
// generate one because we attached or launched in synchronous mode.
std::unique_lock<std::mutex> stop_lock(dap.first_stop_mutex);
dap.first_stop_cv.wait(stop_lock, [&] {
return dap.first_stop_state == DAP::FirstStopState::NoStopEvent ||
dap.first_stop_state == DAP::FirstStopState::IgnoredStopEvent;
});

// Relock the API mutex.
lock.lock();
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should move this above sending the response to the configurationDone.

The initialized event triggers the rest of the adapter configs (e.g. sending all the breakpoints) and the configurationDone request should be the last in that chain.

In theory, could could even move the dap.WaitForProcessToStop(arguments.timeout); call out of the attach/launch requests and have that here instead and fully expect the attach/launch flow to be async.

if (dap.stop_at_entry)
SendThreadStoppedEvent(dap);
else {
Expand Down
21 changes: 15 additions & 6 deletions lldb/tools/lldb-dap/Handler/InitializeRequestHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -167,14 +167,23 @@ static void EventThreadFunction(DAP &dap) {
// stop events which we do not want to send an event for. We will
// manually send a stopped event in request_configurationDone(...)
// so don't send any before then.
if (dap.configuration_done_sent) {
// Only report a stopped event if the process was not
// automatically restarted.
if (!lldb::SBProcess::GetRestartedFromEvent(event)) {
SendStdOutStdErr(dap, process);
SendThreadStoppedEvent(dap);
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we didn't start the event thread until the configurationDone request was called?

Do we miss any events that would have happened before this point?

Or maybe we don't subscribe to process events until we get the configurationDone request? Could that alleviate the need for this lock?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's not an option. Every process must have an event handler and IIUC (@jimingham should be able to confirm) if you don't handle the event, the process will never get out of the "launching" or "attaching" state.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is true, but it should only matter if someone actually cares about the process in the mean time. Provided we don't need to inspect the state, a sequence like this should be fine:

  1. cause a process event to be emitted
  2. start the listener thread
  3. listener thread receives the event

But I don't know if that's the case here...

Another potential option: If we are sure the event is going to be emitted (and the listener thread is not running), maybe we can wait for the event (synchronously) on the thread which is processing the request.

std::unique_lock<std::mutex> lock(dap.first_stop_mutex);
if (dap.first_stop_state ==
DAP::FirstStopState::PendingStopEvent) {
dap.first_stop_state = DAP::FirstStopState::IgnoredStopEvent;
lock.unlock();
dap.first_stop_cv.notify_one();
continue;
}
}

// Only report a stopped event if the process was not
// automatically restarted.
if (!lldb::SBProcess::GetRestartedFromEvent(event)) {
SendStdOutStdErr(dap, process);
SendThreadStoppedEvent(dap);
}
break;
case lldb::eStateRunning:
dap.WillContinue();
Expand Down
21 changes: 13 additions & 8 deletions lldb/tools/lldb-dap/Handler/RequestHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,12 @@ void BaseRequestHandler::Run(const Request &request) {
return;
}

lldb::SBMutex lock = dap.GetAPIMutex();
std::lock_guard<lldb::SBMutex> guard(lock);

// FIXME: After all the requests have migrated from LegacyRequestHandler >
// RequestHandler<> we should be able to move this into
// RequestHandler<>::operator().
lock.lock();
operator()(request);
lock.unlock();

// FIXME: After all the requests have migrated from LegacyRequestHandler >
// RequestHandler<> we should be able to check `debugger.InterruptRequest` and
Expand Down Expand Up @@ -250,11 +249,17 @@ llvm::Error BaseRequestHandler::LaunchProcess(
if (error.Fail())
return llvm::make_error<DAPError>(error.GetCString());
} else {
// Set the launch info so that run commands can access the configured
// launch details.
dap.target.SetLaunchInfo(launch_info);
if (llvm::Error err = dap.RunLaunchCommands(launchCommands))
return err;
{
std::lock_guard<std::mutex> guard(dap.first_stop_mutex);
dap.first_stop_state = DAP::FirstStopState::PendingStopEvent;

// Set the launch info so that run commands can access the configured
// launch details.
dap.target.SetLaunchInfo(launch_info);
if (llvm::Error err = dap.RunLaunchCommands(launchCommands))
return err;
}
dap.first_stop_cv.notify_all();

// The custom commands might have created a new target so we should use the
// selected target after these commands are run.
Expand Down
5 changes: 4 additions & 1 deletion lldb/tools/lldb-dap/Handler/RequestHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ struct DAP;
/// the RequestHandler template subclass instead.
class BaseRequestHandler {
public:
BaseRequestHandler(DAP &dap) : dap(dap) {}
BaseRequestHandler(DAP &dap)
: dap(dap), mutex(dap.GetAPIMutex()), lock(mutex, std::defer_lock) {}

/// BaseRequestHandler are not copyable.
/// @{
Expand Down Expand Up @@ -80,6 +81,8 @@ class BaseRequestHandler {
/// @}

DAP &dap;
lldb::SBMutex mutex;
mutable std::unique_lock<lldb::SBMutex> lock;
};

/// FIXME: Migrate callers to typed RequestHandler for improved type handling.
Expand Down
Loading