From 8a7c4d2990e1d3d0c9f2702806347bb1dc20a7eb Mon Sep 17 00:00:00 2001 From: "daragan.andrey@demlabs.net" <daragan.andrey@demlabs.net> Date: Fri, 3 May 2019 19:32:08 +0300 Subject: [PATCH] [+] Added DapRPCProtocol files. --- DapRPCProtocol/DapRPCProtocol.pri | 25 ++ DapRPCProtocol/DapRpcAbstractServer.cpp | 29 ++ DapRPCProtocol/DapRpcAbstractServer.h | 31 ++ DapRPCProtocol/DapRpcLocalServer.cpp | 108 +++++ DapRPCProtocol/DapRpcLocalServer.h | 44 +++ DapRPCProtocol/DapRpcMessage.cpp | 373 ++++++++++++++++++ DapRPCProtocol/DapRpcMessage.h | 96 +++++ DapRPCProtocol/DapRpcService.cpp | 340 ++++++++++++++++ DapRPCProtocol/DapRpcService.h | 72 ++++ DapRPCProtocol/DapRpcServiceProvider.cpp | 93 +++++ DapRPCProtocol/DapRpcServiceProvider.h | 29 ++ DapRPCProtocol/DapRpcServiceReply.cpp | 31 ++ DapRPCProtocol/DapRpcServiceReply.h | 30 ++ DapRPCProtocol/DapRpcServiceRequest.cpp | 69 ++++ DapRPCProtocol/DapRpcServiceRequest.h | 31 ++ DapRPCProtocol/DapRpcSocket.cpp | 262 ++++++++++++ DapRPCProtocol/DapRpcSocket.h | 74 ++++ DapRPCProtocol/DapRpcTCPServer.cpp | 110 ++++++ DapRPCProtocol/DapRpcTCPServer.h | 44 +++ KelvinDashboardGUI/qml.qrc | 4 +- .../Resources/Icons/iconErrorNetwork.png | Bin 0 -> 2548 bytes .../Resources/Icons/iconNetwork.png | Bin 0 -> 2300 bytes 22 files changed, 1893 insertions(+), 2 deletions(-) create mode 100644 DapRPCProtocol/DapRPCProtocol.pri create mode 100644 DapRPCProtocol/DapRpcAbstractServer.cpp create mode 100644 DapRPCProtocol/DapRpcAbstractServer.h create mode 100644 DapRPCProtocol/DapRpcLocalServer.cpp create mode 100644 DapRPCProtocol/DapRpcLocalServer.h create mode 100644 DapRPCProtocol/DapRpcMessage.cpp create mode 100644 DapRPCProtocol/DapRpcMessage.h create mode 100644 DapRPCProtocol/DapRpcService.cpp create mode 100644 DapRPCProtocol/DapRpcService.h create mode 100644 DapRPCProtocol/DapRpcServiceProvider.cpp create mode 100644 DapRPCProtocol/DapRpcServiceProvider.h create mode 100644 DapRPCProtocol/DapRpcServiceReply.cpp create mode 100644 DapRPCProtocol/DapRpcServiceReply.h create mode 100644 DapRPCProtocol/DapRpcServiceRequest.cpp create mode 100644 DapRPCProtocol/DapRpcServiceRequest.h create mode 100644 DapRPCProtocol/DapRpcSocket.cpp create mode 100644 DapRPCProtocol/DapRpcSocket.h create mode 100644 DapRPCProtocol/DapRpcTCPServer.cpp create mode 100644 DapRPCProtocol/DapRpcTCPServer.h create mode 100644 KelvinDashboardService/Resources/Icons/iconErrorNetwork.png create mode 100644 KelvinDashboardService/Resources/Icons/iconNetwork.png diff --git a/DapRPCProtocol/DapRPCProtocol.pri b/DapRPCProtocol/DapRPCProtocol.pri new file mode 100644 index 0000000..c6899ff --- /dev/null +++ b/DapRPCProtocol/DapRPCProtocol.pri @@ -0,0 +1,25 @@ +INCLUDEPATH += $$PWD + +HEADERS += \ + $$PWD/DapRpcAbstractServer.h \ + $$PWD/DapRpcLocalServer.h \ + $$PWD/DapRpcMessage.h \ + $$PWD/DapRpcService.h \ + $$PWD/DapRpcServiceProvider.h \ + $$PWD/DapRpcServiceReply.h \ + $$PWD/DapRpcServiceRequest.h \ + $$PWD/DapRpcSocket.h \ + $$PWD/DapRpcTCPServer.h + +SOURCES += \ + $$PWD/DapRpcAbstractServer.cpp \ + $$PWD/DapRpcLocalServer.cpp \ + $$PWD/DapRpcMessage.cpp \ + $$PWD/DapRpcService.cpp \ + $$PWD/DapRpcServiceProvider.cpp \ + $$PWD/DapRpcServiceReply.cpp \ + $$PWD/DapRpcServiceRequest.cpp \ + $$PWD/DapRpcSocket.cpp \ + $$PWD/DapRpcTCPServer.cpp + + diff --git a/DapRPCProtocol/DapRpcAbstractServer.cpp b/DapRPCProtocol/DapRpcAbstractServer.cpp new file mode 100644 index 0000000..7a8882d --- /dev/null +++ b/DapRPCProtocol/DapRpcAbstractServer.cpp @@ -0,0 +1,29 @@ +#include "DapRpcAbstractServer.h" + +DapRpcAbstractServer::DapRpcAbstractServer() +{ + +} + +DapRpcAbstractServer::~DapRpcAbstractServer() +{ + +} + +int DapRpcAbstractServer::connectedClientCount() const +{ + return m_clients.size(); +} + +void DapRpcAbstractServer::notifyConnectedClients(const DapRpcMessage &message) +{ + for (int i = 0; i < m_clients.size(); ++i) + m_clients[i]->sendMessage(message); +} + +void DapRpcAbstractServer::notifyConnectedClients(const QString &method, const QJsonArray ¶ms) +{ + DapRpcMessage notification = + DapRpcMessage::createNotification(method, params); + notifyConnectedClients(notification); +} diff --git a/DapRPCProtocol/DapRpcAbstractServer.h b/DapRPCProtocol/DapRpcAbstractServer.h new file mode 100644 index 0000000..56fa07d --- /dev/null +++ b/DapRPCProtocol/DapRpcAbstractServer.h @@ -0,0 +1,31 @@ +#ifndef DapRPCABSTRACTSERVER_H +#define DapRPCABSTRACTSERVER_H + +#include <QList> +#include <QHostAddress> + +#include "DapRpcSocket.h" +#include "DapRpcMessage.h" +#include "DapRpcServiceProvider.h" + +class DapRpcAbstractServer : public DapRpcServiceProvider +{ +protected: + QList<DapRpcSocket*> m_clients; + +public: + DapRpcAbstractServer(); + + virtual ~DapRpcAbstractServer(); + virtual int connectedClientCount() const; + virtual bool listen(const QString &asAddress = QString(), quint16 aPort = 0) = 0; +// signals: + virtual void onClientConnected() = 0; + virtual void onClientDisconnected() = 0; + +// public slots: + virtual void notifyConnectedClients(const DapRpcMessage &message); + virtual void notifyConnectedClients(const QString &method, const QJsonArray ¶ms); +}; + +#endif // DapRPCABSTRACTSERVER_H diff --git a/DapRPCProtocol/DapRpcLocalServer.cpp b/DapRPCProtocol/DapRpcLocalServer.cpp new file mode 100644 index 0000000..11a2e84 --- /dev/null +++ b/DapRPCProtocol/DapRpcLocalServer.cpp @@ -0,0 +1,108 @@ +#include "DapRpcLocalServer.h" + +DapRpcLocalServer::DapRpcLocalServer(QObject *apParent) + : QLocalServer(apParent) +{ + this->setSocketOptions(QLocalServer::WorldAccessOption); +} + +DapRpcLocalServer::~DapRpcLocalServer() +{ + foreach (QLocalSocket *socket, m_socketLookup.keys()) { + socket->flush(); + socket->deleteLater(); + } + m_socketLookup.clear(); + + foreach (DapRpcSocket *client, m_clients) + client->deleteLater(); + m_clients.clear(); +} + +bool DapRpcLocalServer::listen(const QString &asAddress, quint16 aPort) +{ + Q_UNUSED(aPort); + + return QLocalServer::listen(asAddress); +} + +bool DapRpcLocalServer::addService(DapRpcService *apService) +{ + if (!DapRpcServiceProvider::addService(apService)) + return false; + + connect(apService, SIGNAL(notifyConnectedClients(DapRpcMessage)), + this, SLOT(notifyConnectedClients(DapRpcMessage))); + connect(apService, SIGNAL(notifyConnectedClients(QString,QJsonArray)), + this, SLOT(notifyConnectedClients(QString,QJsonArray))); + return true; +} + +bool DapRpcLocalServer::removeService(DapRpcService *apService) +{ + if (!DapRpcServiceProvider::removeService(apService)) + return false; + + disconnect(apService, SIGNAL(notifyConnectedClients(DapRpcMessage)), + this, SLOT(notifyConnectedClients(DapRpcMessage))); + disconnect(apService, SIGNAL(notifyConnectedClients(QString,QJsonArray)), + this, SLOT(notifyConnectedClients(QString,QJsonArray))); + return true; +} + +void DapRpcLocalServer::clientDisconnected() +{ + QLocalSocket *localSocket = static_cast<QLocalSocket*>(sender()); + if (!localSocket) { + qJsonRpcDebug() << "called with invalid socket"; + return; + } + if (m_socketLookup.contains(localSocket)) { + DapRpcSocket *socket = m_socketLookup.take(localSocket); + m_clients.removeAll(socket); + socket->deleteLater(); + } + + localSocket->deleteLater(); + emit onClientDisconnected(); +} + +void DapRpcLocalServer::messageProcessing(const DapRpcMessage &asMessage) +{ + DapRpcSocket *socket = static_cast<DapRpcSocket*>(sender()); + if (!socket) { + qJsonRpcDebug() << "called without service socket"; + return; + } + + processMessage(socket, asMessage); +} + +void DapRpcLocalServer::notifyConnectedClients(const DapRpcMessage &message) +{ + DapRpcAbstractServer::notifyConnectedClients(message); +} + +void DapRpcLocalServer::notifyConnectedClients(const QString &method, const QJsonArray ¶ms) +{ + DapRpcAbstractServer::notifyConnectedClients(method, params); +} + +void DapRpcLocalServer::incomingConnection(quintptr aSocketDescriptor) +{ + QLocalSocket *localSocket = new QLocalSocket(this); + if (!localSocket->setSocketDescriptor(aSocketDescriptor)) { + qJsonRpcDebug() << "nextPendingConnection is null"; + localSocket->deleteLater(); + return; + } + + QIODevice *device = qobject_cast<QIODevice*>(localSocket); + DapRpcSocket *socket = new DapRpcSocket(device, this); + connect(socket, SIGNAL(messageReceived(DapRpcMessage)), + this, SLOT(messageProcessing(DapRpcMessage))); + m_clients.append(socket); + connect(localSocket, SIGNAL(disconnected()), this, SLOT(clientDisconnected())); + m_socketLookup.insert(localSocket, socket); + emit onClientConnected(); +} diff --git a/DapRPCProtocol/DapRpcLocalServer.h b/DapRPCProtocol/DapRpcLocalServer.h new file mode 100644 index 0000000..9d7a7e7 --- /dev/null +++ b/DapRPCProtocol/DapRpcLocalServer.h @@ -0,0 +1,44 @@ +#ifndef DapRPCLOCALSERVER_H +#define DapRPCLOCALSERVER_H + +#include <QObject> +#include <QLocalSocket> +#include <QLocalServer> + +#include "DapRpcSocket.h" +#include "DapRpcService.h" +#include "DapRpcAbstractServer.h" + +class DapRpcLocalServer : public QLocalServer, public DapRpcAbstractServer +{ + Q_OBJECT + Q_DISABLE_COPY(DapRpcLocalServer) + + QHash<QLocalSocket*, DapRpcSocket*> m_socketLookup; + +protected: + virtual void incomingConnection(quintptr aSocketDescriptor); + +public: + explicit DapRpcLocalServer(QObject *apParent = nullptr); + virtual ~DapRpcLocalServer(); + + virtual bool listen(const QString &asAddress = QString(), quint16 aPort = 0); + bool addService(DapRpcService *apService); + bool removeService(DapRpcService *apService); + +signals: + void onClientConnected(); + void onClientDisconnected(); + +private slots: + void clientDisconnected(); + void messageProcessing(const DapRpcMessage &asMessage); + + // DapRpcAbstractServer interface +public slots: + void notifyConnectedClients(const DapRpcMessage &message); + void notifyConnectedClients(const QString &method, const QJsonArray ¶ms); +}; + +#endif // DapRPCLOCALSERVER_H diff --git a/DapRPCProtocol/DapRpcMessage.cpp b/DapRPCProtocol/DapRpcMessage.cpp new file mode 100644 index 0000000..d07901b --- /dev/null +++ b/DapRPCProtocol/DapRpcMessage.cpp @@ -0,0 +1,373 @@ +#include "DapRpcMessage.h" + +class DapRpcMessagePrivate : public QSharedData +{ +public: + DapRpcMessagePrivate(); + ~DapRpcMessagePrivate(); + DapRpcMessagePrivate(const DapRpcMessagePrivate &aDapRpcMessagePrivate); + + void initializeWithObject(const QJsonObject &aMessage); + static DapRpcMessage createBasicRequest(const QString &asMethod, const QJsonArray &aParams); + static DapRpcMessage createBasicRequest(const QString &asMethod, + const QJsonObject &aNamedParameters); + + DapRpcMessage::Type m_type; + QScopedPointer<QJsonObject> m_pObject; + + static int uniqueRequestCounter; +}; + +int DapRpcMessagePrivate::uniqueRequestCounter = 0; + +DapRpcMessagePrivate::DapRpcMessagePrivate() + : m_type(DapRpcMessage::Invalid), + m_pObject(nullptr) +{ +} + +DapRpcMessagePrivate::DapRpcMessagePrivate(const DapRpcMessagePrivate &aDapRpcMessagePrivate) + : QSharedData(aDapRpcMessagePrivate), + m_type(aDapRpcMessagePrivate.m_type), + m_pObject(aDapRpcMessagePrivate.m_pObject ? new QJsonObject(*aDapRpcMessagePrivate.m_pObject) : nullptr) +{ +} + +void DapRpcMessagePrivate::initializeWithObject(const QJsonObject &aMessage) +{ + m_pObject.reset(new QJsonObject(aMessage)); + if (aMessage.contains(QLatin1String("id"))) { + if (aMessage.contains(QLatin1String("result")) || + aMessage.contains(QLatin1String("error"))) { + if (aMessage.contains(QLatin1String("error")) && + !aMessage.value(QLatin1String("error")).isNull()) + m_type = DapRpcMessage::Error; + else + m_type = DapRpcMessage::Response; + } else if (aMessage.contains(QLatin1String("method"))) { + m_type = DapRpcMessage::Request; + } + } else { + if (aMessage.contains(QLatin1String("method"))) + m_type = DapRpcMessage::Notification; + } +} + +DapRpcMessagePrivate::~DapRpcMessagePrivate() +{ +} + +DapRpcMessage::DapRpcMessage() + : d(new DapRpcMessagePrivate) +{ + d->m_pObject.reset(new QJsonObject); +} + +DapRpcMessage::DapRpcMessage(const DapRpcMessage &aDapRPCMessage) + : d(aDapRPCMessage.d) +{ +} + +DapRpcMessage::~DapRpcMessage() +{ +} + +DapRpcMessage &DapRpcMessage::operator=(const DapRpcMessage &aDapRPCMessage) +{ + d = aDapRPCMessage.d; + return *this; +} + +bool DapRpcMessage::operator==(const DapRpcMessage &aDapRpcMessage) const +{ + if (aDapRpcMessage.d == d) + return true; + + if (aDapRpcMessage.type() == type()) { + if (aDapRpcMessage.type() == DapRpcMessage::Error) { + return (aDapRpcMessage.errorCode() == errorCode() && + aDapRpcMessage.errorMessage() == errorMessage() && + aDapRpcMessage.errorData() == errorData()); + } else { + if (aDapRpcMessage.type() == DapRpcMessage::Notification) { + return (aDapRpcMessage.method() == method() && + aDapRpcMessage.params() == params()); + } else { + return (aDapRpcMessage.id() == id() && + aDapRpcMessage.method() == method() && + aDapRpcMessage.params() == params()); + } + } + } + + return false; +} + +DapRpcMessage DapRpcMessage::fromJson(const QByteArray &aData) +{ + DapRpcMessage result; + QJsonParseError error; + QJsonDocument document = QJsonDocument::fromJson(aData, &error); + if (error.error != QJsonParseError::NoError) { + qJsonRpcDebug() << error.errorString(); + return result; + } + + if (!document.isObject()) { + qJsonRpcDebug() << "invalid message: " << aData; + return result; + } + + result.d->initializeWithObject(document.object()); + return result; +} + +DapRpcMessage DapRpcMessage::fromObject(const QJsonObject &aObject) +{ + DapRpcMessage result; + result.d->initializeWithObject(aObject); + return result; +} + +QJsonObject DapRpcMessage::toObject() const +{ + if (d->m_pObject) + return QJsonObject(*d->m_pObject); + return QJsonObject(); +} + +QByteArray DapRpcMessage::toJson() const +{ + if (d->m_pObject) { + QJsonDocument doc(*d->m_pObject); + return doc.toJson(); + } + + return QByteArray(); +} + +bool DapRpcMessage::isValid() const +{ + return d->m_type != DapRpcMessage::Invalid; +} + +DapRpcMessage::Type DapRpcMessage::type() const +{ + return d->m_type; +} + +DapRpcMessage DapRpcMessagePrivate::createBasicRequest(const QString &asMethod, const QJsonArray &aParams) +{ + DapRpcMessage request; + request.d->m_pObject->insert(QLatin1String("jsonrpc"), QLatin1String("2.0")); + request.d->m_pObject->insert(QLatin1String("method"), asMethod); + if (!aParams.isEmpty()) + request.d->m_pObject->insert(QLatin1String("params"), aParams); + return request; +} + +DapRpcMessage DapRpcMessagePrivate::createBasicRequest(const QString &asMethod, + const QJsonObject &aNamedParameters) +{ + DapRpcMessage request; + request.d->m_pObject->insert(QLatin1String("jsonrpc"), QLatin1String("2.0")); + request.d->m_pObject->insert(QLatin1String("method"), asMethod); + if (!aNamedParameters.isEmpty()) + request.d->m_pObject->insert(QLatin1String("params"), aNamedParameters); + return request; +} + +DapRpcMessage DapRpcMessage::createRequest(const QString &asMethod, const QJsonArray &aParams) +{ + DapRpcMessage request = DapRpcMessagePrivate::createBasicRequest(asMethod, aParams); + request.d->m_type = DapRpcMessage::Request; + DapRpcMessagePrivate::uniqueRequestCounter++; + request.d->m_pObject->insert(QLatin1String("id"), DapRpcMessagePrivate::uniqueRequestCounter); + return request; +} + +DapRpcMessage DapRpcMessage::createRequest(const QString &asMethod, const QJsonValue &aParam) +{ + QJsonArray params; + params.append(aParam); + return createRequest(asMethod, params); +} + +DapRpcMessage DapRpcMessage::createRequest(const QString &asMethod, + const QJsonObject &aNamedParameters) +{ + DapRpcMessage request = + DapRpcMessagePrivate::createBasicRequest(asMethod, aNamedParameters); + request.d->m_type = DapRpcMessage::Request; + DapRpcMessagePrivate::uniqueRequestCounter++; + request.d->m_pObject->insert(QLatin1String("id"), DapRpcMessagePrivate::uniqueRequestCounter); + return request; +} + +DapRpcMessage DapRpcMessage::createNotification(const QString &asMethod, const QJsonArray &aParams) +{ + DapRpcMessage notification = DapRpcMessagePrivate::createBasicRequest(asMethod, aParams); + notification.d->m_type = DapRpcMessage::Notification; + return notification; +} + +DapRpcMessage DapRpcMessage::createNotification(const QString &asMethod, const QJsonValue &aParam) +{ + QJsonArray params; + params.append(aParam); + return createNotification(asMethod, params); +} + +DapRpcMessage DapRpcMessage::createNotification(const QString &asMethod, + const QJsonObject &aNamedParameters) +{ + DapRpcMessage notification = + DapRpcMessagePrivate::createBasicRequest(asMethod, aNamedParameters); + notification.d->m_type = DapRpcMessage::Notification; + return notification; +} + +DapRpcMessage DapRpcMessage::createResponse(const QJsonValue &aResult) const +{ + DapRpcMessage response; + if (d->m_pObject->contains(QLatin1String("id"))) { + QJsonObject *object = response.d->m_pObject.data(); + object->insert(QLatin1String("jsonrpc"), QLatin1String("2.0")); + object->insert(QLatin1String("id"), d->m_pObject->value(QLatin1String("id"))); + object->insert(QLatin1String("result"), aResult); + response.d->m_type = DapRpcMessage::Response; + } + + return response; +} + +DapRpcMessage DapRpcMessage::createErrorResponse(DapErrorCode aCode, + const QString &asMessage, + const QJsonValue &aData) const +{ + DapRpcMessage response; + QJsonObject error; + error.insert(QLatin1String("code"), aCode); + if (!asMessage.isEmpty()) + error.insert(QLatin1String("message"), asMessage); + if (!aData.isUndefined()) + error.insert(QLatin1String("data"), aData); + + response.d->m_type = DapRpcMessage::Error; + QJsonObject *object = response.d->m_pObject.data(); + object->insert(QLatin1String("jsonrpc"), QLatin1String("2.0")); + if (d->m_pObject->contains(QLatin1String("id"))) + object->insert(QLatin1String("id"), d->m_pObject->value(QLatin1String("id"))); + else + object->insert(QLatin1String("id"), 0); + object->insert(QLatin1String("error"), error); + return response; +} + +int DapRpcMessage::id() const +{ + if (d->m_type == DapRpcMessage::Notification || !d->m_pObject) + return -1; + + const QJsonValue &value = d->m_pObject->value(QLatin1String("id")); + if (value.isString()) + return value.toString().toInt(); + return value.toInt(); +} + +QString DapRpcMessage::method() const +{ + if (d->m_type == DapRpcMessage::Response || !d->m_pObject) + return QString(); + + return d->m_pObject->value(QLatin1String("method")).toString(); +} + +QJsonValue DapRpcMessage::params() const +{ + if (d->m_type == DapRpcMessage::Response || d->m_type == DapRpcMessage::Error) + return QJsonValue(QJsonValue::Undefined); + if (!d->m_pObject) + return QJsonValue(QJsonValue::Undefined); + + return d->m_pObject->value(QLatin1String("params")); +} + +QJsonValue DapRpcMessage::result() const +{ + if (d->m_type != DapRpcMessage::Response || !d->m_pObject) + return QJsonValue(QJsonValue::Undefined); + + return d->m_pObject->value(QLatin1String("result")); +} + +int DapRpcMessage::errorCode() const +{ + if (d->m_type != DapRpcMessage::Error || !d->m_pObject) + return 0; + + QJsonObject error = + d->m_pObject->value(QLatin1String("error")).toObject(); + const QJsonValue &value = error.value(QLatin1String("code")); + if (value.isString()) + return value.toString().toInt(); + return value.toInt(); +} + +QString DapRpcMessage::errorMessage() const +{ + if (d->m_type != DapRpcMessage::Error || !d->m_pObject) + return QString(); + + QJsonObject error = + d->m_pObject->value(QLatin1String("error")).toObject(); + return error.value(QLatin1String("message")).toString(); +} + +QJsonValue DapRpcMessage::errorData() const +{ + if (d->m_type != DapRpcMessage::Error || !d->m_pObject) + return QJsonValue(QJsonValue::Undefined); + + QJsonObject error = + d->m_pObject->value(QLatin1String("error")).toObject(); + return error.value(QLatin1String("data")); +} + +static QDebug operator<<(QDebug dbg, DapRpcMessage::Type type) +{ + switch (type) { + case DapRpcMessage::Request: + return dbg << "DapRpcMessage::Request"; + case DapRpcMessage::Response: + return dbg << "DapRpcMessage::Response"; + case DapRpcMessage::Notification: + return dbg << "DapRpcMessage::Notification"; + case DapRpcMessage::Error: + return dbg << "DapRpcMessage::Error"; + default: + return dbg << "DapRpcMessage::Invalid"; + } +} + +QDebug operator<<(QDebug dbg, const DapRpcMessage &msg) +{ + dbg.nospace() << "DapRpcMessage(type=" << msg.type(); + if (msg.type() != DapRpcMessage::Notification) { + dbg.nospace() << ", id=" << msg.id(); + } + + if (msg.type() == DapRpcMessage::Request || + msg.type() == DapRpcMessage::Notification) { + dbg.nospace() << ", method=" << msg.method() + << ", params=" << msg.params(); + } else if (msg.type() == DapRpcMessage::Response) { + dbg.nospace() << ", result=" << msg.result(); + } else if (msg.type() == DapRpcMessage::Error) { + dbg.nospace() << ", code=" << msg.errorCode() + << ", message=" << msg.errorMessage() + << ", data=" << msg.errorData(); + } + dbg.nospace() << ")"; + return dbg.space(); +} diff --git a/DapRPCProtocol/DapRpcMessage.h b/DapRPCProtocol/DapRpcMessage.h new file mode 100644 index 0000000..92b47aa --- /dev/null +++ b/DapRPCProtocol/DapRpcMessage.h @@ -0,0 +1,96 @@ +#ifndef DapRPCMESSAGE_H +#define DapRPCMESSAGE_H + +#include <QSharedDataPointer> +#include <QMetaType> +#include <QJsonDocument> +#include <QJsonValue> +#include <QJsonObject> +#include <QJsonArray> +#include <QDebug> + +#define qJsonRpcDebug if (qgetenv("QJSONRPC_DEBUG").isEmpty()); else qDebug + +enum DapErrorCode { + NoError = 0, + ParseError = -32700, // Invalid JSON was received by the server. + // An error occurred on the server while parsing the JSON text. + InvalidRequest = -32600, // The JSON sent is not a valid Request object. + MethodNotFound = -32601, // The method does not exist / is not available. + InvalidParams = -32602, // Invalid method parameter(s). + InternalError = -32603, // Internal JSON-RPC error. + ServerErrorBase = -32000, // Reserved for implementation-defined server-errors. + UserError = -32099, // Anything after this is user defined + TimeoutError = -32100 +}; +Q_DECLARE_METATYPE(DapErrorCode) + +class DapRpcMessagePrivate; +class DapRpcMessage +{ + friend class DapRpcMessagePrivate; + QSharedDataPointer<DapRpcMessagePrivate> d; + +public: + DapRpcMessage(); + DapRpcMessage(const DapRpcMessage &aDapRPCMessage); + DapRpcMessage &operator=(const DapRpcMessage &aDapRPCMessage); + ~DapRpcMessage(); + + inline void swap(DapRpcMessage &aDapRPCMessage) { qSwap(d, aDapRPCMessage.d); } + + enum Type { + Invalid, + Request, + Response, + Notification, + Error + }; + + static DapRpcMessage createRequest(const QString &asMethod, + const QJsonArray &aParams = QJsonArray()); + static DapRpcMessage createRequest(const QString &asMethod, const QJsonValue &aParam); + static DapRpcMessage createRequest(const QString &asMethod, const QJsonObject &aNamedParameters); + + static DapRpcMessage createNotification(const QString &asMethod, + const QJsonArray &aParams = QJsonArray()); + static DapRpcMessage createNotification(const QString &asMethod, const QJsonValue &aParam); + static DapRpcMessage createNotification(const QString &asMethod, + const QJsonObject &aNamedParameters); + + DapRpcMessage createResponse(const QJsonValue &aResult) const; + DapRpcMessage createErrorResponse(DapErrorCode aCode, + const QString &asMessage = QString(), + const QJsonValue &aData = QJsonValue()) const; + + DapRpcMessage::Type type() const; + bool isValid() const; + int id() const; + + // request + QString method() const; + QJsonValue params() const; + + // response + QJsonValue result() const; + + // error + int errorCode() const; + QString errorMessage() const; + QJsonValue errorData() const; + + QJsonObject toObject() const; + static DapRpcMessage fromObject(const QJsonObject &aObject); + + QByteArray toJson() const; + static DapRpcMessage fromJson(const QByteArray &aData); + + bool operator==(const DapRpcMessage &aDapRpcMessage) const; + inline bool operator!=(const DapRpcMessage &aDapRpcMessage) const { return !(operator==(aDapRpcMessage)); } +}; + +QDebug operator<<(QDebug, const DapRpcMessage &); +Q_DECLARE_METATYPE(DapRpcMessage) +Q_DECLARE_SHARED(DapRpcMessage) + +#endif // DapRPCMESSAGE_H diff --git a/DapRPCProtocol/DapRpcService.cpp b/DapRPCProtocol/DapRpcService.cpp new file mode 100644 index 0000000..f52ce29 --- /dev/null +++ b/DapRPCProtocol/DapRpcService.cpp @@ -0,0 +1,340 @@ +#include "DapRpcService.h" +#include "DapRpcSocket.h" + +ParameterInfo::ParameterInfo(const QString &asName, int aType, bool aOut) + : type(aType), + jsType(DapRpcService::convertVariantTypeToJSType(aType)), + name(asName), + out(aOut) +{ +} + +MethodInfo::MethodInfo() + : returnType(QMetaType::Void), + valid(false), + hasOut(false) +{ +} + +MethodInfo::MethodInfo(const QMetaMethod &aMethod) + : returnType(QMetaType::Void), + valid(true), + hasOut(false) +{ + returnType = aMethod.returnType(); + if (returnType == QMetaType::UnknownType) { + qJsonRpcDebug() << "DapRpcService: can't bind method's return type" + << QString(aMethod.name()); + valid = false; + return; + } + + parameters.reserve(aMethod.parameterCount()); + + const QList<QByteArray> &types = aMethod.parameterTypes(); + const QList<QByteArray> &names = aMethod.parameterNames(); + for (int i = 0; i < types.size(); ++i) { + QByteArray parameterType = types.at(i); + const QByteArray ¶meterName = names.at(i); + bool out = parameterType.endsWith('&'); + + if (out) { + hasOut = true; + parameterType.resize(parameterType.size() - 1); + } + + int type = QMetaType::type(parameterType); + if (type == 0) { + qJsonRpcDebug() << "DapRpcService: can't bind method's parameter" + << QString(parameterType); + valid = false; + break; + } + + parameters.append(ParameterInfo(parameterName, type, out)); + } +} + +void DapRpcService::setCurrentRequest(const DapRpcServiceRequest &aCurrentRequest) +{ + m_currentRequest = aCurrentRequest; +} + +DapRpcService::DapRpcService(QObject *apParent) + : QObject(apParent) +{ +} + +DapRpcService::~DapRpcService() +{ +} + +DapRpcServiceRequest DapRpcService::currentRequest() const +{ + return m_currentRequest; +} + +void DapRpcService::beginDelayedResponse() +{ + m_delayedResponse = true; +} + +int DapRpcService::convertVariantTypeToJSType(int aType) +{ + switch (aType) { + case QMetaType::Int: + case QMetaType::UInt: + case QMetaType::Double: + case QMetaType::Long: + case QMetaType::LongLong: + case QMetaType::Short: + case QMetaType::Char: + case QMetaType::ULong: + case QMetaType::ULongLong: + case QMetaType::UShort: + case QMetaType::UChar: + case QMetaType::Float: + return QJsonValue::Double; // all numeric types in js are doubles + case QMetaType::QVariantList: + case QMetaType::QStringList: + return QJsonValue::Array; + case QMetaType::QVariantMap: + return QJsonValue::Object; + case QMetaType::QString: + return QJsonValue::String; + case QMetaType::Bool: + return QJsonValue::Bool; + default: + break; + } + + return QJsonValue::Undefined; +} + +int DapRpcService::qjsonRpcMessageType = qRegisterMetaType<DapRpcMessage>("DapRpcMessage"); +void DapRpcService::cacheInvokableInfo() +{ + const QMetaObject *obj = metaObject(); + int startIdx = staticMetaObject.methodCount(); // skip QObject slots + for (int idx = startIdx; idx < obj->methodCount(); ++idx) { + const QMetaMethod method = obj->method(idx); + if ((method.methodType() == QMetaMethod::Slot && + method.access() == QMetaMethod::Public) || + method.methodType() == QMetaMethod::Signal) { + + QByteArray signature = method.methodSignature(); + QByteArray methodName = method.name(); + + MethodInfo info(method); + if (!info.valid) + continue; + + if (signature.contains("QVariant")) + m_invokableMethodHash[methodName].append(idx); + else + m_invokableMethodHash[methodName].prepend(idx); + m_methodInfoHash[idx] = info; + } + } +} + +static bool jsParameterCompare(const QJsonArray ¶meters, + const MethodInfo &info) +{ + int j = 0; + for (int i = 0; i < info.parameters.size() && j < parameters.size(); ++i) { + int jsType = info.parameters.at(i).jsType; + if (jsType != QJsonValue::Undefined && jsType != parameters.at(j).type()) { + if (!info.parameters.at(i).out) + return false; + } else { + ++j; + } + } + + return (j == parameters.size()); +} + +static bool jsParameterCompare(const QJsonObject ¶meters, + const MethodInfo &info) +{ + for (int i = 0; i < info.parameters.size(); ++i) { + int jsType = info.parameters.at(i).jsType; + QJsonValue value = parameters.value(info.parameters.at(i).name); + if (value == QJsonValue::Undefined) { + if (!info.parameters.at(i).out) + return false; + } else if (jsType == QJsonValue::Undefined) { + continue; + } else if (jsType != value.type()) { + return false; + } + } + + return true; +} + +static inline QVariant convertArgument(const QJsonValue &argument, + const ParameterInfo &info) +{ + if (argument.isUndefined()) + return QVariant(info.type, Q_NULLPTR); + + if (info.type == QMetaType::QJsonValue || info.type == QMetaType::QVariant || + info.type >= QMetaType::User) { + + if (info.type == QMetaType::QVariant) + return argument.toVariant(); + + QVariant result(argument); + if (info.type >= QMetaType::User && result.canConvert(info.type)) + result.convert(info.type); + return result; + } + + QVariant result = argument.toVariant(); + if (result.userType() == info.type || info.type == QMetaType::QVariant) { + return result; + } else if (result.canConvert(info.type)) { + result.convert(info.type); + return result; + } else if (info.type < QMetaType::User) { + // already tried for >= user, this is the last resort + QVariant result(argument); + if (result.canConvert(info.type)) { + result.convert(info.type); + return result; + } + } + + return QVariant(); +} + +QJsonValue DapRpcService::convertReturnValue(QVariant &aReturnValue) +{ + if (static_cast<int>(aReturnValue.type()) == qMetaTypeId<QJsonObject>()) + return QJsonValue(aReturnValue.toJsonObject()); + else if (static_cast<int>(aReturnValue.type()) == qMetaTypeId<QJsonArray>()) + return QJsonValue(aReturnValue.toJsonArray()); + + switch (aReturnValue.type()) { + case QMetaType::Bool: + case QMetaType::Int: + case QMetaType::Double: + case QMetaType::LongLong: + case QMetaType::ULongLong: + case QMetaType::UInt: + case QMetaType::QString: + case QMetaType::QStringList: + case QMetaType::QVariantList: + case QMetaType::QVariantMap: + return QJsonValue::fromVariant(aReturnValue); + default: + // if a conversion operator was registered it will be used + if (aReturnValue.convert(QMetaType::QJsonValue)) + return aReturnValue.toJsonValue(); + else + return QJsonValue(); + } +} + +static inline QByteArray methodName(const DapRpcMessage &request) +{ + const QString &methodPath(request.method()); + return methodPath.midRef(methodPath.lastIndexOf('.') + 1).toLatin1(); +} + +DapRpcMessage DapRpcService::dispatch(const DapRpcMessage &aRequest) +{ + if (aRequest.type() != DapRpcMessage::Request && + aRequest.type() != DapRpcMessage::Notification) { + return aRequest.createErrorResponse(DapErrorCode::InvalidRequest, "invalid request"); + } + + const QByteArray &method(methodName(aRequest)); + if (!m_invokableMethodHash.contains(method)) { + return aRequest.createErrorResponse(DapErrorCode::MethodNotFound, "invalid method called"); + } + + int idx = -1; + QVariantList arguments; + const QList<int> &indexes = m_invokableMethodHash.value(method); + const QJsonValue ¶ms = aRequest.params(); + QVarLengthArray<void *, 10> parameters; + QVariant returnValue; + QMetaType::Type returnType = QMetaType::Void; + + bool usingNamedParameters = params.isObject(); + foreach (int methodIndex, indexes) { + MethodInfo &info = m_methodInfoHash[methodIndex]; + bool methodMatch = usingNamedParameters ? + jsParameterCompare(params.toObject(), info) : + jsParameterCompare(params.toArray(), info); + + if (methodMatch) { + idx = methodIndex; + arguments.reserve(info.parameters.size()); + returnType = static_cast<QMetaType::Type>(info.returnType); + returnValue = (returnType == QMetaType::Void) ? + QVariant() : QVariant(returnType, Q_NULLPTR); + if (returnType == QMetaType::QVariant) + parameters.append(&returnValue); + else + parameters.append(returnValue.data()); + + for (int i = 0; i < info.parameters.size(); ++i) { + const ParameterInfo ¶meterInfo = info.parameters.at(i); + QJsonValue incomingArgument = usingNamedParameters ? + params.toObject().value(parameterInfo.name) : + params.toArray().at(i); + + QVariant argument = convertArgument(incomingArgument, parameterInfo); + if (!argument.isValid()) { + QString message = incomingArgument.isUndefined() ? + QString("failed to construct default object for '%1'").arg(parameterInfo.name) : + QString("failed to convert from JSON for '%1'").arg(parameterInfo.name); + return aRequest.createErrorResponse(DapErrorCode::InvalidParams, message); + } + + arguments.push_back(argument); + if (parameterInfo.type == QMetaType::QVariant) + parameters.append(static_cast<void *>(&arguments.last())); + else + parameters.append(const_cast<void *>(arguments.last().constData())); + } + break; + } + } + + if (idx == -1) { + return aRequest.createErrorResponse(DapErrorCode::InvalidParams, "invalid parameters"); + } + + MethodInfo &info = m_methodInfoHash[idx]; + + bool success = + const_cast<DapRpcService*>(this)->qt_metacall(QMetaObject::InvokeMetaMethod, idx, parameters.data()) < 0; + if (!success) { + QString message = QString("dispatch for method '%1' failed").arg(method.constData()); + return aRequest.createErrorResponse(DapErrorCode::InvalidRequest, message); + } + + if (m_delayedResponse) { + m_delayedResponse = false; + return DapRpcMessage(); + } + + if (info.hasOut) { + QJsonArray ret; + if (info.returnType != QMetaType::Void) + ret.append(convertReturnValue(returnValue)); + for (int i = 0; i < info.parameters.size(); ++i) + if (info.parameters.at(i).out) + ret.append(convertReturnValue(arguments[i])); + if (ret.size() > 1) + return aRequest.createResponse(ret); + return aRequest.createResponse(ret.first()); + } + + return aRequest.createResponse(convertReturnValue(returnValue)); +} diff --git a/DapRPCProtocol/DapRpcService.h b/DapRPCProtocol/DapRpcService.h new file mode 100644 index 0000000..c13786f --- /dev/null +++ b/DapRPCProtocol/DapRpcService.h @@ -0,0 +1,72 @@ +#ifndef DapRPCSERVICE_H +#define DapRPCSERVICE_H + +#include <QObject> +#include <QVariant> +#include <QPointer> +#include <QVarLengthArray> +#include <QMetaMethod> +#include <QEventLoop> +#include <QDebug> + + +#include "DapRpcMessage.h" +#include "DapRpcServiceRequest.h" + +struct ParameterInfo +{ + ParameterInfo(const QString &asName = QString(), int aType = 0, bool aOut = false); + + int type; + int jsType; + QString name; + bool out; +}; + +struct MethodInfo +{ + MethodInfo(); + MethodInfo(const QMetaMethod &aMethod); + + QVarLengthArray<ParameterInfo> parameters; + int returnType; + bool valid; + bool hasOut; +}; + +class DapRpcService : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(DapRpcService) + + QHash<int, MethodInfo > m_methodInfoHash; + QHash<QByteArray, QList<int> > m_invokableMethodHash; + DapRpcServiceRequest m_currentRequest; + bool m_delayedResponse {false}; + +protected: + DapRpcServiceRequest currentRequest() const; + void beginDelayedResponse(); + +public: + explicit DapRpcService(QObject *apParent = nullptr); + ~DapRpcService(); + + void cacheInvokableInfo(); + static int qjsonRpcMessageType; + static int convertVariantTypeToJSType(int aType); + static QJsonValue convertReturnValue(QVariant &aReturnValue); + + void setCurrentRequest(const DapRpcServiceRequest &aCurrentRequest); + +signals: + void result(const DapRpcMessage &aDapRpcMessage); + void notifyConnectedClients(const DapRpcMessage &aDapRpcMessage); + void notifyConnectedClients(const QString &asMethod, const QJsonArray &aParams = QJsonArray()); + +public slots: + DapRpcMessage dispatch(const DapRpcMessage &aRequest); +}; + +#endif // DapRPCSERVICE_H + diff --git a/DapRPCProtocol/DapRpcServiceProvider.cpp b/DapRPCProtocol/DapRpcServiceProvider.cpp new file mode 100644 index 0000000..106e9fe --- /dev/null +++ b/DapRPCProtocol/DapRpcServiceProvider.cpp @@ -0,0 +1,93 @@ +#include "DapRpcServiceProvider.h" +#include "DapRpcSocket.h" + +DapRpcServiceProvider::DapRpcServiceProvider() +{ +} + +DapRpcServiceProvider::~DapRpcServiceProvider() +{ +} + +QByteArray DapRpcServiceProvider::getServiceName(DapRpcService *apService) +{ + const QMetaObject *mo = apService->metaObject(); + for (int i = 0; i < mo->classInfoCount(); i++) { + const QMetaClassInfo mci = mo->classInfo(i); + if (mci.name() == QLatin1String("serviceName")) + return mci.value(); + } + + return QByteArray(mo->className()).toLower(); +} + +bool DapRpcServiceProvider::addService(DapRpcService *apService) +{ + QByteArray serviceName = getServiceName(apService); + if (serviceName.isEmpty()) { + qJsonRpcDebug() << "service added without serviceName classinfo, aborting"; + return false; + } + + if (m_services.contains(serviceName)) { + qJsonRpcDebug() << "service with name " << serviceName << " already exist"; + return false; + } + + apService->cacheInvokableInfo(); + m_services.insert(serviceName, apService); + if (!apService->parent()) + m_cleanupHandler.add(apService); + return true; +} + +bool DapRpcServiceProvider::removeService(DapRpcService *apService) +{ + QByteArray serviceName = getServiceName(apService); + if (!m_services.contains(serviceName)) { + qJsonRpcDebug() << "can not find service with name " << serviceName; + return false; + } + + m_cleanupHandler.remove(m_services.value(serviceName)); + m_services.remove(serviceName); + return true; +} + +void DapRpcServiceProvider::processMessage(DapRpcSocket *apSocket, const DapRpcMessage &aMessage) +{ + switch (aMessage.type()) { + case DapRpcMessage::Request: + case DapRpcMessage::Notification: { + QByteArray serviceName = aMessage.method().section(".", 0, -2).toLatin1(); + if (serviceName.isEmpty() || !m_services.contains(serviceName)) { + if (aMessage.type() == DapRpcMessage::Request) { + DapRpcMessage error = + aMessage.createErrorResponse(DapErrorCode::MethodNotFound, + QString("service '%1' not found").arg(serviceName.constData())); + apSocket->notify(error); + } + } else { + DapRpcService *service = m_services.value(serviceName); + service->setCurrentRequest(DapRpcServiceRequest(aMessage, apSocket)); + if (aMessage.type() == DapRpcMessage::Request) + QObject::connect(service, SIGNAL(result(DapRpcMessage)), + apSocket, SLOT(notify(DapRpcMessage)), Qt::UniqueConnection); + DapRpcMessage response = service->dispatch(aMessage); + if (response.isValid()) + apSocket->notify(response); + } + } + break; + + case DapRpcMessage::Response: + break; + + default: { + DapRpcMessage error = + aMessage.createErrorResponse(DapErrorCode::InvalidRequest, QString("invalid request")); + apSocket->notify(error); + break; + } + }; +} diff --git a/DapRPCProtocol/DapRpcServiceProvider.h b/DapRPCProtocol/DapRpcServiceProvider.h new file mode 100644 index 0000000..6e74d30 --- /dev/null +++ b/DapRPCProtocol/DapRpcServiceProvider.h @@ -0,0 +1,29 @@ +#ifndef DapRPCSERVICEPROVIDER_H +#define DapRPCSERVICEPROVIDER_H + +#include <QScopedPointer> +#include <QObjectCleanupHandler> +#include <QHash> +#include <QMetaObject> +#include <QMetaClassInfo> +#include <QDebug> + +#include "DapRpcService.h" + +class DapRpcServiceProvider +{ + QHash<QByteArray, DapRpcService*> m_services; + QObjectCleanupHandler m_cleanupHandler; + +protected: + DapRpcServiceProvider(); + void processMessage(DapRpcSocket *apSocket, const DapRpcMessage &aMessage); + +public: + virtual ~DapRpcServiceProvider(); + virtual bool addService(DapRpcService *apService); + virtual bool removeService(DapRpcService *apService); + QByteArray getServiceName(DapRpcService *apService); +}; + +#endif // DapRPCSERVICEPROVIDER_H diff --git a/DapRPCProtocol/DapRpcServiceReply.cpp b/DapRPCProtocol/DapRpcServiceReply.cpp new file mode 100644 index 0000000..ba26f38 --- /dev/null +++ b/DapRPCProtocol/DapRpcServiceReply.cpp @@ -0,0 +1,31 @@ +#include "DapRpcServiceReply.h" + +DapRpcServiceReply::DapRpcServiceReply(QObject *apParent) + : QObject(apParent) +{ +} + +DapRpcServiceReply::~DapRpcServiceReply() +{ + +} + +void DapRpcServiceReply::setRequest(const DapRpcMessage &aRequest) +{ + m_request = aRequest; +} + +void DapRpcServiceReply::setResponse(const DapRpcMessage &aResponse) +{ + m_response = aResponse; +} + +DapRpcMessage DapRpcServiceReply::request() const +{ + return m_request; +} + +DapRpcMessage DapRpcServiceReply::response() const +{ + return m_response; +} diff --git a/DapRPCProtocol/DapRpcServiceReply.h b/DapRPCProtocol/DapRpcServiceReply.h new file mode 100644 index 0000000..2916114 --- /dev/null +++ b/DapRPCProtocol/DapRpcServiceReply.h @@ -0,0 +1,30 @@ +#ifndef DapRPCSERVICEREPLY_H +#define DapRPCSERVICEREPLY_H + +#include <QObject> +#include <QNetworkReply> + +#include "DapRpcMessage.h" + +class DapRpcServiceReply : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(DapRpcServiceReply) + DapRpcMessage m_request; + DapRpcMessage m_response; + +public: + explicit DapRpcServiceReply(QObject *apParent = nullptr); + virtual ~DapRpcServiceReply(); + + DapRpcMessage request() const; + DapRpcMessage response() const; + + void setRequest(const DapRpcMessage &aRequest); + void setResponse(const DapRpcMessage &aResponse); + +signals: + void finished(); +}; + +#endif // DapRPCSERVICEREPLY_H diff --git a/DapRPCProtocol/DapRpcServiceRequest.cpp b/DapRPCProtocol/DapRpcServiceRequest.cpp new file mode 100644 index 0000000..3a00a07 --- /dev/null +++ b/DapRPCProtocol/DapRpcServiceRequest.cpp @@ -0,0 +1,69 @@ +#include "DapRpcSocket.h" +#include "DapRpcServiceRequest.h" + + +DapRpcServiceRequest::DapRpcServiceRequest() +{ +} + +DapRpcServiceRequest::~DapRpcServiceRequest() +{ +} + +DapRpcServiceRequest::DapRpcServiceRequest(const DapRpcServiceRequest &aDapRpcServiceRequest) +{ + m_request = aDapRpcServiceRequest.m_request; + m_socket = aDapRpcServiceRequest.m_socket; +} + +DapRpcServiceRequest::DapRpcServiceRequest(const DapRpcMessage &aRequest, + DapRpcSocket *apSocket) +{ + m_request = aRequest; + m_socket = apSocket; +} + +DapRpcServiceRequest &DapRpcServiceRequest::operator=(const DapRpcServiceRequest &other) +{ + m_request = other.m_request; + m_socket = other.m_socket; + return *this; +} + +bool DapRpcServiceRequest::isValid() const +{ + return (m_request.isValid() && !m_socket.isNull()); +} + +DapRpcMessage DapRpcServiceRequest::request() const +{ + return m_request; +} + +DapRpcSocket *DapRpcServiceRequest::socket() const +{ + return m_socket; +} + +bool DapRpcServiceRequest::respond(QVariant aReturnValue) +{ + if (!m_socket) { + qJsonRpcDebug() << "socket was closed"; + return false; + } + + DapRpcMessage response = + m_request.createResponse(DapRpcService::convertReturnValue(aReturnValue)); + return respond(response); +} + +bool DapRpcServiceRequest::respond(const DapRpcMessage &aResponse) +{ + if (!m_socket) { + qJsonRpcDebug() << "socket was closed"; + return false; + } + + QMetaObject::invokeMethod(m_socket, "notify", Q_ARG(DapRpcMessage, aResponse)); + return true; +} diff --git a/DapRPCProtocol/DapRpcServiceRequest.h b/DapRPCProtocol/DapRpcServiceRequest.h new file mode 100644 index 0000000..aa689f8 --- /dev/null +++ b/DapRPCProtocol/DapRpcServiceRequest.h @@ -0,0 +1,31 @@ +#ifndef DapRPCSERVICEREQUEST_H +#define DapRPCSERVICEREQUEST_H + +#include <QPointer> +#include <QMetaObject> +#include <QDebug> + +#include "DapRpcMessage.h" + +class DapRpcSocket; +class DapRpcServiceRequest +{ + DapRpcMessage m_request; + QPointer<DapRpcSocket> m_socket; + +public: + DapRpcServiceRequest(); + DapRpcServiceRequest(const DapRpcServiceRequest &aDapRpcServiceRequest); + DapRpcServiceRequest(const DapRpcMessage &aRequest, DapRpcSocket *apSocket); + DapRpcServiceRequest &operator=(const DapRpcServiceRequest &aDapRpcServiceRequest); + ~DapRpcServiceRequest(); + + bool isValid() const; + DapRpcMessage request() const; + DapRpcSocket *socket() const; + + bool respond(const DapRpcMessage &aResponse); + bool respond(QVariant aReturnValue); +}; + +#endif // DapRPCSERVICEREQUEST_H diff --git a/DapRPCProtocol/DapRpcSocket.cpp b/DapRPCProtocol/DapRpcSocket.cpp new file mode 100644 index 0000000..d769b41 --- /dev/null +++ b/DapRPCProtocol/DapRpcSocket.cpp @@ -0,0 +1,262 @@ +#include "DapRpcSocket.h" +#include "DapRpcService.h" + +DapRpcSocket::DapRpcSocket(QObject *apParent) + : QObject(apParent) +{ + +} + +DapRpcSocket::DapRpcSocket(QIODevice *apDevice, QObject *apParent) + : QObject(apParent) +{ + m_pDevice = apDevice; + connect(m_pDevice, SIGNAL(readyRead()), this, SLOT(processIncomingData())); +} + +DapRpcSocket::~DapRpcSocket() +{ +} + +int DapRpcSocket::findJsonDocumentEnd(const QByteArray &aJsonData) +{ + const char* pos = aJsonData.constData(); + const char* end = pos + aJsonData.length(); + + char blockStart = 0; + char blockEnd = 0; + int index = 0; + + while (true) { + if (pos == end) { + return -1; + } else if (*pos == '{') { + blockStart = '{'; + blockEnd = '}'; + break; + } else if(*pos == '[') { + blockStart = '['; + blockEnd = ']'; + break; + } + + pos++; + index++; + } + + pos++; + index++; + int depth = 1; + bool inString = false; + while (depth > 0 && pos <= end) { + if (*pos == '\\') { + pos += 2; + index += 2; + continue; + } else if (*pos == '"') { + inString = !inString; + } else if (!inString) { + if (*pos == blockStart) + depth++; + else if (*pos == blockEnd) + depth--; + } + + pos++; + index++; + } + + return depth == 0 ? index-1 : -1; +} + +void DapRpcSocket::writeData(const DapRpcMessage &asMessage) +{ + QJsonDocument doc = QJsonDocument(asMessage.toObject()); + QByteArray data = doc.toJson(QJsonDocument::Compact); + + m_pDevice.data()->write(data); + qJsonRpcDebug() << "sending: " << data; +} + +void DapRpcSocket::setDefaultRequestTimeout(int aiMsecs) +{ + if (aiMsecs < 0) { + qJsonRpcDebug() << "Cannot set a negative request timeout msecs value"; + return; + } + + m_defaultRequestTimeout = aiMsecs; +} + +int DapRpcSocket::getDefaultRequestTimeout() const +{ + return m_defaultRequestTimeout; +} + +bool DapRpcSocket::isValid() const +{ + return m_pDevice && m_pDevice.data()->isOpen(); +} + +DapRpcMessage DapRpcSocket::sendMessageBlocking(const DapRpcMessage &asMessage, int aMsecs) +{ + DapRpcServiceReply *reply = sendMessage(asMessage); + QScopedPointer<DapRpcServiceReply> replyPtr(reply); + + QEventLoop responseLoop; + connect(reply, SIGNAL(finished()), &responseLoop, SLOT(quit())); + QTimer::singleShot(aMsecs, &responseLoop, SLOT(quit())); + responseLoop.exec(); + + if (!reply->response().isValid()) { + m_replies.remove(asMessage.id()); + return asMessage.createErrorResponse(DapErrorCode::TimeoutError, "request timed out"); + } + + return reply->response(); +} + +DapRpcServiceReply *DapRpcSocket::sendMessage(const DapRpcMessage &asMessage) +{ + if (!m_pDevice) { + qJsonRpcDebug() << "trying to send message without device"; + return nullptr; + } + + notify(asMessage); + QPointer<DapRpcServiceReply> reply(new DapRpcServiceReply); + reply->setRequest(asMessage); + m_replies.insert(asMessage.id(), reply); + return reply; +} + +void DapRpcSocket::notify(const DapRpcMessage &asMessage) +{ + if (!m_pDevice) { + qJsonRpcDebug() << "trying to send message without device"; + return; + } + + DapRpcService *service = qobject_cast<DapRpcService*>(sender()); + if (service) + disconnect(service, SIGNAL(result(DapRpcMessage)), this, SLOT(notify(DapRpcMessage))); + + writeData(asMessage); +} + +DapRpcMessage DapRpcSocket::invokeRemoteMethodBlocking(const QString &asMethod, int aMsecs, const QVariant ¶m1, + const QVariant ¶m2, const QVariant ¶m3, + const QVariant ¶m4, const QVariant ¶m5, + const QVariant ¶m6, const QVariant ¶m7, + const QVariant ¶m8, const QVariant ¶m9, + const QVariant ¶m10) +{ + QVariantList params; + if (param1.isValid()) params.append(param1); + if (param2.isValid()) params.append(param2); + if (param3.isValid()) params.append(param3); + if (param4.isValid()) params.append(param4); + if (param5.isValid()) params.append(param5); + if (param6.isValid()) params.append(param6); + if (param7.isValid()) params.append(param7); + if (param8.isValid()) params.append(param8); + if (param9.isValid()) params.append(param9); + if (param10.isValid()) params.append(param10); + + DapRpcMessage request = + DapRpcMessage::createRequest(asMethod, QJsonArray::fromVariantList(params)); + return sendMessageBlocking(request, aMsecs); +} + +DapRpcMessage DapRpcSocket::invokeRemoteMethodBlocking(const QString &asMethod, const QVariant ¶m1, + const QVariant ¶m2, const QVariant ¶m3, + const QVariant ¶m4, const QVariant ¶m5, + const QVariant ¶m6, const QVariant ¶m7, + const QVariant ¶m8, const QVariant ¶m9, + const QVariant ¶m10) +{ + return invokeRemoteMethodBlocking(asMethod, m_defaultRequestTimeout, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10); +} + +DapRpcServiceReply *DapRpcSocket::invokeRemoteMethod(const QString &asMethod, const QVariant ¶m1, + const QVariant ¶m2, const QVariant ¶m3, + const QVariant ¶m4, const QVariant ¶m5, + const QVariant ¶m6, const QVariant ¶m7, + const QVariant ¶m8, const QVariant ¶m9, + const QVariant ¶m10) +{ + QVariantList params; + if (param1.isValid()) params.append(param1); + if (param2.isValid()) params.append(param2); + if (param3.isValid()) params.append(param3); + if (param4.isValid()) params.append(param4); + if (param5.isValid()) params.append(param5); + if (param6.isValid()) params.append(param6); + if (param7.isValid()) params.append(param7); + if (param8.isValid()) params.append(param8); + if (param9.isValid()) params.append(param9); + if (param10.isValid()) params.append(param10); + + DapRpcMessage request = + DapRpcMessage::createRequest(asMethod, QJsonArray::fromVariantList(params)); + return sendMessage(request); +} + +void DapRpcSocket::processIncomingData() +{ + if (!m_pDevice) { + qJsonRpcDebug() << "called without device"; + return; + } + + m_aBuffer.append(m_pDevice.data()->readAll()); + while (!m_aBuffer.isEmpty()) { + int dataSize = findJsonDocumentEnd(m_aBuffer); + if (dataSize == -1) { + return; + } + + QJsonParseError error; + QJsonDocument document = QJsonDocument::fromJson(m_aBuffer.mid(0, dataSize + 1), &error); + if (document.isEmpty()) { + if (error.error != QJsonParseError::NoError) { + qJsonRpcDebug() << error.errorString(); + } + + break; + } + + m_aBuffer = m_aBuffer.mid(dataSize + 1); + if (document.isArray()) { + qJsonRpcDebug() << "bulk support is current disabled"; + } else if (document.isObject()){ + qJsonRpcDebug() << "received: " << document.toJson(QJsonDocument::Compact); + DapRpcMessage message = DapRpcMessage::fromObject(document.object()); + Q_EMIT messageReceived(message); + + if (message.type() == DapRpcMessage::Response || + message.type() == DapRpcMessage::Error) { + if (m_replies.contains(message.id())) { + QPointer<DapRpcServiceReply> reply = m_replies.take(message.id()); + if (!reply.isNull()) { + reply->setResponse(message); + reply->finished(); + } + } + } else { + processRequestMessage(message); + } + } + } +} + +void DapRpcSocket::setIODevice(QIODevice *pDevice) +{ + m_pDevice = pDevice; + connect(m_pDevice, SIGNAL(readyRead()), this, SLOT(processIncomingData())); +} + +void DapRpcSocket::processRequestMessage(const DapRpcMessage &asMessage) +{ + Q_UNUSED(asMessage) +} diff --git a/DapRPCProtocol/DapRpcSocket.h b/DapRPCProtocol/DapRpcSocket.h new file mode 100644 index 0000000..9a2836d --- /dev/null +++ b/DapRPCProtocol/DapRpcSocket.h @@ -0,0 +1,74 @@ +#ifndef DapRPCSOCKET_H +#define DapRPCSOCKET_H + +#include <QObject> +#include <QIODevice> +#include <QPointer> +#include <QTimer> +#include <QEventLoop> +#include <QDebug> +#include <QJsonDocument> + +#include "DapRpcServiceProvider.h" +#include "DapRpcMessage.h" +#include "DapRpcServiceReply.h" + +#define DEFAULT_MSECS_REQUEST_TIMEOUT (30000) + +class DapRpcSocket : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(DapRpcSocket) + + QPointer<QIODevice> m_pDevice; + QByteArray m_aBuffer; + QHash<int, QPointer<DapRpcServiceReply>> m_replies; + int m_defaultRequestTimeout; + +protected: + virtual void processRequestMessage(const DapRpcMessage &asMessage); + +public: + explicit DapRpcSocket(QObject *apParent = nullptr); + explicit DapRpcSocket(QIODevice *apDevice, QObject *apParent = nullptr); + virtual ~DapRpcSocket(); + + virtual bool isValid() const; + void setDefaultRequestTimeout(int aiMsecs); + int getDefaultRequestTimeout() const; + + void setIODevice(QIODevice *pDevice); + +signals: + void messageReceived(const DapRpcMessage &asMessage); + +private slots: + virtual void processIncomingData(); + int findJsonDocumentEnd(const QByteArray &aJsonData); + void writeData(const DapRpcMessage &asMessage); + +public slots: + virtual void notify(const DapRpcMessage &asMessage); + virtual DapRpcMessage sendMessageBlocking(const DapRpcMessage &asMessage, int aMsecs = DEFAULT_MSECS_REQUEST_TIMEOUT); + virtual DapRpcServiceReply *sendMessage(const DapRpcMessage &asMessage); + DapRpcMessage invokeRemoteMethodBlocking(const QString &asMethod, int aMsecs, const QVariant &arg1 = QVariant(), + const QVariant &arg2 = QVariant(), const QVariant &arg3 = QVariant(), + const QVariant &arg4 = QVariant(), const QVariant &arg5 = QVariant(), + const QVariant &arg6 = QVariant(), const QVariant &arg7 = QVariant(), + const QVariant &arg8 = QVariant(), const QVariant &arg9 = QVariant(), + const QVariant &arg10 = QVariant()); + DapRpcMessage invokeRemoteMethodBlocking(const QString &asMethod, const QVariant &arg1 = QVariant(), + const QVariant &arg2 = QVariant(), const QVariant &arg3 = QVariant(), + const QVariant &arg4 = QVariant(), const QVariant &arg5 = QVariant(), + const QVariant &arg6 = QVariant(), const QVariant &arg7 = QVariant(), + const QVariant &arg8 = QVariant(), const QVariant &arg9 = QVariant(), + const QVariant &arg10 = QVariant()); + DapRpcServiceReply *invokeRemoteMethod(const QString &asMethod, const QVariant &arg1 = QVariant(), + const QVariant &arg2 = QVariant(), const QVariant &arg3 = QVariant(), + const QVariant &arg4 = QVariant(), const QVariant &arg5 = QVariant(), + const QVariant &arg6 = QVariant(), const QVariant &arg7 = QVariant(), + const QVariant &arg8 = QVariant(), const QVariant &arg9 = QVariant(), + const QVariant &arg10 = QVariant()); +}; + +#endif // DapRPCSOCKET_H diff --git a/DapRPCProtocol/DapRpcTCPServer.cpp b/DapRPCProtocol/DapRpcTCPServer.cpp new file mode 100644 index 0000000..3c361b1 --- /dev/null +++ b/DapRPCProtocol/DapRpcTCPServer.cpp @@ -0,0 +1,110 @@ +#include "DapRpcTCPServer.h" + +DapRpcTCPServer::DapRpcTCPServer(QObject *apParent) + : QTcpServer(apParent) +{ + +} + +void DapRpcTCPServer::notifyConnectedClients(const DapRpcMessage &message) +{ + DapRpcAbstractServer::notifyConnectedClients(message); +} + +void DapRpcTCPServer::notifyConnectedClients(const QString &method, const QJsonArray ¶ms) +{ + DapRpcAbstractServer::notifyConnectedClients(method, params); +} + +DapRpcTCPServer::~DapRpcTCPServer() +{ + foreach (QTcpSocket *socket, m_socketLookup.keys()) { + socket->flush(); + socket->deleteLater(); + } + m_socketLookup.clear(); + + foreach (DapRpcSocket *client, m_clients) + client->deleteLater(); + m_clients.clear(); +} + +bool DapRpcTCPServer::listen(const QString &asAddress, quint16 aPort) +{ + return ((asAddress.isNull() || asAddress.isEmpty()) ? + QTcpServer::listen(QHostAddress::Any, aPort) : + QTcpServer::listen(QHostAddress(asAddress), aPort)); +} + +bool DapRpcTCPServer::addService(DapRpcService *apService) +{ + if (!DapRpcServiceProvider::addService(apService)) + return false; + + connect(apService, SIGNAL(notifyConnectedClients(DapRpcMessage)), + this, SLOT(notifyConnectedClients(DapRpcMessage))); + connect(apService, SIGNAL(notifyConnectedClients(QString,QJsonArray)), + this, SLOT(notifyConnectedClients(QString,QJsonArray))); + return true; +} + +bool DapRpcTCPServer::removeService(DapRpcService *apService) +{ + if (!DapRpcServiceProvider::removeService(apService)) + return false; + + disconnect(apService, SIGNAL(notifyConnectedClients(DapRpcMessage)), + this, SLOT(notifyConnectedClients(DapRpcMessage))); + disconnect(apService, SIGNAL(notifyConnectedClients(QString,QJsonArray)), + this, SLOT(notifyConnectedClients(QString,QJsonArray))); + return true; +} + +void DapRpcTCPServer::clientDisconnected() +{ + QTcpSocket *tcpSocket = static_cast<QTcpSocket*>(sender()); + if (!tcpSocket) { + qJsonRpcDebug() << "called with invalid socket"; + return; + } + + if (m_socketLookup.contains(tcpSocket)) { + DapRpcSocket *socket = m_socketLookup.take(tcpSocket); + m_clients.removeAll(socket); + socket->deleteLater(); + } + + tcpSocket->deleteLater(); + emit onClientDisconnected(); +} + +void DapRpcTCPServer::messageProcessing(const DapRpcMessage &asMessage) +{ + DapRpcSocket *socket = static_cast<DapRpcSocket*>(sender()); + if (!socket) { + qJsonRpcDebug() << "called without service socket"; + return; + } + + processMessage(socket, asMessage); +} + +void DapRpcTCPServer::incomingConnection(qintptr aSocketDescriptor) +{ + QTcpSocket *tcpSocket = new QTcpSocket(this); + if (!tcpSocket->setSocketDescriptor(aSocketDescriptor)) + { + qJsonRpcDebug() << "can't set socket descriptor"; + tcpSocket->deleteLater(); + return; + } + + QIODevice *device = qobject_cast<QIODevice*>(tcpSocket); + DapRpcSocket *socket = new DapRpcSocket(device, this); + connect(socket, SIGNAL(messageReceived(DapRpcMessage)), + this, SLOT(_q_processMessage(DapRpcMessage))); + m_clients.append(socket); + connect(tcpSocket, SIGNAL(disconnected()), this, SLOT(_q_clientDisconnected())); + m_socketLookup.insert(tcpSocket, socket); + emit onClientConnected(); +} diff --git a/DapRPCProtocol/DapRpcTCPServer.h b/DapRPCProtocol/DapRpcTCPServer.h new file mode 100644 index 0000000..00b2e47 --- /dev/null +++ b/DapRPCProtocol/DapRpcTCPServer.h @@ -0,0 +1,44 @@ +#ifndef DapRPCTCPSERVER_H +#define DapRPCTCPSERVER_H + +#include <QObject> +#include <QTcpSocket> +#include <QTcpServer> + +#include "DapRpcSocket.h" +#include "DapRpcAbstractServer.h" + +class DapRpcTCPServer : public QTcpServer, public DapRpcAbstractServer +{ + Q_OBJECT + Q_DISABLE_COPY(DapRpcTCPServer) + + QHash<QTcpSocket*, DapRpcSocket*> m_socketLookup; + +protected: + virtual void incomingConnection(qintptr aSocketDescriptor); + +public: + explicit DapRpcTCPServer(QObject *apParent = nullptr); + virtual ~DapRpcTCPServer(); + + virtual bool listen(const QString &asAddress = QString(), quint16 aPort = 0); + + bool addService(DapRpcService *apService); + bool removeService(DapRpcService *apService); + +signals: + void onClientConnected(); + void onClientDisconnected(); + +protected slots: + void clientDisconnected(); + void messageProcessing(const DapRpcMessage &asMessage); + + // DapRpcAbstractServer interface +public slots: + void notifyConnectedClients(const DapRpcMessage &message); + void notifyConnectedClients(const QString &method, const QJsonArray ¶ms); +}; + +#endif // DapRPCTCPSERVER_H diff --git a/KelvinDashboardGUI/qml.qrc b/KelvinDashboardGUI/qml.qrc index 9bc4dd2..8d93e1b 100755 --- a/KelvinDashboardGUI/qml.qrc +++ b/KelvinDashboardGUI/qml.qrc @@ -12,8 +12,6 @@ <file>DapUiQmlWidgetChainWallet.ui.qml</file> <file>DapUiQmlScreenDialog.qml</file> <file>Resources/Icons/icon.png</file> - <file>Resources/Icons/icon.ico</file> - <file>Resources/Icons/add.png</file> <file>DapUiQmlScreenAbout.ui.qml</file> <file>DapQmlScreenAbout.qml</file> <file>Resources/Icons/iconErrorNetwork.png</file> @@ -22,5 +20,7 @@ <file>DapUiQmlWidgetDelegateForm.ui.qml</file> <file>DapUiQmlScreenChangeWidget.qml</file> <file>DapUiQmlWidgetChainNodeLogs.ui.qml</file> + <file>Resources/Icons/add.png</file> + <file>Resources/Icons/icon.ico</file> </qresource> </RCC> diff --git a/KelvinDashboardService/Resources/Icons/iconErrorNetwork.png b/KelvinDashboardService/Resources/Icons/iconErrorNetwork.png new file mode 100644 index 0000000000000000000000000000000000000000..57fa419adeeb2cd4c7de38cf11d9ca445b628f25 GIT binary patch literal 2548 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�FW`8%^4iW@2j=l-Q7|)o1K?!JDtvZ03fxrv^)ui!*7>> zhlhtrgwQwTpv}$AlG$wjj_3LDot+)BnrQcWy)RZ)R^FEd48we&{EuOnBnZMMa-kq! zR;O3sJv}}5S65e2Eve@7`Ce>oZOtn*q|Ijg=?bsrWl>%Q{ymjS4XJ6tU@#O8hd&Jj z0zY<kcK)h1$g-@I%jMoHx6{79zM1y+cJ=j^1ZN%qq=A8fN6P#y7K=oGfB!r}=y9ch z^ZWhJH#RojQC#cYbUGbZ?ZZve^l#<h002PK^z8orJ}!TckB`5p)9HS`F4_@7|LpGW zzALYb#bR0cA5GH_Baw((!7u^9#I@;2QPhVBp=(o(VVLKeo14?}y0NjbCAG%kaQw;h ze7u$-l&&VveLmmc>+9=xlmX}S`M$<5?3Glu+wBrUh_0m=<}1M|ih8!ay?v+LLXXFD zi(#0(N&$DHO(2Axt*xzH-eV*Zp^uM`_3Fi<C?*JkC}@Stos9ZndAhi`cw5#?PELOO zvg}^?{r+!fXJ;cy8;L~X)_>Vd{I$Hid<oBHv)ko7)Z5$p#opeYQFA9@u~;}Y+~IIs zWHK31)+{V6d{!Yw3Ovt;G{K!tCs*#|qobp5$(md)_jx6_Uaya8f*Xy-s7|L-r%)l4 zN?pEQBO@cdSHTH_C?JG5P4GY<AP@vmR2sBetwm83V`}Zt(9lQaO7Zmc^oS-n$8j;W z+f61@k)o(8PctzwkymJ?si~>En&2$U3MP}OsCIiO6cVcF`SSoE84L#PZFXB*Tgh9E z<9MN~tBbEOUA2#fh6d7LFdWJ78ynOzY&IKdG#VdRtyZqy?0*5j=){>wpTMX90000< KMNUMnLSTaD&fT~G literal 0 HcmV?d00001 diff --git a/KelvinDashboardService/Resources/Icons/iconNetwork.png b/KelvinDashboardService/Resources/Icons/iconNetwork.png new file mode 100644 index 0000000000000000000000000000000000000000..aa2d0ac25a1a88aeebe52eb8cb9dada732a46287 GIT binary patch literal 2300 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 W0d1+f8X+?P0000<MNUMnLSTYk>N@HG literal 0 HcmV?d00001 -- GitLab