win-update.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. #include "update-helpers.hpp"
  2. #include "shared-update.hpp"
  3. #include "update-window.hpp"
  4. #include "remote-text.hpp"
  5. #include "qt-wrappers.hpp"
  6. #include "win-update.hpp"
  7. #include "obs-app.hpp"
  8. #include <QMessageBox>
  9. #include <string>
  10. #include <mutex>
  11. #define WIN32_LEAN_AND_MEAN
  12. #include <windows.h>
  13. #include <shellapi.h>
  14. #include <util/windows/WinHandle.hpp>
  15. #include <util/util.hpp>
  16. #include <json11.hpp>
  17. #ifdef BROWSER_AVAILABLE
  18. #include <browser-panel.hpp>
  19. #endif
  20. using namespace std;
  21. using namespace json11;
  22. /* ------------------------------------------------------------------------ */
  23. #ifndef WIN_MANIFEST_URL
  24. #define WIN_MANIFEST_URL "https://obsproject.com/update_studio/manifest.json"
  25. #endif
  26. #ifndef WIN_MANIFEST_BASE_URL
  27. #define WIN_MANIFEST_BASE_URL "https://obsproject.com/update_studio/"
  28. #endif
  29. #ifndef WIN_BRANCHES_URL
  30. #define WIN_BRANCHES_URL "https://obsproject.com/update_studio/branches.json"
  31. #endif
  32. #ifndef WIN_DEFAULT_BRANCH
  33. #define WIN_DEFAULT_BRANCH "stable"
  34. #endif
  35. #ifndef WIN_UPDATER_URL
  36. #define WIN_UPDATER_URL "https://obsproject.com/update_studio/updater.exe"
  37. #endif
  38. /* ------------------------------------------------------------------------ */
  39. #if defined(OBS_RELEASE_CANDIDATE) && OBS_RELEASE_CANDIDATE > 0
  40. #define CUR_VER \
  41. ((uint64_t)OBS_RELEASE_CANDIDATE_VER << 16ULL | OBS_RELEASE_CANDIDATE \
  42. << 8ULL)
  43. #define PRE_RELEASE true
  44. #elif OBS_BETA > 0
  45. #define CUR_VER ((uint64_t)OBS_BETA_VER << 16ULL | OBS_BETA)
  46. #define PRE_RELEASE true
  47. #elif defined(OBS_COMMIT)
  48. #define CUR_VER 1 << 16ULL
  49. #define CUR_COMMIT OBS_COMMIT
  50. #define PRE_RELEASE true
  51. #else
  52. #define CUR_VER ((uint64_t)LIBOBS_API_VER << 16ULL)
  53. #define PRE_RELEASE false
  54. #endif
  55. #ifndef CUR_COMMIT
  56. #define CUR_COMMIT "00000000"
  57. #endif
  58. static bool ParseUpdateManifest(const char *manifest, bool *updatesAvailable,
  59. string &notes_str, uint64_t &updateVer,
  60. string &branch)
  61. try {
  62. string error;
  63. Json root = Json::parse(manifest, error);
  64. if (!error.empty())
  65. throw strprintf("Failed reading json string: %s",
  66. error.c_str());
  67. if (!root.is_object())
  68. throw string("Root of manifest is not an object");
  69. int major = root["version_major"].int_value();
  70. int minor = root["version_minor"].int_value();
  71. int patch = root["version_patch"].int_value();
  72. int rc = root["rc"].int_value();
  73. int beta = root["beta"].int_value();
  74. string commit_hash = root["commit"].string_value();
  75. if (major == 0 && commit_hash.empty())
  76. throw strprintf("Invalid version number: %d.%d.%d", major,
  77. minor, patch);
  78. const Json &notes = root["notes"];
  79. if (!notes.is_string())
  80. throw string("'notes' value invalid");
  81. notes_str = notes.string_value();
  82. const Json &packages = root["packages"];
  83. if (!packages.is_array())
  84. throw string("'packages' value invalid");
  85. uint64_t cur_ver;
  86. uint64_t new_ver;
  87. if (commit_hash.empty()) {
  88. cur_ver = CUR_VER;
  89. new_ver = MAKE_SEMANTIC_VERSION(
  90. (uint64_t)major, (uint64_t)minor, (uint64_t)patch);
  91. new_ver <<= 16;
  92. /* RC builds are shifted so that rc1 and beta1 versions do not result
  93. * in the same new_ver. */
  94. if (rc > 0)
  95. new_ver |= (uint64_t)rc << 8;
  96. else if (beta > 0)
  97. new_ver |= (uint64_t)beta;
  98. } else {
  99. /* Test or nightly builds may not have a (valid) version number,
  100. * so compare commit hashes instead. */
  101. cur_ver = stoul(CUR_COMMIT, nullptr, 16);
  102. new_ver = stoul(commit_hash.substr(0, 8), nullptr, 16);
  103. }
  104. updateVer = new_ver;
  105. /* When using a pre-release build or non-default branch we only check if
  106. * the manifest version is different, so that it can be rolled-back. */
  107. if (branch != WIN_DEFAULT_BRANCH || PRE_RELEASE)
  108. *updatesAvailable = new_ver != cur_ver;
  109. else
  110. *updatesAvailable = new_ver > cur_ver;
  111. return true;
  112. } catch (string &text) {
  113. blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str());
  114. return false;
  115. }
  116. #undef CUR_COMMIT
  117. #undef CUR_VER
  118. #undef PRE_RELEASE
  119. /* ------------------------------------------------------------------------ */
  120. bool GetBranchAndUrl(string &selectedBranch, string &manifestUrl)
  121. {
  122. const char *config_branch =
  123. config_get_string(GetGlobalConfig(), "General", "UpdateBranch");
  124. if (!config_branch)
  125. return true;
  126. bool found = false;
  127. for (const UpdateBranch &branch : App()->GetBranches()) {
  128. if (branch.name != config_branch)
  129. continue;
  130. /* A branch that is found but disabled will just silently fall back to
  131. * the default. But if the branch was removed entirely, the user should
  132. * be warned, so leave this false *only* if the branch was removed. */
  133. found = true;
  134. if (branch.is_enabled) {
  135. selectedBranch = branch.name.toStdString();
  136. if (branch.name != WIN_DEFAULT_BRANCH) {
  137. manifestUrl = WIN_MANIFEST_BASE_URL;
  138. manifestUrl += "manifest_" +
  139. branch.name.toStdString() +
  140. ".json";
  141. }
  142. }
  143. break;
  144. }
  145. return found;
  146. }
  147. /* ------------------------------------------------------------------------ */
  148. void AutoUpdateThread::infoMsg(const QString &title, const QString &text)
  149. {
  150. OBSMessageBox::information(App()->GetMainWindow(), title, text);
  151. }
  152. void AutoUpdateThread::info(const QString &title, const QString &text)
  153. {
  154. QMetaObject::invokeMethod(this, "infoMsg", Qt::BlockingQueuedConnection,
  155. Q_ARG(QString, title), Q_ARG(QString, text));
  156. }
  157. int AutoUpdateThread::queryUpdateSlot(bool localManualUpdate,
  158. const QString &text)
  159. {
  160. OBSUpdate updateDlg(App()->GetMainWindow(), localManualUpdate, text);
  161. return updateDlg.exec();
  162. }
  163. int AutoUpdateThread::queryUpdate(bool localManualUpdate, const char *text_utf8)
  164. {
  165. int ret = OBSUpdate::No;
  166. QString text = text_utf8;
  167. QMetaObject::invokeMethod(this, "queryUpdateSlot",
  168. Qt::BlockingQueuedConnection,
  169. Q_RETURN_ARG(int, ret),
  170. Q_ARG(bool, localManualUpdate),
  171. Q_ARG(QString, text));
  172. return ret;
  173. }
  174. bool AutoUpdateThread::queryRepairSlot()
  175. {
  176. QMessageBox::StandardButton res = OBSMessageBox::question(
  177. App()->GetMainWindow(), QTStr("Updater.RepairConfirm.Title"),
  178. QTStr("Updater.RepairConfirm.Text"),
  179. QMessageBox::Yes | QMessageBox::Cancel);
  180. return res == QMessageBox::Yes;
  181. }
  182. bool AutoUpdateThread::queryRepair()
  183. {
  184. bool ret = false;
  185. QMetaObject::invokeMethod(this, "queryRepairSlot",
  186. Qt::BlockingQueuedConnection,
  187. Q_RETURN_ARG(bool, ret));
  188. return ret;
  189. }
  190. void AutoUpdateThread::run()
  191. try {
  192. string text;
  193. string branch = WIN_DEFAULT_BRANCH;
  194. string manifestUrl = WIN_MANIFEST_URL;
  195. vector<string> extraHeaders;
  196. bool updatesAvailable = false;
  197. struct FinishedTrigger {
  198. inline ~FinishedTrigger()
  199. {
  200. QMetaObject::invokeMethod(App()->GetMainWindow(),
  201. "updateCheckFinished");
  202. }
  203. } finishedTrigger;
  204. /* ----------------------------------- *
  205. * get branches from server */
  206. if (FetchAndVerifyFile("branches", "obs-studio\\updates\\branches.json",
  207. WIN_BRANCHES_URL, &text))
  208. App()->SetBranchData(text);
  209. /* ----------------------------------- *
  210. * check branch and get manifest url */
  211. if (!GetBranchAndUrl(branch, manifestUrl)) {
  212. config_set_string(GetGlobalConfig(), "General", "UpdateBranch",
  213. WIN_DEFAULT_BRANCH);
  214. info(QTStr("Updater.BranchNotFound.Title"),
  215. QTStr("Updater.BranchNotFound.Text"));
  216. }
  217. /* allow server to know if this was a manual update check in case
  218. * we want to allow people to bypass a configured rollout rate */
  219. if (manualUpdate)
  220. extraHeaders.emplace_back("X-OBS2-ManualUpdate: 1");
  221. /* ----------------------------------- *
  222. * get manifest from server */
  223. text.clear();
  224. if (!FetchAndVerifyFile("manifest",
  225. "obs-studio\\updates\\manifest.json",
  226. manifestUrl.c_str(), &text, extraHeaders))
  227. return;
  228. /* ----------------------------------- *
  229. * check manifest for update */
  230. string notes;
  231. uint64_t updateVer = 0;
  232. if (!ParseUpdateManifest(text.c_str(), &updatesAvailable, notes,
  233. updateVer, branch))
  234. throw string("Failed to parse manifest");
  235. if (!updatesAvailable && !repairMode) {
  236. if (manualUpdate)
  237. info(QTStr("Updater.NoUpdatesAvailable.Title"),
  238. QTStr("Updater.NoUpdatesAvailable.Text"));
  239. return;
  240. } else if (updatesAvailable && repairMode) {
  241. info(QTStr("Updater.RepairButUpdatesAvailable.Title"),
  242. QTStr("Updater.RepairButUpdatesAvailable.Text"));
  243. return;
  244. }
  245. /* ----------------------------------- *
  246. * skip this version if set to skip */
  247. uint64_t skipUpdateVer = config_get_uint(GetGlobalConfig(), "General",
  248. "SkipUpdateVersion");
  249. if (!manualUpdate && updateVer == skipUpdateVer && !repairMode)
  250. return;
  251. /* ----------------------------------- *
  252. * fetch updater module */
  253. if (!FetchAndVerifyFile("updater", "obs-studio\\updates\\updater.exe",
  254. WIN_UPDATER_URL, nullptr))
  255. return;
  256. /* ----------------------------------- *
  257. * query user for update */
  258. if (repairMode) {
  259. if (!queryRepair())
  260. return;
  261. } else {
  262. int queryResult = queryUpdate(manualUpdate, notes.c_str());
  263. if (queryResult == OBSUpdate::No) {
  264. if (!manualUpdate) {
  265. long long t = (long long)time(nullptr);
  266. config_set_int(GetGlobalConfig(), "General",
  267. "LastUpdateCheck", t);
  268. }
  269. return;
  270. } else if (queryResult == OBSUpdate::Skip) {
  271. config_set_uint(GetGlobalConfig(), "General",
  272. "SkipUpdateVersion", updateVer);
  273. return;
  274. }
  275. }
  276. /* ----------------------------------- *
  277. * get working dir */
  278. wchar_t cwd[MAX_PATH];
  279. GetModuleFileNameW(nullptr, cwd, _countof(cwd) - 1);
  280. wchar_t *p = wcsrchr(cwd, '\\');
  281. if (p)
  282. *p = 0;
  283. /* ----------------------------------- *
  284. * execute updater */
  285. BPtr<char> updateFilePath =
  286. GetConfigPathPtr("obs-studio\\updates\\updater.exe");
  287. BPtr<wchar_t> wUpdateFilePath;
  288. size_t size = os_utf8_to_wcs_ptr(updateFilePath, 0, &wUpdateFilePath);
  289. if (!size)
  290. throw string("Could not convert updateFilePath to wide");
  291. /* note, can't use CreateProcess to launch as admin. */
  292. SHELLEXECUTEINFO execInfo = {};
  293. execInfo.cbSize = sizeof(execInfo);
  294. execInfo.lpFile = wUpdateFilePath;
  295. string parameters = "";
  296. if (App()->IsPortableMode())
  297. parameters += "--portable";
  298. if (branch != WIN_DEFAULT_BRANCH) {
  299. if (!parameters.empty())
  300. parameters += " ";
  301. parameters += "--branch=" + branch;
  302. }
  303. BPtr<wchar_t> lpParameters;
  304. size = os_utf8_to_wcs_ptr(parameters.c_str(), 0, &lpParameters);
  305. if (!size && !parameters.empty())
  306. throw string("Could not convert parameters to wide");
  307. execInfo.lpParameters = lpParameters;
  308. execInfo.lpDirectory = cwd;
  309. execInfo.nShow = SW_SHOWNORMAL;
  310. if (!ShellExecuteEx(&execInfo)) {
  311. QString msg = QTStr("Updater.FailedToLaunch");
  312. info(msg, msg);
  313. throw strprintf("Can't launch updater '%s': %d",
  314. updateFilePath.Get(), GetLastError());
  315. }
  316. /* force OBS to perform another update check immediately after updating
  317. * in case of issues with the new version */
  318. config_set_int(GetGlobalConfig(), "General", "LastUpdateCheck", 0);
  319. config_set_int(GetGlobalConfig(), "General", "SkipUpdateVersion", 0);
  320. QMetaObject::invokeMethod(App()->GetMainWindow(), "close");
  321. } catch (string &text) {
  322. blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str());
  323. }