#include "clientthread.h" #include "../../bscript/bstruct.h" #include "../../bscript/dict.h" #include "../../bscript/eprog.h" #include "../../bscript/impstr.h" #include "../../clib/esignal.h" #include "../../clib/fileutil.h" #include "../../clib/logfacility.h" #include "../../plib/pkg.h" #include "../../plib/systemstate.h" #include "../module/uomod.h" #include "../polsem.h" #include "../scrdef.h" #include "../scrsched.h" #include #include #include namespace fs = std::filesystem; namespace Pol { using namespace Core; using namespace Bscript; using namespace EscriptGrammar; using namespace Compiler; namespace Network { namespace DAP { namespace fs = std::filesystem; unsigned int DebugClientThread::_instance_counter = 0; DebugClientThread::DebugClientThread( const std::shared_ptr& rw ) : _instance( ++_instance_counter ), _rw( rw ), _session( dap::Session::create() ), _uoexec_wptr( nullptr ), _expression_evaluator(), _global_scope_handle( 0 ), _was_launch_requested( false ), _exit_sent( false ) { } void DebugClientThread::on_halt() { dap::StoppedEvent event; event.reason = "pause"; event.threadId = 1; _session->send( event ); } void DebugClientThread::on_destroy() { POLLOG_INFOLN( "Debugger#{} script destroyed.", _instance ); dap::ExitedEvent event; event.exitCode = 0; _session->send( event ); _exit_sent = true; } void DebugClientThread::on_print( const std::string& output ) { dap::OutputEvent event; event.category = "stdout"; event.output = output + "\n"; // The `ExecutorDebugListener` methods are called from the script thread // (which runs inside a PolLock), so no need to lock. Additionally, the // `UOExecutor` should always exist, as these methods are all called within an // `Executor` instance method, but check just in case. Not a real performance // hit, since this is only called when printing with the debugger is attached. if ( _uoexec_wptr.exists() ) { UOExecutor* exec = _uoexec_wptr.get_weakptr(); dap::Source source; source.path = _script->dbg_filenames[_script->dbg_filenum[exec->PC]]; event.source = source; event.line = _script->dbg_linenum[exec->PC]; } _session->send( event ); } dap::ConfigurationDoneResponse DebugClientThread::handle_configurationDone( const dap::ConfigurationDoneRequest& ) { return dap::ConfigurationDoneResponse{}; } dap::ResponseOrError DebugClientThread::handle_attach( const dap::PolAttachRequest& request ) { PolLock lock; UOExecutor* uoexec; if ( _uoexec_wptr.exists() ) { uoexec = _uoexec_wptr.get_weakptr(); if ( uoexec->in_debugger_holdlist() ) uoexec->revive_debugged(); _variable_handles.reset(); _global_scope_handle = 0; uoexec->detach_debugger(); _uoexec_wptr.clear(); } auto pid = static_cast( request.pid ); if ( find_uoexec( pid, &uoexec ) ) { EScriptProgram* prog = const_cast( uoexec->prog() ); if ( prog->read_dbg_file() == 0 ) { if ( !uoexec->attach_debugger( weak_from_this() ) ) return dap::Error( "Debugger already attached." ); _uoexec_wptr = uoexec->weakptr; _script.set( prog ); return dap::AttachResponse{}; } return dap::Error( "No debug information available." ); } return dap::Error( "Unknown process id '%d'", int( pid ) ); } dap::ResponseOrError DebugClientThread::handle_launch( const dap::PolLaunchRequest& request ) { PolLock lock; ScriptDef sd; fs::path p( request.script ); if ( p.extension().u8string() == ".src" ) p.replace_extension( fs::path( ".ecl" ) ); bool config_result; // A package-path if ( request.script.find( ':' ) == 0 ) { config_result = sd.config_nodie( p.u8string(), nullptr, "scripts/" ); } else { std::string relative = ( p.is_absolute() ? fs::relative( p ) : p ).u8string(); if ( relative.find( "scripts/" ) == 0 ) { config_result = sd.config_nodie( relative.substr( 8 ), nullptr, "scripts/" ); } else { const Plib::Package* package = nullptr; for ( const auto* pkg : Plib::systemstate.packages ) { if ( relative.find( pkg->dir() ) == 0 ) { package = pkg; break; } } if ( package ) { config_result = sd.config_nodie( relative.substr( package->dir().length() ), package, "scripts/" ); } else { config_result = sd.config_nodie( request.script, nullptr, "scripts/" ); } } } if ( !config_result ) return dap::Error( "Error in script name." ); if ( !sd.exists() ) return dap::Error( "Script " + sd.name() + " does not exist." ); Module::UOExecutorModule* new_uoemod; if ( request.arg.has_value() && request.arg->length() > 0 ) { new_uoemod = Core::start_script( sd, BObjectImp::unpack( request.arg->c_str() ) ); } else { new_uoemod = Core::start_script( sd, nullptr ); } if ( new_uoemod == nullptr ) { return dap::Error( "Unable to start script" ); } UOExecutor* uoexec = static_cast( &new_uoemod->exec ); EScriptProgram* prog = const_cast( uoexec->prog() ); if ( prog->read_dbg_file() == 0 ) { bool stopAtEntry = request.stopAtEntry.value( false ); if ( !uoexec->attach_debugger( weak_from_this(), stopAtEntry ) ) return dap::Error( "Debugger already attached." ); _uoexec_wptr = uoexec->weakptr; _script.set( prog ); _was_launch_requested = true; return dap::LaunchResponse{}; } return dap::Error( "No debug information available." ); } dap::ResponseOrError DebugClientThread::handle_processes( const dap::PolProcessesRequest& request ) { PolLock lock; dap::PolProcessesResponse response; for ( const auto& [pid, uoexec] : scriptScheduler.getPidlist() ) { if ( !request.filter.has_value() || boost::icontains( uoexec->scriptname(), request.filter.value() ) ) { dap::PolProcess entry; entry.id = pid; entry.script = uoexec->scriptname(); if ( uoexec->halt() ) entry.state = 2; // debugging else if ( uoexec->in_hold_list() == NO_LIST ) entry.state = 1; // running else entry.state = 0; // sleeping response.processes.push_back( entry ); } } return response; } dap::ResponseOrError DebugClientThread::handle_threads( const dap::ThreadsRequest& ) { PolLock lock; if ( !_uoexec_wptr.exists() ) return dap::Error( "No script attached." ); UOExecutor* exec = _uoexec_wptr.get_weakptr(); dap::ThreadsResponse response; dap::Thread thread; thread.id = 1; thread.name = Clib::tostring( exec->pid() ); response.threads.push_back( thread ); return response; } dap::ResponseOrError DebugClientThread::handle_setExceptionBreakpoints( const dap::SetExceptionBreakpointsRequest& ) { return dap::SetExceptionBreakpointsResponse{}; } dap::ResponseOrError DebugClientThread::handle_continue( const dap::ContinueRequest& ) { PolLock lock; if ( !_uoexec_wptr.exists() ) { return dap::Error( "No script attached." ); } UOExecutor* uoexec = _uoexec_wptr.get_weakptr(); if ( !uoexec->in_debugger_holdlist() ) { return dap::Error( "Script not ready to trace." ); } _variable_handles.reset(); _global_scope_handle = 0; uoexec->dbg_run(); uoexec->revive_debugged(); return dap::ContinueResponse{}; } dap::ResponseOrError DebugClientThread::handle_pause( const dap::PauseRequest& ) { PolLock lock; if ( !_uoexec_wptr.exists() ) { return dap::Error( "No script attached." ); } UOExecutor* exec = _uoexec_wptr.get_weakptr(); exec->dbg_break(); return dap::PauseResponse{}; } dap::ResponseOrError DebugClientThread::handle_evaluate( const dap::EvaluateRequest& request ) { PolLock lock; if ( !_uoexec_wptr.exists() ) { return dap::Error( "No script attached." ); } BObjectRef result; try { result = _expression_evaluator.evaluate( _uoexec_wptr.get_weakptr(), _script.get(), request.expression ); } catch ( std::exception& ex ) { return dap::Error( ex.what() ); } dap::EvaluateResponse response; _variable_handles.set_response_details( result, response ); return response; } dap::ResponseOrError DebugClientThread::handle_setVariable( const dap::SetVariableRequest& request ) { PolLock lock; if ( !_uoexec_wptr.exists() ) { return dap::Error( "No script attached." ); } BObjectRef value; try { value = _expression_evaluator.evaluate( _uoexec_wptr.get_weakptr(), _script.get(), request.value ); } catch ( std::exception& ex ) { return dap::Error( ex.what() ); } dap::SetVariableResponse response; auto reference_ptr = _variable_handles.get( static_cast( request.variablesReference ) ); if ( reference_ptr == nullptr ) { return dap::Error( "Invalid variablesReference id %d", int( request.variablesReference ) ); } std::visit( [&]( auto&& arg ) { using T = std::decay_t; if constexpr ( (std::is_same_v || std::is_same_v)) { for ( auto& [key, member] : arg.contents ) { if ( key == request.name ) { *member = value; _variable_handles.set_response_details( value, response ); } } } else if constexpr ( std::is_same_v ) { auto value_set = _variable_handles.set_index_or_member( arg, request.name, value ); _variable_handles.set_response_details( value_set, response ); } }, *reference_ptr ); return response; } dap::ResponseOrError DebugClientThread::handle_next( const dap::NextRequest& ) { PolLock lock; if ( !_uoexec_wptr.exists() ) { return dap::Error( "No script attached." ); } UOExecutor* uoexec = _uoexec_wptr.get_weakptr(); if ( !uoexec->in_debugger_holdlist() ) { return dap::Error( "Script not ready to trace." ); } _variable_handles.reset(); _global_scope_handle = 0; uoexec->dbg_step_over(); uoexec->revive_debugged(); return dap::NextResponse{}; } dap::ResponseOrError DebugClientThread::handle_stepIn( const dap::StepInRequest& ) { PolLock lock; if ( !_uoexec_wptr.exists() ) { return dap::Error( "No script attached." ); } UOExecutor* uoexec = _uoexec_wptr.get_weakptr(); if ( !uoexec->in_debugger_holdlist() ) { return dap::Error( "Script not ready to trace." ); } _variable_handles.reset(); _global_scope_handle = 0; uoexec->dbg_step_into(); uoexec->revive_debugged(); return dap::StepInResponse{}; } dap::ResponseOrError DebugClientThread::handle_setBreakpoints( const dap::SetBreakpointsRequest& request ) { PolLock lock; if ( !_uoexec_wptr.exists() ) { return dap::Error( "No script attached." ); } UOExecutor* uoexec = _uoexec_wptr.get_weakptr(); unsigned int filenum; if ( request.source.sourceReference.has_value() ) { filenum = static_cast( request.source.sourceReference.value() ); } else { if ( !request.source.path.has_value() ) { return dap::Error( "No source location provided." ); } auto filename_iter = std::find( _script->dbg_filenames.begin(), _script->dbg_filenames.end(), request.source.path.value() ); if ( filename_iter == _script->dbg_filenames.end() ) { return dap::Error( "File not in scope" ); } filenum = static_cast( std::distance( _script->dbg_filenames.begin(), filename_iter ) ); } if ( filenum >= _script->dbg_filenames.size() ) { return dap::Error( "File %d out of range", filenum ); } std::set remove_bps; for ( unsigned i = 0; i < _script->dbg_filenum.size(); ++i ) { if ( _script->dbg_filenum[i] == filenum ) { remove_bps.insert( i ); } } uoexec->dbg_clrbps( remove_bps ); dap::SetBreakpointsResponse response; auto breakpoints = request.breakpoints.value( {} ); response.breakpoints.resize( breakpoints.size() ); for ( size_t breakpoint_index = 0; breakpoint_index < breakpoints.size(); breakpoint_index++ ) { // Conditional breakpoints are not supported. if ( breakpoints[breakpoint_index].condition ) { response.breakpoints[breakpoint_index].verified = false; continue; } for ( unsigned i = 0; i < _script->dbg_filenum.size(); ++i ) { if ( _script->dbg_filenum[i] == filenum && _script->dbg_linenum[i] == breakpoints[breakpoint_index].line ) { response.breakpoints[breakpoint_index].verified = true; uoexec->dbg_setbp( i ); break; } } } return response; } dap::ResponseOrError DebugClientThread::handle_source( const dap::SourceRequest& request ) { PolLock lock; if ( !_uoexec_wptr.exists() ) { return dap::Error( "No script attached." ); } size_t filenum = request.sourceReference; if ( filenum >= _script->dbg_filenames.size() ) { return dap::Error( "File out of range: '%d'", int( filenum ) ); } auto filepath = _script->dbg_filenames[filenum]; std::ifstream ifs( filepath ); if ( !ifs.is_open() ) return dap::Error( "File # out of range" ); std::stringstream buffer; buffer << ifs.rdbuf(); dap::SourceResponse response; response.content = buffer.str(); return response; } dap::ResponseOrError DebugClientThread::handle_stackTrace( const dap::StackTraceRequest& ) { PolLock lock; if ( !_uoexec_wptr.exists() ) { return dap::Error( "No script attached." ); } UOExecutor* exec = _uoexec_wptr.get_weakptr(); if ( !exec->halt() ) { return dap::Error( "Script must be halted to retrieve stack trace." ); } std::vector stack = exec->ControlStack; dap::StackTraceResponse response; unsigned int PC; { ReturnContext rc; rc.PC = exec->PC; rc.ValueStackDepth = static_cast( exec->ValueStack.size() ); stack.push_back( rc ); } // Bottom frame starts at index 1 auto frameId = exec->ControlStack.size() + 1; while ( !stack.empty() ) { ReturnContext& rc = stack.back(); PC = rc.PC; stack.pop_back(); dap::Source source; auto filepath = _script->dbg_filenames[_script->dbg_filenum[PC]]; fs::path p( filepath ); std::string abs_path = ( p.is_relative() ? ( fs::current_path() / p ) : p ).u8string(); source.name = abs_path; source.path = abs_path; dap::StackFrame frame; frame.line = _script->dbg_linenum[PC]; frame.column = 1; auto dbgFunction = std::find_if( _script->dbg_functions.begin(), _script->dbg_functions.end(), [&]( auto& i ) { return i.firstPC <= PC && PC <= i.lastPC; } ); if ( dbgFunction != _script->dbg_functions.end() ) { frame.name = dbgFunction->name; } else { frame.name = "(program)"; } frame.id = frameId--; frame.source = source; response.stackFrames.push_back( frame ); } return response; } dap::ResponseOrError DebugClientThread::handle_scopes( const dap::ScopesRequest& request ) { PolLock lock; if ( !_uoexec_wptr.exists() ) { return dap::Error( "No script attached." ); } UOExecutor* uoexec = _uoexec_wptr.get_weakptr(); if ( !uoexec->halt() ) { return dap::Error( "Script must be halted to retrieve scopes." ); } size_t frameId = request.frameId; // Frame IDs start a 1 if ( frameId > uoexec->ControlStack.size() + 1 ) { return dap::Error( "Unknown frameId '%d'", frameId ); } if ( !_global_scope_handle ) { _global_scope_handle = _variable_handles.create( GlobalReference( uoexec, _script.get() ) ); } dap::ScopesResponse response; { dap::Scope scope; scope.name = "Locals @ " + Clib::tostring( frameId ); scope.presentationHint = "locals"; scope.variablesReference = _variable_handles.create( FrameReference( uoexec, _script.get(), frameId - 1 ) ); response.scopes.push_back( scope ); } { dap::Scope scope; scope.name = "Globals"; scope.variablesReference = _global_scope_handle; response.scopes.push_back( scope ); } return response; } dap::ResponseOrError DebugClientThread::handle_variables( const dap::VariablesRequest& request ) { PolLock lock; if ( !_uoexec_wptr.exists() ) { return dap::Error( "No script attached." ); } UOExecutor* uoexec = _uoexec_wptr.get_weakptr(); if ( !uoexec->halt() ) { return dap::Error( "Script must be halted to retrieve variables." ); } dap::VariablesResponse response; auto reference_ptr = _variable_handles.get( static_cast( request.variablesReference ) ); if ( reference_ptr == nullptr ) { return dap::Error( "Invalid variablesReference id %d", int( request.variablesReference ) ); } std::visit( [&]( auto&& arg ) { using T = std::decay_t; if constexpr ( (std::is_same_v) || (std::is_same_v)) { for ( auto const& [key, member] : arg.contents ) { dap::Variable current_var; current_var.name = key; _variable_handles.set_response_details( *member, current_var ); response.variables.push_back( std::move( current_var ) ); } } else if constexpr ( std::is_same_v ) { response.variables = _variable_handles.to_variables( arg ); } }, *reference_ptr ); return response; } dap::ResponseOrError DebugClientThread::handle_initialize( const dap::PolInitializeRequest& request ) { if ( !Plib::systemstate.config.debug_password.empty() && request.password.value( "" ) != Plib::systemstate.config.debug_password ) return dap::Error( "Password not recognized." ); dap::InitializeResponse response; response.supportsSetVariable = true; return response; } dap::ResponseOrError DebugClientThread::handle_stepOut( const dap::StepOutRequest& ) { PolLock lock; if ( !_uoexec_wptr.exists() ) { return dap::Error( "No script attached." ); } UOExecutor* uoexec = _uoexec_wptr.get_weakptr(); if ( !uoexec->in_debugger_holdlist() ) { return dap::Error( "Script not ready to trace." ); } _variable_handles.reset(); _global_scope_handle = 0; uoexec->dbg_step_out(); uoexec->revive_debugged(); return dap::StepOutResponse{}; } dap::ResponseOrError DebugClientThread::handle_disconnect( const dap::DisconnectRequest& ) { PolLock lock; if ( _uoexec_wptr.exists() ) { UOExecutor* exec = _uoexec_wptr.get_weakptr(); if ( _was_launch_requested ) { exec->seterror( true ); exec->revive(); } if ( exec->in_debugger_holdlist() ) exec->revive_debugged(); _variable_handles.reset(); _global_scope_handle = 0; exec->detach_debugger(); _uoexec_wptr.clear(); } return dap::DisconnectResponse{}; } void DebugClientThread::after_pause( const dap::ResponseOrError& ) { PolLock lock; if ( !_uoexec_wptr.exists() ) { return; } UOExecutor* uoexec = _uoexec_wptr.get_weakptr(); if ( !uoexec->halt() ) { return; } on_halt(); } void DebugClientThread::after_initialize( const dap::ResponseOrError& response ) { if ( response.error ) _rw->close(); else _session->send( dap::InitializedEvent() ); } void DebugClientThread::after_disconnect( const dap::ResponseOrError& ) { _rw->close(); } void DebugClientThread::after_attach( const dap::ResponseOrError& ) { PolLock lock; if ( _uoexec_wptr.exists() ) if ( _uoexec_wptr.get_weakptr()->halt() ) { on_halt(); } } void DebugClientThread::on_error( const char* msg ) { POLLOG_ERRORLN( "Debugger#{} session error: {}", _instance, msg ); _rw->close(); } void DebugClientThread::run() { POLLOG_INFOLN( "Debugger#{} client thread started.", _instance ); // Session event handlers that are only attached once initialized with the password (if // required). auto attach_authorized_handlers = [this]() { _session->registerHandler( [this]( const dap::ConfigurationDoneRequest& request ) { return handle_configurationDone( request ); } ); _session->registerHandler( [this]( const dap::PolAttachRequest& request ) { return handle_attach( request ); } ); // After sending an AttachResponse, check if executor is halted. If so, trigger the on_halt() // event. _session->registerSentHandler( [this]( const dap::ResponseOrError& response ) { after_attach( response ); } ); _session->registerHandler( [this]( const dap::PolLaunchRequest& request ) { return handle_launch( request ); } ); _session->registerHandler( [this]( const dap::PolProcessesRequest& request ) { return handle_processes( request ); } ); _session->registerHandler( [this]( const dap::ThreadsRequest& request ) { return handle_threads( request ); } ); _session->registerHandler( [this]( const dap::SetExceptionBreakpointsRequest& request ) { return handle_setExceptionBreakpoints( request ); } ); _session->registerHandler( [this]( const dap::ContinueRequest& request ) { return handle_continue( request ); } ); _session->registerHandler( [this]( const dap::PauseRequest& request ) { return handle_pause( request ); } ); _session->registerHandler( [this]( const dap::EvaluateRequest& request ) { return handle_evaluate( request ); } ); _session->registerHandler( [this]( const dap::SetVariableRequest& request ) { return handle_setVariable( request ); } ); // After sending a PauseResponse, check if the script is paused. If so, send a StoppedEvent. _session->registerSentHandler( [this]( const dap::ResponseOrError& response ) { after_pause( response ); } ); _session->registerHandler( [this]( const dap::NextRequest& request ) { return handle_next( request ); } ); _session->registerHandler( [this]( const dap::StepInRequest& request ) { return handle_stepIn( request ); } ); _session->registerHandler( [this]( const dap::SetBreakpointsRequest& request ) { return handle_setBreakpoints( request ); } ); _session->registerHandler( [this]( const dap::SourceRequest& request ) { return handle_source( request ); } ); _session->registerHandler( [this]( const dap::StackTraceRequest& request ) { return handle_stackTrace( request ); } ); _session->registerHandler( [this]( const dap::ScopesRequest& request ) { return handle_scopes( request ); } ); _session->registerHandler( [this]( const dap::VariablesRequest& request ) { return handle_variables( request ); } ); _session->registerHandler( [this]( const dap::StepOutRequest& request ) { return handle_stepOut( request ); } ); }; // Session event handlers added before initialization _session->registerHandler( [&]( const dap::PolInitializeRequest& request ) { auto response = handle_initialize( request ); if ( !response.error ) { attach_authorized_handlers(); } return response; } ); _session->registerHandler( [this]( const dap::DisconnectRequest& request ) { return handle_disconnect( request ); } ); _session->registerSentHandler( [this]( const dap::ResponseOrError& response ) { after_disconnect( response ); } ); _session->registerSentHandler( [this]( const dap::ResponseOrError& response ) { after_initialize( response ); } ); // Error handler _session->onError( [this]( const char* msg ) { on_error( msg ); } ); // Attach the SocketReaderWriter to the Session and begin processing events. _session->bind( _rw, [&]() { POLLOG_INFOLN( "Debugger#{} session endpoint closed.", _instance ); } ); while ( !Clib::exit_signalled && !_exit_sent && _rw->isOpen() ) { pol_sleep_ms( 1000 ); } { // Detach debugger in case a DisconnectRequest was not sent. PolLock lock; if ( _uoexec_wptr.exists() ) { UOExecutor* exec = _uoexec_wptr.get_weakptr(); if ( exec->in_debugger_holdlist() ) exec->revive_debugged(); _variable_handles.reset(); _global_scope_handle = 0; exec->detach_debugger(); _uoexec_wptr.clear(); } } _session.reset(); // Close the socket endpoint if necessary. if ( _rw->isOpen() ) { _rw->close(); } POLLOG_INFOLN( "Debugger#{} client thread closing.", _instance ); } } // namespace DAP } // namespace Network } // namespace Pol