提交 c1c84e91 编写于 作者: J jp9000

UI: Add front-end auto-updater

上级 33e44940
......@@ -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
......
......@@ -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"
......
......@@ -168,6 +168,9 @@
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="topMargin">
<number>2</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
......@@ -194,6 +197,32 @@
<item row="1" column="1">
<widget class="QComboBox" name="theme"/>
</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>
</widget>
</item>
......@@ -203,6 +232,9 @@
<string>Basic.Settings.Output</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<property name="topMargin">
<number>2</number>
</property>
<item row="0" column="0">
<spacer name="horizontalSpacer_5">
<property name="orientation">
......@@ -211,7 +243,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>170</width>
<height>11</height>
<height>5</height>
</size>
</property>
</spacer>
......
<?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>
......@@ -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))
......
#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);
}
#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;
};
#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;
}
#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, ...);
#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());
}
#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_) {}
};
......@@ -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 <fstream>
......@@ -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)
......
......@@ -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();
......
......@@ -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",
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册