Add VSCode protocol async commands read and execution.
authorMikhail Kurinnoi <m.kurinnoi@samsung.com>
Tue, 21 Dec 2021 10:26:34 +0000 (02:26 -0800)
committerAlexander Soldatov/Platform Lab /SRR/Staff Engineer/Samsung Electronics <soldatov.a@samsung.com>
Tue, 1 Feb 2022 16:21:07 +0000 (19:21 +0300)
Add VSCode protocol 'cancel' command implementation.

src/debugger/evalwaiter.cpp
src/debugger/evalwaiter.h
src/debugger/manageddebugger.cpp
src/debugger/manageddebugger.h
src/debugger/variables.cpp
src/interfaces/idebugger.h
src/interfaces/iprotocol.h
src/protocols/vscodeprotocol.cpp
src/protocols/vscodeprotocol.h

index 3270d30f5c000aac7d4ef5e1377572ef2e88004f..172cd78b7a3c10e7c01c6265f3433c3feb5aa75d 100644 (file)
@@ -40,6 +40,20 @@ bool EvalWaiter::IsEvalRunning()
     return !!m_evalResult;\r
 }\r
 \r
+void EvalWaiter::CancelEvalRunning()\r
+{\r
+    std::lock_guard<std::mutex> lock(m_evalResultMutex);\r
+\r
+    if (!m_evalResult)\r
+        return;\r
+\r
+    ToRelease<ICorDebugEval2> iCorEval2;\r
+    if (SUCCEEDED(m_evalResult->pEval->Abort()) ||\r
+        (SUCCEEDED(m_evalResult->pEval->QueryInterface(IID_ICorDebugEval2, (LPVOID*) &iCorEval2)) &&\r
+         SUCCEEDED(iCorEval2->RudeAbort())))\r
+        m_evalCanceled = true;\r
+}\r
+\r
 std::future<std::unique_ptr<EvalWaiter::evalResultData_t> > EvalWaiter::RunEval(\r
     ICorDebugProcess *pProcess,\r
     ICorDebugThread *pThread,\r
@@ -189,10 +203,12 @@ HRESULT EvalWaiter::WaitEvalResult(ICorDebugThread *pThread,
             return E_FAIL;\r
         }\r
     };\r
+\r
+    m_evalCanceled = false;\r
     HRESULT ret = WaitResult();\r
 \r
     if (ret == CORDBG_S_FUNC_EVAL_ABORTED)\r
-        ret = COR_E_TIMEOUT;\r
+        ret = m_evalCanceled ? COR_E_OPERATIONCANCELED : COR_E_TIMEOUT;\r
 \r
     ChangeThreadsState(THREAD_RUN);\r
     return ret;\r
@@ -215,9 +231,9 @@ HRESULT EvalWaiter::ManagedCallbackCustomNotification(ICorDebugThread *pThread)
 \r
     HRESULT Status;\r
     ToRelease<ICorDebugEval2> iCorEval2;\r
-    if (FAILED(Status = pEval->Abort()) ||\r
-        FAILED(Status = pEval->QueryInterface(IID_ICorDebugEval2, (LPVOID*) &iCorEval2)) ||\r
-        FAILED(Status = iCorEval2->RudeAbort()))\r
+    if (FAILED(Status = pEval->Abort()) &&\r
+        (FAILED(Status = pEval->QueryInterface(IID_ICorDebugEval2, (LPVOID*) &iCorEval2)) ||\r
+         FAILED(Status = iCorEval2->RudeAbort())))\r
     {\r
         LOGE("Can't abort evaluation in custom notification callback, %0x", Status);\r
         return Status;\r
index 6ac457d2d1d40880c095c796ecb011564ff8c9a3..b212887a868a154c5b0147ad861088752d5968cf 100644 (file)
@@ -22,9 +22,10 @@ public:
 \r
     typedef std::function<HRESULT(ICorDebugEval*)> WaitEvalResultCallback;\r
 \r
-    EvalWaiter(std::shared_ptr<Threads> &sharedThreads) : m_sharedThreads(sharedThreads) {}\r
+    EvalWaiter(std::shared_ptr<Threads> &sharedThreads) : m_sharedThreads(sharedThreads), m_evalCanceled(false) {}\r
 \r
     bool IsEvalRunning();\r
+    void CancelEvalRunning();\r
     ICorDebugEval *FindEvalForThread(ICorDebugThread *pThread);\r
 \r
     HRESULT WaitEvalResult(ICorDebugThread *pThread,\r
@@ -38,6 +39,7 @@ public:
 private:\r
 \r
     std::shared_ptr<Threads> m_sharedThreads;\r
+    bool m_evalCanceled;\r
 \r
     struct evalResultData_t\r
     {\r
index e4d963ef6179ab96dbcf25163143dc3e113ecd08..7f8ce161f27957763d7806ea85e41bdefcb9fd33 100644 (file)
@@ -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();
index cbb2b910c78cd63624f67343a91e65d3f4a22da9..741e981424b71dcfcf8c70d451164988069f33bb 100644 (file)
@@ -158,6 +158,7 @@ public:
     HRESULT GetVariables(uint32_t variablesReference, VariablesFilter filter, int start, int count, std::vector<Variable> &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;
index 4bb7d15fed6110dd6d261153acab7b9f598169f8..746bf7db7e038034a76a8f7c1defeeae5f77ee72 100644 (file)
@@ -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<ICorDebugValue> 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)
index f3885e13678a2d55862ae2e41016fd1ce5c73341..c377c873918eec55bf8b4e25cb96a5284b17067c 100644 (file)
@@ -95,6 +95,7 @@ public:
     virtual HRESULT GetVariables(uint32_t variablesReference, VariablesFilter filter, int start, int count, std::vector<Variable> &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;
index 5e491cb0c11675766cf2e69956179ccac985ca75..b905ef18513774e17fe664e42a44a25693e8c3da 100644 (file)
@@ -7,6 +7,7 @@
 #include <string>
 #include <vector>
 #include <memory>
+#include <atomic>
 #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<bool> m_exit;
     std::shared_ptr<IDebugger> m_sharedDebugger;
 
     // File streams used to read commands and write responses.
index ede0b35af678c433735daba7b9c6fc4c9ec76f6a..5098a4983f5fe8ece68967c72ca4d978ea80d004 100644 (file)
@@ -7,9 +7,12 @@
 #include <vector>
 #include <unordered_map>
 #include <map>
+#include <unordered_set>
 #include <iomanip>
 #include <sstream>
 #include <algorithm>
+#include <thread>
+#include <future>
 
 // 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<std::string> g_syncCommandExecutionSet{
+        "configurationDone", "disconnect", "terminate"};
+    // Commands, that trigger command queue canceling routine.
+    const std::unordered_set<std::string> 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<std::string> 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<std::mutex> 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<HRESULT(
-    const json &arguments,
-    json &body)> 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<std::mutex> 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<IDebugger> &sharedDebugger, std::string &fileExec, std::vector<std::string> &execArgs,
+                             const std::string &command, const json &arguments, json &body)
 {
+    typedef std::function<HRESULT(const json &arguments, json &body)> CommandCallback;
     static std::unordered_map<std::string, CommandCallback> 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<std::string> filters = arguments.value("filters", std::vector<std::string>());
         std::vector<std::map<std::string, std::string>> filterOptions = arguments.value("filterOptions", std::vector<std::map<std::string, std::string>>());
 
@@ -498,21 +520,21 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar
 
         HRESULT Status;
         std::vector<Breakpoint> 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<LineBreakpoint> 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<Breakpoint> 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::string{});
         std::map<std::string, std::string> 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<std::string> args = arguments.value("args", std::vector<std::string>());
         args.insert(args.begin(), arguments.at("program").get<std::string>());
 
-        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<Thread> 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<bool>() ? 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<StackFrame> 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<Scope> 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<Variable> 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<FuncBreakpoint> funcBreakpoints;
@@ -804,7 +824,7 @@ HRESULT VSCodeProtocol::HandleCommand(const std::string &command, const json &ar
         }
 
         std::vector<Breakpoint> 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<IDebugger> &sharedDebugger, std::string &fileExec, std::vector<std::string> &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<std::mutex> 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<HRESULT> 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<VSCodeProtocol::CommandQueueEntry>::iterator VSCodeProtocol::CancelCommand(const std::list<VSCodeProtocol::CommandQueueEntry>::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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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;
         }
     }
index a904ac30e2118e52ea1031336487a6567a78eabf..f710151c3532fb2350b053a4e78b9814b305fae9 100644 (file)
@@ -6,6 +6,8 @@
 #include <fstream>
 #include <mutex>
 #include <string>
+#include <list>
+#include <condition_variable>
 
 #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<std::string> 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<CommandQueueEntry> m_commandsQueue;
+
+    void CommandsWorker();
+    std::list<CommandQueueEntry>::iterator CancelCommand(const std::list<CommandQueueEntry>::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<std::string> &args) override
     {