mirror of
https://github.com/Xaymar/obs-StreamFX
synced 2024-11-25 12:55:12 +00:00
493 lines
15 KiB
C++
493 lines
15 KiB
C++
|
// Copyright (c) 2020 Michael Fabian Dirks <info@xaymar.com>
|
||
|
//
|
||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
|
// of this software and associated documentation files (the "Software"), to deal
|
||
|
// in the Software without restriction, including without limitation the rights
|
||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
// copies of the Software, and to permit persons to whom the Software is
|
||
|
// furnished to do so, subject to the following conditions:
|
||
|
//
|
||
|
// The above copyright notice and this permission notice shall be included in all
|
||
|
// copies or substantial portions of the Software.
|
||
|
//
|
||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||
|
// SOFTWARE.
|
||
|
|
||
|
#include "updater.hpp"
|
||
|
#include "version.hpp"
|
||
|
#include <mutex>
|
||
|
#include <string_view>
|
||
|
#include "configuration.hpp"
|
||
|
#include "plugin.hpp"
|
||
|
|
||
|
// 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_PREFIX "<updater> "
|
||
|
#define D_LOG_ERROR(...) DLOG_ERROR(ST_PREFIX __VA_ARGS__)
|
||
|
#define D_LOG_WARNING(...) DLOG_WARNING(ST_PREFIX __VA_ARGS__)
|
||
|
#define D_LOG_INFO(...) DLOG_INFO(ST_PREFIX __VA_ARGS__)
|
||
|
#ifdef _DEBUG
|
||
|
#define D_LOG_DEBUG(...) DLOG_DEBUG(ST_PREFIX __VA_ARGS__)
|
||
|
#else
|
||
|
#define D_LOG_DEBUG(...)
|
||
|
#endif
|
||
|
|
||
|
#define ST_CFG_GDPR "updater.gdpr"
|
||
|
#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)
|
||
|
{
|
||
|
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;
|
||
|
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)
|
||
|
{
|
||
|
if (json.find("html_url") != json.end()) {
|
||
|
// 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())) {
|
||
|
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>();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool streamfx::update_info::is_newer(update_info& other)
|
||
|
{
|
||
|
// 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)
|
||
|
return false;
|
||
|
if (version_major < other.version_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)
|
||
|
return false;
|
||
|
if (version_minor < other.version_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)
|
||
|
return false;
|
||
|
if (version_patch < other.version_patch)
|
||
|
return true;
|
||
|
|
||
|
// 4. Compare Type:
|
||
|
// A. Ours empty: Remote is older.
|
||
|
// B. Theirs empty: 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)
|
||
|
return false;
|
||
|
if (other.version_type == 0)
|
||
|
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:
|
||
|
// A. Ours is greater or equal: Remote is older or identical.
|
||
|
// B. Remote is newer
|
||
|
if (version_index >= other.version_index)
|
||
|
return false;
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
void streamfx::updater::task(util::threadpool_data_t)
|
||
|
try {
|
||
|
{
|
||
|
std::vector<char> buffer;
|
||
|
task_query(buffer);
|
||
|
task_parse(buffer);
|
||
|
}
|
||
|
|
||
|
#ifdef _DEBUG
|
||
|
{
|
||
|
std::lock_guard<std::mutex> 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
|
||
|
|
||
|
// Log that we have a potential update.
|
||
|
if (have_update()) {
|
||
|
auto info = get_update_info();
|
||
|
|
||
|
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());
|
||
|
}
|
||
|
} else {
|
||
|
D_LOG_DEBUG("No update 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);
|
||
|
}
|
||
|
|
||
|
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";
|
||
|
|
||
|
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
|
||
|
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<std::mutex> 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_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));
|
||
|
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_GDPR, _gdpr);
|
||
|
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()));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
streamfx::updater::updater()
|
||
|
: _lock(), _task(),
|
||
|
|
||
|
_gdpr(false), _automation(true), _channel(update_channel::RELEASE), _lastcheckedat(),
|
||
|
|
||
|
_current_info(), _release_info(), _testing_info(), _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));
|
||
|
}
|
||
|
} catch (...) {
|
||
|
D_LOG_ERROR("Failed to parse current version information, results may be inaccurate.");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
streamfx::updater::~updater()
|
||
|
{
|
||
|
save();
|
||
|
}
|
||
|
|
||
|
bool streamfx::updater::gdpr()
|
||
|
{
|
||
|
return _gdpr;
|
||
|
}
|
||
|
|
||
|
void streamfx::updater::set_gdpr(bool value)
|
||
|
{
|
||
|
_dirty = true;
|
||
|
_gdpr = value;
|
||
|
events.gdpr_changed(*this, _gdpr);
|
||
|
|
||
|
{
|
||
|
std::lock_guard<std::mutex> lock(_lock);
|
||
|
save();
|
||
|
}
|
||
|
|
||
|
D_LOG_INFO("User %s the processing of data.", _gdpr ? "allowed" : "disallowed");
|
||
|
}
|
||
|
|
||
|
bool streamfx::updater::automation()
|
||
|
{
|
||
|
return _automation;
|
||
|
}
|
||
|
|
||
|
void streamfx::updater::set_automation(bool value)
|
||
|
{
|
||
|
_automation = value;
|
||
|
events.automation_changed(*this, _automation);
|
||
|
|
||
|
{
|
||
|
std::lock_guard<std::mutex> lock(_lock);
|
||
|
save();
|
||
|
}
|
||
|
|
||
|
D_LOG_INFO("Automatic checks at launch are now %s.", value ? "enabled" : "disabled");
|
||
|
}
|
||
|
|
||
|
streamfx::update_channel streamfx::updater::channel()
|
||
|
{
|
||
|
return _channel;
|
||
|
}
|
||
|
|
||
|
void streamfx::updater::set_channel(update_channel value)
|
||
|
{
|
||
|
std::lock_guard<std::mutex> lock(_lock);
|
||
|
|
||
|
_dirty = true;
|
||
|
_channel = value;
|
||
|
events.channel_changed(*this, _channel);
|
||
|
|
||
|
save();
|
||
|
|
||
|
D_LOG_INFO("Update channel changed to %s.", value == update_channel::RELEASE ? "Release" : "Testing");
|
||
|
}
|
||
|
|
||
|
void streamfx::updater::refresh()
|
||
|
{
|
||
|
if (!_task.expired() || !gdpr()) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (can_check()) {
|
||
|
std::lock_guard<std::mutex> 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::have_update()
|
||
|
{
|
||
|
auto info = get_update_info();
|
||
|
return _current_info.is_newer(info);
|
||
|
}
|
||
|
|
||
|
streamfx::update_info streamfx::updater::get_current_info()
|
||
|
{
|
||
|
return _current_info;
|
||
|
}
|
||
|
|
||
|
streamfx::update_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 info;
|
||
|
}
|
||
|
|
||
|
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();
|
||
|
}
|
||
|
}
|