#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 #include #include #define WIN32_LEAN_AND_MEAN #include #include #include #include #ifdef BROWSER_AVAILABLE #include #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 ¬es, 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::length(OBS_COMMIT); json manifestContents = json::parse(manifest_data); Manifest manifest = manifestContents.get(); 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 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 updateFilePath = GetConfigPathPtr("obs-studio\\updates\\updater.exe"); BPtr wUpdateFilePath; size_t size = os_utf8_to_wcs_ptr(updateFilePath, 0, &wUpdateFilePath); if (!size) throw string("Could not convert updateFilePath to wide"); /* note, can't use CreateProcess to launch as admin. */ SHELLEXECUTEINFO execInfo = {}; execInfo.cbSize = sizeof(execInfo); execInfo.lpFile = wUpdateFilePath; 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 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()); }