diff --git a/lldb/tools/lldb-dap/Handler/ExceptionInfoRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/ExceptionInfoRequestHandler.cpp index c1c2adb32a510..fa01a2036e1dd 100644 --- a/lldb/tools/lldb-dap/Handler/ExceptionInfoRequestHandler.cpp +++ b/lldb/tools/lldb-dap/Handler/ExceptionInfoRequestHandler.cpp @@ -7,168 +7,77 @@ //===----------------------------------------------------------------------===// #include "DAP.h" -#include "EventHelper.h" -#include "JSONUtils.h" +#include "DAPError.h" +#include "Protocol/ProtocolRequests.h" +#include "Protocol/ProtocolTypes.h" #include "RequestHandler.h" #include "lldb/API/SBStream.h" +using namespace lldb_dap::protocol; + namespace lldb_dap { -// "ExceptionInfoRequest": { -// "allOf": [ { "$ref": "#/definitions/Request" }, { -// "type": "object", -// "description": "Retrieves the details of the exception that -// caused this event to be raised. Clients should only call this request if -// the corresponding capability `supportsExceptionInfoRequest` is true.", -// "properties": { -// "command": { -// "type": "string", -// "enum": [ "exceptionInfo" ] -// }, -// "arguments": { -// "$ref": "#/definitions/ExceptionInfoArguments" -// } -// }, -// "required": [ "command", "arguments" ] -// }] -// }, -// "ExceptionInfoArguments": { -// "type": "object", -// "description": "Arguments for `exceptionInfo` request.", -// "properties": { -// "threadId": { -// "type": "integer", -// "description": "Thread for which exception information should be -// retrieved." -// } -// }, -// "required": [ "threadId" ] -// }, -// "ExceptionInfoResponse": { -// "allOf": [ { "$ref": "#/definitions/Response" }, { -// "type": "object", -// "description": "Response to `exceptionInfo` request.", -// "properties": { -// "body": { -// "type": "object", -// "properties": { -// "exceptionId": { -// "type": "string", -// "description": "ID of the exception that was thrown." -// }, -// "description": { -// "type": "string", -// "description": "Descriptive text for the exception." -// }, -// "breakMode": { -// "$ref": "#/definitions/ExceptionBreakMode", -// "description": "Mode that caused the exception notification to -// be raised." -// }, -// "details": { -// "$ref": "#/definitions/ExceptionDetails", -// "description": "Detailed information about the exception." -// } -// }, -// "required": [ "exceptionId", "breakMode" ] -// } -// }, -// "required": [ "body" ] -// }] -// } -// "ExceptionDetails": { -// "type": "object", -// "description": "Detailed information about an exception that has -// occurred.", "properties": { -// "message": { -// "type": "string", -// "description": "Message contained in the exception." -// }, -// "typeName": { -// "type": "string", -// "description": "Short type name of the exception object." -// }, -// "fullTypeName": { -// "type": "string", -// "description": "Fully-qualified type name of the exception object." -// }, -// "evaluateName": { -// "type": "string", -// "description": "An expression that can be evaluated in the current -// scope to obtain the exception object." -// }, -// "stackTrace": { -// "type": "string", -// "description": "Stack trace at the time the exception was thrown." -// }, -// "innerException": { -// "type": "array", -// "items": { -// "$ref": "#/definitions/ExceptionDetails" -// }, -// "description": "Details of the exception contained by this exception, -// if any." -// } -// } -// }, -void ExceptionInfoRequestHandler::operator()( - const llvm::json::Object &request) const { - llvm::json::Object response; - FillResponse(request, response); - const auto *arguments = request.getObject("arguments"); - llvm::json::Object body; - lldb::SBThread thread = dap.GetLLDBThread(*arguments); - if (thread.IsValid()) { - auto stopReason = thread.GetStopReason(); - if (stopReason == lldb::eStopReasonSignal) - body.try_emplace("exceptionId", "signal"); - else if (stopReason == lldb::eStopReasonBreakpoint) { - ExceptionBreakpoint *exc_bp = dap.GetExceptionBPFromStopReason(thread); - if (exc_bp) { - EmplaceSafeString(body, "exceptionId", exc_bp->GetFilter()); - EmplaceSafeString(body, "description", exc_bp->GetLabel()); - } else { - body.try_emplace("exceptionId", "exception"); - } +/// Retrieves the details of the exception that caused this event to be raised. +/// +/// Clients should only call this request if the corresponding capability +/// `supportsExceptionInfoRequest` is true. +llvm::Expected +ExceptionInfoRequestHandler::Run(const ExceptionInfoArguments &args) const { + + lldb::SBThread thread = dap.GetLLDBThread(args.threadId); + if (!thread.IsValid()) + return llvm::make_error( + llvm::formatv("Invalid thread id: {}", args.threadId).str()); + + ExceptionInfoResponseBody response; + response.breakMode = eExceptionBreakModeAlways; + const lldb::StopReason stop_reason = thread.GetStopReason(); + switch (stop_reason) { + case lldb::eStopReasonSignal: + response.exceptionId = "signal"; + break; + case lldb::eStopReasonBreakpoint: { + const ExceptionBreakpoint *exc_bp = + dap.GetExceptionBPFromStopReason(thread); + if (exc_bp) { + response.exceptionId = exc_bp->GetFilter(); + response.description = exc_bp->GetLabel(); } else { - body.try_emplace("exceptionId", "exception"); + response.exceptionId = "exception"; } - if (!ObjectContainsKey(body, "description")) { - char description[1024]; - if (thread.GetStopDescription(description, sizeof(description))) { - EmplaceSafeString(body, "description", description); - } + } break; + default: + response.exceptionId = "exception"; + } + + if (response.description.empty()) { + const size_t buffer_size = thread.GetStopDescription(nullptr, 0); + if (buffer_size > 0) { + std::string &buffer = response.description; + buffer.resize(buffer_size); + thread.GetStopDescription(buffer.data(), buffer.size()); } - body.try_emplace("breakMode", "always"); - auto exception = thread.GetCurrentException(); - if (exception.IsValid()) { - llvm::json::Object details; - lldb::SBStream stream; - if (exception.GetDescription(stream)) { - EmplaceSafeString(details, "message", stream.GetData()); - } + } - auto exceptionBacktrace = thread.GetCurrentExceptionBacktrace(); - if (exceptionBacktrace.IsValid()) { - lldb::SBStream stream; - exceptionBacktrace.GetDescription(stream); - for (uint32_t i = 0; i < exceptionBacktrace.GetNumFrames(); i++) { - lldb::SBFrame frame = exceptionBacktrace.GetFrameAtIndex(i); - frame.GetDescription(stream); - } - EmplaceSafeString(details, "stackTrace", stream.GetData()); - } + if (lldb::SBValue exception = thread.GetCurrentException()) { + lldb::SBStream stream; + response.details = ExceptionDetails{}; + if (exception.GetDescription(stream)) { + response.details->message = stream.GetData(); + } + + if (lldb::SBThread exception_backtrace = + thread.GetCurrentExceptionBacktrace()) { + stream.Clear(); + exception_backtrace.GetDescription(stream); - body.try_emplace("details", std::move(details)); + for (uint32_t idx = 0; idx < exception_backtrace.GetNumFrames(); idx++) { + lldb::SBFrame frame = exception_backtrace.GetFrameAtIndex(idx); + frame.GetDescription(stream); + } + response.details->stackTrace = stream.GetData(); } - // auto excInfoCount = thread.GetStopReasonDataCount(); - // for (auto i=0; i> { public: - using LegacyRequestHandler::LegacyRequestHandler; + using RequestHandler::RequestHandler; static llvm::StringLiteral GetCommand() { return "exceptionInfo"; } FeatureSet GetSupportedFeatures() const override { return {protocol::eAdapterFeatureExceptionInfoRequest}; } - void operator()(const llvm::json::Object &request) const override; + llvm::Expected + Run(const protocol::ExceptionInfoArguments &args) const override; }; class InitializeRequestHandler diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp b/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp index b9393356b4e01..e207aad2167d6 100644 --- a/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp +++ b/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp @@ -625,4 +625,22 @@ llvm::json::Value toJSON(const ModuleSymbolsResponseBody &DGMSR) { return result; } +bool fromJSON(const json::Value &Params, ExceptionInfoArguments &Args, + json::Path Path) { + json::ObjectMapper O(Params, Path); + return O && O.map("threadId", Args.threadId); +} + +json::Value toJSON(const ExceptionInfoResponseBody &ERB) { + json::Object result{{"exceptionId", ERB.exceptionId}, + {"breakMode", ERB.breakMode}}; + + if (!ERB.description.empty()) + result.insert({"description", ERB.description.c_str()}); + if (ERB.details.has_value()) + result.insert({"details", *ERB.details}); + + return result; +} + } // namespace lldb_dap::protocol diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolRequests.h b/lldb/tools/lldb-dap/Protocol/ProtocolRequests.h index a85a68b87014c..53e551ac2ec64 100644 --- a/lldb/tools/lldb-dap/Protocol/ProtocolRequests.h +++ b/lldb/tools/lldb-dap/Protocol/ProtocolRequests.h @@ -1039,6 +1039,28 @@ struct ModuleSymbolsResponseBody { }; llvm::json::Value toJSON(const ModuleSymbolsResponseBody &); +struct ExceptionInfoArguments { + /// Thread for which exception information should be retrieved. + lldb::tid_t threadId = LLDB_INVALID_THREAD_ID; +}; +bool fromJSON(const llvm::json::Value &, ExceptionInfoArguments &, + llvm::json::Path); + +struct ExceptionInfoResponseBody { + /// ID of the exception that was thrown. + std::string exceptionId; + + /// Descriptive text for the exception. + std::string description; + + /// Mode that caused the exception notification to be raised. + ExceptionBreakMode breakMode; + + /// Detailed information about the exception. + std::optional details; +}; +llvm::json::Value toJSON(const ExceptionInfoResponseBody &); + } // namespace lldb_dap::protocol #endif diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolTypes.cpp b/lldb/tools/lldb-dap/Protocol/ProtocolTypes.cpp index dc8edaadcd9bb..95007013742a0 100644 --- a/lldb/tools/lldb-dap/Protocol/ProtocolTypes.cpp +++ b/lldb/tools/lldb-dap/Protocol/ProtocolTypes.cpp @@ -1136,4 +1136,37 @@ bool fromJSON(const json::Value &Param, Variable &V, json::Path Path) { Path, /*required=*/false); } +json::Value toJSON(const ExceptionBreakMode Mode) { + switch (Mode) { + case eExceptionBreakModeNever: + return "never"; + case eExceptionBreakModeAlways: + return "always"; + case eExceptionBreakModeUnhandled: + return "unhandled"; + case eExceptionBreakModeUserUnhandled: + return "userUnhandled"; + } + llvm_unreachable("unhandled exception breakMode."); +} + +json::Value toJSON(const ExceptionDetails &ED) { + json::Object result; + + if (!ED.message.empty()) + result.insert({"message", ED.message}); + if (!ED.typeName.empty()) + result.insert({"typeName", ED.typeName}); + if (!ED.fullTypeName.empty()) + result.insert({"fullTypeName", ED.fullTypeName}); + if (!ED.evaluateName.empty()) + result.insert({"evaluateName", ED.evaluateName}); + if (!ED.stackTrace.empty()) + result.insert({"stackTrace", ED.stackTrace}); + if (!ED.innerException.empty()) + result.insert({"innerException", ED.innerException}); + + return result; +} + } // namespace lldb_dap::protocol diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolTypes.h b/lldb/tools/lldb-dap/Protocol/ProtocolTypes.h index 7077df90a85b5..6d85c74377bd3 100644 --- a/lldb/tools/lldb-dap/Protocol/ProtocolTypes.h +++ b/lldb/tools/lldb-dap/Protocol/ProtocolTypes.h @@ -1007,6 +1007,36 @@ struct Variable { llvm::json::Value toJSON(const Variable &); bool fromJSON(const llvm::json::Value &, Variable &, llvm::json::Path); +enum ExceptionBreakMode : unsigned { + eExceptionBreakModeNever, + eExceptionBreakModeAlways, + eExceptionBreakModeUnhandled, + eExceptionBreakModeUserUnhandled, +}; +llvm::json::Value toJSON(ExceptionBreakMode); + +struct ExceptionDetails { + /// Message contained in the exception. + std::string message; + + /// Short type name of the exception object. + std::string typeName; + + /// Fully-qualified type name of the exception object. + std::string fullTypeName; + + /// An expression that can be evaluated in the current scope to obtain the + /// exception object. + std::string evaluateName; + + /// Stack trace at the time the exception was thrown. + std::string stackTrace; + + /// Details of the exception contained by this exception, if any. + std::vector innerException; +}; +llvm::json::Value toJSON(const ExceptionDetails &); + } // namespace lldb_dap::protocol #endif diff --git a/lldb/unittests/DAP/CMakeLists.txt b/lldb/unittests/DAP/CMakeLists.txt index a08414c30e6cd..434f5280a97a0 100644 --- a/lldb/unittests/DAP/CMakeLists.txt +++ b/lldb/unittests/DAP/CMakeLists.txt @@ -7,6 +7,7 @@ add_lldb_unittest(DAPTests Handler/ContinueTest.cpp JSONUtilsTest.cpp LLDBUtilsTest.cpp + ProtocolRequestsTest.cpp ProtocolTypesTest.cpp ProtocolUtilsTest.cpp TestBase.cpp diff --git a/lldb/unittests/DAP/ProtocolRequestsTest.cpp b/lldb/unittests/DAP/ProtocolRequestsTest.cpp new file mode 100644 index 0000000000000..498195dc09325 --- /dev/null +++ b/lldb/unittests/DAP/ProtocolRequestsTest.cpp @@ -0,0 +1,69 @@ +//===-- ProtocolRequestsTest.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 "Protocol/ProtocolRequests.h" +#include "Protocol/ProtocolTypes.h" +#include "TestingSupport/TestUtilities.h" +#include "llvm/Testing/Support/Error.h" +#include + +using namespace llvm; +using namespace lldb_dap::protocol; +using lldb_private::PrettyPrint; +using llvm::json::parse; + +TEST(ProtocolRequestsTest, ExceptionInfoArguments) { + llvm::Expected expected = + parse(R"({ + "threadId": 3434 + })"); + ASSERT_THAT_EXPECTED(expected, llvm::Succeeded()); + EXPECT_EQ(expected->threadId, 3434U); + + // Check required keys; + EXPECT_THAT_EXPECTED(parse(R"({})"), + FailedWithMessage("missing value at (root).threadId")); + + EXPECT_THAT_EXPECTED(parse(R"({"id": 10})"), + FailedWithMessage("missing value at (root).threadId")); +} + +TEST(ProtocolRequestsTest, ExceptionInfoResponseBody) { + ExceptionInfoResponseBody body; + body.exceptionId = "signal"; + body.breakMode = eExceptionBreakModeAlways; + + // Check required keys. + Expected expected = parse( + R"({ + "exceptionId": "signal", + "breakMode": "always" + })"); + + ASSERT_THAT_EXPECTED(expected, llvm::Succeeded()); + EXPECT_EQ(PrettyPrint(*expected), PrettyPrint(body)); + + // Check optional keys. + body.description = "SIGNAL SIGWINCH"; + body.breakMode = eExceptionBreakModeNever; + body.details = ExceptionDetails{}; + body.details->message = "some message"; + + Expected expected_opt = parse( + R"({ + "exceptionId": "signal", + "description": "SIGNAL SIGWINCH", + "breakMode": "never", + "details": { + "message": "some message" + } + })"); + + ASSERT_THAT_EXPECTED(expected_opt, llvm::Succeeded()); + EXPECT_EQ(PrettyPrint(*expected_opt), PrettyPrint(body)); +} diff --git a/lldb/unittests/DAP/ProtocolTypesTest.cpp b/lldb/unittests/DAP/ProtocolTypesTest.cpp index a5ae856a185b7..f892d10d75304 100644 --- a/lldb/unittests/DAP/ProtocolTypesTest.cpp +++ b/lldb/unittests/DAP/ProtocolTypesTest.cpp @@ -1126,3 +1126,50 @@ TEST(ProtocolTypesTest, DataBreakpointInfoArguments) { EXPECT_THAT_EXPECTED(parse(R"({"name":"data"})"), llvm::Succeeded()); } + +TEST(ProtocolTypesTest, ExceptionBreakMode) { + const std::vector> test_cases = + {{ExceptionBreakMode::eExceptionBreakModeAlways, "always"}, + {ExceptionBreakMode::eExceptionBreakModeNever, "never"}, + {ExceptionBreakMode::eExceptionBreakModeUnhandled, "unhandled"}, + {ExceptionBreakMode::eExceptionBreakModeUserUnhandled, "userUnhandled"}}; + + for (const auto [value, expected] : test_cases) { + json::Value const serialized = toJSON(value); + ASSERT_EQ(serialized.kind(), llvm::json::Value::Kind::String); + EXPECT_EQ(serialized.getAsString(), expected); + } +} + +TEST(ProtocolTypesTest, ExceptionDetails) { + ExceptionDetails details; + + // Check required keys. + Expected expected = parse(R"({})"); + ASSERT_THAT_EXPECTED(expected, llvm::Succeeded()); + EXPECT_EQ(pp(*expected), pp(details)); + + // Check optional keys. + details.message = "SIGABRT exception"; + details.typeName = "signal"; + details.fullTypeName = "SIGABRT"; + details.evaluateName = "process handle SIGABRT"; + details.stackTrace = "some stacktrace"; + ExceptionDetails inner_details; + inner_details.message = "inner message"; + details.innerException = {std::move(inner_details)}; + + Expected expected_opt = parse(R"({ + "message": "SIGABRT exception", + "typeName": "signal", + "fullTypeName": "SIGABRT", + "evaluateName": "process handle SIGABRT", + "stackTrace": "some stacktrace", + "innerException": [{ + "message": "inner message" + }] + })"); + + ASSERT_THAT_EXPECTED(expected_opt, llvm::Succeeded()); + EXPECT_EQ(pp(*expected_opt), pp(details)); +} diff --git a/lldb/unittests/TestingSupport/TestUtilities.cpp b/lldb/unittests/TestingSupport/TestUtilities.cpp index b53822e38324b..d164c227afb9e 100644 --- a/lldb/unittests/TestingSupport/TestUtilities.cpp +++ b/lldb/unittests/TestingSupport/TestUtilities.cpp @@ -20,6 +20,11 @@ using namespace lldb_private; extern const char *TestMainArgv0; std::once_flag TestUtilities::g_debugger_initialize_flag; + +std::string lldb_private::PrettyPrint(const llvm::json::Value &value) { + return llvm::formatv("{0:2}", value).str(); +} + std::string lldb_private::GetInputFilePath(const llvm::Twine &name) { llvm::SmallString<128> result = llvm::sys::path::parent_path(TestMainArgv0); llvm::sys::fs::make_absolute(result); diff --git a/lldb/unittests/TestingSupport/TestUtilities.h b/lldb/unittests/TestingSupport/TestUtilities.h index cc93a68a6a431..f05d176618fa0 100644 --- a/lldb/unittests/TestingSupport/TestUtilities.h +++ b/lldb/unittests/TestingSupport/TestUtilities.h @@ -30,6 +30,10 @@ } namespace lldb_private { + +/// Returns a pretty printed json string of a `llvm::json::Value`. +std::string PrettyPrint(const llvm::json::Value &E); + std::string GetInputFilePath(const llvm::Twine &name); class TestUtilities {