From a2e6a48d2eee2d863c8fc2dfce60a574b14f4048 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sun, 9 Jun 2024 20:55:29 +1000 Subject: [PATCH] Qt: Add very-early VC++ runtime version check Yay for ABI breaks. --- src/duckstation-qt/CMakeLists.txt | 5 +- src/duckstation-qt/duckstation-qt.vcxproj | 1 + .../duckstation-qt.vcxproj.filters | 1 + src/duckstation-qt/vcruntimecheck.cpp | 108 ++++++++++++++++++ 4 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 src/duckstation-qt/vcruntimecheck.cpp diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt index 1c4306172..bb44f73ef 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -179,7 +179,10 @@ target_compile_definitions(duckstation-qt PRIVATE QT_NO_EXCEPTIONS) add_core_resources(duckstation-qt) if(WIN32) - target_sources(duckstation-qt PRIVATE duckstation-qt.rc) + target_sources(duckstation-qt PRIVATE + duckstation-qt.rc + vcruntimecheck.cpp + ) # We want a Windows subsystem application not console. set_target_properties(duckstation-qt PROPERTIES diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj index 07e34f8ee..1696c6390 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj +++ b/src/duckstation-qt/duckstation-qt.vcxproj @@ -51,6 +51,7 @@ + diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters index 25961d409..cfe5d458f 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj.filters +++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters @@ -178,6 +178,7 @@ moc + diff --git a/src/duckstation-qt/vcruntimecheck.cpp b/src/duckstation-qt/vcruntimecheck.cpp new file mode 100644 index 000000000..dc892c17b --- /dev/null +++ b/src/duckstation-qt/vcruntimecheck.cpp @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#include "common/windows_headers.h" +#include + +#include "fmt/format.h" + +// Minimum version is 14.38.33135.0. +static constexpr u32 MIN_VERSION_V0 = 14; +static constexpr u32 MIN_VERSION_V1 = 38; +static constexpr u32 MIN_VERSION_V2 = 33135; +static constexpr u32 MIN_VERSION_V3 = 0; +static constexpr const char* DOWNLOAD_URL = "https://aka.ms/vs/17/release/vc_redist.x64.exe"; + +struct VCRuntimeCheckObject +{ + VCRuntimeCheckObject() + { + const HMODULE crt_handle = GetModuleHandleW(L"msvcp140.dll"); + if (!crt_handle) + return; + + const HANDLE heap = GetProcessHeap(); + DWORD filename_length = MAX_PATH; + LPWSTR filename = static_cast(HeapAlloc(heap, 0, filename_length)); + if (!filename) + return; + + for (;;) + { + DWORD len = GetModuleFileNameW(crt_handle, filename, filename_length); + if (len == filename_length && GetLastError() == ERROR_INSUFFICIENT_BUFFER) + { + filename_length *= 2; + if (filename_length >= 4 * 1024) + return; + LPWSTR new_filename = static_cast(HeapReAlloc(heap, 0, filename, filename_length)); + if (!new_filename) + { + HeapFree(heap, 0, filename); + return; + } + filename = new_filename; + continue; + } + + break; + } + + const DWORD version_size = GetFileVersionInfoSizeExW(0, filename, nullptr); + LPVOID version_block; + if (version_size == 0 || !(version_block = HeapAlloc(heap, 0, version_size))) + { + HeapFree(heap, 0, filename); + return; + } + + VS_FIXEDFILEINFO* fi; + UINT fi_size; + if (!GetFileVersionInfoExW(0, filename, 0, version_size, version_block) || + !VerQueryValueW(version_block, L"\\", reinterpret_cast(&fi), &fi_size)) + { + HeapFree(heap, 0, version_block); + HeapFree(heap, 0, filename); + return; + } + + const DWORD v0 = (fi->dwFileVersionMS >> 16) & 0xFFFFu; + const DWORD v1 = fi->dwFileVersionMS & 0xFFFFu; + const DWORD v2 = (fi->dwFileVersionLS >> 16) & 0xFFFFu; + const DWORD v3 = fi->dwFileVersionLS & 0xFFFFu; + + HeapFree(heap, 0, version_block); + HeapFree(heap, 0, filename); + + if (v0 >= MIN_VERSION_V0 && v1 >= MIN_VERSION_V1 && v2 >= MIN_VERSION_V2) + return; + + // fmt is self-contained, hopefully it'll be okay. + char message[512]; + const auto fmt_result = + fmt::format_to_n(message, sizeof(message), + "Your Microsoft Visual C++ Runtime appears to be too old for this build of DuckStation.\n\n" + "Your version: {}.{}.{}.{}\n" + "Required version: {}.{}.{}.{}\n\n" + "You can download the latest version from {}.\n\n" + "Do you want to exit and download this version now?\n" + "If you select No, DuckStation will likely crash.", + v0, v1, v2, v3, MIN_VERSION_V0, MIN_VERSION_V1, MIN_VERSION_V2, MIN_VERSION_V3, DOWNLOAD_URL); + message[(fmt_result.size > (sizeof(message) - 1)) ? (sizeof(message) - 1) : fmt_result.size] = 0; + + if (MessageBoxA(NULL, message, "Old Visual C++ Runtime Detected", MB_ICONERROR | MB_YESNO) == IDNO) + return; + + if (!ShellExecuteA(NULL, "open", DOWNLOAD_URL, nullptr, nullptr, SW_SHOWNORMAL)) + MessageBoxA(NULL, "ShellExecuteA() failed, you may need to manually open the URL.", "Error", MB_OK); + + TerminateProcess(GetCurrentProcess(), 0xFFFFFFFF); + } +}; + +// We have to use a special object which gets initialized before all other global objects, because those might use the +// CRT and go kaboom. Yucky, but gets the job done. +#pragma optimize("", off) +#pragma warning(disable : 4075) // warning C4075: initializers put in unrecognized initialization area +#pragma init_seg(".CRT$XCT") +VCRuntimeCheckObject s_vcruntime_checker;