forked from qt-creator/qt-creator
DAP : Refactor DapEngine
Extracted client part to additional class DapClient. Change-Id: Iba472d2fb2c2390f38ad9a3a75e9e1d9a76f912c Reviewed-by: <github-actions-qt-creator@cristianadam.eu> Reviewed-by: hjk <hjk@qt.io>
This commit is contained in:
@@ -28,6 +28,7 @@ add_qtc_plugin(Debugger
|
||||
console/consoleproxymodel.cpp console/consoleproxymodel.h
|
||||
console/consoleview.cpp console/consoleview.h
|
||||
dap/cmakedapengine.cpp dap/cmakedapengine.h
|
||||
dap/dapclient.cpp dap/dapclient.h
|
||||
dap/dapengine.cpp dap/dapengine.h
|
||||
dap/gdbdapengine.cpp dap/gdbdapengine.h
|
||||
debugger.qrc
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#include "cmakedapengine.h"
|
||||
|
||||
#include "dapclient.h"
|
||||
|
||||
#include <coreplugin/messagemanager.h>
|
||||
|
||||
#include <debugger/debuggermainwindow.h>
|
||||
@@ -69,6 +71,23 @@ private:
|
||||
const QString m_socketName;
|
||||
};
|
||||
|
||||
class CMakeDapClient : public DapClient
|
||||
{
|
||||
public:
|
||||
CMakeDapClient(std::unique_ptr<IDataProvider> provider)
|
||||
: DapClient(std::move(provider))
|
||||
{}
|
||||
|
||||
void sendInitialize()
|
||||
{
|
||||
postRequest("initialize",
|
||||
QJsonObject{{"clientID", "QtCreator"},
|
||||
{"clientName", "QtCreator"},
|
||||
{"adapterID", "cmake"},
|
||||
{"pathFormat", "path"}});
|
||||
}
|
||||
};
|
||||
|
||||
CMakeDapEngine::CMakeDapEngine()
|
||||
: DapEngine()
|
||||
{
|
||||
@@ -76,24 +95,6 @@ CMakeDapEngine::CMakeDapEngine()
|
||||
setDebuggerName("CmakeDAP");
|
||||
}
|
||||
|
||||
void CMakeDapEngine::handleDapStarted()
|
||||
{
|
||||
QTC_ASSERT(state() == EngineRunRequested, qCDebug(dapEngineLog) << state());
|
||||
|
||||
postDirectCommand({
|
||||
{"command", "initialize"},
|
||||
{"type", "request"},
|
||||
{"arguments", QJsonObject {
|
||||
{"clientID", "QtCreator"}, // The ID of the client using this adapter.
|
||||
{"clientName", "QtCreator"}, // The human-readable name of the client using this adapter.
|
||||
{"adapterID", "cmake"},
|
||||
{"pathFormat", "path"}
|
||||
}}
|
||||
});
|
||||
|
||||
qCDebug(dapEngineLog) << "handleDapStarted";
|
||||
}
|
||||
|
||||
void CMakeDapEngine::setupEngine()
|
||||
{
|
||||
QTC_ASSERT(state() == EngineSetupRequested, qCDebug(dapEngineLog) << state());
|
||||
@@ -101,24 +102,26 @@ void CMakeDapEngine::setupEngine()
|
||||
qCDebug(dapEngineLog) << "build system name"
|
||||
<< ProjectExplorer::ProjectTree::currentBuildSystem()->name();
|
||||
|
||||
std::unique_ptr<IDataProvider> dataProvider;
|
||||
if (TemporaryDirectory::masterDirectoryFilePath().osType() == Utils::OsType::OsTypeWindows) {
|
||||
m_dataGenerator = std::make_unique<LocalSocketDataProvider>("\\\\.\\pipe\\cmake-dap");
|
||||
dataProvider = std::make_unique<LocalSocketDataProvider>("\\\\.\\pipe\\cmake-dap");
|
||||
} else {
|
||||
m_dataGenerator = std::make_unique<LocalSocketDataProvider>(
|
||||
dataProvider = std::make_unique<LocalSocketDataProvider>(
|
||||
TemporaryDirectory::masterDirectoryPath() + "/cmake-dap.sock");
|
||||
}
|
||||
m_dapClient = std::make_unique<CMakeDapClient>(std::move(dataProvider));
|
||||
connectDataGeneratorSignals();
|
||||
|
||||
connect(ProjectExplorer::ProjectTree::currentBuildSystem(),
|
||||
&ProjectExplorer::BuildSystem::debuggingStarted,
|
||||
this,
|
||||
[this] { m_dataGenerator->start(); });
|
||||
[this] { m_dapClient->dataProvider()->start(); });
|
||||
|
||||
ProjectExplorer::ProjectTree::currentBuildSystem()->requestDebugging();
|
||||
|
||||
QTimer::singleShot(5000, this, [this] {
|
||||
if (!m_dataGenerator->isRunning()) {
|
||||
m_dataGenerator->kill();
|
||||
if (!m_dapClient->dataProvider()->isRunning()) {
|
||||
m_dapClient->dataProvider()->kill();
|
||||
MessageManager::writeDisrupting(
|
||||
"CMake server is not running. Please check that your CMake is 3.27 or higher.");
|
||||
return;
|
||||
|
||||
@@ -13,7 +13,6 @@ public:
|
||||
CMakeDapEngine();
|
||||
|
||||
private:
|
||||
void handleDapStarted() override;
|
||||
void setupEngine() override;
|
||||
|
||||
/* Needed for CMake support issue:25176 */
|
||||
|
||||
237
src/plugins/debugger/dap/dapclient.cpp
Normal file
237
src/plugins/debugger/dap/dapclient.cpp
Normal file
@@ -0,0 +1,237 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||
|
||||
#include "dapclient.h"
|
||||
#include "qjsonarray.h"
|
||||
|
||||
#include <utils/process.h>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
using namespace Core;
|
||||
using namespace Utils;
|
||||
|
||||
static Q_LOGGING_CATEGORY(dapEngineLog, "qtc.dbg.dapengine", QtWarningMsg);
|
||||
|
||||
namespace Debugger::Internal {
|
||||
|
||||
DapClient::DapClient(std::unique_ptr<IDataProvider> dataProvider)
|
||||
: m_dataProvider(std::move(dataProvider))
|
||||
{
|
||||
connect(m_dataProvider.get(),
|
||||
&IDataProvider::readyReadStandardOutput,
|
||||
this,
|
||||
&DapClient::readOutput);
|
||||
connect(m_dataProvider.get(),
|
||||
&IDataProvider::readyReadStandardError,
|
||||
this,
|
||||
&DapClient::readyReadStandardError);
|
||||
|
||||
connect(m_dataProvider.get(), &IDataProvider::done, this, &DapClient::done);
|
||||
connect(m_dataProvider.get(), &IDataProvider::started, this, &DapClient::started);
|
||||
}
|
||||
|
||||
DapClient::~DapClient() = default;
|
||||
|
||||
|
||||
void DapClient::postRequest(const QString &command, const QJsonObject &arguments)
|
||||
{
|
||||
QJsonObject ob = {
|
||||
{"command", command},
|
||||
{"type", "request"},
|
||||
{"arguments", arguments}
|
||||
};
|
||||
|
||||
static int seq = 1;
|
||||
QJsonObject obseq = ob;
|
||||
obseq.insert("seq", seq++);
|
||||
|
||||
const QByteArray data = QJsonDocument(obseq).toJson(QJsonDocument::Compact);
|
||||
const QByteArray msg = "Content-Length: " + QByteArray::number(data.size()) + "\r\n\r\n" + data;
|
||||
qCDebug(dapEngineLog) << msg;
|
||||
|
||||
m_dataProvider->writeRaw(msg);
|
||||
}
|
||||
|
||||
void DapClient::sendInitialize()
|
||||
{
|
||||
postRequest("initialize", QJsonObject{{"clientID", "QtCreator"}, {"clientName", "QtCreator"}});
|
||||
}
|
||||
|
||||
void DapClient::sendLaunch(const Utils::FilePath &executable)
|
||||
{
|
||||
postRequest("launch",
|
||||
QJsonObject{{"noDebug", false}, {"program", executable.path()}, {"__restart", ""}});
|
||||
}
|
||||
|
||||
void DapClient::sendConfigurationDone()
|
||||
{
|
||||
postRequest("configurationDone");
|
||||
}
|
||||
|
||||
void DapClient::sendDisconnect()
|
||||
{
|
||||
postRequest("disconnect", QJsonObject{{"restart", false}, {"terminateDebuggee", true}});
|
||||
}
|
||||
|
||||
void DapClient::sendTerminate()
|
||||
{
|
||||
postRequest("terminate", QJsonObject{{"restart", false}});
|
||||
}
|
||||
|
||||
void DapClient::sendContinue(int threadId)
|
||||
{
|
||||
QTC_ASSERT(threadId != -1, return);
|
||||
postRequest("continue", QJsonObject{{"threadId", threadId}});
|
||||
}
|
||||
|
||||
void DapClient::sendPause()
|
||||
{
|
||||
postRequest("pause");
|
||||
}
|
||||
|
||||
|
||||
void DapClient::sendStepIn(int threadId)
|
||||
{
|
||||
QTC_ASSERT(threadId != -1, return);
|
||||
postRequest("stepIn", QJsonObject{{"threadId", threadId}});
|
||||
}
|
||||
|
||||
void DapClient::sendStepOut(int threadId)
|
||||
{
|
||||
QTC_ASSERT(threadId != -1, return);
|
||||
postRequest("stepOut", QJsonObject{{"threadId", threadId}});
|
||||
}
|
||||
|
||||
void DapClient::sendStepOver(int threadId)
|
||||
{
|
||||
QTC_ASSERT(threadId != -1, return);
|
||||
postRequest("next", QJsonObject{{"threadId", threadId}});
|
||||
}
|
||||
|
||||
void DapClient::stackTrace(int threadId)
|
||||
{
|
||||
QTC_ASSERT(threadId != -1, return);
|
||||
postRequest("stackTrace",
|
||||
QJsonObject{{"threadId", threadId}, {"startFrame", 0}, {"levels", 10}});
|
||||
}
|
||||
|
||||
void DapClient::scopes(int frameId)
|
||||
{
|
||||
postRequest("scopes", QJsonObject{{"frameId", frameId}});
|
||||
}
|
||||
|
||||
void DapClient::threads()
|
||||
{
|
||||
postRequest("threads");
|
||||
}
|
||||
|
||||
void DapClient::variables(int variablesReference)
|
||||
{
|
||||
postRequest("variables", QJsonObject{{"variablesReference", variablesReference}});
|
||||
}
|
||||
|
||||
void DapClient::setBreakpoints(const QJsonArray &breakpoints, const FilePath &fileName)
|
||||
{
|
||||
postRequest("setBreakpoints",
|
||||
QJsonObject{{"source", QJsonObject{{"path", fileName.path()}}},
|
||||
{"breakpoints", breakpoints}});
|
||||
}
|
||||
|
||||
void DapClient::readOutput()
|
||||
{
|
||||
m_inbuffer.append(m_dataProvider->readAllStandardOutput());
|
||||
|
||||
qCDebug(dapEngineLog) << m_inbuffer;
|
||||
|
||||
while (true) {
|
||||
// Something like
|
||||
// Content-Length: 128\r\n
|
||||
// {"type": "event", "event": "output", "body": {"category": "stdout", "output": "...\n"}, "seq": 1}\r\n
|
||||
// FIXME: There coud be more than one header line.
|
||||
int pos1 = m_inbuffer.indexOf("Content-Length:");
|
||||
if (pos1 == -1)
|
||||
break;
|
||||
pos1 += 15;
|
||||
|
||||
int pos2 = m_inbuffer.indexOf('\n', pos1);
|
||||
if (pos2 == -1)
|
||||
break;
|
||||
|
||||
const int len = m_inbuffer.mid(pos1, pos2 - pos1).trimmed().toInt();
|
||||
if (len < 4)
|
||||
break;
|
||||
|
||||
pos2 += 3; // Skip \r\n\r
|
||||
|
||||
if (pos2 + len > m_inbuffer.size())
|
||||
break;
|
||||
|
||||
QJsonParseError error;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(m_inbuffer.mid(pos2, len), &error);
|
||||
|
||||
m_inbuffer = m_inbuffer.mid(pos2 + len);
|
||||
|
||||
emitSignals(doc);
|
||||
}
|
||||
}
|
||||
|
||||
void DapClient::emitSignals(const QJsonDocument &doc)
|
||||
{
|
||||
QJsonObject ob = doc.object();
|
||||
const QJsonValue t = ob.value("type");
|
||||
const QString type = t.toString();
|
||||
|
||||
qCDebug(dapEngineLog) << "dap response" << ob;
|
||||
|
||||
if (type == "response") {
|
||||
DapResponseType type = DapResponseType::Unknown;
|
||||
const QString command = ob.value("command").toString();
|
||||
if (command == "configurationDone") {
|
||||
type = DapResponseType::ConfigurationDone;
|
||||
} else if (command == "continue") {
|
||||
type = DapResponseType::Continue;
|
||||
} else if (command == "stackTrace") {
|
||||
type = DapResponseType::StackTrace;
|
||||
} else if (command == "scopes") {
|
||||
type = DapResponseType::Scopes;
|
||||
} else if (command == "variables") {
|
||||
type = DapResponseType::Variables;
|
||||
} else if (command == "stepIn") {
|
||||
type = DapResponseType::StepIn;
|
||||
} else if (command == "stepOut") {
|
||||
type = DapResponseType::StepOut;
|
||||
} else if (command == "next") {
|
||||
type = DapResponseType::StepOver;
|
||||
} else if (command == "threads") {
|
||||
type = DapResponseType::DapThreads;
|
||||
}
|
||||
emit responseReady(type, ob);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == "event") {
|
||||
const QString event = ob.value("event").toString();
|
||||
|
||||
DapEventType type = DapEventType::Unknown;
|
||||
if (event == "initialized") {
|
||||
type = DapEventType::Initialized;
|
||||
} else if (event == "stopped") {
|
||||
type = DapEventType::Stopped;
|
||||
} else if (event == "thread") {
|
||||
type = DapEventType::DapThread;
|
||||
} else if (event == "output") {
|
||||
type = DapEventType::Output;
|
||||
} else if (event == "breakpoint") {
|
||||
type = DapEventType::DapBreakpoint;
|
||||
} else if (event == "exited") {
|
||||
type = DapEventType::Exited;
|
||||
}
|
||||
emit eventReady(type, ob);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Debugger::Internal
|
||||
|
||||
113
src/plugins/debugger/dap/dapclient.h
Normal file
113
src/plugins/debugger/dap/dapclient.h
Normal file
@@ -0,0 +1,113 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <debugger/debuggerengine.h>
|
||||
|
||||
#include <utils/process.h>
|
||||
|
||||
namespace Debugger::Internal {
|
||||
|
||||
class IDataProvider : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
virtual void start() = 0;
|
||||
virtual bool isRunning() const = 0;
|
||||
virtual void writeRaw(const QByteArray &input) = 0;
|
||||
virtual void kill() = 0;
|
||||
virtual QByteArray readAllStandardOutput() = 0;
|
||||
virtual QString readAllStandardError() = 0;
|
||||
virtual int exitCode() const = 0;
|
||||
virtual QString executable() const = 0;
|
||||
|
||||
virtual QProcess::ExitStatus exitStatus() const = 0;
|
||||
virtual QProcess::ProcessError error() const = 0;
|
||||
virtual Utils::ProcessResult result() const = 0;
|
||||
virtual QString exitMessage() const = 0;
|
||||
|
||||
signals:
|
||||
void started();
|
||||
void done();
|
||||
void readyReadStandardOutput();
|
||||
void readyReadStandardError();
|
||||
};
|
||||
|
||||
enum class DapResponseType
|
||||
{
|
||||
ConfigurationDone,
|
||||
Continue,
|
||||
StackTrace,
|
||||
Scopes,
|
||||
DapThreads,
|
||||
Variables,
|
||||
StepIn,
|
||||
StepOut,
|
||||
StepOver,
|
||||
Unknown
|
||||
};
|
||||
|
||||
enum class DapEventType
|
||||
{
|
||||
Initialized,
|
||||
Stopped,
|
||||
Exited,
|
||||
DapThread,
|
||||
Output,
|
||||
DapBreakpoint,
|
||||
Unknown
|
||||
};
|
||||
|
||||
class DapClient : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DapClient(std::unique_ptr<IDataProvider> dataProvider);
|
||||
~DapClient();
|
||||
|
||||
IDataProvider *dataProvider() const { return m_dataProvider.get(); }
|
||||
|
||||
void postRequest(const QString &command, const QJsonObject &arguments = {});
|
||||
|
||||
virtual void sendInitialize();
|
||||
|
||||
void sendLaunch(const Utils::FilePath &executable);
|
||||
void sendConfigurationDone();
|
||||
|
||||
void sendDisconnect();
|
||||
void sendTerminate();
|
||||
|
||||
void sendPause();
|
||||
void sendContinue(int threadId);
|
||||
|
||||
void sendStepIn(int threadId);
|
||||
void sendStepOut(int threadId);
|
||||
void sendStepOver(int threadId);
|
||||
|
||||
void stackTrace(int threadId);
|
||||
void scopes(int frameId);
|
||||
void threads();
|
||||
void variables(int variablesReference);
|
||||
void setBreakpoints(const QJsonArray &breakpoints, const Utils::FilePath &fileName);
|
||||
void emitSignals(const QJsonDocument &doc);
|
||||
|
||||
signals:
|
||||
void started();
|
||||
void done();
|
||||
void readyReadStandardError();
|
||||
|
||||
void responseReady(DapResponseType type, const QJsonObject &response);
|
||||
void eventReady(DapEventType type, const QJsonObject &response);
|
||||
|
||||
private:
|
||||
void readOutput();
|
||||
|
||||
private:
|
||||
std::unique_ptr<IDataProvider> m_dataProvider = nullptr;
|
||||
|
||||
QByteArray m_inbuffer;
|
||||
};
|
||||
|
||||
} // namespace Debugger::Internal
|
||||
@@ -2,7 +2,9 @@
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||
|
||||
#include "dapengine.h"
|
||||
|
||||
#include "cmakedapengine.h"
|
||||
#include "dapclient.h"
|
||||
#include "gdbdapengine.h"
|
||||
|
||||
#include <debugger/breakhandler.h>
|
||||
@@ -79,28 +81,13 @@ void DapEngine::executeDebuggerCommand(const QString &/*command*/)
|
||||
// postDirectCommand(command);
|
||||
}
|
||||
|
||||
void DapEngine::postDirectCommand(const QJsonObject &ob)
|
||||
{
|
||||
static int seq = 1;
|
||||
QJsonObject obseq = ob;
|
||||
obseq.insert("seq", seq++);
|
||||
|
||||
const QByteArray data = QJsonDocument(obseq).toJson(QJsonDocument::Compact);
|
||||
const QByteArray msg = "Content-Length: " + QByteArray::number(data.size()) + "\r\n\r\n" + data;
|
||||
qCDebug(dapEngineLog) << msg;
|
||||
|
||||
m_dataGenerator->writeRaw(msg);
|
||||
|
||||
showMessage(QString::fromUtf8(msg), LogInput);
|
||||
}
|
||||
|
||||
void DapEngine::runCommand(const DebuggerCommand &cmd)
|
||||
{
|
||||
if (state() == EngineSetupRequested) { // cmd has been triggered too early
|
||||
showMessage("IGNORED COMMAND: " + cmd.function);
|
||||
return;
|
||||
}
|
||||
QTC_ASSERT(m_dataGenerator->isRunning(), notifyEngineIll());
|
||||
QTC_ASSERT(m_dapClient->dataProvider()->isRunning(), notifyEngineIll());
|
||||
// postDirectCommand(cmd.args.toObject());
|
||||
// const QByteArray data = QJsonDocument(cmd.args.toObject()).toJson(QJsonDocument::Compact);
|
||||
// m_proc.writeRaw("Content-Length: " + QByteArray::number(data.size()) + "\r\n" + data + "\r\n");
|
||||
@@ -112,14 +99,7 @@ void DapEngine::shutdownInferior()
|
||||
{
|
||||
QTC_ASSERT(state() == InferiorShutdownRequested, qCDebug(dapEngineLog) << state());
|
||||
|
||||
postDirectCommand({
|
||||
{"command", "disconnect"},
|
||||
{"type", "request"},
|
||||
{"arguments", QJsonObject{
|
||||
{"restart", false},
|
||||
{"terminateDebuggee", true}
|
||||
}}
|
||||
});
|
||||
m_dapClient->sendDisconnect();
|
||||
|
||||
qCDebug(dapEngineLog) << "DapEngine::shutdownInferior()";
|
||||
notifyInferiorShutdownFinished();
|
||||
@@ -129,30 +109,17 @@ void DapEngine::shutdownEngine()
|
||||
{
|
||||
QTC_ASSERT(state() == EngineShutdownRequested, qCDebug(dapEngineLog) << state());
|
||||
|
||||
postDirectCommand({
|
||||
{"command", "terminate"},
|
||||
{"type", "request"},
|
||||
{"arguments", QJsonObject{
|
||||
{"restart", false}
|
||||
}}
|
||||
});
|
||||
m_dapClient->sendTerminate();
|
||||
|
||||
qCDebug(dapEngineLog) << "DapEngine::shutdownEngine()";
|
||||
m_dataGenerator->kill();
|
||||
m_dapClient->dataProvider()->kill();
|
||||
}
|
||||
|
||||
void DapEngine::handleDapStarted()
|
||||
{
|
||||
QTC_ASSERT(state() == EngineRunRequested, qCDebug(dapEngineLog) << state());
|
||||
|
||||
postDirectCommand({
|
||||
{"command", "initialize"},
|
||||
{"type", "request"},
|
||||
{"arguments", QJsonObject {
|
||||
{"clientID", "QtCreator"}, // The ID of the client using this adapter.
|
||||
{"clientName", "QtCreator"} // The human-readable name of the client using this adapter.
|
||||
}}
|
||||
});
|
||||
m_dapClient->sendInitialize();
|
||||
|
||||
qCDebug(dapEngineLog) << "handleDapStarted";
|
||||
}
|
||||
@@ -161,10 +128,7 @@ void DapEngine::handleDapConfigurationDone()
|
||||
{
|
||||
QTC_ASSERT(state() == EngineRunRequested, qCDebug(dapEngineLog) << state());
|
||||
|
||||
postDirectCommand({
|
||||
{"command", "configurationDone"},
|
||||
{"type", "request"}
|
||||
});
|
||||
m_dapClient->sendConfigurationDone();
|
||||
|
||||
qCDebug(dapEngineLog) << "handleDapConfigurationDone";
|
||||
}
|
||||
@@ -174,70 +138,14 @@ void DapEngine::handleDapLaunch()
|
||||
{
|
||||
QTC_ASSERT(state() == EngineRunRequested, qCDebug(dapEngineLog) << state());
|
||||
|
||||
postDirectCommand({
|
||||
{"command", "launch"},
|
||||
{"type", "request"},
|
||||
{"arguments", QJsonObject{
|
||||
{"noDebug", false},
|
||||
{"program", runParameters().inferior.command.executable().path()},
|
||||
{"__restart", ""}
|
||||
}}
|
||||
});
|
||||
m_dapClient->sendLaunch(runParameters().inferior.command.executable());
|
||||
|
||||
qCDebug(dapEngineLog) << "handleDapLaunch";
|
||||
}
|
||||
|
||||
void DapEngine::interruptInferior()
|
||||
{
|
||||
postDirectCommand({
|
||||
{"command", "pause"},
|
||||
{"type", "request"}
|
||||
});
|
||||
}
|
||||
|
||||
void DapEngine::dapStackTrace()
|
||||
{
|
||||
if (m_currentThreadId == -1)
|
||||
return;
|
||||
|
||||
postDirectCommand({
|
||||
{"command", "stackTrace"},
|
||||
{"type", "request"},
|
||||
{"arguments", QJsonObject{
|
||||
{"threadId", m_currentThreadId},
|
||||
{"startFrame", 0},
|
||||
{"levels", 10}
|
||||
}}
|
||||
});
|
||||
}
|
||||
|
||||
void DapEngine::dapScopes(int frameId)
|
||||
{
|
||||
postDirectCommand({
|
||||
{"command", "scopes"},
|
||||
{"type", "request"},
|
||||
{"arguments", QJsonObject{
|
||||
{"frameId", frameId}
|
||||
}}
|
||||
});
|
||||
}
|
||||
|
||||
void DapEngine::threads()
|
||||
{
|
||||
postDirectCommand({
|
||||
{"command", "threads"},
|
||||
{"type", "request"}
|
||||
});
|
||||
}
|
||||
|
||||
void DapEngine::dapVariables(int variablesReference)
|
||||
{
|
||||
postDirectCommand({
|
||||
{"command", "variables"},
|
||||
{"type", "request"},
|
||||
{"arguments", QJsonObject{
|
||||
{"variablesReference", variablesReference}
|
||||
}}
|
||||
});
|
||||
m_dapClient->sendPause();
|
||||
}
|
||||
|
||||
void DapEngine::executeStepIn(bool)
|
||||
@@ -246,15 +154,7 @@ void DapEngine::executeStepIn(bool)
|
||||
return;
|
||||
|
||||
notifyInferiorRunRequested();
|
||||
|
||||
postDirectCommand({
|
||||
{"command", "stepIn"},
|
||||
{"type", "request"},
|
||||
{"arguments", QJsonObject{
|
||||
{"threadId", m_currentThreadId},
|
||||
}}
|
||||
});
|
||||
|
||||
m_dapClient->sendStepIn(m_currentThreadId);
|
||||
}
|
||||
|
||||
void DapEngine::executeStepOut()
|
||||
@@ -263,15 +163,7 @@ void DapEngine::executeStepOut()
|
||||
return;
|
||||
|
||||
notifyInferiorRunRequested();
|
||||
|
||||
postDirectCommand({
|
||||
{"command", "stepOut"},
|
||||
{"type", "request"},
|
||||
{"arguments", QJsonObject{
|
||||
{"threadId", m_currentThreadId},
|
||||
}}
|
||||
});
|
||||
|
||||
m_dapClient->sendStepOut(m_currentThreadId);
|
||||
}
|
||||
|
||||
void DapEngine::executeStepOver(bool)
|
||||
@@ -281,26 +173,13 @@ void DapEngine::executeStepOver(bool)
|
||||
|
||||
notifyInferiorRunRequested();
|
||||
|
||||
postDirectCommand({
|
||||
{"command", "next"},
|
||||
{"type", "request"},
|
||||
{"arguments", QJsonObject{
|
||||
{"threadId", m_currentThreadId},
|
||||
}}
|
||||
});
|
||||
|
||||
m_dapClient->sendStepOver(m_currentThreadId);
|
||||
}
|
||||
|
||||
void DapEngine::continueInferior()
|
||||
{
|
||||
notifyInferiorRunRequested();
|
||||
postDirectCommand({
|
||||
{"command", "continue"},
|
||||
{"type", "request"},
|
||||
{"arguments", QJsonObject{
|
||||
{"threadId", m_currentThreadId},
|
||||
}}
|
||||
});
|
||||
m_dapClient->sendContinue(m_currentThreadId);
|
||||
}
|
||||
|
||||
void DapEngine::executeRunToLine(const ContextData &data)
|
||||
@@ -395,16 +274,7 @@ void DapEngine::dapInsertBreakpoint(const Breakpoint &bp)
|
||||
}
|
||||
}
|
||||
|
||||
postDirectCommand( {
|
||||
{"command", "setBreakpoints"},
|
||||
{"type", "request"},
|
||||
{"arguments", QJsonObject{
|
||||
{"source", QJsonObject{
|
||||
{"path", params.fileName.path()}
|
||||
}},
|
||||
{"breakpoints", breakpoints}
|
||||
}}
|
||||
});
|
||||
m_dapClient->setBreakpoints(breakpoints, params.fileName);
|
||||
|
||||
qCDebug(dapEngineLog) << "insertBreakpoint" << bp->modelId() << bp->responseId();
|
||||
}
|
||||
@@ -437,23 +307,15 @@ void DapEngine::dapRemoveBreakpoint(const Breakpoint &bp)
|
||||
const BreakpointParameters ¶ms = bp->requestedParameters();
|
||||
|
||||
QJsonArray breakpoints;
|
||||
for (const auto &breakpoint : breakHandler()->breakpoints())
|
||||
for (const auto &breakpoint : breakHandler()->breakpoints()) {
|
||||
if (breakpoint->responseId() != bp->responseId()
|
||||
&& params.fileName == breakpoint->requestedParameters().fileName) {
|
||||
QJsonObject jsonBp = createBreakpoint(breakpoint);
|
||||
breakpoints.append(jsonBp);
|
||||
}
|
||||
}
|
||||
|
||||
postDirectCommand({
|
||||
{"command", "setBreakpoints"},
|
||||
{"type", "request"},
|
||||
{"arguments", QJsonObject{
|
||||
{"source", QJsonObject{
|
||||
{"path", params.fileName.path()}
|
||||
}},
|
||||
{"breakpoints", breakpoints}
|
||||
}}
|
||||
});
|
||||
m_dapClient->setBreakpoints(breakpoints, params.fileName);
|
||||
|
||||
qCDebug(dapEngineLog) << "removeBreakpoint" << bp->modelId() << bp->responseId();
|
||||
}
|
||||
@@ -566,7 +428,7 @@ QString DapEngine::errorMessage(QProcess::ProcessError error) const
|
||||
return Tr::tr("The DAP process failed to start. Either the "
|
||||
"invoked program \"%1\" is missing, or you may have insufficient "
|
||||
"permissions to invoke the program.")
|
||||
.arg(m_dataGenerator->executable());
|
||||
.arg(m_dapClient->dataProvider()->executable());
|
||||
case QProcess::Crashed:
|
||||
return Tr::tr("The DAP process crashed some time after starting "
|
||||
"successfully.");
|
||||
@@ -588,14 +450,15 @@ QString DapEngine::errorMessage(QProcess::ProcessError error) const
|
||||
|
||||
void DapEngine::handleDapDone()
|
||||
{
|
||||
if (m_dataGenerator->result() == ProcessResult::StartFailed) {
|
||||
if (m_dapClient->dataProvider()->result() == ProcessResult::StartFailed) {
|
||||
notifyEngineSetupFailed();
|
||||
showMessage("ADAPTER START FAILED");
|
||||
ICore::showWarningWithOptions(Tr::tr("Adapter start failed"), m_dataGenerator->exitMessage());
|
||||
ICore::showWarningWithOptions(Tr::tr("Adapter start failed"),
|
||||
m_dapClient->dataProvider()->exitMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
const QProcess::ProcessError error = m_dataGenerator->error();
|
||||
const QProcess::ProcessError error = m_dapClient->dataProvider()->error();
|
||||
if (error != QProcess::UnknownError) {
|
||||
showMessage("HANDLE DAP ERROR");
|
||||
if (error != QProcess::Crashed)
|
||||
@@ -604,129 +467,62 @@ void DapEngine::handleDapDone()
|
||||
return;
|
||||
}
|
||||
showMessage(QString("DAP PROCESS FINISHED, status %1, code %2")
|
||||
.arg(m_dataGenerator->exitStatus()).arg(m_dataGenerator->exitCode()));
|
||||
.arg(m_dapClient->dataProvider()->exitStatus()).arg(m_dapClient->dataProvider()->exitCode()));
|
||||
notifyEngineSpontaneousShutdown();
|
||||
}
|
||||
|
||||
void DapEngine::readDapStandardError()
|
||||
{
|
||||
QString err = m_dataGenerator->readAllStandardError();
|
||||
QString err = m_dapClient->dataProvider()->readAllStandardError();
|
||||
qCDebug(dapEngineLog) << "DAP STDERR:" << err;
|
||||
//qWarning() << "Unexpected DAP stderr:" << err;
|
||||
showMessage("Unexpected DAP stderr: " + err);
|
||||
//handleOutput(err);
|
||||
}
|
||||
|
||||
void DapEngine::readDapStandardOutput()
|
||||
{
|
||||
m_inbuffer.append(m_dataGenerator->readAllStandardOutput());
|
||||
|
||||
qCDebug(dapEngineLog) << m_inbuffer;
|
||||
|
||||
while (true) {
|
||||
// Something like
|
||||
// Content-Length: 128\r\n
|
||||
// {"type": "event", "event": "output", "body": {"category": "stdout", "output": "...\n"}, "seq": 1}\r\n
|
||||
// FIXME: There coud be more than one header line.
|
||||
int pos1 = m_inbuffer.indexOf("Content-Length:");
|
||||
if (pos1 == -1)
|
||||
break;
|
||||
pos1 += 15;
|
||||
|
||||
int pos2 = m_inbuffer.indexOf('\n', pos1);
|
||||
if (pos2 == -1)
|
||||
break;
|
||||
|
||||
const int len = m_inbuffer.mid(pos1, pos2 - pos1).trimmed().toInt();
|
||||
if (len < 4)
|
||||
break;
|
||||
|
||||
pos2 += 3; // Skip \r\n\r
|
||||
|
||||
if (pos2 + len > m_inbuffer.size())
|
||||
break;
|
||||
|
||||
QJsonParseError error;
|
||||
const auto doc = QJsonDocument::fromJson(m_inbuffer.mid(pos2, len), &error);
|
||||
|
||||
m_inbuffer = m_inbuffer.mid(pos2 + len);
|
||||
|
||||
handleOutput(doc);
|
||||
}
|
||||
}
|
||||
|
||||
void DapEngine::handleOutput(const QJsonDocument &data)
|
||||
{
|
||||
QJsonObject ob = data.object();
|
||||
|
||||
const QJsonValue t = ob.value("type");
|
||||
const QString type = t.toString();
|
||||
|
||||
qCDebug(dapEngineLog) << "dap response" << ob;
|
||||
|
||||
if (type == "response") {
|
||||
handleResponse(ob);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == "event") {
|
||||
handleEvent(ob);
|
||||
return;
|
||||
}
|
||||
|
||||
showMessage("UNKNOWN TYPE:" + type);
|
||||
}
|
||||
|
||||
void DapEngine::handleResponse(const QJsonObject &response)
|
||||
void DapEngine::handleResponse(DapResponseType type, const QJsonObject &response)
|
||||
{
|
||||
const QString command = response.value("command").toString();
|
||||
|
||||
if (command == "configurationDone") {
|
||||
switch (type) {
|
||||
case DapResponseType::ConfigurationDone:
|
||||
showMessage("configurationDone", LogDebug);
|
||||
qCDebug(dapEngineLog) << "configurationDone success";
|
||||
notifyEngineRunAndInferiorRunOk();
|
||||
return;
|
||||
}
|
||||
|
||||
if (command == "continue") {
|
||||
break;
|
||||
case DapResponseType::Continue:
|
||||
showMessage("continue", LogDebug);
|
||||
qCDebug(dapEngineLog) << "continue success";
|
||||
notifyInferiorRunOk();
|
||||
return;
|
||||
}
|
||||
|
||||
if (command == "stackTrace") {
|
||||
break;
|
||||
case DapResponseType::StackTrace:
|
||||
handleStackTraceResponse(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command == "scopes") {
|
||||
break;
|
||||
case DapResponseType::Scopes:
|
||||
handleScopesResponse(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command == "variables") {
|
||||
break;
|
||||
case DapResponseType::Variables: {
|
||||
auto variables = response.value("body").toObject().value("variables").toArray();
|
||||
refreshLocals(variables);
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
if (command == "stepIn" || command == "stepOut" || command == "next") {
|
||||
case DapResponseType::StepIn:
|
||||
case DapResponseType::StepOut:
|
||||
case DapResponseType::StepOver:
|
||||
if (response.value("success").toBool()) {
|
||||
showMessage(command, LogDebug);
|
||||
notifyInferiorRunOk();
|
||||
} else {
|
||||
notifyInferiorRunFailed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (command == "threads") {
|
||||
break;
|
||||
case DapResponseType::DapThreads:
|
||||
handleThreadsResponse(response);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
showMessage("UNKNOWN RESPONSE:" + command);
|
||||
};
|
||||
}
|
||||
|
||||
void DapEngine::handleStackTraceResponse(const QJsonObject &response)
|
||||
@@ -743,7 +539,7 @@ void DapEngine::handleStackTraceResponse(const QJsonObject &response)
|
||||
gotoLocation(Location(file, line));
|
||||
|
||||
refreshStack(stackFrames);
|
||||
dapScopes(stackFrame.value("id").toInt());
|
||||
m_dapClient->scopes(stackFrame.value("id").toInt());
|
||||
}
|
||||
|
||||
void DapEngine::handleScopesResponse(const QJsonObject &response)
|
||||
@@ -761,7 +557,7 @@ void DapEngine::handleScopesResponse(const QJsonObject &response)
|
||||
m_currentWatchItem = m_rootWatchItem;
|
||||
watchHandler()->removeAllData();
|
||||
watchHandler()->notifyUpdateStarted();
|
||||
dapVariables(variablesReference);
|
||||
m_dapClient->variables(variablesReference);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -785,18 +581,33 @@ void DapEngine::handleThreadsResponse(const QJsonObject &response)
|
||||
handler->setCurrentThread(threadsHandler()->threadForId(QString::number(m_currentThreadId)));
|
||||
}
|
||||
|
||||
void DapEngine::handleEvent(const QJsonObject &event)
|
||||
void DapEngine::handleEvent(DapEventType type, const QJsonObject &event)
|
||||
{
|
||||
const QString eventType = event.value("event").toString();
|
||||
const QJsonObject body = event.value("body").toObject();
|
||||
showMessage(eventType, LogDebug);
|
||||
|
||||
if (eventType == "exited") {
|
||||
switch (type) {
|
||||
case DapEventType::Initialized:
|
||||
qCDebug(dapEngineLog) << "initialize success";
|
||||
handleDapLaunch();
|
||||
handleDapConfigurationDone();
|
||||
break;
|
||||
case DapEventType::Stopped:
|
||||
handleStoppedEvent(event);
|
||||
break;
|
||||
case DapEventType::Exited:
|
||||
notifyInferiorExited();
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventType == "output") {
|
||||
break;
|
||||
case DapEventType::DapThread:
|
||||
m_dapClient->threads();
|
||||
if (body.value("reason").toString() == "started" && body.value("threadId").toInt() == 1)
|
||||
claimInitialBreakpoints();
|
||||
break;
|
||||
case DapEventType::DapBreakpoint:
|
||||
handleBreakpointEvent(event);
|
||||
break;
|
||||
case DapEventType::Output: {
|
||||
const QString category = body.value("category").toString();
|
||||
const QString output = body.value("output").toString();
|
||||
if (category == "stdout")
|
||||
@@ -805,34 +616,11 @@ void DapEngine::handleEvent(const QJsonObject &event)
|
||||
showMessage(output, AppError);
|
||||
else
|
||||
showMessage(output, LogDebug);
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
if (eventType == "initialized") {
|
||||
qCDebug(dapEngineLog) << "initialize success";
|
||||
handleDapLaunch();
|
||||
handleDapConfigurationDone();
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventType == "stopped") {
|
||||
handleStoppedEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventType == "thread") {
|
||||
threads();
|
||||
if (body.value("reason").toString() == "started" && body.value("threadId").toInt() == 1)
|
||||
claimInitialBreakpoints();
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventType == "breakpoint") {
|
||||
handleBreakpointEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
default:
|
||||
showMessage("UNKNOWN EVENT:" + eventType);
|
||||
};
|
||||
}
|
||||
|
||||
void DapEngine::handleStoppedEvent(const QJsonObject &event)
|
||||
@@ -857,8 +645,8 @@ void DapEngine::handleStoppedEvent(const QJsonObject &event)
|
||||
else
|
||||
notifyInferiorSpontaneousStop();
|
||||
|
||||
dapStackTrace();
|
||||
threads();
|
||||
m_dapClient->stackTrace(m_currentThreadId);
|
||||
m_dapClient->threads();
|
||||
}
|
||||
|
||||
void DapEngine::handleBreakpointEvent(const QJsonObject &event)
|
||||
@@ -936,7 +724,7 @@ void DapEngine::refreshLocals(const QJsonArray &variables)
|
||||
const auto front = m_variablesReferenceQueue.front();
|
||||
m_variablesReferenceQueue.pop();
|
||||
|
||||
dapVariables(front.first);
|
||||
m_dapClient->variables(front.first);
|
||||
m_currentWatchItem = front.second;
|
||||
}
|
||||
|
||||
@@ -977,7 +765,7 @@ void DapEngine::updateAll()
|
||||
|
||||
void DapEngine::updateLocals()
|
||||
{
|
||||
dapStackTrace();
|
||||
m_dapClient->stackTrace(m_currentThreadId);
|
||||
}
|
||||
|
||||
bool DapEngine::hasCapability(unsigned cap) const
|
||||
@@ -996,19 +784,18 @@ void DapEngine::claimInitialBreakpoints()
|
||||
|
||||
void DapEngine::connectDataGeneratorSignals()
|
||||
{
|
||||
if (!m_dataGenerator)
|
||||
if (!m_dapClient)
|
||||
return;
|
||||
|
||||
connect(m_dataGenerator.get(), &IDataProvider::started, this, &DapEngine::handleDapStarted);
|
||||
connect(m_dataGenerator.get(), &IDataProvider::done, this, &DapEngine::handleDapDone);
|
||||
connect(m_dataGenerator.get(),
|
||||
&IDataProvider::readyReadStandardOutput,
|
||||
this,
|
||||
&DapEngine::readDapStandardOutput);
|
||||
connect(m_dataGenerator.get(),
|
||||
&IDataProvider::readyReadStandardError,
|
||||
connect(m_dapClient.get(), &DapClient::started, this, &DapEngine::handleDapStarted);
|
||||
connect(m_dapClient.get(), &DapClient::done, this, &DapEngine::handleDapDone);
|
||||
connect(m_dapClient.get(),
|
||||
&DapClient::readyReadStandardError,
|
||||
this,
|
||||
&DapEngine::readDapStandardError);
|
||||
|
||||
connect(m_dapClient.get(), &DapClient::responseReady, this, &DapEngine::handleResponse);
|
||||
connect(m_dapClient.get(), &DapClient::eventReady, this, &DapEngine::handleEvent);
|
||||
}
|
||||
|
||||
DebuggerEngine *createDapEngine(Utils::Id runMode)
|
||||
|
||||
@@ -13,38 +13,16 @@
|
||||
|
||||
namespace Debugger::Internal {
|
||||
|
||||
class DapClient;
|
||||
class DebuggerCommand;
|
||||
class IDataProvider;
|
||||
class GdbMi;
|
||||
enum class DapResponseType;
|
||||
enum class DapEventType;
|
||||
|
||||
/*
|
||||
* A debugger engine for the debugger adapter protocol.
|
||||
*/
|
||||
|
||||
class IDataProvider : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
virtual void start() = 0;
|
||||
virtual bool isRunning() const = 0;
|
||||
virtual void writeRaw(const QByteArray &input) = 0;
|
||||
virtual void kill() = 0;
|
||||
virtual QByteArray readAllStandardOutput() = 0;
|
||||
virtual QString readAllStandardError() = 0;
|
||||
virtual int exitCode() const = 0;
|
||||
virtual QString executable() const = 0;
|
||||
|
||||
virtual QProcess::ExitStatus exitStatus() const = 0;
|
||||
virtual QProcess::ProcessError error() const = 0;
|
||||
virtual Utils::ProcessResult result() const = 0;
|
||||
virtual QString exitMessage() const = 0;
|
||||
|
||||
signals:
|
||||
void started();
|
||||
void done();
|
||||
void readyReadStandardOutput();
|
||||
void readyReadStandardError();
|
||||
};
|
||||
|
||||
class DapEngine : public DebuggerEngine
|
||||
{
|
||||
public:
|
||||
@@ -91,7 +69,6 @@ protected:
|
||||
void updateItem(const QString &iname) override;
|
||||
|
||||
void runCommand(const DebuggerCommand &cmd) override;
|
||||
void postDirectCommand(const QJsonObject &ob);
|
||||
|
||||
void refreshLocation(const GdbMi &reportedLocation);
|
||||
void refreshStack(const QJsonArray &stackFrames);
|
||||
@@ -105,14 +82,10 @@ protected:
|
||||
|
||||
void claimInitialBreakpoints();
|
||||
|
||||
virtual void handleDapStarted();
|
||||
void handleDapStarted();
|
||||
void handleDapLaunch();
|
||||
void handleDapConfigurationDone();
|
||||
|
||||
void dapStackTrace();
|
||||
void dapScopes(int frameId);
|
||||
void threads();
|
||||
void dapVariables(int variablesReference);
|
||||
void dapRemoveBreakpoint(const Breakpoint &bp);
|
||||
void dapInsertBreakpoint(const Breakpoint &bp);
|
||||
|
||||
@@ -120,14 +93,12 @@ protected:
|
||||
void readDapStandardOutput();
|
||||
void readDapStandardError();
|
||||
|
||||
void handleOutput(const QJsonDocument &data);
|
||||
|
||||
void handleResponse(const QJsonObject &response);
|
||||
void handleResponse(DapResponseType type, const QJsonObject &response);
|
||||
void handleStackTraceResponse(const QJsonObject &response);
|
||||
void handleScopesResponse(const QJsonObject &response);
|
||||
void handleThreadsResponse(const QJsonObject &response);
|
||||
|
||||
void handleEvent(const QJsonObject &event);
|
||||
void handleEvent(DapEventType type, const QJsonObject &event);
|
||||
void handleBreakpointEvent(const QJsonObject &event);
|
||||
void handleStoppedEvent(const QJsonObject &event);
|
||||
|
||||
@@ -136,7 +107,7 @@ protected:
|
||||
void connectDataGeneratorSignals();
|
||||
|
||||
QByteArray m_inbuffer;
|
||||
std::unique_ptr<IDataProvider> m_dataGenerator = nullptr;
|
||||
std::unique_ptr<DapClient> m_dapClient = nullptr;
|
||||
|
||||
int m_nextBreakpointId = 1;
|
||||
int m_currentThreadId = -1;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#include "gdbdapengine.h"
|
||||
|
||||
#include "dapclient.h"
|
||||
|
||||
#include <debugger/debuggermainwindow.h>
|
||||
|
||||
#include <utils/temporarydirectory.h>
|
||||
@@ -87,9 +89,11 @@ void GdbDapEngine::setupEngine()
|
||||
const DebuggerRunParameters &rp = runParameters();
|
||||
const CommandLine cmd{rp.debugger.command.executable(), {"-i", "dap"}};
|
||||
|
||||
m_dataGenerator = std::make_unique<ProcessDataProvider>(rp, cmd);
|
||||
std::unique_ptr<IDataProvider> dataProvider = std::make_unique<ProcessDataProvider>(rp, cmd);
|
||||
m_dapClient = std::make_unique<DapClient>(std::move(dataProvider));
|
||||
|
||||
connectDataGeneratorSignals();
|
||||
m_dataGenerator->start();
|
||||
m_dapClient->dataProvider()->start();
|
||||
|
||||
notifyEngineSetupOk();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user