From c1c84e91019db295a042ee06bd2ccb481bbddd97 Mon Sep 17 00:00:00 2001 From: jp9000 Date: Mon, 20 Feb 2017 04:46:29 -0800 Subject: [PATCH] UI: Add front-end auto-updater --- UI/CMakeLists.txt | 20 + UI/data/locale/en-US.ini | 15 + UI/forms/OBSBasicSettings.ui | 34 +- UI/forms/OBSUpdate.ui | 103 ++++ UI/obs-app.cpp | 8 + UI/win-update/update-window.cpp | 44 ++ UI/win-update/update-window.hpp | 29 + UI/win-update/win-update-helpers.cpp | 40 ++ UI/win-update/win-update-helpers.hpp | 139 +++++ UI/win-update/win-update.cpp | 778 +++++++++++++++++++++++++++ UI/win-update/win-update.hpp | 23 + UI/window-basic-main.cpp | 83 +-- UI/window-basic-main.hpp | 4 +- UI/window-basic-settings.cpp | 9 + 14 files changed, 1263 insertions(+), 66 deletions(-) create mode 100644 UI/forms/OBSUpdate.ui create mode 100644 UI/win-update/update-window.cpp create mode 100644 UI/win-update/update-window.hpp create mode 100644 UI/win-update/win-update-helpers.cpp create mode 100644 UI/win-update/win-update-helpers.hpp create mode 100644 UI/win-update/win-update.cpp create mode 100644 UI/win-update/win-update.hpp diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 99a67a845..fd5ea8b1e 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -14,6 +14,8 @@ add_subdirectory(obs-frontend-api) project(obs) +set(ENABLE_WIN_UPDATER FALSE CACHE BOOL "Enable the windows updater") + if(DEFINED QTDIR${_lib_suffix}) list(APPEND CMAKE_PREFIX_PATH "${QTDIR${_lib_suffix}}") elseif(DEFINED QTDIR) @@ -52,9 +54,25 @@ include_directories(${LIBCURL_INCLUDE_DIRS}) add_definitions(${LIBCURL_DEFINITIONS}) if(WIN32) + include_directories(${OBS_JANSSON_INCLUDE_DIRS}) + set(obs_PLATFORM_SOURCES platform-windows.cpp + win-update/update-window.cpp + win-update/win-update.cpp + win-update/win-update-helpers.cpp obs.rc) + set(obs_PLATFORM_HEADERS + win-update/update-window.hpp + win-update/win-update.hpp + win-update/win-update-helpers.hpp) + set(obs_PLATFORM_LIBRARIES + crypt32 + ${OBS_JANSSON_IMPORT}) + + if(ENABLE_WIN_UPDATER) + add_definitions(-DENABLE_WIN_UPDATER) + endif() elseif(APPLE) set(obs_PLATFORM_SOURCES platform-osx.mm) @@ -132,6 +150,7 @@ set(obs_SOURCES qt-wrappers.cpp) set(obs_HEADERS + ${obs_PLATFORM_HEADERS} obs-app.hpp platform.hpp window-main.hpp @@ -184,6 +203,7 @@ set(obs_UI forms/OBSBasicSettings.ui forms/OBSBasicSourceSelect.ui forms/OBSBasicInteraction.ui + forms/OBSUpdate.ui forms/OBSRemux.ui) set(obs_QRC diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 84bf3444a..1d916569c 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -62,6 +62,20 @@ ReplayBuffer="Replay Buffer" Import="Import" Export="Export" +# updater +Updater.Title="New update available" +Updater.Text="There is a new update available:" +Updater.UpdateNow="Update Now" +Updater.RemindMeLater="Remind me Later" +Updater.Skip="Skip Version" +Updater.Running.Title="Program currently active" +Updater.Running.Text="Outputs are currently active, please shut down any active outputs before attempting to update" +Updater.NoUpdatesAvailable.Title="No updates available" +Updater.NoUpdatesAvailable.Text="No updates are currently available" +Updater.FailedToLaunch="Failed to launch updater" +Updater.GameCaptureActive.Title="Game capture active" +Updater.GameCaptureActive.Text="Game capture hook library is currently in use. Please close any games/programs being captured (or restart windows) and try again." + # quick transitions QuickTransitions.SwapScenes="Swap Preview/Output Scenes After Transitioning" QuickTransitions.SwapScenesTT="Swaps the preview and output scenes after transitioning (if the output's original scene still exists).\nThis will not undo any changes that may have been made to the output's original scene." @@ -407,6 +421,7 @@ Basic.Settings.Confirm="You have unsaved changes. Save changes?" Basic.Settings.General="General" Basic.Settings.General.Theme="Theme" Basic.Settings.General.Language="Language" +Basic.Settings.General.EnableAutoUpdates="Automatically check for updates on startup" Basic.Settings.General.WarnBeforeStartingStream="Show confirmation dialog when starting streams" Basic.Settings.General.WarnBeforeStoppingStream="Show confirmation dialog when stopping streams" Basic.Settings.General.Projectors="Projectors" diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui index 820c65012..36bc107fc 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/UI/forms/OBSBasicSettings.ui @@ -168,6 +168,9 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + 2 + @@ -194,6 +197,32 @@ + + + + Basic.Settings.General.EnableAutoUpdates + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 170 + 5 + + + + @@ -203,6 +232,9 @@ Basic.Settings.Output + + 2 + @@ -211,7 +243,7 @@ 170 - 11 + 5 diff --git a/UI/forms/OBSUpdate.ui b/UI/forms/OBSUpdate.ui new file mode 100644 index 000000000..f7a77e9b1 --- /dev/null +++ b/UI/forms/OBSUpdate.ui @@ -0,0 +1,103 @@ + + + OBSUpdate + + + + 0 + 0 + 611 + 526 + + + + Updater.Title + + + + + + Updater.Text + + + + + + + true + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:8pt;"><br /></p></body></html> + + + + + + + Qt::Horizontal + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Updater.UpdateNow + + + true + + + + + + + Updater.RemindMeLater + + + + + + + Updater.Skip + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + diff --git a/UI/obs-app.cpp b/UI/obs-app.cpp index 8f5853810..e0fcd2dbd 100644 --- a/UI/obs-app.cpp +++ b/UI/obs-app.cpp @@ -363,6 +363,8 @@ bool OBSApp::InitGlobalConfigDefaults() config_set_default_uint(globalConfig, "General", "MaxLogs", 10); config_set_default_string(globalConfig, "General", "ProcessPriority", "Normal"); + config_set_default_bool(globalConfig, "General", "EnableAutoUpdates", + true); #if _WIN32 config_set_default_string(globalConfig, "Video", "Renderer", @@ -448,7 +450,13 @@ static bool MakeUserDirs() return false; if (!do_mkdir(path)) return false; + + if (GetConfigPath(path, sizeof(path), "obs-studio/updates") <= 0) + return false; + if (!do_mkdir(path)) + return false; #endif + if (GetConfigPath(path, sizeof(path), "obs-studio/plugin_config") <= 0) return false; if (!do_mkdir(path)) diff --git a/UI/win-update/update-window.cpp b/UI/win-update/update-window.cpp new file mode 100644 index 000000000..9c2dcf79b --- /dev/null +++ b/UI/win-update/update-window.cpp @@ -0,0 +1,44 @@ +#include "update-window.hpp" +#include "obs-app.hpp" + +OBSUpdate::OBSUpdate(QWidget *parent, bool manualUpdate, const QString &text) + : QDialog (parent, Qt::WindowSystemMenuHint | + Qt::WindowTitleHint | + Qt::WindowCloseButtonHint), + ui (new Ui_OBSUpdate) +{ + ui->setupUi(this); + ui->text->setHtml(text); + + if (manualUpdate) { + delete ui->skip; + ui->skip = nullptr; + + ui->no->setText(QTStr("Cancel")); + } +} + +void OBSUpdate::on_yes_clicked() +{ + done(OBSUpdate::Yes); +} + +void OBSUpdate::on_no_clicked() +{ + done(OBSUpdate::No); +} + +void OBSUpdate::on_skip_clicked() +{ + done(OBSUpdate::Skip); +} + +void OBSUpdate::accept() +{ + done(OBSUpdate::Yes); +} + +void OBSUpdate::reject() +{ + done(OBSUpdate::No); +} diff --git a/UI/win-update/update-window.hpp b/UI/win-update/update-window.hpp new file mode 100644 index 000000000..361e730c5 --- /dev/null +++ b/UI/win-update/update-window.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +#include "ui_OBSUpdate.h" + +class OBSUpdate : public QDialog { + Q_OBJECT + +public: + enum ReturnVal { + No, + Yes, + Skip + }; + + OBSUpdate(QWidget *parent, bool manualUpdate, const QString &text); + +public slots: + void on_yes_clicked(); + void on_no_clicked(); + void on_skip_clicked(); + virtual void accept() override; + virtual void reject() override; + +private: + std::unique_ptr ui; +}; diff --git a/UI/win-update/win-update-helpers.cpp b/UI/win-update/win-update-helpers.cpp new file mode 100644 index 000000000..b969e3283 --- /dev/null +++ b/UI/win-update/win-update-helpers.cpp @@ -0,0 +1,40 @@ +#include "win-update-helpers.hpp" + +void FreeProvider(HCRYPTPROV prov) +{ + CryptReleaseContext(prov, 0); +} + +void FreeHash(HCRYPTHASH hash) +{ + CryptDestroyHash(hash); +} + +void FreeKey(HCRYPTKEY key) +{ + CryptDestroyKey(key); +} + +std::string vstrprintf(const char *format, va_list args) +{ + if (!format) + return std::string(); + + std::string str; + int size = (int)vsnprintf(nullptr, 0, format, args); + str.resize(size); + vsnprintf(&str[0], size, format, args); + return str; +} + +std::string strprintf(const char *format, ...) +{ + std::string str; + va_list args; + + va_start(args, format); + str = vstrprintf(format, args); + va_end(args); + + return str; +} diff --git a/UI/win-update/win-update-helpers.hpp b/UI/win-update/win-update-helpers.hpp new file mode 100644 index 000000000..51ea16a11 --- /dev/null +++ b/UI/win-update/win-update-helpers.hpp @@ -0,0 +1,139 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include + +#include +#include + +/* ------------------------------------------------------------------------ */ + +template class CustomHandle { + T handle; + +public: + inline CustomHandle() : handle(0) {} + inline CustomHandle(T in) : handle(in) {} + inline ~CustomHandle() + { + if (handle) + freefunc(handle); + } + + inline T *operator&() {return &handle;} + inline operator T() const {return handle;} + inline T get() const {return handle;} + + inline CustomHandle &operator=(T in) + { + if (handle) + freefunc(handle); + handle = in; + return *this; + } + + inline bool operator!() const {return !handle;} +}; + +void FreeProvider(HCRYPTPROV prov); +void FreeHash(HCRYPTHASH hash); +void FreeKey(HCRYPTKEY key); + +using CryptProvider = CustomHandle; +using CryptHash = CustomHandle; +using CryptKey = CustomHandle; + +/* ------------------------------------------------------------------------ */ + +template class LocalPtr { + T *ptr = nullptr; + +public: + inline ~LocalPtr() + { + if (ptr) + LocalFree(ptr); + } + + inline T **operator&() {return &ptr;} + inline operator T() const {return ptr;} + inline T *get() const {return ptr;} + + inline bool operator!() const {return !ptr;} + + inline T *operator->() {return ptr;} +}; + +/* ------------------------------------------------------------------------ */ + +class Json { + json_t *json; + +public: + inline Json() : json(nullptr) {} + explicit inline Json(json_t *json_) : json(json_) {} + inline Json(const Json &from) : json(json_incref(from.json)) {} + inline Json(Json &&from) : json(from.json) {from.json = nullptr;} + + inline ~Json() { + if (json) + json_decref(json); + } + + inline Json &operator=(json_t *json_) + { + if (json) + json_decref(json); + json = json_; + return *this; + } + inline Json &operator=(const Json &from) + { + if (json) + json_decref(json); + json = json_incref(from.json); + return *this; + } + inline Json &operator=(Json &&from) + { + if (json) + json_decref(json); + json = from.json; + from.json = nullptr; + return *this; + } + + inline operator json_t *() const {return json;} + + inline bool operator!() const {return !json;} + + inline const char *GetString(const char *name, + const char *def = nullptr) const + { + json_t *obj(json_object_get(json, name)); + if (!obj) + return def; + return json_string_value(obj); + } + inline int64_t GetInt(const char *name, int def = 0) const + { + json_t *obj(json_object_get(json, name)); + if (!obj) + return def; + return json_integer_value(obj); + } + inline json_t *GetObject(const char *name) const + { + return json_object_get(json, name); + } + + inline json_t *get() const {return json;} +}; + +/* ------------------------------------------------------------------------ */ + +std::string vstrprintf(const char *format, va_list args); +std::string strprintf(const char *format, ...); diff --git a/UI/win-update/win-update.cpp b/UI/win-update/win-update.cpp new file mode 100644 index 000000000..7937de8aa --- /dev/null +++ b/UI/win-update/win-update.cpp @@ -0,0 +1,778 @@ +#include "win-update-helpers.hpp" +#include "update-window.hpp" +#include "remote-text.hpp" +#include "win-update.hpp" +#include "obs-app.hpp" + +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include + +using namespace std; + +/* ------------------------------------------------------------------------ */ + +#ifndef WIN_MANIFEST_URL +#define WIN_MANIFEST_URL "https://obsproject.com/update_studio/manifest.json" +#endif + +#ifndef WIN_UPDATER_URL +#define WIN_UPDATER_URL "https://obsproject.com/update_studio/updater.exe" +#endif + +static HCRYPTPROV provider = 0; + +#pragma pack(push, r1, 1) + +typedef struct { + BLOBHEADER blobheader; + RSAPUBKEY rsapubkey; +} PUBLICKEYHEADER; + +#pragma pack(pop, r1) + +#define TEST_BUILD + +// Hard coded 4096 bit RSA public key for obsproject.com in PEM format +static const unsigned char obs_pub[] = { + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x42, 0x45, 0x47, 0x49, 0x4e, 0x20, 0x50, + 0x55, 0x42, 0x4c, 0x49, 0x43, 0x20, 0x4b, 0x45, 0x59, 0x2d, 0x2d, 0x2d, + 0x2d, 0x2d, 0x0a, 0x4d, 0x49, 0x49, 0x43, 0x49, 0x6a, 0x41, 0x4e, 0x42, + 0x67, 0x6b, 0x71, 0x68, 0x6b, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, + 0x51, 0x45, 0x46, 0x41, 0x41, 0x4f, 0x43, 0x41, 0x67, 0x38, 0x41, 0x4d, + 0x49, 0x49, 0x43, 0x43, 0x67, 0x4b, 0x43, 0x41, 0x67, 0x45, 0x41, 0x6c, + 0x33, 0x73, 0x76, 0x65, 0x72, 0x77, 0x39, 0x48, 0x51, 0x2b, 0x72, 0x59, + 0x51, 0x4e, 0x6e, 0x39, 0x43, 0x61, 0x37, 0x0a, 0x39, 0x4c, 0x55, 0x36, + 0x32, 0x6e, 0x47, 0x36, 0x4e, 0x6f, 0x7a, 0x45, 0x2f, 0x46, 0x73, 0x49, + 0x56, 0x4e, 0x65, 0x72, 0x2b, 0x57, 0x2f, 0x68, 0x75, 0x65, 0x45, 0x38, + 0x57, 0x51, 0x31, 0x6d, 0x72, 0x46, 0x50, 0x2b, 0x32, 0x79, 0x41, 0x2b, + 0x69, 0x59, 0x52, 0x75, 0x74, 0x59, 0x50, 0x65, 0x45, 0x67, 0x70, 0x78, + 0x74, 0x6f, 0x64, 0x48, 0x68, 0x67, 0x6b, 0x52, 0x34, 0x70, 0x45, 0x4b, + 0x0a, 0x56, 0x6e, 0x72, 0x72, 0x31, 0x38, 0x71, 0x34, 0x73, 0x7a, 0x6c, + 0x76, 0x38, 0x39, 0x51, 0x49, 0x37, 0x74, 0x38, 0x6c, 0x4d, 0x6f, 0x4c, + 0x54, 0x6c, 0x46, 0x2b, 0x74, 0x31, 0x49, 0x52, 0x30, 0x56, 0x34, 0x77, + 0x4a, 0x56, 0x33, 0x34, 0x49, 0x33, 0x43, 0x2b, 0x33, 0x35, 0x39, 0x4b, + 0x69, 0x78, 0x6e, 0x7a, 0x4c, 0x30, 0x42, 0x6c, 0x39, 0x61, 0x6a, 0x2f, + 0x7a, 0x44, 0x63, 0x72, 0x58, 0x0a, 0x57, 0x6c, 0x35, 0x70, 0x48, 0x54, + 0x69, 0x6f, 0x4a, 0x77, 0x59, 0x4f, 0x67, 0x4d, 0x69, 0x42, 0x47, 0x4c, + 0x79, 0x50, 0x65, 0x69, 0x74, 0x4d, 0x46, 0x64, 0x6a, 0x6a, 0x54, 0x49, + 0x70, 0x43, 0x4d, 0x2b, 0x6d, 0x78, 0x54, 0x57, 0x58, 0x43, 0x72, 0x5a, + 0x39, 0x64, 0x50, 0x55, 0x4b, 0x76, 0x5a, 0x74, 0x67, 0x7a, 0x6a, 0x64, + 0x2b, 0x49, 0x7a, 0x6c, 0x48, 0x69, 0x64, 0x48, 0x74, 0x4f, 0x0a, 0x4f, + 0x52, 0x42, 0x4e, 0x35, 0x6d, 0x52, 0x73, 0x38, 0x4c, 0x4e, 0x4f, 0x35, + 0x38, 0x6b, 0x37, 0x39, 0x72, 0x37, 0x37, 0x44, 0x63, 0x67, 0x51, 0x59, + 0x50, 0x4e, 0x69, 0x69, 0x43, 0x74, 0x57, 0x67, 0x43, 0x2b, 0x59, 0x34, + 0x4b, 0x37, 0x75, 0x53, 0x5a, 0x58, 0x33, 0x48, 0x76, 0x65, 0x6f, 0x6d, + 0x32, 0x74, 0x48, 0x62, 0x56, 0x58, 0x79, 0x30, 0x4c, 0x2f, 0x43, 0x6c, + 0x37, 0x66, 0x4d, 0x0a, 0x48, 0x4b, 0x71, 0x66, 0x63, 0x51, 0x47, 0x75, + 0x79, 0x72, 0x76, 0x75, 0x64, 0x34, 0x32, 0x4f, 0x72, 0x57, 0x61, 0x72, + 0x41, 0x73, 0x6e, 0x32, 0x70, 0x32, 0x45, 0x69, 0x36, 0x4b, 0x7a, 0x78, + 0x62, 0x33, 0x47, 0x36, 0x45, 0x53, 0x43, 0x77, 0x31, 0x35, 0x6e, 0x48, + 0x41, 0x67, 0x4c, 0x61, 0x6c, 0x38, 0x7a, 0x53, 0x71, 0x37, 0x2b, 0x72, + 0x61, 0x45, 0x2f, 0x78, 0x6b, 0x4c, 0x70, 0x43, 0x0a, 0x62, 0x59, 0x67, + 0x35, 0x67, 0x6d, 0x59, 0x36, 0x76, 0x62, 0x6d, 0x57, 0x6e, 0x71, 0x39, + 0x64, 0x71, 0x57, 0x72, 0x55, 0x7a, 0x61, 0x71, 0x4f, 0x66, 0x72, 0x5a, + 0x50, 0x67, 0x76, 0x67, 0x47, 0x30, 0x57, 0x76, 0x6b, 0x42, 0x53, 0x68, + 0x66, 0x61, 0x45, 0x4f, 0x42, 0x61, 0x49, 0x55, 0x78, 0x41, 0x33, 0x51, + 0x42, 0x67, 0x7a, 0x41, 0x5a, 0x68, 0x71, 0x65, 0x65, 0x64, 0x46, 0x39, + 0x68, 0x0a, 0x61, 0x66, 0x4d, 0x47, 0x4d, 0x4d, 0x39, 0x71, 0x56, 0x62, + 0x66, 0x77, 0x75, 0x75, 0x7a, 0x4a, 0x32, 0x75, 0x68, 0x2b, 0x49, 0x6e, + 0x61, 0x47, 0x61, 0x65, 0x48, 0x32, 0x63, 0x30, 0x34, 0x6f, 0x56, 0x63, + 0x44, 0x46, 0x66, 0x65, 0x4f, 0x61, 0x44, 0x75, 0x78, 0x52, 0x6a, 0x43, + 0x43, 0x62, 0x71, 0x72, 0x35, 0x73, 0x4c, 0x53, 0x6f, 0x31, 0x43, 0x57, + 0x6f, 0x6b, 0x79, 0x6e, 0x6a, 0x4e, 0x0a, 0x43, 0x42, 0x2b, 0x62, 0x32, + 0x72, 0x51, 0x46, 0x37, 0x44, 0x50, 0x50, 0x62, 0x44, 0x34, 0x73, 0x2f, + 0x6e, 0x54, 0x39, 0x4e, 0x73, 0x63, 0x6b, 0x2f, 0x4e, 0x46, 0x7a, 0x72, + 0x42, 0x58, 0x52, 0x4f, 0x2b, 0x64, 0x71, 0x6b, 0x65, 0x42, 0x77, 0x44, + 0x55, 0x43, 0x76, 0x37, 0x62, 0x5a, 0x67, 0x57, 0x37, 0x4f, 0x78, 0x75, + 0x4f, 0x58, 0x30, 0x37, 0x4c, 0x54, 0x71, 0x66, 0x70, 0x35, 0x73, 0x0a, + 0x4f, 0x65, 0x47, 0x67, 0x75, 0x62, 0x75, 0x62, 0x69, 0x77, 0x59, 0x33, + 0x55, 0x64, 0x48, 0x59, 0x71, 0x2b, 0x4c, 0x39, 0x4a, 0x71, 0x49, 0x53, + 0x47, 0x31, 0x74, 0x4d, 0x34, 0x48, 0x65, 0x4b, 0x6a, 0x61, 0x48, 0x6a, + 0x75, 0x31, 0x4d, 0x44, 0x6a, 0x76, 0x48, 0x5a, 0x32, 0x44, 0x62, 0x6d, + 0x4c, 0x77, 0x55, 0x78, 0x75, 0x59, 0x61, 0x36, 0x4a, 0x5a, 0x44, 0x4b, + 0x57, 0x73, 0x37, 0x72, 0x0a, 0x49, 0x72, 0x64, 0x44, 0x77, 0x78, 0x33, + 0x4a, 0x77, 0x61, 0x63, 0x46, 0x36, 0x36, 0x68, 0x33, 0x59, 0x55, 0x57, + 0x36, 0x74, 0x7a, 0x55, 0x5a, 0x68, 0x7a, 0x74, 0x63, 0x6d, 0x51, 0x65, + 0x70, 0x50, 0x2f, 0x75, 0x37, 0x42, 0x67, 0x47, 0x72, 0x6b, 0x4f, 0x50, + 0x50, 0x70, 0x59, 0x41, 0x30, 0x4e, 0x45, 0x4a, 0x38, 0x30, 0x53, 0x65, + 0x41, 0x78, 0x37, 0x68, 0x69, 0x4e, 0x34, 0x76, 0x61, 0x0a, 0x65, 0x45, + 0x51, 0x4b, 0x6e, 0x52, 0x6e, 0x2b, 0x45, 0x70, 0x42, 0x4e, 0x36, 0x55, + 0x42, 0x61, 0x35, 0x66, 0x37, 0x4c, 0x6f, 0x4b, 0x38, 0x43, 0x41, 0x77, + 0x45, 0x41, 0x41, 0x51, 0x3d, 0x3d, 0x0a, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, + 0x45, 0x4e, 0x44, 0x20, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x20, 0x4b, + 0x45, 0x59, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x0a +}; +static const unsigned int obs_pub_len = 800; + +/* ------------------------------------------------------------------------ */ + +static bool QuickWriteFile(const char *file, const void *data, size_t size) +try { + BPtr w_file; + if (os_utf8_to_wcs_ptr(file, 0, &w_file) == 0) + return false; + + WinHandle handle = CreateFileW( + w_file, + GENERIC_WRITE, + 0, + nullptr, + CREATE_ALWAYS, + FILE_FLAG_WRITE_THROUGH, + nullptr); + + if (handle == INVALID_HANDLE_VALUE) + throw strprintf("Failed to open file '%s': %lu", + file, GetLastError()); + + DWORD written; + if (!WriteFile(handle, data, (DWORD)size, &written, nullptr)) + throw strprintf("Failed to write file '%s': %lu", + file, GetLastError()); + + return true; + +} catch (string text) { + blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); + return false; +} + +static bool QuickReadFile(const char *file, string &data) +try { + BPtr w_file; + if (os_utf8_to_wcs_ptr(file, 0, &w_file) == 0) + return false; + + WinHandle handle = CreateFileW( + w_file, + GENERIC_READ, + FILE_SHARE_READ, + nullptr, + OPEN_EXISTING, + 0, + nullptr); + + if (handle == INVALID_HANDLE_VALUE) + throw strprintf("Failed to open file '%s': %lu", + file, GetLastError()); + + DWORD size = GetFileSize(handle, nullptr); + data.resize(size); + + DWORD read; + if (!ReadFile(handle, &data[0], size, &read, nullptr)) + throw strprintf("Failed to write file '%s': %lu", + file, GetLastError()); + + return true; + +} catch (string text) { + blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); + return false; +} + +static void HashToString(const uint8_t *in, char *out) +{ + const char alphabet[] = "0123456789abcdef"; + + for (int i = 0; i != 20; ++i) { + out[2 * i] = alphabet[in[i] / 16]; + out[2 * i + 1] = alphabet[in[i] % 16]; + } + + out[40] = 0; +} + +static bool CalculateFileHash(const char *path, uint8_t *hash) +try { + CryptHash hHash; + if (!CryptCreateHash(provider, CALG_SHA1, 0, 0, &hHash)) + return false; + + BPtr w_path; + if (os_utf8_to_wcs_ptr(path, 0, &w_path) == 0) + return false; + + WinHandle handle = CreateFileW(w_path, GENERIC_READ, FILE_SHARE_READ, + nullptr, OPEN_EXISTING, 0, nullptr); + if (handle == INVALID_HANDLE_VALUE) + throw strprintf("Failed to open file '%s': %lu", + path, GetLastError()); + + vector buf; + buf.resize(65536); + + for (;;) { + DWORD read = 0; + if (!ReadFile(handle, buf.data(), (DWORD)buf.size(), &read, + nullptr)) + throw strprintf("Failed to read file '%s': %lu", + path, GetLastError()); + + if (!read) + break; + + if (!CryptHashData(hHash, buf.data(), read, 0)) + return false; + } + + DWORD hashLength = 20; + if (!CryptGetHashParam(hHash, HP_HASHVAL, hash, &hashLength, 0)) + return false; + + return true; + +} catch (string text) { + blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); + return false; +} + +/* ------------------------------------------------------------------------ */ + +static bool VerifyDigitalSignature(uint8_t *buf, size_t len, uint8_t *sig, + size_t sigLen) +{ + /* ASN of PEM public key */ + BYTE binaryKey[1024]; + DWORD binaryKeyLen = sizeof(binaryKey); + + /* Windows X509 public key info from ASN */ + LocalPtr publicPBLOB; + DWORD iPBLOBSize; + + /* RSA BLOB info from X509 public key */ + LocalPtr rsaPublicBLOB; + DWORD rsaPublicBLOBSize; + + /* Handle to public key */ + CryptKey keyOut; + + /* Handle to hash context */ + CryptHash hash; + + /* Signature in little-endian format */ + vector reversedSig; + + if (!CryptStringToBinaryA((LPCSTR)obs_pub, + obs_pub_len, + CRYPT_STRING_BASE64HEADER, + binaryKey, + &binaryKeyLen, + nullptr, + nullptr)) + return false; + + if (!CryptDecodeObjectEx(X509_ASN_ENCODING, + X509_PUBLIC_KEY_INFO, + binaryKey, + binaryKeyLen, + CRYPT_ENCODE_ALLOC_FLAG, + nullptr, + &publicPBLOB, + &iPBLOBSize)) + return false; + + if (!CryptDecodeObjectEx(X509_ASN_ENCODING, + RSA_CSP_PUBLICKEYBLOB, + publicPBLOB->PublicKey.pbData, + publicPBLOB->PublicKey.cbData, + CRYPT_ENCODE_ALLOC_FLAG, + nullptr, + &rsaPublicBLOB, + &rsaPublicBLOBSize)) + return false; + + if (!CryptImportKey(provider, + (const BYTE *)rsaPublicBLOB.get(), + rsaPublicBLOBSize, + 0, + 0, + &keyOut)) + return false; + + if (!CryptCreateHash(provider, CALG_SHA_512, 0, 0, &hash)) + return false; + + if (!CryptHashData(hash, buf, (DWORD)len, 0)) + return false; + + /* Windows requires signature in little-endian. Every other crypto + * provider is big-endian of course. */ + reversedSig.resize(sigLen); + for (size_t i = 0; i < sigLen; i++) + reversedSig[i] = sig[sigLen - i - 1]; + + if (!CryptVerifySignature(hash, + reversedSig.data(), + (DWORD)sigLen, + keyOut, + nullptr, + 0)) + return false; + + return true; +} + +static inline void HexToByteArray(const char *hexStr, size_t hexLen, + vector &out) +{ + char ptr[3]; + + ptr[2] = 0; + + for (size_t i = 0; i < hexLen; i += 2) { + ptr[0] = hexStr[i]; + ptr[1] = hexStr[i + 1]; + out.push_back((uint8_t)strtoul(ptr, nullptr, 16)); + } +} + +static bool CheckDataSignature(const string &data, const char *name, + const char *hexSig, size_t sigLen) +try { + if (sigLen == 0 || sigLen > 0xFFFF || (sigLen & 1) != 0) + throw strprintf("Missing or invalid signature for %s", name); + + /* Convert TCHAR signature to byte array */ + vector signature; + signature.reserve(sigLen); + HexToByteArray(hexSig, sigLen, signature); + + if (!VerifyDigitalSignature((uint8_t*)data.data(), + data.size(), + signature.data(), + signature.size())) + throw strprintf("Signature check failed for %s", name); + + return true; + +} catch (string text) { + blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); + return false; +} + +/* ------------------------------------------------------------------------ */ + +static bool FetchUpdaterModule(const char *url) +try { + long responseCode; + uint8_t updateFileHash[20]; + vector extraHeaders; + + BPtr updateFilePath = GetConfigPathPtr( + "obs-studio\\updates\\updater.exe"); + + if (CalculateFileHash(updateFilePath, updateFileHash)) { + char hashString[41]; + HashToString(updateFileHash, hashString); + + string header = "If-None-Match: "; + header += hashString; + extraHeaders.push_back(move(header)); + } + + string signature; + string error; + string data; + + bool success = GetRemoteFile(url, data, error, &responseCode, + nullptr, nullptr, extraHeaders, &signature); + + if (!success || (responseCode != 200 && responseCode != 304)) { + if (responseCode == 404) + return false; + + throw strprintf("Could not fetch '%s': %s", url, error.c_str()); + } + + /* A new file must be digitally signed */ + if (responseCode == 200) { + bool valid = CheckDataSignature(data, url, signature.data(), + signature.size()); + if (!valid) + throw string("Invalid updater module signature"); + + if (!QuickWriteFile(updateFilePath, data.data(), data.size())) + return false; + } + + return true; + +} catch (string text) { + blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); + return false; +} + +/* ------------------------------------------------------------------------ */ + +static bool ParseUpdateManifest(const char *manifest, bool *updatesAvailable, + string ¬es_str, int &updateVer) +try { + + json_error_t error; + Json root(json_loads(manifest, 0, &error)); + if (!root) + throw strprintf("Failed reading json string (%d): %s", + error.line, error.text); + + if (!json_is_object(root.get())) + throw string("Root of manifest is not an object"); + + int major = root.GetInt("version_major"); + int minor = root.GetInt("version_minor"); + int patch = root.GetInt("version_patch"); + + if (major == 0) + throw strprintf("Invalid version number: %d.%d.%d", + major, + minor, + patch); + + json_t *notes = json_object_get(root, "notes"); + if (!json_is_string(notes)) + throw string("'notes' value invalid"); + + notes_str = json_string_value(notes); + + json_t *packages = json_object_get(root, "packages"); + if (!json_is_array(packages)) + throw string("'packages' value invalid"); + + int cur_ver = LIBOBS_API_VER; + int new_ver = MAKE_SEMANTIC_VERSION(major, minor, patch); + + updateVer = new_ver; + *updatesAvailable = new_ver > cur_ver; + + return true; + +} catch (string text) { + blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); + return false; +} + +/* ------------------------------------------------------------------------ */ + +void GenerateGUID(string &guid) +{ + BYTE junk[20]; + + if (!CryptGenRandom(provider, sizeof(junk), junk)) + return; + + guid.resize(41); + HashToString(junk, &guid[0]); +} + +void AutoUpdateThread::infoMsg(const QString &title, const QString &text) +{ + QMessageBox::information(App()->GetMainWindow(), title, text); +} + +void AutoUpdateThread::info(const QString &title, const QString &text) +{ + QMetaObject::invokeMethod(this, "infoMsg", + Qt::BlockingQueuedConnection, + Q_ARG(QString, title), + Q_ARG(QString, text)); +} + +int AutoUpdateThread::queryUpdateSlot(bool manualUpdate, const QString &text) +{ + OBSUpdate updateDlg(App()->GetMainWindow(), manualUpdate, text); + return updateDlg.exec(); +} + +int AutoUpdateThread::queryUpdate(bool manualUpdate, const char *text_utf8) +{ + int ret = OBSUpdate::No; + QString text = text_utf8; + QMetaObject::invokeMethod(this, "queryUpdateSlot", + Qt::BlockingQueuedConnection, + Q_RETURN_ARG(int, ret), + Q_ARG(bool, manualUpdate), + Q_ARG(QString, text)); + return ret; +} + +static bool IsFileInUse(const wstring &file) +{ + WinHandle f = CreateFile(file.c_str(), GENERIC_READ, 0, nullptr, + OPEN_EXISTING, 0, nullptr); + if (!f.Valid()) { + int err = GetLastError(); + if (err == ERROR_SHARING_VIOLATION || + err == ERROR_LOCK_VIOLATION) + return true; + } + + return false; +} + +static bool IsGameCaptureInUse() +{ + wstring path = L"..\\..\\data\\obs-plugins\\win-capture\\graphics-hook"; + return IsFileInUse(path + L"32.dll") || + IsFileInUse(path + L"64.dll"); +} + +void AutoUpdateThread::run() +try { + long responseCode; + vector extraHeaders; + string text; + string error; + string signature; + CryptProvider provider; + BYTE manifestHash[20]; + bool updatesAvailable = false; + bool success; + + struct FinishedTrigger { + inline ~FinishedTrigger() + { + QMetaObject::invokeMethod(App()->GetMainWindow(), + "updateCheckFinished"); + } + } finishedTrigger; + + BPtr manifestPath = GetConfigPathPtr( + "obs-studio\\updates\\manifest.json"); + + auto ActiveOrGameCaptureLocked = [this] () + { + if (video_output_active(obs_get_video())) { + if (manualUpdate) + info(QTStr("Updater.Running.Title"), + QTStr("Updater.Running.Text")); + return true; + } + if (IsGameCaptureInUse()) { + if (manualUpdate) + info(QTStr("Updater.GameCaptureActive.Title"), + QTStr("Updater.GameCaptureActive.Text")); + return true; + } + + return false; + }; + + /* ----------------------------------- * + * warn if running or gc locked */ + + if (ActiveOrGameCaptureLocked()) + return; + + /* ----------------------------------- * + * create signature provider */ + + if (!CryptAcquireContext(&provider, + nullptr, + MS_ENH_RSA_AES_PROV, + PROV_RSA_AES, + CRYPT_VERIFYCONTEXT)) + throw strprintf("CryptAcquireContext failed: %lu", + GetLastError()); + + ::provider = provider; + + /* ----------------------------------- * + * avoid downloading manifest again */ + + if (CalculateFileHash(manifestPath, manifestHash)) { + char hashString[41]; + HashToString(manifestHash, hashString); + + string header = "If-None-Match: "; + header += hashString; + extraHeaders.push_back(move(header)); + } + + /* ----------------------------------- * + * get current install GUID */ + + /* NOTE: this is an arbitrary random number that we use to count the + * number of unique OBS installations and is not associated with any + * kind of identifiable information */ + const char *pguid = config_get_string(GetGlobalConfig(), + "General", "InstallGUID"); + string guid; + if (pguid) + guid = pguid; + + if (guid.empty()) { + GenerateGUID(guid); + + if (!guid.empty()) + config_set_string(GetGlobalConfig(), + "General", "InstallGUID", + guid.c_str()); + } + + if (!guid.empty()) { + string header = "X-OBS-GUID: "; + header += guid; + extraHeaders.push_back(move(header)); + } + + /* ----------------------------------- * + * get manifest from server */ + + success = GetRemoteFile(WIN_MANIFEST_URL, text, error, &responseCode, + nullptr, nullptr, extraHeaders, &signature); + + if (!success || (responseCode != 200 && responseCode != 304)) { + if (responseCode == 404) + return; + + throw strprintf("Failed to fetch manifest file: %s", error); + } + + /* ----------------------------------- * + * verify file signature */ + + /* a new file must be digitally signed */ + if (responseCode == 200) { + success = CheckDataSignature(text, "manifest", + signature.data(), signature.size()); + if (!success) + throw string("Invalid manifest signature"); + } + + /* ----------------------------------- * + * write or load manifest */ + + if (responseCode == 200) { + if (!QuickWriteFile(manifestPath, text.data(), text.size())) + throw strprintf("Could not write file '%s'", + manifestPath); + } else { + if (!QuickReadFile(manifestPath, text)) + throw strprintf("Could not read file '%s'", + manifestPath); + } + + /* ----------------------------------- * + * check manifest for update */ + + string notes; + int updateVer = 0; + + success = ParseUpdateManifest(text.c_str(), &updatesAvailable, notes, + updateVer); + if (!success) + throw string("Failed to parse manifest"); + + if (!updatesAvailable) { + if (manualUpdate) + info(QTStr("Updater.NoUpdatesAvailable.Title"), + QTStr("Updater.NoUpdatesAvailable.Text")); + return; + } + + /* ----------------------------------- * + * skip this version if set to skip */ + + int skipUpdateVer = config_get_int(GetGlobalConfig(), "General", + "SkipUpdateVersion"); + if (!manualUpdate && updateVer == skipUpdateVer) + return; + + /* ----------------------------------- * + * warn again if running or gc locked */ + + if (ActiveOrGameCaptureLocked()) + return; + + /* ----------------------------------- * + * fetch updater module */ + + if (!FetchUpdaterModule(WIN_UPDATER_URL)) + return; + + /* ----------------------------------- * + * query user for update */ + + int queryResult = queryUpdate(manualUpdate, notes.c_str()); + + if (queryResult == OBSUpdate::No) { + if (!manualUpdate) { + long long t = (long long)time(nullptr); + config_set_int(GetGlobalConfig(), "General", + "LastUpdateCheck", t); + } + return; + + } else if (queryResult == OBSUpdate::Skip) { + config_set_int(GetGlobalConfig(), "General", + "SkipUpdateVersion", updateVer); + return; + } + + /* ----------------------------------- * + * get working dir */ + + wchar_t cwd[MAX_PATH]; + GetModuleFileNameW(nullptr, cwd, _countof(cwd) - 1); + wchar_t *p = wcsrchr(cwd, '\\'); + if (p) + *p = 0; + + /* ----------------------------------- * + * execute updater */ + + BPtr updateFilePath = GetConfigPathPtr( + "obs-studio\\updates\\updater.exe"); + BPtr wUpdateFilePath; + + size_t size = os_utf8_to_wcs_ptr(updateFilePath, 0, &wUpdateFilePath); + if (!size) + throw string("Could not convert updateFilePath to wide"); + + /* note, can't use CreateProcess to launch as admin. */ + SHELLEXECUTEINFO execInfo = {}; + + execInfo.cbSize = sizeof(execInfo); + execInfo.lpFile = wUpdateFilePath; +#ifndef UPDATE_CHANNEL +#define UPDATE_ARG_SUFFIX L"" +#else +#define UPDATE_ARG_SUFFIX UPDATE_CHANNEL +#endif + if (App()->IsPortableMode()) + execInfo.lpParameters = UPDATE_ARG_SUFFIX L" Portable"; + else + execInfo.lpParameters = UPDATE_ARG_SUFFIX; + + execInfo.lpDirectory = cwd; + execInfo.nShow = SW_SHOWNORMAL; + + if (!ShellExecuteEx(&execInfo)) { + QString msg = QTStr("Updater.FailedToLaunch"); + info(msg, msg); + throw strprintf("Can't launch updater '%s': %d", + updateFilePath, GetLastError()); + } + + /* force OBS to perform another update check immediately after updating + * in case of issues with the new version */ + config_set_int(GetGlobalConfig(), "General", "LastUpdateCheck", 0); + config_set_int(GetGlobalConfig(), "General", "SkipUpdateVersion", 0); + config_set_string(GetGlobalConfig(), "General", "InstallGUID", + guid.c_str()); + + QMetaObject::invokeMethod(App()->GetMainWindow(), "close"); + +} catch (string text) { + blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); +} diff --git a/UI/win-update/win-update.hpp b/UI/win-update/win-update.hpp new file mode 100644 index 000000000..47bdd03be --- /dev/null +++ b/UI/win-update/win-update.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +class AutoUpdateThread : public QThread { + Q_OBJECT + + bool manualUpdate; + bool user_confirmed = false; + + virtual void run() override; + + void info(const QString &title, const QString &text); + int queryUpdate(bool manualUpdate, const char *text_utf8); + +private slots: + void infoMsg(const QString &title, const QString &text); + int queryUpdateSlot(bool manualUpdate, const QString &text); + +public: + AutoUpdateThread(bool manualUpdate_) : manualUpdate(manualUpdate_) {} +}; diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index e62122750..8a87a7c64 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -52,6 +52,10 @@ #include "volume-control.hpp" #include "remote-text.hpp" +#if defined(_WIN32) && defined(ENABLE_WIN_UPDATER) +#include "win-update/win-update.hpp" +#endif + #include "ui_OBSBasic.h" #include @@ -1585,6 +1589,9 @@ void OBSBasic::ClearHotkeys() OBSBasic::~OBSBasic() { + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + delete programOptions; delete program; @@ -2123,10 +2130,14 @@ void trigger_sparkle_update(); void OBSBasic::TimedCheckForUpdates() { + if (!config_get_bool(App()->GlobalConfig(), "General", + "EnableAutoUpdates")) + return; + #ifdef UPDATE_SPARKLE init_sparkle_updater(config_get_bool(App()->GlobalConfig(), "General", "UpdateToUndeployed")); -#else +#elif ENABLE_WIN_UPDATER long long lastUpdate = config_get_int(App()->GlobalConfig(), "General", "LastUpdateCheck"); uint32_t lastVersion = config_get_int(App()->GlobalConfig(), "General", @@ -2142,27 +2153,21 @@ void OBSBasic::TimedCheckForUpdates() long long secs = t - lastUpdate; if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(); + CheckForUpdates(false); #endif } -void OBSBasic::CheckForUpdates() +void OBSBasic::CheckForUpdates(bool manualUpdate) { #ifdef UPDATE_SPARKLE trigger_sparkle_update(); -#else +#elif ENABLE_WIN_UPDATER ui->actionCheckForUpdates->setEnabled(false); - if (updateCheckThread) { - updateCheckThread->wait(); - delete updateCheckThread; - } + if (updateCheckThread && updateCheckThread->isRunning()) + return; - RemoteTextThread *thread = new RemoteTextThread( - "https://obsproject.com/obs2_update/basic.json"); - updateCheckThread = thread; - connect(thread, &RemoteTextThread::Result, - this, &OBSBasic::updateFileFinished); + updateCheckThread = new AutoUpdateThread(manualUpdate); updateCheckThread->start(); #endif } @@ -2175,57 +2180,9 @@ void OBSBasic::CheckForUpdates() #define VERSION_ENTRY "other" #endif -void OBSBasic::updateFileFinished(const QString &text, const QString &error) +void OBSBasic::updateCheckFinished() { ui->actionCheckForUpdates->setEnabled(true); - - if (text.isEmpty()) { - blog(LOG_WARNING, "Update check failed: %s", QT_TO_UTF8(error)); - return; - } - - obs_data_t *returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - obs_data_t *versionData = obs_data_get_obj(returnData, VERSION_ENTRY); - const char *description = obs_data_get_string(returnData, - "description"); - const char *download = obs_data_get_string(versionData, "download"); - - if (returnData && versionData && description && download) { - long major = obs_data_get_int(versionData, "major"); - long minor = obs_data_get_int(versionData, "minor"); - long patch = obs_data_get_int(versionData, "patch"); - long version = MAKE_SEMANTIC_VERSION(major, minor, patch); - - blog(LOG_INFO, "Update check: last known remote version " - "is %ld.%ld.%ld", - major, minor, patch); - - if (version > LIBOBS_API_VER) { - QString str = QTStr("UpdateAvailable.Text"); - QMessageBox messageBox(this); - - str = str.arg(QString::number(major), - QString::number(minor), - QString::number(patch), - download); - - messageBox.setWindowTitle(QTStr("UpdateAvailable")); - messageBox.setTextFormat(Qt::RichText); - messageBox.setText(str); - messageBox.setInformativeText(QT_UTF8(description)); - messageBox.exec(); - - long long t = (long long)time(nullptr); - config_set_int(App()->GlobalConfig(), "General", - "LastUpdateCheck", t); - config_save_safe(App()->GlobalConfig(), "tmp", nullptr); - } - } else { - blog(LOG_WARNING, "Bad JSON file received from server"); - } - - obs_data_release(versionData); - obs_data_release(returnData); } void OBSBasic::DuplicateSelectedScene() @@ -3730,7 +3687,7 @@ void OBSBasic::on_actionViewCurrentLog_triggered() void OBSBasic::on_actionCheckForUpdates_triggered() { - CheckForUpdates(); + CheckForUpdates(true); } void OBSBasic::logUploadFinished(const QString &text, const QString &error) diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 85afd1c44..d89be1383 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -200,7 +200,7 @@ private: bool QueryRemoveSource(obs_source_t *source); void TimedCheckForUpdates(); - void CheckForUpdates(); + void CheckForUpdates(bool manualUpdate); void GetFPSCommon(uint32_t &num, uint32_t &den) const; void GetFPSInteger(uint32_t &num, uint32_t &den) const; @@ -595,7 +595,7 @@ private slots: void logUploadFinished(const QString &text, const QString &error); - void updateFileFinished(const QString &text, const QString &error); + void updateCheckFinished(); void AddSourceFromAction(); diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index cd12606c1..5d0b816b4 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -273,6 +273,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->language, COMBO_CHANGED, GENERAL_CHANGED); HookWidget(ui->theme, COMBO_CHANGED, GENERAL_CHANGED); + HookWidget(ui->enableAutoUpdates, CHECK_CHANGED, GENERAL_CHANGED); HookWidget(ui->warnBeforeStreamStart,CHECK_CHANGED, GENERAL_CHANGED); HookWidget(ui->warnBeforeStreamStop, CHECK_CHANGED, GENERAL_CHANGED); HookWidget(ui->hideProjectorCursor, CHECK_CHANGED, GENERAL_CHANGED); @@ -896,6 +897,10 @@ void OBSBasicSettings::LoadGeneralSettings() LoadLanguageList(); LoadThemeList(); + bool enableAutoUpdates = config_get_bool(GetGlobalConfig(), + "General", "EnableAutoUpdates"); + ui->enableAutoUpdates->setChecked(enableAutoUpdates); + bool recordWhenStreaming = config_get_bool(GetGlobalConfig(), "BasicWindow", "RecordWhenStreaming"); ui->recordWhenStreaming->setChecked(recordWhenStreaming); @@ -2351,6 +2356,10 @@ void OBSBasicSettings::SaveGeneralSettings() App()->SetTheme(theme); } + if (WidgetChanged(ui->enableAutoUpdates)) + config_set_bool(GetGlobalConfig(), "General", + "EnableAutoUpdates", + ui->enableAutoUpdates->isChecked()); if (WidgetChanged(ui->snappingEnabled)) config_set_bool(GetGlobalConfig(), "BasicWindow", "SnappingEnabled",