From 10910aa06ddb2d872ce8274eda72aabab5cf4cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lepin?= Date: Tue, 2 Feb 2021 11:26:22 +0100 Subject: [PATCH 1/7] requests(general): ExecuteBatch WIP --- src/WSRequestHandler.cpp | 4 +- src/WSRequestHandler.h | 2 + src/WSRequestHandler_General.cpp | 45 +++++++++++++++++++ src/WSServer.cpp | 6 +-- src/protocol/OBSRemoteProtocol.cpp | 69 +++++++++++++++++++----------- src/protocol/OBSRemoteProtocol.h | 15 ++++--- src/rpc/RpcResponse.h | 2 +- 7 files changed, 105 insertions(+), 38 deletions(-) diff --git a/src/WSRequestHandler.cpp b/src/WSRequestHandler.cpp index 5c502727..67036179 100644 --- a/src/WSRequestHandler.cpp +++ b/src/WSRequestHandler.cpp @@ -46,6 +46,8 @@ const QHash WSRequestHandler::messageMap{ { "TriggerHotkeyByName", &WSRequestHandler::TriggerHotkeyByName }, { "TriggerHotkeyBySequence", &WSRequestHandler::TriggerHotkeyBySequence }, + { "ExecuteBatch", &WSRequestHandler::ExecuteBatch }, + { "SetCurrentScene", &WSRequestHandler::SetCurrentScene }, { "GetCurrentScene", &WSRequestHandler::GetCurrentScene }, { "GetSceneList", &WSRequestHandler::GetSceneList }, @@ -189,7 +191,7 @@ WSRequestHandler::WSRequestHandler(ConnectionProperties& connProperties) : { } -RpcResponse WSRequestHandler::processRequest(const RpcRequest& request){ +RpcResponse WSRequestHandler::processRequest(const RpcRequest& request) { if (GetConfig()->AuthRequired && (!authNotRequired.contains(request.methodName())) && (!_connProperties.isAuthenticated())) diff --git a/src/WSRequestHandler.h b/src/WSRequestHandler.h index 16a64742..6ac744a4 100644 --- a/src/WSRequestHandler.h +++ b/src/WSRequestHandler.h @@ -64,6 +64,8 @@ class WSRequestHandler { RpcResponse TriggerHotkeyByName(const RpcRequest&); RpcResponse TriggerHotkeyBySequence(const RpcRequest&); + RpcResponse ExecuteBatch(const RpcRequest&); + RpcResponse SetCurrentScene(const RpcRequest&); RpcResponse GetCurrentScene(const RpcRequest&); RpcResponse GetSceneList(const RpcRequest&); diff --git a/src/WSRequestHandler_General.cpp b/src/WSRequestHandler_General.cpp index d42513bc..fdef74e4 100644 --- a/src/WSRequestHandler_General.cpp +++ b/src/WSRequestHandler_General.cpp @@ -7,6 +7,7 @@ #include "Config.h" #include "Utils.h" #include "WSEvents.h" +#include "protocol/OBSRemoteProtocol.h" #define CASE(x) case x: return #x; const char *describe_output_format(int format) { @@ -415,3 +416,47 @@ RpcResponse WSRequestHandler::TriggerHotkeyBySequence(const RpcRequest& request) return request.success(); } + +/** +* Executes a list of requests sequentially. +* +* @param {Array} `requests` +* +* @return {Array} `results` +* +* @api requests +* @name ExecuteBatch +* @category general +* @since unreleased +*/ +RpcResponse WSRequestHandler::ExecuteBatch(const RpcRequest& request) { + if (!request.hasField("requests")) { + return request.failed("missing request parameters"); + } + + OBSDataArrayAutoRelease results = obs_data_array_create(); + + OBSDataArrayAutoRelease requests = obs_data_get_array(request.parameters(), "requests"); + size_t requestsCount = obs_data_array_count(requests); + for (size_t i = 0; i < requestsCount; i++) { + OBSDataAutoRelease requestData = obs_data_array_item(requests, i); + QString methodName = obs_data_get_string(requestData, "request-type"); + obs_data_unset_user_value(requestData, "request-type"); + obs_data_unset_user_value(requestData, "message-id"); + + // build RpcRequest from json data object + RpcRequest subRequest(QString::Null(), methodName, requestData); + + // execute the request + RpcResponse subResponse = processRequest(subRequest); + + // transform response into json data + OBSDataAutoRelease subResponseData = OBSRemoteProtocol::rpcResponseToJsonData(subResponse); + + obs_data_array_push_back(results, subResponseData); + } + + OBSDataAutoRelease response = obs_data_create(); + obs_data_set_array(response, "results", results); + return request.success(response); +} diff --git a/src/WSServer.cpp b/src/WSServer.cpp index 6d155658..8f1ec8bf 100644 --- a/src/WSServer.cpp +++ b/src/WSServer.cpp @@ -133,8 +133,7 @@ void WSServer::stop() void WSServer::broadcast(const RpcEvent& event) { - OBSRemoteProtocol protocol; - std::string message = protocol.encodeEvent(event); + std::string message = OBSRemoteProtocol::encodeEvent(event); if (GetConfig()->DebugEnabled) { blog(LOG_INFO, "Update << '%s'", message.c_str()); @@ -190,8 +189,7 @@ void WSServer::onMessage(connection_hdl hdl, server::message_ptr message) } WSRequestHandler requestHandler(connProperties); - OBSRemoteProtocol protocol; - std::string response = protocol.processMessage(requestHandler, payload); + std::string response = OBSRemoteProtocol::processMessage(requestHandler, payload); if (GetConfig()->DebugEnabled) { blog(LOG_INFO, "Response << '%s'", response.c_str()); diff --git a/src/protocol/OBSRemoteProtocol.cpp b/src/protocol/OBSRemoteProtocol.cpp index a7555b6e..e02be58d 100644 --- a/src/protocol/OBSRemoteProtocol.cpp +++ b/src/protocol/OBSRemoteProtocol.cpp @@ -31,11 +31,15 @@ std::string OBSRemoteProtocol::processMessage(WSRequestHandler& requestHandler, OBSDataAutoRelease data = obs_data_create_from_json(msg); if (!data) { blog(LOG_ERROR, "invalid JSON payload received for '%s'", msg); - return errorResponse(QString::Null(), "invalid JSON payload"); + return jsonDataToString( + errorResponse(nullptr, "invalid JSON payload") + ); } if (!obs_data_has_user_value(data, "request-type") || !obs_data_has_user_value(data, "message-id")) { - return errorResponse(QString::Null(), "missing request parameters"); + return jsonDataToString( + errorResponse(nullptr, "missing request parameters") + ); } QString methodName = obs_data_get_string(data, "request-type"); @@ -49,15 +53,8 @@ std::string OBSRemoteProtocol::processMessage(WSRequestHandler& requestHandler, RpcRequest request(messageId, methodName, params); RpcResponse response = requestHandler.processRequest(request); - OBSData additionalFields = response.additionalFields(); - switch (response.status()) { - case RpcResponse::Status::Ok: - return successResponse(messageId, additionalFields); - case RpcResponse::Status::Error: - return errorResponse(messageId, response.errorMessage(), additionalFields); - } - - return std::string(); + OBSData responseData = rpcResponseToJsonData(response); + return jsonDataToString(responseData); } std::string OBSRemoteProtocol::encodeEvent(const RpcEvent& event) @@ -87,33 +84,53 @@ std::string OBSRemoteProtocol::encodeEvent(const RpcEvent& event) return std::string(obs_data_get_json(eventData)); } -std::string OBSRemoteProtocol::buildResponse(QString messageId, QString status, obs_data_t* fields) +obs_data_t* OBSRemoteProtocol::rpcResponseToJsonData(const RpcResponse& response) { - OBSDataAutoRelease response = obs_data_create(); - if (!messageId.isNull()) { - obs_data_set_string(response, "message-id", messageId.toUtf8().constData()); + const char* messageId = response.messageId().toUtf8().constData(); + OBSData additionalFields = response.additionalFields(); + switch (response.status()) { + case RpcResponse::Status::Ok: + return successResponse(messageId, additionalFields); + case RpcResponse::Status::Error: + return errorResponse(messageId, response.errorMessage().toUtf8().constData(), additionalFields); + default: + assert(false); } - obs_data_set_string(response, "status", status.toUtf8().constData()); - - if (fields) { - obs_data_apply(response, fields); - } - - std::string responseString = obs_data_get_json(response); - return responseString; } -std::string OBSRemoteProtocol::successResponse(QString messageId, obs_data_t* fields) +obs_data_t* OBSRemoteProtocol::successResponse(const char* messageId, obs_data_t* fields) { return buildResponse(messageId, "ok", fields); } -std::string OBSRemoteProtocol::errorResponse(QString messageId, QString errorMessage, obs_data_t* additionalFields) +obs_data_t* OBSRemoteProtocol::errorResponse(const char* messageId, const char* errorMessage, obs_data_t* additionalFields) { OBSDataAutoRelease fields = obs_data_create(); if (additionalFields) { obs_data_apply(fields, additionalFields); } - obs_data_set_string(fields, "error", errorMessage.toUtf8().constData()); + obs_data_set_string(fields, "error", errorMessage); return buildResponse(messageId, "error", fields); } + +obs_data_t* OBSRemoteProtocol::buildResponse(const char* messageId, const char* status, obs_data_t* fields) +{ + OBSDataAutoRelease response = obs_data_create(); + if (messageId) { + obs_data_set_string(response, "message-id", messageId); + } + obs_data_set_string(response, "status", status); + + if (fields) { + obs_data_apply(response, fields); + } + + obs_data_addref(response); + return response; +} + +std::string OBSRemoteProtocol::jsonDataToString(obs_data_t* data) +{ + std::string responseString = obs_data_get_json(data); + return responseString; +} diff --git a/src/protocol/OBSRemoteProtocol.h b/src/protocol/OBSRemoteProtocol.h index 03d8aa7d..f518a002 100644 --- a/src/protocol/OBSRemoteProtocol.h +++ b/src/protocol/OBSRemoteProtocol.h @@ -20,7 +20,8 @@ with this program. If not, see #include #include -#include + +#include "../rpc/RpcResponse.h" class WSRequestHandler; class RpcEvent; @@ -28,11 +29,13 @@ class RpcEvent; class OBSRemoteProtocol { public: - std::string processMessage(WSRequestHandler& requestHandler, std::string message); - std::string encodeEvent(const RpcEvent& event); + static std::string processMessage(WSRequestHandler& requestHandler, std::string message); + static std::string encodeEvent(const RpcEvent& event); + static obs_data_t* rpcResponseToJsonData(const RpcResponse& response); private: - std::string buildResponse(QString messageId, QString status, obs_data_t* fields = nullptr); - std::string successResponse(QString messageId, obs_data_t* fields = nullptr); - std::string errorResponse(QString messageId, QString errorMessage, obs_data_t* additionalFields = nullptr); + static obs_data_t* successResponse(const char* messageId, obs_data_t* fields = nullptr); + static obs_data_t* errorResponse(const char* messageId, const char* errorMessage, obs_data_t* additionalFields = nullptr); + static obs_data_t* buildResponse(const char* messageId, const char*, obs_data_t* fields = nullptr); + static std::string jsonDataToString(obs_data_t* data); }; diff --git a/src/rpc/RpcResponse.h b/src/rpc/RpcResponse.h index a6381bfd..d0c96d6e 100644 --- a/src/rpc/RpcResponse.h +++ b/src/rpc/RpcResponse.h @@ -36,7 +36,7 @@ public: obs_data_t* additionalFields = nullptr ); - Status status() { + const Status status() const { return _status; } From 87cd36673ed8fa59f2a6f47decc4279996d45f40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lepin?= Date: Wed, 3 Feb 2021 04:16:04 +0100 Subject: [PATCH 2/7] OBSRemoteProtocol: fix memory leak --- src/protocol/OBSRemoteProtocol.cpp | 7 +++---- src/protocol/OBSRemoteProtocol.h | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/protocol/OBSRemoteProtocol.cpp b/src/protocol/OBSRemoteProtocol.cpp index e02be58d..87693add 100644 --- a/src/protocol/OBSRemoteProtocol.cpp +++ b/src/protocol/OBSRemoteProtocol.cpp @@ -53,7 +53,7 @@ std::string OBSRemoteProtocol::processMessage(WSRequestHandler& requestHandler, RpcRequest request(messageId, methodName, params); RpcResponse response = requestHandler.processRequest(request); - OBSData responseData = rpcResponseToJsonData(response); + OBSDataAutoRelease responseData = rpcResponseToJsonData(response); return jsonDataToString(responseData); } @@ -115,7 +115,7 @@ obs_data_t* OBSRemoteProtocol::errorResponse(const char* messageId, const char* obs_data_t* OBSRemoteProtocol::buildResponse(const char* messageId, const char* status, obs_data_t* fields) { - OBSDataAutoRelease response = obs_data_create(); + obs_data_t* response = obs_data_create(); if (messageId) { obs_data_set_string(response, "message-id", messageId); } @@ -125,11 +125,10 @@ obs_data_t* OBSRemoteProtocol::buildResponse(const char* messageId, const char* obs_data_apply(response, fields); } - obs_data_addref(response); return response; } -std::string OBSRemoteProtocol::jsonDataToString(obs_data_t* data) +std::string OBSRemoteProtocol::jsonDataToString(OBSDataAutoRelease data) { std::string responseString = obs_data_get_json(data); return responseString; diff --git a/src/protocol/OBSRemoteProtocol.h b/src/protocol/OBSRemoteProtocol.h index f518a002..354ca514 100644 --- a/src/protocol/OBSRemoteProtocol.h +++ b/src/protocol/OBSRemoteProtocol.h @@ -37,5 +37,5 @@ private: static obs_data_t* successResponse(const char* messageId, obs_data_t* fields = nullptr); static obs_data_t* errorResponse(const char* messageId, const char* errorMessage, obs_data_t* additionalFields = nullptr); static obs_data_t* buildResponse(const char* messageId, const char*, obs_data_t* fields = nullptr); - static std::string jsonDataToString(obs_data_t* data); + static std::string jsonDataToString(OBSDataAutoRelease data); }; From 5b100d15d72f9439e74c42cd6fb4b14f7c2805ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lepin?= Date: Wed, 3 Feb 2021 05:31:53 +0100 Subject: [PATCH 3/7] OBSRemoteProtocol: fix missing message id --- src/protocol/OBSRemoteProtocol.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/protocol/OBSRemoteProtocol.cpp b/src/protocol/OBSRemoteProtocol.cpp index 87693add..13ca1591 100644 --- a/src/protocol/OBSRemoteProtocol.cpp +++ b/src/protocol/OBSRemoteProtocol.cpp @@ -86,7 +86,9 @@ std::string OBSRemoteProtocol::encodeEvent(const RpcEvent& event) obs_data_t* OBSRemoteProtocol::rpcResponseToJsonData(const RpcResponse& response) { - const char* messageId = response.messageId().toUtf8().constData(); + QByteArray messageIdBytes = response.messageId().toUtf8(); + const char* messageId = messageIdBytes.constData(); + OBSData additionalFields = response.additionalFields(); switch (response.status()) { case RpcResponse::Status::Ok: @@ -96,6 +98,8 @@ obs_data_t* OBSRemoteProtocol::rpcResponseToJsonData(const RpcResponse& response default: assert(false); } + + return nullptr; } obs_data_t* OBSRemoteProtocol::successResponse(const char* messageId, obs_data_t* fields) From fe2e87074a045d5fdec6891e1d687c40e9f28313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lepin?= Date: Wed, 3 Feb 2021 05:33:08 +0100 Subject: [PATCH 4/7] requests(ExecuteBatch): handle message-id in sub-requests --- src/WSRequestHandler_General.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/WSRequestHandler_General.cpp b/src/WSRequestHandler_General.cpp index fdef74e4..31d66d30 100644 --- a/src/WSRequestHandler_General.cpp +++ b/src/WSRequestHandler_General.cpp @@ -440,12 +440,13 @@ RpcResponse WSRequestHandler::ExecuteBatch(const RpcRequest& request) { size_t requestsCount = obs_data_array_count(requests); for (size_t i = 0; i < requestsCount; i++) { OBSDataAutoRelease requestData = obs_data_array_item(requests, i); + QString messageId = obs_data_get_string(requestData, "message-id"); QString methodName = obs_data_get_string(requestData, "request-type"); obs_data_unset_user_value(requestData, "request-type"); obs_data_unset_user_value(requestData, "message-id"); // build RpcRequest from json data object - RpcRequest subRequest(QString::Null(), methodName, requestData); + RpcRequest subRequest(messageId, methodName, requestData); // execute the request RpcResponse subResponse = processRequest(subRequest); From f4465e2e9b3b1c49a32172a740e59461ca0fec02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lepin?= Date: Wed, 3 Feb 2021 05:47:53 +0100 Subject: [PATCH 5/7] requests(ExecuteBatch): documentation --- src/WSRequestHandler_General.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/WSRequestHandler_General.cpp b/src/WSRequestHandler_General.cpp index 31d66d30..dc6610d6 100644 --- a/src/WSRequestHandler_General.cpp +++ b/src/WSRequestHandler_General.cpp @@ -418,16 +418,16 @@ RpcResponse WSRequestHandler::TriggerHotkeyBySequence(const RpcRequest& request) } /** -* Executes a list of requests sequentially. +* Executes a list of requests sequentially (one-by-one on the same thread). * -* @param {Array} `requests` +* @param {Array} `requests` Array of batch requests. They have the same fields as a traditional request object: `message-id`, `request-type` and the method-specific parameters. * -* @return {Array} `results` +* @return {Array} `results` Batch requests results, ordered sequentially. * * @api requests * @name ExecuteBatch * @category general -* @since unreleased +* @since 4.9.0 */ RpcResponse WSRequestHandler::ExecuteBatch(const RpcRequest& request) { if (!request.hasField("requests")) { From ed23aba0ace3ca23a4376d8cbe9026aff6a36587 Mon Sep 17 00:00:00 2001 From: tt2468 Date: Thu, 4 Feb 2021 07:15:37 -0800 Subject: [PATCH 6/7] Requests: Improve documentation of `ExecuteBatch` --- src/WSRequestHandler_General.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/WSRequestHandler_General.cpp b/src/WSRequestHandler_General.cpp index dc6610d6..ec9bda58 100644 --- a/src/WSRequestHandler_General.cpp +++ b/src/WSRequestHandler_General.cpp @@ -420,9 +420,14 @@ RpcResponse WSRequestHandler::TriggerHotkeyBySequence(const RpcRequest& request) /** * Executes a list of requests sequentially (one-by-one on the same thread). * -* @param {Array} `requests` Array of batch requests. They have the same fields as a traditional request object: `message-id`, `request-type` and the method-specific parameters. +* @param {Array} `requests` Array of requests to perform. Executed in order. +* @param {String} `requests.*.request-type` Request type. Eg. `GetVersion`. +* @param {String} `requests.*.message-id` ID of the individual request. Can be any string and not required to be unique. * * @return {Array} `results` Batch requests results, ordered sequentially. +* @return {String} `results.*.message-id` ID of the individual request which was originally provided by the client. +* @return {String} `results.*.status` Status response as string. Either `ok` or `error`. +* @return {String (Optional)} `results.*.error` Error message accompanying an `error` status. * * @api requests * @name ExecuteBatch From 6d3aa3a828ea3655f927fccadb5bcda967e55223 Mon Sep 17 00:00:00 2001 From: tt2468 Date: Thu, 4 Feb 2021 07:37:27 -0800 Subject: [PATCH 7/7] Requests: Add `abortOnFail` to ExecuteBatch request We do not currently have atomicy in this request, as it would be incredibly difficult to add, but this is at least useful for avoiding further data corruption in the case that there is a malformed request and multiple requests depend on the success of the previous one. --- src/WSRequestHandler_General.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/WSRequestHandler_General.cpp b/src/WSRequestHandler_General.cpp index ec9bda58..6be43fc8 100644 --- a/src/WSRequestHandler_General.cpp +++ b/src/WSRequestHandler_General.cpp @@ -422,7 +422,8 @@ RpcResponse WSRequestHandler::TriggerHotkeyBySequence(const RpcRequest& request) * * @param {Array} `requests` Array of requests to perform. Executed in order. * @param {String} `requests.*.request-type` Request type. Eg. `GetVersion`. -* @param {String} `requests.*.message-id` ID of the individual request. Can be any string and not required to be unique. +* @param {String (Optional)} `requests.*.message-id` ID of the individual request. Can be any string and not required to be unique. Defaults to empty string if not specified. +* @param {boolean (Optional)} `abortOnFail` Stop processing batch requests if one returns a failure. * * @return {Array} `results` Batch requests results, ordered sequentially. * @return {String} `results.*.message-id` ID of the individual request which was originally provided by the client. @@ -439,6 +440,8 @@ RpcResponse WSRequestHandler::ExecuteBatch(const RpcRequest& request) { return request.failed("missing request parameters"); } + bool abortOnFail = obs_data_get_bool(request.parameters(), "abortOnFail"); + OBSDataArrayAutoRelease results = obs_data_array_create(); OBSDataArrayAutoRelease requests = obs_data_get_array(request.parameters(), "requests"); @@ -460,6 +463,10 @@ RpcResponse WSRequestHandler::ExecuteBatch(const RpcRequest& request) { OBSDataAutoRelease subResponseData = OBSRemoteProtocol::rpcResponseToJsonData(subResponse); obs_data_array_push_back(results, subResponseData); + + // if told to abort on fail and a failure occurs, stop request processing and return the progress + if (abortOnFail && (subResponse.status() == RpcResponse::Status::Error)) + break; } OBSDataAutoRelease response = obs_data_create();