瀏覽代碼

UI: Add front-end auto-updater

jp9000 8 年之前
父節點
當前提交
c1c84e9101

+ 20 - 0
UI/CMakeLists.txt

@@ -14,6 +14,8 @@ add_subdirectory(obs-frontend-api)
 
 
 project(obs)
 project(obs)
 
 
+set(ENABLE_WIN_UPDATER FALSE CACHE BOOL "Enable the windows updater")
+
 if(DEFINED QTDIR${_lib_suffix})
 if(DEFINED QTDIR${_lib_suffix})
 	list(APPEND CMAKE_PREFIX_PATH "${QTDIR${_lib_suffix}}")
 	list(APPEND CMAKE_PREFIX_PATH "${QTDIR${_lib_suffix}}")
 elseif(DEFINED QTDIR)
 elseif(DEFINED QTDIR)
@@ -52,9 +54,25 @@ include_directories(${LIBCURL_INCLUDE_DIRS})
 add_definitions(${LIBCURL_DEFINITIONS})
 add_definitions(${LIBCURL_DEFINITIONS})
 
 
 if(WIN32)
 if(WIN32)
+	include_directories(${OBS_JANSSON_INCLUDE_DIRS})
+
 	set(obs_PLATFORM_SOURCES
 	set(obs_PLATFORM_SOURCES
 		platform-windows.cpp
 		platform-windows.cpp
+		win-update/update-window.cpp
+		win-update/win-update.cpp
+		win-update/win-update-helpers.cpp
 		obs.rc)
 		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)
 elseif(APPLE)
 	set(obs_PLATFORM_SOURCES
 	set(obs_PLATFORM_SOURCES
 		platform-osx.mm)
 		platform-osx.mm)
@@ -132,6 +150,7 @@ set(obs_SOURCES
 	qt-wrappers.cpp)
 	qt-wrappers.cpp)
 
 
 set(obs_HEADERS
 set(obs_HEADERS
+	${obs_PLATFORM_HEADERS}
 	obs-app.hpp
 	obs-app.hpp
 	platform.hpp
 	platform.hpp
 	window-main.hpp
 	window-main.hpp
@@ -184,6 +203,7 @@ set(obs_UI
 	forms/OBSBasicSettings.ui
 	forms/OBSBasicSettings.ui
 	forms/OBSBasicSourceSelect.ui
 	forms/OBSBasicSourceSelect.ui
 	forms/OBSBasicInteraction.ui
 	forms/OBSBasicInteraction.ui
+	forms/OBSUpdate.ui
 	forms/OBSRemux.ui)
 	forms/OBSRemux.ui)
 
 
 set(obs_QRC
 set(obs_QRC

+ 15 - 0
UI/data/locale/en-US.ini

@@ -62,6 +62,20 @@ ReplayBuffer="Replay Buffer"
 Import="Import"
 Import="Import"
 Export="Export"
 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
 # quick transitions
 QuickTransitions.SwapScenes="Swap Preview/Output Scenes After Transitioning"
 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."
 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="General"
 Basic.Settings.General.Theme="Theme"
 Basic.Settings.General.Theme="Theme"
 Basic.Settings.General.Language="Language"
 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.WarnBeforeStartingStream="Show confirmation dialog when starting streams"
 Basic.Settings.General.WarnBeforeStoppingStream="Show confirmation dialog when stopping streams"
 Basic.Settings.General.WarnBeforeStoppingStream="Show confirmation dialog when stopping streams"
 Basic.Settings.General.Projectors="Projectors"
 Basic.Settings.General.Projectors="Projectors"

+ 33 - 1
UI/forms/OBSBasicSettings.ui

@@ -168,6 +168,9 @@
                    <property name="labelAlignment">
                    <property name="labelAlignment">
                     <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
                     <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
                    </property>
                    </property>
+                   <property name="topMargin">
+                    <number>2</number>
+                   </property>
                    <item row="0" column="0">
                    <item row="0" column="0">
                     <widget class="QLabel" name="label">
                     <widget class="QLabel" name="label">
                      <property name="text">
                      <property name="text">
@@ -194,6 +197,32 @@
                    <item row="1" column="1">
                    <item row="1" column="1">
                     <widget class="QComboBox" name="theme"/>
                     <widget class="QComboBox" name="theme"/>
                    </item>
                    </item>
+                   <item row="2" column="1">
+                    <widget class="QCheckBox" name="enableAutoUpdates">
+                     <property name="text">
+                      <string>Basic.Settings.General.EnableAutoUpdates</string>
+                     </property>
+                     <property name="checked">
+                      <bool>true</bool>
+                     </property>
+                    </widget>
+                   </item>
+                   <item row="2" column="0">
+                    <spacer name="horizontalSpacer_3">
+                     <property name="orientation">
+                      <enum>Qt::Horizontal</enum>
+                     </property>
+                     <property name="sizeType">
+                      <enum>QSizePolicy::Fixed</enum>
+                     </property>
+                     <property name="sizeHint" stdset="0">
+                      <size>
+                       <width>170</width>
+                       <height>5</height>
+                      </size>
+                     </property>
+                    </spacer>
+                   </item>
                   </layout>
                   </layout>
                  </widget>
                  </widget>
                 </item>
                 </item>
@@ -203,6 +232,9 @@
                    <string>Basic.Settings.Output</string>
                    <string>Basic.Settings.Output</string>
                   </property>
                   </property>
                   <layout class="QFormLayout" name="formLayout_2">
                   <layout class="QFormLayout" name="formLayout_2">
+                   <property name="topMargin">
+                    <number>2</number>
+                   </property>
                    <item row="0" column="0">
                    <item row="0" column="0">
                     <spacer name="horizontalSpacer_5">
                     <spacer name="horizontalSpacer_5">
                      <property name="orientation">
                      <property name="orientation">
@@ -211,7 +243,7 @@
                      <property name="sizeHint" stdset="0">
                      <property name="sizeHint" stdset="0">
                       <size>
                       <size>
                        <width>170</width>
                        <width>170</width>
-                       <height>11</height>
+                       <height>5</height>
                       </size>
                       </size>
                      </property>
                      </property>
                     </spacer>
                     </spacer>

+ 103 - 0
UI/forms/OBSUpdate.ui

@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>OBSUpdate</class>
+ <widget class="QDialog" name="OBSUpdate">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>611</width>
+    <height>526</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Updater.Title</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>Updater.Text</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QTextEdit" name="text">
+     <property name="readOnly">
+      <bool>true</bool>
+     </property>
+     <property name="html">
+      <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot;-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;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="Line" name="line">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QPushButton" name="yes">
+       <property name="text">
+        <string>Updater.UpdateNow</string>
+       </property>
+       <property name="default">
+        <bool>true</bool>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="no">
+       <property name="text">
+        <string>Updater.RemindMeLater</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="skip">
+       <property name="text">
+        <string>Updater.Skip</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer_2">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 8 - 0
UI/obs-app.cpp

@@ -363,6 +363,8 @@ bool OBSApp::InitGlobalConfigDefaults()
 	config_set_default_uint(globalConfig, "General", "MaxLogs", 10);
 	config_set_default_uint(globalConfig, "General", "MaxLogs", 10);
 	config_set_default_string(globalConfig, "General", "ProcessPriority",
 	config_set_default_string(globalConfig, "General", "ProcessPriority",
 			"Normal");
 			"Normal");
+	config_set_default_bool(globalConfig, "General", "EnableAutoUpdates",
+			true);
 
 
 #if _WIN32
 #if _WIN32
 	config_set_default_string(globalConfig, "Video", "Renderer",
 	config_set_default_string(globalConfig, "Video", "Renderer",
@@ -448,7 +450,13 @@ static bool MakeUserDirs()
 		return false;
 		return false;
 	if (!do_mkdir(path))
 	if (!do_mkdir(path))
 		return false;
 		return false;
+
+	if (GetConfigPath(path, sizeof(path), "obs-studio/updates") <= 0)
+		return false;
+	if (!do_mkdir(path))
+		return false;
 #endif
 #endif
+
 	if (GetConfigPath(path, sizeof(path), "obs-studio/plugin_config") <= 0)
 	if (GetConfigPath(path, sizeof(path), "obs-studio/plugin_config") <= 0)
 		return false;
 		return false;
 	if (!do_mkdir(path))
 	if (!do_mkdir(path))

+ 44 - 0
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);
+}

+ 29 - 0
UI/win-update/update-window.hpp

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <QDialog>
+#include <memory>
+
+#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_OBSUpdate> ui;
+};

+ 40 - 0
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;
+}

+ 139 - 0
UI/win-update/win-update-helpers.hpp

@@ -0,0 +1,139 @@
+#pragma once
+
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#include <Wincrypt.h>
+
+#include <jansson.h>
+
+#include <cstdint>
+#include <string>
+
+/* ------------------------------------------------------------------------ */
+
+template<typename T, void freefunc(T)> 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<T, freefunc> &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<HCRYPTPROV, FreeProvider>;
+using CryptHash     = CustomHandle<HCRYPTHASH, FreeHash>;
+using CryptKey      = CustomHandle<HCRYPTKEY,  FreeKey>;
+
+/* ------------------------------------------------------------------------ */
+
+template<typename T> 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, ...);

+ 778 - 0
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 <QMessageBox>
+
+#include <string>
+
+#include <util/windows/WinHandle.hpp>
+#include <util/util.hpp>
+#include <jansson.h>
+
+#include <time.h>
+#include <strsafe.h>
+#include <winhttp.h>
+#include <shellapi.h>
+
+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<wchar_t> 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<wchar_t> 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<wchar_t> 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<BYTE> 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<CERT_PUBLIC_KEY_INFO> publicPBLOB;
+	DWORD                          iPBLOBSize;
+
+	/* RSA BLOB info from X509 public key */
+	LocalPtr<PUBLICKEYHEADER> rsaPublicBLOB;
+	DWORD                     rsaPublicBLOBSize;
+
+	/* Handle to public key */
+	CryptKey keyOut;
+
+	/* Handle to hash context */
+	CryptHash hash;
+
+	/* Signature in little-endian format */
+	vector<BYTE> 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<uint8_t> &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<uint8_t> 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<string> extraHeaders;
+
+	BPtr<char> 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 &notes_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<string> 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<char> 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<char> updateFilePath = GetConfigPathPtr(
+			"obs-studio\\updates\\updater.exe");
+	BPtr<wchar_t> 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());
+}

+ 23 - 0
UI/win-update/win-update.hpp

@@ -0,0 +1,23 @@
+#pragma once
+
+#include <QThread>
+#include <QString>
+
+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_) {}
+};

+ 20 - 63
UI/window-basic-main.cpp

@@ -52,6 +52,10 @@
 #include "volume-control.hpp"
 #include "volume-control.hpp"
 #include "remote-text.hpp"
 #include "remote-text.hpp"
 
 
+#if defined(_WIN32) && defined(ENABLE_WIN_UPDATER)
+#include "win-update/win-update.hpp"
+#endif
+
 #include "ui_OBSBasic.h"
 #include "ui_OBSBasic.h"
 
 
 #include <fstream>
 #include <fstream>
@@ -1585,6 +1589,9 @@ void OBSBasic::ClearHotkeys()
 
 
 OBSBasic::~OBSBasic()
 OBSBasic::~OBSBasic()
 {
 {
+	if (updateCheckThread && updateCheckThread->isRunning())
+		updateCheckThread->wait();
+
 	delete programOptions;
 	delete programOptions;
 	delete program;
 	delete program;
 
 
@@ -2123,10 +2130,14 @@ void trigger_sparkle_update();
 
 
 void OBSBasic::TimedCheckForUpdates()
 void OBSBasic::TimedCheckForUpdates()
 {
 {
+	if (!config_get_bool(App()->GlobalConfig(), "General",
+				"EnableAutoUpdates"))
+		return;
+
 #ifdef UPDATE_SPARKLE
 #ifdef UPDATE_SPARKLE
 	init_sparkle_updater(config_get_bool(App()->GlobalConfig(), "General",
 	init_sparkle_updater(config_get_bool(App()->GlobalConfig(), "General",
 				"UpdateToUndeployed"));
 				"UpdateToUndeployed"));
-#else
+#elif ENABLE_WIN_UPDATER
 	long long lastUpdate = config_get_int(App()->GlobalConfig(), "General",
 	long long lastUpdate = config_get_int(App()->GlobalConfig(), "General",
 			"LastUpdateCheck");
 			"LastUpdateCheck");
 	uint32_t lastVersion = config_get_int(App()->GlobalConfig(), "General",
 	uint32_t lastVersion = config_get_int(App()->GlobalConfig(), "General",
@@ -2142,27 +2153,21 @@ void OBSBasic::TimedCheckForUpdates()
 	long long secs = t - lastUpdate;
 	long long secs = t - lastUpdate;
 
 
 	if (secs > UPDATE_CHECK_INTERVAL)
 	if (secs > UPDATE_CHECK_INTERVAL)
-		CheckForUpdates();
+		CheckForUpdates(false);
 #endif
 #endif
 }
 }
 
 
-void OBSBasic::CheckForUpdates()
+void OBSBasic::CheckForUpdates(bool manualUpdate)
 {
 {
 #ifdef UPDATE_SPARKLE
 #ifdef UPDATE_SPARKLE
 	trigger_sparkle_update();
 	trigger_sparkle_update();
-#else
+#elif ENABLE_WIN_UPDATER
 	ui->actionCheckForUpdates->setEnabled(false);
 	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();
 	updateCheckThread->start();
 #endif
 #endif
 }
 }
@@ -2175,57 +2180,9 @@ void OBSBasic::CheckForUpdates()
 #define VERSION_ENTRY "other"
 #define VERSION_ENTRY "other"
 #endif
 #endif
 
 
-void OBSBasic::updateFileFinished(const QString &text, const QString &error)
+void OBSBasic::updateCheckFinished()
 {
 {
 	ui->actionCheckForUpdates->setEnabled(true);
 	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()
 void OBSBasic::DuplicateSelectedScene()
@@ -3730,7 +3687,7 @@ void OBSBasic::on_actionViewCurrentLog_triggered()
 
 
 void OBSBasic::on_actionCheckForUpdates_triggered()
 void OBSBasic::on_actionCheckForUpdates_triggered()
 {
 {
-	CheckForUpdates();
+	CheckForUpdates(true);
 }
 }
 
 
 void OBSBasic::logUploadFinished(const QString &text, const QString &error)
 void OBSBasic::logUploadFinished(const QString &text, const QString &error)

+ 2 - 2
UI/window-basic-main.hpp

@@ -200,7 +200,7 @@ private:
 	bool          QueryRemoveSource(obs_source_t *source);
 	bool          QueryRemoveSource(obs_source_t *source);
 
 
 	void          TimedCheckForUpdates();
 	void          TimedCheckForUpdates();
-	void          CheckForUpdates();
+	void          CheckForUpdates(bool manualUpdate);
 
 
 	void GetFPSCommon(uint32_t &num, uint32_t &den) const;
 	void GetFPSCommon(uint32_t &num, uint32_t &den) const;
 	void GetFPSInteger(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 logUploadFinished(const QString &text, const QString &error);
 
 
-	void updateFileFinished(const QString &text, const QString &error);
+	void updateCheckFinished();
 
 
 	void AddSourceFromAction();
 	void AddSourceFromAction();
 
 

+ 9 - 0
UI/window-basic-settings.cpp

@@ -273,6 +273,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
 
 
 	HookWidget(ui->language,             COMBO_CHANGED,  GENERAL_CHANGED);
 	HookWidget(ui->language,             COMBO_CHANGED,  GENERAL_CHANGED);
 	HookWidget(ui->theme, 		     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->warnBeforeStreamStart,CHECK_CHANGED,  GENERAL_CHANGED);
 	HookWidget(ui->warnBeforeStreamStop, CHECK_CHANGED,  GENERAL_CHANGED);
 	HookWidget(ui->warnBeforeStreamStop, CHECK_CHANGED,  GENERAL_CHANGED);
 	HookWidget(ui->hideProjectorCursor,  CHECK_CHANGED,  GENERAL_CHANGED);
 	HookWidget(ui->hideProjectorCursor,  CHECK_CHANGED,  GENERAL_CHANGED);
@@ -896,6 +897,10 @@ void OBSBasicSettings::LoadGeneralSettings()
 	LoadLanguageList();
 	LoadLanguageList();
 	LoadThemeList();
 	LoadThemeList();
 
 
+	bool enableAutoUpdates = config_get_bool(GetGlobalConfig(),
+			"General", "EnableAutoUpdates");
+	ui->enableAutoUpdates->setChecked(enableAutoUpdates);
+
 	bool recordWhenStreaming = config_get_bool(GetGlobalConfig(),
 	bool recordWhenStreaming = config_get_bool(GetGlobalConfig(),
 			"BasicWindow", "RecordWhenStreaming");
 			"BasicWindow", "RecordWhenStreaming");
 	ui->recordWhenStreaming->setChecked(recordWhenStreaming);
 	ui->recordWhenStreaming->setChecked(recordWhenStreaming);
@@ -2351,6 +2356,10 @@ void OBSBasicSettings::SaveGeneralSettings()
 		App()->SetTheme(theme);
 		App()->SetTheme(theme);
 	}
 	}
 
 
+	if (WidgetChanged(ui->enableAutoUpdates))
+		config_set_bool(GetGlobalConfig(), "General",
+				"EnableAutoUpdates",
+				ui->enableAutoUpdates->isChecked());
 	if (WidgetChanged(ui->snappingEnabled))
 	if (WidgetChanged(ui->snappingEnabled))
 		config_set_bool(GetGlobalConfig(), "BasicWindow",
 		config_set_bool(GetGlobalConfig(), "BasicWindow",
 				"SnappingEnabled",
 				"SnappingEnabled",