diff --git a/src/tools/ecode/plugins/debugger/config.hpp b/src/tools/ecode/plugins/debugger/config.hpp index 339d8b39c..c81f30e6b 100644 --- a/src/tools/ecode/plugins/debugger/config.hpp +++ b/src/tools/ecode/plugins/debugger/config.hpp @@ -7,6 +7,8 @@ #include +using namespace std::literals; + using json = nlohmann::json; using namespace EE; @@ -33,6 +35,10 @@ struct BusSettings { bool hasConnection() const; }; +static const auto REQUEST = "request"sv; +static const auto REDIRECT_STDERR = "redirectStderr"sv; +static const auto REDIRECT_STDOUT = "redirectStdout"sv; + struct ProtocolSettings { bool linesStartAt1; bool columnsStartAt1; @@ -42,6 +48,25 @@ struct ProtocolSettings { bool supportsSourceRequest; json launchRequest; std::string locale; + + ProtocolSettings() : + linesStartAt1( true ), + columnsStartAt1( true ), + pathFormatURI( false ), + redirectStderr( false ), + redirectStdout( false ), + supportsSourceRequest( true ), + locale( "en-US" ) {} + + ProtocolSettings( const nlohmann::json& configuration ) : + linesStartAt1( true ), + columnsStartAt1( true ), + pathFormatURI( false ), + redirectStderr( configuration.value( REDIRECT_STDERR, false ) ), + redirectStdout( configuration.value( REDIRECT_STDOUT, false ) ), + supportsSourceRequest( configuration.value( "supportsSourceRequest", true ) ), + launchRequest( configuration[REQUEST] ), + locale( "en-US" ) {} }; } // namespace ecode diff --git a/src/tools/ecode/plugins/debugger/dap/debuggerclientdap.cpp b/src/tools/ecode/plugins/debugger/dap/debuggerclientdap.cpp index b44c763a9..156133ca7 100644 --- a/src/tools/ecode/plugins/debugger/dap/debuggerclientdap.cpp +++ b/src/tools/ecode/plugins/debugger/dap/debuggerclientdap.cpp @@ -9,25 +9,33 @@ namespace ecode::dap { constexpr int MAX_HEADER_SIZE = 1 << 16; +template +inline DebuggerClientDap::ResponseHandler +makeResponseHandler( void ( T::*member )( const Response& response, const nlohmann::json& request ), + T* object ) { + return [object, member]( const Response& response, const nlohmann::json& request ) { + return ( object->*member )( response, request ); + }; +} + DebuggerClientDap::DebuggerClientDap( std::unique_ptr&& bus ) : mBus( std::move( bus ) ) {} -void DebuggerClientDap::makeRequest( const std::string& command, const nlohmann::json& arguments, - std::function onFinish ) { - nlohmann::json json_cmd = { +void DebuggerClientDap::makeRequest( const std::string_view& command, + const nlohmann::json& arguments, ResponseHandler onFinish ) { + nlohmann::json jsonCmd = { { "seq", mIdx.load() }, { "type", "request" }, { "command", command }, { "arguments", arguments.empty() ? nlohmann::json::object() : arguments } }; - std::string cmd = json_cmd.dump(); + std::string cmd = jsonCmd.dump(); Log::instance()->writel( mDebug ? LogLevel::Info : LogLevel::Debug, cmd ); std::string msg( String::format( "Content-Length: %zu\r\n\r\n%s", cmd.size(), cmd ) ); mBus->write( msg.data(), msg.size() ); - if ( onFinish ) - mCommandQueue[mIdx] = onFinish; + mRequests[mIdx] = { std::string{ command }, arguments, onFinish }; mIdx.fetch_add( 1, std::memory_order_relaxed ); } @@ -49,9 +57,86 @@ bool DebuggerClientDap::start() { if ( started ) mBus->startAsyncRead( [this]( const char* bytes, size_t n ) { asyncRead( bytes, n ); } ); mStarted = started; + return started; } +void DebuggerClientDap::processResponseInitialize( const Response& response, + const nlohmann::json& ) { + if ( mState != State::Initializing ) { + Log::warning( + "DebuggerClientDap::processResponseInitialize: unexpected initialize response" ); + setState( State::None ); + return; + } + + if ( !response.success && response.isCancelled() ) { + Log::warning( "DebuggerClientDap::processResponseInitialize: InitializeResponse error: %s", + response.message ); + if ( response.errorBody ) { + Log::warning( "DebuggerClientDap::processResponseInitialize: error %ld %s", + response.errorBody->id, response.errorBody->format ); + } + setState( State::None ); + return; + } + + // get server capabilities + mAdapterCapabilities = Capabilities( response.body ); + if ( mObserver ) + mObserver->capabilitiesReceived( mAdapterCapabilities ); + + requestLaunchCommand(); +} + +void DebuggerClientDap::processResponseLaunch( const Response& response, const nlohmann::json& ) { + if ( response.success ) { + mLaunched = true; + if ( mObserver ) + mObserver->launched(); + checkRunning(); + } else { + setState( State::Failed ); + } +} + +void DebuggerClientDap::requestLaunchCommand() { + + if ( mState != State::Initializing ) { + Log::warning( + "DebuggerClientDap::requestLaunchCommand: trying to launch in an unexpected state" ); + return; + } + + if ( mLaunchCommand.empty() ) + return; + + makeRequest( mLaunchCommand, mProtocol.launchRequest, + makeResponseHandler( &DebuggerClientDap::processResponseLaunch, this ) ); +} + +void DebuggerClientDap::requestInitialize() { + const nlohmann::json capabilities{ + { DAP_CLIENT_ID, "ecode-dap" }, + { DAP_CLIENT_NAME, "ecode dap" }, + { "locale", mProtocol.locale }, + { DAP_ADAPTER_ID, "qdap" }, + { DAP_LINES_START_AT1, mProtocol.linesStartAt1 }, + { DAP_COLUMNS_START_AT2, mProtocol.columnsStartAt1 }, + { DAP_PATH, ( mProtocol.pathFormatURI ? DAP_URI : DAP_PATH ) }, + { DAP_SUPPORTS_VARIABLE_TYPE, true }, + { DAP_SUPPORTS_VARIABLE_PAGING, false }, + { DAP_SUPPORTS_RUN_IN_TERMINAL_REQUEST, false }, + { DAP_SUPPORTS_MEMORY_REFERENCES, false }, + { DAP_SUPPORTS_PROGRESS_REPORTING, false }, + { DAP_SUPPORTS_INVALIDATED_EVENT, false }, + { DAP_SUPPORTS_MEMORY_EVENT, false } }; + + setState( State::Initializing ); + makeRequest( DAP_INITIALIZE, capabilities, + makeResponseHandler( &DebuggerClientDap::processResponseInitialize, this ) ); +} + void DebuggerClientDap::asyncRead( const char* bytes, size_t n ) { std::string_view read( bytes, n ); mBuffer += read; @@ -96,9 +181,122 @@ void DebuggerClientDap::processProtocolMessage( const nlohmann::json& msg ) { } } -void DebuggerClientDap::processResponse( const nlohmann::json& msg ) {} +void DebuggerClientDap::processResponse( const nlohmann::json& msg ) { + const Response response( msg ); -void DebuggerClientDap::processEvent( const nlohmann::json& msg ) {} + // check sequence + if ( ( response.request_seq < 0 ) || 0 == mRequests.count( response.request_seq ) ) { + Log::error( "DebuggerClientDap::processResponse: unexpected requested seq in response" ); + return; + } + + const auto request = mRequests.extract( response.request_seq ).mapped(); + + // check response + if ( response.command != request.command ) { + Log::error( + "DebuggerClientDap::processResponse: unexpected command in response: %s (expected: %s)", + response.command, request.command ); + } + + if ( response.isCancelled() ) + Log::debug( "DebuggerClientDap::processResponse: request cancelled: %s", response.command ); + + if ( !response.success ) + return errorResponse( response.message, response.errorBody ); + + if ( request.handler ) { + request.handler( response, request.arguments ); + } +} + +void DebuggerClientDap::errorResponse( const std::string& summary, + const std::optional& message ) { + if ( mObserver ) + mObserver->errorResponse( summary, message ); +} + +void DebuggerClientDap::processEvent( const nlohmann::json& msg ) { + const std::string event = msg.value( DAP_EVENT, "" ); + const auto body = msg[DAP_BODY]; + + if ( "initialized"sv == event ) { + processEventInitialized(); + } else if ( "terminated"sv == event ) { + processEventTerminated(); + } else if ( "exited"sv == event ) { + processEventExited( body ); + } else if ( DAP_OUTPUT == event ) { + processEventOutput( body ); + } else if ( "process"sv == event ) { + processEventProcess( body ); + } else if ( "thread"sv == event ) { + processEventThread( body ); + } else if ( "stopped"sv == event ) { + processEventStopped( body ); + } else if ( "module"sv == event ) { + processEventModule( body ); + } else if ( "continued"sv == event ) { + processEventContinued( body ); + } else if ( DAP_BREAKPOINT == event ) { + processEventBreakpoint( body ); + } else { + Log::info( "DebuggerClientDap::processEvent: unsupported event: %s", event ); + } +} + +void DebuggerClientDap::processEventInitialized() { + if ( ( mState != State::Initializing ) ) { + Log::error( "DebuggerClientDap::processEventInitialized: unexpected initialized event" ); + return; + } + setState( State::Initialized ); +} + +void DebuggerClientDap::processEventTerminated() { + setState( State::Terminated ); +} + +void DebuggerClientDap::processEventExited( const nlohmann::json& body ) { + const int exitCode = body.value( "exitCode", -1 ); + if ( mObserver ) + mObserver->debuggeeExited( exitCode ); +} + +void DebuggerClientDap::processEventOutput( const nlohmann::json& body ) { + if ( mObserver ) + mObserver->outputProduced( Output( body ) ); +} + +void DebuggerClientDap::processEventProcess( const nlohmann::json& body ) { + if ( mObserver ) + mObserver->debuggingProcess( ProcessInfo( body ) ); +} + +void DebuggerClientDap::processEventThread( const nlohmann::json& body ) { + if ( mObserver ) + mObserver->threadChanged( ThreadEvent( body ) ); +} + +void DebuggerClientDap::processEventStopped( const nlohmann::json& body ) { + if ( mObserver ) + mObserver->debuggeeStopped( StoppedEvent( body ) ); +} + +void DebuggerClientDap::processEventModule( const nlohmann::json& body ) { + if ( mObserver ) + mObserver->moduleChanged( ModuleEvent( body ) ); +} + +void DebuggerClientDap::processEventContinued( const nlohmann::json& body ) { + if ( mObserver ) + mObserver->debuggeeContinued( ContinuedEvent( body ) ); +} + +void DebuggerClientDap::processEventBreakpoint( const nlohmann::json& body ) { + if ( mObserver ) + mObserver->breakpointChanged( BreakpointEvent( body ) ); +} std::optional DebuggerClientDap::readHeader() { Uint64 length = std::string::npos; @@ -159,7 +357,7 @@ std::optional DebuggerClientDap::readHeader() { if ( length < 0 ) return std::nullopt; - return HeaderInfo{ .payloadStart = end, .payloadLength = length }; + return HeaderInfo{ end, length }; } bool DebuggerClientDap::attach() { diff --git a/src/tools/ecode/plugins/debugger/dap/debuggerclientdap.hpp b/src/tools/ecode/plugins/debugger/dap/debuggerclientdap.hpp index 7e8b39158..25c3ea9dd 100644 --- a/src/tools/ecode/plugins/debugger/dap/debuggerclientdap.hpp +++ b/src/tools/ecode/plugins/debugger/dap/debuggerclientdap.hpp @@ -2,6 +2,7 @@ #include "../bus.hpp" #include "../debuggerclient.hpp" +#include "protocol.hpp" #include #include @@ -11,6 +12,8 @@ namespace ecode::dap { class DebuggerClientDap : public DebuggerClient { public: + typedef std::function ResponseHandler; + DebuggerClientDap( std::unique_ptr&& bus ); bool hasBreakpoint( const std::string& path, size_t line ); @@ -54,11 +57,19 @@ class DebuggerClientDap : public DebuggerClient { Uint64 mThreadId{ 1 }; bool mDebug{ true }; bool mStarted{ false }; - UnorderedMap> mCommandQueue; + struct Request { + std::string command; + nlohmann::json arguments; + ResponseHandler handler; + }; + std::unordered_map mRequests; std::string mBuffer; + ProtocolSettings mProtocol; + Capabilities mAdapterCapabilities; + std::string mLaunchCommand; - void makeRequest( const std::string& command, const nlohmann::json& arguments, - std::function onFinish = nullptr ); + void makeRequest( const std::string_view& command, const nlohmann::json& arguments, + ResponseHandler onFinish = nullptr ); void asyncRead( const char* bytes, size_t n ); @@ -72,7 +83,38 @@ class DebuggerClientDap : public DebuggerClient { Uint64 payloadStart; Uint64 payloadLength; }; + std::optional readHeader(); + + void errorResponse( const std::string& summary, const std::optional& message ); + + void processEventInitialized(); + + void processEventTerminated(); + + void processEventExited( const nlohmann::json& body ); + + void processEventOutput( const nlohmann::json& body ); + + void processEventProcess( const nlohmann::json& body ); + + void processEventThread( const nlohmann::json& body ); + + void processEventStopped( const nlohmann::json& body ); + + void processEventModule( const nlohmann::json& body ); + + void processEventContinued( const nlohmann::json& body ); + + void processEventBreakpoint( const nlohmann::json& body ); + + void requestInitialize(); + + void requestLaunchCommand(); + + void processResponseInitialize( const Response& response, const nlohmann::json& ); + + void processResponseLaunch( const Response& response, const nlohmann::json& ); }; } // namespace ecode::dap diff --git a/src/tools/ecode/plugins/debugger/dap/messages.hpp b/src/tools/ecode/plugins/debugger/dap/messages.hpp index 0e9ad31af..fd2181af9 100644 --- a/src/tools/ecode/plugins/debugger/dap/messages.hpp +++ b/src/tools/ecode/plugins/debugger/dap/messages.hpp @@ -19,6 +19,8 @@ static const auto DAP_ARGUMENTS = "arguments"sv; static const auto DAP_BODY = "body"sv; // capabilities +static const auto DAP_CLIENT_ID = "clientID"sv; +static const auto DAP_CLIENT_NAME = "clientName"sv; 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; diff --git a/src/tools/ecode/plugins/debugger/debuggerclient.cpp b/src/tools/ecode/plugins/debugger/debuggerclient.cpp new file mode 100644 index 000000000..343587af4 --- /dev/null +++ b/src/tools/ecode/plugins/debugger/debuggerclient.cpp @@ -0,0 +1,60 @@ +#include "debuggerclient.hpp" + +namespace ecode { + +void DebuggerClient::stateChanged( State state ) { + if ( mObserver ) + mObserver->stateChanged( state ); +} + +void DebuggerClient::initialized() { + if ( mObserver ) + mObserver->initialized(); +} + +void DebuggerClient::debuggeeRunning() { + if ( mObserver ) + mObserver->debuggeeRunning(); +} + +void DebuggerClient::debuggeeTerminated() { + if ( mObserver ) + mObserver->debuggeeRunning(); +} + +void DebuggerClient::failed() { + if ( mObserver ) + mObserver->failed(); +} + +void DebuggerClient::setState( const State& state ) { + if ( state != mState ) { + mState = state; + stateChanged( mState ); + + switch ( mState ) { + case State::Initialized: + initialized(); + checkRunning(); + break; + case State::Running: + debuggeeRunning(); + break; + case State::Terminated: + debuggeeTerminated(); + break; + case State::Failed: + failed(); + break; + default:; + } + } +} + +void DebuggerClient::checkRunning() { + if ( mLaunched && mConfigured && mState == State::Initialized ) { + setState( State::Running ); + } +} + +} // namespace ecode diff --git a/src/tools/ecode/plugins/debugger/debuggerclient.hpp b/src/tools/ecode/plugins/debugger/debuggerclient.hpp index 5f8c58a52..8bbd8c52f 100644 --- a/src/tools/ecode/plugins/debugger/debuggerclient.hpp +++ b/src/tools/ecode/plugins/debugger/debuggerclient.hpp @@ -1,11 +1,55 @@ - +#pragma once +#include "dap/protocol.hpp" #include #include namespace ecode { +using namespace dap; + class DebuggerClient { public: + enum class State { None, Initializing, Initialized, Running, Terminated, Failed }; + + class Observer { + public: + virtual void stateChanged( State ) = 0; + virtual void initialized() = 0; + virtual void launched() = 0; + virtual void failed() = 0; + virtual void debuggeeRunning() = 0; + virtual void debuggeeTerminated() = 0; + + virtual void capabilitiesReceived( const Capabilities& capabilities ) = 0; + virtual void debuggeeExited( int exitCode ) = 0; + virtual void debuggeeStopped( const StoppedEvent& ) = 0; + virtual void debuggeeContinued( const ContinuedEvent& ) = 0; + virtual void outputProduced( const Output& ) = 0; + virtual void debuggingProcess( const ProcessInfo& ) = 0; + virtual void errorResponse( const std::string& summary, + const std::optional& message ) = 0; + virtual void threadChanged( const ThreadEvent& ) = 0; + virtual void moduleChanged( const ModuleEvent& ) = 0; + virtual void threads( const std::vector& ) = 0; + virtual void stackTrace( const int threadId, const StackTraceInfo& ) = 0; + virtual void scopes( const int frameId, const std::vector& ) = 0; + virtual void variables( const int variablesReference, const std::vector& ) = 0; + virtual void modules( const ModulesInfo& ) = 0; + virtual void serverDisconnected() = 0; + virtual void sourceContent( const std::string& path, int reference = 0, + const SourceContent& content = SourceContent() ) = 0; + virtual void + sourceBreakpoints( const std::string& path, int reference, + const std::optional>& breakpoints ) = 0; + virtual void breakpointChanged( const BreakpointEvent& ) = 0; + virtual void expressionEvaluated( const std::string& expression, + const std::optional& ) = 0; + virtual void gotoTargets( const Source& source, const int line, + const std::vector& targets ) = 0; + }; + + State state() const { return mState; } + virtual bool hasBreakpoint( const std::string& path, size_t line ) = 0; virtual bool addBreakpoint( const std::string& path, size_t line ) = 0; @@ -39,6 +83,22 @@ class DebuggerClient { virtual bool stopped() = 0; virtual bool completed() = 0; + + protected: + void setState( const State& state ); + + State mState{ State::None }; + bool mLaunched{ false }; + bool mConfigured{ false }; + Observer* mObserver{ nullptr }; + + void checkRunning(); + + void stateChanged( State ); + void initialized(); + void debuggeeRunning(); + void debuggeeTerminated(); + void failed(); }; } // namespace ecode