From 5ed72776d8743091cded734f88e642d33df10444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Mon, 2 Dec 2024 01:02:14 -0300 Subject: [PATCH] Sketching debugger protocol. --- include/eepp/system/process.hpp | 9 + src/eepp/system/process.cpp | 7 + src/tools/ecode/plugins/debugger/bus.cpp | 7 + src/tools/ecode/plugins/debugger/bus.hpp | 16 + .../ecode/plugins/debugger/busprocess.cpp | 19 + .../ecode/plugins/debugger/busprocess.hpp | 20 + .../ecode/plugins/debugger/bussocket.cpp | 20 + .../ecode/plugins/debugger/bussocket.hpp | 20 + src/tools/ecode/plugins/debugger/config.cpp | 7 + src/tools/ecode/plugins/debugger/config.hpp | 47 ++ .../ecode/plugins/debugger/dap/messages.hpp | 99 ++++ .../ecode/plugins/debugger/dap/protocol.cpp | 418 +++++++++++++++ .../ecode/plugins/debugger/dap/protocol.hpp | 477 ++++++++++++++++++ .../ecode/plugins/debugger/debuggerclient.hpp | 38 ++ 14 files changed, 1204 insertions(+) create mode 100644 src/tools/ecode/plugins/debugger/bus.cpp create mode 100644 src/tools/ecode/plugins/debugger/bus.hpp create mode 100644 src/tools/ecode/plugins/debugger/busprocess.cpp create mode 100644 src/tools/ecode/plugins/debugger/busprocess.hpp create mode 100644 src/tools/ecode/plugins/debugger/bussocket.cpp create mode 100644 src/tools/ecode/plugins/debugger/bussocket.hpp create mode 100644 src/tools/ecode/plugins/debugger/config.cpp create mode 100644 src/tools/ecode/plugins/debugger/config.hpp create mode 100644 src/tools/ecode/plugins/debugger/dap/messages.hpp create mode 100644 src/tools/ecode/plugins/debugger/dap/protocol.cpp create mode 100644 src/tools/ecode/plugins/debugger/dap/protocol.hpp create mode 100644 src/tools/ecode/plugins/debugger/debuggerclient.hpp diff --git a/include/eepp/system/process.hpp b/include/eepp/system/process.hpp index 0afae89a9..57ee165ed 100644 --- a/include/eepp/system/process.hpp +++ b/include/eepp/system/process.hpp @@ -53,6 +53,15 @@ class EE_API Process { const std::unordered_map& environment = {}, const std::string& workingDirectory = "", const size_t& bufferSize = 132072 ); + /** @brief Create a process. + ** @param command Command line to execute for this process. + ** @param args Command line arguments + ** @param options A bit field of Options's to pass. */ + Process( const std::string& command, const std::vector& args, + Uint32 options = getDefaultOptions(), + const std::unordered_map& environment = {}, + const std::string& workingDirectory = "", const size_t& bufferSize = 132072 ); + ~Process(); /** @brief Create a process. diff --git a/src/eepp/system/process.cpp b/src/eepp/system/process.cpp index eee880d73..3d9dd7e07 100644 --- a/src/eepp/system/process.cpp +++ b/src/eepp/system/process.cpp @@ -87,6 +87,13 @@ Process::Process( const std::string& command, Uint32 options, create( command, options, environment, workingDirectory ); } +Process::Process( const std::string& command, const std::vector& args, Uint32 options, + const std::unordered_map& environment, + const std::string& workingDirectory, const size_t& bufferSize ) : + mBufferSize( bufferSize ) { + create( command, args, options, environment, workingDirectory ); +} + Process::~Process() { mShuttingDown = true; if ( mProcess ) { diff --git a/src/tools/ecode/plugins/debugger/bus.cpp b/src/tools/ecode/plugins/debugger/bus.cpp new file mode 100644 index 000000000..56ebf9461 --- /dev/null +++ b/src/tools/ecode/plugins/debugger/bus.cpp @@ -0,0 +1,7 @@ +#include "bus.hpp" + +namespace ecode { + + + +} diff --git a/src/tools/ecode/plugins/debugger/bus.hpp b/src/tools/ecode/plugins/debugger/bus.hpp new file mode 100644 index 000000000..ad2af5896 --- /dev/null +++ b/src/tools/ecode/plugins/debugger/bus.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "config.hpp" + +namespace ecode { + +class Bus { + public: + typedef std::function ReadFn; + + virtual void startAsyncRead( ReadFn readFn ) = 0; + + virtual size_t write( const char* buffer, const size_t& size ) = 0; +}; + +} // namespace ecode diff --git a/src/tools/ecode/plugins/debugger/busprocess.cpp b/src/tools/ecode/plugins/debugger/busprocess.cpp new file mode 100644 index 000000000..49812a583 --- /dev/null +++ b/src/tools/ecode/plugins/debugger/busprocess.cpp @@ -0,0 +1,19 @@ +#include "busprocess.hpp" + +namespace ecode { + +BusProcess::BusProcess( const Command& command ) : + mProcess( command.command, command.arguments, + Process::getDefaultOptions() | Process::Options::EnableAsync | + Process::Options::CombinedStdoutStderr, + command.environment ) {} + +void BusProcess::startAsyncRead( ReadFn readFn ) { + mProcess.startAsyncRead( readFn, readFn ); +} + +size_t BusProcess::write( const char* buffer, const size_t& size ) { + return mProcess.write( buffer, size ); +} + +} // namespace ecode diff --git a/src/tools/ecode/plugins/debugger/busprocess.hpp b/src/tools/ecode/plugins/debugger/busprocess.hpp new file mode 100644 index 000000000..b0bd64824 --- /dev/null +++ b/src/tools/ecode/plugins/debugger/busprocess.hpp @@ -0,0 +1,20 @@ +#include "bus.hpp" +#include + +using namespace EE::System; + +namespace ecode { + +class BusProcess : public Bus { + public: + BusProcess( const Command& command ); + + virtual void startAsyncRead( ReadFn readFn ); + + virtual size_t write( const char* buffer, const size_t& size ); + + protected: + Process mProcess; +}; + +} // namespace ecode diff --git a/src/tools/ecode/plugins/debugger/bussocket.cpp b/src/tools/ecode/plugins/debugger/bussocket.cpp new file mode 100644 index 000000000..b9496f6bb --- /dev/null +++ b/src/tools/ecode/plugins/debugger/bussocket.cpp @@ -0,0 +1,20 @@ +#include "bussocket.hpp" +#include + +namespace ecode { + +BusSocket::BusSocket( const Connection& connection ) { + mSocket.connect( IpAddress( connection.host ), connection.port ); +} + +void BusSocket::startAsyncRead( ReadFn readFn ) { + mSocket.startAsyncRead( readFn ); +} + +size_t BusSocket::write( const char* buffer, const size_t& size ) { + size_t sent = 0; + mSocket.send( buffer, size, sent ); + return sent; +} + +} // namespace ecode diff --git a/src/tools/ecode/plugins/debugger/bussocket.hpp b/src/tools/ecode/plugins/debugger/bussocket.hpp new file mode 100644 index 000000000..0c2139d08 --- /dev/null +++ b/src/tools/ecode/plugins/debugger/bussocket.hpp @@ -0,0 +1,20 @@ +#include "bus.hpp" +#include + +using namespace EE::Network; + +namespace ecode { + +class BusSocket : public Bus { + public: + BusSocket( const Connection& connection ); + + void startAsyncRead( ReadFn readFn ); + + size_t write( const char* buffer, const size_t& size ); + + protected: + TcpSocket mSocket; +}; + +} // namespace ecode diff --git a/src/tools/ecode/plugins/debugger/config.cpp b/src/tools/ecode/plugins/debugger/config.cpp new file mode 100644 index 000000000..4b842a6fe --- /dev/null +++ b/src/tools/ecode/plugins/debugger/config.cpp @@ -0,0 +1,7 @@ +#include "config.hpp" + +namespace ecode { + + + +} diff --git a/src/tools/ecode/plugins/debugger/config.hpp b/src/tools/ecode/plugins/debugger/config.hpp new file mode 100644 index 000000000..339d8b39c --- /dev/null +++ b/src/tools/ecode/plugins/debugger/config.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include + +#include + +using json = nlohmann::json; + +using namespace EE; + +namespace ecode { + +struct Command { + std::string command; + std::vector arguments; + std::unordered_map environment; +}; + +struct Connection { + int port; + std::string host; +}; + +struct BusSettings { + std::optional command; + std::optional connection; + + bool isValid() const; + bool hasCommand() const; + bool hasConnection() const; +}; + +struct ProtocolSettings { + bool linesStartAt1; + bool columnsStartAt1; + bool pathFormatURI; + bool redirectStderr; + bool redirectStdout; + bool supportsSourceRequest; + json launchRequest; + std::string locale; +}; + +} // namespace ecode diff --git a/src/tools/ecode/plugins/debugger/dap/messages.hpp b/src/tools/ecode/plugins/debugger/dap/messages.hpp new file mode 100644 index 000000000..0e9ad31af --- /dev/null +++ b/src/tools/ecode/plugins/debugger/dap/messages.hpp @@ -0,0 +1,99 @@ +#pragma once + +#include + +using namespace std::literals; + +#define DAP_CONTENT_LENGTH "Content-Length" +#define DAP_SEP "\r\n" + +namespace ecode::dap { + +constexpr int DAP_SEP_SIZE = 2; +static const auto DAP_TPL_HEADER_FIELD = "%1: %2" DAP_SEP; + +static const auto DAP_SEQ = "seq"sv; +static const auto DAP_TYPE = "type"sv; +static const auto DAP_COMMAND = "command"sv; +static const auto DAP_ARGUMENTS = "arguments"sv; +static const auto DAP_BODY = "body"sv; + +// capabilities +static const auto DAP_ADAPTER_ID = "adapterID"sv; +static const auto DAP_LINES_START_AT1 = "linesStartAt1"sv; +static const auto DAP_COLUMNS_START_AT2 = "columnsStartAt1"sv; +static const auto DAP_PATH_FORMAT = "pathFormat"sv; +static const auto DAP_SUPPORTS_VARIABLE_TYPE = "supportsVariableType"sv; +static const auto DAP_SUPPORTS_VARIABLE_PAGING = "supportsVariablePaging"sv; +static const auto DAP_SUPPORTS_RUN_IN_TERMINAL_REQUEST = "supportsRunInTerminalRequest"sv; +static const auto DAP_SUPPORTS_MEMORY_REFERENCES = "supportsMemoryReferences"sv; +static const auto DAP_SUPPORTS_PROGRESS_REPORTING = "supportsProgressReporting"sv; +static const auto DAP_SUPPORTS_INVALIDATED_EVENT = "supportsInvalidatedEvent"sv; +static const auto DAP_SUPPORTS_MEMORY_EVENT = "supportsMemoryEvent"sv; + +// pathFormat values +static const auto DAP_URI = "uri"sv; +static const auto DAP_PATH = "path"sv; + +// type values +static const auto DAP_REQUEST = "request"sv; +static const auto DAP_EVENT = "event"sv; +static const auto DAP_RESPONSE = "response"sv; + +// command values +static const auto DAP_INITIALIZE = "initialize"sv; +static const auto DAP_LAUNCH = "launch"sv; +static const auto DAP_ATTACH = "attach"sv; +static const auto DAP_MODULES = "modules"sv; +static const auto DAP_VARIABLES = "variables"sv; +static const auto DAP_SCOPES = "scopes"sv; +static const auto DAP_THREADS = "threads"sv; + +// event values +static const auto DAP_OUTPUT = "output"sv; + +// fields +static const auto DAP_NAME = "name"sv; +static const auto DAP_SYSTEM_PROCESS_ID = "systemProcessId"sv; +static const auto DAP_IS_LOCAL_PROCESS = "isLocalProcess"sv; +static const auto DAP_POINTER_SIZE = "pointerSize"sv; +static const auto DAP_START_METHOD = "startMethod"sv; +static const auto DAP_DATA = "data"sv; +static const auto DAP_VARIABLES_REFERENCE = "variablesReference"sv; +static const auto DAP_SOURCE = "source"sv; +static const auto DAP_GROUP = "group"sv; +static const auto DAP_LINE = "line"sv; +static const auto DAP_COLUMN = "column"sv; +static const auto DAP_PRESENTATION_HINT = "presentationHint"sv; +static const auto DAP_SOURCES = "sources"sv; +static const auto DAP_CHECKSUMS = "checksums"sv; +static const auto DAP_CATEGORY = "category"sv; +static const auto DAP_THREAD_ID = "threadId"sv; +static const auto DAP_ID = "id"sv; +static const auto DAP_MODULE_ID = "moduleId"sv; +static const auto DAP_REASON = "reason"sv; +static const auto DAP_FRAME_ID = "frameId"sv; +static const auto DAP_FILTER = "filter"sv; +static const auto DAP_START = "start"sv; +static const auto DAP_COUNT = "count"sv; +static const auto DAP_SINGLE_THREAD = "singleThread"sv; +static const auto DAP_ALL_THREADS_CONTINUED = "allThreadsContinued"sv; +static const auto DAP_SOURCE_REFERENCE = "sourceReference"sv; +static const auto DAP_BREAKPOINTS = "breakpoints"sv; +static const auto DAP_ADAPTER_DATA = "adapterData"sv; +static const auto DAP_CONDITION = "condition"sv; +static const auto DAP_HIT_CONDITION = "hitCondition"sv; +static const auto DAP_LOG_MESSAGE = "logMessage"sv; +static const auto DAP_LINES = "lines"sv; +static const auto DAP_ORIGIN = "origin"sv; +static const auto DAP_CHECKSUM = "checksum"sv; +static const auto DAP_ALGORITHM = "algorithm"sv; +static const auto DAP_BREAKPOINT = "breakpoint"sv; +static const auto DAP_EXPRESSION = "expression"sv; +static const auto DAP_CONTEXT = "context"sv; +static const auto DAP_RESULT = "result"sv; +static const auto DAP_TARGET_ID = "targetId"sv; +static const auto DAP_END_LINE = "endLine"sv; +static const auto DAP_END_COLUMN = "endColumn"sv; + +} // namespace dap diff --git a/src/tools/ecode/plugins/debugger/dap/protocol.cpp b/src/tools/ecode/plugins/debugger/dap/protocol.cpp new file mode 100644 index 000000000..9df7f9959 --- /dev/null +++ b/src/tools/ecode/plugins/debugger/dap/protocol.cpp @@ -0,0 +1,418 @@ +#include "messages.hpp" +#include "protocol.hpp" +#include +#include +#include + +using namespace EE; +using namespace EE::System; + +std::optional parseOptionalInt( const json& value ) { + if ( value.is_null() || value.empty() || !value.is_number() ) { + return std::nullopt; + } + return value.get(); +} + +std::optional parseOptionalBool( const json& value ) { + if ( value.is_null() || value.empty() || !value.is_boolean() ) { + return std::nullopt; + } + return value.get(); +} + +std::optional parseOptionalString( const json& value ) { + if ( value.is_null() || value.empty() || !value.is_string() ) { + return std::nullopt; + } + return value.get(); +} + +template std::optional parseOptionalObject( const json& value ) { + if ( value.is_null() || value.empty() || !value.is_object() ) { + return std::nullopt; + } + return T( value ); +} + +std::optional> +parseOptionalStringMap( const json& value ) { + if ( value.is_null() || value.empty() || !value.is_object() ) { + return std::nullopt; + } + const auto& dict = value; + std::unordered_map map; + for ( auto it = dict.begin(); it != dict.end(); ++it ) { + map[it.key()] = it.value().get(); + } + return map; +} + +template std::vector parseObjectList( const json& array ) { + std::vector out; + for ( const auto& item : array ) { + out.emplace_back( T( item ) ); + } + return out; +} + +std::optional> parseOptionalIntList( const json& value ) { + if ( value.is_null() || value.empty() || !value.is_array() ) { + return std::nullopt; + } + std::vector values; + for ( const auto& item : value ) { + values.push_back( item.get() ); + } + return values; +} + +template json toJsonArray( const std::vector& items ) { + json out = json::array(); + for ( const auto& item : items ) { + out.emplace_back( item.toJson() ); + } + return out; +} + +namespace ecode::dap { + +Message::Message( const json& body ) : + id( body[DAP_ID].get() ), + format( body["format"].get() ), + variables( parseOptionalStringMap( body["variables"] ) ), + sendTelemetry( parseOptionalBool( body["sendTelemetry"] ) ), + showUser( parseOptionalBool( body["showUser"] ) ), + url( parseOptionalString( body["url"] ) ), + urlLabel( parseOptionalString( body["urlLabel"] ) ) {} + +Response::Response( const json& msg ) : + request_seq( msg.value( "request_seq", -1 ) ), + success( msg.value( "success", false ) ), + command( msg.value( DAP_COMMAND, "" ) ), + message( msg.value( "message", "" ) ), + body( msg[DAP_BODY] ), + errorBody( success ? std::nullopt : parseOptionalObject( body["error"] ) ) {} + +bool Response::isCancelled() const { + return message == "cancelled"; +} + +ProcessInfo::ProcessInfo( const json& body ) : + name( body.value( DAP_NAME, "" ) ), + systemProcessId( parseOptionalInt( body[DAP_SYSTEM_PROCESS_ID] ) ), + isLocalProcess( parseOptionalBool( body[DAP_IS_LOCAL_PROCESS] ) ), + startMethod( parseOptionalString( body[DAP_START_METHOD] ) ), + pointerSize( parseOptionalInt( body[DAP_POINTER_SIZE] ) ) {} + +Output::Output( const json& body ) : + category( Category::Unknown ), + output( body.value( DAP_OUTPUT, "" ) ), + group( std::nullopt ), + variablesReference( parseOptionalInt( body[DAP_VARIABLES_REFERENCE] ) ), + source( parseOptionalObject( DAP_SOURCE ) ), + line( parseOptionalInt( body[DAP_LINE] ) ), + column( parseOptionalInt( body[DAP_COLUMN] ) ), + data( body[DAP_DATA] ) { + if ( body.contains( DAP_GROUP ) ) { + const auto value = body[DAP_GROUP].get(); + if ( DAP_START == value ) { + group = Group::Start; + } else if ( "startCollapsed" == value ) { + group = Group::StartCollapsed; + } else if ( "end" == value ) { + group = Group::End; + } + } + if ( body.contains( DAP_CATEGORY ) ) { + const auto value = body[DAP_CATEGORY].get(); + if ( "console" == value ) { + category = Category::Console; + } else if ( "important" == value ) { + category = Category::Important; + } else if ( "stdout" == value ) { + category = Category::Stdout; + } else if ( "stderr" == value ) { + category = Category::Stderr; + } else if ( "telemetry" == value ) { + category = Category::Telemetry; + } + } +} + +Output::Output( const std::string& output, const Output::Category& category ) : + category( category ), output( output ) {} + +bool Output::isSpecialOutput() const { + return ( category != Category::Stderr ) && ( category != Category::Stdout ); +} + +std::string Source::unifiedId() const { + return getUnifiedId( path, sourceReference ); +} + +std::string Source::getUnifiedId( const std::string& path, std::optional sourceReference ) { + if ( sourceReference.value_or( 0 ) > 0 ) { + return String::toString( *sourceReference ); + } + return path; +} + +Source::Source( const json& body ) : + name( body[DAP_NAME].get() ), + path( body[DAP_PATH].get() ), + sourceReference( parseOptionalInt( body[DAP_SOURCE_REFERENCE] ) ), + presentationHint( parseOptionalString( body[DAP_PRESENTATION_HINT] ) ), + origin( body[DAP_ORIGIN].get() ), + adapterData( body[DAP_ADAPTER_DATA] ) { + // sources + if ( body.contains( DAP_SOURCES ) ) { + const auto values = body[DAP_SOURCES]; + for ( const auto& item : values ) { + sources.emplace_back( Source( item ) ); + } + } + + // checksums + if ( body.contains( DAP_CHECKSUMS ) ) { + const auto values = body[DAP_CHECKSUMS]; + for ( const auto& item : values ) { + checksums.emplace_back( Checksum( item ) ); + } + } +} + +Source::Source( const std::string& path ) : path( path ) {} + +json Source::toJson() const { + json out; + if ( !name.empty() ) { + out[DAP_NAME] = name; + } + if ( !path.empty() ) { + out[DAP_PATH] = path; + } + if ( sourceReference ) { + out[DAP_SOURCE_REFERENCE] = *sourceReference; + } + if ( presentationHint ) { + out[DAP_PRESENTATION_HINT] = *presentationHint; + } + if ( !origin.empty() ) { + out[DAP_ORIGIN] = origin; + } + if ( !adapterData.is_null() && !adapterData.empty() ) { + out[DAP_ADAPTER_DATA] = adapterData; + } + if ( !sources.empty() ) { + out[DAP_SOURCES] = toJsonArray( sources ); + } + if ( !checksums.empty() ) { + out[DAP_CHECKSUMS] = toJsonArray( checksums ); + } + return out; +} + +Checksum::Checksum( const json& body ) : + checksum( body[DAP_CHECKSUM].get() ), + algorithm( body[DAP_ALGORITHM].get() ) {} + +json Checksum::toJson() const { + json out; + out[DAP_CHECKSUM] = checksum; + out[DAP_ALGORITHM] = algorithm; + return out; +} + +Capabilities::Capabilities( const json& body ) : + supportsConfigurationDoneRequest( body["supportsConfigurationDoneRequest"].get() ), + supportsFunctionBreakpoints( body["supportsFunctionBreakpoints"].get() ), + supportsConditionalBreakpoints( body["supportsConditionalBreakpoints"].get() ), + supportsHitConditionalBreakpoints( body["supportsHitConditionalBreakpoints"].get() ), + supportsLogPoints( body["supportsLogPoints"].get() ), + supportsModulesRequest( body["supportsModulesRequest"].get() ), + supportsTerminateRequest( body["supportsTerminateRequest"].get() ), + supportTerminateDebuggee( body["supportTerminateDebuggee"].get() ), + supportsGotoTargetsRequest( body["supportsGotoTargetsRequest"].get() ) {} + +ThreadEvent::ThreadEvent( const json& body ) : + reason( body[DAP_REASON].get() ), threadId( body[DAP_THREAD_ID].get() ) {} + +StoppedEvent::StoppedEvent( const json& body ) : + reason( body[DAP_REASON].get() ), + description( parseOptionalString( body["description"] ) ), + threadId( body[DAP_THREAD_ID].get() ), + preserveFocusHint( parseOptionalBool( body["preserveFocusHint"] ) ), + text( parseOptionalString( body["text"] ) ), + allThreadsStopped( parseOptionalBool( body["allThreadsStopped"] ) ), + hitBreakpointsIds( parseOptionalIntList( body["hitBreakpointsIds"] ) ) {} + +Thread::Thread( const json& body ) : + id( body[DAP_ID].get() ), name( body[DAP_NAME].get() ) {} + +Thread::Thread( const int id ) : id( id ), name( std::string() ) {} + +std::vector Thread::parseList( const json& threads ) { + return parseObjectList( threads ); +} + +StackFrame::StackFrame( const json& body ) : + id( body[DAP_ID].get() ), + name( body[DAP_NAME].get() ), + source( parseOptionalObject( body[DAP_SOURCE] ) ), + line( body[DAP_LINE].get() ), + column( body[DAP_COLUMN].get() ), + endLine( parseOptionalInt( body["endLine"] ) ), + canRestart( parseOptionalBool( ( body["canRestart"] ) ) ), + instructionPointerReference( parseOptionalString( body["instructionPointerReference"] ) ), + moduleId_int( parseOptionalInt( body[DAP_MODULE_ID] ) ), + moduleId_str( parseOptionalString( body[DAP_MODULE_ID] ) ), + presentationHint( parseOptionalString( body[DAP_PRESENTATION_HINT] ) ) {} + +StackTraceInfo::StackTraceInfo( const json& body ) : + stackFrames( parseObjectList( body["stackFrames"] ) ), + totalFrames( parseOptionalInt( body["totalFrames"] ) ) {} + +Module::Module( const json& body ) : + id_int( parseOptionalInt( body[DAP_ID] ) ), + id_str( parseOptionalString( body[DAP_ID] ) ), + name( body[DAP_NAME].get() ), + path( parseOptionalString( body[DAP_PATH] ) ), + isOptimized( parseOptionalBool( body["isOptimized"] ) ), + isUserCode( parseOptionalBool( body["isUserCode"] ) ), + version( parseOptionalString( body["version"] ) ), + symbolStatus( parseOptionalString( body["symbolStatus"] ) ), + symbolFilePath( parseOptionalString( body["symbolFilePath"] ) ), + dateTimeStamp( parseOptionalString( body["dateTimeStamp"] ) ), + addressRange( parseOptionalString( body["addressRange"] ) ) {} + +ModuleEvent::ModuleEvent( const json& body ) : + reason( body[DAP_REASON].get() ), module( Module( body["module"] ) ) {} + +Scope::Scope( const json& body ) : + name( body[DAP_NAME].get() ), + presentationHint( parseOptionalString( body[DAP_PRESENTATION_HINT] ) ), + variablesReference( body[DAP_VARIABLES_REFERENCE].get() ), + namedVariables( parseOptionalInt( body["namedVariables"] ) ), + indexedVariables( parseOptionalInt( body["indexedVariables"] ) ), + expensive( parseOptionalBool( body["expensive"] ) ), + source( parseOptionalObject( body["source"] ) ), + line( parseOptionalInt( body["line"] ) ), + column( parseOptionalInt( body["column"] ) ), + endLine( parseOptionalInt( body["endLine"] ) ), + endColumn( parseOptionalInt( body["endColumn"] ) ) {} + +Scope::Scope( int variablesReference, std::string name ) : + name( name ), variablesReference( variablesReference ) {} + +std::vector Scope::parseList( const json& scopes ) { + return parseObjectList( scopes ); +} + +Variable::Variable( const json& body ) : + name( body[DAP_NAME].get() ), + value( body["value"].get() ), + type( parseOptionalString( body[DAP_TYPE].get() ) ), + evaluateName( parseOptionalString( body["evaluateName"].get() ) ), + variablesReference( body[DAP_VARIABLES_REFERENCE].get() ), + namedVariables( parseOptionalInt( body["namedVariables"] ) ), + indexedVariables( parseOptionalInt( body["indexedVariables"] ) ), + memoryReference( parseOptionalString( body["memoryReference"] ) ) {} + +Variable::Variable( const std::string& name, const std::string& value, const int reference ) : + name( name ), value( value ), variablesReference( reference ) {} + +std::vector Variable::parseList( const json& variables ) { + return parseObjectList( variables ); +} + +ModulesInfo::ModulesInfo( const json& body ) : + modules( parseObjectList( body[DAP_MODULES] ) ), + totalModules( parseOptionalInt( body["totalModules"] ) ) {} + +ContinuedEvent::ContinuedEvent( const json& body ) : + threadId( body[DAP_THREAD_ID].get() ), + allThreadsContinued( parseOptionalBool( body[DAP_ALL_THREADS_CONTINUED] ) ) {} + +ContinuedEvent::ContinuedEvent( int threadId, bool allThreadsContinued ) : + threadId( threadId ), allThreadsContinued( allThreadsContinued ) {} + +SourceContent::SourceContent( const json& body ) : + content( body["content"].get() ), + mimeType( parseOptionalString( body["mimeType"] ) ) {} + +SourceContent::SourceContent( const std::string& path ) { + const FileInfo file( path ); + if ( file.isRegularFile() && file.exists() ) { + std::string contents; + FileSystem::fileGet( path, contents ); + } +} + +SourceBreakpoint::SourceBreakpoint( const json& body ) : + line( body[DAP_LINE].get() ), + column( parseOptionalInt( body[DAP_COLUMN] ) ), + condition( parseOptionalString( body[DAP_CONDITION] ) ), + hitCondition( parseOptionalString( body[DAP_HIT_CONDITION] ) ), + logMessage( parseOptionalString( body["logMessage"] ) ) {} + +SourceBreakpoint::SourceBreakpoint( const int line ) : line( line ) {} + +json SourceBreakpoint::toJson() const { + json out; + out[DAP_LINE] = line; + if ( condition ) { + out[DAP_CONDITION] = *condition; + } + if ( column ) { + out[DAP_COLUMN] = *column; + } + if ( hitCondition ) { + out[DAP_HIT_CONDITION] = *hitCondition; + } + if ( logMessage ) { + out[DAP_LOG_MESSAGE] = *logMessage; + } + return out; +} + +Breakpoint::Breakpoint( const json& body ) : + id( parseOptionalInt( body[DAP_ID] ) ), + verified( body["verified"].get() ), + message( parseOptionalString( body["message"] ) ), + source( parseOptionalObject( body[DAP_SOURCE] ) ), + line( parseOptionalInt( body[DAP_LINE] ) ), + column( parseOptionalInt( body[DAP_COLUMN] ) ), + endLine( parseOptionalInt( body[DAP_END_LINE] ) ), + endColumn( parseOptionalInt( body[DAP_END_COLUMN] ) ), + instructionReference( parseOptionalString( body["instructionReference"] ) ), + offset( parseOptionalInt( body["offset"] ) ) {} + +Breakpoint::Breakpoint( const int line ) : line( line ) {} + +BreakpointEvent::BreakpointEvent( const json& body ) : + reason( body[DAP_REASON].get() ), + breakpoint( Breakpoint( body[DAP_BREAKPOINT] ) ) {} + +EvaluateInfo::EvaluateInfo( const json& body ) : + result( body[DAP_RESULT].get() ), + type( parseOptionalString( body[DAP_TYPE] ) ), + variablesReference( body[DAP_VARIABLES_REFERENCE].get() ), + namedVariables( parseOptionalInt( body["namedVariables"] ) ), + indexedVariables( parseOptionalInt( body["indexedVariables"] ) ), + memoryReference( parseOptionalString( body["memoryReference"] ) ) {} + +GotoTarget::GotoTarget( const json& body ) : + id( body[DAP_ID].get() ), + label( body["label"].get() ), + line( body[DAP_LINE].get() ), + column( parseOptionalInt( body[DAP_COLUMN] ) ), + endLine( parseOptionalInt( body[DAP_END_LINE] ) ), + endColumn( parseOptionalInt( body[DAP_END_COLUMN] ) ), + instructionPointerReference( parseOptionalString( body["instructionPointerReference"] ) ) {} + +std::vector GotoTarget::parseList( const json& variables ) { + return parseObjectList( variables ); +} + +} // namespace ecode::dap diff --git a/src/tools/ecode/plugins/debugger/dap/protocol.hpp b/src/tools/ecode/plugins/debugger/dap/protocol.hpp new file mode 100644 index 000000000..20e3dde6e --- /dev/null +++ b/src/tools/ecode/plugins/debugger/dap/protocol.hpp @@ -0,0 +1,477 @@ +#pragma once + +#include +#include +#include +#include + +#include + +using json = nlohmann::json; + +namespace ecode::dap { + +struct Message { + /** + * @brief id unique identifier for the message + */ + int id; + /** + * @brief format A format string for the message. + * + * Embedded variables have the form '{name}'. If variable + * name starts with an underscore character, the variable + * does not contain user data (PII) and can be safely used for + * telemetry purposes. + */ + std::string format; + /** + * @brief variables An object used as a dictionary for looking + * up the variables in the format string. + */ + std::optional> variables; + /** + * @brief sendTelemetry If true send telemetry + */ + std::optional sendTelemetry; + /** + * @brief showUser If true show user + */ + std::optional showUser; + /** + * @brief showUser An optional url where additional information + * about this message can be found. + */ + std::optional url; + /** + * @brief urlLabel An optional label that is presented to the user as the UI + * for opening the url. + */ + std::optional urlLabel; + + Message() = default; + Message( const json& body ); +}; + +struct Response { + int request_seq; + bool success; + std::string command; + std::string message; + json body; + std::optional errorBody; + + Response() = default; + Response( const json& msg ); + + bool isCancelled() const; +}; + +struct ProcessInfo { + /** + * @brief name the logical name of the process + * + * This is usually the full path to process's executable file. + */ + std::string name; + /** + * @brief systemProcessId the system process id of the debugged process + * + * This property will be missing for non-system processes. + */ + std::optional systemProcessId; + /** + * @brief isLocalProcess if true, the process is running on the same computer as DA + */ + std::optional isLocalProcess; + /** + * @brief startMethod describes how the debug engine started debugging this process. + */ + std::optional startMethod; + /** + * @brief pointerSize the sized of a pointer or address for this process, in bits. + * + * This value may be used by clients when formatting addresses for display. + */ + std::optional pointerSize; + + ProcessInfo() = default; + ProcessInfo( const json& body ); +}; + +struct Checksum { + std::string checksum; + std::string algorithm; + + Checksum() = default; + Checksum( const json& body ); + + json toJson() const; +}; + +struct Source { + std::string name; + std::string path; + std::optional sourceReference; + std::optional presentationHint; + std::string origin; + std::vector sources; + json adapterData; + std::vector checksums; + + std::string unifiedId() const; + static std::string getUnifiedId( const std::string& path, std::optional sourceReference ); + + Source() = default; + Source( const json& body ); + Source( const std::string& path ); + + json toJson() const; +}; + +struct SourceContent { + std::string content; + std::optional mimeType; + + SourceContent() = default; + SourceContent( const json& body ); + SourceContent( const std::string& path ); +}; + +struct SourceBreakpoint { + int line; + std::optional column; + /** + * An optional expression for conditional breakpoints. + * It is only honored by a debug adapter if the capability + * 'supportsConditionalBreakpoints' is true. + */ + std::optional condition; + /** + * An optional expression that controls how many hits of the breakpoint are + * ignored. + * The backend is expected to interpret the expression as needed. + * The attribute is only honored by a debug adapter if the capability + * 'supportsHitConditionalBreakpoints' is true. + */ + std::optional hitCondition; + /** + * If this attribute exists and is non-empty, the backend must not 'break' + * (stop) + * but log the message instead. Expressions within {} are interpolated. + * The attribute is only honored by a debug adapter if the capability + * 'supportsLogPoints' is true. + */ + std::optional logMessage; + + SourceBreakpoint() = default; + SourceBreakpoint( const json& body ); + SourceBreakpoint( const int line ); + + json toJson() const; +}; + +struct Breakpoint { + /** + * An optional identifier for the breakpoint. It is needed if breakpoint + * events are used to update or remove breakpoints. + */ + std::optional id; + /** + * If true breakpoint could be set (but not necessarily at the desired + * location). + */ + bool verified; + /** + * An optional message about the state of the breakpoint. + * This is shown to the user and can be used to explain why a breakpoint could + * not be verified. + */ + std::optional message; + std::optional source; + /** + * The start line of the actual range covered by the breakpoint. + */ + std::optional line; + std::optional column; + std::optional endLine; + std::optional endColumn; + /** + * An optional memory reference to where the breakpoint is set. + */ + std::optional instructionReference; + /** + * An optional offset from the instruction reference. + * This can be negative. + */ + std::optional offset; + + Breakpoint() = default; + Breakpoint( const json& body ); + Breakpoint( const int line ); +}; + +class Output { + public: + enum class Category { Console, Important, Stdout, Stderr, Telemetry, Unknown }; + + enum class Group { + Start, + StartCollapsed, + End, + }; + + Category category; + std::string output; + std::optional group; + std::optional variablesReference; + std::optional source; + std::optional line; + std::optional column; + json data; + + Output() = default; + Output( const json& body ); + Output( const std::string& output, const Category& category ); + + bool isSpecialOutput() const; +}; + +struct Capabilities { + bool supportsConfigurationDoneRequest; + bool supportsFunctionBreakpoints; + bool supportsConditionalBreakpoints; + bool supportsHitConditionalBreakpoints; + bool supportsLogPoints; + bool supportsModulesRequest; + bool supportsTerminateRequest; + bool supportTerminateDebuggee; + bool supportsGotoTargetsRequest; + + Capabilities() = default; + Capabilities( const json& body ); +}; + +struct ThreadEvent { + std::string reason; + int threadId; + + ThreadEvent() = default; + ThreadEvent( const json& body ); +}; + +struct Module { + std::optional id_int; + std::optional id_str; + std::string name; + std::optional path; + std::optional isOptimized; + std::optional isUserCode; + std::optional version; + std::optional symbolStatus; + std::optional symbolFilePath; + std::optional dateTimeStamp; + std::optional addressRange; + + Module() = default; + Module( const json& body ); +}; + +struct ModulesInfo { + std::vector modules; + std::optional totalModules; + + ModulesInfo() = default; + ModulesInfo( const json& body ); +}; + +struct ModuleEvent { + std::string reason; + Module module; + + ModuleEvent() = default; + ModuleEvent( const json& body ); +}; + +struct StoppedEvent { + std::string reason; + std::optional description; + /** + * @brief threadId The thread which was stopped. + */ + std::optional threadId; + /** + * @brief preserverFocusHint A value of true hints to the frontend that + * this event should not change the focus + */ + std::optional preserveFocusHint; + /** + * @brief text Additional information. + */ + std::optional text; + /** + * @brief allThreadsStopped if true, a DA can announce + * that all threads have stopped. + * - The client should use this information to enable that all threads can be + * expanded to access their stacktraces. + * - If the attribute is missing or false, only the thread with the given threadId + * can be expanded. + */ + std::optional allThreadsStopped; + /** + * @brief hitBreakpointsIds ids of the breakpoints that triggered the event + */ + std::optional> hitBreakpointsIds; + + StoppedEvent() = default; + StoppedEvent( const json& body ); +}; + +struct ContinuedEvent { + int threadId; + /** + * If 'allThreadsContinued' is true, a debug adapter can announce that all + * threads have continued. + */ + std::optional allThreadsContinued; + + ContinuedEvent() = default; + ContinuedEvent( int threadId, bool allThreadsContinued ); + ContinuedEvent( const json& body ); +}; + +struct BreakpointEvent { + std::string reason; + Breakpoint breakpoint; + + BreakpointEvent() = default; + BreakpointEvent( const json& body ); +}; + +struct Thread { + int id; + std::string name; + + Thread() = default; + Thread( const json& body ); + explicit Thread( const int id ); + + static std::vector parseList( const json& threads ); +}; + +struct StackFrame { + int id; + std::string name; + std::optional source; + int line; + int column; + std::optional endLine; + std::optional endColumn; + std::optional canRestart; + std::optional instructionPointerReference; + std::optional moduleId_int; + std::optional moduleId_str; + std::optional presentationHint; + + StackFrame() = default; + StackFrame( const json& body ); +}; + +struct StackTraceInfo { + std::vector stackFrames; + std::optional totalFrames; + + StackTraceInfo() = default; + StackTraceInfo( const json& body ); +}; + +struct Scope { + std::string name; + std::optional presentationHint; + int variablesReference; + std::optional namedVariables; + std::optional indexedVariables; + std::optional expensive; + std::optional source; + std::optional line; + std::optional column; + std::optional endLine; + std::optional endColumn; + + Scope() = default; + Scope( const json& body ); + Scope( int variablesReference, std::string name ); + + static std::vector parseList( const json& scopes ); +}; + +struct Variable { + enum Type { Indexed = 1, Named = 2, Both = 3 }; + + std::string name; + std::string value; + std::optional type; + /** + * @brief evaluateName Optional evaluatable name of tihs variable which can be + * passed to the EvaluateRequest to fetch the variable's value + */ + std::optional evaluateName; + /** + * @brief variablesReference if > 0, its children can be retrieved by VariablesRequest + */ + int variablesReference; + /** + * @brief namedVariables number of named child variables + */ + std::optional namedVariables; + /** + * @brief indexedVariables number of indexed child variables + */ + std::optional indexedVariables; + /** + * @brief memoryReference optional memory reference for the variable if the + * variable represents executable code, such as a function pointer. + * Requires 'supportsMemoryReferences' + */ + std::optional memoryReference; + + /** + * the value has changed since the last time + */ + std::optional valueChanged; + + Variable() = default; + Variable( const json& body ); + Variable( const std::string& name, const std::string& value, const int reference = 0 ); + + static std::vector parseList( const json& variables ); +}; + +struct EvaluateInfo { + std::string result; + std::optional type; + int variablesReference; + std::optional namedVariables; + std::optional indexedVariables; + std::optional memoryReference; + + EvaluateInfo(); + EvaluateInfo( const json& body ); +}; + +struct GotoTarget { + int id; + std::string label; + int line; + std::optional column; + std::optional endLine; + std::optional endColumn; + std::optional instructionPointerReference; + + GotoTarget() = default; + GotoTarget( const json& body ); + + static std::vector parseList( const json& variables ); +}; + +} // namespace dap diff --git a/src/tools/ecode/plugins/debugger/debuggerclient.hpp b/src/tools/ecode/plugins/debugger/debuggerclient.hpp new file mode 100644 index 000000000..5c7be5ba4 --- /dev/null +++ b/src/tools/ecode/plugins/debugger/debuggerclient.hpp @@ -0,0 +1,38 @@ + +#include +#include + +namespace ecode { + +class DebuggerClient { + public: + virtual bool hasBreakpoint( const std::string& path, size_t line ) = 0; + + virtual bool addBreakpoint( const std::string& path, size_t line ) = 0; + + virtual bool removeBreakpoint( const std::string& path, size_t line ) = 0; + + virtual bool start() = 0; + + virtual bool attach() = 0; + + virtual bool started() const = 0; + + virtual bool cont() = 0; + + virtual bool stepInto() = 0; + + virtual bool stepOver() = 0; + + virtual bool stepOut() = 0; + + virtual bool halt() = 0; + + virtual bool terminate() = 0; + + virtual bool stopped() = 0; + + virtual bool completed() = 0; +}; + +} // namespace ecode