From: Mikhail Kurinnoi Date: Tue, 21 Dec 2021 10:26:34 +0000 (-0800) Subject: Add VSCode protocol async commands read and execution. X-Git-Tag: submit/tizen/20220215.173642~2 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=9cfd2ac77c84f619c7f98f94a6b0b26a3328fe19;p=sdk%2Ftools%2Fnetcoredbg.git Add VSCode protocol async commands read and execution. Add VSCode protocol 'cancel' command implementation. --- diff --git a/src/debugger/evalwaiter.cpp b/src/debugger/evalwaiter.cpp index 3270d30..172cd78 100644 --- a/src/debugger/evalwaiter.cpp +++ b/src/debugger/evalwaiter.cpp @@ -40,6 +40,20 @@ bool EvalWaiter::IsEvalRunning() return !!m_evalResult; } +void EvalWaiter::CancelEvalRunning() +{ + std::lock_guard lock(m_evalResultMutex); + + if (!m_evalResult) + return; + + ToRelease iCorEval2; + if (SUCCEEDED(m_evalResult->pEval->Abort()) || + (SUCCEEDED(m_evalResult->pEval->QueryInterface(IID_ICorDebugEval2, (LPVOID*) &iCorEval2)) && + SUCCEEDED(iCorEval2->RudeAbort()))) + m_evalCanceled = true; +} + std::future > EvalWaiter::RunEval( ICorDebugProcess *pProcess, ICorDebugThread *pThread, @@ -189,10 +203,12 @@ HRESULT EvalWaiter::WaitEvalResult(ICorDebugThread *pThread, return E_FAIL; } }; + + m_evalCanceled = false; HRESULT ret = WaitResult(); if (ret == CORDBG_S_FUNC_EVAL_ABORTED) - ret = COR_E_TIMEOUT; + ret = m_evalCanceled ? COR_E_OPERATIONCANCELED : COR_E_TIMEOUT; ChangeThreadsState(THREAD_RUN); return ret; @@ -215,9 +231,9 @@ HRESULT EvalWaiter::ManagedCallbackCustomNotification(ICorDebugThread *pThread) HRESULT Status; ToRelease iCorEval2; - if (FAILED(Status = pEval->Abort()) || - FAILED(Status = pEval->QueryInterface(IID_ICorDebugEval2, (LPVOID*) &iCorEval2)) || - FAILED(Status = iCorEval2->RudeAbort())) + if (FAILED(Status = pEval->Abort()) && + (FAILED(Status = pEval->QueryInterface(IID_ICorDebugEval2, (LPVOID*) &iCorEval2)) || + FAILED(Status = iCorEval2->RudeAbort()))) { LOGE("Can't abort evaluation in custom notification callback, %0x", Status); return Status; diff --git a/src/debugger/evalwaiter.h b/src/debugger/evalwaiter.h index 6ac457d..b212887 100644 --- a/src/debugger/evalwaiter.h +++ b/src/debugger/evalwaiter.h @@ -22,9 +22,10 @@ public: typedef std::function WaitEvalResultCallback; - EvalWaiter(std::shared_ptr &sharedThreads) : m_sharedThreads(sharedThreads) {} + EvalWaiter(std::shared_ptr &sharedThreads) : m_sharedThreads(sharedThreads), m_evalCanceled(false) {} bool IsEvalRunning(); + void CancelEvalRunning(); ICorDebugEval *FindEvalForThread(ICorDebugThread *pThread); HRESULT WaitEvalResult(ICorDebugThread *pThread, @@ -38,6 +39,7 @@ public: private: std::shared_ptr m_sharedThreads; + bool m_evalCanceled; struct evalResultData_t { diff --git a/src/debugger/manageddebugger.cpp b/src/debugger/manageddebugger.cpp index e4d963e..7f8ce16 100644 --- a/src/debugger/manageddebugger.cpp +++ b/src/debugger/manageddebugger.cpp @@ -1059,6 +1059,13 @@ HRESULT ManagedDebugger::Evaluate(FrameId frameId, const std::string &expression return m_sharedVariables->Evaluate(m_iCorProcess, frameId, expression, variable, output); } +void ManagedDebugger::CancelEvalRunning() +{ + LogFuncEntry(); + + m_sharedEvalWaiter->CancelEvalRunning(); +} + HRESULT ManagedDebugger::SetVariable(const std::string &name, const std::string &value, uint32_t ref, std::string &output) { LogFuncEntry(); diff --git a/src/debugger/manageddebugger.h b/src/debugger/manageddebugger.h index cbb2b91..741e981 100644 --- a/src/debugger/manageddebugger.h +++ b/src/debugger/manageddebugger.h @@ -158,6 +158,7 @@ public: HRESULT GetVariables(uint32_t variablesReference, VariablesFilter filter, int start, int count, std::vector &variables) override; int GetNamedVariables(uint32_t variablesReference) override; HRESULT Evaluate(FrameId frameId, const std::string &expression, Variable &variable, std::string &output) override; + void CancelEvalRunning() override; HRESULT SetVariable(const std::string &name, const std::string &value, uint32_t ref, std::string &output) override; HRESULT SetExpression(FrameId frameId, const std::string &expression, int evalFlags, const std::string &value, std::string &output) override; HRESULT GetExceptionInfo(ThreadId threadId, ExceptionInfo &exceptionInfo) override; diff --git a/src/debugger/variables.cpp b/src/debugger/variables.cpp index 4bb7d15..746bf7d 100644 --- a/src/debugger/variables.cpp +++ b/src/debugger/variables.cpp @@ -129,8 +129,10 @@ HRESULT Variables::FetchFieldsAndProperties( if (currentIndex >= childEnd) return S_OK; + // Note, in this case error is not fatal, but if protocol side need cancel command execution, stop walk and return error to caller. ToRelease iCorResultValue; - getValue(&iCorResultValue, evalFlags); // no result check here, since error is result too + if (getValue(&iCorResultValue, evalFlags) == COR_E_OPERATIONCANCELED) + return COR_E_OPERATIONCANCELED; std::string className; if (pType) diff --git a/src/interfaces/idebugger.h b/src/interfaces/idebugger.h index f3885e1..c377c87 100644 --- a/src/interfaces/idebugger.h +++ b/src/interfaces/idebugger.h @@ -95,6 +95,7 @@ public: virtual HRESULT GetVariables(uint32_t variablesReference, VariablesFilter filter, int start, int count, std::vector &variables) = 0; virtual int GetNamedVariables(uint32_t variablesReference) = 0; virtual HRESULT Evaluate(FrameId frameId, const std::string &expression, Variable &variable, std::string &output) = 0; + virtual void CancelEvalRunning() = 0; virtual HRESULT SetVariable(const std::string &name, const std::string &value, uint32_t ref, std::string &output) = 0; virtual HRESULT SetExpression(FrameId frameId, const std::string &expression, int evalFlags, const std::string &value, std::string &output) = 0; virtual HRESULT GetExceptionInfo(ThreadId threadId, ExceptionInfo &exceptionInfo) = 0; diff --git a/src/interfaces/iprotocol.h b/src/interfaces/iprotocol.h index 5e491cb..b905ef1 100644 --- a/src/interfaces/iprotocol.h +++ b/src/interfaces/iprotocol.h @@ -7,6 +7,7 @@ #include #include #include +#include #include "interfaces/types.h" #include "utils/string_view.h" #include "utils/streams.h" @@ -20,7 +21,7 @@ class IDebugger; class IProtocol { protected: - bool m_exit; + std::atomic m_exit; std::shared_ptr m_sharedDebugger; // File streams used to read commands and write responses. diff --git a/src/protocols/vscodeprotocol.cpp b/src/protocols/vscodeprotocol.cpp index ede0b35..5098a49 100644 --- a/src/protocols/vscodeprotocol.cpp +++ b/src/protocols/vscodeprotocol.cpp @@ -7,9 +7,12 @@ #include #include #include +#include #include #include #include +#include +#include // note: order matters, vscodeprotocol.h should be included before winerror.h #include "protocols/vscodeprotocol.h" @@ -34,6 +37,23 @@ namespace {"all", ExceptionBreakpointFilter::THROW}, {"user-unhandled", ExceptionBreakpointFilter::USER_UNHANDLED}}; + const std::string TWO_CRLF("\r\n\r\n"); + const std::string CONTENT_LENGTH("Content-Length: "); + + const std::string LOG_COMMAND("-> (C) "); + const std::string LOG_RESPONSE("<- (R) "); + const std::string LOG_EVENT("<- (E) "); + + // Make sure we continue add new commands into queue only after current command execution is finished. + // Note, configurationDone: prevent deadlock in _dup() call during std::getline() from stdin in main thread. + const std::unordered_set g_syncCommandExecutionSet{ + "configurationDone", "disconnect", "terminate"}; + // Commands, that trigger command queue canceling routine. + const std::unordered_set g_cancelCommandQueueSet{ + "disconnect", "terminate", "continue", "next", "stepIn", "stepOut"}; + // Don't cancel commands related to debugger configuration. For example, breakpoint setup could be done in any time (even if process don't attached at all). + const std::unordered_set g_debuggerSetupCommandSet{ + "initialize", "setExceptionBreakpoints", "configurationDone", "setBreakpoints", "launch", "disconnect", "terminate", "attach", "setFunctionBreakpoints"}; } // unnamed namespace void to_json(json &j, const Source &s) { @@ -365,6 +385,30 @@ void VSCodeProtocol::EmitExecEvent(PID pid, const std::string& argv0) EmitEvent("process", body); } +static void AddCapabilitiesTo(json &capabilities) +{ + capabilities["supportsConfigurationDoneRequest"] = true; + capabilities["supportsFunctionBreakpoints"] = true; + capabilities["supportsConditionalBreakpoints"] = true; + capabilities["supportTerminateDebuggee"] = true; + capabilities["supportsSetVariable"] = true; + capabilities["supportsSetExpression"] = true; + capabilities["supportsTerminateRequest"] = true; + capabilities["supportsCancelRequest"] = true; + + capabilities["supportsExceptionInfoRequest"] = true; + capabilities["supportsExceptionFilterOptions"] = true; + json excFilters = json::array(); + for (const auto &entry : g_VSCodeFilters) + { + json filter{{"filter", entry.first}, + {"label", entry.first}}; + excFilters.push_back(filter); + } + capabilities["exceptionBreakpointFilters"] = excFilters; + capabilities["supportsExceptionOptions"] = false; // TODO add implementation +} + void VSCodeProtocol::EmitCapabilitiesEvent() { LogFuncEntry(); @@ -384,68 +428,46 @@ void VSCodeProtocol::Cleanup() } -static std::string VSCodeSeq(uint64_t id) -{ - return std::string("{\"seq\":" + std::to_string(id) + ","); -} - -void VSCodeProtocol::EmitEvent(const std::string &name, const nlohmann::json &body) +// Caller must care about m_outMutex. +void VSCodeProtocol::EmitMessage(nlohmann::json &message, std::string &output) { - std::lock_guard lock(m_outMutex); - json response; - response["type"] = "event"; - response["event"] = name; - response["body"] = body; - std::string output = response.dump(); - output = VSCodeSeq(m_seqCounter) + output.substr(1); + message["seq"] = std::to_string(m_seqCounter); ++m_seqCounter; - + output = message.dump(); cout << CONTENT_LENGTH << output.size() << TWO_CRLF << output; cout.flush(); - Log(LOG_EVENT, output); } -typedef std::function CommandCallback; - -void VSCodeProtocol::AddCapabilitiesTo(json &capabilities) +void VSCodeProtocol::EmitMessageWithLog(const std::string &message_prefix, nlohmann::json &message) { - capabilities["supportsConfigurationDoneRequest"] = true; - capabilities["supportsFunctionBreakpoints"] = true; - capabilities["supportsConditionalBreakpoints"] = true; - capabilities["supportTerminateDebuggee"] = true; - capabilities["supportsSetVariable"] = true; - capabilities["supportsSetExpression"] = true; - capabilities["supportsTerminateRequest"] = true; + std::lock_guard lock(m_outMutex); + std::string output; + EmitMessage(message, output); + Log(message_prefix, output); +} - capabilities["supportsExceptionInfoRequest"] = true; - capabilities["supportsExceptionFilterOptions"] = true; - json excFilters = json::array(); - for (const auto &entry : g_VSCodeFilters) - { - json filter{{"filter", entry.first}, - {"label", entry.first}}; - excFilters.push_back(filter); - } - capabilities["exceptionBreakpointFilters"] = excFilters; - capabilities["supportsExceptionOptions"] = false; // TODO add implementation +void VSCodeProtocol::EmitEvent(const std::string &name, const nlohmann::json &body) +{ + json message; + message["type"] = "event"; + message["event"] = name; + message["body"] = body; + EmitMessageWithLog(LOG_EVENT, message); } -HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &arguments, json &body) +static HRESULT HandleCommand(std::shared_ptr &sharedDebugger, std::string &fileExec, std::vector &execArgs, + const std::string &command, const json &arguments, json &body) { + typedef std::function CommandCallback; static std::unordered_map commands { - { "initialize", [this](const json &arguments, json &body){ - - EmitCapabilitiesEvent(); - - m_sharedDebugger->Initialize(); + { "initialize", [&](const json &arguments, json &body){ + sharedDebugger->Initialize(); AddCapabilitiesTo(body); return S_OK; } }, - { "setExceptionBreakpoints", [this](const json &arguments, json &body) { + { "setExceptionBreakpoints", [&](const json &arguments, json &body) { std::vector filters = arguments.value("filters", std::vector()); std::vector> filterOptions = arguments.value("filterOptions", std::vector>()); @@ -498,21 +520,21 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar HRESULT Status; std::vector breakpoints; - IfFailRet(m_sharedDebugger->SetExceptionBreakpoints(exceptionBreakpoints, breakpoints)); + IfFailRet(sharedDebugger->SetExceptionBreakpoints(exceptionBreakpoints, breakpoints)); // TODO form body with breakpoints (optional output, MS vsdbg don't provide it for VSCode IDE now) // body["breakpoints"] = breakpoints; return S_OK; } }, - { "configurationDone", [this](const json &arguments, json &body){ - return m_sharedDebugger->ConfigurationDone(); + { "configurationDone", [&](const json &arguments, json &body){ + return sharedDebugger->ConfigurationDone(); } }, - { "exceptionInfo", [this](const json &arguments, json &body) { + { "exceptionInfo", [&](const json &arguments, json &body) { HRESULT Status; ThreadId threadId{int(arguments.at("threadId"))}; ExceptionInfo exceptionInfo; - IfFailRet(m_sharedDebugger->GetExceptionInfo(threadId, exceptionInfo)); + IfFailRet(sharedDebugger->GetExceptionInfo(threadId, exceptionInfo)); body["exceptionId"] = exceptionInfo.exceptionId; body["description"] = exceptionInfo.description; @@ -520,7 +542,7 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar body["details"] = FormJsonForExceptionDetails(exceptionInfo.details); return S_OK; } }, - { "setBreakpoints", [this](const json &arguments, json &body){ + { "setBreakpoints", [&](const json &arguments, json &body){ HRESULT Status; std::vector lineBreakpoints; @@ -528,13 +550,13 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar lineBreakpoints.emplace_back(std::string(), b.at("line"), b.value("condition", std::string())); std::vector breakpoints; - IfFailRet(m_sharedDebugger->SetLineBreakpoints(arguments.at("source").at("path"), lineBreakpoints, breakpoints)); + IfFailRet(sharedDebugger->SetLineBreakpoints(arguments.at("source").at("path"), lineBreakpoints, breakpoints)); body["breakpoints"] = breakpoints; return S_OK; } }, - {"launch", [this](const json &arguments, json &body) { + { "launch", [&](const json &arguments, json &body){ auto cwdIt = arguments.find("cwd"); const std::string cwd(cwdIt != arguments.end() ? cwdIt.value().get() : std::string{}); std::map env; @@ -549,27 +571,27 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar env.clear(); } - m_sharedDebugger->SetJustMyCode(arguments.value("justMyCode", true)); // MS vsdbg have "justMyCode" enabled by default. - m_sharedDebugger->SetStepFiltering(arguments.value("enableStepFiltering", true)); // MS vsdbg have "enableStepFiltering" enabled by default. + sharedDebugger->SetJustMyCode(arguments.value("justMyCode", true)); // MS vsdbg have "justMyCode" enabled by default. + sharedDebugger->SetStepFiltering(arguments.value("enableStepFiltering", true)); // MS vsdbg have "enableStepFiltering" enabled by default. - if (!m_fileExec.empty()) - return m_sharedDebugger->Launch(m_fileExec, m_execArgs, env, cwd, arguments.value("stopAtEntry", false)); + if (!fileExec.empty()) + return sharedDebugger->Launch(fileExec, execArgs, env, cwd, arguments.value("stopAtEntry", false)); std::vector args = arguments.value("args", std::vector()); args.insert(args.begin(), arguments.at("program").get()); - return m_sharedDebugger->Launch("dotnet", args, env, cwd, arguments.value("stopAtEntry", false)); + return sharedDebugger->Launch("dotnet", args, env, cwd, arguments.value("stopAtEntry", false)); } }, - { "threads", [this](const json &arguments, json &body){ + { "threads", [&](const json &arguments, json &body){ HRESULT Status; std::vector threads; - IfFailRet(m_sharedDebugger->GetThreads(threads)); + IfFailRet(sharedDebugger->GetThreads(threads)); body["threads"] = threads; return S_OK; } }, - { "disconnect", [this](const json &arguments, json &body){ + { "disconnect", [&](const json &arguments, json &body){ auto terminateArgIter = arguments.find("terminateDebuggee"); IDebugger::DisconnectAction action; if (terminateArgIter == arguments.end()) @@ -577,23 +599,22 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar else action = terminateArgIter.value().get() ? IDebugger::DisconnectAction::DisconnectTerminate : IDebugger::DisconnectAction::DisconnectDetach; - m_sharedDebugger->Disconnect(action); + sharedDebugger->Disconnect(action); - m_exit = true; return S_OK; } }, - { "terminate", [this](const json &arguments, json &body){ - m_sharedDebugger->Disconnect(IDebugger::DisconnectAction::DisconnectTerminate); + { "terminate", [&](const json &arguments, json &body){ + sharedDebugger->Disconnect(IDebugger::DisconnectAction::DisconnectTerminate); return S_OK; } }, - { "stackTrace", [this](const json &arguments, json &body){ + { "stackTrace", [&](const json &arguments, json &body){ HRESULT Status; int totalFrames = 0; ThreadId threadId{int(arguments.at("threadId"))}; std::vector stackFrames; - IfFailRet(m_sharedDebugger->GetStackTrace( + IfFailRet(sharedDebugger->GetStackTrace( threadId, FrameLevel{arguments.value("startFrame", 0)}, unsigned(arguments.value("levels", 0)), @@ -606,40 +627,39 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar return S_OK; } }, - { "continue", [this](const json &arguments, json &body){ + { "continue", [&](const json &arguments, json &body){ body["allThreadsContinued"] = true; ThreadId threadId{int(arguments.at("threadId"))}; body["threadId"] = int(threadId); - return m_sharedDebugger->Continue(threadId); + return sharedDebugger->Continue(threadId); } }, - { "pause", [this](const json &arguments, json &body){ + { "pause", [&](const json &arguments, json &body){ // Ignore `threadId` argument, since only pause for all threads are supported now. - return m_sharedDebugger->Pause(); + return sharedDebugger->Pause(); } }, - { "next", [this](const json &arguments, json &body){ - return m_sharedDebugger->StepCommand(ThreadId{int(arguments.at("threadId"))}, IDebugger::StepType::STEP_OVER); + { "next", [&](const json &arguments, json &body){ + return sharedDebugger->StepCommand(ThreadId{int(arguments.at("threadId"))}, IDebugger::StepType::STEP_OVER); } }, - { "stepIn", [this](const json &arguments, json &body){ - return m_sharedDebugger->StepCommand(ThreadId{int(arguments.at("threadId"))}, IDebugger::StepType::STEP_IN); + { "stepIn", [&](const json &arguments, json &body){ + return sharedDebugger->StepCommand(ThreadId{int(arguments.at("threadId"))}, IDebugger::StepType::STEP_IN); } }, - { "stepOut", [this](const json &arguments, json &body){ - return m_sharedDebugger->StepCommand(ThreadId{int(arguments.at("threadId"))}, IDebugger::StepType::STEP_OUT); + { "stepOut", [&](const json &arguments, json &body){ + return sharedDebugger->StepCommand(ThreadId{int(arguments.at("threadId"))}, IDebugger::StepType::STEP_OUT); } }, - { "scopes", [this](const json &arguments, json &body){ + { "scopes", [&](const json &arguments, json &body){ HRESULT Status; std::vector scopes; FrameId frameId{int(arguments.at("frameId"))}; - IfFailRet(m_sharedDebugger->GetScopes(frameId, scopes)); + IfFailRet(sharedDebugger->GetScopes(frameId, scopes)); body["scopes"] = scopes; return S_OK; } }, - { "variables", [this](const json &arguments, json &body){ + { "variables", [&](const json &arguments, json &body){ HRESULT Status; - - std::string filterName = arguments.value("filter", ""); + std::string filterName = arguments.value("filter", ""); VariablesFilter filter = VariablesBoth; if (filterName == "named") filter = VariablesNamed; @@ -647,7 +667,7 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar filter = VariablesIndexed; std::vector variables; - IfFailRet(m_sharedDebugger->GetVariables( + IfFailRet(sharedDebugger->GetVariables( arguments.at("variablesReference"), filter, arguments.value("start", 0), @@ -658,14 +678,14 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar return S_OK; } }, - { "evaluate", [this](const json &arguments, json &body){ + { "evaluate", [&](const json &arguments, json &body){ HRESULT Status; std::string expression = arguments.at("expression"); FrameId frameId([&](){ auto frameIdIter = arguments.find("frameId"); if (frameIdIter == arguments.end()) { - ThreadId threadId = m_sharedDebugger->GetLastStoppedThreadId(); + ThreadId threadId = sharedDebugger->GetLastStoppedThreadId(); return FrameId{threadId, FrameLevel{0}}; } else { @@ -678,7 +698,7 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar // https://github.com/OmniSharp/omnisharp-vscode/issues/3173 Variable variable; std::string output; - Status = m_sharedDebugger->Evaluate(frameId, expression, variable, output); + Status = sharedDebugger->Evaluate(frameId, expression, variable, output); if (FAILED(Status)) { if (output.empty()) @@ -703,7 +723,7 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar } return S_OK; } }, - { "setExpression", [this](const json &arguments, json &body){ + { "setExpression", [&](const json &arguments, json &body){ HRESULT Status; std::string expression = arguments.at("expression"); std::string value = arguments.at("value"); @@ -711,7 +731,7 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar auto frameIdIter = arguments.find("frameId"); if (frameIdIter == arguments.end()) { - ThreadId threadId = m_sharedDebugger->GetLastStoppedThreadId(); + ThreadId threadId = sharedDebugger->GetLastStoppedThreadId(); return FrameId{threadId, FrameLevel{0}}; } else { @@ -723,7 +743,7 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar // VSCode don't support evaluation flags, we can't disable implicit function calls during evaluation. // https://github.com/OmniSharp/omnisharp-vscode/issues/3173 std::string output; - Status = m_sharedDebugger->SetExpression(frameId, expression, defaultEvalFlags, value, output); + Status = sharedDebugger->SetExpression(frameId, expression, defaultEvalFlags, value, output); if (FAILED(Status)) { if (output.empty()) @@ -741,7 +761,7 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar body["value"] = output; return S_OK; } }, - { "attach", [this](const json &arguments, json &body){ + { "attach", [&](const json &arguments, json &body){ int processId; const json &processIdArg = arguments.at("processId"); @@ -752,9 +772,9 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar else return E_INVALIDARG; - return m_sharedDebugger->Attach(processId); + return sharedDebugger->Attach(processId); } }, - { "setVariable", [this](const json &arguments, json &body) { + { "setVariable", [&](const json &arguments, json &body) { HRESULT Status; std::string name = arguments.at("name"); @@ -762,7 +782,7 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar int ref = arguments.at("variablesReference"); std::string output; - Status = m_sharedDebugger->SetVariable(name, value, ref, output); + Status = sharedDebugger->SetVariable(name, value, ref, output); if (FAILED(Status)) { body["message"] = output; @@ -773,7 +793,7 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar return S_OK; } }, - { "setFunctionBreakpoints", [this](const json &arguments, json &body) { + { "setFunctionBreakpoints", [&](const json &arguments, json &body) { HRESULT Status = S_OK; std::vector funcBreakpoints; @@ -804,7 +824,7 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar } std::vector breakpoints; - IfFailRet(m_sharedDebugger->SetFuncBreakpoints(funcBreakpoints, breakpoints)); + IfFailRet(sharedDebugger->SetFuncBreakpoints(funcBreakpoints, breakpoints)); body["breakpoints"] = breakpoints; @@ -821,10 +841,23 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar return command_it->second(arguments, body); } -const std::string VSCodeProtocol::TWO_CRLF("\r\n\r\n"); -const std::string VSCodeProtocol::CONTENT_LENGTH("Content-Length: "); +static HRESULT HandleCommandJSON(std::shared_ptr &sharedDebugger, std::string &fileExec, std::vector &execArgs, + const std::string &command, const json &arguments, json &body) +{ + try + { + return HandleCommand(sharedDebugger, fileExec, execArgs, command, arguments, body); + } + catch (nlohmann::detail::exception& ex) + { + LOGE("JSON error: %s", ex.what()); + body["message"] = std::string("can't parse: ") + ex.what(); + } + + return E_FAIL; +} -std::string VSCodeProtocol::ReadData() +static std::string ReadData(std::istream& cin) { // parse header (only content len) until empty line long content_len = -1; @@ -882,14 +915,121 @@ std::string VSCodeProtocol::ReadData() return result; } +void VSCodeProtocol::CommandsWorker() +{ + std::unique_lock lockCommandsMutex(m_commandsMutex); + + while (true) + { + while (m_commandsQueue.empty()) + { + // Note, during m_commandsCV.wait() (waiting for notify_one call with entry added into queue), + // m_commandsMutex will be unlocked (see std::condition_variable for more info). + m_commandsCV.wait(lockCommandsMutex); + } + + CommandQueueEntry c = std::move(m_commandsQueue.front()); + m_commandsQueue.pop_front(); + lockCommandsMutex.unlock(); + + // Check for ncdbg internal commands. + if (c.command == "ncdbg_disconnect") + { + m_sharedDebugger->Disconnect(); + break; + } + + json body = json::object(); + std::future future = std::async(std::launch::async, [&](){ + return HandleCommandJSON(m_sharedDebugger, m_fileExec, m_execArgs, c.command, c.arguments, body); + }); + HRESULT Status; + // Note, CommandsWorker() loop should never hangs, but even in case some command execution is timed out, + // this could be not critical issue. Let IDE decide. + + // MSVS debugger use config file, for Visual Studio 2022 Community Edition located at + // C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\Profiles\CSharp.vssettings + // Visual Studio have timeout setup for each type of requests, for example: + // LocalsTimeout = 1000 + // LongEvalTimeout = 10000 + // NormalEvalTimeout = 5000 + // QuickwatchTimeout = 15000 + // SetValueTimeout = 10000 + // ... + // we use max default timeout (15000), one timeout for all requests. + + // TODO add timeout configuration feature + std::future_status timeoutStatus = future.wait_for(std::chrono::milliseconds(15000)); + if (timeoutStatus == std::future_status::timeout) + { + body["message"] = "Command execution timed out."; + Status = COR_E_TIMEOUT; + } + else + Status = future.get(); + + if (SUCCEEDED(Status)) + { + c.response["success"] = true; + c.response["body"] = body; + } + else + { + if (body.find("message") == body.end()) + { + std::ostringstream ss; + ss << "Failed command '" << c.command << "' : " + << "0x" << std::setw(8) << std::setfill('0') << std::hex << Status; + c.response["message"] = ss.str(); + } + else + c.response["message"] = body["message"]; + + c.response["success"] = false; + } + + EmitMessageWithLog(LOG_RESPONSE, c.response); + + // Post command action. + if (g_syncCommandExecutionSet.find(c.command) != g_syncCommandExecutionSet.end()) + m_commandSyncCV.notify_one(); + if (c.command == "disconnect") + break; + + lockCommandsMutex.lock(); + } + + m_exit = true; +} + +// Caller must care about m_commandsMutex. +std::list::iterator VSCodeProtocol::CancelCommand(const std::list::iterator &iter) +{ + iter->response["success"] = false; + iter->response["message"] = std::string("Error processing '") + iter->command + std::string("' request. The operation was canceled."); + EmitMessageWithLog(LOG_RESPONSE, iter->response); + return m_commandsQueue.erase(iter); +} + void VSCodeProtocol::CommandLoop() { + std::thread commandsWorker{&VSCodeProtocol::CommandsWorker, this}; + + m_exit = false; + while (!m_exit) { - - std::string requestText = ReadData(); + std::string requestText = ReadData(cin); if (requestText.empty()) + { + CommandQueueEntry queueEntry; + queueEntry.command = "ncdbg_disconnect"; + std::lock_guard guardCommandsMutex(m_commandsMutex); + m_commandsQueue.clear(); + m_commandsQueue.emplace_back(std::move(queueEntry)); + m_commandsCV.notify_one(); // notify_one with lock break; + } { std::lock_guard lock(m_outMutex); @@ -901,8 +1041,9 @@ void VSCodeProtocol::CommandLoop() bad_format(const char *s) : invalid_argument(s) {} }; - json response; - try { + CommandQueueEntry queueEntry; + try + { json request = json::parse(requestText); // Variable `resp' is used to construct response and assign it to `response' @@ -913,78 +1054,93 @@ void VSCodeProtocol::CommandLoop() json resp; resp["type"] = "response"; resp["request_seq"] = request.at("seq"); - response = resp; + queueEntry.response = resp; - std::string command = request.at("command"); - resp["command"] = command; - response = resp; + queueEntry.command = request.at("command"); + resp["command"] = queueEntry.command; + queueEntry.response = resp; if (request["type"] != "request") throw bad_format("wrong request type!"); auto argIter = request.find("arguments"); - json arguments = (argIter == request.end() ? json::object() : argIter.value()); - - json body = json::object(); - HRESULT Status = HandleCommand(command, arguments, body); + queueEntry.arguments = (argIter == request.end() ? json::object() : argIter.value()); - if (SUCCEEDED(Status)) + // Pre command action. + if (queueEntry.command == "initialize") + EmitCapabilitiesEvent(); + else if (g_cancelCommandQueueSet.find(queueEntry.command) != g_cancelCommandQueueSet.end()) { - resp["success"] = true; - resp["body"] = body; + std::lock_guard guardCommandsMutex(m_commandsMutex); + m_sharedDebugger->CancelEvalRunning(); + + for (auto iter = m_commandsQueue.begin(); iter != m_commandsQueue.end();) + { + if (g_debuggerSetupCommandSet.find(iter->command) != g_debuggerSetupCommandSet.end()) + ++iter; + else + iter = CancelCommand(iter); + } } - else + // Note, in case "cancel" this is command implementation itself. + else if (queueEntry.command == "cancel") { - if (body.find("message") == body.end()) + auto requestId = queueEntry.arguments.at("requestId"); + std::unique_lock lockCommandsMutex(m_commandsMutex); + queueEntry.response["success"] = false; + for (auto iter = m_commandsQueue.begin(); iter != m_commandsQueue.end(); ++iter) { - std::ostringstream ss; - ss << "Failed command '" << command << "' : " - << "0x" << std::setw(8) << std::setfill('0') << std::hex << Status; - resp["message"] = ss.str(); + if (requestId != iter->response["request_seq"]) + continue; + + if (g_debuggerSetupCommandSet.find(iter->command) != g_debuggerSetupCommandSet.end()) + break; + + CancelCommand(iter); + + queueEntry.response["success"] = true; + break; } - else - resp["message"] = body["message"]; + lockCommandsMutex.unlock(); + + if (!queueEntry.response["success"]) + queueEntry.response["message"] = "CancelRequest is not supported for requestId."; - resp["success"] = false; + EmitMessageWithLog(LOG_RESPONSE, queueEntry.response); + continue; } - response = resp; + + std::unique_lock lockCommandsMutex(m_commandsMutex); + bool isCommandNeedSync = g_syncCommandExecutionSet.find(queueEntry.command) != g_syncCommandExecutionSet.end(); + m_commandsQueue.emplace_back(std::move(queueEntry)); + m_commandsCV.notify_one(); // notify_one with lock + + if (isCommandNeedSync) + m_commandSyncCV.wait(lockCommandsMutex); + + continue; } catch (nlohmann::detail::exception& ex) { LOGE("JSON error: %s", ex.what()); - response["type"] = "response"; - response["success"] = false; - response["message"] = std::string("can't parse: ") + ex.what(); + queueEntry.response["type"] = "response"; + queueEntry.response["success"] = false; + queueEntry.response["message"] = std::string("can't parse: ") + ex.what(); } catch (bad_format& ex) { LOGE("JSON error: %s", ex.what()); - response["type"] = "response"; - response["success"] = false; - response["message"] = std::string("can't parse: ") + ex.what(); + queueEntry.response["type"] = "response"; + queueEntry.response["success"] = false; + queueEntry.response["message"] = std::string("can't parse: ") + ex.what(); } - std::string output = response.dump(); - - std::lock_guard lock(m_outMutex); - - output = VSCodeSeq(m_seqCounter) + output.substr(1); - ++m_seqCounter; - - cout << CONTENT_LENGTH << output.size() << TWO_CRLF << output; - cout.flush(); - Log(LOG_RESPONSE, output); + EmitMessageWithLog(LOG_RESPONSE, queueEntry.response); } - if (!m_exit) - m_sharedDebugger->Disconnect(); - + commandsWorker.join(); } -const std::string VSCodeProtocol::LOG_COMMAND("-> (C) "); -const std::string VSCodeProtocol::LOG_RESPONSE("<- (R) "); -const std::string VSCodeProtocol::LOG_EVENT("<- (E) "); - void VSCodeProtocol::EngineLogging(const std::string &path) { if (path.empty()) @@ -1018,11 +1174,8 @@ void VSCodeProtocol::Log(const std::string &prefix, const std::string &text) {"category", "console"}, {"output", prefix + text + "\n"} }; - std::string output = response.dump(); - output = VSCodeSeq(m_seqCounter) + output.substr(1); - ++m_seqCounter; - cout << CONTENT_LENGTH << output.size() << TWO_CRLF << output; - cout.flush(); + std::string output; + EmitMessage(response, output); return; } } diff --git a/src/protocols/vscodeprotocol.h b/src/protocols/vscodeprotocol.h index a904ac3..f710151 100644 --- a/src/protocols/vscodeprotocol.h +++ b/src/protocols/vscodeprotocol.h @@ -6,6 +6,8 @@ #include #include #include +#include +#include #pragma warning (disable:4068) // Visual Studio should ignore GCC pragmas #pragma GCC diagnostic push @@ -20,13 +22,6 @@ namespace netcoredbg class VSCodeProtocol : public IProtocol { - static const std::string TWO_CRLF; - static const std::string CONTENT_LENGTH; - - static const std::string LOG_COMMAND; - static const std::string LOG_RESPONSE; - static const std::string LOG_EVENT; - std::mutex m_outMutex; enum { LogNone, @@ -39,17 +34,31 @@ class VSCodeProtocol : public IProtocol std::string m_fileExec; std::vector m_execArgs; - std::string ReadData(); - - void AddCapabilitiesTo(nlohmann::json &capabilities); + void EmitMessage(nlohmann::json &message, std::string &output); + void EmitMessageWithLog(const std::string &message_prefix, nlohmann::json &message); void EmitEvent(const std::string &name, const nlohmann::json &body); - HRESULT HandleCommand(const std::string &command, const nlohmann::json &arguments, nlohmann::json &body); void Log(const std::string &prefix, const std::string &text); + struct CommandQueueEntry + { + std::string command; + nlohmann::json arguments; + nlohmann::json response; + }; + + std::mutex m_commandsMutex; + std::condition_variable m_commandsCV; + std::condition_variable m_commandSyncCV; + std::list m_commandsQueue; + + void CommandsWorker(); + std::list::iterator CancelCommand(const std::list::iterator &iter); + public: - VSCodeProtocol(std::istream& input, std::ostream& output) : IProtocol(input, output), m_engineLogOutput(LogNone), m_seqCounter(1) {} + VSCodeProtocol(std::istream& input, std::ostream& output) : + IProtocol(input, output), m_engineLogOutput(LogNone), m_seqCounter(1) {} void EngineLogging(const std::string &path); void SetLaunchCommand(const std::string &fileExec, const std::vector &args) override {