diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fbef083..110a5b7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -97,7 +97,10 @@ add_executable(MassBuilderSaveTool WIN32 Maps/WeaponTypes.hpp ToastQueue/ToastQueue.h ToastQueue/ToastQueue.cpp + UpdateChecker/UpdateChecker.h + UpdateChecker/UpdateChecker.cpp Utilities/Crc32.h + Version/Version.h FontAwesome/IconsFontAwesome5.h FontAwesome/IconsFontAwesome5Brands.h ${SAVETOOL_RC_FILE} diff --git a/src/SaveTool/SaveTool.cpp b/src/SaveTool/SaveTool.cpp index 6402768..2859392 100644 --- a/src/SaveTool/SaveTool.cpp +++ b/src/SaveTool/SaveTool.cpp @@ -29,8 +29,7 @@ #include -#include - +#include #include #include @@ -131,8 +130,6 @@ SaveTool::SaveTool(const Arguments& arguments): initialiseConfiguration(); - LOG_INFO("Initialising update checker."); - curl_global_init(CURL_GLOBAL_DEFAULT); if(conf().checkUpdatesOnStartup()) { _queue.addToast(Toast::Type::Default, "Checking for updates..."_s); _updateThread = std::thread{[this]{ checkForUpdates(); }}; @@ -155,9 +152,6 @@ SaveTool::SaveTool(const Arguments& arguments): SaveTool::~SaveTool() { LOG_INFO("Cleaning up."); - LOG_INFO("Shutting libcurl down."); - curl_global_cleanup(); - SDL_RemoveTimer(_gameCheckTimerId); LOG_INFO("Saving the configuration."); diff --git a/src/SaveTool/SaveTool.h b/src/SaveTool/SaveTool.h index 9e0a8c6..e1a3077 100644 --- a/src/SaveTool/SaveTool.h +++ b/src/SaveTool/SaveTool.h @@ -16,6 +16,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +#include #include #include @@ -40,6 +41,7 @@ #include "../ProfileManager/ProfileManager.h" #include "../MassManager/MassManager.h" #include "../ToastQueue/ToastQueue.h" +#include "../UpdateChecker/UpdateChecker.h" #ifdef SAVETOOL_DEBUG_BUILD #define tw CORRADE_TWEAKABLE @@ -83,11 +85,6 @@ class SaveTool: public Platform::Sdl2Application, public efsw::FileWatchListener }; void initEvent(SDL_Event& event); - enum UpdateCheckStatus : std::int32_t { - CurlInitFailed = 0, - CurlError = 1, - CurlTimeout = 2, - }; void updateCheckEvent(SDL_Event& event); enum FileEventType: std::int32_t { @@ -260,10 +257,8 @@ class SaveTool: public Platform::Sdl2Application, public efsw::FileWatchListener }; Containers::StaticArray<2, efsw::WatchID> _watchIDs; - bool _updateAvailable{false}; - Containers::String _latestVersion; - Containers::String _releaseLink; - Containers::String _downloadLink; + Containers::Optional _checker{Containers::NullOpt}; + std::mutex _checkerMutex; bool _modifiedBySaveTool{false}; bool _jointsDirty{false}; diff --git a/src/SaveTool/SaveTool_UpdateChecker.cpp b/src/SaveTool/SaveTool_UpdateChecker.cpp index a89a482..e5350b4 100644 --- a/src/SaveTool/SaveTool_UpdateChecker.cpp +++ b/src/SaveTool/SaveTool_UpdateChecker.cpp @@ -18,8 +18,6 @@ #include -#include - #include "../Logger/Logger.h" #include "SaveTool.h" @@ -28,108 +26,51 @@ void SaveTool::updateCheckEvent(SDL_Event& event) { _updateThread.join(); - if(event.user.code == CurlInitFailed) { - _queue.addToast(Toast::Type::Error, "Couldn't initialise libcurl. Update check aborted."_s); - LOG_ERROR("Couldn't initialise libcurl. Update check aborted."); - return; - } - else if(event.user.code == CurlError) { - Containers::String error{static_cast(event.user.data2), CURL_ERROR_SIZE, nullptr}; - _queue.addToast(Toast::Type::Error, error, std::chrono::milliseconds{5000}); - _queue.addToast(Toast::Type::Error, static_cast(event.user.data1), - std::chrono::milliseconds{5000}); - LOG_ERROR_FORMAT("{}: {}", static_cast(event.user.data1), static_cast(event.user.data2)); - return; - } - else if(event.user.code == CurlTimeout) { - _queue.addToast(Toast::Type::Error, "The request timed out."_s); - LOG_ERROR("The request timed out."); - return; - } - else if(event.user.code != 200) { - _queue.addToast(Toast::Type::Error, - Utility::format("The request failed with error code {}.", event.user.code)); - LOG_ERROR_FORMAT("The request failed with error code {}.", event.user.code); - return; - } + LOG_INFO("in updateCheckEvent()."); - struct Version { - explicit Version(Containers::StringView str) { - std::size_t start_point = 0; - - if(str[0] == 'v') { - start_point++; - } - - auto components = Containers::StringView{str.data() + start_point}.split('.'); - - major = std::strtol(components[0].data(), nullptr, 10); - minor = std::strtol(components[1].data(), nullptr, 10); - patch = std::strtol(components[2].data(), nullptr, 10); - - fullVersion = major * 10000 + minor * 100 + patch; - - if(str.hasSuffix("-pre")) { - prerelease = true; - } - } - std::int32_t fullVersion; - std::int32_t major = 0; - std::int32_t minor = 0; - std::int32_t patch = 0; - bool prerelease = false; - - bool operator==(const Version& other) const { - return fullVersion == other.fullVersion && prerelease == other.prerelease; - } - bool operator>(const Version& other) const { - if((fullVersion > other.fullVersion) || - (fullVersion == other.fullVersion && !prerelease && other.prerelease)) - { - return true; + switch(static_cast(event.user.code)) { + case UpdateChecker::Success: + _checkerMutex.lock(); + if(_checker->updateAvailable()) { + using namespace std::chrono_literals; + _queue.addToast(Toast::Type::Warning, + "Your version is out of date and thus unsupported.\nCheck the settings for more information."_s, 5s); } else { - return false; + if(_checker->version() == current_version || (current_version > _checker->version() && current_version.prerelease)) { + _queue.addToast(Toast::Type::Success, "The application is already up to date."_s); + } + else if(_checker->version() > current_version && !current_version.prerelease) { + _queue.addToast(Toast::Type::Warning, + "Your version is more recent than the latest one in the repo. How???"_s); + } } - } - explicit operator Containers::String() const { - return Utility::format("{}.{}.{}{}", major, minor, patch, prerelease ? "-pre" : ""); - } - }; - - static const Version current_ver{SAVETOOL_VERSION}; - - auto str = static_cast(event.user.data1); - Containers::String response{str, strlen(str), nullptr}; - auto components = response.splitOnAnyWithoutEmptyParts("\r\n"); - - Version latest_ver{components.front()}; - - if(latest_ver > current_ver) { - _queue.addToast(Toast::Type::Warning, - "Your version is out of date.\nCheck the settings for more information."_s, - std::chrono::milliseconds{5000}); - _updateAvailable = true; - _latestVersion = Containers::String{latest_ver}; - _releaseLink = Utility::format("https://williamjcm.ovh/git/williamjcm/MassBuilderSaveTool/releases/tag/v{}", - components.front()); - _downloadLink = components.back(); + _checkerMutex.unlock(); + break; + case UpdateChecker::HttpError: + _checkerMutex.lock(); + _queue.addToast(Toast::Type::Error, _checker->error()); + LOG_ERROR(_checker->error()); + _checkerMutex.unlock(); + break; + case UpdateChecker::CurlInitFailed: + _queue.addToast(Toast::Type::Error, "Couldn't initialise libcurl. Update check aborted."_s); + LOG_ERROR("Couldn't initialise libcurl. Update check aborted."); + break; + case UpdateChecker::CurlError: + { + using namespace std::chrono_literals; + _checkerMutex.lock(); + _queue.addToast(Toast::Type::Error, _checker->error(), 10s); + LOG_ERROR(_checker->error()); + _checkerMutex.unlock(); + } + break; + case UpdateChecker::CurlTimeout: + _queue.addToast(Toast::Type::Error, "The request timed out."_s); + LOG_ERROR("The request timed out."); + break; } - else if(latest_ver == current_ver || (current_ver > latest_ver && current_ver.prerelease)) { - _queue.addToast(Toast::Type::Success, "The application is already up to date."_s); - } - else if(current_ver > latest_ver && !current_ver.prerelease) { - _queue.addToast(Toast::Type::Warning, - "Your version is more recent than the latest one in the repo. How???"_s); - } -} - -inline -std::size_t -writeData(char* ptr, std::size_t size, std::size_t nmemb, Containers::String* buf) { - if(!ptr || !buf) return 0; - (*buf) = Utility::format("{}{}", *buf, Containers::StringView{ptr, size * nmemb}); - return size * nmemb; } void @@ -138,43 +79,15 @@ SaveTool::checkForUpdates() { SDL_zero(event); event.type = _updateEventId; - auto curl = curl_easy_init(); - if(!curl) { - event.user.code = CurlInitFailed; + _checkerMutex.lock(); + + if(!_checker) { + _checker.emplace(); } - if(curl) { - Containers::String response_body{Containers::AllocatedInit, ""}; - Containers::String error_buffer{ValueInit, CURL_ERROR_SIZE * 2}; + event.user.code = _checker->check(); - curl_easy_setopt(curl, CURLOPT_URL, "https://williamjcm.ovh/mbst/version"); - curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); - curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeData); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_body); - curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error_buffer.data()); - curl_easy_setopt(curl, CURLOPT_TCP_KEEPALIVE, 0L); - curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 10000L); - - auto code = curl_easy_perform(curl); - - if(code == CURLE_OK) { - long status = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status); - event.user.code = std::int32_t(status); - event.user.data1 = response_body.release(); - } - else if(code == CURLE_OPERATION_TIMEDOUT) { - event.user.code = CurlTimeout; - } - else { - event.user.code = CurlError; - event.user.data1 = const_cast(curl_easy_strerror(code)); - event.user.data2 = Containers::String{error_buffer}.release(); - } - - curl_easy_cleanup(curl); - } + _checkerMutex.unlock(); SDL_PushEvent(&event); } diff --git a/src/SaveTool/SaveTool_drawMainMenu.cpp b/src/SaveTool/SaveTool_drawMainMenu.cpp index fdc4d35..65185ca 100644 --- a/src/SaveTool/SaveTool_drawMainMenu.cpp +++ b/src/SaveTool/SaveTool_drawMainMenu.cpp @@ -132,15 +132,16 @@ SaveTool::drawMainMenu() { _updateThread = std::thread{[this]{ checkForUpdates(); }}; } - if(_updateAvailable) { - drawAlignedText("Version %s is available.", _latestVersion.data()); + if(_checker && (_checkerMutex.try_lock() && _checker->updateAvailable())) { + drawAlignedText("Version %s is available.", Containers::String{_checker->version()}.data()); if(ImGui::Button(ICON_FA_FILE_SIGNATURE " Release notes")) { - openUri(_releaseLink); + openUri("https://williamjcm.ovh/mbst"); } ImGui::SameLine(); if(ImGui::Button(ICON_FA_DOWNLOAD " Download now")) { - openUri(_downloadLink); + openUri(_checker->downloadLink()); } + _checkerMutex.unlock(); } if(drawCheckbox("Skip disclaimer", conf().skipDisclaimer())) { diff --git a/src/UpdateChecker/UpdateChecker.cpp b/src/UpdateChecker/UpdateChecker.cpp new file mode 100644 index 0000000..abd3cc4 --- /dev/null +++ b/src/UpdateChecker/UpdateChecker.cpp @@ -0,0 +1,112 @@ +// MassBuilderSaveTool +// Copyright (C) 2021-2023 Guillaume Jacquemin +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include +#include +#include +#include + +#include + +#include "../Logger/Logger.h" + +#include "UpdateChecker.h" + +using namespace Corrade; + +UpdateChecker::UpdateChecker() { + LOG_INFO("Initialising update checker."); + curl_global_init(CURL_GLOBAL_DEFAULT); +} + +UpdateChecker::~UpdateChecker() { + LOG_INFO("Shutting libcurl down."); + curl_global_cleanup(); +} + +UpdateChecker::Result +UpdateChecker::check() { + auto curl = curl_easy_init(); + if(!curl) { + return Result::CurlInitFailed; + } + + Containers::ScopeGuard guard(curl, curl_easy_cleanup); + + Containers::String response_body{Containers::AllocatedInit, ""}; + Containers::String error_buffer{ValueInit, CURL_ERROR_SIZE}; + static auto write_data = [](char* ptr, std::size_t size, std::size_t nmemb, Containers::String* buf){ + if(!ptr || !buf) return std::size_t{}; + (*buf) = Utility::format("{}{}", *buf, Containers::StringView{ptr, size * nmemb}); + return size * nmemb; + }; + + curl_easy_setopt(curl, CURLOPT_URL, "https://williamjcm.ovh/mbst/version"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_body); + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error_buffer.data()); + curl_easy_setopt(curl, CURLOPT_TCP_KEEPALIVE, 0L); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); + + auto code = curl_easy_perform(curl); + + if(code == CURLE_OK) { + long status = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status); + + if(status != 200) { + _error = Utility::format("The request failed with error code {}.", status); + return Result::HttpError; + } + + auto parts = response_body.splitOnAnyWithoutEmptyParts("\r\n"); + + _foundVersion = Version{parts.front()}; + _downloadLink = parts.back(); + LOG_INFO_FORMAT("Found version: {}", Containers::String{_foundVersion}); + + return Result::Success; + } + else if(code == CURLE_OPERATION_TIMEDOUT) { + return Result::CurlTimeout; + } + else { + _error = Utility::format("{}: {}", curl_easy_strerror(code), error_buffer); + return Result::CurlError; + } +} + +Containers::StringView +UpdateChecker::error() const { + return _error; +} + +bool +UpdateChecker::updateAvailable() const { + return _foundVersion > current_version; +} + +const Version& +UpdateChecker::version() const { + return _foundVersion; +} + +Containers::StringView +UpdateChecker::downloadLink() const { + return _downloadLink; +} diff --git a/src/UpdateChecker/UpdateChecker.h b/src/UpdateChecker/UpdateChecker.h new file mode 100644 index 0000000..f460010 --- /dev/null +++ b/src/UpdateChecker/UpdateChecker.h @@ -0,0 +1,49 @@ +// MassBuilderSaveTool +// Copyright (C) 2021-2023 Guillaume Jacquemin +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include "../Version/Version.h" + +class UpdateChecker { + public: + explicit UpdateChecker(); + ~UpdateChecker(); + + enum Result: std::int32_t { + Success, + HttpError, + CurlInitFailed, + CurlError, + CurlTimeout + }; + + auto check() -> Result; + + auto error() const -> Containers::StringView; + + bool updateAvailable() const; + + auto version() const -> const Version&; + + auto downloadLink() const -> Containers::StringView; + + private: + Containers::String _error; + + Version _foundVersion{}; + Containers::String _downloadLink; +}; diff --git a/src/Version/Version.h b/src/Version/Version.h new file mode 100644 index 0000000..fb1ad23 --- /dev/null +++ b/src/Version/Version.h @@ -0,0 +1,89 @@ +// MassBuilderSaveTool +// Copyright (C) 2021-2023 Guillaume Jacquemin +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include + +#include +#include +#include +#include + +using namespace Corrade; + +struct Version { + explicit Version() = default; + + explicit Version(Containers::StringView str) { + std::size_t start_point = 0; + + if(str[0] == 'v') { + start_point++; + } + + auto components = Containers::StringView{str.data() + start_point}.split('.'); + + major = std::strtol(components[0].data(), nullptr, 10); + minor = std::strtol(components[1].data(), nullptr, 10); + patch = std::strtol(components[2].data(), nullptr, 10); + + fullVersion = major * 10000 + minor * 100 + patch; + + if(str.hasSuffix("-pre")) { + prerelease = true; + } + } + + constexpr explicit Version(long major_, long minor_, long patch_, bool prerelease_): + major{major_}, minor{minor_}, patch{patch_}, prerelease{prerelease_}, + fullVersion{major_ * 10000 + minor_ * 100 + patch_} + { + //ctor + } + + long major = 0; + long minor = 0; + long patch = 0; + bool prerelease = false; + long fullVersion = 0; + + bool operator==(const Version& other) const { + return fullVersion == other.fullVersion && prerelease == other.prerelease; + } + + bool operator>(const Version& other) const { + if((fullVersion > other.fullVersion) || + (fullVersion == other.fullVersion && !prerelease && other.prerelease)) + { + return true; + } + else { + return false; + } + } + + explicit operator Containers::String() const { + return Utility::format("{}.{}.{}{}", major, minor, patch, prerelease ? "-pre" : ""); + } +}; + +constexpr Version current_version{ + SAVETOOL_VERSION_MAJOR, + SAVETOOL_VERSION_MINOR, + SAVETOOL_VERSION_PATCH, + SAVETOOL_VERSION_PRERELEASE +};