mirror of
https://github.com/Xaymar/obs-StreamFX
synced 2024-11-24 04:15:11 +00:00
65c45c4461
This is a lot more complex that I expected it to be.
537 lines
16 KiB
C++
537 lines
16 KiB
C++
// AUTOGENERATED COPYRIGHT HEADER START
|
|
// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks <info@xaymar.com>
|
|
// AUTOGENERATED COPYRIGHT HEADER END
|
|
|
|
#include "updater.hpp"
|
|
#include "version.hpp"
|
|
#include "configuration.hpp"
|
|
#include "plugin.hpp"
|
|
#include "util/util-logging.hpp"
|
|
|
|
#include "warning-disable.hpp"
|
|
#include <fstream>
|
|
#include <mutex>
|
|
#include <regex>
|
|
#include <string_view>
|
|
#include "warning-enable.hpp"
|
|
|
|
#ifdef _DEBUG
|
|
#define ST_PREFIX "<%s> "
|
|
#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__)
|
|
#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__)
|
|
#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__)
|
|
#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__)
|
|
#else
|
|
#define ST_PREFIX "<updater> "
|
|
#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__)
|
|
#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__)
|
|
#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__)
|
|
#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__)
|
|
#endif
|
|
|
|
// TODO:
|
|
// - Cache result in the configuration directory (not as a configuration value).
|
|
// - Move 'autoupdater.last_checked_at' to out of the configuration.
|
|
// - Figure out if nightly updates are viable at all.
|
|
|
|
#define ST_CFG_DATASHARING "updater.datasharing"
|
|
#define ST_CFG_AUTOMATION "updater.automation"
|
|
#define ST_CFG_CHANNEL "updater.channel"
|
|
#define ST_CFG_LASTCHECKEDAT "updater.lastcheckedat"
|
|
|
|
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.major;
|
|
version["minor"] = info.minor;
|
|
version["patch"] = info.patch;
|
|
version["type"] = info.stage;
|
|
version["tweak"] = info.tweak;
|
|
json["version"] = version;
|
|
json["url"] = info.url;
|
|
json["name"] = info.name;
|
|
}
|
|
|
|
void streamfx::from_json(const nlohmann::json& json, version_info& info)
|
|
{
|
|
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");
|
|
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.");
|
|
}
|
|
|
|
// 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::version_info::is_older_than(const version_info other)
|
|
{
|
|
// 'true' if other is newer, otherwise false.
|
|
// Except for stable releases, this simply compares the numbers of the version.
|
|
|
|
// Compare Major version:
|
|
// - Theirs is greater: Remote is newer.
|
|
if (major < other.major)
|
|
return true;
|
|
// - Ours is greater: Remote is older.
|
|
if (major > other.major)
|
|
return false;
|
|
// - Continue the check.
|
|
|
|
// Compare Minor version:
|
|
// - Theirs is greater: Remote is newer.
|
|
if (minor < other.minor)
|
|
return true;
|
|
// - Ours is greater: Remote is older.
|
|
if (minor > other.minor)
|
|
return false;
|
|
// - Continue the check.
|
|
|
|
// Compare Patch version:
|
|
// - Theirs is greater: Remote is newer.
|
|
if (patch < other.patch)
|
|
return true;
|
|
// - Ours is greater: Remote is older.
|
|
if (patch > other.patch)
|
|
return false;
|
|
// - Continue the check.
|
|
|
|
// Compare Stage
|
|
// - Theirs is Stable, we are not: Remote is newer.
|
|
if ((stage != version_stage::STABLE) && (other.stage == version_stage::STABLE))
|
|
return true;
|
|
// - Continue the check.
|
|
|
|
// Compare Tweak
|
|
// - Theirs is greater: Remote is newer.
|
|
if (tweak < other.tweak)
|
|
return true;
|
|
// - Ours is greater: Remote is older.
|
|
if (tweak > other.tweak)
|
|
return false;
|
|
// - Continue the check.
|
|
|
|
// Compare Stage (again)
|
|
// - Ours is greater: Remote is newer.
|
|
if (stage > other.stage)
|
|
return true;
|
|
// - Theirs is greater: Remote is older.
|
|
if (stage < other.stage)
|
|
return false;
|
|
|
|
// If all tests failed so far, assume the compared version is identical or newer.
|
|
return false;
|
|
}
|
|
|
|
streamfx::version_info::operator std::string()
|
|
{
|
|
std::vector<char> buffer(25, 0);
|
|
if (stage != version_stage::STABLE) {
|
|
auto types = stage_to_string(stage);
|
|
int 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 {
|
|
int 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::task_data_t)
|
|
{
|
|
try {
|
|
auto query_fn = [](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";
|
|
|
|
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([&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.");
|
|
}
|
|
};
|
|
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.
|
|
events.refreshed.call(*this);
|
|
} catch (const std::exception& ex) {
|
|
// Notify about the error.
|
|
std::string message = ex.what();
|
|
events.error.call(*this, message);
|
|
}
|
|
}
|
|
|
|
bool streamfx::updater::can_check()
|
|
{
|
|
#ifdef _DEBUG
|
|
return true;
|
|
#else
|
|
auto now = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch());
|
|
auto threshold = (_lastcheckedat + std::chrono::minutes(10));
|
|
return (now > threshold);
|
|
#endif
|
|
}
|
|
|
|
void streamfx::updater::load()
|
|
{
|
|
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_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<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));
|
|
}
|
|
}
|
|
|
|
void streamfx::updater::save()
|
|
{
|
|
if (auto config = streamfx::configuration::instance(); config) {
|
|
auto dataptr = config->get();
|
|
|
|
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()));
|
|
|
|
config->save();
|
|
}
|
|
}
|
|
|
|
streamfx::updater::updater()
|
|
: _lock(), _task(),
|
|
|
|
_data_sharing_allowed(false), _automation(true), _channel(version_stage::STABLE), _lastcheckedat(),
|
|
|
|
_current_info(), _updates(), _dirty(false)
|
|
{
|
|
// Load information from configuration.
|
|
load();
|
|
|
|
// Build current version information.
|
|
try {
|
|
_current_info = {STREAMFX_VERSION_STRING};
|
|
} catch (...) {
|
|
D_LOG_ERROR("Failed to parse current version information, results may be inaccurate.", "");
|
|
}
|
|
}
|
|
|
|
streamfx::updater::~updater()
|
|
{
|
|
save();
|
|
}
|
|
|
|
bool streamfx::updater::is_data_sharing_allowed()
|
|
{
|
|
return _data_sharing_allowed;
|
|
}
|
|
|
|
void streamfx::updater::set_data_sharing_allowed(bool value)
|
|
{
|
|
_dirty = true;
|
|
_data_sharing_allowed = value;
|
|
events.gdpr_changed(*this, _data_sharing_allowed);
|
|
|
|
{
|
|
std::lock_guard<decltype(_lock)> lock(_lock);
|
|
save();
|
|
}
|
|
|
|
D_LOG_INFO("User %s the processing of data.", _data_sharing_allowed ? "allowed" : "disallowed");
|
|
}
|
|
|
|
bool streamfx::updater::is_automated()
|
|
{
|
|
return _automation;
|
|
}
|
|
|
|
void streamfx::updater::set_automation(bool value)
|
|
{
|
|
_automation = value;
|
|
events.automation_changed(*this, _automation);
|
|
|
|
{
|
|
std::lock_guard<decltype(_lock)> lock(_lock);
|
|
save();
|
|
}
|
|
|
|
D_LOG_INFO("Automatic checks at launch are now %s.", value ? "enabled" : "disabled");
|
|
}
|
|
|
|
streamfx::version_stage streamfx::updater::get_channel()
|
|
{
|
|
return _channel;
|
|
}
|
|
|
|
void streamfx::updater::set_channel(version_stage value)
|
|
{
|
|
std::lock_guard<decltype(_lock)> lock(_lock);
|
|
|
|
_dirty = true;
|
|
_channel = value;
|
|
events.channel_changed(*this, _channel);
|
|
|
|
save();
|
|
|
|
D_LOG_INFO("Update channel changed to '%s'.", stage_to_string(value).data());
|
|
}
|
|
|
|
void streamfx::updater::refresh()
|
|
{
|
|
if (!_task.expired() || !is_data_sharing_allowed()) {
|
|
return;
|
|
}
|
|
|
|
if (can_check()) {
|
|
std::lock_guard<decltype(_lock)> lock(_lock);
|
|
|
|
// Update last checked time.
|
|
_lastcheckedat = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch());
|
|
save();
|
|
|
|
// Spawn a new task.
|
|
_task = streamfx::threadpool()->push(std::bind(&streamfx::updater::task, this, std::placeholders::_1), nullptr);
|
|
} else {
|
|
events.refreshed(*this);
|
|
}
|
|
}
|
|
|
|
bool streamfx::updater::is_update_available()
|
|
{
|
|
return _current_info.is_older_than(get_update_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::version_info streamfx::updater::get_update_info()
|
|
{
|
|
return get_update_info(_channel);
|
|
}
|
|
|
|
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()
|
|
{
|
|
static std::weak_ptr<streamfx::updater> _instance;
|
|
static std::mutex _lock;
|
|
|
|
std::lock_guard<std::mutex> lock(_lock);
|
|
if (_instance.expired()) {
|
|
auto ptr = std::make_shared<streamfx::updater>();
|
|
_instance = ptr;
|
|
return ptr;
|
|
} else {
|
|
return _instance.lock();
|
|
}
|
|
}
|