From 3d46c14a3de17c2a3c91bcbc5a56a334b5794dbd Mon Sep 17 00:00:00 2001 From: Michael Fabian 'Xaymar' Dirks Date: Sat, 11 Jun 2022 08:02:41 +0200 Subject: [PATCH] project: Add support for multi stage release cycles To ensure better stability of future releases, we need to adopt multiple stages in the release cycle. As we already label Alpha, Beta, Candidate and Stable differently, simply adopting this classification system already does everything for us. This also allows us to maintain compatibility with the existing system, while offering something new entirely. --- CMakeLists.txt | 204 ++++++++++----- data/locale/en-US.ini | 6 +- source/ui/ui-updater.cpp | 130 +++++----- source/ui/ui-updater.hpp | 14 +- source/updater.cpp | 517 +++++++++++++++++++++------------------ source/updater.hpp | 87 ++++--- templates/version.hpp.in | 3 +- 7 files changed, 549 insertions(+), 412 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d67eceae2d..d6061dd568 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,15 +50,88 @@ endif() # Versioning ################################################################################ -set(VERSION_MAJOR 0) -set(VERSION_MINOR 12) -set(VERSION_PATCH 0) -set(VERSION_TWEAK 0) -set(VERSION_SUFFIX "a1") +# Variables for Versioning +set(VERSION_MAJOR 0) # Major +set(VERSION_MINOR 0) # Minor +set(VERSION_PATCH 0) # Patch +set(VERSION_STAGE "") # Prefix for Tweak, if left empty will assume '.'. +set(VERSION_TWEAK 0) # Tweak set(VERSION_COMMIT "00000000") +# Final format will be [MAJOR].[MINOR].[PATCH]([TYPE][TWEAK])(-[COMMIT]) + +function(parse_version_string) + # Parses a version in the format A.B.C[.|a|b|rc]D-E + cmake_parse_arguments( + PARSE_ARGV 0 + _ARGS + "" + "INPUT;OUTPUT" + "" + ) + + set(_tmp_MAJOR 0) + set(_tmp_MINOR 0) + set(_tmp_PATCH 0) + set(_tmp_STAGE "") + set(_tmp_TWEAK "") + set(_tmp_COMMIT "") + + # Replace separators with list separators + string(REPLACE "-" "." _tmp "${_ARGS_INPUT}") + string(REPLACE "." ";" _tmp "${_tmp}") + + # Parse version differently depending on total string length. + list(LENGTH _tmp _tmp_len) + if(_tmp_len GREATER_EQUAL 1) # A[...] + list(GET _tmp 0 _tmp_MAJOR) + endif() + if(_tmp_len GREATER_EQUAL 2) # A.B[...] + list(GET _tmp 1 _tmp_MINOR) + endif() + if(_tmp_len GREATER_EQUAL 3) # A.B.C[...] + list(GET _tmp 2 _tmp_PATCH) + endif() + if(_tmp_len EQUAL 4) # A.B.C.D or A.B.C-gEEEEEEEE + list(GET _tmp 3 _tmp2) + # No support for '{N,M}' in CMake-Regex! + if(_tmp2 MATCHES "^g[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]") + # A.B.C-gEEEEEEEE + set(_tmp_COMMIT "${_tmp2}") + else() + # It is A.B.C.D + set(_tmp_TWEAK "${_tmp2}") + set(_tmp_STAGE "a") + endif() + elseif(_tmp_len EQUAL 5) # A.B.C.D-gEEEEEEEE + set(_tmp_STAGE "a") + list(GET _tmp 3 _tmp_TWEAK) + list(GET _tmp 4 _tmp_COMMIT) + endif() + if(_tmp_TWEAK STREQUAL "") # Is A.B.C-gEEEEEEEE actually A.B.CxD-gEEEEEEEE? + string(REGEX MATCHALL "^([0-9]+)([_a-c]+)([0-9]+)" T_MATCHES "${_tmp_PATCH}") + if(T_MATCHES) + set(_tmp_PATCH ${CMAKE_MATCH_1}) + set(_tmp_STAGE ${CMAKE_MATCH_2}) + set(_tmp_TWEAK ${CMAKE_MATCH_3}) + endif() + endif() + + set(${_ARGS_OUTPUT}_MAJOR ${_tmp_MAJOR} PARENT_SCOPE) + set(${_ARGS_OUTPUT}_MINOR ${_tmp_MINOR} PARENT_SCOPE) + set(${_ARGS_OUTPUT}_PATCH ${_tmp_PATCH} PARENT_SCOPE) + set(${_ARGS_OUTPUT}_STAGE ${_tmp_STAGE} PARENT_SCOPE) + set(${_ARGS_OUTPUT}_TWEAK ${_tmp_TWEAK} PARENT_SCOPE) + set(${_ARGS_OUTPUT}_COMMIT ${_tmp_COMMIT} PARENT_SCOPE) +endfunction() # Check if we are in a git repository. if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/.git") + set(VERSION_BASE "0.11.0") # Automatic versioning base + set(VERSION_TARGET "0.12.0") # Automatic versioning target + + # Parse target version as it is for output. + parse_version_string(OUTPUT "VERSION" INPUT "${VERSION_TARGET}") + # Try and figure out where git is. find_program(GIT git PATHS @@ -69,12 +142,9 @@ if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/.git") ) if(GIT) - set(GIT_RESULT) - set(GIT_OUTPUT) - set(GIT_ERROR) - + # Tweak execute_process( - COMMAND "${GIT}" describe --tags --long --match "[0-9]*.[0-9]*.[0-9]*" --abbrev=8 HEAD + COMMAND "${GIT}" describe --tags --long --match "${VERSION_BASE}" --abbrev=8 HEAD WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} RESULT_VARIABLE GIT_RESULT OUTPUT_VARIABLE GIT_OUTPUT @@ -83,29 +153,34 @@ if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/.git") ERROR_STRIP_TRAILING_WHITESPACE ERROR_QUIET ) - - if(GIT_RESULT EQUAL 0) - string(REPLACE "-" "." GIT_OUTPUT "${GIT_OUTPUT}") - string(REPLACE "." ";" GIT_OUTPUT "${GIT_OUTPUT}") - - # Parse Version - list(GET GIT_OUTPUT 0 VERSION_MAJOR) - list(GET GIT_OUTPUT 1 VERSION_MINOR) - list(GET GIT_OUTPUT 2 VERSION_PATCH) - list(GET GIT_OUTPUT 3 VERSION_TWEAK) - list(GET GIT_OUTPUT 4 VERSION_COMMIT) - - # Patch needs additional parsing. - # This may be a [0-9]*[a-z]*[0-9]+ string. - string(REGEX MATCHALL "^([0-9]+)([a-z]+[0-9]+)?" T_MATCHES "${VERSION_PATCH}") - set(VERSION_PATCH "${CMAKE_MATCH_1}") - if(CMAKE_MATCH_2) - set(VERSION_SUFFIX "${CMAKE_MATCH_2}") - else() - set(VERSION_SUFFIX "") - endif() - else() + if(NOT GIT_RESULT EQUAL 0) message(WARNING "${LOGPREFIX}Failed to detect version, using default instead.") + else() + parse_version_string(OUTPUT "GIT_VERSION" INPUT "${GIT_OUTPUT}") + set(VERSION_STAGE ${GIT_VERSION_STAGE}) + set(VERSION_TWEAK ${GIT_VERSION_TWEAK}) + set(VERSION_COMMIT ${GIT_VERSION_COMMIT}) + endif() + + # Is there a tag on the current commit? + execute_process( + COMMAND "${GIT}" tag "--sort=-v:refname" "--points-at" HEAD + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} + RESULT_VARIABLE GIT_RESULT + OUTPUT_VARIABLE TAG_OUTPUT + ERROR_VARIABLE GIT_ERROR + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + if((GIT_RESULT EQUAL 0) AND (NOT "${TAG_OUTPUT}" STREQUAL "")) + string(REGEX REPLACE "[\r\n]+.*" "" T_MATCHES "${TAG_OUTPUT}") + parse_version_string(OUTPUT "TAG_VERSION" INPUT "${T_MATCHES}") + set(VERSION_MAJOR "${TAG_VERSION_MAJOR}") + set(VERSION_MINOR "${TAG_VERSION_MINOR}") + set(VERSION_PATCH "${TAG_VERSION_PATCH}") + set(VERSION_STAGE "${TAG_VERSION_STAGE}") + set(VERSION_TWEAK "${TAG_VERSION_TWEAK}") endif() endif() else() @@ -113,42 +188,16 @@ else() endif() # Allow manual overrides of the detected version. -set(${PREFIX}VERSION "" CACHE STRING "Override StreamFX version with this string. Format: Major.Minor.Patch[Suffix][-Tweak[-Commit8c]]") +set(${PREFIX}VERSION "" CACHE STRING "Override StreamFX version with this string. Format: Major.Minor.Patch[Stage][Tweak[-Commit8c]]") if(NOT (${PREFIX}VERSION STREQUAL "")) - string(REPLACE "-" "." T_VERSION "${${PREFIX}VERSION}") - string(REPLACE "." ";" T_VERSION "${${PREFIX}VERSION}") - - list(LENGTH T_VERSION T_VERSIONLEN) - list(GET T_VERSION 0 VERSION_MAJOR) - list(GET T_VERSION 1 VERSION_MINOR) - list(GET T_VERSION 2 VERSION_PATCH) - if(T_VERSIONLEN GREATER_EQUAL 3) - list(GET T_VERSION 3 VERSION_TWEAK) - else() - set(VERSION_BUILD 0) - endif() - if(T_VERSIONLEN GREATER_EQUAL 4) - list(GET T_VERSION 4 VERSION_COMMIT) - else() - set(VERSION_COMMIT "") - endif() - - # Patch needs additional parsing. - # This may be a [0-9]*[a-z]*[0-9]+ string. - string(REGEX MATCHALL "^([0-9]+)([a-z]+[0-9]+)?" T_MATCHES "${VERSION_PATCH}") - set(VERSION_PATCH "${CMAKE_MATCH_1}") - if(CMAKE_MATCH_2) - set(VERSION_SUFFIX "${CMAKE_MATCH_2}") - else() - set(VERSION_SUFFIX "") - endif() + parse_version_string(OUTPUT "VERSION" INPUT "${${PREFIX}VERSION}") endif() # Generate Version String -if(NOT (VERSION_COMMIT STREQUAL "")) - set(VERSION_STRING "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}.${VERSION_TWEAK}${VERSION_SUFFIX}-${VERSION_COMMIT}") +if(VERSION_COMMIT) + set(VERSION_STRING "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}${VERSION_STAGE}${VERSION_TWEAK}-${VERSION_COMMIT}") else() - set(VERSION_STRING "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}.${VERSION_TWEAK}${VERSION_SUFFIX}") + set(VERSION_STRING "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}${VERSION_STAGE}${VERSION_TWEAK}") endif() # Log the detected version. @@ -157,13 +206,40 @@ message(STATUS "${LOGPREFIX}Version ${VERSION_STRING}") ################################################################################ # Project ################################################################################ +set(_VERSION_TWEAK "0${VERSION_TWEAK}") project( StreamFX - VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}.${VERSION_TWEAK} + VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}.${_VERSION_TWEAK} DESCRIPTION "Additional sources, filters, transitions and encoders for OBS Studio." HOMEPAGE_URL "https://streamfx.xaymar.com/" ) +# Helpers for CI +if(VERSION_STAGE STREQUAL "") + file( + GENERATE + OUTPUT "$/tag_name" + CONTENT "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}" + TARGET ${PROJECT_NAME} + FILE_PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_READ GROUP_EXECUTE + ) +else() + file( + GENERATE + OUTPUT "$/tag_name" + CONTENT "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}${VERSION_STAGE}${VERSION_TWEAK}" + TARGET ${PROJECT_NAME} + FILE_PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_READ GROUP_EXECUTE + ) +endif() +file( + GENERATE + OUTPUT "$/version" + CONTENT "${VERSION_STRING}" + TARGET ${PROJECT_NAME} + FILE_PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_READ GROUP_EXECUTE +) + # Full Project Name set(PROJECT_FULL_NAME "StreamFX (for OBS Studio)") diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index aaa7b062dd..9cb5a8f007 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -59,8 +59,10 @@ UI.Updater.GitHubPermission.Text="In order to provide manual or automated update UI.Updater.Menu.CheckForUpdates="Check for Updates" UI.Updater.Menu.CheckForUpdates.Automatically="Automatically check for Updates" UI.Updater.Menu.Channel="Update Channel" -UI.Updater.Menu.Channel.Release="Release" -UI.Updater.Menu.Channel.Testing="Testing" +UI.Updater.Menu.Channel.Stable="Stable" +UI.Updater.Menu.Channel.Candidate="Candidate" +UI.Updater.Menu.Channel.Beta="Beta" +UI.Updater.Menu.Channel.Alpha="Alpha" # Encoder/AOM-AV1 Encoder.AOM.AV1="AOM AV1 (direct)" diff --git a/source/ui/ui-updater.cpp b/source/ui/ui-updater.cpp index 1d9499d339..11c2ccf8b1 100644 --- a/source/ui/ui-updater.cpp +++ b/source/ui/ui-updater.cpp @@ -39,8 +39,10 @@ #define D_I18N_MENU_CHECKFORUPDATES "UI.Updater.Menu.CheckForUpdates" #define D_I18N_MENU_CHECKFORUPDATES_AUTOMATICALLY "UI.Updater.Menu.CheckForUpdates.Automatically" #define D_I18N_MENU_CHANNEL "UI.Updater.Menu.Channel" -#define D_I18N_MENU_CHANNEL_RELEASE "UI.Updater.Menu.Channel.Release" -#define D_I18N_MENU_CHANNEL_TESTING "UI.Updater.Menu.Channel.Testing" +#define D_I18N_MENU_CHANNEL_STABLE "UI.Updater.Menu.Channel.Stable" +#define D_I18N_MENU_CHANNEL_CANDIDATE "UI.Updater.Menu.Channel.Candidate" +#define D_I18N_MENU_CHANNEL_BETA "UI.Updater.Menu.Channel.Beta" +#define D_I18N_MENU_CHANNEL_ALPHA "UI.Updater.Menu.Channel.Alpha" #define D_I18N_DIALOG_TITLE "UI.Updater.Dialog.Title" #define D_I18N_GITHUBPERMISSION_TITLE "UI.Updater.GitHubPermission.Title" #define D_I18N_GITHUBPERMISSION_TEXT "UI.Updater.GitHubPermission.Text" @@ -51,6 +53,7 @@ streamfx::ui::updater_dialog::updater_dialog() : QDialog(reinterpret_cast buf; - if (current.version_type) { - buf.resize(static_cast(snprintf(nullptr, 0, "%" PRIu16 ".%" PRIu16 ".%" PRIu16 "%.1s%" PRIu16, - current.version_major, current.version_minor, current.version_patch, - ¤t.version_type, current.version_index)) - + 1); - snprintf(buf.data(), buf.size(), "%" PRIu16 ".%" PRIu16 ".%" PRIu16 "%.1s%" PRIu16, current.version_major, - current.version_minor, current.version_patch, ¤t.version_type, current.version_index); - } else { - buf.resize( - static_cast(snprintf(nullptr, 0, "%" PRIu16 ".%" PRIu16 ".%" PRIu16, current.version_major, - current.version_minor, current.version_patch)) - + 1); - snprintf(buf.data(), buf.size(), "%" PRIu16 ".%" PRIu16 ".%" PRIu16, current.version_major, - current.version_minor, current.version_patch); - } - currentVersion->setText(QString::fromUtf8(buf.data())); - } + currentVersion->setText(QString::fromStdString(static_cast(current))); + latestVersion->setText(QString::fromStdString(static_cast(update))); { - std::vector buf; - if (update.version_type) { - buf.resize(static_cast(snprintf(nullptr, 0, "%" PRIu16 ".%" PRIu16 ".%" PRIu16 "%.1s%" PRIu16, - update.version_major, update.version_minor, update.version_patch, - &update.version_type, update.version_index)) - + 1); - snprintf(buf.data(), buf.size(), "%" PRIu16 ".%" PRIu16 ".%" PRIu16 "%.1s%" PRIu16, update.version_major, - update.version_minor, update.version_patch, &update.version_type, update.version_index); - } else { - buf.resize(static_cast(snprintf(nullptr, 0, "%" PRIu16 ".%" PRIu16 ".%" PRIu16, - update.version_major, update.version_minor, update.version_patch)) - + 1); - snprintf(buf.data(), buf.size(), "%" PRIu16 ".%" PRIu16 ".%" PRIu16, update.version_major, - update.version_minor, update.version_patch); - } - latestVersion->setText(QString::fromUtf8(buf.data())); - - { - std::vector buf2; - buf2.resize(static_cast(snprintf(nullptr, 0, D_TRANSLATE(D_I18N_DIALOG_TITLE), buf.data())) + 1); - snprintf(buf2.data(), buf2.size(), D_TRANSLATE(D_I18N_DIALOG_TITLE), buf.data()); - setWindowTitle(QString::fromUtf8(buf2.data())); - } + std::string buf = latestVersion->text().toStdString(); + std::vector buf2; + buf2.resize(static_cast(snprintf(nullptr, 0, D_TRANSLATE(D_I18N_DIALOG_TITLE), buf.data())) + 1); + snprintf(buf2.data(), buf2.size(), D_TRANSLATE(D_I18N_DIALOG_TITLE), buf.data()); + setWindowTitle(QString::fromUtf8(buf2.data())); } _update_url = QUrl(QString::fromStdString(update.url)); @@ -122,16 +90,19 @@ void streamfx::ui::updater_dialog::on_ok() { QDesktopServices::openUrl(_update_url); hide(); + accept(); } void streamfx::ui::updater_dialog::on_cancel() { hide(); + reject(); } streamfx::ui::updater::updater(QMenu* menu) : _updater(), _dialog(nullptr), _gdpr(nullptr), _cfu(nullptr), _cfu_auto(nullptr), _channel(nullptr), - _channel_menu(nullptr), _channel_stable(nullptr), _channel_preview(nullptr), _channel_group(nullptr) + _channel_menu(nullptr), _channel_stable(nullptr), _channel_candidate(nullptr), _channel_beta(nullptr), + _channel_alpha(nullptr), _channel_group(nullptr) { // Create dialog. _dialog = new updater_dialog(); @@ -154,17 +125,27 @@ streamfx::ui::updater::updater(QMenu* menu) _channel_menu = menu->addMenu(QString::fromUtf8(D_TRANSLATE(D_I18N_MENU_CHANNEL))); _channel_menu->menuAction()->setMenuRole(QAction::NoRole); - _channel_stable = _channel_menu->addAction(QString::fromUtf8(D_TRANSLATE(D_I18N_MENU_CHANNEL_RELEASE))); + _channel_stable = _channel_menu->addAction(QString::fromUtf8(D_TRANSLATE(D_I18N_MENU_CHANNEL_STABLE))); _channel_stable->setMenuRole(QAction::NoRole); _channel_stable->setCheckable(true); - _channel_preview = _channel_menu->addAction(QString::fromUtf8(D_TRANSLATE(D_I18N_MENU_CHANNEL_TESTING))); - _channel_preview->setMenuRole(QAction::NoRole); - _channel_preview->setCheckable(true); + _channel_candidate = _channel_menu->addAction(QString::fromUtf8(D_TRANSLATE(D_I18N_MENU_CHANNEL_CANDIDATE))); + _channel_candidate->setMenuRole(QAction::NoRole); + _channel_candidate->setCheckable(true); + + _channel_beta = _channel_menu->addAction(QString::fromUtf8(D_TRANSLATE(D_I18N_MENU_CHANNEL_BETA))); + _channel_beta->setMenuRole(QAction::NoRole); + _channel_beta->setCheckable(true); + + _channel_alpha = _channel_menu->addAction(QString::fromUtf8(D_TRANSLATE(D_I18N_MENU_CHANNEL_ALPHA))); + _channel_alpha->setMenuRole(QAction::NoRole); + _channel_alpha->setCheckable(true); _channel_group = new QActionGroup(_channel_menu); _channel_group->addAction(_channel_stable); - _channel_group->addAction(_channel_preview); + _channel_group->addAction(_channel_candidate); + _channel_group->addAction(_channel_beta); + _channel_group->addAction(_channel_alpha); connect(_channel_group, &QActionGroup::triggered, this, &streamfx::ui::updater::on_channel_group_triggered); } @@ -188,8 +169,8 @@ streamfx::ui::updater::updater(QMenu* menu) std::bind(&streamfx::ui::updater::on_updater_refreshed, this, std::placeholders::_1)); // Sync with updater information. - emit autoupdate_changed(_updater->automation()); - emit channel_changed(_updater->channel()); + emit autoupdate_changed(_updater->is_automated()); + emit channel_changed(_updater->get_channel()); } } @@ -200,7 +181,7 @@ void streamfx::ui::updater::on_updater_automation_changed(streamfx::updater&, bo emit autoupdate_changed(value); } -void streamfx::ui::updater::on_updater_channel_changed(streamfx::updater&, streamfx::update_channel channel) +void streamfx::ui::updater::on_updater_channel_changed(streamfx::updater&, streamfx::version_stage channel) { emit channel_changed(channel); } @@ -209,7 +190,7 @@ void streamfx::ui::updater::on_updater_refreshed(streamfx::updater&) { emit check_active(false); - if (!_updater->have_update()) + if (!_updater->is_update_available()) return; emit update_detected(); @@ -217,8 +198,8 @@ void streamfx::ui::updater::on_updater_refreshed(streamfx::updater&) void streamfx::ui::updater::obs_ready() { - if (_updater->automation()) { - if (_updater->gdpr()) { + if (_updater->is_automated()) { + if (_updater->is_data_sharing_allowed()) { _updater->refresh(); } else { create_gdpr_box(); @@ -227,11 +208,17 @@ void streamfx::ui::updater::obs_ready() } } -void streamfx::ui::updater::on_channel_changed(streamfx::update_channel channel) +void streamfx::ui::updater::on_channel_changed(streamfx::version_stage ch) { - bool is_stable = channel == streamfx::update_channel::RELEASE; - _channel_stable->setChecked(is_stable); - _channel_preview->setChecked(!is_stable); + QSignalBlocker bgroup(_channel_group); + QSignalBlocker bs(_channel_stable); + QSignalBlocker bc(_channel_candidate); + QSignalBlocker bb(_channel_beta); + QSignalBlocker ba(_channel_alpha); + _channel_stable->setChecked(ch == streamfx::version_stage::STABLE); + _channel_candidate->setChecked(ch == streamfx::version_stage::CANDIDATE); + _channel_beta->setChecked(ch == streamfx::version_stage::BETA); + _channel_alpha->setChecked(ch == streamfx::version_stage::ALPHA); } void streamfx::ui::updater::on_update_detected() @@ -241,24 +228,25 @@ void streamfx::ui::updater::on_update_detected() void streamfx::ui::updater::on_autoupdate_changed(bool enabled) { + QSignalBlocker blocker(_cfu_auto); _cfu_auto->setChecked(enabled); } void streamfx::ui::updater::on_gdpr_button(QAbstractButton* btn) { if (_gdpr->standardButton(btn) == QMessageBox::Ok) { - _updater->set_gdpr(true); + _updater->set_data_sharing_allowed(true); emit check_active(true); _updater->refresh(); } else { - _updater->set_gdpr(false); + _updater->set_data_sharing_allowed(false); _updater->set_automation(false); } } void streamfx::ui::updater::on_cfu_triggered(bool) { - if (!_updater->gdpr()) { + if (!_updater->is_data_sharing_allowed()) { create_gdpr_box(); _gdpr->exec(); } else { @@ -274,10 +262,14 @@ void streamfx::ui::updater::on_cfu_auto_toggled(bool flag) void streamfx::ui::updater::on_channel_group_triggered(QAction* action) { - if (action == _channel_stable) { - _updater->set_channel(update_channel::RELEASE); + if (action == _channel_alpha) { + _updater->set_channel(streamfx::version_stage::ALPHA); + } else if (action == _channel_beta) { + _updater->set_channel(streamfx::version_stage::BETA); + } else if (action == _channel_candidate) { + _updater->set_channel(streamfx::version_stage::CANDIDATE); } else { - _updater->set_channel(update_channel::TESTING); + _updater->set_channel(streamfx::version_stage::STABLE); } } @@ -300,7 +292,9 @@ void streamfx::ui::updater::on_check_active(bool active) { _cfu->setEnabled(!active); _channel_group->setEnabled(!active); - _channel_preview->setEnabled(!active); + _channel_alpha->setEnabled(!active); + _channel_beta->setEnabled(!active); + _channel_candidate->setEnabled(!active); _channel_stable->setEnabled(!active); _channel_menu->setEnabled(!active); } diff --git a/source/ui/ui-updater.hpp b/source/ui/ui-updater.hpp index 77cefb35d7..9d60077258 100644 --- a/source/ui/ui-updater.hpp +++ b/source/ui/ui-updater.hpp @@ -36,7 +36,7 @@ #include #include "ui_updater.h" -Q_DECLARE_METATYPE(streamfx::update_channel); +Q_DECLARE_METATYPE(::streamfx::version_stage); #ifdef _MSC_VER #pragma warning(pop) @@ -53,7 +53,7 @@ namespace streamfx::ui { updater_dialog(); ~updater_dialog(); - void show(streamfx::update_info current, streamfx::update_info update); + void show(streamfx::version_info current, streamfx::version_info update); void hide(); public slots: @@ -77,7 +77,9 @@ namespace streamfx::ui { QAction* _channel; QMenu* _channel_menu; QAction* _channel_stable; - QAction* _channel_preview; + QAction* _channel_candidate; + QAction* _channel_beta; + QAction* _channel_alpha; QActionGroup* _channel_group; public: @@ -87,7 +89,7 @@ namespace streamfx::ui { void create_gdpr_box(); void on_updater_automation_changed(streamfx::updater&, bool); - void on_updater_channel_changed(streamfx::updater&, streamfx::update_channel); + void on_updater_channel_changed(streamfx::updater&, streamfx::version_stage); void on_updater_refreshed(streamfx::updater&); void obs_ready(); @@ -96,7 +98,7 @@ namespace streamfx::ui { ; // Needed by some linters. void autoupdate_changed(bool); - void channel_changed(streamfx::update_channel); + void channel_changed(streamfx::version_stage); void update_detected(); void check_active(bool); @@ -105,7 +107,7 @@ namespace streamfx::ui { // Internal void on_autoupdate_changed(bool); - void on_channel_changed(streamfx::update_channel); + void on_channel_changed(streamfx::version_stage); void on_update_detected(); void on_check_active(bool); diff --git a/source/updater.cpp b/source/updater.cpp index c63e810fae..d0e1d5864e 100644 --- a/source/updater.cpp +++ b/source/updater.cpp @@ -20,7 +20,9 @@ #include "updater.hpp" #include "version.hpp" +#include #include +#include #include #include "configuration.hpp" #include "plugin.hpp" @@ -45,179 +47,308 @@ // - Move 'autoupdater.last_checked_at' to out of the configuration. // - Figure out if nightly updates are viable at all. -#define ST_CFG_GDPR "updater.gdpr" +#define ST_CFG_DATASHARING "updater.datasharing" #define ST_CFG_AUTOMATION "updater.automation" #define ST_CFG_CHANNEL "updater.channel" #define ST_CFG_LASTCHECKEDAT "updater.lastcheckedat" -void streamfx::to_json(nlohmann::json& json, const update_info& info) +streamfx::version_stage streamfx::stage_from_string(std::string_view str) +{ + if (str == "a") { + return version_stage::ALPHA; + } else if (str == "b") { + return version_stage::BETA; + } else if (str == "c") { + return version_stage::CANDIDATE; + } else { + return version_stage::STABLE; + } +} + +std::string_view streamfx::stage_to_string(version_stage t) +{ + switch (t) { + case version_stage::ALPHA: + return "a"; + case version_stage::BETA: + return "b"; + case version_stage::CANDIDATE: + return "c"; + default: + case version_stage::STABLE: + return "."; + } +} + +void streamfx::to_json(nlohmann::json& json, const version_stage& stage) +{ + json = stage_to_string(stage); +} + +void streamfx::from_json(const nlohmann::json& json, version_stage& stage) +{ + stage = stage_from_string(json.get()); +} + +streamfx::version_info::version_info() + : major(0), minor(0), patch(0), tweak(0), stage(version_stage::STABLE), url(""), name("") +{} + +streamfx::version_info::version_info(const std::string& text) : version_info() +{ + // text can be: + // 0.0.0 (Stable) + // 0.0.0a0 (Testing) + // 0.0.0b0 (Testing) + // 0.0.0c0 (Testing) + // 0.0.0_0 (Development) + static const std::regex re_version( + "([0-9]+)\\.([0-9]+)\\.([0-9]+)(([\\._abc]{1,1})([0-9]+|)|)(-g([0-9a-fA-F]{8,8})|)"); + std::smatch matches; + if (std::regex_match(text, matches, re_version, + std::regex_constants::match_any | std::regex_constants::match_continuous)) { + major = static_cast(strtoul(matches[1].str().c_str(), nullptr, 10)); + minor = static_cast(strtoul(matches[2].str().c_str(), nullptr, 10)); + patch = static_cast(strtoul(matches[3].str().c_str(), nullptr, 10)); + if (matches.size() >= 5) { + stage = stage_from_string(matches[5].str()); + tweak = static_cast(strtoul(matches[6].str().c_str(), nullptr, 10)); + } + } else { + throw std::invalid_argument("Provided string is not a version."); + } +} + +void streamfx::to_json(nlohmann::json& json, const version_info& info) { auto version = nlohmann::json::object(); - version["major"] = info.version_major; - version["minor"] = info.version_minor; - version["patch"] = info.version_patch; - if (info.version_type) - version["alpha"] = info.version_type == 'a' ? true : false; - version["index"] = info.version_index; + version["major"] = info.major; + version["minor"] = info.minor; + version["patch"] = info.patch; + version["type"] = info.stage; + version["tweak"] = info.tweak; json["version"] = version; - json["preview"] = info.channel == update_channel::TESTING; json["url"] = info.url; json["name"] = info.name; } -void streamfx::from_json(const nlohmann::json& json, update_info& info) +void streamfx::from_json(const nlohmann::json& json, version_info& info) { - if (json.find("html_url") != json.end()) { + if (json.find("html_url") == json.end()) { + auto version = json.at("version"); + info.major = version.at("major").get(); + info.minor = version.at("minor").get(); + info.patch = version.at("patch").get(); + info.stage = version.at("type"); + info.tweak = version.at("tweak").get(); + info.url = json.at("url").get(); + info.name = json.at("name").get(); + } else { // This is a response from GitHub. // Retrieve entries from the release object. auto entry_tag_name = json.find("tag_name"); auto entry_name = json.find("name"); auto entry_url = json.find("html_url"); - auto entry_preview = json.find("prerelease"); - if ((entry_tag_name == json.end()) || (entry_name == json.end()) || (entry_url == json.end()) - || (entry_preview == json.end())) { + if ((entry_tag_name == json.end()) || (entry_name == json.end()) || (entry_url == json.end())) { throw std::runtime_error("JSON is missing one or more required keys."); } - // Initialize update information. - info.channel = entry_preview->get() ? update_channel::TESTING : update_channel::RELEASE; - info.url = entry_url->get(); - info.name = entry_name->get(); - - // Parse the tag name as SemVer (A.B.C) - { - std::string tag_name = entry_tag_name->get(); - - size_t dot_1 = tag_name.find_first_of(".", 0) + 1; - info.version_major = static_cast(strtoul(&tag_name.at(0), nullptr, 10)); - if (dot_1 < tag_name.size()) { - info.version_minor = static_cast(strtoul(&tag_name.at(dot_1), nullptr, 10)); - } - - char* endptr = nullptr; - size_t dot_2 = tag_name.find_first_of(".", dot_1) + 1; - if (dot_2 < tag_name.size()) { - info.version_patch = static_cast(strtoul(&tag_name.at(dot_2), &endptr, 10)); - } - - // Check if there's data following the SemVer structure. (A.B.CdE) - if ((endptr != nullptr) && (endptr < (tag_name.data() + tag_name.size()))) { - size_t last_num = static_cast(endptr - tag_name.data()) + 1; - info.version_type = *endptr; - if (last_num < tag_name.size()) - info.version_index = static_cast(strtoul(&tag_name.at(last_num), nullptr, 10)); - } else { - info.version_type = 0; - info.version_index = 0; - } - } - } else { - auto version = json.at("version"); - info.version_major = version.at("major").get(); - info.version_minor = version.at("minor").get(); - info.version_patch = version.at("patch").get(); - if (version.find("type") != version.end()) - info.version_type = version.at("alpha").get() ? 'a' : 'b'; - info.version_index = version.at("index").get(); - info.channel = json.at("preview").get() ? update_channel::TESTING : update_channel::RELEASE; - info.url = json.at("url").get(); - info.name = json.at("name").get(); + // Parse the information. + std::string tag_name = entry_tag_name->get(); + info = {tag_name}; + info.url = entry_url->get(); + info.name = entry_name->get(); } } -bool streamfx::update_info::is_newer(update_info& other) +bool streamfx::version_info::is_older_than(version_info& other) { + // 'true' if other is newer, otherwise false. + // 1. Compare Major version: // A. Ours is greater: Remote is older. // B. Theirs is greater: Remote is newer. // C. Continue the check. - if (version_major > other.version_major) + if (major > other.major) return false; - if (version_major < other.version_major) + if (major < other.major) return true; // 2. Compare Minor version: // A. Ours is greater: Remote is older. // B. Theirs is greater: Remote is newer. // C. Continue the check. - if (version_minor > other.version_minor) + if (minor > other.minor) return false; - if (version_minor < other.version_minor) + if (minor < other.minor) return true; // 3. Compare Patch version: // A. Ours is greater: Remote is older. // B. Theirs is greater: Remote is newer. // C. Continue the check. - if (version_patch > other.version_patch) + if (patch > other.patch) return false; - if (version_patch < other.version_patch) + if (patch < other.patch) return true; // 4. Compare Type: - // A. Ours empty: Remote is older. - // B. Theirs empty: Remote is newer. + // A. Outs is smaller: Remote is older. + // B. Theirs is smaller: Remote is newer. // C. Continue the check. - // A automatically implies that ours is not empty for B. A&B combined imply that both are not empty for step 5. - if (version_type == 0) + if (stage < other.stage) return false; - if (other.version_type == 0) + if (stage > other.stage) return true; - // 5. Compare Type value: - // A. Ours is greater: Remote is older. - // B. Theirs is greater: Remote is newer. - // C. Continue the check. - if (version_type > other.version_type) - return false; - if (version_type < other.version_type) - return true; - - // 6. Compare Index: + // 5. Compare Tweak: // A. Ours is greater or equal: Remote is older or identical. // B. Remote is newer - if (version_index >= other.version_index) + if (tweak >= other.tweak) return false; return true; } +streamfx::version_info::operator std::string() +{ + std::vector buffer(25, 0); + if (stage != version_stage::STABLE) { + auto types = stage_to_string(stage); + size_t len = snprintf(buffer.data(), buffer.size(), "%" PRIu16 ".%" PRIu16 ".%" PRIu16 "%.1s%" PRIu16, major, + minor, patch, types.data(), tweak); + return std::string(buffer.data(), buffer.data() + len); + } else { + size_t len = snprintf(buffer.data(), buffer.size(), "%" PRIu16 ".%" PRIu16 ".%" PRIu16, major, minor, patch); + return std::string(buffer.data(), buffer.data() + len); + } +} + void streamfx::updater::task(streamfx::util::threadpool_data_t) try { - { - std::vector buffer; - task_query(buffer); - task_parse(buffer); - } + auto query_fn = [this](std::vector& buffer) { + static constexpr std::string_view ST_API_URL = + "https://api.github.com/repos/Xaymar/obs-StreamFX/releases?per_page=25&page=1"; + + streamfx::util::curl curl; + size_t buffer_offset = 0; + + // Set headers (User-Agent is needed so Github can contact us!). + curl.set_header("User-Agent", "StreamFX Updater v" STREAMFX_VERSION_STRING); + curl.set_header("Accept", "application/vnd.github.v3+json"); + + // Set up request. + curl.set_option(CURLOPT_HTTPGET, true); // GET + curl.set_option(CURLOPT_POST, false); // Not POST + curl.set_option(CURLOPT_URL, ST_API_URL); + curl.set_option(CURLOPT_TIMEOUT, 30); // 10s until we fail. + + // Callbacks + curl.set_write_callback([this, &buffer, &buffer_offset](void* data, size_t s1, size_t s2) { + size_t size = s1 * s2; + if (buffer.size() < (size + buffer_offset)) + buffer.resize(buffer_offset + size); + + memcpy(buffer.data() + buffer_offset, data, size); + buffer_offset += size; + + return s1 * s2; + }); + //std::bind(&streamfx::updater::task_write_cb, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3) + + // Clear any unknown data and reserve 64KiB of memory. + buffer.clear(); + buffer.reserve(0xFFFF); + + // Finally, execute the request. + D_LOG_DEBUG("Querying for latest releases...", ""); + if (CURLcode res = curl.perform(); res != CURLE_OK) { + D_LOG_ERROR("Performing query failed with error: %s", curl_easy_strerror(res)); + throw std::runtime_error(curl_easy_strerror(res)); + } -#ifdef _DEBUG - { - std::lock_guard lock(_lock); - nlohmann::json current = _current_info; - nlohmann::json stable = _release_info; - nlohmann::json preview = _testing_info; - - D_LOG_DEBUG("Current Version: %s", current.dump().c_str()); - D_LOG_DEBUG("Stable Version: %s", stable.dump().c_str()); - D_LOG_DEBUG("Preview Version: %s", preview.dump().c_str()); - } -#endif + int32_t status_code = 0; + if (CURLcode res = curl.get_info(CURLINFO_HTTP_CODE, status_code); res != CURLE_OK) { + D_LOG_ERROR("Retrieving status code failed with error: %s", curl_easy_strerror(res)); + throw std::runtime_error(curl_easy_strerror(res)); + } + D_LOG_DEBUG("API returned status code %d.", status_code); + + if (status_code != 200) { + D_LOG_ERROR("API returned unexpected status code %d.", status_code); + throw std::runtime_error("Request failed due to one or more reasons."); + } + }; + auto parse_fn = [this](nlohmann::json json) { + // Check if it was parsed as an object. + if (json.type() != nlohmann::json::value_t::array) { + throw std::runtime_error("Invalid response from API."); + } + + // Decide on the latest version for all update channels. + std::lock_guard lock(_lock); + _updates.clear(); + for (auto obj : json) { + try { + auto info = obj.get(); + + switch (info.stage) { + case version_stage::STABLE: + if (get_update_info(version_stage::STABLE).is_older_than(info)) { + _updates.emplace(version_stage::STABLE, info); + } + [[fallthrough]]; + case version_stage::CANDIDATE: + if (get_update_info(version_stage::CANDIDATE).is_older_than(info)) { + _updates.emplace(version_stage::CANDIDATE, info); + } + [[fallthrough]]; + case version_stage::BETA: + if (get_update_info(version_stage::BETA).is_older_than(info)) { + _updates.emplace(version_stage::BETA, info); + } + [[fallthrough]]; + case version_stage::ALPHA: + if (get_update_info(version_stage::ALPHA).is_older_than(info)) { + _updates.emplace(version_stage::ALPHA, info); + } + } + + } catch (const std::exception& ex) { + D_LOG_DEBUG("Failed to parse entry, error: %s", ex.what()); + } + } + }; - // Log that we have a potential update. - if (have_update()) { - auto info = get_update_info(); + { // Query and parse the response. + nlohmann::json json; - if (info.version_type) { - D_LOG_INFO("Update to version %d.%d.%d%.1s%d is available at \"%s\".", info.version_major, - info.version_minor, info.version_patch, &info.version_type, info.version_index, - info.url.c_str()); + // Query the API or parse a crafted response. + auto debug_path = streamfx::config_file_path("github_release_query_response.json"); + if (std::filesystem::exists(debug_path)) { + std::ifstream fs{debug_path}; + json = nlohmann::json::parse(fs); + fs.close(); } else { - D_LOG_INFO("Update to version %d.%d.%d is available at \"%s\".", info.version_major, info.version_minor, - info.version_patch, info.url.c_str()); + std::vector buffer; + query_fn(buffer); + json = nlohmann::json::parse(buffer.begin(), buffer.end()); } - } else { - D_LOG_DEBUG("No update available.", ""); + + // Parse the JSON response from the API. + parse_fn(json); + } + + // Print all update information to the log file. + D_LOG_INFO("Current Version: %s", static_cast(_current_info).c_str()); + D_LOG_INFO("Latest Stable Version: %s", static_cast(get_update_info(version_stage::STABLE)).c_str()); + D_LOG_INFO("Latest Candidate Version: %s", + static_cast(get_update_info(version_stage::CANDIDATE)).c_str()); + D_LOG_INFO("Latest Beta Version: %s", static_cast(get_update_info(version_stage::BETA)).c_str()); + D_LOG_INFO("Latest Alpha Version: %s", static_cast(get_update_info(version_stage::ALPHA)).c_str()); + if (is_update_available()) { + D_LOG_INFO("Update is available.", ""); } // Notify listeners of the update. @@ -228,91 +359,6 @@ try { events.error.call(*this, message); } -void streamfx::updater::task_query(std::vector& buffer) -{ - static constexpr std::string_view ST_API_URL = "https://api.github.com/repos/Xaymar/obs-StreamFX/releases"; - - streamfx::util::curl curl; - size_t buffer_offset = 0; - - // Set headers (User-Agent is needed so Github can contact us!). - curl.set_header("User-Agent", "StreamFX Updater v" STREAMFX_VERSION_STRING); - curl.set_header("Accept", "application/vnd.github.v3+json"); - - // Set up request. - curl.set_option(CURLOPT_HTTPGET, true); // GET - curl.set_option(CURLOPT_POST, false); // Not POST - curl.set_option(CURLOPT_URL, ST_API_URL); - curl.set_option(CURLOPT_TIMEOUT, 10); // 10s until we fail. - - // Callbacks - curl.set_write_callback([this, &buffer, &buffer_offset](void* data, size_t s1, size_t s2) { - size_t size = s1 * s2; - if (buffer.size() < (size + buffer_offset)) - buffer.resize(buffer_offset + size); - - memcpy(buffer.data() + buffer_offset, data, size); - buffer_offset += size; - - return s1 * s2; - }); - //std::bind(&streamfx::updater::task_write_cb, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3) - - // Clear any unknown data and reserve 64KiB of memory. - buffer.clear(); - buffer.reserve(0xFFFF); - - // Finally, execute the request. - D_LOG_DEBUG("Querying for latest releases...", ""); - if (CURLcode res = curl.perform(); res != CURLE_OK) { - D_LOG_ERROR("Performing query failed with error: %s", curl_easy_strerror(res)); - throw std::runtime_error(curl_easy_strerror(res)); - } - - int32_t status_code = 0; - if (CURLcode res = curl.get_info(CURLINFO_HTTP_CODE, status_code); res != CURLE_OK) { - D_LOG_ERROR("Retrieving status code failed with error: %s", curl_easy_strerror(res)); - throw std::runtime_error(curl_easy_strerror(res)); - } - D_LOG_DEBUG("API returned status code %d.", status_code); - - if (status_code != 200) { - D_LOG_ERROR("API returned unexpected status code %d.", status_code); - throw std::runtime_error("Request failed due to one or more reasons."); - } -} - -void streamfx::updater::task_parse(std::vector& buffer) -{ - // Parse the JSON response from the API. - nlohmann::json json = nlohmann::json::parse(buffer.begin(), buffer.end()); - - // Check if it was parsed as an object. - if (json.type() != nlohmann::json::value_t::array) { - throw std::runtime_error("Response is not a JSON array."); - } - - // Iterate over all entries. - std::lock_guard lock(_lock); - for (auto obj : json) { - try { - auto info = obj.get(); - - if (info.channel == update_channel::RELEASE) { - if (_release_info.is_newer(info)) - _release_info = info; - if (_testing_info.is_newer(info)) - _testing_info = info; - } else { - if (_testing_info.is_newer(info)) - _testing_info = info; - } - } catch (const std::exception& ex) { - D_LOG_DEBUG("Failed to parse entry, error: %s", ex.what()); - } - } -} - bool streamfx::updater::can_check() { #ifdef _DEBUG @@ -326,16 +372,16 @@ bool streamfx::updater::can_check() void streamfx::updater::load() { - std::lock_guard lock(_lock); + std::lock_guard lock(_lock); if (auto config = streamfx::configuration::instance(); config) { auto dataptr = config->get(); - if (obs_data_has_user_value(dataptr.get(), ST_CFG_GDPR)) - _gdpr = obs_data_get_bool(dataptr.get(), ST_CFG_GDPR); + if (obs_data_has_user_value(dataptr.get(), ST_CFG_DATASHARING)) + _data_sharing_allowed = obs_data_get_bool(dataptr.get(), ST_CFG_DATASHARING); if (obs_data_has_user_value(dataptr.get(), ST_CFG_AUTOMATION)) _automation = obs_data_get_bool(dataptr.get(), ST_CFG_AUTOMATION); if (obs_data_has_user_value(dataptr.get(), ST_CFG_CHANNEL)) - _channel = static_cast(obs_data_get_int(dataptr.get(), ST_CFG_CHANNEL)); + _channel = static_cast(obs_data_get_int(dataptr.get(), ST_CFG_CHANNEL)); if (obs_data_has_user_value(dataptr.get(), ST_CFG_LASTCHECKEDAT)) _lastcheckedat = std::chrono::seconds(obs_data_get_int(dataptr.get(), ST_CFG_LASTCHECKEDAT)); } @@ -346,7 +392,7 @@ void streamfx::updater::save() if (auto config = streamfx::configuration::instance(); config) { auto dataptr = config->get(); - obs_data_set_bool(dataptr.get(), ST_CFG_GDPR, _gdpr); + obs_data_set_bool(dataptr.get(), ST_CFG_DATASHARING, _data_sharing_allowed); obs_data_set_bool(dataptr.get(), ST_CFG_AUTOMATION, _automation); obs_data_set_int(dataptr.get(), ST_CFG_CHANNEL, static_cast(_channel)); obs_data_set_int(dataptr.get(), ST_CFG_LASTCHECKEDAT, static_cast(_lastcheckedat.count())); @@ -356,24 +402,16 @@ void streamfx::updater::save() streamfx::updater::updater() : _lock(), _task(), - _gdpr(false), _automation(true), _channel(update_channel::RELEASE), _lastcheckedat(), + _data_sharing_allowed(false), _automation(true), _channel(version_stage::STABLE), _lastcheckedat(), - _current_info(), _release_info(), _testing_info(), _dirty(false) + _current_info(), _updates(), _dirty(false) { // Load information from configuration. load(); // Build current version information. try { - _current_info.version_major = STREAMFX_VERSION_MAJOR; - _current_info.version_minor = STREAMFX_VERSION_MINOR; - _current_info.version_patch = STREAMFX_VERSION_PATCH; - _current_info.channel = _channel; - std::string suffix = STREAMFX_VERSION_SUFFIX; - if (suffix.length()) { - _current_info.version_type = suffix.at(0); - _current_info.version_index = static_cast(strtoul(&suffix.at(1), nullptr, 10)); - } + _current_info = {STREAMFX_VERSION_STRING}; } catch (...) { D_LOG_ERROR("Failed to parse current version information, results may be inaccurate.", ""); } @@ -384,26 +422,26 @@ streamfx::updater::~updater() save(); } -bool streamfx::updater::gdpr() +bool streamfx::updater::is_data_sharing_allowed() { - return _gdpr; + return _data_sharing_allowed; } -void streamfx::updater::set_gdpr(bool value) +void streamfx::updater::set_data_sharing_allowed(bool value) { - _dirty = true; - _gdpr = value; - events.gdpr_changed(*this, _gdpr); + _dirty = true; + _data_sharing_allowed = value; + events.gdpr_changed(*this, _data_sharing_allowed); { - std::lock_guard lock(_lock); + std::lock_guard lock(_lock); save(); } - D_LOG_INFO("User %s the processing of data.", _gdpr ? "allowed" : "disallowed"); + D_LOG_INFO("User %s the processing of data.", _data_sharing_allowed ? "allowed" : "disallowed"); } -bool streamfx::updater::automation() +bool streamfx::updater::is_automated() { return _automation; } @@ -414,21 +452,21 @@ void streamfx::updater::set_automation(bool value) events.automation_changed(*this, _automation); { - std::lock_guard lock(_lock); + std::lock_guard lock(_lock); save(); } D_LOG_INFO("Automatic checks at launch are now %s.", value ? "enabled" : "disabled"); } -streamfx::update_channel streamfx::updater::channel() +streamfx::version_stage streamfx::updater::get_channel() { return _channel; } -void streamfx::updater::set_channel(update_channel value) +void streamfx::updater::set_channel(version_stage value) { - std::lock_guard lock(_lock); + std::lock_guard lock(_lock); _dirty = true; _channel = value; @@ -436,17 +474,17 @@ void streamfx::updater::set_channel(update_channel value) save(); - D_LOG_INFO("Update channel changed to %s.", value == update_channel::RELEASE ? "Release" : "Testing"); + D_LOG_INFO("Update channel changed to '%s'.", stage_to_string(value).data()); } void streamfx::updater::refresh() { - if (!_task.expired() || !gdpr()) { + if (!_task.expired() || !is_data_sharing_allowed()) { return; } if (can_check()) { - std::lock_guard lock(_lock); + std::lock_guard lock(_lock); // Update last checked time. _lastcheckedat = @@ -460,25 +498,34 @@ void streamfx::updater::refresh() } } -bool streamfx::updater::have_update() +bool streamfx::updater::is_update_available() +{ + return _current_info.is_older_than(get_update_info()); +} + +bool streamfx::updater::is_update_available(version_stage channel) { - auto info = get_update_info(); - return _current_info.is_newer(info); + return _current_info.is_older_than(get_update_info(channel)); } -streamfx::update_info streamfx::updater::get_current_info() +streamfx::version_info streamfx::updater::get_current_info() { return _current_info; } -streamfx::update_info streamfx::updater::get_update_info() +streamfx::version_info streamfx::updater::get_update_info() { - std::lock_guard lock(_lock); - update_info info = _release_info; - if (info.is_newer(_testing_info) && (_channel == update_channel::TESTING)) - info = _testing_info; + return get_update_info(_channel); +} - return info; +streamfx::version_info streamfx::updater::get_update_info(version_stage channel) +{ + std::lock_guard lock(_lock); + if (auto iter = _updates.find(channel); iter != _updates.end()) { + return iter->second; + } else { + return {}; + } } std::shared_ptr streamfx::updater::instance() diff --git a/source/updater.hpp b/source/updater.hpp index 45e5c6f761..fcd76a3ca8 100644 --- a/source/updater.hpp +++ b/source/updater.hpp @@ -21,54 +21,65 @@ #pragma once #include #include +#include #include #include "util/util-curl.hpp" #include "util/util-event.hpp" #include "util/util-threadpool.hpp" namespace streamfx { - enum class update_channel { - RELEASE, - TESTING, + enum class version_stage : uint8_t { + STABLE, // A.B.C + CANDIDATE, // A.B.CcD + BETA, // A.B.CbD + ALPHA, // A.B.CaD }; + version_stage stage_from_string(std::string_view str); + std::string_view stage_to_string(version_stage t); - struct update_info { - uint16_t version_major = 0; - uint16_t version_minor = 0; - uint16_t version_patch = 0; - char version_type = 0; - uint16_t version_index = 0; - update_channel channel = update_channel::RELEASE; - std::string url = ""; - std::string name = ""; - - bool is_newer(update_info& other); + void to_json(nlohmann::json&, const version_stage&); + void from_json(const nlohmann::json&, version_stage&); + + struct version_info { + public: + uint16_t major; + uint16_t minor; + uint16_t patch; + uint16_t tweak; + version_stage stage; + std::string url; + std::string name; + + public: + version_info(); + version_info(const std::string& text); + + bool is_older_than(version_info& other); + + operator std::string(); }; - void to_json(nlohmann::json&, const update_info&); - void from_json(const nlohmann::json&, update_info&); + void to_json(nlohmann::json&, const version_info&); + void from_json(const nlohmann::json&, version_info&); class updater { // Internal - std::mutex _lock; + std::recursive_mutex _lock; std::weak_ptr<::streamfx::util::threadpool::task> _task; // Options - std::atomic_bool _gdpr; + std::atomic_bool _data_sharing_allowed; std::atomic_bool _automation; - update_channel _channel; + version_stage _channel; std::chrono::seconds _lastcheckedat; // Update Information - update_info _current_info; - update_info _release_info; - update_info _testing_info; - bool _dirty; + version_info _current_info; + std::map _updates; + bool _dirty; private: void task(streamfx::util::threadpool_data_t); - void task_query(std::vector& buffer); - void task_parse(std::vector& buffer); bool can_check(); @@ -80,30 +91,34 @@ namespace streamfx { ~updater(); // GDPR compliance (must require user interaction!) - bool gdpr(); - void set_gdpr(bool); + bool is_data_sharing_allowed(); + void set_data_sharing_allowed(bool); // Automatic Update checks - bool automation(); + bool is_automated(); void set_automation(bool); // Update Channel - update_channel channel(); - void set_channel(update_channel channel); + version_stage get_channel(); + void set_channel(version_stage channel); // Refresh information. void refresh(); // Check current data. - bool have_update(); - update_info get_current_info(); - update_info get_update_info(); + version_info get_current_info(); + + // Check update data. + bool is_update_available(); + bool is_update_available(version_stage channel); + version_info get_update_info(); + version_info get_update_info(version_stage channel); public: struct _ { - streamfx::util::event gdpr_changed; - streamfx::util::event automation_changed; - streamfx::util::event channel_changed; + streamfx::util::event gdpr_changed; + streamfx::util::event automation_changed; + streamfx::util::event channel_changed; streamfx::util::event error; streamfx::util::event refreshed; diff --git a/templates/version.hpp.in b/templates/version.hpp.in index 0622c30acc..68b7badfa0 100644 --- a/templates/version.hpp.in +++ b/templates/version.hpp.in @@ -39,6 +39,7 @@ #define STREAMFX_VERSION_PATCH @PROJECT_VERSION_PATCH@ #define STREAMFX_VERSION_BUILD @PROJECT_VERSION_TWEAK@ #define STREAMFX_VERSION_TWEAK @PROJECT_VERSION_TWEAK@ -#define STREAMFX_VERSION_SUFFIX "@VERSION_SUFFIX@" +#define STREAMFX_VERSION_SUFFIX "@VERSION_STAGE@" +#define STREAMFX_VERSION_STAGE "@VERSION_STAGE@" #define STREAMFX_VERSION_COMMIT "@VERSION_COMMIT@" #define STREAMFX_VERSION_STRING "@VERSION_STRING@"