diff --git a/.github/workflows/build-qi-qmp-linux.yml b/.github/workflows/build-qi-qmp-linux.yml index 1fff518..6be4d8a 100644 --- a/.github/workflows/build-qi-qmp-linux.yml +++ b/.github/workflows/build-qi-qmp-linux.yml @@ -18,14 +18,9 @@ jobs: fail-fast: false matrix: os: [ubuntu-20.04, ubuntu-22.04] - compiler: [gcc, clang] + compiler: [clang] lib_linkage: [shared, static] include: - - os: ubuntu-20.04 - compiler: gcc - c_comp: gcc-10 - cxx_comp: g++-10 - qt_comp: clang12 - os: ubuntu-20.04 compiler: clang c_comp: clang-12 @@ -36,6 +31,13 @@ jobs: c_comp: gcc-12 cxx_comp: g++-12 qt_comp: clang14 + lib_linkage: shared + - os: ubuntu-22.04 + compiler: gcc + c_comp: gcc-12 + cxx_comp: g++-12 + qt_comp: clang14 + lib_linkage: static - os: ubuntu-22.04 compiler: clang c_comp: clang-14 diff --git a/.github/workflows/master-pull-request-merge-reaction.yml b/.github/workflows/master-pull-request-merge-reaction.yml index 54f5340..c68aa27 100644 --- a/.github/workflows/master-pull-request-merge-reaction.yml +++ b/.github/workflows/master-pull-request-merge-reaction.yml @@ -97,7 +97,7 @@ jobs: - name: Zip up release artifacts shell: pwsh run: | - $artifact_folders = Get-ChildItem -Directory -Path "${{ env.artifacts_path }}" + $artifact_folders = Get-ChildItem -Directory -Path "${{ env.artifacts_path }}" -Exclude "github-pages" foreach($art_dir in $artifact_folders) { $name = $art_dir.name diff --git a/CMakeLists.txt b/CMakeLists.txt index 8073684..1f4c451 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,14 +6,14 @@ cmake_minimum_required(VERSION 3.23.0...3.25.0) # Project # NOTE: DON'T USE TRAILING ZEROS IN VERSIONS project(QI-QMP - VERSION 0.2.1 + VERSION 0.2.2 LANGUAGES CXX DESCRIPTION "Qt-based Interface for QEMU Machine Protocol" ) # Get helper scripts include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchOBCMake.cmake) -fetch_ob_cmake("v0.3") +fetch_ob_cmake("v0.3.2") # Initialize project according to standard rules include(OB/Project) @@ -53,7 +53,7 @@ set(QI_QMP_QX_COMPONENTS include(OB/FetchQx) ob_fetch_qx( - REF "v0.5" + REF "v0.5.1" COMPONENTS ${QI_QMP_QX_COMPONENTS} ) diff --git a/doc/cmake/file_templates/mainpage.md.in b/doc/cmake/file_templates/mainpage.md.in index 91cb6ee..ffed3c1 100644 --- a/doc/cmake/file_templates/mainpage.md.in +++ b/doc/cmake/file_templates/mainpage.md.in @@ -1,5 +1,5 @@ QI-QMP {#mainpage} -============== +================== QI-QMP is a minuscule C++ library, which utilizes Qt, that provides an interface to QEMU instances via the [QEMU Machine Protocol](https://wiki.qemu.org/Documentation/QMP). In other words, this library implements the client side of QMP for C++. The interface takes the form of the library's sole class, `Qmpi`. @@ -22,7 +22,7 @@ Packaging ---------- QI-QMP is provided as a CMake package composed of a single library and accompanying public header file. -#### Package Components: +### Package Components: - `Qmpi` - The main library @@ -95,6 +95,9 @@ QObject::connect(&iQemu, &Qmpi::responseReceived, [](QJsonValue data, std::any c QObject::connect(&iQemu, &Qmpi::errorResponseReceived, [](QString errorClass, QString desc, std::any context){ qDebug() << "Command " << std::any_cast(context) << " threw the error: [" << errorClass << "] " << desc; }); + +iQemu.connectToHost(); +... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Building From Source @@ -107,19 +110,19 @@ If newer to working with Qt, it is easiest to build from within Qt creator as it The CMake project is designed to be used with multi-configuration generators such as Visual Studio or Ninja Multi-Config (recommended), and may require some tweaking to work with single configuration generators. -#### CMake Options: +### CMake Options: - `QI_QMP_DOCS` - Set to `ON` in order to generate the documentation target (OFF) - `BUILD_SHARED_LIBS` - Build QI-QMP as a shared library instead of a static one (OFF) -#### CMake Targets: +### CMake Targets: - `all` - Effectively an alias for the `qi_qmp_qmpi` target, also builds documentation if enabled - `install` - Installs the build output into `CMAKE_INSTALL_PREFIX` - `qi_qmp_docs` - Builds the QI-QMP documentation - `qi_qmp_qmpi` - Builds the project's sole library -#### CMake Install Components: +### CMake Install Components: - `qi_qmp` - Installs top-level files (README.md, CMake package configuration files, etc.) - `qi_qmp_docs` - Installs documentation @@ -127,7 +130,7 @@ The CMake project is designed to be used with multi-configuration generators suc If QI-QMP is configured as a sub-project, its install components are automatically removed from the default install component, as to not pollute the install directory of the top-level project. They can still be installed by directly referencing their component names as shown above. -#### Documentation: +### Documentation: In order for the `qi_qmp_docs` target to be generated the CMake cache variable **QI_QMP_DOCS** must be set to *ON* when CMake is invoked: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ cmake.exe (...) -D QI_QMP_DOCS=ON @@ -174,7 +177,7 @@ The path for this documentation varies depending on how you obtained Qt, but is # By default on Windows is C:\Program Files\Qt # On Linux it is often /usr/local/Qt -#### Package +### Package By default, the CMakeLists project configures CPack to create an artifact ZIP containing the binaries/archives for the library configurations, as well as documentation. The following is the general build process required to successfully generate this package via a shadow build on Windows. Adjust the configuration as you see fit:: diff --git a/lib/include/qi-qmp/qmpi.h b/lib/include/qi-qmp/qmpi.h index adf21fd..d7b4b4c 100644 --- a/lib/include/qi-qmp/qmpi.h +++ b/lib/include/qi-qmp/qmpi.h @@ -33,53 +33,6 @@ class QI_QMP_QMPI_EXPORT Qmpi : public QObject std::any context; }; -//-Class Variables----------------------------------------------------------------------------------------------------------- -private: - /*! @cond */ - class JsonKeys - { - public: - static inline const QString GREETING = "QMP"; - class Greeting - { - public: - static inline const QString VERSION = "version"; - static inline const QString CAPABILITIES = "capabilities"; - }; - - static inline const QString RETURN = "return"; - static inline const QString ERROR = "error"; - class Error - { - public: - static inline const QString CLASS = "class"; - static inline const QString DESCRIPTION = "desc"; - }; - - static inline const QString EVENT = "event"; - class Event - { - public: - static inline const QString DATA = "data"; - static inline const QString TIMESTAMP = "timestamp"; - class Timestamp - { - public: - static inline const QString SECONDS = "seconds"; - static inline const QString MICROSECONDS = "microseconds"; - }; - }; - - static inline const QString EXECUTE = "execute"; - class Execute - { - public: - static inline const QString ARGUMENTS = "arguments"; - }; - }; - /*! @endcond */ - static inline const QString NEGOTIATION_COMMAND = "qmp_capabilities"; - //-Instance Variables-------------------------------------------------------------------------------------------------------- private: // Network @@ -122,11 +75,11 @@ class QI_QMP_QMPI_EXPORT Qmpi : public QObject void propagate(); // Message Processing - void processServerMessage(const QJsonObject& msg); - bool processGreetingMessage(const QJsonObject& greeting); - bool processReturnMessage(const QJsonObject& ret); - bool processErrorMessage(const QJsonObject& error); - bool processEventMessage(const QJsonObject& event); + void processServerMessage(const QJsonObject& jMsg); + bool processGreetingMessage(const QJsonObject& jGreeting); + bool processSuccessMessage(const QJsonObject& jSuccess); + bool processErrorMessage(const QJsonObject& jError); + bool processEventMessage(const QJsonObject& jEvent); public: // Info @@ -170,6 +123,7 @@ private slots: void communicationErrorOccurred(Qmpi::CommunicationError error); // Will disconnect after void errorResponseReceived(QString errorClass, QString description, std::any context); // Will not disconnect after void stateChanged(Qmpi::State state); + void commandQueueExhausted(); }; #endif // QMPI_H diff --git a/lib/src/qmpi.cpp b/lib/src/qmpi.cpp index 05813e5..c8cf7ec 100644 --- a/lib/src/qmpi.cpp +++ b/lib/src/qmpi.cpp @@ -3,7 +3,80 @@ // Qx Includes #include -#include + +/*! @cond */ +namespace Json +{ + +struct GreetingBody +{ + QJsonObject version; + QJsonArray capabilities; + + QX_JSON_STRUCT(version, capabilities); +}; + +struct Greeting +{ + GreetingBody QMP; + + QX_JSON_STRUCT(QMP); +}; + +struct SuccessResponse +{ + static inline const QString IDENTIFIER_KEY =u"return"_s; +}; + +struct ErrorBody +{ + QString eClass; + QString desc; + + QX_JSON_STRUCT_X( + QX_JSON_MEMBER_ALIASED(eClass, "class"), + QX_JSON_MEMBER(desc) + ); +}; + +struct ErrorResponse +{ + static inline const QString IDENTIFIER_KEY =u"error"_s; + + ErrorBody error; + + QX_JSON_STRUCT(error); +}; + +struct Timestamp +{ + double seconds; + double microseconds; + + QX_JSON_STRUCT(seconds, microseconds); +}; + +struct AsyncEvent +{ + static inline const QString IDENTIFIER_KEY =u"event"_s; + + QString event; + QJsonObject data; + Timestamp timestamp; + + QX_JSON_STRUCT(event, data, timestamp); +}; + +// TODO: Use QX_JSON for serializing these after the serialization portion of QX_JSON is implemented +struct Execute +{ + static inline const QString IDENTIFIER_KEY =u"execute"_s; + static inline const QString ARGUMENTS =u"arguments"_s; +}; + +static inline const QString NEGOTIATION_COMMAND =u"qmp_capabilities"_s; +} +/*! @endcond */ //=============================================================================================================== // Qmpi @@ -122,8 +195,8 @@ */ Qmpi::Qmpi(QObject* parent) : QObject(parent), - mPort(4444), mHostId(QHostAddress::LocalHost), + mPort(4444), mSocket(this), mState(Disconnected) { @@ -238,7 +311,7 @@ void Qmpi::finish() void Qmpi::negotiateCapabilities() { changeState(State::Negotiating); - if(!sendCommand(NEGOTIATION_COMMAND)) + if(!sendCommand(Json::NEGOTIATION_COMMAND)) return; startTransactionTimer(); } @@ -247,9 +320,9 @@ bool Qmpi::sendCommand(QString command, QJsonObject args) { // Build JSON object QJsonObject commandObject; - commandObject[JsonKeys::EXECUTE] = command; + commandObject[Json::Execute::IDENTIFIER_KEY] = command; if(!args.isEmpty()) - commandObject[JsonKeys::Execute::ARGUMENTS] = args; + commandObject[Json::Execute::ARGUMENTS] = args; // Convert to raw message QJsonDocument execution(commandObject); @@ -264,7 +337,7 @@ bool Qmpi::sendCommand(QString command, QJsonObject args) // Check for error if(bytesWritten == -1) { - raiseCommunicationError(CommunicationError::ReadFailed); + raiseCommunicationError(CommunicationError::WriteFailed); return false; } @@ -293,25 +366,21 @@ void Qmpi::propagate() startTransactionTimer(); } else + { + emit commandQueueExhausted(); changeState(State::Idle); + } } -void Qmpi::processServerMessage(const QJsonObject& msg) +void Qmpi::processServerMessage(const QJsonObject& jMsg) { // Stop transaction timer since message has arrived bool timerWasRunning = stopTransactionTimer(); if(mState == State::AwaitingWelcome) { - // Get and parse the greeting - QJsonObject greeting; - if(Qx::Json::checkedKeyRetrieval(greeting, msg, JsonKeys::GREETING).isValid()) - { - raiseCommunicationError(CommunicationError::UnexpectedResponse); - return; - } - - if(!processGreetingMessage(greeting)) + // Parse the greeting + if(!processGreetingMessage(jMsg)) return; // Automatically proceed to enabling commands since this currently doesn't support extra capabilities @@ -320,7 +389,7 @@ void Qmpi::processServerMessage(const QJsonObject& msg) else if(mState == State::Negotiating) { // Just ensure an error didn't happen, value doesn't matter (should be empty) - if(!msg.contains(JsonKeys::RETURN)) + if(!jMsg.contains(Json::SuccessResponse::IDENTIFIER_KEY)) { raiseCommunicationError(CommunicationError::UnexpectedResponse); return; @@ -334,9 +403,9 @@ void Qmpi::processServerMessage(const QJsonObject& msg) { changeState(State::ReadingMessage); - if(msg.contains(JsonKeys::EVENT)) + if(jMsg.contains(Json::AsyncEvent::IDENTIFIER_KEY)) { - if(!processEventMessage(msg)) + if(!processEventMessage(jMsg)) return; // Restart the transaction timer if it was running since events are just informative @@ -354,25 +423,17 @@ void Qmpi::processServerMessage(const QJsonObject& msg) } // Check for each response type - if(msg.contains(JsonKeys::RETURN)) + if(jMsg.contains(Json::SuccessResponse::IDENTIFIER_KEY)) { - if(!processReturnMessage(msg)) + if(!processSuccessMessage(jMsg)) return; // Send next command propagate(); } - else if(msg.contains(JsonKeys::ERROR)) + else if(jMsg.contains(Json::ErrorResponse::IDENTIFIER_KEY)) { - // Get and parse the error - QJsonObject error; - if(Qx::Json::checkedKeyRetrieval(error, msg, JsonKeys::ERROR).isValid()) - { - raiseCommunicationError(CommunicationError::UnexpectedResponse); - return; - } - - if(!processErrorMessage(error)) + if(!processErrorMessage(jMsg)) return; // Send next command @@ -387,30 +448,27 @@ void Qmpi::processServerMessage(const QJsonObject& msg) } } -bool Qmpi::processGreetingMessage(const QJsonObject& greeting) +bool Qmpi::processGreetingMessage(const QJsonObject& jGreeting) { - QJsonObject version; - QJsonArray capabilities; - if(Qx::Json::checkedKeyRetrieval(version, greeting, JsonKeys::Greeting::VERSION).isValid()) - { - raiseCommunicationError(CommunicationError::UnexpectedResponse); - return false; - } - if(Qx::Json::checkedKeyRetrieval(capabilities, greeting, JsonKeys::Greeting::CAPABILITIES).isValid()) + Json::Greeting greeting; + if(Qx::parseJson(greeting, jGreeting).isValid()) { raiseCommunicationError(CommunicationError::UnexpectedResponse); return false; } - emit connected(version, capabilities); + Json::GreetingBody gb = greeting.QMP; + + emit connected(gb.version, gb.capabilities); return true; } -bool Qmpi::processReturnMessage(const QJsonObject& ret) +bool Qmpi::processSuccessMessage(const QJsonObject& jSuccess) { Q_ASSERT(!mExecutionQueue.empty()); - QJsonValue value = ret[JsonKeys::RETURN]; + // Get value + QJsonValue value = jSuccess[Json::SuccessResponse::IDENTIFIER_KEY]; std::any context = mExecutionQueue.front().context; mExecutionQueue.pop(); emit responseReceived(value, context); @@ -418,65 +476,49 @@ bool Qmpi::processReturnMessage(const QJsonObject& ret) return true; // Can't fail as of yet } -bool Qmpi::processErrorMessage(const QJsonObject& error) +bool Qmpi::processErrorMessage(const QJsonObject& jError) { Q_ASSERT(!mExecutionQueue.empty()); std::any context = mExecutionQueue.front().context; mExecutionQueue.pop(); - QString errorClass; - QString description; - if(Qx::Json::checkedKeyRetrieval(errorClass, error, JsonKeys::Error::CLASS).isValid()) - { - raiseCommunicationError(CommunicationError::UnexpectedResponse); - return false; - } - if(Qx::Json::checkedKeyRetrieval(description, error, JsonKeys::Error::DESCRIPTION).isValid()) + Json::ErrorResponse er; + if(Qx::parseJson(er, jError).isValid()) { raiseCommunicationError(CommunicationError::UnexpectedResponse); return false; } - emit errorResponseReceived(errorClass, description, context); + Json::ErrorBody eb = er.error; + + emit errorResponseReceived(eb.eClass, eb.desc, context); return true; } -bool Qmpi::processEventMessage(const QJsonObject& event) +bool Qmpi::processEventMessage(const QJsonObject& jEvent) { - QString eventName; - QJsonObject data; - QJsonObject timestamp; - double seconds; - double microseconds; - - // Get all the values (check for error after since there's a decent number) - Qx::SetOnce jsonError(false); - - jsonError = Qx::Json::checkedKeyRetrieval(eventName, event, JsonKeys::EVENT).isValid(); - jsonError = Qx::Json::checkedKeyRetrieval(data, event, JsonKeys::Event::DATA).isValid(); - jsonError = Qx::Json::checkedKeyRetrieval(seconds, timestamp, JsonKeys::Event::Timestamp::SECONDS).isValid(); - jsonError = Qx::Json::checkedKeyRetrieval(microseconds, timestamp, JsonKeys::Event::Timestamp::MICROSECONDS).isValid(); - - // Check if there was an error at any point - if(jsonError.value()) + Json::AsyncEvent ae; + if(Qx::parseJson(ae, jEvent).isValid()) { raiseCommunicationError(CommunicationError::UnexpectedResponse); return false; } // Convert native timestamp to QDateTime + Json::Timestamp ts = ae.timestamp; + qint64 milliseconds = -1; - if(seconds != -1) + if(ts.seconds != -1) { - milliseconds = std::round(seconds) * 1000; - if(microseconds != -1) - milliseconds += std::round(microseconds / 1000); + milliseconds = std::round(ts.seconds) * 1000; + if(ts.microseconds != -1) + milliseconds += std::round(ts.microseconds / 1000); } QDateTime standardTimestamp = (milliseconds != -1) ? QDateTime::fromMSecsSinceEpoch(milliseconds) : QDateTime(); // Notify - emit eventReceived(eventName, data, standardTimestamp); + emit eventReceived(ae.event, ae.data, standardTimestamp); // Return success return true; @@ -893,3 +935,12 @@ void Qmpi::handleTransactionTimeout() { raiseCommunicationError(CommunicationErr * * @sa connected(), disconnected(), readyForCommands() and finished(). */ + +/*! + * @fn void Qmpi::commandQueueExhausted() + * + * This signal is emitted when the interface's command queue becomes empty and the response to the last command + * in the queue has been received, just before it enters State::Idle. + * + * @sa stateChanged(), and execute(). + */