diff --git a/CMake/FindPowerShell.cmake b/CMake/FindPowerShell.cmake new file mode 100644 index 0000000000..c0e92a269e --- /dev/null +++ b/CMake/FindPowerShell.cmake @@ -0,0 +1,4 @@ +find_program(POWERSHELL_EXE NAMES powershell) + +INCLUDE(FindPackageHandleStandardArgs) +find_package_handle_standard_args(PowerShell DEFAULT_MSG POWERSHELL_EXE) diff --git a/Source/Core/Common/CMakeLists.txt b/Source/Core/Common/CMakeLists.txt index 134d38062c..4dc764408d 100644 --- a/Source/Core/Common/CMakeLists.txt +++ b/Source/Core/Common/CMakeLists.txt @@ -324,6 +324,17 @@ if(UNIX) target_link_libraries(traversal_server PRIVATE ${SYSTEMD_LIBRARIES}) endif() elseif(WIN32) + find_package(PowerShell REQUIRED) + execute_process( + COMMAND ${POWERSHELL_EXE} -Command "[System.Diagnostics.FileVersionInfo]::GetVersionInfo('$ENV{VCToolsRedistDir}vc_redist.x64.exe').ProductVersion" + OUTPUT_VARIABLE VC_TOOLS_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/build_info.txt.in" + "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/build_info.txt" + ) + target_link_libraries(common PRIVATE "-INCLUDE:enableCompatPatches") endif() diff --git a/Source/Core/Common/build_info.txt.in b/Source/Core/Common/build_info.txt.in new file mode 100644 index 0000000000..3ae938cfc5 --- /dev/null +++ b/Source/Core/Common/build_info.txt.in @@ -0,0 +1,24 @@ +// Indicate the minimum OS version required for the binary to run properly. +// Updater will fail the update if the user does not meet this requirement. +OSMinimumVersionWin10=10.0.15063.0 +OSMinimumVersionWin11=10.0.22000.0 +OSMinimumVersionMacOS=10.14 + +// This is the runtime which was compiled against - providing a way for Updater to detect if update +// is needed before executing this binary. Note that, annoyingly, the version in environment +// variables does not match the "real" version. Consider: +// VersionInfo : File: C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Redist\MSVC\14.32.31326\vc_redist.x64.exe +// InternalName: setup +// OriginalFilename: VC_redist.x64.exe +// FileVersion: 14.32.31332.0 +// FileDescription: Microsoft Visual C++ 2015-2022 Redistributable (x64) - 14.32.31332 +// Product: Microsoft Visual C++ 2015-2022 Redistributable (x64) - 14.32.31332 +// ProductVersion: 14.32.31332.0 +// Whereas the environment variables look like: +// VCToolsInstallDir=C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\14.33.31629\ +// VCToolsRedistDir=C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Redist\MSVC\14.32.31326\ +// VCToolsVersion=14.33.31629 +// We're really looking for "14.32.31332.0" (because that's what will appear in the registry once +// installed), NOT the other values! +VCToolsVersion=${VC_TOOLS_VERSION} +VCToolsUpdateURL=https://aka.ms/vs/17/release/vc_redist.x64.exe diff --git a/Source/Core/DolphinLib.vcxproj b/Source/Core/DolphinLib.vcxproj index ae19db8c19..1aad7a65c3 100644 --- a/Source/Core/DolphinLib.vcxproj +++ b/Source/Core/DolphinLib.vcxproj @@ -60,4 +60,38 @@ + + Common\build_info.txt.in + $(BinaryOutputDir)build_info.txt + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Source/Core/MacUpdater/MacUI.mm b/Source/Core/MacUpdater/MacUI.mm index a2ac4a4a83..0b2699b1dd 100644 --- a/Source/Core/MacUpdater/MacUI.mm +++ b/Source/Core/MacUpdater/MacUI.mm @@ -3,6 +3,7 @@ #include "MacUpdater/ViewController.h" +#include "UpdaterCommon/Platform.h" #include "UpdaterCommon/UI.h" #include @@ -136,3 +137,21 @@ void UI::Stop() void UI::Init() { } + +Platform::BuildInfo::BuildInfo(const std::string& content) +{ + map = {{"OSMinimumVersionMacOS", ""}}; + Parse(content); +} + +bool Platform::VersionCheck(const BuildInfo& this_build_info, const BuildInfo& next_build_info) +{ + // TODO implement OS Minimum Version check + // It should go something like this: + // auto target_version = next_build_info.GetVersion("OSMinimumVersionMacOS"); + // if (!target_version.has_value() || current_version >= target_version) + // return true; + // show error + // return false; + return true; +} diff --git a/Source/Core/UICommon/AutoUpdate.cpp b/Source/Core/UICommon/AutoUpdate.cpp index de8d426c7f..814f66c87b 100644 --- a/Source/Core/UICommon/AutoUpdate.cpp +++ b/Source/Core/UICommon/AutoUpdate.cpp @@ -246,11 +246,11 @@ void AutoUpdateChecker::TriggerUpdate(const AutoUpdateChecker::NewVersionInforma #endif // Run the updater! - const std::string command_line = MakeUpdaterCommandLine(updater_flags); + std::string command_line = MakeUpdaterCommandLine(updater_flags); INFO_LOG_FMT(COMMON, "Updater command line: {}", command_line); #ifdef _WIN32 - STARTUPINFO sinfo = {sizeof(sinfo)}; + STARTUPINFO sinfo{.cb = sizeof(sinfo)}; sinfo.dwFlags = STARTF_FORCEOFFFEEDBACK; // No hourglass cursor after starting the process. PROCESS_INFORMATION pinfo; if (CreateProcessW(UTF8ToWString(reloc_updater_path).c_str(), UTF8ToWString(command_line).data(), diff --git a/Source/Core/UpdaterCommon/Platform.h b/Source/Core/UpdaterCommon/Platform.h new file mode 100644 index 0000000000..9489ba9947 --- /dev/null +++ b/Source/Core/UpdaterCommon/Platform.h @@ -0,0 +1,100 @@ +// Copyright 2018 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Common/StringUtil.h" + +namespace Platform +{ +struct BuildVersion +{ + u32 major{}; + u32 minor{}; + u32 build{}; + auto operator<=>(BuildVersion const& rhs) const = default; + static std::optional from_string(const std::string& str) + { + auto components = SplitString(str, '.'); + // Allow variable number of components (truncating after "build"), but not + // empty. + if (components.size() == 0) + return {}; + BuildVersion version; + if (!TryParse(components[0], &version.major, 10)) + return {}; + if (components.size() > 1 && !TryParse(components[1], &version.minor, 10)) + return {}; + if (components.size() > 2 && !TryParse(components[2], &version.build, 10)) + return {}; + return version; + } +}; + +enum class VersionCheckStatus +{ + NothingToDo, + UpdateOptional, + UpdateRequired, +}; + +struct VersionCheckResult +{ + VersionCheckStatus status{VersionCheckStatus::NothingToDo}; + std::optional current_version{}; + std::optional target_version{}; +}; + +class BuildInfo +{ + using Map = std::map; + +public: + BuildInfo() = default; + BuildInfo(const std::string& content); + + std::optional GetString(const std::string& name) const + { + auto it = map.find(name); + if (it == map.end() || it->second.size() == 0) + return {}; + return it->second; + } + + std::optional GetVersion(const std::string& name) const + { + auto str = GetString(name); + if (!str.has_value()) + return {}; + return BuildVersion::from_string(str.value()); + } + +private: + void Parse(const std::string& content) + { + std::stringstream content_stream(content); + std::string line; + while (std::getline(content_stream, line)) + { + if (line.starts_with("//")) + continue; + const size_t equals_index = line.find('='); + if (equals_index == line.npos) + continue; + auto key = line.substr(0, equals_index); + auto key_it = map.find(key); + if (key_it == map.end()) + continue; + key_it->second = line.substr(equals_index + 1); + } + } + Map map; +}; + +bool VersionCheck(const BuildInfo& this_build_info, const BuildInfo& next_build_info); +} // namespace Platform diff --git a/Source/Core/UpdaterCommon/UpdaterCommon.cpp b/Source/Core/UpdaterCommon/UpdaterCommon.cpp index fb9606ca49..dee55965bf 100644 --- a/Source/Core/UpdaterCommon/UpdaterCommon.cpp +++ b/Source/Core/UpdaterCommon/UpdaterCommon.cpp @@ -18,6 +18,7 @@ #include "Common/HttpRequest.h" #include "Common/ScopeGuard.h" #include "Common/StringUtil.h" +#include "UpdaterCommon/Platform.h" #include "UpdaterCommon/UI.h" #ifndef _WIN32 @@ -278,6 +279,41 @@ bool DownloadContent(const std::vector& to_download, return true; } +bool PlatformVersionCheck(const std::vector& to_update, + const std::string& install_base_path, const std::string& temp_dir) +{ + UI::SetDescription("Checking platform..."); + + const auto op_it = std::find_if(to_update.cbegin(), to_update.cend(), + [&](const auto& op) { return op.filename == "build_info.txt"; }); + if (op_it == to_update.cend()) + return true; + + const auto op = *op_it; + std::string build_info_path = + temp_dir + DIR_SEP + HexEncode(op.new_hash.data(), op.new_hash.size()); + std::string build_info_content; + if (!File::ReadFileToString(build_info_path, build_info_content) || + op.new_hash != ComputeHash(build_info_content)) + { + fprintf(log_fp, "Failed to read %s\n.", build_info_path.c_str()); + return false; + } + auto next_build_info = Platform::BuildInfo(build_info_content); + + build_info_path = install_base_path + DIR_SEP + "build_info.txt"; + auto this_build_info = Platform::BuildInfo(); + if (File::ReadFileToString(build_info_path, build_info_content)) + { + if (op.old_hash != ComputeHash(build_info_content)) + fprintf(log_fp, "Using modified existing BuildInfo %s.\n", build_info_path.c_str()); + this_build_info = Platform::BuildInfo(build_info_content); + } + + // The existing BuildInfo may have been modified. Be careful not to overly trust its contents! + return Platform::VersionCheck(this_build_info, next_build_info); +} + TodoList ComputeActionsToDo(Manifest this_manifest, Manifest next_manifest) { TodoList todo; @@ -474,6 +510,11 @@ bool PerformUpdate(const TodoList& todo, const std::string& install_base_path, return false; fprintf(log_fp, "Download step completed.\n"); + fprintf(log_fp, "Starting platform version check step...\n"); + if (!PlatformVersionCheck(todo.to_update, install_base_path, temp_path)) + return false; + fprintf(log_fp, "Platform version check step completed.\n"); + fprintf(log_fp, "Starting update step...\n"); if (!UpdateFiles(todo.to_update, install_base_path, temp_path)) return false; diff --git a/Source/Core/WinUpdater/CMakeLists.txt b/Source/Core/WinUpdater/CMakeLists.txt index a57d039209..23135d706f 100644 --- a/Source/Core/WinUpdater/CMakeLists.txt +++ b/Source/Core/WinUpdater/CMakeLists.txt @@ -2,6 +2,7 @@ set (MANIFEST_FILE Updater.exe.manifest) add_executable(winupdater WIN32 Main.cpp + Platform.cpp WinUI.cpp ${MANIFEST_FILE}) diff --git a/Source/Core/WinUpdater/Main.cpp b/Source/Core/WinUpdater/Main.cpp index 71e67ecf1f..caa9f40958 100644 --- a/Source/Core/WinUpdater/Main.cpp +++ b/Source/Core/WinUpdater/Main.cpp @@ -21,10 +21,10 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine { if (lstrlenW(pCmdLine) == 0) { - MessageBox(nullptr, - L"This updater is not meant to be launched directly. Configure Auto-Update in " - "Dolphin's settings instead.", - L"Error", MB_ICONERROR); + MessageBoxW(nullptr, + L"This updater is not meant to be launched directly. Configure Auto-Update in " + "Dolphin's settings instead.", + L"Error", MB_ICONERROR); return 1; } diff --git a/Source/Core/WinUpdater/Platform.cpp b/Source/Core/WinUpdater/Platform.cpp new file mode 100644 index 0000000000..8c79479560 --- /dev/null +++ b/Source/Core/WinUpdater/Platform.cpp @@ -0,0 +1,187 @@ +#include + +#include +#include +#include + +#include "Common/FileUtil.h" +#include "Common/HttpRequest.h" +#include "Common/IOFile.h" +#include "Common/ScopeGuard.h" +#include "Common/StringUtil.h" + +#include "UpdaterCommon/Platform.h" +#include "UpdaterCommon/UI.h" + +namespace Platform +{ +BuildInfo::BuildInfo(const std::string& content) +{ + map = {{"OSMinimumVersionWin10", ""}, + {"OSMinimumVersionWin11", ""}, + {"VCToolsVersion", ""}, + {"VCToolsUpdateURL", ""}}; + Parse(content); +} + +// This default value should be kept in sync with the value of VCToolsUpdateURL in +// build_info.txt.in +static const char* VCToolsUpdateURLDefault = "https://aka.ms/vs/17/release/vc_redist.x64.exe"; +#define VC_RUNTIME_REGKEY R"(SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\)" + +static const char* VCRuntimeRegistrySubkey() +{ + return VC_RUNTIME_REGKEY +#ifdef _M_X86_64 + "x64"; +#elif _M_ARM_64 + "arm64"; +#else +#error unsupported architecture +#endif +} + +static bool ReadVCRuntimeVersionField(u32* value, const char* name) +{ + DWORD value_len = sizeof(*value); + return RegGetValueA(HKEY_LOCAL_MACHINE, VCRuntimeRegistrySubkey(), name, RRF_RT_REG_DWORD, + nullptr, value, &value_len) == ERROR_SUCCESS; +} + +static std::optional GetInstalledVCRuntimeVersion() +{ + u32 installed; + if (!ReadVCRuntimeVersionField(&installed, "Installed") || !installed) + return {}; + BuildVersion version; + if (!ReadVCRuntimeVersionField(&version.major, "Major") || + !ReadVCRuntimeVersionField(&version.minor, "Minor") || + !ReadVCRuntimeVersionField(&version.build, "Bld")) + { + return {}; + } + return version; +} + +static VersionCheckResult VCRuntimeVersionCheck(const BuildInfo& this_build_info, + const BuildInfo& next_build_info) +{ + VersionCheckResult result; + result.current_version = GetInstalledVCRuntimeVersion(); + result.target_version = next_build_info.GetVersion("VCToolsVersion"); + + auto existing_version = this_build_info.GetVersion("VCToolsVersion"); + + if (!result.target_version.has_value()) + result.status = VersionCheckStatus::UpdateOptional; + else if (!result.current_version.has_value() || result.current_version < result.target_version) + result.status = VersionCheckStatus::UpdateRequired; + + // See if the current build was already running on acceptable version of the runtime. This could + // happen if the user manually copied the redist DLLs and got Dolphin running that way. + if (existing_version.has_value() && existing_version >= result.target_version) + result.status = VersionCheckStatus::NothingToDo; + + return result; +} + +static bool VCRuntimeUpdate(const BuildInfo& build_info) +{ + UI::SetDescription("Updating VC++ Redist, please wait..."); + + Common::HttpRequest req; + req.FollowRedirects(10); + auto resp = req.Get(build_info.GetString("VCToolsUpdateURL").value_or(VCToolsUpdateURLDefault)); + if (!resp) + return false; + + // Write it to current working directory. + auto redist_path = std::filesystem::current_path() / L"vc_redist.x64.exe"; + auto redist_path_u8 = WStringToUTF8(redist_path.wstring()); + File::IOFile redist_file; + redist_file.Open(redist_path_u8, "wb"); + if (!redist_file.WriteBytes(resp->data(), resp->size())) + return false; + redist_file.Close(); + + Common::ScopeGuard redist_deleter([&] { File::Delete(redist_path_u8); }); + + // The installer also supports /passive and /quiet. We pass neither to allow the user to see and + // interact with the installer. + std::wstring cmdline = redist_path.filename().wstring() + L" /install /norestart"; + STARTUPINFOW startup_info{.cb = sizeof(startup_info)}; + PROCESS_INFORMATION process_info; + if (!CreateProcessW(redist_path.c_str(), cmdline.data(), nullptr, nullptr, TRUE, 0, nullptr, + nullptr, &startup_info, &process_info)) + { + return false; + } + CloseHandle(process_info.hThread); + + // Wait for it to run + WaitForSingleObject(process_info.hProcess, INFINITE); + DWORD exit_code; + bool has_exit_code = GetExitCodeProcess(process_info.hProcess, &exit_code); + CloseHandle(process_info.hProcess); + // NOTE: Some nonzero exit codes can still be considered success (e.g. if installation was + // bypassed because the same version already installed). + return has_exit_code && exit_code == EXIT_SUCCESS; +} + +static BuildVersion CurrentOSVersion() +{ + typedef DWORD(WINAPI * RtlGetVersion_t)(PRTL_OSVERSIONINFOW); + auto RtlGetVersion = + (RtlGetVersion_t)GetProcAddress(GetModuleHandle(TEXT("ntdll")), "RtlGetVersion"); + RTL_OSVERSIONINFOW info{.dwOSVersionInfoSize = sizeof(info)}; + RtlGetVersion(&info); + return {.major = info.dwMajorVersion, .minor = info.dwMinorVersion, .build = info.dwBuildNumber}; +} + +static VersionCheckResult OSVersionCheck(const BuildInfo& build_info) +{ + VersionCheckResult result; + result.current_version = CurrentOSVersion(); + + constexpr BuildVersion WIN11_BASE{10, 0, 22000}; + const char* version_name = + (result.current_version >= WIN11_BASE) ? "OSMinimumVersionWin11" : "OSMinimumVersionWin10"; + result.target_version = build_info.GetVersion(version_name); + + if (!result.target_version.has_value() || result.current_version >= result.target_version) + result.status = VersionCheckStatus::NothingToDo; + else + result.status = VersionCheckStatus::UpdateRequired; + return result; +} + +bool VersionCheck(const BuildInfo& this_build_info, const BuildInfo& next_build_info) +{ + // If the binary requires more recent OS, inform the user. + auto os_check = OSVersionCheck(next_build_info); + if (os_check.status == VersionCheckStatus::UpdateRequired) + { + UI::Error("Please update Windows in order to update Dolphin."); + return false; + } + + // Check if application being launched needs more recent version of VC Redist. If so, download + // latest updater and execute it. + auto vc_check = VCRuntimeVersionCheck(this_build_info, next_build_info); + if (vc_check.status != VersionCheckStatus::NothingToDo) + { + // Don't bother checking status of the install itself, just check if we actually see the new + // version. + VCRuntimeUpdate(next_build_info); + vc_check = VCRuntimeVersionCheck(this_build_info, next_build_info); + if (vc_check.status == VersionCheckStatus::UpdateRequired) + { + // The update is required and the install failed for some reason. + UI::Error("Please update VC++ Runtime in order to update Dolphin."); + return false; + } + } + + return true; +} +} // namespace Platform diff --git a/Source/Core/WinUpdater/WinUI.cpp b/Source/Core/WinUpdater/WinUI.cpp index 62b02eb0c0..1671f43457 100644 --- a/Source/Core/WinUpdater/WinUI.cpp +++ b/Source/Core/WinUpdater/WinUI.cpp @@ -253,8 +253,8 @@ void Stop() void LaunchApplication(std::string path) { - // Hack: Launching the updater over the explorer ensures that admin priviliges are dropped. Why? - // Ask Microsoft. + // Indirectly start the application via explorer. This effectively drops admin priviliges because + // explorer is running as current user. ShellExecuteW(nullptr, nullptr, L"explorer.exe", UTF8ToWString(path).c_str(), nullptr, SW_SHOW); } diff --git a/Source/Core/WinUpdater/WinUpdater.vcxproj b/Source/Core/WinUpdater/WinUpdater.vcxproj index 9ec5d4afb2..28ae61bb34 100644 --- a/Source/Core/WinUpdater/WinUpdater.vcxproj +++ b/Source/Core/WinUpdater/WinUpdater.vcxproj @@ -24,9 +24,15 @@ {D79392F7-06D6-4B4B-A39F-4D587C215D3A} + + + + + +