#include "debuggerclientdap.hpp" #include "messages.hpp" #include #include using namespace EE::System; 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( const ProtocolSettings& protocolSettings, std::unique_ptr&& bus ) : mBus( std::move( bus ) ), mProtocol( protocolSettings ) {} DebuggerClientDap::~DebuggerClientDap() { mBus.reset(); } 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 = jsonCmd.dump(); std::string msg( String::format( "Content-Length: %zu\r\n\r\n%s", cmd.size(), cmd ) ); Log::instance()->writel( mDebug ? LogLevel::Info : LogLevel::Debug, "DebuggerClientDap::makeRequest:" ); Log::instance()->writel( mDebug ? LogLevel::Info : LogLevel::Debug, msg ); mBus->write( msg.data(), msg.size() ); mRequests[mIdx] = { std::string{ command }, arguments, onFinish }; mIdx.fetch_add( 1, std::memory_order_relaxed ); } bool DebuggerClientDap::isServerConnected() const { return ( mState != State::None ) && ( mState != State::Failed ) && ( mBus->state() == Bus::State::Running ); } bool DebuggerClientDap::supportsTerminate() const { return mAdapterCapabilities.supportsTerminateRequest && ( mProtocol.launchRequest.value( DAP_COMMAND, "" ) == DAP_LAUNCH ); } bool DebuggerClientDap::start() { bool started = mBus->start(); if ( started ) mBus->startAsyncRead( [this]( const char* bytes, size_t n ) { asyncRead( bytes, n ); } ); else { Log::warning( "DebuggerClientDap::start: could not initialize the debugger" ); return false; } mStarted = started; mLaunched = false; mConfigured = false; if ( mState != State::None ) { Log::warning( "DebuggerClientDap::start: trying to re-start has no effect" ); return false; } requestInitialize(); 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 ); for ( auto listener : mListeners ) listener->capabilitiesReceived( mAdapterCapabilities ); requestLaunchCommand(); } void DebuggerClientDap::requestLaunchCommand() { if ( mState != State::Initializing ) { Log::warning( "DebuggerClientDap::requestLaunchCommand: trying to launch in an unexpected state" ); return; } if ( mProtocol.launchCommand.empty() ) return; makeRequest( mProtocol.launchCommand, mProtocol.launchRequest, [this]( const Response& response, const auto& ) { if ( response.success ) { mLaunched = true; checkRunning(); for ( auto listener : mListeners ) listener->launched(); } else { if ( response.errorBody ) { Log::warning( "DebuggerClientDap::requestLaunchCommand: error %ld %s", response.errorBody->id, response.errorBody->format ); } setState( State::Failed ); } } ); } void DebuggerClientDap::requestInitialize() { const nlohmann::json capabilities{ { DAP_CLIENT_ID, "ecode-dap" }, { DAP_CLIENT_NAME, "ecode dap" }, { "locale", mProtocol.locale }, { DAP_ADAPTER_ID, "ecode-dap" }, { 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; while ( true ) { const auto info = readHeader(); if ( !info ) break; const auto data = mBuffer.substr( info->payloadStart, info->payloadLength ); if ( data.size() < info->payloadLength ) break; mBuffer.erase( 0, info->payloadStart + info->payloadLength ); #ifndef EE_DEBUG try { #endif auto message = json::parse( data ); if ( mDebug ) { Log::debug( "DebuggerClientDap::asyncRead:" ); Log::debug( message.dump() ); } processProtocolMessage( message ); #ifndef EE_DEBUG } catch ( const json::exception& e ) { Log::error( "DebuggerClientDap::asyncRead: JSON bad format: %s", e.what() ); } #endif } } void DebuggerClientDap::processProtocolMessage( const nlohmann::json& msg ) { const auto type = msg.value( DAP_TYPE, "" ); if ( DAP_RESPONSE == type ) { processResponse( msg ); } else if ( DAP_EVENT == type ) { processEvent( msg ); } else { Log::warning( "DebuggerClientDap::processProtocolMessage: unknown, empty or unexpected " "ProtocolMessage::%s (%s)", DAP_TYPE, type ); } } void DebuggerClientDap::processResponse( const nlohmann::json& msg ) { const Response response( 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 ) { for ( auto listener : mListeners ) listener->errorResponse( summary, message ); } void DebuggerClientDap::processEvent( const nlohmann::json& msg ) { const std::string event = msg.value( DAP_EVENT, "" ); const auto body = msg.contains( DAP_BODY ) ? msg[DAP_BODY] : nlohmann::json{}; 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 ); configurationDone(); } void DebuggerClientDap::processEventTerminated() { setState( State::Terminated ); } void DebuggerClientDap::processEventExited( const nlohmann::json& body ) { const int exitCode = body.value( "exitCode", -1 ); for ( auto listener : mListeners ) listener->debuggeeExited( exitCode ); } void DebuggerClientDap::processEventOutput( const nlohmann::json& body ) { Output output( body ); for ( auto listener : mListeners ) listener->outputProduced( output ); } void DebuggerClientDap::processEventProcess( const nlohmann::json& body ) { ProcessInfo processInfo( body ); for ( auto listener : mListeners ) listener->debuggingProcess( processInfo ); } void DebuggerClientDap::processEventThread( const nlohmann::json& body ) { ThreadEvent threadEvent( body ); for ( auto listener : mListeners ) listener->threadChanged( threadEvent ); } void DebuggerClientDap::processEventStopped( const nlohmann::json& body ) { StoppedEvent stoppedEvent( body ); for ( auto listener : mListeners ) listener->debuggeeStopped( stoppedEvent ); } void DebuggerClientDap::processEventModule( const nlohmann::json& body ) { ModuleEvent moduleEvent( body ); for ( auto listener : mListeners ) listener->moduleChanged( moduleEvent ); } void DebuggerClientDap::processEventContinued( const nlohmann::json& body ) { ContinuedEvent continuedEvent( body ); for ( auto listener : mListeners ) listener->debuggeeContinued( continuedEvent ); } void DebuggerClientDap::processEventBreakpoint( const nlohmann::json& body ) { BreakpointEvent breakpointEvent( body ); for ( auto listener : mListeners ) listener->breakpointChanged( breakpointEvent ); } std::optional DebuggerClientDap::readHeader() { Uint64 length = std::string::npos; Uint64 start = 0; Uint64 end = std::string::npos; auto discardExploredBuffer = [this, length, start, end]() mutable { mBuffer.erase( 0, end ); length = end = std::string::npos; start = 0; }; while ( true ) { end = mBuffer.find( DAP_SEP, start ); if ( end == std::string::npos ) { if ( mBuffer.size() > MAX_HEADER_SIZE ) mBuffer.clear(); length = std::string::npos; break; // PENDING } const auto header = mBuffer.substr( start, end - start ); while ( std::string_view{ mBuffer }.substr( end, 2 ) == DAP_SEP ) end += DAP_SEP_SIZE; // header block separator if ( header.size() == 0 ) { if ( length < 0 ) { // unexpected end of header Log::error( "DebuggerClientDap::readHeader unexpected end of header block" ); discardExploredBuffer(); continue; } break; // END HEADER (length>0, end>0) } // parse field const auto sep = header.find_first_of( ":" ); if ( sep == std::string::npos ) { Log::error( "DebuggerClientDap::readHeader cannot parse header field: ", header ); discardExploredBuffer(); continue; // CONTINUE HEADER } // parse content-length if ( String::startsWith( header, DAP_CONTENT_LENGTH ) ) { std::string lengthStr( header.substr( sep + 1, header.size() - sep ) ); String::trimInPlace( lengthStr ); if ( !String::fromString( length, lengthStr ) ) { Log::error( "DebuggerClientDap::readHeader invalid value: ", header ); discardExploredBuffer(); continue; // CONTINUE HEADER } else { break; } } start = end; } if ( length < 0 || length == std::string::npos ) return std::nullopt; return HeaderInfo{ end, length }; } bool DebuggerClientDap::resume( int threadId, bool singleThread ) { nlohmann::json arguments{ { DAP_THREAD_ID, threadId } }; if ( singleThread ) arguments[DAP_SINGLE_THREAD] = true; makeRequest( "continue", arguments, [this]( const Response& response, const nlohmann::json& request ) { if ( response.success ) { ContinuedEvent continuedEvent( request.value( DAP_THREAD_ID, 1 ), response.body.value( DAP_ALL_THREADS_CONTINUED, true ) ); for ( auto listener : mListeners ) listener->debuggeeContinued( continuedEvent ); } } ); return true; } bool DebuggerClientDap::pause( int threadId ) { nlohmann::json arguments{ { DAP_THREAD_ID, threadId } }; makeRequest( "pause", arguments ); return true; } void DebuggerClientDap::processResponseNext( const Response& response, const nlohmann::json& request ) { if ( response.success ) { ContinuedEvent continuedEvent( request.value( DAP_THREAD_ID, 1 ), !response.body.value( DAP_SINGLE_THREAD, false ) ); for ( auto listener : mListeners ) listener->debuggeeContinued( continuedEvent ); } } bool DebuggerClientDap::stepOver( int threadId, bool singleThread ) { nlohmann::json arguments{ { DAP_THREAD_ID, threadId } }; if ( singleThread ) arguments[DAP_SINGLE_THREAD] = true; makeRequest( "next", arguments, makeResponseHandler( &DebuggerClientDap::processResponseNext, this ) ); return true; } bool DebuggerClientDap::goTo( int threadId, int targetId ) { const nlohmann::json arguments{ { DAP_THREAD_ID, threadId }, { DAP_TARGET_ID, targetId } }; makeRequest( "goto", arguments, makeResponseHandler( &DebuggerClientDap::processResponseNext, this ) ); return true; } bool DebuggerClientDap::stepInto( int threadId, bool singleThread ) { nlohmann::json arguments{ { DAP_THREAD_ID, threadId } }; if ( singleThread ) arguments[DAP_SINGLE_THREAD] = true; makeRequest( "stepIn", arguments, makeResponseHandler( &DebuggerClientDap::processResponseNext, this ) ); return true; } bool DebuggerClientDap::stepOut( int threadId, bool singleThread ) { nlohmann::json arguments{ { DAP_THREAD_ID, threadId } }; if ( singleThread ) arguments[DAP_SINGLE_THREAD] = true; makeRequest( "stepOut", arguments, makeResponseHandler( &DebuggerClientDap::processResponseNext, this ) ); return true; } bool DebuggerClientDap::terminate( bool restart ) { nlohmann::json arguments; if ( restart ) arguments["restart"] = true; makeRequest( "terminate", arguments ); return true; } bool DebuggerClientDap::disconnect( bool restart ) { nlohmann::json arguments; if ( restart ) arguments["restart"] = true; makeRequest( "disconnect", arguments, [this]( const Response& response, const nlohmann::json& ) { if ( response.success ) { for ( auto listener : mListeners ) listener->serverDisconnected(); } } ); return true; } bool DebuggerClientDap::threads() { makeRequest( DAP_THREADS, {}, [this]( const Response& response, const nlohmann::json& ) { if ( response.success ) { auto threads( Thread::parseList( response.body[DAP_THREADS] ) ); for ( auto listener : mListeners ) listener->threads( threads ); } else { for ( auto listener : mListeners ) listener->threads( {} ); } } ); return true; } bool DebuggerClientDap::stackTrace( int threadId, int startFrame, int levels ) { const nlohmann::json arguments{ { DAP_THREAD_ID, threadId }, { "startFrame", startFrame }, { "levels", levels } }; makeRequest( "stackTrace", arguments, [this]( const Response& response, const nlohmann::json& request ) { const int threadId = request.value( DAP_THREAD_ID, 1 ); if ( response.success ) { StackTraceInfo stackTraceInfo( response.body ); for ( auto listener : mListeners ) listener->stackTrace( threadId, stackTraceInfo ); } else { StackTraceInfo stackTraceInfo; for ( auto listener : mListeners ) listener->stackTrace( threadId, stackTraceInfo ); } } ); return true; } bool DebuggerClientDap::scopes( int frameId ) { const nlohmann::json arguments{ { DAP_FRAME_ID, frameId } }; makeRequest( DAP_SCOPES, arguments, [this]( const Response& response, const nlohmann::json& request ) { const int frameId = request.value( DAP_FRAME_ID, 1 ); if ( response.success ) { auto scopes( Scope::parseList( response.body[DAP_SCOPES] ) ); for ( auto listener : mListeners ) listener->scopes( frameId, scopes ); } else { std::vector scopes; for ( auto listener : mListeners ) listener->scopes( frameId, scopes ); } } ); return true; } bool DebuggerClientDap::variables( int variablesReference, Variable::Type filter, int start, int count ) { nlohmann::json arguments{ { DAP_VARIABLES_REFERENCE, variablesReference }, { DAP_START, start }, { DAP_COUNT, count }, }; switch ( filter ) { case Variable::Type::Indexed: arguments[DAP_FILTER] = "indexed"; break; case Variable::Type::Named: arguments[DAP_FILTER] = "named"; break; default: break; } makeRequest( DAP_VARIABLES, arguments, [this]( const Response& response, const nlohmann::json& request ) { const int variablesReference = request.value( DAP_VARIABLES_REFERENCE, 0 ); if ( response.success ) { auto variableList( Variable::parseList( response.body[DAP_VARIABLES] ) ); for ( auto listener : mListeners ) listener->variables( variablesReference, variableList ); } else { std::vector variableList; for ( auto listener : mListeners ) listener->variables( variablesReference, variableList ); } } ); return true; } bool DebuggerClientDap::modules( int start, int count ) { makeRequest( DAP_MODULES, { { DAP_START, start }, { DAP_COUNT, count } }, [this]( const auto& response, const auto& ) { if ( response.success ) { ModulesInfo info( response.body ); for ( auto listener : mListeners ) listener->modules( info ); } else { ModulesInfo info; for ( auto listener : mListeners ) listener->modules( info ); } } ); return true; } bool DebuggerClientDap::evaluate( const std::string& expression, const std::string& context, std::optional frameId ) { nlohmann::json arguments{ { DAP_EXPRESSION, expression } }; if ( !context.empty() ) arguments[DAP_CONTEXT] = context; if ( frameId ) arguments[DAP_FRAME_ID] = *frameId; makeRequest( "evaluate", arguments, [this]( const auto& response, const auto& request ) { auto expression = request.value( DAP_EXPRESSION, "" ); if ( response.success ) { EvaluateInfo info( response.body ); for ( auto listener : mListeners ) listener->expressionEvaluated( expression, info ); } else { for ( auto listener : mListeners ) listener->expressionEvaluated( expression, std::nullopt ); } } ); return true; } bool DebuggerClientDap::setBreakpoints( const std::string& path, const std::vector breakpoints, bool sourceModified ) { return setBreakpoints( Source( path ), breakpoints, sourceModified ); } bool DebuggerClientDap::setBreakpoints( const dap::Source& source, const std::vector breakpoints, bool sourceModified ) { nlohmann::json bpoints = nlohmann::json::array(); for ( const auto& item : breakpoints ) bpoints.push_back( item.toJson() ); nlohmann::json arguments{ { DAP_SOURCE, source.toJson() }, { DAP_BREAKPOINTS, bpoints }, { "sourceModified", sourceModified } }; makeRequest( "setBreakpoints", arguments, [this]( const Response& response, const nlohmann::json& request ) { const auto source = Source( request[DAP_SOURCE] ); if ( response.success ) { const auto resp = response.body; if ( resp.contains( DAP_BREAKPOINTS ) ) { std::vector breakpoints; breakpoints.reserve( resp[DAP_BREAKPOINTS].size() ); for ( const auto& item : resp[DAP_BREAKPOINTS] ) breakpoints.emplace_back( item ); for ( auto listener : mListeners ) listener->sourceBreakpoints( source.path, source.sourceReference.value_or( 0 ), breakpoints ); } else { std::vector breakpoints; breakpoints.reserve( resp[DAP_LINES].size() ); for ( const auto& item : resp[DAP_LINES] ) breakpoints.emplace_back( item.get() ); for ( auto listener : mListeners ) listener->sourceBreakpoints( source.path, source.sourceReference.value_or( 0 ), breakpoints ); } } else { for ( auto listener : mListeners ) listener->sourceBreakpoints( source.path, source.sourceReference.value_or( 0 ), std::nullopt ); } } ); return true; } bool DebuggerClientDap::gotoTargets( const std::string& path, const int line, const std::optional column ) { return gotoTargets( Source( path ), line, column ); } bool DebuggerClientDap::gotoTargets( const Source& source, const int line, const std::optional column ) { nlohmann::json arguments{ { DAP_SOURCE, source.toJson() }, { DAP_LINE, line } }; if ( column ) arguments[DAP_COLUMN] = *column; makeRequest( "gotoTargets", arguments, [this]( const auto& response, const auto& req ) { const auto source = Source( req[DAP_SOURCE] ); const int line = req.value( DAP_LINE, 1 ); if ( response.success ) { auto list = GotoTarget::parseList( response.body["targets"] ); for ( auto listener : mListeners ) listener->gotoTargets( source, line, list ); } else { std::vector list; for ( auto listener : mListeners ) listener->gotoTargets( source, line, list ); } } ); return true; } bool DebuggerClientDap::watch( const std::string& expression, std::optional frameId ) { return evaluate( expression, "watch", frameId ); } bool DebuggerClientDap::configurationDone() { if ( mState != State::Initialized ) { Log::warning( "DebuggerClientDap::requestConfigurationDone: trying to configure in an " "unexpected status" ); return false; } if ( !mAdapterCapabilities.supportsConfigurationDoneRequest ) { for ( auto listener : mListeners ) listener->configured(); return true; } makeRequest( "configurationDone", nlohmann::json{}, [this]( const auto& response, const auto& ) { if ( response.success ) { mConfigured = true; checkRunning(); for ( auto listener : mListeners ) listener->configured(); } } ); return true; } } // namespace ecode::dap