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