obs-studio/UI/update/win-update.cpp

378 lines
10 KiB
C++

#include "../win-update/updater/manifest.hpp"
#include "update-helpers.hpp"
#include "shared-update.hpp"
#include "update-window.hpp"
#include "remote-text.hpp"
#include "qt-wrappers.hpp"
#include "win-update.hpp"
#include "obs-app.hpp"
#include <QMessageBox>
#include <string>
#include <mutex>
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <shellapi.h>
#include <util/windows/WinHandle.hpp>
#include <util/util.hpp>
#ifdef BROWSER_AVAILABLE
#include <browser-panel.hpp>
#endif
using namespace std;
using namespace updater;
/* ------------------------------------------------------------------------ */
#ifndef WIN_MANIFEST_URL
#define WIN_MANIFEST_URL "https://obsproject.com/update_studio/manifest.json"
#endif
#ifndef WIN_MANIFEST_BASE_URL
#define WIN_MANIFEST_BASE_URL "https://obsproject.com/update_studio/"
#endif
#ifndef WIN_BRANCHES_URL
#define WIN_BRANCHES_URL "https://obsproject.com/update_studio/branches.json"
#endif
#ifndef WIN_DEFAULT_BRANCH
#define WIN_DEFAULT_BRANCH "stable"
#endif
#ifndef WIN_UPDATER_URL
#define WIN_UPDATER_URL "https://obsproject.com/update_studio/updater.exe"
#endif
/* ------------------------------------------------------------------------ */
static bool ParseUpdateManifest(const char *manifest_data,
bool *updatesAvailable, string &notes,
string &updateVer, const string &branch)
try {
constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL |
OBS_RELEASE_CANDIDATE << 8ULL |
OBS_BETA;
constexpr bool isPreRelease =
currentVersion & 0xffff ||
std::char_traits<char>::length(OBS_COMMIT);
json manifestContents = json::parse(manifest_data);
Manifest manifest = manifestContents.get<Manifest>();
if (manifest.version_major == 0 && manifest.commit.empty())
throw strprintf("Invalid version number: %d.%d.%d",
manifest.version_major, manifest.version_minor,
manifest.version_patch);
notes = manifest.notes;
if (manifest.commit.empty()) {
uint64_t new_ver =
MAKE_SEMANTIC_VERSION((uint64_t)manifest.version_major,
(uint64_t)manifest.version_minor,
(uint64_t)manifest.version_patch);
new_ver <<= 16;
/* RC builds are shifted so that rc1 and beta1 versions do not result
* in the same new_ver. */
if (manifest.rc > 0)
new_ver |= (uint64_t)manifest.rc << 8;
else if (manifest.beta > 0)
new_ver |= (uint64_t)manifest.beta;
updateVer = to_string(new_ver);
/* When using a pre-release build or non-default branch we only check if
* the manifest version is different, so that it can be rolled back. */
if (branch != WIN_DEFAULT_BRANCH || isPreRelease)
*updatesAvailable = new_ver != currentVersion;
else
*updatesAvailable = new_ver > currentVersion;
} else {
/* Test or nightly builds may not have a (valid) version number,
* so compare commit hashes instead. */
updateVer = manifest.commit.substr(0, 8);
*updatesAvailable = !currentVersion ||
!manifest.commit.compare(
0, strlen(OBS_COMMIT), OBS_COMMIT);
}
return true;
} catch (string &text) {
blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str());
return false;
}
/* ------------------------------------------------------------------------ */
bool GetBranchAndUrl(string &selectedBranch, string &manifestUrl)
{
const char *config_branch =
config_get_string(GetGlobalConfig(), "General", "UpdateBranch");
if (!config_branch)
return true;
bool found = false;
for (const UpdateBranch &branch : App()->GetBranches()) {
if (branch.name != config_branch)
continue;
/* A branch that is found but disabled will just silently fall back to
* the default. But if the branch was removed entirely, the user should
* be warned, so leave this false *only* if the branch was removed. */
found = true;
if (branch.is_enabled) {
selectedBranch = branch.name.toStdString();
if (branch.name != WIN_DEFAULT_BRANCH) {
manifestUrl = WIN_MANIFEST_BASE_URL;
manifestUrl += "manifest_" +
branch.name.toStdString() +
".json";
}
}
break;
}
return found;
}
/* ------------------------------------------------------------------------ */
void AutoUpdateThread::infoMsg(const QString &title, const QString &text)
{
OBSMessageBox::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 localManualUpdate,
const QString &text)
{
OBSUpdate updateDlg(App()->GetMainWindow(), localManualUpdate, text);
return updateDlg.exec();
}
int AutoUpdateThread::queryUpdate(bool localManualUpdate, 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, localManualUpdate),
Q_ARG(QString, text));
return ret;
}
bool AutoUpdateThread::queryRepairSlot()
{
QMessageBox::StandardButton res = OBSMessageBox::question(
App()->GetMainWindow(), QTStr("Updater.RepairConfirm.Title"),
QTStr("Updater.RepairConfirm.Text"),
QMessageBox::Yes | QMessageBox::Cancel);
return res == QMessageBox::Yes;
}
bool AutoUpdateThread::queryRepair()
{
bool ret = false;
QMetaObject::invokeMethod(this, "queryRepairSlot",
Qt::BlockingQueuedConnection,
Q_RETURN_ARG(bool, ret));
return ret;
}
void AutoUpdateThread::run()
try {
string text;
string branch = WIN_DEFAULT_BRANCH;
string manifestUrl = WIN_MANIFEST_URL;
vector<string> extraHeaders;
bool updatesAvailable = false;
struct FinishedTrigger {
inline ~FinishedTrigger()
{
QMetaObject::invokeMethod(App()->GetMainWindow(),
"updateCheckFinished");
}
} finishedTrigger;
/* ----------------------------------- *
* get branches from server */
if (FetchAndVerifyFile("branches", "obs-studio\\updates\\branches.json",
WIN_BRANCHES_URL, &text))
App()->SetBranchData(text);
/* ----------------------------------- *
* check branch and get manifest url */
if (!GetBranchAndUrl(branch, manifestUrl)) {
config_set_string(GetGlobalConfig(), "General", "UpdateBranch",
WIN_DEFAULT_BRANCH);
info(QTStr("Updater.BranchNotFound.Title"),
QTStr("Updater.BranchNotFound.Text"));
}
/* allow server to know if this was a manual update check in case
* we want to allow people to bypass a configured rollout rate */
if (manualUpdate)
extraHeaders.emplace_back("X-OBS2-ManualUpdate: 1");
/* ----------------------------------- *
* get manifest from server */
text.clear();
if (!FetchAndVerifyFile("manifest",
"obs-studio\\updates\\manifest.json",
manifestUrl.c_str(), &text, extraHeaders))
return;
/* ----------------------------------- *
* check manifest for update */
string notes;
string updateVer;
if (!ParseUpdateManifest(text.c_str(), &updatesAvailable, notes,
updateVer, branch))
throw string("Failed to parse manifest");
if (!updatesAvailable && !repairMode) {
if (manualUpdate)
info(QTStr("Updater.NoUpdatesAvailable.Title"),
QTStr("Updater.NoUpdatesAvailable.Text"));
return;
} else if (updatesAvailable && repairMode) {
info(QTStr("Updater.RepairButUpdatesAvailable.Title"),
QTStr("Updater.RepairButUpdatesAvailable.Text"));
return;
}
/* ----------------------------------- *
* skip this version if set to skip */
const char *skipUpdateVer = config_get_string(
GetGlobalConfig(), "General", "SkipUpdateVersion");
if (!manualUpdate && !repairMode && skipUpdateVer &&
updateVer == skipUpdateVer)
return;
/* ----------------------------------- *
* fetch updater module */
if (!FetchAndVerifyFile("updater", "obs-studio\\updates\\updater.exe",
WIN_UPDATER_URL, nullptr))
return;
/* ----------------------------------- *
* query user for update */
if (repairMode) {
if (!queryRepair())
return;
} else {
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_string(GetGlobalConfig(), "General",
"SkipUpdateVersion",
updateVer.c_str());
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;
string parameters;
if (branch != WIN_DEFAULT_BRANCH)
parameters += "--branch=" + branch;
obs_cmdline_args obs_args = obs_get_cmdline_args();
for (int idx = 1; idx < obs_args.argc; idx++) {
if (!parameters.empty())
parameters += " ";
parameters += obs_args.argv[idx];
}
/* Portable mode can be enabled via sentinel files, so copying the
* command line doesn't guarantee the flag to be there. */
if (App()->IsPortableMode() &&
parameters.find("--portable") == string::npos) {
if (!parameters.empty())
parameters += " ";
parameters += "--portable";
}
BPtr<wchar_t> lpParameters;
size = os_utf8_to_wcs_ptr(parameters.c_str(), 0, &lpParameters);
if (!size && !parameters.empty())
throw string("Could not convert parameters to wide");
execInfo.lpParameters = lpParameters;
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.Get(), 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_string(GetGlobalConfig(), "General", "SkipUpdateVersion",
"0");
QMetaObject::invokeMethod(App()->GetMainWindow(), "close");
} catch (string &text) {
blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str());
}