Platform.cpp 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. #include <Windows.h>
  2. #include <filesystem>
  3. #include <map>
  4. #include <optional>
  5. #include "Common/CommonPaths.h"
  6. #include "Common/FileUtil.h"
  7. #include "Common/HttpRequest.h"
  8. #include "Common/IOFile.h"
  9. #include "Common/ScopeGuard.h"
  10. #include "Common/StringUtil.h"
  11. #include "Common/WindowsRegistry.h"
  12. #include "UpdaterCommon/Platform.h"
  13. #include "UpdaterCommon/UI.h"
  14. #include "UpdaterCommon/UpdaterCommon.h"
  15. namespace Platform
  16. {
  17. struct BuildVersion
  18. {
  19. u32 major{};
  20. u32 minor{};
  21. u32 build{};
  22. auto operator<=>(BuildVersion const& rhs) const = default;
  23. static std::optional<BuildVersion> from_string(const std::string& str)
  24. {
  25. auto components = SplitString(str, '.');
  26. // Allow variable number of components (truncating after "build"), but not
  27. // empty.
  28. if (components.size() == 0)
  29. return {};
  30. BuildVersion version;
  31. if (!TryParse(components[0], &version.major, 10))
  32. return {};
  33. if (components.size() > 1 && !TryParse(components[1], &version.minor, 10))
  34. return {};
  35. if (components.size() > 2 && !TryParse(components[2], &version.build, 10))
  36. return {};
  37. return version;
  38. }
  39. };
  40. enum class VersionCheckStatus
  41. {
  42. NothingToDo,
  43. UpdateOptional,
  44. UpdateRequired,
  45. };
  46. struct VersionCheckResult
  47. {
  48. VersionCheckStatus status{VersionCheckStatus::NothingToDo};
  49. std::optional<BuildVersion> current_version{};
  50. std::optional<BuildVersion> target_version{};
  51. };
  52. class BuildInfo
  53. {
  54. using Map = std::map<std::string, std::string>;
  55. public:
  56. BuildInfo() = default;
  57. BuildInfo(const std::string& content)
  58. {
  59. map = {{"OSMinimumVersionWin10", ""},
  60. {"OSMinimumVersionWin11", ""},
  61. {"VCToolsVersion", ""},
  62. {"VCToolsUpdateURL", ""}};
  63. Parse(content);
  64. }
  65. std::optional<std::string> GetString(const std::string& name) const
  66. {
  67. auto it = map.find(name);
  68. if (it == map.end() || it->second.size() == 0)
  69. return {};
  70. return it->second;
  71. }
  72. std::optional<BuildVersion> GetVersion(const std::string& name) const
  73. {
  74. auto str = GetString(name);
  75. if (!str.has_value())
  76. return {};
  77. return BuildVersion::from_string(str.value());
  78. }
  79. private:
  80. void Parse(const std::string& content)
  81. {
  82. std::stringstream content_stream(content);
  83. std::string line;
  84. while (std::getline(content_stream, line))
  85. {
  86. if (line.starts_with("//"))
  87. continue;
  88. const size_t equals_index = line.find('=');
  89. if (equals_index == line.npos)
  90. continue;
  91. auto key = line.substr(0, equals_index);
  92. auto key_it = map.find(key);
  93. if (key_it == map.end())
  94. continue;
  95. auto val_start = equals_index + 1;
  96. auto eol = line.find('\r', val_start);
  97. auto val_size = (eol == line.npos) ? line.npos : eol - val_start;
  98. key_it->second = line.substr(val_start, val_size);
  99. }
  100. }
  101. Map map;
  102. };
  103. struct BuildInfos
  104. {
  105. BuildInfo current;
  106. BuildInfo next;
  107. };
  108. // This default value should be kept in sync with the value of VCToolsUpdateURL in
  109. // build_info.txt.in
  110. static const char* VCToolsUpdateURLDefault = "https://aka.ms/vs/17/release/vc_redist.x64.exe";
  111. #define VC_RUNTIME_REGKEY R"(SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\)"
  112. static const char* VCRuntimeRegistrySubkey()
  113. {
  114. return VC_RUNTIME_REGKEY
  115. #ifdef _M_X86_64
  116. "x64";
  117. #elif _M_ARM_64
  118. "arm64";
  119. #else
  120. #error unsupported architecture
  121. #endif
  122. }
  123. static bool ReadVCRuntimeVersionField(u32* value, const char* name)
  124. {
  125. return WindowsRegistry::ReadValue(value, VCRuntimeRegistrySubkey(), name);
  126. }
  127. static std::optional<BuildVersion> GetInstalledVCRuntimeVersion()
  128. {
  129. u32 installed;
  130. if (!ReadVCRuntimeVersionField(&installed, "Installed") || !installed)
  131. return {};
  132. BuildVersion version;
  133. if (!ReadVCRuntimeVersionField(&version.major, "Major") ||
  134. !ReadVCRuntimeVersionField(&version.minor, "Minor") ||
  135. !ReadVCRuntimeVersionField(&version.build, "Bld"))
  136. {
  137. return {};
  138. }
  139. return version;
  140. }
  141. static VersionCheckResult VCRuntimeVersionCheck(const BuildInfos& build_infos)
  142. {
  143. VersionCheckResult result;
  144. result.current_version = GetInstalledVCRuntimeVersion();
  145. result.target_version = build_infos.next.GetVersion("VCToolsVersion");
  146. auto existing_version = build_infos.current.GetVersion("VCToolsVersion");
  147. if (!result.target_version.has_value())
  148. result.status = VersionCheckStatus::UpdateOptional;
  149. else if (!result.current_version.has_value() || result.current_version < result.target_version)
  150. result.status = VersionCheckStatus::UpdateRequired;
  151. // See if the current build was already running on acceptable version of the runtime. This could
  152. // happen if the user manually copied the redist DLLs and got Dolphin running that way.
  153. if (existing_version.has_value() && existing_version >= result.target_version)
  154. result.status = VersionCheckStatus::NothingToDo;
  155. return result;
  156. }
  157. static bool VCRuntimeUpdate(const BuildInfo& build_info)
  158. {
  159. UI::SetDescription("Updating VC++ Redist, please wait...");
  160. Common::HttpRequest req;
  161. req.FollowRedirects(10);
  162. auto resp = req.Get(build_info.GetString("VCToolsUpdateURL").value_or(VCToolsUpdateURLDefault));
  163. if (!resp)
  164. return false;
  165. // Write it to current working directory.
  166. auto redist_path = std::filesystem::current_path() / L"vc_redist.x64.exe";
  167. auto redist_path_u8 = WStringToUTF8(redist_path.wstring());
  168. File::IOFile redist_file;
  169. redist_file.Open(redist_path_u8, "wb");
  170. if (!redist_file.WriteBytes(resp->data(), resp->size()))
  171. return false;
  172. redist_file.Close();
  173. Common::ScopeGuard redist_deleter([&] { File::Delete(redist_path_u8); });
  174. // The installer also supports /passive and /quiet. We normally pass neither (the
  175. // exception being test automation) to allow the user to see and interact with the installer.
  176. std::wstring cmdline = redist_path.filename().wstring() + L" /install /norestart";
  177. if (UI::IsTestMode())
  178. cmdline += L" /passive /quiet";
  179. STARTUPINFOW startup_info{.cb = sizeof(startup_info)};
  180. PROCESS_INFORMATION process_info;
  181. if (!CreateProcessW(redist_path.c_str(), cmdline.data(), nullptr, nullptr, TRUE, 0, nullptr,
  182. nullptr, &startup_info, &process_info))
  183. {
  184. return false;
  185. }
  186. CloseHandle(process_info.hThread);
  187. // Wait for it to run
  188. WaitForSingleObject(process_info.hProcess, INFINITE);
  189. DWORD exit_code;
  190. bool has_exit_code = GetExitCodeProcess(process_info.hProcess, &exit_code);
  191. CloseHandle(process_info.hProcess);
  192. // NOTE: Some nonzero exit codes can still be considered success (e.g. if installation was
  193. // bypassed because the same version already installed).
  194. return has_exit_code &&
  195. (exit_code == ERROR_SUCCESS || exit_code == ERROR_SUCCESS_REBOOT_REQUIRED);
  196. }
  197. static BuildVersion CurrentOSVersion()
  198. {
  199. OSVERSIONINFOW info = WindowsRegistry::GetOSVersion();
  200. return {.major = info.dwMajorVersion, .minor = info.dwMinorVersion, .build = info.dwBuildNumber};
  201. }
  202. static VersionCheckResult OSVersionCheck(const BuildInfo& build_info)
  203. {
  204. VersionCheckResult result;
  205. result.current_version = CurrentOSVersion();
  206. constexpr BuildVersion WIN11_BASE{10, 0, 22000};
  207. const char* version_name =
  208. (result.current_version >= WIN11_BASE) ? "OSMinimumVersionWin11" : "OSMinimumVersionWin10";
  209. result.target_version = build_info.GetVersion(version_name);
  210. if (!result.target_version.has_value() || result.current_version >= result.target_version)
  211. result.status = VersionCheckStatus::NothingToDo;
  212. else
  213. result.status = VersionCheckStatus::UpdateRequired;
  214. return result;
  215. }
  216. std::optional<BuildInfos> InitBuildInfos(const std::vector<TodoList::UpdateOp>& to_update,
  217. const std::string& install_base_path,
  218. const std::string& temp_dir)
  219. {
  220. const auto op_it = std::find_if(to_update.cbegin(), to_update.cend(),
  221. [&](const auto& op) { return op.filename == "build_info.txt"; });
  222. if (op_it == to_update.cend())
  223. return {};
  224. const auto op = *op_it;
  225. std::string build_info_path =
  226. temp_dir + DIR_SEP + HexEncode(op.new_hash.data(), op.new_hash.size());
  227. std::string build_info_content;
  228. if (!File::ReadFileToString(build_info_path, build_info_content) ||
  229. op.new_hash != ComputeHash(build_info_content))
  230. {
  231. LogToFile("Failed to read %s\n.", build_info_path.c_str());
  232. return {};
  233. }
  234. BuildInfos build_infos;
  235. build_infos.next = Platform::BuildInfo(build_info_content);
  236. build_info_path = install_base_path + DIR_SEP + "build_info.txt";
  237. build_infos.current = Platform::BuildInfo();
  238. if (File::ReadFileToString(build_info_path, build_info_content))
  239. {
  240. if (op.old_hash != ComputeHash(build_info_content))
  241. LogToFile("Using modified existing BuildInfo %s.\n", build_info_path.c_str());
  242. build_infos.current = Platform::BuildInfo(build_info_content);
  243. }
  244. return build_infos;
  245. }
  246. bool CheckBuildInfo(const BuildInfos& build_infos)
  247. {
  248. // The existing BuildInfo may have been modified. Be careful not to overly trust its contents!
  249. // If the binary requires more recent OS, inform the user.
  250. auto os_check = OSVersionCheck(build_infos.next);
  251. if (os_check.status == VersionCheckStatus::UpdateRequired)
  252. {
  253. UI::Error("Please update Windows in order to update Dolphin.");
  254. return false;
  255. }
  256. // Check if application being launched needs more recent version of VC Redist. If so, download
  257. // latest updater and execute it.
  258. auto vc_check = VCRuntimeVersionCheck(build_infos);
  259. const auto is_test_mode = UI::IsTestMode();
  260. if (vc_check.status != VersionCheckStatus::NothingToDo || is_test_mode)
  261. {
  262. auto update_ok = VCRuntimeUpdate(build_infos.next);
  263. if (!update_ok && is_test_mode)
  264. {
  265. // For now, only check return value when test automation is running.
  266. // The vc_redist exe may return other non-zero status that we don't check for, yet.
  267. return false;
  268. }
  269. vc_check = VCRuntimeVersionCheck(build_infos);
  270. if (vc_check.status == VersionCheckStatus::UpdateRequired)
  271. {
  272. // The update is required and the install failed for some reason.
  273. UI::Error("Please update VC++ Runtime in order to update Dolphin.");
  274. return false;
  275. }
  276. }
  277. return true;
  278. }
  279. bool VersionCheck(const std::vector<TodoList::UpdateOp>& to_update,
  280. const std::string& install_base_path, const std::string& temp_dir)
  281. {
  282. auto build_infos = InitBuildInfos(to_update, install_base_path, temp_dir);
  283. // If there's no build info, it means the check should be skipped.
  284. if (!build_infos.has_value())
  285. {
  286. return true;
  287. }
  288. return CheckBuildInfo(build_infos.value());
  289. }
  290. } // namespace Platform