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.
This commit is contained in:
Michael Fabian 'Xaymar' Dirks 2022-06-11 08:02:41 +02:00
parent ba15203c67
commit e9283aec37
7 changed files with 551 additions and 414 deletions

View file

@ -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 "$<TARGET_PROPERTY:${PROJECT_NAME},BINARY_DIR>/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 "$<TARGET_PROPERTY:${PROJECT_NAME},BINARY_DIR>/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 "$<TARGET_PROPERTY:${PROJECT_NAME},BINARY_DIR>/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)")

View file

@ -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)"

View file

@ -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<QWidge
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
setWindowFlag(Qt::WindowMinimizeButtonHint, false);
setWindowFlag(Qt::WindowMaximizeButtonHint, false);
setAttribute(Qt::WA_DeleteOnClose, false); // Do not delete on close.
connect(ok, &QPushButton::clicked, this, &streamfx::ui::updater_dialog::on_ok);
connect(cancel, &QPushButton::clicked, this, &streamfx::ui::updater_dialog::on_cancel);
@ -58,52 +61,17 @@ streamfx::ui::updater_dialog::updater_dialog() : QDialog(reinterpret_cast<QWidge
streamfx::ui::updater_dialog::~updater_dialog() {}
void streamfx::ui::updater_dialog::show(streamfx::update_info current, streamfx::update_info update)
void streamfx::ui::updater_dialog::show(streamfx::version_info current, streamfx::version_info update)
{
{
std::vector<char> buf;
if (current.version_type) {
buf.resize(static_cast<size_t>(snprintf(nullptr, 0, "%" PRIu16 ".%" PRIu16 ".%" PRIu16 "%.1s%" PRIu16,
current.version_major, current.version_minor, current.version_patch,
&current.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, &current.version_type, current.version_index);
} else {
buf.resize(
static_cast<size_t>(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<std::string>(current)));
latestVersion->setText(QString::fromStdString(static_cast<std::string>(update)));
{
std::vector<char> buf;
if (update.version_type) {
buf.resize(static_cast<size_t>(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<size_t>(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<char> buf2;
buf2.resize(static_cast<size_t>(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<char> buf2;
buf2.resize(static_cast<size_t>(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);
}

View file

@ -36,7 +36,7 @@
#include <QWidgetAction>
#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);

View file

@ -20,7 +20,9 @@
#include "updater.hpp"
#include "version.hpp"
#include <fstream>
#include <mutex>
#include <regex>
#include <string_view>
#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<std::string>());
}
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<uint16_t>(strtoul(matches[1].str().c_str(), nullptr, 10));
minor = static_cast<uint16_t>(strtoul(matches[2].str().c_str(), nullptr, 10));
patch = static_cast<uint16_t>(strtoul(matches[3].str().c_str(), nullptr, 10));
if (matches.size() >= 5) {
stage = stage_from_string(matches[5].str());
tweak = static_cast<uint16_t>(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<uint16_t>();
info.minor = version.at("minor").get<uint16_t>();
info.patch = version.at("patch").get<uint16_t>();
info.stage = version.at("type");
info.tweak = version.at("tweak").get<uint16_t>();
info.url = json.at("url").get<std::string>();
info.name = json.at("name").get<std::string>();
} 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<bool>() ? update_channel::TESTING : update_channel::RELEASE;
info.url = entry_url->get<std::string>();
info.name = entry_name->get<std::string>();
// Parse the tag name as SemVer (A.B.C)
{
std::string tag_name = entry_tag_name->get<std::string>();
size_t dot_1 = tag_name.find_first_of(".", 0) + 1;
info.version_major = static_cast<uint16_t>(strtoul(&tag_name.at(0), nullptr, 10));
if (dot_1 < tag_name.size()) {
info.version_minor = static_cast<uint16_t>(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<uint16_t>(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<size_t>(endptr - tag_name.data()) + 1;
info.version_type = *endptr;
if (last_num < tag_name.size())
info.version_index = static_cast<uint16_t>(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<uint16_t>();
info.version_minor = version.at("minor").get<uint16_t>();
info.version_patch = version.at("patch").get<uint16_t>();
if (version.find("type") != version.end())
info.version_type = version.at("alpha").get<bool>() ? 'a' : 'b';
info.version_index = version.at("index").get<uint16_t>();
info.channel = json.at("preview").get<bool>() ? update_channel::TESTING : update_channel::RELEASE;
info.url = json.at("url").get<std::string>();
info.name = json.at("name").get<std::string>();
// Parse the information.
std::string tag_name = entry_tag_name->get<std::string>();
info = {tag_name};
info.url = entry_url->get<std::string>();
info.name = entry_name->get<std::string>();
}
}
bool streamfx::update_info::is_newer(update_info& other)
bool streamfx::version_info::is_older_than(const 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<char> 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<char> buffer;
task_query(buffer);
task_parse(buffer);
}
auto query_fn = [this](std::vector<char>& buffer) {
static constexpr std::string_view ST_API_URL =
"https://api.github.com/repos/Xaymar/obs-StreamFX/releases?per_page=25&page=1";
#ifdef _DEBUG
{
std::lock_guard<std::mutex> lock(_lock);
nlohmann::json current = _current_info;
nlohmann::json stable = _release_info;
nlohmann::json preview = _testing_info;
streamfx::util::curl curl;
size_t buffer_offset = 0;
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
// 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");
// Log that we have a potential update.
if (have_update()) {
auto info = get_update_info();
// 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.
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());
} 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());
// 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));
}
} else {
D_LOG_DEBUG("No update available.", "");
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<decltype(_lock)> lock(_lock);
_updates.clear();
for (auto obj : json) {
try {
auto info = obj.get<streamfx::version_info>();
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());
}
}
};
{ // Query and parse the response.
nlohmann::json json;
// 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 {
std::vector<char> buffer;
query_fn(buffer);
json = nlohmann::json::parse(buffer.begin(), buffer.end());
}
// 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<std::string>(_current_info).c_str());
D_LOG_INFO("Latest Stable Version: %s", static_cast<std::string>(get_update_info(version_stage::STABLE)).c_str());
D_LOG_INFO("Latest Candidate Version: %s",
static_cast<std::string>(get_update_info(version_stage::CANDIDATE)).c_str());
D_LOG_INFO("Latest Beta Version: %s", static_cast<std::string>(get_update_info(version_stage::BETA)).c_str());
D_LOG_INFO("Latest Alpha Version: %s", static_cast<std::string>(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<char>& 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<char>& 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<std::mutex> lock(_lock);
for (auto obj : json) {
try {
auto info = obj.get<streamfx::update_info>();
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<std::mutex> lock(_lock);
std::lock_guard<decltype(_lock)> 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<update_channel>(obs_data_get_int(dataptr.get(), ST_CFG_CHANNEL));
_channel = static_cast<version_stage>(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<long long>(_channel));
obs_data_set_int(dataptr.get(), ST_CFG_LASTCHECKEDAT, static_cast<long long>(_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<uint16_t>(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<std::mutex> lock(_lock);
std::lock_guard<decltype(_lock)> 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<std::mutex> lock(_lock);
std::lock_guard<decltype(_lock)> 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<std::mutex> lock(_lock);
std::lock_guard<decltype(_lock)> 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<std::mutex> lock(_lock);
std::lock_guard<decltype(_lock)> 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()
{
auto info = get_update_info();
return _current_info.is_newer(info);
return _current_info.is_older_than(get_update_info());
}
streamfx::update_info streamfx::updater::get_current_info()
bool streamfx::updater::is_update_available(version_stage channel)
{
return _current_info.is_older_than(get_update_info(channel));
}
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<std::mutex> 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<decltype(_lock)> lock(_lock);
if (auto iter = _updates.find(channel); iter != _updates.end()) {
return iter->second;
} else {
return {};
}
}
std::shared_ptr<streamfx::updater> streamfx::updater::instance()

View file

@ -21,54 +21,65 @@
#pragma once
#include <atomic>
#include <chrono>
#include <map>
#include <nlohmann/json.hpp>
#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);
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(const version_info other);
operator std::string();
};
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 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<version_stage, version_info> _updates;
bool _dirty;
private:
void task(streamfx::util::threadpool_data_t);
void task_query(std::vector<char>& buffer);
void task_parse(std::vector<char>& 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<updater&, bool> gdpr_changed;
streamfx::util::event<updater&, bool> automation_changed;
streamfx::util::event<updater&, update_channel> channel_changed;
streamfx::util::event<updater&, bool> gdpr_changed;
streamfx::util::event<updater&, bool> automation_changed;
streamfx::util::event<updater&, version_stage> channel_changed;
streamfx::util::event<updater&, std::string&> error;
streamfx::util::event<updater&> refreshed;

View file

@ -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@"