AutoUpdateThread.cpp 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. #include "AutoUpdateThread.hpp"
  2. #include "ui_OBSUpdate.h"
  3. #include <OBSApp.hpp>
  4. #include <dialogs/OBSUpdate.hpp>
  5. #include <updater/manifest.hpp>
  6. #include <utility/WhatsNewInfoThread.hpp>
  7. #include <utility/update-helpers.hpp>
  8. #include <qt-wrappers.hpp>
  9. #define WIN32_LEAN_AND_MEAN
  10. #include <windows.h>
  11. #include <shellapi.h>
  12. #include "moc_AutoUpdateThread.cpp"
  13. /* ------------------------------------------------------------------------ */
  14. #ifndef WIN_MANIFEST_URL
  15. #define WIN_MANIFEST_URL "https://obsproject.com/update_studio/manifest.json"
  16. #endif
  17. #ifndef WIN_MANIFEST_BASE_URL
  18. #define WIN_MANIFEST_BASE_URL "https://obsproject.com/update_studio/"
  19. #endif
  20. #ifndef WIN_BRANCHES_URL
  21. #define WIN_BRANCHES_URL "https://obsproject.com/update_studio/branches.json"
  22. #endif
  23. #ifndef WIN_DEFAULT_BRANCH
  24. #define WIN_DEFAULT_BRANCH "stable"
  25. #endif
  26. #ifndef WIN_UPDATER_URL
  27. #define WIN_UPDATER_URL "https://obsproject.com/update_studio/updater.exe"
  28. #endif
  29. /* ------------------------------------------------------------------------ */
  30. using namespace std;
  31. using namespace updater;
  32. extern char *GetConfigPathPtr(const char *name);
  33. static bool ParseUpdateManifest(const char *manifest_data, bool *updatesAvailable, string &notes, string &updateVer,
  34. const string &branch)
  35. try {
  36. constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL |
  37. OBS_BETA;
  38. constexpr bool isPreRelease = currentVersion & 0xffff || std::char_traits<char>::length(OBS_COMMIT);
  39. json manifestContents = json::parse(manifest_data);
  40. Manifest manifest = manifestContents.get<Manifest>();
  41. if (manifest.version_major == 0 && manifest.commit.empty())
  42. throw strprintf("Invalid version number: %d.%d.%d", manifest.version_major, manifest.version_minor,
  43. manifest.version_patch);
  44. notes = manifest.notes;
  45. if (manifest.commit.empty()) {
  46. uint64_t new_ver = MAKE_SEMANTIC_VERSION((uint64_t)manifest.version_major,
  47. (uint64_t)manifest.version_minor,
  48. (uint64_t)manifest.version_patch);
  49. new_ver <<= 16;
  50. /* RC builds are shifted so that rc1 and beta1 versions do not result
  51. * in the same new_ver. */
  52. if (manifest.rc > 0)
  53. new_ver |= (uint64_t)manifest.rc << 8;
  54. else if (manifest.beta > 0)
  55. new_ver |= (uint64_t)manifest.beta;
  56. updateVer = to_string(new_ver);
  57. /* When using a pre-release build or non-default branch we only check if
  58. * the manifest version is different, so that it can be rolled back. */
  59. if (branch != WIN_DEFAULT_BRANCH || isPreRelease)
  60. *updatesAvailable = new_ver != currentVersion;
  61. else
  62. *updatesAvailable = new_ver > currentVersion;
  63. } else {
  64. /* Test or nightly builds may not have a (valid) version number,
  65. * so compare commit hashes instead. */
  66. updateVer = manifest.commit.substr(0, 8);
  67. *updatesAvailable = !currentVersion || manifest.commit.compare(0, strlen(OBS_COMMIT), OBS_COMMIT) != 0;
  68. }
  69. return true;
  70. } catch (string &text) {
  71. blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str());
  72. return false;
  73. }
  74. /* ------------------------------------------------------------------------ */
  75. bool GetBranchAndUrl(string &selectedBranch, string &manifestUrl)
  76. {
  77. const char *config_branch = config_get_string(App()->GetAppConfig(), "General", "UpdateBranch");
  78. if (!config_branch)
  79. return true;
  80. bool found = false;
  81. for (const UpdateBranch &branch : App()->GetBranches()) {
  82. if (branch.name != config_branch)
  83. continue;
  84. /* A branch that is found but disabled will just silently fall back to
  85. * the default. But if the branch was removed entirely, the user should
  86. * be warned, so leave this false *only* if the branch was removed. */
  87. found = true;
  88. if (branch.is_enabled) {
  89. selectedBranch = branch.name.toStdString();
  90. if (branch.name != WIN_DEFAULT_BRANCH) {
  91. manifestUrl = WIN_MANIFEST_BASE_URL;
  92. manifestUrl += "manifest_" + branch.name.toStdString() + ".json";
  93. }
  94. }
  95. break;
  96. }
  97. return found;
  98. }
  99. /* ------------------------------------------------------------------------ */
  100. void AutoUpdateThread::infoMsg(const QString &title, const QString &text)
  101. {
  102. OBSMessageBox::information(App()->GetMainWindow(), title, text);
  103. }
  104. void AutoUpdateThread::info(const QString &title, const QString &text)
  105. {
  106. QMetaObject::invokeMethod(this, "infoMsg", Qt::BlockingQueuedConnection, Q_ARG(QString, title),
  107. Q_ARG(QString, text));
  108. }
  109. int AutoUpdateThread::queryUpdateSlot(bool localManualUpdate, const QString &text)
  110. {
  111. OBSUpdate updateDlg(App()->GetMainWindow(), localManualUpdate, text);
  112. return updateDlg.exec();
  113. }
  114. int AutoUpdateThread::queryUpdate(bool localManualUpdate, const char *text_utf8)
  115. {
  116. int ret = OBSUpdate::No;
  117. QString text = text_utf8;
  118. QMetaObject::invokeMethod(this, "queryUpdateSlot", Qt::BlockingQueuedConnection, Q_RETURN_ARG(int, ret),
  119. Q_ARG(bool, localManualUpdate), Q_ARG(QString, text));
  120. return ret;
  121. }
  122. bool AutoUpdateThread::queryRepairSlot()
  123. {
  124. QMessageBox::StandardButton res =
  125. OBSMessageBox::question(App()->GetMainWindow(), QTStr("Updater.RepairConfirm.Title"),
  126. QTStr("Updater.RepairConfirm.Text"), QMessageBox::Yes | QMessageBox::Cancel);
  127. return res == QMessageBox::Yes;
  128. }
  129. bool AutoUpdateThread::queryRepair()
  130. {
  131. bool ret = false;
  132. QMetaObject::invokeMethod(this, "queryRepairSlot", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, ret));
  133. return ret;
  134. }
  135. void AutoUpdateThread::run()
  136. try {
  137. string text;
  138. string branch = WIN_DEFAULT_BRANCH;
  139. string manifestUrl = WIN_MANIFEST_URL;
  140. vector<string> extraHeaders;
  141. bool updatesAvailable = false;
  142. struct FinishedTrigger {
  143. inline ~FinishedTrigger() { QMetaObject::invokeMethod(App()->GetMainWindow(), "updateCheckFinished"); }
  144. } finishedTrigger;
  145. /* ----------------------------------- *
  146. * get branches from server */
  147. if (FetchAndVerifyFile("branches", "obs-studio\\updates\\branches.json", WIN_BRANCHES_URL, &text))
  148. App()->SetBranchData(text);
  149. /* ----------------------------------- *
  150. * check branch and get manifest url */
  151. if (!GetBranchAndUrl(branch, manifestUrl)) {
  152. config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", WIN_DEFAULT_BRANCH);
  153. info(QTStr("Updater.BranchNotFound.Title"), QTStr("Updater.BranchNotFound.Text"));
  154. }
  155. /* allow server to know if this was a manual update check in case
  156. * we want to allow people to bypass a configured rollout rate */
  157. if (manualUpdate)
  158. extraHeaders.emplace_back("X-OBS2-ManualUpdate: 1");
  159. /* ----------------------------------- *
  160. * get manifest from server */
  161. text.clear();
  162. if (!FetchAndVerifyFile("manifest", "obs-studio\\updates\\manifest.json", manifestUrl.c_str(), &text,
  163. extraHeaders))
  164. return;
  165. /* ----------------------------------- *
  166. * check manifest for update */
  167. string notes;
  168. string updateVer;
  169. if (!ParseUpdateManifest(text.c_str(), &updatesAvailable, notes, updateVer, branch))
  170. throw string("Failed to parse manifest");
  171. if (!updatesAvailable && !repairMode) {
  172. if (manualUpdate)
  173. info(QTStr("Updater.NoUpdatesAvailable.Title"), QTStr("Updater.NoUpdatesAvailable.Text"));
  174. return;
  175. } else if (updatesAvailable && repairMode) {
  176. info(QTStr("Updater.RepairButUpdatesAvailable.Title"), QTStr("Updater.RepairButUpdatesAvailable.Text"));
  177. return;
  178. }
  179. /* ----------------------------------- *
  180. * skip this version if set to skip */
  181. const char *skipUpdateVer = config_get_string(App()->GetAppConfig(), "General", "SkipUpdateVersion");
  182. if (!manualUpdate && !repairMode && skipUpdateVer && updateVer == skipUpdateVer)
  183. return;
  184. /* ----------------------------------- *
  185. * fetch updater module */
  186. if (!FetchAndVerifyFile("updater", "obs-studio\\updates\\updater.exe", WIN_UPDATER_URL, nullptr))
  187. return;
  188. /* ----------------------------------- *
  189. * query user for update */
  190. if (repairMode) {
  191. if (!queryRepair())
  192. return;
  193. } else {
  194. int queryResult = queryUpdate(manualUpdate, notes.c_str());
  195. if (queryResult == OBSUpdate::No) {
  196. if (!manualUpdate) {
  197. long long t = (long long)time(nullptr);
  198. config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", t);
  199. }
  200. return;
  201. } else if (queryResult == OBSUpdate::Skip) {
  202. config_set_string(App()->GetAppConfig(), "General", "SkipUpdateVersion", updateVer.c_str());
  203. return;
  204. }
  205. }
  206. /* ----------------------------------- *
  207. * get working dir */
  208. wchar_t cwd[MAX_PATH];
  209. GetModuleFileNameW(nullptr, cwd, _countof(cwd) - 1);
  210. wchar_t *p = wcsrchr(cwd, '\\');
  211. if (p)
  212. *p = 0;
  213. /* ----------------------------------- *
  214. * execute updater */
  215. BPtr<char> updateFilePath = GetAppConfigPathPtr("obs-studio\\updates\\updater.exe");
  216. BPtr<wchar_t> wUpdateFilePath;
  217. size_t size = os_utf8_to_wcs_ptr(updateFilePath, 0, &wUpdateFilePath);
  218. if (!size)
  219. throw string("Could not convert updateFilePath to wide");
  220. /* note, can't use CreateProcess to launch as admin. */
  221. SHELLEXECUTEINFO execInfo = {};
  222. execInfo.cbSize = sizeof(execInfo);
  223. execInfo.lpFile = wUpdateFilePath;
  224. string parameters;
  225. if (branch != WIN_DEFAULT_BRANCH)
  226. parameters += "--branch=" + branch;
  227. obs_cmdline_args obs_args = obs_get_cmdline_args();
  228. for (int idx = 1; idx < obs_args.argc; idx++) {
  229. if (!parameters.empty())
  230. parameters += " ";
  231. parameters += obs_args.argv[idx];
  232. }
  233. /* Portable mode can be enabled via sentinel files, so copying the
  234. * command line doesn't guarantee the flag to be there. */
  235. if (App()->IsPortableMode() && parameters.find("--portable") == string::npos) {
  236. if (!parameters.empty())
  237. parameters += " ";
  238. parameters += "--portable";
  239. }
  240. BPtr<wchar_t> lpParameters;
  241. size = os_utf8_to_wcs_ptr(parameters.c_str(), 0, &lpParameters);
  242. if (!size && !parameters.empty())
  243. throw string("Could not convert parameters to wide");
  244. execInfo.lpParameters = lpParameters;
  245. execInfo.lpDirectory = cwd;
  246. execInfo.nShow = SW_SHOWNORMAL;
  247. if (!ShellExecuteEx(&execInfo)) {
  248. QString msg = QTStr("Updater.FailedToLaunch");
  249. info(msg, msg);
  250. throw strprintf("Can't launch updater '%s': %d", updateFilePath.Get(), GetLastError());
  251. }
  252. /* force OBS to perform another update check immediately after updating
  253. * in case of issues with the new version */
  254. config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0);
  255. config_set_string(App()->GetAppConfig(), "General", "SkipUpdateVersion", "0");
  256. QMetaObject::invokeMethod(App()->GetMainWindow(), "close");
  257. } catch (string &text) {
  258. blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str());
  259. }