From 0432fc17eb5dba2189157e337da12341ee472606 Mon Sep 17 00:00:00 2001 From: B3n30 Date: Tue, 31 Oct 2017 10:02:42 +0100 Subject: [PATCH 1/6] Add a service to announce multiplayer rooms to web service; Add the abiltiy to receive a list of all announced rooms from web service --- src/citra/config.cpp | 3 + src/citra/default_ini.h | 2 + src/common/CMakeLists.txt | 1 + src/common/announce_multiplayer_room.h | 105 +++++++++++++++++++ src/core/CMakeLists.txt | 2 + src/core/announce_multiplayer_session.cpp | 120 ++++++++++++++++++++++ src/core/announce_multiplayer_session.h | 71 +++++++++++++ src/core/settings.h | 1 + src/web_service/CMakeLists.txt | 2 + src/web_service/announce_room_json.cpp | 112 ++++++++++++++++++++ src/web_service/announce_room_json.h | 42 ++++++++ src/web_service/telemetry_json.cpp | 5 +- src/web_service/web_backend.cpp | 88 +++++++++++++--- src/web_service/web_backend.h | 18 +++- 14 files changed, 554 insertions(+), 18 deletions(-) create mode 100644 src/common/announce_multiplayer_room.h create mode 100644 src/core/announce_multiplayer_session.cpp create mode 100644 src/core/announce_multiplayer_session.h create mode 100644 src/web_service/announce_room_json.cpp create mode 100644 src/web_service/announce_room_json.h diff --git a/src/citra/config.cpp b/src/citra/config.cpp index 45c28ad09..e4c23cc9b 100644 --- a/src/citra/config.cpp +++ b/src/citra/config.cpp @@ -164,6 +164,9 @@ void Config::ReadValues() { "WebService", "telemetry_endpoint_url", "https://services.citra-emu.org/api/telemetry"); Settings::values.verify_endpoint_url = sdl2_config->Get( "WebService", "verify_endpoint_url", "https://services.citra-emu.org/api/profile"); + Settings::values.announce_multiplayer_room_endpoint_url = + sdl2_config->Get("WebService", "announce_multiplayer_room_endpoint_url", + "https://services.citra-emu.org/api/multiplayer/rooms"); Settings::values.citra_username = sdl2_config->Get("WebService", "citra_username", ""); Settings::values.citra_token = sdl2_config->Get("WebService", "citra_token", ""); } diff --git a/src/citra/default_ini.h b/src/citra/default_ini.h index 7bac9474d..7ce5b7e32 100644 --- a/src/citra/default_ini.h +++ b/src/citra/default_ini.h @@ -187,6 +187,8 @@ enable_telemetry = telemetry_endpoint_url = https://services.citra-emu.org/api/telemetry # Endpoint URL to verify the username and token verify_endpoint_url = https://services.citra-emu.org/api/profile +# Endpoint URL for announcing public rooms +announce_multiplayer_room_endpoint_url = https://services.citra-emu.org/api/multiplayer/rooms # Username and token for Citra Web Service # See https://services.citra-emu.org/ for more info citra_username = diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index bdec3c43f..e0f5f4906 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -26,6 +26,7 @@ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/scm_rev.cpp.in" "${CMAKE_CURRENT_SOU add_library(common STATIC alignment.h + announce_multiplayer_room.h assert.h bit_field.h bit_set.h diff --git a/src/common/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h new file mode 100644 index 000000000..519b7050b --- /dev/null +++ b/src/common/announce_multiplayer_room.h @@ -0,0 +1,105 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include "common/common_types.h" + +namespace Common { +struct WebResult { + enum Code : u32 { + Success, + InvalidURL, + CredentialsMissing, + CprError, + HttpError, + WrongContent, + NoWebservice, + }; + Code result_code; + std::string result_string; +}; +} // namespace Common + +namespace AnnounceMultiplayerRoom { + +using MacAddress = std::array; + +struct Room { + struct Member { + std::string name; + MacAddress mac_address; + std::string game_name; + u64 game_id; + }; + std::string name; + std::string GUID; + std::string owner; + std::string ip; + u16 port; + u32 max_player; + u32 net_version; + bool has_password; + std::string preferred_game; + u64 preferred_game_id; + + std::vector members; +}; +using RoomList = std::vector; + +/** + * A AnnounceMultiplayerRoom interface class. A backend to submit/get to/from a web service should + * implement this interface. + */ +class Backend : NonCopyable { +public: + virtual ~Backend() = default; + virtual void SetRoomInformation(const std::string& guid, const std::string& name, + const u16 port, const u32 max_player, const u32 net_version, + const bool has_password, const std::string& preferred_game, + const u64 preferred_game_id) = 0; + virtual void AddPlayer(const std::string& nickname, const MacAddress& mac_address, + const u64 game_id, const std::string& game_name) = 0; + virtual std::future Announce() = 0; + virtual void ClearPlayers() = 0; + virtual std::future GetRoomList(std::function func) = 0; + virtual void Delete() = 0; +}; + +/** + * Empty implementation of AnnounceMultiplayerRoom interface that drops all data. Used when a + * functional backend implementation is not available. + */ +class NullBackend : public Backend { +public: + ~NullBackend() = default; + void SetRoomInformation(const std::string& /*guid*/, const std::string& /*name*/, + const u16 /*port*/, const u32 /*max_player*/, const u32 /*net_version*/, + const bool /*has_password*/, const std::string& /*preferred_game*/, + const u64 /*preferred_game_id*/) override {} + void AddPlayer(const std::string& /*nickname*/, const MacAddress& /*mac_address*/, + const u64 /*game_id*/, const std::string& /*game_name*/) override {} + std::future Announce() override { + return std::async(std::launch::async, []() { + return Common::WebResult{Common::WebResult::Code::NoWebservice, + "WebService is missing"}; + }); + } + void ClearPlayers() override {} + std::future GetRoomList(std::function func) override { + return std::async(std::launch::async, [func]() { + func(); + return RoomList{}; + }); + } + + void Delete() override {} +}; + +} // namespace AnnounceMultiplayerRoom diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b8d22bd1a..192d5375e 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,5 +1,7 @@ add_library(core STATIC 3ds.h + announce_multiplayer_session.cpp + announce_multiplayer_session.h arm/arm_interface.h arm/dynarmic/arm_dynarmic.cpp arm/dynarmic/arm_dynarmic.h diff --git a/src/core/announce_multiplayer_session.cpp b/src/core/announce_multiplayer_session.cpp new file mode 100644 index 000000000..b4b732c7c --- /dev/null +++ b/src/core/announce_multiplayer_session.cpp @@ -0,0 +1,120 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "announce_multiplayer_session.h" +#include "common/announce_multiplayer_room.h" +#include "common/assert.h" +#include "core/settings.h" +#include "network/network.h" + +#ifdef ENABLE_WEB_SERVICE +#include "web_service/announce_room_json.h" +#endif + +namespace Core { + +// Time between room is announced to web_service +static constexpr std::chrono::seconds announce_time_interval(15); + +AnnounceMultiplayerSession::AnnounceMultiplayerSession() : announce(false), finished(true) { +#ifdef ENABLE_WEB_SERVICE + backend = std::make_unique( + Settings::values.announce_multiplayer_room_endpoint_url, Settings::values.citra_username, + Settings::values.citra_token); +#else + backend = std::make_unique(); +#endif +} + +void AnnounceMultiplayerSession::Start() { + if (announce_multiplayer_thread) { + Stop(); + } + + announce_multiplayer_thread = + std::make_unique(&AnnounceMultiplayerSession::AnnounceMultiplayerLoop, this); +} + +void AnnounceMultiplayerSession::Stop() { + if (!announce && finished) + return; + announce = false; + // Detaching the loop, to not wait for the sleep to finish. The loop thread will finish soon. + if (announce_multiplayer_thread) { + announce_multiplayer_thread->detach(); + announce_multiplayer_thread.reset(); + backend->Delete(); + } +} + +std::shared_ptr> +AnnounceMultiplayerSession::BindErrorCallback( + std::function function) { + std::lock_guard lock(callback_mutex); + auto handle = std::make_shared>(function); + error_callbacks.insert(handle); + return handle; +} + +void AnnounceMultiplayerSession::UnbindErrorCallback( + std::shared_ptr> handle) { + std::lock_guard lock(callback_mutex); + error_callbacks.erase(handle); +} + +AnnounceMultiplayerSession::~AnnounceMultiplayerSession() { + Stop(); +} + +void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() { + while (!finished) { + std::this_thread::sleep_for(announce_time_interval / 10); + } + announce = true; + finished = false; + std::future future; + while (announce) { + if (std::shared_ptr room = Network::GetRoom().lock()) { + if (room->GetState() == Network::Room::State::Open) { + Network::RoomInformation room_information = room->GetRoomInformation(); + std::vector memberlist = room->GetRoomMemberList(); + backend->SetRoomInformation( + room_information.guid, room_information.name, room_information.port, + room_information.member_slots, Network::network_version, room->HasPassword(), + room_information.preferred_game, room_information.preferred_game_id); + backend->ClearPlayers(); + for (const auto& member : memberlist) { + backend->AddPlayer(member.nickname, member.mac_address, member.game_info.id, + member.game_info.name); + } + future = backend->Announce(); + } else { + announce = false; + } + } else { + announce = false; + } + if (future.valid()) { + Common::WebResult result = future.get(); + if (result.result_code != Common::WebResult::Success) { + std::lock_guard lock(callback_mutex); + for (auto callback : error_callbacks) { + (*callback)(result); + } + announce = false; + } + } + std::this_thread::sleep_for(announce_time_interval); + } + finished = true; +} + +std::future AnnounceMultiplayerSession::GetRoomList( + std::function func) { + return backend->GetRoomList(func); +} + +} // namespace Core diff --git a/src/core/announce_multiplayer_session.h b/src/core/announce_multiplayer_session.h new file mode 100644 index 000000000..a7a36b82d --- /dev/null +++ b/src/core/announce_multiplayer_session.h @@ -0,0 +1,71 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include "common/announce_multiplayer_room.h" +#include "common/common_types.h" + +namespace Core { + +/** + * Instruments AnnounceMultiplayerRoom::Backend. + * Creates a thread that regularly updates the room information and submits them + * An async get of room information is also possible + */ +class AnnounceMultiplayerSession : NonCopyable { +public: + AnnounceMultiplayerSession(); + ~AnnounceMultiplayerSession(); + + /** + * Allows to bind a function that will get called if the announce encounters an error + * @param function The function that gets called + * @return A handle that can be used the unbind the function + */ + std::shared_ptr> BindErrorCallback( + std::function function); + + /** + * Unbind a function from the error callbacks + * @param handle The handle for the function that should get unbind + */ + void UnbindErrorCallback(std::shared_ptr> handle); + + /** + * Starts the announce of a room to web services + */ + void Start(); + + /** + * Stops the announce to web services + */ + void Stop(); + + /** + * Returns a list of all room information the backend got + * @param func A function that gets executed when the async get finished, e.g. a signal + * @return a list of rooms received from the web service + */ + std::future GetRoomList(std::function func); + +private: + std::atomic announce{false}; + std::atomic finished{true}; + std::mutex callback_mutex; + std::set>> error_callbacks; + std::unique_ptr announce_multiplayer_thread; + + std::unique_ptr + backend; ///< Backend interface that logs fields + + void AnnounceMultiplayerLoop(); +}; + +} // namespace Core diff --git a/src/core/settings.h b/src/core/settings.h index 8d78cb424..9657f7f8b 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -134,6 +134,7 @@ struct Values { bool enable_telemetry; std::string telemetry_endpoint_url; std::string verify_endpoint_url; + std::string announce_multiplayer_room_endpoint_url; std::string citra_username; std::string citra_token; } extern values; diff --git a/src/web_service/CMakeLists.txt b/src/web_service/CMakeLists.txt index b6de2632b..911fbe26b 100644 --- a/src/web_service/CMakeLists.txt +++ b/src/web_service/CMakeLists.txt @@ -1,4 +1,6 @@ add_library(web_service STATIC + announce_room_json.cpp + announce_room_json.h telemetry_json.cpp telemetry_json.h verify_login.cpp diff --git a/src/web_service/announce_room_json.cpp b/src/web_service/announce_room_json.cpp new file mode 100644 index 000000000..b8af09720 --- /dev/null +++ b/src/web_service/announce_room_json.cpp @@ -0,0 +1,112 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "common/logging/log.h" +#include "web_service/announce_room_json.h" +#include "web_service/web_backend.h" + +namespace AnnounceMultiplayerRoom { + +void to_json(nlohmann::json& json, const Room::Member& member) { + json["name"] = member.name; + json["gameName"] = member.game_name; + json["gameId"] = member.game_id; +} + +void from_json(const nlohmann::json& json, Room::Member& member) { + member.name = json.at("name").get(); + member.game_name = json.at("gameName").get(); + member.game_id = json.at("gameId").get(); +} + +void to_json(nlohmann::json& json, const Room& room) { + json["id"] = room.GUID; + json["port"] = room.port; + json["name"] = room.name; + json["preferredGameName"] = room.preferred_game; + json["preferredGameId"] = room.preferred_game_id; + json["maxPlayers"] = room.max_player; + json["netVersion"] = room.net_version; + json["hasPassword"] = room.has_password; + if (room.members.size() > 0) { + nlohmann::json member_json = room.members; + json["players"] = member_json; + } +} + +void from_json(const nlohmann::json& json, Room& room) { + room.ip = json.at("address").get(); + room.name = json.at("name").get(); + room.owner = json.at("owner").get(); + room.port = json.at("port").get(); + room.preferred_game = json.at("preferredGameName").get(); + room.preferred_game_id = json.at("preferredGameId").get(); + room.max_player = json.at("maxPlayers").get(); + room.net_version = json.at("netVersion").get(); + room.has_password = json.at("hasPassword").get(); + try { + room.members = json.at("players").get>(); + } catch (const nlohmann::detail::out_of_range& e) { + LOG_DEBUG(Network, "Out of range %s", e.what()); + } +} + +} // namespace AnnounceMultiplayerRoom + +namespace WebService { + +void RoomJson::SetRoomInformation(const std::string& guid, const std::string& name, const u16 port, + const u32 max_player, const u32 net_version, + const bool has_password, const std::string& preferred_game, + const u64 preferred_game_id) { + room.name = name; + room.GUID = guid; + room.port = port; + room.max_player = max_player; + room.net_version = net_version; + room.has_password = has_password; + room.preferred_game = preferred_game; + room.preferred_game_id = preferred_game_id; +} +void RoomJson::AddPlayer(const std::string& nickname, + const AnnounceMultiplayerRoom::MacAddress& mac_address, const u64 game_id, + const std::string& game_name) { + AnnounceMultiplayerRoom::Room::Member member; + member.name = nickname; + member.mac_address = mac_address; + member.game_id = game_id; + member.game_name = game_name; + room.members.push_back(member); +} + +std::future RoomJson::Announce() { + nlohmann::json json = room; + return PostJson(endpoint_url, json.dump(), false, username, token); +} + +void RoomJson::ClearPlayers() { + room.members.clear(); +} + +std::future RoomJson::GetRoomList(std::function func) { + auto DeSerialize = [func](const std::string& reply) -> AnnounceMultiplayerRoom::RoomList { + nlohmann::json json = nlohmann::json::parse(reply); + AnnounceMultiplayerRoom::RoomList room_list = + json.at("rooms").get(); + func(); + return room_list; + }; + return GetJson(DeSerialize, endpoint_url, true, username, + token); +} + +void RoomJson::Delete() { + nlohmann::json json; + json["id"] = room.GUID; + DeleteJson(endpoint_url, json.dump(), username, token); +} + +} // namespace WebService diff --git a/src/web_service/announce_room_json.h b/src/web_service/announce_room_json.h new file mode 100644 index 000000000..907a76b3a --- /dev/null +++ b/src/web_service/announce_room_json.h @@ -0,0 +1,42 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "common/announce_multiplayer_room.h" + +namespace WebService { + +/** + * Implementation of AnnounceMultiplayerRoom::Backend that (de)serializes room information into/from + * JSON, and submits/gets it to/from the Citra web service + */ +class RoomJson : public AnnounceMultiplayerRoom::Backend { +public: + RoomJson(const std::string& endpoint_url, const std::string& username, const std::string& token) + : endpoint_url(endpoint_url), username(username), token(token) {} + ~RoomJson() = default; + void SetRoomInformation(const std::string& guid, const std::string& name, const u16 port, + const u32 max_player, const u32 net_version, const bool has_password, + const std::string& preferred_game, + const u64 preferred_game_id) override; + void AddPlayer(const std::string& nickname, + const AnnounceMultiplayerRoom::MacAddress& mac_address, const u64 game_id, + const std::string& game_name) override; + std::future Announce() override; + void ClearPlayers() override; + std::future GetRoomList(std::function func) override; + void Delete() override; + +private: + AnnounceMultiplayerRoom::Room room; + std::string endpoint_url; + std::string username; + std::string token; +}; + +} // namespace WebService diff --git a/src/web_service/telemetry_json.cpp b/src/web_service/telemetry_json.cpp index 6ad2ffcd4..642c2617a 100644 --- a/src/web_service/telemetry_json.cpp +++ b/src/web_service/telemetry_json.cpp @@ -80,7 +80,10 @@ void TelemetryJson::Complete() { SerializeSection(Telemetry::FieldType::UserFeedback, "UserFeedback"); SerializeSection(Telemetry::FieldType::UserConfig, "UserConfig"); SerializeSection(Telemetry::FieldType::UserSystem, "UserSystem"); - PostJson(endpoint_url, TopSection().dump(), true, username, token); + + // Send the telemetry async but don't handle the errors since the were written to the log + static std::future future = + PostJson(endpoint_url, TopSection().dump(), true, username, token); } } // namespace WebService diff --git a/src/web_service/web_backend.cpp b/src/web_service/web_backend.cpp index b17d82f9c..2b0f5a482 100644 --- a/src/web_service/web_backend.cpp +++ b/src/web_service/web_backend.cpp @@ -9,6 +9,7 @@ #include #include #include +#include "common/announce_multiplayer_room.h" #include "common/logging/log.h" #include "web_service/web_backend.h" @@ -31,17 +32,23 @@ void Win32WSAStartup() { #endif } -void PostJson(const std::string& url, const std::string& data, bool allow_anonymous, - const std::string& username, const std::string& token) { +std::future PostJson(const std::string& url, const std::string& data, + bool allow_anonymous, const std::string& username, + const std::string& token) { if (url.empty()) { LOG_ERROR(WebService, "URL is invalid"); - return; + return std::async(std::launch::async, []() { + return Common::WebResult{Common::WebResult::Code::InvalidURL, "URL is invalid"}; + }); } const bool are_credentials_provided{!token.empty() && !username.empty()}; if (!allow_anonymous && !are_credentials_provided) { LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); - return; + return std::async(std::launch::async, []() { + return Common::WebResult{Common::WebResult::Code::CredentialsMissing, + "Credentials needed"}; + }); } Win32WSAStartup(); @@ -60,23 +67,26 @@ void PostJson(const std::string& url, const std::string& data, bool allow_anonym } // Post JSON asynchronously - static std::future future; - future = cpr::PostCallback( + return cpr::PostCallback( [](cpr::Response r) { if (r.error) { - LOG_ERROR(WebService, "POST returned cpr error: %u:%s", + LOG_ERROR(WebService, "POST to %s returned cpr error: %u:%s", r.url.c_str(), static_cast(r.error.code), r.error.message.c_str()); - return; + return Common::WebResult{Common::WebResult::Code::CprError, r.error.message}; } if (r.status_code >= 400) { - LOG_ERROR(WebService, "POST returned error status code: %u", r.status_code); - return; + LOG_ERROR(WebService, "POST to %s returned error status code: %u", r.url.c_str(), + r.status_code); + return Common::WebResult{Common::WebResult::Code::HttpError, + std::to_string(r.status_code)}; } if (r.header["content-type"].find("application/json") == std::string::npos) { - LOG_ERROR(WebService, "POST returned wrong content: %s", + LOG_ERROR(WebService, "POST to %s returned wrong content: %s", r.url.c_str(), r.header["content-type"].c_str()); - return; + return Common::WebResult{Common::WebResult::Code::WrongContent, + r.header["content-type"]}; } + return Common::WebResult{Common::WebResult::Code::Success, ""}; }, cpr::Url{url}, cpr::Body{data}, header); } @@ -115,16 +125,17 @@ std::future GetJson(std::function func, const std::str return cpr::GetCallback( [func{std::move(func)}](cpr::Response r) { if (r.error) { - LOG_ERROR(WebService, "GET returned cpr error: %u:%s", + LOG_ERROR(WebService, "GET to %s returned cpr error: %u:%s", r.url.c_str(), static_cast(r.error.code), r.error.message.c_str()); return func(""); } if (r.status_code >= 400) { - LOG_ERROR(WebService, "GET returned error code: %u", r.status_code); + LOG_ERROR(WebService, "GET to %s returned error code: %u", r.url.c_str(), + r.status_code); return func(""); } if (r.header["content-type"].find("application/json") == std::string::npos) { - LOG_ERROR(WebService, "GET returned wrong content: %s", + LOG_ERROR(WebService, "GET to %s returned wrong content: %s", r.url.c_str(), r.header["content-type"].c_str()); return func(""); } @@ -136,5 +147,52 @@ std::future GetJson(std::function func, const std::str template std::future GetJson(std::function func, const std::string& url, bool allow_anonymous, const std::string& username, const std::string& token); +template std::future GetJson( + std::function func, + const std::string& url, bool allow_anonymous, const std::string& username, + const std::string& token); + +void DeleteJson(const std::string& url, const std::string& data, const std::string& username, + const std::string& token) { + if (url.empty()) { + LOG_ERROR(WebService, "URL is invalid"); + return; + } + + if (token.empty() || username.empty()) { + LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); + return; + } + + Win32WSAStartup(); + + // Built request header + cpr::Header header = {{"Content-Type", "application/json"}, + {"x-username", username.c_str()}, + {"x-token", token.c_str()}, + {"api-version", API_VERSION}}; + + // Delete JSON asynchronously + static std::future future; + future = cpr::DeleteCallback( + [](cpr::Response r) { + if (r.error) { + LOG_ERROR(WebService, "Delete to %s returned cpr error: %u:%s", r.url.c_str(), + static_cast(r.error.code), r.error.message.c_str()); + return; + } + if (r.status_code >= 400) { + LOG_ERROR(WebService, "Delete to %s returned error status code: %u", r.url.c_str(), + r.status_code); + return; + } + if (r.header["content-type"].find("application/json") == std::string::npos) { + LOG_ERROR(WebService, "Delete to %s returned wrong content: %s", r.url.c_str(), + r.header["content-type"].c_str()); + return; + } + }, + cpr::Url{url}, cpr::Body{data}, header); +} } // namespace WebService diff --git a/src/web_service/web_backend.h b/src/web_service/web_backend.h index a63c75d13..29aab00cb 100644 --- a/src/web_service/web_backend.h +++ b/src/web_service/web_backend.h @@ -7,6 +7,8 @@ #include #include #include +#include +#include "common/announce_multiplayer_room.h" #include "common/common_types.h" namespace WebService { @@ -18,9 +20,11 @@ namespace WebService { * @param allow_anonymous If true, allow anonymous unauthenticated requests. * @param username Citra username to use for authentication. * @param token Citra token to use for authentication. + * @return future with the error or result of the POST */ -void PostJson(const std::string& url, const std::string& data, bool allow_anonymous, - const std::string& username = {}, const std::string& token = {}); +std::future PostJson(const std::string& url, const std::string& data, + bool allow_anonymous, const std::string& username = {}, + const std::string& token = {}); /** * Gets JSON from services.citra-emu.org. @@ -36,4 +40,14 @@ std::future GetJson(std::function func, const std::str bool allow_anonymous, const std::string& username = {}, const std::string& token = {}); +/** + * Delete JSON to services.citra-emu.org. + * @param url URL of the services.citra-emu.org endpoint to post data to. + * @param data String of JSON data to use for the body of the DELETE request. + * @param username Citra username to use for authentication. + * @param token Citra token to use for authentication. + */ +void DeleteJson(const std::string& url, const std::string& data, const std::string& username = {}, + const std::string& token = {}); + } // namespace WebService From 1485093fd91bf1eca2b19e929e05af1305bb7580 Mon Sep 17 00:00:00 2001 From: B3n30 Date: Thu, 2 Nov 2017 20:24:32 +0100 Subject: [PATCH 2/6] fixup! Add a service to announce multiplayer rooms to web service; Add the abiltiy to receive a list of all announced rooms from web service --- src/core/announce_multiplayer_session.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/announce_multiplayer_session.cpp b/src/core/announce_multiplayer_session.cpp index b4b732c7c..5cc2781a9 100644 --- a/src/core/announce_multiplayer_session.cpp +++ b/src/core/announce_multiplayer_session.cpp @@ -104,7 +104,6 @@ void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() { for (auto callback : error_callbacks) { (*callback)(result); } - announce = false; } } std::this_thread::sleep_for(announce_time_interval); From 93742f17b3021257953e217f413091843ec48d62 Mon Sep 17 00:00:00 2001 From: B3n30 Date: Sun, 5 Nov 2017 20:23:22 +0100 Subject: [PATCH 3/6] fixup! fixup! Add a service to announce multiplayer rooms to web service; Add the abiltiy to receive a list of all announced rooms from web service --- src/common/announce_multiplayer_room.h | 10 +++--- src/core/announce_multiplayer_session.cpp | 39 ++++++++++++----------- src/web_service/announce_room_json.cpp | 8 ++--- src/web_service/announce_room_json.h | 2 +- src/web_service/telemetry_json.cpp | 5 ++- src/web_service/telemetry_json.h | 3 ++ 6 files changed, 35 insertions(+), 32 deletions(-) diff --git a/src/common/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h index 519b7050b..091dded8f 100644 --- a/src/common/announce_multiplayer_room.h +++ b/src/common/announce_multiplayer_room.h @@ -13,7 +13,7 @@ namespace Common { struct WebResult { - enum Code : u32 { + enum class Code : u32 { Success, InvalidURL, CredentialsMissing, @@ -39,7 +39,7 @@ struct Room { u64 game_id; }; std::string name; - std::string GUID; + std::string UID; std::string owner; std::string ip; u16 port; @@ -60,8 +60,8 @@ using RoomList = std::vector; class Backend : NonCopyable { public: virtual ~Backend() = default; - virtual void SetRoomInformation(const std::string& guid, const std::string& name, - const u16 port, const u32 max_player, const u32 net_version, + virtual void SetRoomInformation(const std::string& uid, const std::string& name, const u16 port, + const u32 max_player, const u32 net_version, const bool has_password, const std::string& preferred_game, const u64 preferred_game_id) = 0; virtual void AddPlayer(const std::string& nickname, const MacAddress& mac_address, @@ -79,7 +79,7 @@ public: class NullBackend : public Backend { public: ~NullBackend() = default; - void SetRoomInformation(const std::string& /*guid*/, const std::string& /*name*/, + void SetRoomInformation(const std::string& /*uid*/, const std::string& /*name*/, const u16 /*port*/, const u32 /*max_player*/, const u32 /*net_version*/, const bool /*has_password*/, const std::string& /*preferred_game*/, const u64 /*preferred_game_id*/) override {} diff --git a/src/core/announce_multiplayer_session.cpp b/src/core/announce_multiplayer_session.cpp index 5cc2781a9..a3ba78d78 100644 --- a/src/core/announce_multiplayer_session.cpp +++ b/src/core/announce_multiplayer_session.cpp @@ -77,29 +77,30 @@ void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() { finished = false; std::future future; while (announce) { - if (std::shared_ptr room = Network::GetRoom().lock()) { - if (room->GetState() == Network::Room::State::Open) { - Network::RoomInformation room_information = room->GetRoomInformation(); - std::vector memberlist = room->GetRoomMemberList(); - backend->SetRoomInformation( - room_information.guid, room_information.name, room_information.port, - room_information.member_slots, Network::network_version, room->HasPassword(), - room_information.preferred_game, room_information.preferred_game_id); - backend->ClearPlayers(); - for (const auto& member : memberlist) { - backend->AddPlayer(member.nickname, member.mac_address, member.game_info.id, - member.game_info.name); - } - future = backend->Announce(); - } else { - announce = false; - } - } else { + std::shared_ptr room = Network::GetRoom().lock(); + if (!room) { announce = false; + continue; } + if (room->GetState() != Network::Room::State::Open) { + announce = false; + continue; + } + Network::RoomInformation room_information = room->GetRoomInformation(); + std::vector memberlist = room->GetRoomMemberList(); + backend->SetRoomInformation( + room_information.uid, room_information.name, room_information.port, + room_information.member_slots, Network::network_version, room->HasPassword(), + room_information.preferred_game, room_information.preferred_game_id); + backend->ClearPlayers(); + for (const auto& member : memberlist) { + backend->AddPlayer(member.nickname, member.mac_address, member.game_info.id, + member.game_info.name); + } + future = backend->Announce(); if (future.valid()) { Common::WebResult result = future.get(); - if (result.result_code != Common::WebResult::Success) { + if (result.result_code != Common::WebResult::Code::Success) { std::lock_guard lock(callback_mutex); for (auto callback : error_callbacks) { (*callback)(result); diff --git a/src/web_service/announce_room_json.cpp b/src/web_service/announce_room_json.cpp index b8af09720..d2b79cbae 100644 --- a/src/web_service/announce_room_json.cpp +++ b/src/web_service/announce_room_json.cpp @@ -23,7 +23,7 @@ void from_json(const nlohmann::json& json, Room::Member& member) { } void to_json(nlohmann::json& json, const Room& room) { - json["id"] = room.GUID; + json["id"] = room.UID; json["port"] = room.port; json["name"] = room.name; json["preferredGameName"] = room.preferred_game; @@ -58,12 +58,12 @@ void from_json(const nlohmann::json& json, Room& room) { namespace WebService { -void RoomJson::SetRoomInformation(const std::string& guid, const std::string& name, const u16 port, +void RoomJson::SetRoomInformation(const std::string& uid, const std::string& name, const u16 port, const u32 max_player, const u32 net_version, const bool has_password, const std::string& preferred_game, const u64 preferred_game_id) { room.name = name; - room.GUID = guid; + room.UID = uid; room.port = port; room.max_player = max_player; room.net_version = net_version; @@ -105,7 +105,7 @@ std::future RoomJson::GetRoomList(std::functi void RoomJson::Delete() { nlohmann::json json; - json["id"] = room.GUID; + json["id"] = room.UID; DeleteJson(endpoint_url, json.dump(), username, token); } diff --git a/src/web_service/announce_room_json.h b/src/web_service/announce_room_json.h index 907a76b3a..85550e838 100644 --- a/src/web_service/announce_room_json.h +++ b/src/web_service/announce_room_json.h @@ -20,7 +20,7 @@ public: RoomJson(const std::string& endpoint_url, const std::string& username, const std::string& token) : endpoint_url(endpoint_url), username(username), token(token) {} ~RoomJson() = default; - void SetRoomInformation(const std::string& guid, const std::string& name, const u16 port, + void SetRoomInformation(const std::string& uid, const std::string& name, const u16 port, const u32 max_player, const u32 net_version, const bool has_password, const std::string& preferred_game, const u64 preferred_game_id) override; diff --git a/src/web_service/telemetry_json.cpp b/src/web_service/telemetry_json.cpp index 642c2617a..28ae0d34e 100644 --- a/src/web_service/telemetry_json.cpp +++ b/src/web_service/telemetry_json.cpp @@ -81,9 +81,8 @@ void TelemetryJson::Complete() { SerializeSection(Telemetry::FieldType::UserConfig, "UserConfig"); SerializeSection(Telemetry::FieldType::UserSystem, "UserSystem"); - // Send the telemetry async but don't handle the errors since the were written to the log - static std::future future = - PostJson(endpoint_url, TopSection().dump(), true, username, token); + // Send the telemetry async but don't handle the errors since they were written to the log + future = PostJson(endpoint_url, TopSection().dump(), true, username, token); } } // namespace WebService diff --git a/src/web_service/telemetry_json.h b/src/web_service/telemetry_json.h index 9e78c6803..ae4a6f3c7 100644 --- a/src/web_service/telemetry_json.h +++ b/src/web_service/telemetry_json.h @@ -5,8 +5,10 @@ #pragma once #include +#include #include #include +#include "common/announce_multiplayer_room.h" #include "common/telemetry.h" namespace WebService { @@ -54,6 +56,7 @@ private: std::string endpoint_url; std::string username; std::string token; + std::future future; }; } // namespace WebService From eba2351f9eb5d96216f37822949b613324cca4bb Mon Sep 17 00:00:00 2001 From: B3n30 Date: Tue, 7 Nov 2017 21:51:11 +0100 Subject: [PATCH 4/6] Announce-Service: Add conditional variable for the wait in the announce thread --- src/common/announce_multiplayer_room.h | 40 ++++++++++++++++++++++- src/core/announce_multiplayer_session.cpp | 21 +++++------- src/core/announce_multiplayer_session.h | 19 +++++++---- src/web_service/web_backend.cpp | 8 ++--- 4 files changed, 63 insertions(+), 25 deletions(-) diff --git a/src/common/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h index 091dded8f..f01e7cf74 100644 --- a/src/common/announce_multiplayer_room.h +++ b/src/common/announce_multiplayer_room.h @@ -60,15 +60,53 @@ using RoomList = std::vector; class Backend : NonCopyable { public: virtual ~Backend() = default; + + /** + * Sets the Information that gets used for the announce + * @param uid The Id of the room + * @param name The name of the room + * @param port The port of the room + * @param net_version The version of the libNetwork that gets used + * @param has_password True if the room is passowrd protected + * @param preferred_game The preferred game of the room + * @param preferred_game_id The title id of the preferred game + */ virtual void SetRoomInformation(const std::string& uid, const std::string& name, const u16 port, const u32 max_player, const u32 net_version, const bool has_password, const std::string& preferred_game, const u64 preferred_game_id) = 0; + /** + * Adds a player information to the data that gets announced + * @param nickname The nickname of the player + * @param mac_address The MAC Address of the player + * @param game_id The title id of the game the player plays + * @param game_name The name of the game the player plays + */ virtual void AddPlayer(const std::string& nickname, const MacAddress& mac_address, const u64 game_id, const std::string& game_name) = 0; + + /** + * Send the data to the announce service + * @result The result of the announce attempt + */ virtual std::future Announce() = 0; + + /** + * Empties the stored players + */ virtual void ClearPlayers() = 0; + + /** + * Get the room information from the announce service + * @param func a function that gets exectued when the get finished. + * Can be used as a callback + * @result A list of all rooms the announce service has + */ virtual std::future GetRoomList(std::function func) = 0; + + /** + * Sends a delete message to the announce service + */ virtual void Delete() = 0; }; @@ -93,7 +131,7 @@ public: } void ClearPlayers() override {} std::future GetRoomList(std::function func) override { - return std::async(std::launch::async, [func]() { + return std::async(std::launch::deferred, [func]() { func(); return RoomList{}; }); diff --git a/src/core/announce_multiplayer_session.cpp b/src/core/announce_multiplayer_session.cpp index a3ba78d78..af7d4fe58 100644 --- a/src/core/announce_multiplayer_session.cpp +++ b/src/core/announce_multiplayer_session.cpp @@ -19,7 +19,7 @@ namespace Core { // Time between room is announced to web_service static constexpr std::chrono::seconds announce_time_interval(15); -AnnounceMultiplayerSession::AnnounceMultiplayerSession() : announce(false), finished(true) { +AnnounceMultiplayerSession::AnnounceMultiplayerSession() : announce(false) { #ifdef ENABLE_WEB_SERVICE backend = std::make_unique( Settings::values.announce_multiplayer_room_endpoint_url, Settings::values.citra_username, @@ -39,19 +39,19 @@ void AnnounceMultiplayerSession::Start() { } void AnnounceMultiplayerSession::Stop() { - if (!announce && finished) + if (!announce) return; announce = false; // Detaching the loop, to not wait for the sleep to finish. The loop thread will finish soon. if (announce_multiplayer_thread) { - announce_multiplayer_thread->detach(); + cv.notify_all(); + announce_multiplayer_thread->join(); announce_multiplayer_thread.reset(); backend->Delete(); } } -std::shared_ptr> -AnnounceMultiplayerSession::BindErrorCallback( +AnnounceMultiplayerSession::CallbackHandle AnnounceMultiplayerSession::BindErrorCallback( std::function function) { std::lock_guard lock(callback_mutex); auto handle = std::make_shared>(function); @@ -59,8 +59,7 @@ AnnounceMultiplayerSession::BindErrorCallback( return handle; } -void AnnounceMultiplayerSession::UnbindErrorCallback( - std::shared_ptr> handle) { +void AnnounceMultiplayerSession::UnbindErrorCallback(CallbackHandle handle) { std::lock_guard lock(callback_mutex); error_callbacks.erase(handle); } @@ -70,13 +69,11 @@ AnnounceMultiplayerSession::~AnnounceMultiplayerSession() { } void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() { - while (!finished) { - std::this_thread::sleep_for(announce_time_interval / 10); - } announce = true; - finished = false; std::future future; while (announce) { + std::unique_lock lock(cv_m); + cv.wait_for(lock, announce_time_interval); std::shared_ptr room = Network::GetRoom().lock(); if (!room) { announce = false; @@ -107,9 +104,7 @@ void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() { } } } - std::this_thread::sleep_for(announce_time_interval); } - finished = true; } std::future AnnounceMultiplayerSession::GetRoomList( diff --git a/src/core/announce_multiplayer_session.h b/src/core/announce_multiplayer_session.h index a7a36b82d..6faea3ae3 100644 --- a/src/core/announce_multiplayer_session.h +++ b/src/core/announce_multiplayer_session.h @@ -5,8 +5,10 @@ #pragma once #include +#include #include #include +#include #include #include #include "common/announce_multiplayer_room.h" @@ -21,6 +23,7 @@ namespace Core { */ class AnnounceMultiplayerSession : NonCopyable { public: + using CallbackHandle = std::shared_ptr>; AnnounceMultiplayerSession(); ~AnnounceMultiplayerSession(); @@ -29,14 +32,13 @@ public: * @param function The function that gets called * @return A handle that can be used the unbind the function */ - std::shared_ptr> BindErrorCallback( - std::function function); + CallbackHandle BindErrorCallback(std::function function); /** * Unbind a function from the error callbacks * @param handle The handle for the function that should get unbind */ - void UnbindErrorCallback(std::shared_ptr> handle); + void UnbindErrorCallback(CallbackHandle handle); /** * Starts the announce of a room to web services @@ -57,13 +59,16 @@ public: private: std::atomic announce{false}; - std::atomic finished{true}; + + /// conditional variable to notify the announce thread to end early + std::condition_variable cv; + std::mutex cv_m; ///< mutex for cv std::mutex callback_mutex; - std::set>> error_callbacks; + std::set error_callbacks; std::unique_ptr announce_multiplayer_thread; - std::unique_ptr - backend; ///< Backend interface that logs fields + /// Backend interface that logs fields + std::unique_ptr backend; void AnnounceMultiplayerLoop(); }; diff --git a/src/web_service/web_backend.cpp b/src/web_service/web_backend.cpp index 2b0f5a482..64f6a69bd 100644 --- a/src/web_service/web_backend.cpp +++ b/src/web_service/web_backend.cpp @@ -37,7 +37,7 @@ std::future PostJson(const std::string& url, const std::strin const std::string& token) { if (url.empty()) { LOG_ERROR(WebService, "URL is invalid"); - return std::async(std::launch::async, []() { + return std::async(std::launch::deferred, []() { return Common::WebResult{Common::WebResult::Code::InvalidURL, "URL is invalid"}; }); } @@ -45,7 +45,7 @@ std::future PostJson(const std::string& url, const std::strin const bool are_credentials_provided{!token.empty() && !username.empty()}; if (!allow_anonymous && !are_credentials_provided) { LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); - return std::async(std::launch::async, []() { + return std::async(std::launch::deferred, []() { return Common::WebResult{Common::WebResult::Code::CredentialsMissing, "Credentials needed"}; }); @@ -97,13 +97,13 @@ std::future GetJson(std::function func, const std::str const std::string& token) { if (url.empty()) { LOG_ERROR(WebService, "URL is invalid"); - return std::async(std::launch::async, [func{std::move(func)}]() { return func(""); }); + return std::async(std::launch::deferred, [func{std::move(func)}]() { return func(""); }); } const bool are_credentials_provided{!token.empty() && !username.empty()}; if (!allow_anonymous && !are_credentials_provided) { LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); - return std::async(std::launch::async, [func{std::move(func)}]() { return func(""); }); + return std::async(std::launch::deferred, [func{std::move(func)}]() { return func(""); }); } Win32WSAStartup(); From 423df498d93a5038fe9026ff5af64d4fb6dfb3c4 Mon Sep 17 00:00:00 2001 From: B3n30 Date: Tue, 7 Nov 2017 22:18:17 +0100 Subject: [PATCH 5/6] fixup! Announce-Service: Add conditional variable for the wait in the announce thread --- src/common/announce_multiplayer_room.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h index f01e7cf74..60d7933f7 100644 --- a/src/common/announce_multiplayer_room.h +++ b/src/common/announce_multiplayer_room.h @@ -124,7 +124,7 @@ public: void AddPlayer(const std::string& /*nickname*/, const MacAddress& /*mac_address*/, const u64 /*game_id*/, const std::string& /*game_name*/) override {} std::future Announce() override { - return std::async(std::launch::async, []() { + return std::async(std::launch::deferred, []() { return Common::WebResult{Common::WebResult::Code::NoWebservice, "WebService is missing"}; }); From a5e63a8c35f02022f2c393c5941f53b2518641fd Mon Sep 17 00:00:00 2001 From: B3n30 Date: Fri, 10 Nov 2017 19:37:26 +0100 Subject: [PATCH 6/6] Use Common::Event --- src/core/announce_multiplayer_session.cpp | 23 ++++++++--------------- src/core/announce_multiplayer_session.h | 9 ++------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/src/core/announce_multiplayer_session.cpp b/src/core/announce_multiplayer_session.cpp index af7d4fe58..b6a224ad6 100644 --- a/src/core/announce_multiplayer_session.cpp +++ b/src/core/announce_multiplayer_session.cpp @@ -19,7 +19,7 @@ namespace Core { // Time between room is announced to web_service static constexpr std::chrono::seconds announce_time_interval(15); -AnnounceMultiplayerSession::AnnounceMultiplayerSession() : announce(false) { +AnnounceMultiplayerSession::AnnounceMultiplayerSession() { #ifdef ENABLE_WEB_SERVICE backend = std::make_unique( Settings::values.announce_multiplayer_room_endpoint_url, Settings::values.citra_username, @@ -33,18 +33,14 @@ void AnnounceMultiplayerSession::Start() { if (announce_multiplayer_thread) { Stop(); } - + shutdown_event.Reset(); announce_multiplayer_thread = std::make_unique(&AnnounceMultiplayerSession::AnnounceMultiplayerLoop, this); } void AnnounceMultiplayerSession::Stop() { - if (!announce) - return; - announce = false; - // Detaching the loop, to not wait for the sleep to finish. The loop thread will finish soon. if (announce_multiplayer_thread) { - cv.notify_all(); + shutdown_event.Set(); announce_multiplayer_thread->join(); announce_multiplayer_thread.reset(); backend->Delete(); @@ -69,19 +65,16 @@ AnnounceMultiplayerSession::~AnnounceMultiplayerSession() { } void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() { - announce = true; + auto update_time = std::chrono::steady_clock::now(); std::future future; - while (announce) { - std::unique_lock lock(cv_m); - cv.wait_for(lock, announce_time_interval); + while (!shutdown_event.WaitUntil(update_time)) { + update_time += announce_time_interval; std::shared_ptr room = Network::GetRoom().lock(); if (!room) { - announce = false; - continue; + break; } if (room->GetState() != Network::Room::State::Open) { - announce = false; - continue; + break; } Network::RoomInformation room_information = room->GetRoomInformation(); std::vector memberlist = room->GetRoomMemberList(); diff --git a/src/core/announce_multiplayer_session.h b/src/core/announce_multiplayer_session.h index 6faea3ae3..0ea357e3a 100644 --- a/src/core/announce_multiplayer_session.h +++ b/src/core/announce_multiplayer_session.h @@ -4,8 +4,6 @@ #pragma once -#include -#include #include #include #include @@ -13,6 +11,7 @@ #include #include "common/announce_multiplayer_room.h" #include "common/common_types.h" +#include "common/thread.h" namespace Core { @@ -58,11 +57,7 @@ public: std::future GetRoomList(std::function func); private: - std::atomic announce{false}; - - /// conditional variable to notify the announce thread to end early - std::condition_variable cv; - std::mutex cv_m; ///< mutex for cv + Common::Event shutdown_event; std::mutex callback_mutex; std::set error_callbacks; std::unique_ptr announce_multiplayer_thread;